bitwrench 2.0.17 → 2.0.18

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 (67) hide show
  1. package/README.md +127 -38
  2. package/dist/bitwrench-bccl.cjs.js +8 -8
  3. package/dist/bitwrench-bccl.cjs.min.js +3 -3
  4. package/dist/bitwrench-bccl.esm.js +8 -8
  5. package/dist/bitwrench-bccl.esm.min.js +3 -3
  6. package/dist/bitwrench-bccl.umd.js +8 -8
  7. package/dist/bitwrench-bccl.umd.min.js +2 -2
  8. package/dist/bitwrench-code-edit.cjs.js +1 -1
  9. package/dist/bitwrench-code-edit.cjs.min.js +1 -1
  10. package/dist/bitwrench-code-edit.es5.js +1 -1
  11. package/dist/bitwrench-code-edit.es5.min.js +1 -1
  12. package/dist/bitwrench-code-edit.esm.js +1 -1
  13. package/dist/bitwrench-code-edit.esm.min.js +1 -1
  14. package/dist/bitwrench-code-edit.umd.js +1 -1
  15. package/dist/bitwrench-code-edit.umd.min.js +1 -1
  16. package/dist/bitwrench-lean.cjs.js +941 -775
  17. package/dist/bitwrench-lean.cjs.min.js +20 -20
  18. package/dist/bitwrench-lean.es5.js +1012 -961
  19. package/dist/bitwrench-lean.es5.min.js +18 -18
  20. package/dist/bitwrench-lean.esm.js +941 -775
  21. package/dist/bitwrench-lean.esm.min.js +20 -20
  22. package/dist/bitwrench-lean.umd.js +941 -775
  23. package/dist/bitwrench-lean.umd.min.js +20 -20
  24. package/dist/bitwrench-util-css.cjs.js +236 -0
  25. package/dist/bitwrench-util-css.cjs.min.js +22 -0
  26. package/dist/bitwrench-util-css.es5.js +414 -0
  27. package/dist/bitwrench-util-css.es5.min.js +21 -0
  28. package/dist/bitwrench-util-css.esm.js +230 -0
  29. package/dist/bitwrench-util-css.esm.min.js +21 -0
  30. package/dist/bitwrench-util-css.umd.js +242 -0
  31. package/dist/bitwrench-util-css.umd.min.js +21 -0
  32. package/dist/bitwrench.cjs.js +948 -782
  33. package/dist/bitwrench.cjs.min.js +21 -21
  34. package/dist/bitwrench.css +456 -132
  35. package/dist/bitwrench.es5.js +1024 -970
  36. package/dist/bitwrench.es5.min.js +19 -19
  37. package/dist/bitwrench.esm.js +949 -783
  38. package/dist/bitwrench.esm.min.js +21 -21
  39. package/dist/bitwrench.min.css +1 -1
  40. package/dist/bitwrench.umd.js +948 -782
  41. package/dist/bitwrench.umd.min.js +21 -21
  42. package/dist/builds.json +178 -90
  43. package/dist/bwserve.cjs.js +514 -68
  44. package/dist/bwserve.esm.js +513 -69
  45. package/dist/sri.json +44 -36
  46. package/package.json +3 -2
  47. package/readme.html +136 -49
  48. package/src/bitwrench-bccl.js +7 -7
  49. package/src/bitwrench-color-utils.js +31 -9
  50. package/src/bitwrench-esm-entry.js +11 -0
  51. package/src/bitwrench-styles.js +439 -232
  52. package/src/bitwrench-util-css.js +229 -0
  53. package/src/bitwrench.js +483 -485
  54. package/src/bwserve/attach.js +57 -0
  55. package/src/bwserve/bwclient.js +141 -0
  56. package/src/bwserve/bwshell.js +102 -0
  57. package/src/bwserve/client.js +151 -1
  58. package/src/bwserve/index.js +127 -28
  59. package/src/cli/attach.js +555 -0
  60. package/src/cli/convert.js +2 -5
  61. package/src/cli/index.js +7 -0
  62. package/src/cli/inject.js +1 -1
  63. package/src/cli/serve.js +6 -2
  64. package/src/generate-css.js +11 -4
  65. package/src/vendor/html2canvas.min.js +20 -0
  66. package/src/version.js +3 -3
  67. 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.18 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
2
2
  (function (global, factory) {
3
3
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
4
4
  typeof define === 'function' && define.amd ? define(factory) :
@@ -190,14 +190,14 @@
190
190
  */
191
191
 
192
192
  var VERSION_INFO = {
193
- version: '2.0.17',
193
+ version: '2.0.18',
194
194
  name: 'bitwrench',
195
195
  description: 'A library for javascript UI functions.',
196
196
  license: 'BSD-2-Clause',
197
197
  homepage: 'https://deftio.github.com/bitwrench/pages',
198
198
  repository: 'git+https://github.com/deftio/bitwrench.git',
199
199
  author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
200
- buildDate: '2026-03-13T23:15:10.823Z'
200
+ buildDate: '2026-03-17T00:50:09.505Z'
201
201
  };
202
202
 
203
203
  /**
@@ -486,13 +486,16 @@
486
486
  */
487
487
  function deriveShades(hex) {
488
488
  var rgb = colorParse(hex);
489
+ // For light input colors (L > 75), mixing toward white produces invisible borders.
490
+ // Darken instead so borders remain visible against light backgrounds.
491
+ var borderColor = hexToHsl(hex)[2] > 75 ? adjustLightness(hex, -18) : mixColor(hex, '#ffffff', 0.60);
489
492
  return {
490
493
  base: hex,
491
494
  hover: adjustLightness(hex, -10),
492
495
  active: adjustLightness(hex, -15),
493
496
  light: mixColor(hex, '#ffffff', 0.85),
494
497
  darkText: adjustLightness(hex, -40),
495
- border: mixColor(hex, '#ffffff', 0.60),
498
+ border: borderColor,
496
499
  focus: 'rgba(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ',0.25)',
497
500
  textOn: textOnColor(hex)
498
501
  };
@@ -551,18 +554,26 @@
551
554
  alt.secondary = deriveAlternateSeed(config.secondary);
552
555
  alt.tertiary = config.tertiary ? deriveAlternateSeed(config.tertiary) : alt.primary;
553
556
 
554
- // Derive alternate surface colors from primary hue
557
+ // Derive alternate surface colors from primary hue.
558
+ // Check actual page surface brightness (not seed color brightness) to decide
559
+ // whether alternate should be dark or light. The page surface is what the
560
+ // user sees; seeds can be dark while the page is still light (default L=96).
555
561
  var priHsl = hexToHsl(config.primary);
556
562
  var h = priHsl[0];
557
- var isLight = isLightPalette(config);
563
+ var primarySurface = config.surface || hslToHex([h, 8, 96]);
564
+ var isLight = relativeLuminance(primarySurface) > 0.179;
558
565
  if (isLight) {
559
- // Primary is light → alternate needs dark surfaces
566
+ // Page surface is light → alternate needs dark surfaces
560
567
  alt.light = hslToHex([h, Math.min(priHsl[1], 15), 15]);
561
568
  alt.dark = hslToHex([h, 5, 88]);
569
+ alt.surface = hslToHex([h, 12, 18]);
570
+ alt.background = hslToHex([h, 10, 14]);
562
571
  } else {
563
- // Primary is dark → alternate needs light surfaces
572
+ // Page surface is dark → alternate needs light surfaces
564
573
  alt.light = hslToHex([h, Math.min(priHsl[1], 10), 96]);
565
574
  alt.dark = hslToHex([h, 10, 18]);
575
+ alt.surface = hslToHex([h, 8, 96]);
576
+ alt.background = hslToHex([h, 6, 98]);
566
577
  }
567
578
 
568
579
  // Semantic colors: harmonize toward primary, then invert for alternate
@@ -611,10 +622,16 @@
611
622
  var darkBase = config.dark || hslToHex([h, 10, 13]);
612
623
 
613
624
  // Background & surface tokens — tinted with primary hue for theme personality.
614
- // Very subtle: bg at L=98/S=6, surface at L=96/S=8.
625
+ // Saturation high enough that the hue is visible (each theme feels distinct)
626
+ // but low enough to stay neutral and readable.
615
627
  // User can override with config.background / config.surface.
616
- var bgBase = config.background || hslToHex([h, 6, 98]);
617
- var surfBase = config.surface || hslToHex([h, 8, 96]);
628
+ var bgBase = config.background || hslToHex([h, 22, 96]);
629
+ var surfBase = config.surface || hslToHex([h, 25, 94]);
630
+
631
+ // surfaceAlt: subtle background variant for striped rows, hover states, headers.
632
+ // Slightly lighter than surface in dark mode, slightly darker in light mode.
633
+ var surfHsl = hexToHsl(surfBase);
634
+ var surfAlt = surfHsl[2] <= 50 ? hslToHex([surfHsl[0], surfHsl[1], Math.min(surfHsl[2] + 8, 100)]) : hslToHex([surfHsl[0], surfHsl[1], Math.max(surfHsl[2] - 3, 0)]);
618
635
  var palette = {
619
636
  primary: deriveShades(config.primary),
620
637
  secondary: deriveShades(config.secondary),
@@ -626,7 +643,8 @@
626
643
  light: deriveShades(lightBase),
627
644
  dark: deriveShades(darkBase),
628
645
  background: bgBase,
629
- surface: surfBase
646
+ surface: surfBase,
647
+ surfaceAlt: surfAlt
630
648
  };
631
649
  return palette;
632
650
  }
@@ -651,27 +669,28 @@
651
669
  5: '1.5rem',
652
670
  // 24px
653
671
  6: '2rem'};
672
+ var _S = SPACING_SCALE;
654
673
  var SPACING_PRESETS = {
655
674
  compact: {
656
- btn: SPACING_SCALE[1] + ' ' + SPACING_SCALE[3],
657
- card: SPACING_SCALE[3] + ' ' + SPACING_SCALE[4],
658
- alert: SPACING_SCALE[2] + ' ' + SPACING_SCALE[4],
659
- cell: SPACING_SCALE[2] + ' ' + SPACING_SCALE[3],
660
- input: SPACING_SCALE[1] + ' ' + SPACING_SCALE[3]
675
+ btn: _S[1] + ' ' + _S[3],
676
+ card: _S[3] + ' ' + _S[4],
677
+ alert: _S[2] + ' ' + _S[4],
678
+ cell: _S[2] + ' ' + _S[3],
679
+ input: _S[1] + ' ' + _S[3]
661
680
  },
662
681
  normal: {
663
- btn: SPACING_SCALE[2] + ' ' + SPACING_SCALE[4],
664
- card: SPACING_SCALE[5] + ' ' + SPACING_SCALE[5],
665
- alert: SPACING_SCALE[3] + ' ' + SPACING_SCALE[5],
666
- cell: SPACING_SCALE[3] + ' ' + SPACING_SCALE[4],
667
- input: SPACING_SCALE[2] + ' ' + SPACING_SCALE[3]
682
+ btn: _S[2] + ' ' + _S[4],
683
+ card: _S[5] + ' ' + _S[5],
684
+ alert: _S[3] + ' ' + _S[5],
685
+ cell: _S[3] + ' ' + _S[4],
686
+ input: _S[2] + ' ' + _S[3]
668
687
  },
669
688
  spacious: {
670
- btn: SPACING_SCALE[3] + ' ' + SPACING_SCALE[5],
671
- card: SPACING_SCALE[6] + ' ' + SPACING_SCALE[6],
672
- alert: SPACING_SCALE[4] + ' ' + SPACING_SCALE[5],
673
- cell: SPACING_SCALE[4] + ' ' + SPACING_SCALE[5],
674
- input: SPACING_SCALE[3] + ' ' + SPACING_SCALE[4]
689
+ btn: _S[3] + ' ' + _S[5],
690
+ card: _S[6] + ' ' + _S[6],
691
+ alert: _S[4] + ' ' + _S[5],
692
+ cell: _S[4] + ' ' + _S[5],
693
+ input: _S[3] + ' ' + _S[4]
675
694
  }
676
695
  };
677
696
  var RADIUS_PRESETS = {
@@ -813,68 +832,13 @@
813
832
  * Built-in theme presets — named color combinations
814
833
  * Each preset provides primary, secondary, and tertiary seed colors.
815
834
  */
816
- var THEME_PRESETS = {
817
- teal: {
818
- primary: '#006666',
819
- secondary: '#6c757d',
820
- tertiary: '#006666'
821
- },
822
- ocean: {
823
- primary: '#0077b6',
824
- secondary: '#90e0ef',
825
- tertiary: '#00b4d8'
826
- },
827
- sunset: {
828
- primary: '#e76f51',
829
- secondary: '#264653',
830
- tertiary: '#e9c46a'
831
- },
832
- forest: {
833
- primary: '#2d6a4f',
834
- secondary: '#95d5b2',
835
- tertiary: '#52b788'
836
- },
837
- slate: {
838
- primary: '#343a40',
839
- secondary: '#adb5bd',
840
- tertiary: '#6c757d'
841
- },
842
- rose: {
843
- primary: '#e11d48',
844
- secondary: '#fda4af',
845
- tertiary: '#fb7185'
846
- },
847
- indigo: {
848
- primary: '#4f46e5',
849
- secondary: '#a5b4fc',
850
- tertiary: '#818cf8'
851
- },
852
- amber: {
853
- primary: '#d97706',
854
- secondary: '#fbbf24',
855
- tertiary: '#f59e0b'
856
- },
857
- emerald: {
858
- primary: '#059669',
859
- secondary: '#6ee7b7',
860
- tertiary: '#34d399'
861
- },
862
- nord: {
863
- primary: '#5e81ac',
864
- secondary: '#88c0d0',
865
- tertiary: '#81a1c1'
866
- },
867
- coral: {
868
- primary: '#ef6461',
869
- secondary: '#4a7c7e',
870
- tertiary: '#e8a87c'
871
- },
872
- midnight: {
873
- primary: '#1e3a5f',
874
- secondary: '#7c8db5',
875
- tertiary: '#3d5a80'
876
- }
877
- };
835
+ var THEME_PRESETS = Object.fromEntries([['teal', '#006666', '#6c757d', '#006666'], ['ocean', '#0077b6', '#90e0ef', '#00b4d8'], ['sunset', '#e76f51', '#264653', '#e9c46a'], ['forest', '#2d6a4f', '#95d5b2', '#52b788'], ['slate', '#343a40', '#adb5bd', '#6c757d'], ['rose', '#e11d48', '#fda4af', '#fb7185'], ['indigo', '#4f46e5', '#a5b4fc', '#818cf8'], ['amber', '#d97706', '#fbbf24', '#f59e0b'], ['emerald', '#059669', '#6ee7b7', '#34d399'], ['nord', '#5e81ac', '#88c0d0', '#81a1c1'], ['coral', '#ef6461', '#4a7c7e', '#e8a87c'], ['midnight', '#1e3a5f', '#7c8db5', '#3d5a80']].map(function (e) {
836
+ return [e[0], {
837
+ primary: e[1],
838
+ secondary: e[2],
839
+ tertiary: e[3]
840
+ }];
841
+ }));
878
842
 
879
843
  /**
880
844
  * Resolve layout config to spacing, radius, typeScale, elevation, and motion objects.
@@ -922,6 +886,7 @@
922
886
  }).join(', ');
923
887
  return '.' + name + ' ' + sel;
924
888
  }
889
+ var _sx = scopeSelector;
925
890
 
926
891
  // =========================================================================
927
892
  // Themed CSS generators
@@ -930,12 +895,12 @@
930
895
  function generateTypographyThemed(scope, palette, layout) {
931
896
  var mot = layout.motion;
932
897
  var rules = {};
933
- rules[scopeSelector(scope, 'a')] = {
898
+ rules[_sx(scope, 'a')] = {
934
899
  'color': palette.primary.base,
935
900
  'text-decoration': 'none',
936
901
  'transition': 'color ' + mot.fast + ' ' + mot.easing
937
902
  };
938
- rules[scopeSelector(scope, 'a:hover')] = {
903
+ rules[_sx(scope, 'a:hover')] = {
939
904
  'color': palette.primary.hover,
940
905
  'text-decoration': 'underline'
941
906
  };
@@ -947,11 +912,11 @@
947
912
  var rd = layout.radius;
948
913
 
949
914
  // Base button (only when scoped — unscoped uses defaultStyles)
950
- rules[scopeSelector(scope, '.bw_btn')] = {
915
+ rules[_sx(scope, '.bw_btn')] = {
951
916
  'padding': sp.btn,
952
917
  'border-radius': rd.btn
953
918
  };
954
- rules[scopeSelector(scope, '.bw_btn:focus-visible')] = {
919
+ rules[_sx(scope, '.bw_btn:focus-visible')] = {
955
920
  'outline': '2px solid currentColor',
956
921
  'outline-offset': '2px',
957
922
  'box-shadow': '0 0 0 3px ' + palette.primary.focus
@@ -960,12 +925,12 @@
960
925
  // Variant colors handled by palette class on component root
961
926
 
962
927
  // Size variants (structural, reuse layout radius)
963
- rules[scopeSelector(scope, '.bw_btn_lg')] = {
928
+ rules[_sx(scope, '.bw_btn_lg')] = {
964
929
  'padding': '0.625rem 1.5rem',
965
930
  'font-size': '1rem',
966
931
  'border-radius': rd.btn === '50rem' ? '50rem' : parseInt(rd.btn) + 2 + 'px'
967
932
  };
968
- rules[scopeSelector(scope, '.bw_btn_sm')] = {
933
+ rules[_sx(scope, '.bw_btn_sm')] = {
969
934
  'padding': '0.25rem 0.75rem',
970
935
  'font-size': '0.8125rem',
971
936
  'border-radius': rd.btn === '50rem' ? '50rem' : Math.max(parseInt(rd.btn) - 1, 0) + 'px'
@@ -976,7 +941,7 @@
976
941
  var rules = {};
977
942
  var sp = layout.spacing;
978
943
  var rd = layout.radius;
979
- rules[scopeSelector(scope, '.bw_alert')] = {
944
+ rules[_sx(scope, '.bw_alert')] = {
980
945
  'padding': sp.alert,
981
946
  'border-radius': rd.alert
982
947
  };
@@ -994,38 +959,38 @@
994
959
  var rd = layout.radius;
995
960
  var elev = layout.elevation;
996
961
  var motion = layout.motion;
997
- rules[scopeSelector(scope, '.bw_card')] = {
962
+ rules[_sx(scope, '.bw_card')] = {
998
963
  'background-color': palette.surface || '#fff',
999
964
  'border': '1px solid ' + palette.light.border,
1000
965
  'border-radius': rd.card,
1001
966
  'box-shadow': elev.sm,
1002
967
  'transition': 'box-shadow ' + motion.normal + ' ' + motion.easing + ', transform ' + motion.normal + ' ' + motion.easing
1003
968
  };
1004
- rules[scopeSelector(scope, '.bw_card:hover')] = {
969
+ rules[_sx(scope, '.bw_card:hover')] = {
1005
970
  'box-shadow': elev.md
1006
971
  };
1007
- rules[scopeSelector(scope, '.bw_card_hoverable:hover')] = {
972
+ rules[_sx(scope, '.bw_card_hoverable:hover')] = {
1008
973
  'box-shadow': elev.lg
1009
974
  };
1010
- rules[scopeSelector(scope, '.bw_card_body')] = {
975
+ rules[_sx(scope, '.bw_card_body')] = {
1011
976
  'padding': sp.card
1012
977
  };
1013
- rules[scopeSelector(scope, '.bw_card_header')] = {
978
+ rules[_sx(scope, '.bw_card_header')] = {
1014
979
  'padding': sp.card.split(' ').map(function (v) {
1015
980
  return (parseFloat(v) * 0.7).toFixed(3).replace(/\.?0+$/, '') + 'rem';
1016
981
  }).join(' '),
1017
- 'background-color': palette.light.light,
982
+ 'background-color': palette.surfaceAlt,
1018
983
  'border-bottom': '1px solid ' + palette.light.border
1019
984
  };
1020
- rules[scopeSelector(scope, '.bw_card_footer')] = {
1021
- 'background-color': palette.light.light,
985
+ rules[_sx(scope, '.bw_card_footer')] = {
986
+ 'background-color': palette.surfaceAlt,
1022
987
  'border-top': '1px solid ' + palette.light.border,
1023
988
  'color': palette.secondary.base
1024
989
  };
1025
- rules[scopeSelector(scope, '.bw_card_title')] = {
990
+ rules[_sx(scope, '.bw_card_title')] = {
1026
991
  'color': palette.dark.base
1027
992
  };
1028
- rules[scopeSelector(scope, '.bw_card_subtitle')] = {
993
+ rules[_sx(scope, '.bw_card_subtitle')] = {
1029
994
  'color': palette.secondary.base
1030
995
  };
1031
996
 
@@ -1037,101 +1002,104 @@
1037
1002
  var rules = {};
1038
1003
  var sp = layout.spacing;
1039
1004
  var rd = layout.radius;
1040
- rules[scopeSelector(scope, '.bw_form_control')] = {
1005
+ rules[_sx(scope, '.bw_form_control')] = {
1041
1006
  'padding': sp.input,
1042
1007
  'border-radius': rd.input,
1043
1008
  'color': palette.dark.base,
1044
1009
  'background-color': palette.surface || '#fff',
1045
1010
  'border-color': palette.light.border
1046
1011
  };
1047
- rules[scopeSelector(scope, '.bw_form_control:focus')] = {
1012
+ rules[_sx(scope, '.bw_form_control:focus')] = {
1048
1013
  'border-color': palette.primary.border,
1049
1014
  'outline': '2px solid ' + palette.primary.base,
1050
1015
  'outline-offset': '-1px',
1051
1016
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
1052
1017
  };
1053
- rules[scopeSelector(scope, '.bw_form_control::placeholder')] = {
1018
+ rules[_sx(scope, '.bw_form_control::placeholder')] = {
1054
1019
  'color': palette.secondary.base
1055
1020
  };
1056
- rules[scopeSelector(scope, '.bw_form_label')] = {
1021
+ rules[_sx(scope, '.bw_form_label')] = {
1057
1022
  'color': palette.dark.base
1058
1023
  };
1059
- rules[scopeSelector(scope, '.bw_form_text')] = {
1024
+ rules[_sx(scope, '.bw_form_text')] = {
1060
1025
  'color': palette.secondary.base
1061
1026
  };
1062
- rules[scopeSelector(scope, '.bw_form_check_input:checked')] = {
1027
+ rules[_sx(scope, '.bw_form_check_input:checked')] = {
1063
1028
  'background-color': palette.primary.base,
1064
1029
  'border-color': palette.primary.base
1065
1030
  };
1066
- rules[scopeSelector(scope, '.bw_form_check_input:focus')] = {
1031
+ rules[_sx(scope, '.bw_form_check_input:focus')] = {
1067
1032
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
1068
1033
  };
1069
1034
  // Validation states
1070
- rules[scopeSelector(scope, '.bw_form_control.bw_is_valid')] = {
1035
+ rules[_sx(scope, '.bw_form_control.bw_is_valid')] = {
1071
1036
  'border-color': palette.success.base
1072
1037
  };
1073
- rules[scopeSelector(scope, '.bw_form_control.bw_is_valid:focus')] = {
1038
+ rules[_sx(scope, '.bw_form_control.bw_is_valid:focus')] = {
1074
1039
  'border-color': palette.success.base,
1075
1040
  'box-shadow': '0 0 0 0.2rem ' + palette.success.focus
1076
1041
  };
1077
- rules[scopeSelector(scope, '.bw_form_control.bw_is_invalid')] = {
1042
+ rules[_sx(scope, '.bw_form_control.bw_is_invalid')] = {
1078
1043
  'border-color': palette.danger.base
1079
1044
  };
1080
- rules[scopeSelector(scope, '.bw_form_control.bw_is_invalid:focus')] = {
1045
+ rules[_sx(scope, '.bw_form_control.bw_is_invalid:focus')] = {
1081
1046
  'border-color': palette.danger.base,
1082
1047
  'box-shadow': '0 0 0 0.2rem ' + palette.danger.focus
1083
1048
  };
1084
1049
  // Form select
1085
- rules[scopeSelector(scope, '.bw_form_select')] = {
1050
+ rules[_sx(scope, '.bw_form_select')] = {
1086
1051
  'padding': sp.input,
1087
1052
  'border-radius': rd.input,
1088
1053
  'color': palette.dark.base,
1089
1054
  'background-color': palette.surface || '#fff',
1090
1055
  'border-color': palette.light.border
1091
1056
  };
1092
- rules[scopeSelector(scope, '.bw_form_select:focus')] = {
1057
+ rules[_sx(scope, '.bw_form_select:focus')] = {
1093
1058
  'border-color': palette.primary.border,
1094
1059
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
1095
1060
  };
1096
1061
  return rules;
1097
1062
  }
1098
- function generateNavigation(scope, palette) {
1063
+ function generateNavigation(scope, palette, layout) {
1099
1064
  var rules = {};
1100
- rules[scopeSelector(scope, '.bw_navbar')] = {
1101
- 'background-color': palette.light.light,
1065
+ rules[_sx(scope, '.bw_navbar')] = {
1066
+ 'background-color': palette.surfaceAlt,
1102
1067
  'border-bottom-color': palette.light.border
1103
1068
  };
1104
- rules[scopeSelector(scope, '.bw_navbar_brand')] = {
1069
+ rules[_sx(scope, '.bw_navbar_brand')] = {
1105
1070
  'color': palette.dark.base
1106
1071
  };
1107
- rules[scopeSelector(scope, '.bw_navbar_nav .bw_nav_link')] = {
1108
- 'color': palette.secondary.base
1072
+ rules[_sx(scope, '.bw_navbar_nav .bw_nav_link')] = {
1073
+ 'color': palette.secondary.base,
1074
+ 'border-radius': layout.radius.btn
1109
1075
  };
1110
- rules[scopeSelector(scope, '.bw_navbar_nav .bw_nav_link:hover')] = {
1111
- 'color': palette.dark.base
1076
+ rules[_sx(scope, '.bw_navbar_nav .bw_nav_link:hover')] = {
1077
+ 'color': palette.dark.base,
1078
+ 'background-color': palette.surfaceAlt
1112
1079
  };
1113
- rules[scopeSelector(scope, '.bw_navbar_nav .bw_nav_link.active')] = {
1080
+ rules[_sx(scope, '.bw_navbar_nav .bw_nav_link.active')] = {
1114
1081
  'color': palette.primary.base,
1115
- 'background-color': palette.primary.focus
1082
+ 'background-color': palette.primary.focus,
1083
+ 'font-weight': '600'
1116
1084
  };
1117
- rules[scopeSelector(scope, '.bw_navbar_dark')] = {
1085
+ rules[_sx(scope, '.bw_navbar_dark')] = {
1118
1086
  'background-color': palette.dark.base,
1119
1087
  'border-bottom-color': palette.dark.hover
1120
1088
  };
1121
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_navbar_brand')] = {
1089
+ rules[_sx(scope, '.bw_navbar_dark .bw_navbar_brand')] = {
1122
1090
  'color': palette.light.base
1123
1091
  };
1124
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_nav_link')] = {
1125
- 'color': 'rgba(255,255,255,.65)'
1092
+ rules[_sx(scope, '.bw_navbar_dark .bw_nav_link')] = {
1093
+ 'color': palette.light.border
1126
1094
  };
1127
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_nav_link:hover')] = {
1128
- 'color': '#fff'
1095
+ rules[_sx(scope, '.bw_navbar_dark .bw_nav_link:hover')] = {
1096
+ 'color': palette.light.base
1129
1097
  };
1130
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_nav_link.active')] = {
1131
- 'color': '#fff',
1098
+ rules[_sx(scope, '.bw_navbar_dark .bw_nav_link.active')] = {
1099
+ 'color': palette.light.base,
1132
1100
  'font-weight': '600'
1133
1101
  };
1134
- rules[scopeSelector(scope, '.bw_nav_pills .bw_nav_link.active')] = {
1102
+ rules[_sx(scope, '.bw_nav_pills .bw_nav_link.active')] = {
1135
1103
  'color': palette.primary.textOn,
1136
1104
  'background-color': palette.primary.base
1137
1105
  };
@@ -1140,47 +1108,57 @@
1140
1108
  function generateTables(scope, palette, layout) {
1141
1109
  var rules = {};
1142
1110
  var sp = layout.spacing;
1143
- rules[scopeSelector(scope, '.bw_table')] = {
1111
+ rules[_sx(scope, '.bw_table')] = {
1144
1112
  'color': palette.dark.base,
1145
1113
  'border-color': palette.light.border
1146
1114
  };
1147
- rules[scopeSelector(scope, '.bw_table > :not(caption) > * > *')] = {
1115
+ rules[_sx(scope, '.bw_table > :not(caption) > * > *')] = {
1148
1116
  'padding': sp.cell,
1149
1117
  'border-bottom-color': palette.light.border
1150
1118
  };
1151
- rules[scopeSelector(scope, '.bw_table > thead > tr > *')] = {
1119
+ rules[_sx(scope, '.bw_table > thead > tr > *')] = {
1152
1120
  'color': palette.secondary.base,
1153
1121
  'border-bottom-color': palette.light.border,
1154
- 'background-color': palette.light.light
1122
+ 'background-color': palette.surfaceAlt
1155
1123
  };
1156
- rules[scopeSelector(scope, '.bw_table_striped > tbody > tr:nth-of-type(odd) > *')] = {
1157
- 'background-color': 'rgba(0, 0, 0, 0.05)'
1124
+ rules[_sx(scope, '.bw_table_striped > tbody > tr:nth-of-type(odd) > *')] = {
1125
+ 'background-color': palette.surfaceAlt
1158
1126
  };
1159
- rules[scopeSelector(scope, '.bw_table_hover > tbody > tr:hover > *')] = {
1127
+ rules[_sx(scope, '.bw_table_hover > tbody > tr:hover > *')] = {
1160
1128
  'background-color': palette.primary.focus
1161
1129
  };
1162
- rules[scopeSelector(scope, '.bw_table_bordered')] = {
1130
+ rules[_sx(scope, '.bw_table_selectable > tbody > tr')] = {
1131
+ 'cursor': 'pointer'
1132
+ };
1133
+ rules[_sx(scope, '.bw_table > tbody > tr.bw_table_row_selected > *')] = {
1134
+ 'background-color': palette.primary.light
1135
+ };
1136
+ rules[_sx(scope, '.bw_table_bordered')] = {
1163
1137
  'border-color': palette.light.border
1164
1138
  };
1165
- rules[scopeSelector(scope, '.bw_table caption')] = {
1139
+ rules[_sx(scope, '.bw_table caption')] = {
1166
1140
  'color': palette.secondary.base
1167
1141
  };
1168
1142
  return rules;
1169
1143
  }
1170
- function generateTabs(scope, palette) {
1171
- var rules = {};
1172
- rules[scopeSelector(scope, '.bw_nav_tabs')] = {
1144
+ function generateTabs(scope, palette, layout) {
1145
+ var rules = {},
1146
+ mo = layout.motion;
1147
+ rules[_sx(scope, '.bw_nav_tabs')] = {
1173
1148
  'border-bottom-color': palette.light.border
1174
1149
  };
1175
- rules[scopeSelector(scope, '.bw_nav_link')] = {
1176
- 'color': palette.secondary.base
1150
+ rules[_sx(scope, '.bw_nav_link')] = {
1151
+ 'color': palette.secondary.base,
1152
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing + ', border-color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
1177
1153
  };
1178
- rules[scopeSelector(scope, '.bw_nav_tabs .bw_nav_link:hover')] = {
1154
+ rules[_sx(scope, '.bw_nav_tabs .bw_nav_link:hover')] = {
1179
1155
  'color': palette.dark.base,
1156
+ 'background-color': palette.surfaceAlt,
1180
1157
  'border-bottom-color': palette.light.border
1181
1158
  };
1182
- rules[scopeSelector(scope, '.bw_nav_tabs .bw_nav_link.active')] = {
1159
+ rules[_sx(scope, '.bw_nav_tabs .bw_nav_link.active')] = {
1183
1160
  'color': palette.primary.base,
1161
+ 'background-color': palette.primary.focus,
1184
1162
  'border-bottom': '2px solid ' + palette.primary.base
1185
1163
  };
1186
1164
  return rules;
@@ -1188,49 +1166,62 @@
1188
1166
  function generateListGroups(scope, palette, layout) {
1189
1167
  var rules = {};
1190
1168
  var sp = layout.spacing;
1191
- rules[scopeSelector(scope, '.bw_list_group_item')] = {
1169
+ var mo = layout.motion;
1170
+ rules[_sx(scope, '.bw_list_group_item')] = {
1192
1171
  'padding': sp.cell,
1193
1172
  'color': palette.dark.base,
1194
1173
  'background-color': palette.surface || '#fff',
1195
- 'border-color': palette.light.border
1174
+ 'border-color': palette.light.border,
1175
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
1196
1176
  };
1197
- rules[scopeSelector(scope, 'a.bw_list_group_item:hover')] = {
1198
- 'background-color': palette.light.light,
1177
+ rules[_sx(scope, 'a.bw_list_group_item:hover')] = {
1178
+ 'background-color': palette.surfaceAlt,
1199
1179
  'color': palette.dark.hover
1200
1180
  };
1201
- rules[scopeSelector(scope, '.bw_list_group_item.active')] = {
1181
+ rules[_sx(scope, '.bw_list_group_item.active')] = {
1202
1182
  'color': palette.primary.textOn,
1203
1183
  'background-color': palette.primary.base,
1204
1184
  'border-color': palette.primary.base
1205
1185
  };
1206
- rules[scopeSelector(scope, '.bw_list_group_item.disabled')] = {
1186
+ rules[_sx(scope, '.bw_list_group_item.disabled')] = {
1207
1187
  'color': palette.secondary.base,
1208
1188
  'background-color': palette.surface || '#fff'
1209
1189
  };
1210
1190
  return rules;
1211
1191
  }
1212
- function generatePagination(scope, palette) {
1213
- var rules = {};
1214
- rules[scopeSelector(scope, '.bw_page_link')] = {
1192
+ function generatePagination(scope, palette, layout) {
1193
+ var rules = {},
1194
+ mo = layout.motion,
1195
+ rd = layout.radius;
1196
+ rules[_sx(scope, '.bw_page_item:first-child .bw_page_link')] = {
1197
+ 'border-top-left-radius': rd.btn,
1198
+ 'border-bottom-left-radius': rd.btn
1199
+ };
1200
+ rules[_sx(scope, '.bw_page_item:last-child .bw_page_link')] = {
1201
+ 'border-top-right-radius': rd.btn,
1202
+ 'border-bottom-right-radius': rd.btn
1203
+ };
1204
+ rules[_sx(scope, '.bw_page_link')] = {
1215
1205
  'color': palette.primary.base,
1216
1206
  'background-color': palette.surface || '#fff',
1217
- 'border-color': palette.light.border
1207
+ 'border-color': palette.light.border,
1208
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
1218
1209
  };
1219
- rules[scopeSelector(scope, '.bw_page_link:hover')] = {
1210
+ rules[_sx(scope, '.bw_page_link:hover')] = {
1220
1211
  'color': palette.primary.hover,
1221
- 'background-color': palette.light.light,
1212
+ 'background-color': palette.surfaceAlt,
1222
1213
  'border-color': palette.light.border
1223
1214
  };
1224
- rules[scopeSelector(scope, '.bw_page_link:focus')] = {
1215
+ rules[_sx(scope, '.bw_page_link:focus')] = {
1225
1216
  'outline': '2px solid ' + palette.primary.base,
1226
1217
  'outline-offset': '-2px'
1227
1218
  };
1228
- rules[scopeSelector(scope, '.bw_page_item.bw_active .bw_page_link')] = {
1219
+ rules[_sx(scope, '.bw_page_item.bw_active .bw_page_link')] = {
1229
1220
  'color': palette.primary.textOn,
1230
1221
  'background-color': palette.primary.base,
1231
1222
  'border-color': palette.primary.base
1232
1223
  };
1233
- rules[scopeSelector(scope, '.bw_page_item.bw_disabled .bw_page_link')] = {
1224
+ rules[_sx(scope, '.bw_page_item.bw_disabled .bw_page_link')] = {
1234
1225
  'color': palette.secondary.base,
1235
1226
  'background-color': palette.surface || '#fff',
1236
1227
  'border-color': palette.light.border
@@ -1239,12 +1230,12 @@
1239
1230
  }
1240
1231
  function generateProgress(scope, palette) {
1241
1232
  var rules = {};
1242
- rules[scopeSelector(scope, '.bw_progress')] = {
1243
- 'background-color': palette.light.light,
1233
+ rules[_sx(scope, '.bw_progress')] = {
1234
+ 'background-color': palette.surfaceAlt,
1244
1235
  'box-shadow': 'inset 0 1px 2px rgba(0,0,0,.1)'
1245
1236
  };
1246
- rules[scopeSelector(scope, '.bw_progress_bar')] = {
1247
- 'color': '#fff',
1237
+ rules[_sx(scope, '.bw_progress_bar')] = {
1238
+ 'color': palette.primary.textOn,
1248
1239
  'background-color': palette.primary.base,
1249
1240
  'box-shadow': 'inset 0 -1px 0 rgba(0,0,0,.15)'
1250
1241
  };
@@ -1263,25 +1254,31 @@
1263
1254
  'color': palette.dark.base,
1264
1255
  'background-color': bg
1265
1256
  };
1266
- rules[scopeSelector(scope, 'body')] = baseReset;
1267
- // Also apply to the scope element itself so themes work on any container, not just body
1268
- if (scope) {
1269
- rules['.' + scope] = baseReset;
1270
- }
1257
+ rules[_sx(scope, 'body')] = baseReset;
1271
1258
  return rules;
1272
1259
  }
1273
- function generateBreadcrumbThemed(scope, palette) {
1274
- var rules = {};
1275
- rules[scopeSelector(scope, '.bw_breadcrumb_item + .bw_breadcrumb_item::before')] = {
1260
+ function generateBreadcrumbThemed(scope, palette, layout) {
1261
+ var rules = {},
1262
+ mo = layout.motion;
1263
+ rules[_sx(scope, '.bw_breadcrumb')] = {
1264
+ 'background-color': palette.surfaceAlt,
1265
+ 'padding': '0.625rem 1rem',
1266
+ 'border-radius': layout.radius.btn
1267
+ };
1268
+ rules[_sx(scope, '.bw_breadcrumb_item + .bw_breadcrumb_item::before')] = {
1276
1269
  'color': palette.secondary.base
1277
1270
  };
1278
- rules[scopeSelector(scope, '.bw_breadcrumb_item.active')] = {
1279
- 'color': palette.secondary.base
1271
+ rules[_sx(scope, '.bw_breadcrumb_item a')] = {
1272
+ 'color': palette.primary.base,
1273
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing
1280
1274
  };
1281
- rules[scopeSelector(scope, '.bw_breadcrumb_item a:hover')] = {
1275
+ rules[_sx(scope, '.bw_breadcrumb_item a:hover')] = {
1282
1276
  'color': palette.primary.hover,
1283
1277
  'text-decoration': 'underline'
1284
1278
  };
1279
+ rules[_sx(scope, '.bw_breadcrumb_item.active')] = {
1280
+ 'color': palette.dark.base
1281
+ };
1285
1282
  return rules;
1286
1283
  }
1287
1284
 
@@ -1289,240 +1286,273 @@
1289
1286
 
1290
1287
  function generateCloseButtonThemed(scope, palette) {
1291
1288
  var rules = {};
1292
- rules[scopeSelector(scope, '.bw_close')] = {
1289
+ rules[_sx(scope, '.bw_close')] = {
1293
1290
  'color': palette.dark.base,
1294
1291
  'opacity': '0.5'
1295
1292
  };
1296
- rules[scopeSelector(scope, '.bw_close:focus')] = {
1293
+ rules[_sx(scope, '.bw_close:focus')] = {
1297
1294
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
1298
1295
  };
1299
1296
  return rules;
1300
1297
  }
1301
1298
  function generateSectionsThemed(scope, palette) {
1302
1299
  var rules = {};
1303
- rules[scopeSelector(scope, '.bw_section_subtitle')] = {
1300
+ rules[_sx(scope, '.bw_section_subtitle')] = {
1304
1301
  'color': palette.secondary.base
1305
1302
  };
1306
- rules[scopeSelector(scope, '.bw_feature_description')] = {
1303
+ rules[_sx(scope, '.bw_feature_description')] = {
1307
1304
  'color': palette.secondary.base
1308
1305
  };
1309
- rules[scopeSelector(scope, '.bw_cta_description')] = {
1306
+ rules[_sx(scope, '.bw_cta_description')] = {
1310
1307
  'color': palette.secondary.base
1311
1308
  };
1312
1309
  return rules;
1313
1310
  }
1314
- function generateAccordionThemed(scope, palette) {
1311
+ function generateAccordionThemed(scope, palette, layout) {
1315
1312
  var rules = {};
1316
- rules[scopeSelector(scope, '.bw_accordion_item')] = {
1313
+ var rd = layout ? layout.radius : {
1314
+ card: '8px'
1315
+ };
1316
+ rules[_sx(scope, '.bw_accordion_item')] = {
1317
1317
  'background-color': palette.surface || '#fff',
1318
1318
  'border-color': palette.light.border
1319
1319
  };
1320
- rules[scopeSelector(scope, '.bw_accordion_button')] = {
1320
+ rules[_sx(scope, '.bw_accordion_item:first-child')] = {
1321
+ 'border-top-left-radius': rd.card,
1322
+ 'border-top-right-radius': rd.card
1323
+ };
1324
+ rules[_sx(scope, '.bw_accordion_item:last-child')] = {
1325
+ 'border-bottom-left-radius': rd.card,
1326
+ 'border-bottom-right-radius': rd.card
1327
+ };
1328
+ rules[_sx(scope, '.bw_accordion_button')] = {
1321
1329
  'color': palette.dark.base
1322
1330
  };
1323
- rules[scopeSelector(scope, '.bw_accordion_button:not(.bw_collapsed)')] = {
1331
+ rules[_sx(scope, '.bw_accordion_button:not(.bw_collapsed)')] = {
1324
1332
  'color': palette.primary.darkText,
1325
- 'background-color': palette.primary.light
1333
+ 'background-color': palette.primary.light,
1334
+ 'border-left': '3px solid ' + palette.primary.base
1326
1335
  };
1327
- rules[scopeSelector(scope, '.bw_accordion_button:hover')] = {
1328
- 'background-color': palette.light.light
1336
+ rules[_sx(scope, '.bw_accordion_button:hover')] = {
1337
+ 'background-color': palette.surfaceAlt
1329
1338
  };
1330
- rules[scopeSelector(scope, '.bw_accordion_button:not(.bw_collapsed):hover')] = {
1331
- 'background-color': palette.primary.hover
1339
+ rules[_sx(scope, '.bw_accordion_button:not(.bw_collapsed):hover')] = {
1340
+ 'background-color': palette.primary.base,
1341
+ 'color': palette.primary.textOn
1332
1342
  };
1333
- rules[scopeSelector(scope, '.bw_accordion_button:focus-visible')] = {
1343
+ rules[_sx(scope, '.bw_accordion_button:focus-visible')] = {
1334
1344
  'box-shadow': '0 0 0 0.2rem ' + palette.primary.focus
1335
1345
  };
1336
- rules[scopeSelector(scope, '.bw_accordion_body')] = {
1337
- 'border-top': '1px solid ' + palette.light.border
1346
+ rules[_sx(scope, '.bw_accordion_body')] = {
1347
+ 'border-top': '1px solid ' + palette.light.border,
1348
+ 'background-color': palette.surfaceAlt
1338
1349
  };
1339
1350
  return rules;
1340
1351
  }
1341
1352
  function generateCarouselThemed(scope, palette) {
1342
1353
  var rules = {};
1343
- rules[scopeSelector(scope, '.bw_carousel')] = {
1344
- 'background-color': palette.light.light
1354
+ rules[_sx(scope, '.bw_carousel')] = {
1355
+ 'background-color': palette.surfaceAlt
1345
1356
  };
1346
- rules[scopeSelector(scope, '.bw_carousel_indicator.active')] = {
1357
+ rules[_sx(scope, '.bw_carousel_indicator.active')] = {
1347
1358
  'background-color': palette.primary.base
1348
1359
  };
1349
- rules[scopeSelector(scope, '.bw_carousel_control')] = {
1350
- 'background-color': 'rgba(0,0,0,0.4)',
1351
- 'color': '#fff'
1360
+ rules[_sx(scope, '.bw_carousel_control')] = {
1361
+ 'background-color': palette.dark.base,
1362
+ 'color': palette.dark.textOn
1352
1363
  };
1353
- rules[scopeSelector(scope, '.bw_carousel_control:hover')] = {
1354
- 'background-color': 'rgba(0,0,0,0.6)'
1364
+ rules[_sx(scope, '.bw_carousel_control:hover')] = {
1365
+ 'background-color': palette.dark.hover
1355
1366
  };
1356
- rules[scopeSelector(scope, '.bw_carousel_caption')] = {
1357
- 'background': 'linear-gradient(transparent, rgba(0,0,0,0.6))',
1358
- 'color': '#fff'
1367
+ rules[_sx(scope, '.bw_carousel_caption')] = {
1368
+ 'background': 'linear-gradient(transparent, ' + palette.dark.base + ')',
1369
+ 'color': palette.dark.textOn
1359
1370
  };
1360
1371
  return rules;
1361
1372
  }
1362
1373
  function generateModalThemed(scope, palette, layout) {
1363
1374
  var rules = {};
1364
- rules[scopeSelector(scope, '.bw_modal_content')] = {
1375
+ rules[_sx(scope, '.bw_modal_content')] = {
1365
1376
  'background-color': palette.surface || '#fff',
1366
1377
  'border-color': palette.light.border,
1367
1378
  'box-shadow': layout.elevation.lg
1368
1379
  };
1369
- rules[scopeSelector(scope, '.bw_modal_header')] = {
1380
+ rules[_sx(scope, '.bw_modal_header')] = {
1370
1381
  'border-bottom-color': palette.light.border
1371
1382
  };
1372
- rules[scopeSelector(scope, '.bw_modal_footer')] = {
1383
+ rules[_sx(scope, '.bw_modal_footer')] = {
1373
1384
  'border-top-color': palette.light.border
1374
1385
  };
1375
- rules[scopeSelector(scope, '.bw_modal_title')] = {
1386
+ rules[_sx(scope, '.bw_modal_title')] = {
1376
1387
  'color': palette.dark.base
1377
1388
  };
1378
1389
  return rules;
1379
1390
  }
1380
1391
  function generateToastThemed(scope, palette, layout) {
1381
1392
  var rules = {};
1382
- rules[scopeSelector(scope, '.bw_toast')] = {
1393
+ rules[_sx(scope, '.bw_toast')] = {
1383
1394
  'background-color': palette.surface || '#fff',
1384
- 'border-color': 'rgba(0,0,0,0.1)',
1395
+ 'border-color': palette.light.border,
1385
1396
  'box-shadow': layout.elevation.lg
1386
1397
  };
1387
- rules[scopeSelector(scope, '.bw_toast_header')] = {
1388
- 'border-bottom-color': 'rgba(0,0,0,0.05)'
1398
+ rules[_sx(scope, '.bw_toast_header')] = {
1399
+ 'border-bottom-color': palette.light.border
1389
1400
  };
1390
1401
  // Variant toast borders handled by palette class
1391
1402
  return rules;
1392
1403
  }
1393
1404
  function generateDropdownThemed(scope, palette, layout) {
1394
1405
  var rules = {};
1395
- rules[scopeSelector(scope, '.bw_dropdown_menu')] = {
1406
+ rules[_sx(scope, '.bw_dropdown_menu')] = {
1396
1407
  'background-color': palette.surface || '#fff',
1397
1408
  'border-color': palette.light.border,
1398
1409
  'box-shadow': layout.elevation.md
1399
1410
  };
1400
- rules[scopeSelector(scope, '.bw_dropdown_item')] = {
1401
- 'color': palette.dark.base
1411
+ rules[_sx(scope, '.bw_dropdown_item')] = {
1412
+ 'color': palette.dark.base,
1413
+ 'transition': 'background-color ' + layout.motion.fast + ' ' + layout.motion.easing
1402
1414
  };
1403
- rules[scopeSelector(scope, '.bw_dropdown_item:hover')] = {
1415
+ rules[_sx(scope, '.bw_dropdown_item:hover')] = {
1404
1416
  'color': palette.dark.hover,
1405
- 'background-color': palette.light.light
1417
+ 'background-color': palette.surfaceAlt
1406
1418
  };
1407
- rules[scopeSelector(scope, '.bw_dropdown_item.disabled')] = {
1419
+ rules[_sx(scope, '.bw_dropdown_item.disabled')] = {
1408
1420
  'color': palette.secondary.base
1409
1421
  };
1410
- rules[scopeSelector(scope, '.bw_dropdown_divider')] = {
1422
+ rules[_sx(scope, '.bw_dropdown_divider')] = {
1411
1423
  'border-top-color': palette.light.border
1412
1424
  };
1413
1425
  return rules;
1414
1426
  }
1415
1427
  function generateSwitchThemed(scope, palette) {
1416
1428
  var rules = {};
1417
- rules[scopeSelector(scope, '.bw_form_switch .bw_switch_input')] = {
1429
+ rules[_sx(scope, '.bw_form_switch .bw_switch_input')] = {
1418
1430
  'background-color': palette.secondary.base,
1419
1431
  'border-color': palette.secondary.base
1420
1432
  };
1421
- rules[scopeSelector(scope, '.bw_form_switch .bw_switch_input:checked')] = {
1433
+ rules[_sx(scope, '.bw_form_switch .bw_switch_input:checked')] = {
1422
1434
  'background-color': palette.primary.base,
1423
1435
  'border-color': palette.primary.base
1424
1436
  };
1425
- rules[scopeSelector(scope, '.bw_form_switch .bw_switch_input:focus')] = {
1437
+ rules[_sx(scope, '.bw_form_switch .bw_switch_input:focus')] = {
1426
1438
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
1427
1439
  };
1428
1440
  return rules;
1429
1441
  }
1430
1442
  function generateSkeletonThemed(scope, palette) {
1431
1443
  var rules = {};
1432
- rules[scopeSelector(scope, '.bw_skeleton')] = {
1433
- 'background': 'linear-gradient(90deg, ' + palette.light.border + ' 25%, ' + palette.light.light + ' 37%, ' + palette.light.border + ' 63%)'
1444
+ rules[_sx(scope, '.bw_skeleton')] = {
1445
+ 'background': 'linear-gradient(90deg, ' + palette.light.border + ' 25%, ' + palette.surfaceAlt + ' 37%, ' + palette.light.border + ' 63%)'
1434
1446
  };
1435
1447
  return rules;
1436
1448
  }
1437
1449
 
1438
1450
  // generateAvatarThemed: removed — palette class on root handles variants
1439
1451
 
1440
- function generateStatCardThemed(scope, palette) {
1441
- var rules = {};
1452
+ function generateStatCardThemed(scope, palette, layout) {
1453
+ var rules = {},
1454
+ mo = layout.motion,
1455
+ el = layout.elevation,
1456
+ rd = layout.radius;
1457
+ rules[_sx(scope, '.bw_stat_card')] = {
1458
+ 'background-color': palette.surface || '#fff',
1459
+ 'color': palette.dark.base,
1460
+ 'border': '1px solid ' + palette.light.border,
1461
+ 'border-radius': rd.card,
1462
+ 'box-shadow': el.sm,
1463
+ 'transition': 'box-shadow ' + mo.fast + ' ' + mo.easing + ', transform ' + mo.fast + ' ' + mo.easing
1464
+ };
1465
+ rules[_sx(scope, '.bw_stat_card:hover')] = {
1466
+ 'box-shadow': el.md
1467
+ };
1442
1468
  // Variant border colors handled by palette class
1443
- rules[scopeSelector(scope, '.bw_stat_change_up')] = {
1469
+ rules[_sx(scope, '.bw_stat_change_up')] = {
1444
1470
  'color': palette.success.base
1445
1471
  };
1446
- rules[scopeSelector(scope, '.bw_stat_change_down')] = {
1472
+ rules[_sx(scope, '.bw_stat_change_down')] = {
1447
1473
  'color': palette.danger.base
1448
1474
  };
1449
1475
  return rules;
1450
1476
  }
1451
1477
  function generateTimelineThemed(scope, palette) {
1452
1478
  var rules = {};
1453
- rules[scopeSelector(scope, '.bw_timeline::before')] = {
1479
+ rules[_sx(scope, '.bw_timeline::before')] = {
1454
1480
  'background-color': palette.light.border
1455
1481
  };
1456
1482
  // Variant marker colors handled by palette class
1457
- rules[scopeSelector(scope, '.bw_timeline_date')] = {
1483
+ rules[_sx(scope, '.bw_timeline_date')] = {
1458
1484
  'color': palette.secondary.base
1459
1485
  };
1460
1486
  return rules;
1461
1487
  }
1462
1488
  function generateStepperThemed(scope, palette) {
1463
1489
  var rules = {};
1464
- rules[scopeSelector(scope, '.bw_step_indicator')] = {
1465
- 'background-color': palette.light.light,
1490
+ rules[_sx(scope, '.bw_step_indicator')] = {
1491
+ 'background-color': palette.surfaceAlt,
1466
1492
  'border': '2px solid ' + palette.light.border,
1467
1493
  'color': palette.secondary.base
1468
1494
  };
1469
- rules[scopeSelector(scope, '.bw_step + .bw_step::before')] = {
1495
+ rules[_sx(scope, '.bw_step + .bw_step::before')] = {
1470
1496
  'background-color': palette.light.border
1471
1497
  };
1472
- rules[scopeSelector(scope, '.bw_step_active .bw_step_indicator')] = {
1498
+ rules[_sx(scope, '.bw_step_active .bw_step_indicator')] = {
1473
1499
  'background-color': palette.primary.base,
1474
1500
  'color': palette.primary.textOn
1475
1501
  };
1476
- rules[scopeSelector(scope, '.bw_step_active .bw_step_label')] = {
1502
+ rules[_sx(scope, '.bw_step_active .bw_step_label')] = {
1477
1503
  'color': palette.dark.base,
1478
1504
  'font-weight': '600'
1479
1505
  };
1480
- rules[scopeSelector(scope, '.bw_step_completed .bw_step_indicator')] = {
1506
+ rules[_sx(scope, '.bw_step_completed .bw_step_indicator')] = {
1481
1507
  'background-color': palette.primary.base,
1482
1508
  'color': palette.primary.textOn
1483
1509
  };
1484
- rules[scopeSelector(scope, '.bw_step_completed .bw_step_label')] = {
1510
+ rules[_sx(scope, '.bw_step_completed .bw_step_label')] = {
1485
1511
  'color': palette.primary.base
1486
1512
  };
1487
- rules[scopeSelector(scope, '.bw_step_completed + .bw_step::before')] = {
1513
+ rules[_sx(scope, '.bw_step_completed + .bw_step::before')] = {
1488
1514
  'background-color': palette.primary.base
1489
1515
  };
1490
1516
  return rules;
1491
1517
  }
1492
1518
  function generateChipInputThemed(scope, palette) {
1493
1519
  var rules = {};
1494
- rules[scopeSelector(scope, '.bw_chip_input')] = {
1495
- 'border-color': palette.light.border
1520
+ rules[_sx(scope, '.bw_chip_input')] = {
1521
+ 'border-color': palette.light.border,
1522
+ 'background-color': palette.surface || '#fff',
1523
+ 'color': palette.dark.base
1496
1524
  };
1497
- rules[scopeSelector(scope, '.bw_chip_input:focus-within')] = {
1525
+ rules[_sx(scope, '.bw_chip_input:focus-within')] = {
1498
1526
  'border-color': palette.primary.base,
1499
1527
  'box-shadow': '0 0 0 0.2rem ' + palette.primary.focus
1500
1528
  };
1501
- rules[scopeSelector(scope, '.bw_chip')] = {
1502
- 'background-color': palette.light.light,
1529
+ rules[_sx(scope, '.bw_chip')] = {
1530
+ 'background-color': palette.surfaceAlt,
1503
1531
  'color': palette.dark.base
1504
1532
  };
1505
- rules[scopeSelector(scope, '.bw_chip_remove:hover')] = {
1533
+ rules[_sx(scope, '.bw_chip_remove:hover')] = {
1506
1534
  'color': palette.danger.base,
1507
1535
  'background-color': palette.danger.light
1508
1536
  };
1509
1537
  return rules;
1510
1538
  }
1511
- function generateFileUploadThemed(scope, palette) {
1512
- var rules = {};
1513
- rules[scopeSelector(scope, '.bw_file_upload')] = {
1539
+ function generateFileUploadThemed(scope, palette, layout) {
1540
+ var rules = {},
1541
+ mo = layout.motion;
1542
+ rules[_sx(scope, '.bw_file_upload')] = {
1514
1543
  'border-color': palette.light.border,
1515
- 'background-color': palette.light.light
1544
+ 'background-color': palette.surfaceAlt,
1545
+ 'transition': 'border-color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
1516
1546
  };
1517
- rules[scopeSelector(scope, '.bw_file_upload:hover')] = {
1547
+ rules[_sx(scope, '.bw_file_upload:hover')] = {
1518
1548
  'border-color': palette.primary.base,
1519
1549
  'background-color': palette.primary.light
1520
1550
  };
1521
- rules[scopeSelector(scope, '.bw_file_upload:focus')] = {
1551
+ rules[_sx(scope, '.bw_file_upload:focus')] = {
1522
1552
  'outline': '2px solid ' + palette.primary.base,
1523
1553
  'outline-offset': '2px'
1524
1554
  };
1525
- rules[scopeSelector(scope, '.bw_file_upload.bw_file_upload_active')] = {
1555
+ rules[_sx(scope, '.bw_file_upload.bw_file_upload_active')] = {
1526
1556
  'border-color': palette.primary.base,
1527
1557
  'background-color': palette.primary.light,
1528
1558
  'border-style': 'solid'
@@ -1531,37 +1561,93 @@
1531
1561
  }
1532
1562
  function generateRangeThemed(scope, palette) {
1533
1563
  var rules = {};
1534
- rules[scopeSelector(scope, '.bw_range')] = {
1564
+ rules[_sx(scope, '.bw_range')] = {
1535
1565
  'background-color': palette.light.border
1536
1566
  };
1537
- rules[scopeSelector(scope, '.bw_range::-webkit-slider-thumb')] = {
1567
+ rules[_sx(scope, '.bw_range::-webkit-slider-thumb')] = {
1538
1568
  'background-color': palette.primary.base,
1539
- 'border-color': '#fff',
1569
+ 'border-color': palette.surface || '#fff',
1540
1570
  'box-shadow': '0 1px 3px rgba(0,0,0,0.2)',
1541
1571
  'transition': 'background-color 0.15s ease-out, transform 0.15s ease-out'
1542
1572
  };
1543
- rules[scopeSelector(scope, '.bw_range::-moz-range-thumb')] = {
1573
+ rules[_sx(scope, '.bw_range::-moz-range-thumb')] = {
1544
1574
  'background-color': palette.primary.base,
1545
- 'border-color': '#fff',
1575
+ 'border-color': palette.surface || '#fff',
1546
1576
  'box-shadow': '0 1px 3px rgba(0,0,0,0.2)'
1547
1577
  };
1548
1578
  return rules;
1549
1579
  }
1550
- function generateSearchThemed(scope, palette) {
1551
- var rules = {};
1552
- rules[scopeSelector(scope, '.bw_search_clear:hover')] = {
1580
+ function generateTooltipThemed(scope, palette, layout) {
1581
+ var rules = {},
1582
+ sp = layout.spacing,
1583
+ rd = layout.radius,
1584
+ el = layout.elevation,
1585
+ mo = layout.motion;
1586
+ rules[_sx(scope, '.bw_tooltip')] = {
1587
+ 'background-color': palette.dark.base,
1588
+ 'color': palette.dark.textOn,
1589
+ 'padding': sp.input,
1590
+ 'border-radius': rd.badge,
1591
+ 'box-shadow': el.md,
1592
+ 'transition': 'opacity ' + mo.fast + ' ' + mo.easing + ', transform ' + mo.fast + ' ' + mo.easing
1593
+ };
1594
+ return rules;
1595
+ }
1596
+ function generatePopoverThemed(scope, palette, layout) {
1597
+ var rules = {},
1598
+ sp = layout.spacing,
1599
+ rd = layout.radius,
1600
+ el = layout.elevation,
1601
+ mo = layout.motion;
1602
+ rules[_sx(scope, '.bw_popover')] = {
1603
+ 'background-color': palette.surface || '#fff',
1604
+ 'color': palette.dark.base,
1605
+ 'border': '1px solid ' + palette.light.border,
1606
+ 'border-radius': rd.card,
1607
+ 'box-shadow': el.lg,
1608
+ 'transition': 'opacity ' + mo.fast + ' ' + mo.easing + ', transform ' + mo.fast + ' ' + mo.easing
1609
+ };
1610
+ rules[_sx(scope, '.bw_popover_header')] = {
1611
+ 'background-color': palette.surfaceAlt,
1612
+ 'border-bottom': '1px solid ' + palette.light.border,
1613
+ 'padding': sp.input
1614
+ };
1615
+ rules[_sx(scope, '.bw_popover_body')] = {
1616
+ 'padding': sp.card
1617
+ };
1618
+ return rules;
1619
+ }
1620
+ function generateSearchThemed(scope, palette, layout) {
1621
+ var rules = {},
1622
+ mo = layout.motion;
1623
+ rules[_sx(scope, '.bw_search_input')] = {
1624
+ 'background-color': palette.surface || '#fff',
1625
+ 'color': palette.dark.base
1626
+ };
1627
+ rules[_sx(scope, '.bw_search_clear')] = {
1628
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
1629
+ };
1630
+ rules[_sx(scope, '.bw_search_clear:hover')] = {
1553
1631
  'color': palette.dark.base
1554
1632
  };
1555
1633
  return rules;
1556
1634
  }
1557
- function generateCodeDemoThemed(scope, palette) {
1635
+ function generateCodeDemoThemed(scope, palette, layout) {
1558
1636
  var rules = {};
1559
- rules[scopeSelector(scope, '.bw_code_copy_btn_copied')] = {
1637
+ var rd = layout ? layout.radius : {
1638
+ card: '0.375rem'
1639
+ };
1640
+ rules[_sx(scope, '.bw_code_demo')] = {
1641
+ 'background-color': palette.surface || '#fff',
1642
+ 'color': palette.dark.base,
1643
+ 'border-radius': rd.card
1644
+ };
1645
+ rules[_sx(scope, '.bw_code_copy_btn_copied')] = {
1560
1646
  'background': palette.success.base,
1561
1647
  'color': palette.success.textOn,
1562
1648
  'border-color': palette.success.base
1563
1649
  };
1564
- rules[scopeSelector(scope, '.bw_copy_btn:hover')] = {
1650
+ rules[_sx(scope, '.bw_copy_btn:hover')] = {
1565
1651
  'background': 'rgba(255,255,255,0.2)',
1566
1652
  'color': '#fff'
1567
1653
  };
@@ -1570,7 +1656,7 @@
1570
1656
  function generateNavPillsThemed(scope, palette, layout) {
1571
1657
  var rules = {};
1572
1658
  var rd = layout.radius;
1573
- rules[scopeSelector(scope, '.bw_nav_pills .bw_nav_link')] = {
1659
+ rules[_sx(scope, '.bw_nav_pills .bw_nav_link')] = {
1574
1660
  'border-radius': rd.btn
1575
1661
  };
1576
1662
  return rules;
@@ -1598,21 +1684,21 @@
1598
1684
  var s = palette[k];
1599
1685
 
1600
1686
  // --- Root palette class: sets default bg/color/border ---
1601
- rules[scopeSelector(scope, '.bw_' + k)] = {
1687
+ rules[_sx(scope, '.bw_' + k)] = {
1602
1688
  'background-color': s.base,
1603
1689
  'color': s.textOn,
1604
1690
  'border-color': s.base
1605
1691
  };
1606
1692
 
1607
1693
  // --- Pseudo-states (shared across all components) ---
1608
- rules[scopeSelector(scope, '.bw_' + k + ':hover')] = {
1694
+ rules[_sx(scope, '.bw_' + k + ':hover')] = {
1609
1695
  'background-color': s.hover,
1610
1696
  'border-color': s.active
1611
1697
  };
1612
- rules[scopeSelector(scope, '.bw_' + k + ':active')] = {
1698
+ rules[_sx(scope, '.bw_' + k + ':active')] = {
1613
1699
  'background-color': s.active
1614
1700
  };
1615
- rules[scopeSelector(scope, '.bw_' + k + ':focus-visible')] = {
1701
+ rules[_sx(scope, '.bw_' + k + ':focus-visible')] = {
1616
1702
  'box-shadow': '0 0 0 3px ' + s.focus,
1617
1703
  'outline': 'none'
1618
1704
  };
@@ -1620,71 +1706,110 @@
1620
1706
  // --- Component-specific overrides ---
1621
1707
 
1622
1708
  // Alerts: light bg, dark text, subtle border
1623
- rules[scopeSelector(scope, '.bw_alert.bw_' + k)] = {
1709
+ rules[_sx(scope, '.bw_alert.bw_' + k)] = {
1624
1710
  'background-color': s.light,
1625
1711
  'color': s.darkText,
1626
1712
  'border-color': s.border
1627
1713
  };
1628
1714
 
1629
1715
  // Toast: inherit bg, left border accent
1630
- rules[scopeSelector(scope, '.bw_toast.bw_' + k)] = {
1716
+ rules[_sx(scope, '.bw_toast.bw_' + k)] = {
1631
1717
  'background-color': 'inherit',
1632
1718
  'color': 'inherit',
1633
1719
  'border-left': '4px solid ' + s.base
1634
1720
  };
1635
1721
 
1636
1722
  // Stat card: inherit bg, left border accent
1637
- rules[scopeSelector(scope, '.bw_stat_card.bw_' + k)] = {
1723
+ rules[_sx(scope, '.bw_stat_card.bw_' + k)] = {
1638
1724
  'background-color': 'inherit',
1639
1725
  'color': 'inherit',
1640
1726
  'border-left-color': s.base
1641
1727
  };
1642
1728
 
1643
1729
  // Card accent: left border accent, inherit bg
1644
- rules[scopeSelector(scope, '.bw_card.bw_' + k)] = {
1730
+ rules[_sx(scope, '.bw_card.bw_' + k)] = {
1645
1731
  'background-color': 'inherit',
1646
1732
  'color': 'inherit',
1647
1733
  'border-left': '4px solid ' + s.base
1648
1734
  };
1649
1735
 
1650
1736
  // Timeline marker: colored dot
1651
- rules[scopeSelector(scope, '.bw_timeline_marker.bw_' + k)] = {
1737
+ rules[_sx(scope, '.bw_timeline_marker.bw_' + k)] = {
1652
1738
  'box-shadow': '0 0 0 2px ' + s.base
1653
1739
  };
1654
1740
 
1655
- // Spinner: text color only, transparent bg
1656
- rules[scopeSelector(scope, '.bw_spinner_border.bw_' + k + ',\n' + scopeSelector(scope, '.bw_spinner_grow.bw_' + k))] = {
1741
+ // Spinner: set color, re-apply border pattern so the root palette class
1742
+ // border-color doesn't fill in the transparent gap that makes it spin.
1743
+ // Also neutralize hover/active which would override border-right-color.
1744
+ rules[_sx(scope, '.bw_spinner_border.bw_' + k)] = {
1657
1745
  'background-color': 'transparent',
1658
1746
  'color': s.base,
1659
- 'border-color': 'currentColor'
1747
+ 'border-color': s.base,
1748
+ 'border-right-color': 'transparent'
1749
+ };
1750
+ rules[_sx(scope, '.bw_spinner_border.bw_' + k + ':hover')] = {
1751
+ 'background-color': 'transparent',
1752
+ 'border-color': s.base,
1753
+ 'border-right-color': 'transparent'
1754
+ };
1755
+ rules[_sx(scope, '.bw_spinner_grow.bw_' + k)] = {
1756
+ 'background-color': s.base,
1757
+ 'color': s.base
1660
1758
  };
1661
1759
 
1662
1760
  // Outline button: transparent bg, colored border+text, solid on hover
1663
- rules[scopeSelector(scope, '.bw_btn_outline.bw_' + k)] = {
1761
+ rules[_sx(scope, '.bw_btn_outline.bw_' + k)] = {
1664
1762
  'background-color': 'transparent',
1665
1763
  'color': s.base,
1666
1764
  'border-color': s.base
1667
1765
  };
1668
- rules[scopeSelector(scope, '.bw_btn_outline.bw_' + k + ':hover')] = {
1766
+ rules[_sx(scope, '.bw_btn_outline.bw_' + k + ':hover')] = {
1669
1767
  'background-color': s.base,
1670
1768
  'color': s.textOn
1671
1769
  };
1672
1770
 
1673
1771
  // Hero: gradient background
1674
- rules[scopeSelector(scope, '.bw_hero.bw_' + k)] = {
1772
+ rules[_sx(scope, '.bw_hero.bw_' + k)] = {
1675
1773
  'background': 'linear-gradient(135deg, ' + s.base + ' 0%, ' + s.hover + ' 100%)',
1676
1774
  'color': s.textOn
1677
1775
  };
1678
1776
 
1679
- // Progress bar: white text on colored bg (default is fine, just ensure text)
1680
- rules[scopeSelector(scope, '.bw_progress_bar.bw_' + k)] = {
1681
- 'color': '#fff'
1777
+ // Progress bar: contrasting text on colored bg
1778
+ rules[_sx(scope, '.bw_progress_bar.bw_' + k)] = {
1779
+ 'color': s.textOn
1780
+ };
1781
+
1782
+ // Background utility: .bw_bg_primary, .bw_bg_secondary, etc.
1783
+ rules[_sx(scope, '.bw_bg_' + k)] = {
1784
+ 'background-color': s.base,
1785
+ 'color': s.textOn
1786
+ };
1787
+
1788
+ // Text color utility: .bw_text_primary, .bw_text_secondary, etc.
1789
+ rules[_sx(scope, '.bw_text_' + k)] = {
1790
+ 'color': s.base
1682
1791
  };
1683
1792
  });
1684
1793
 
1685
- // Text muted
1686
- rules[scopeSelector(scope, '.bw_text_muted')] = {
1687
- 'color': palette.secondary.base
1794
+ // Text muted — always a neutral gray, never a brand color
1795
+ rules[_sx(scope, '.bw_text_muted')] = {
1796
+ 'color': '#6c757d'
1797
+ };
1798
+
1799
+ // Common bg/text utilities that aren't per-variant
1800
+ rules[_sx(scope, '.bw_bg_dark')] = {
1801
+ 'background-color': '#212529',
1802
+ 'color': '#f8f9fa'
1803
+ };
1804
+ rules[_sx(scope, '.bw_bg_light')] = {
1805
+ 'background-color': '#f8f9fa',
1806
+ 'color': '#212529'
1807
+ };
1808
+ rules[_sx(scope, '.bw_text_light')] = {
1809
+ 'color': '#f8f9fa'
1810
+ };
1811
+ rules[_sx(scope, '.bw_text_dark')] = {
1812
+ 'color': '#212529'
1688
1813
  };
1689
1814
  return rules;
1690
1815
  }
@@ -1699,7 +1824,7 @@
1699
1824
  * @returns {Object} CSS rules object
1700
1825
  */
1701
1826
  function generateThemedCSS(scopeName, palette, layout) {
1702
- return Object.assign({}, generateResetThemed(scopeName, palette), generateTypographyThemed(scopeName, palette, layout), generateButtons(scopeName, palette, layout), generateAlerts(scopeName, palette, layout), generateCards(scopeName, palette, layout), generateForms(scopeName, palette, layout), generateNavigation(scopeName, palette), generateTables(scopeName, palette, layout), generateTabs(scopeName, palette), generateListGroups(scopeName, palette, layout), generatePagination(scopeName, palette), generateProgress(scopeName, palette), generateBreadcrumbThemed(scopeName, palette), generateCloseButtonThemed(scopeName, palette), generateSectionsThemed(scopeName, palette), generateAccordionThemed(scopeName, palette), generateCarouselThemed(scopeName, palette), generateModalThemed(scopeName, palette, layout), generateToastThemed(scopeName, palette, layout), generateDropdownThemed(scopeName, palette, layout), generateSwitchThemed(scopeName, palette), generateSkeletonThemed(scopeName, palette), generateStatCardThemed(scopeName, palette), generateTimelineThemed(scopeName, palette), generateStepperThemed(scopeName, palette), generateChipInputThemed(scopeName, palette), generateFileUploadThemed(scopeName, palette), generateRangeThemed(scopeName, palette), generateSearchThemed(scopeName, palette), generateCodeDemoThemed(scopeName, palette), generateNavPillsThemed(scopeName, palette, layout), generatePaletteClasses(scopeName, palette));
1827
+ return Object.assign({}, generateResetThemed(scopeName, palette), generateTypographyThemed(scopeName, palette, layout), generateButtons(scopeName, palette, layout), generateAlerts(scopeName, palette, layout), generateCards(scopeName, palette, layout), generateForms(scopeName, palette, layout), generateNavigation(scopeName, palette, layout), generateTables(scopeName, palette, layout), generateTabs(scopeName, palette, layout), generateListGroups(scopeName, palette, layout), generatePagination(scopeName, palette, layout), generateProgress(scopeName, palette), generateBreadcrumbThemed(scopeName, palette, layout), generateCloseButtonThemed(scopeName, palette), generateSectionsThemed(scopeName, palette), generateAccordionThemed(scopeName, palette, layout), generateCarouselThemed(scopeName, palette), generateModalThemed(scopeName, palette, layout), generateToastThemed(scopeName, palette, layout), generateDropdownThemed(scopeName, palette, layout), generateSwitchThemed(scopeName, palette), generateSkeletonThemed(scopeName, palette), generateStatCardThemed(scopeName, palette, layout), generateTimelineThemed(scopeName, palette), generateStepperThemed(scopeName, palette), generateChipInputThemed(scopeName, palette), generateFileUploadThemed(scopeName, palette, layout), generateRangeThemed(scopeName, palette), generateSearchThemed(scopeName, palette, layout), generateTooltipThemed(scopeName, palette, layout), generatePopoverThemed(scopeName, palette, layout), generateCodeDemoThemed(scopeName, palette, layout), generateNavPillsThemed(scopeName, palette, layout), generatePaletteClasses(scopeName, palette));
1703
1828
  }
1704
1829
 
1705
1830
  // =========================================================================
@@ -2194,6 +2319,12 @@
2194
2319
  'border-width': '1px',
2195
2320
  'border-style': 'solid'
2196
2321
  },
2322
+ '.bw_table_selectable > tbody > tr': {
2323
+ 'cursor': 'pointer'
2324
+ },
2325
+ '.bw_table > tbody > tr.bw_table_row_selected > *': {
2326
+ 'background-color': 'rgba(0, 102, 102, 0.1)'
2327
+ },
2197
2328
  '.bw_table_responsive': {
2198
2329
  'overflow-x': 'auto',
2199
2330
  '-webkit-overflow-scrolling': 'touch'
@@ -2307,6 +2438,7 @@
2307
2438
  'display': 'block',
2308
2439
  'font-size': '0.875rem',
2309
2440
  'font-weight': '500',
2441
+ 'padding': '0.625rem 1rem',
2310
2442
  'text-decoration': 'none',
2311
2443
  'cursor': 'pointer',
2312
2444
  'border': 'none',
@@ -2409,16 +2541,15 @@
2409
2541
  'padding': '0.375rem 0.75rem',
2410
2542
  'margin-left': '-1px',
2411
2543
  'line-height': '1.25',
2412
- 'text-decoration': 'none'
2544
+ 'text-decoration': 'none',
2545
+ 'border': '1px solid transparent',
2546
+ 'cursor': 'pointer',
2547
+ 'font-family': 'inherit',
2548
+ 'font-size': 'inherit',
2549
+ 'background': 'none'
2413
2550
  },
2414
2551
  '.bw_page_item:first-child .bw_page_link': {
2415
- 'margin-left': '0',
2416
- 'border-top-left-radius': '0.375rem',
2417
- 'border-bottom-left-radius': '0.375rem'
2418
- },
2419
- '.bw_page_item:last-child .bw_page_link': {
2420
- 'border-top-right-radius': '0.375rem',
2421
- 'border-bottom-right-radius': '0.375rem'
2552
+ 'margin-left': '0'
2422
2553
  },
2423
2554
  '.bw_page_link:focus-visible': {
2424
2555
  'z-index': '3',
@@ -2789,6 +2920,7 @@
2789
2920
  'display': 'flex',
2790
2921
  'align-items': 'center',
2791
2922
  'width': '100%',
2923
+ 'padding': '0.875rem 1.25rem',
2792
2924
  'font-size': '1rem',
2793
2925
  'font-weight': '500',
2794
2926
  'text-align': 'left',
@@ -2811,20 +2943,16 @@
2811
2943
  '.bw_accordion_button:not(.bw_collapsed)::after': {
2812
2944
  'transform': 'rotate(-180deg)'
2813
2945
  },
2946
+ '.bw_accordion_body': {
2947
+ 'padding': '1rem 1.25rem'
2948
+ },
2814
2949
  '.bw_accordion_collapse': {
2815
2950
  'max-height': '0',
2816
- 'overflow': 'hidden'
2951
+ 'overflow': 'hidden',
2952
+ 'transition': 'max-height 0.3s ease'
2817
2953
  },
2818
2954
  '.bw_accordion_collapse.bw_collapse_show': {
2819
2955
  'max-height': 'none'
2820
- },
2821
- '.bw_accordion_item:first-child': {
2822
- 'border-top-left-radius': '8px',
2823
- 'border-top-right-radius': '8px'
2824
- },
2825
- '.bw_accordion_item:last-child': {
2826
- 'border-bottom-left-radius': '8px',
2827
- 'border-bottom-right-radius': '8px'
2828
2956
  }
2829
2957
  },
2830
2958
  // ---- Carousel ----
@@ -3192,7 +3320,11 @@
3192
3320
  // ---- Stat card ----
3193
3321
  statCard: {
3194
3322
  '.bw_stat_card': {
3195
- 'border-left': '4px solid transparent'
3323
+ 'padding': '1.25rem',
3324
+ 'border-left': '4px solid transparent',
3325
+ 'border-radius': '0.375rem',
3326
+ 'background-color': 'inherit',
3327
+ 'transition': 'transform 0.15s ease'
3196
3328
  },
3197
3329
  '.bw_stat_card:hover': {
3198
3330
  'transform': 'translateY(-1px)'
@@ -4210,6 +4342,52 @@
4210
4342
  'margin-right': '.5rem'
4211
4343
  };
4212
4344
 
4345
+ // Typography — bw_ prefixed utilities via loops
4346
+ var _imp = function _imp(p, v) {
4347
+ var o = {};
4348
+ o[p] = v + ' !important';
4349
+ return o;
4350
+ };
4351
+ [['fs', {
4352
+ 'xs': '0.75rem',
4353
+ 'sm': '0.875rem',
4354
+ 'base': '1rem',
4355
+ 'lg': '1.125rem',
4356
+ 'xl': '1.25rem',
4357
+ '2xl': '1.5rem'
4358
+ }, 'font-size'], ['fw', {
4359
+ light: '300',
4360
+ normal: '400',
4361
+ medium: '500',
4362
+ semibold: '600',
4363
+ bold: '700'
4364
+ }, 'font-weight'], ['lh', {
4365
+ tight: '1.25',
4366
+ normal: '1.5',
4367
+ relaxed: '1.75'
4368
+ }, 'line-height']].forEach(function (d) {
4369
+ for (var dk in d[1]) rules['.bw_' + d[0] + '_' + dk] = _imp(d[2], d[1][dk]);
4370
+ });
4371
+
4372
+ // Flex utilities
4373
+ rules['.bw_flex'] = {
4374
+ 'display': 'flex'
4375
+ };
4376
+ rules['.bw_flex_column'] = {
4377
+ 'flex-direction': 'column'
4378
+ };
4379
+ rules['.bw_flex_wrap'] = {
4380
+ 'flex-wrap': 'wrap'
4381
+ };
4382
+ rules['.bw_flex_center'] = {
4383
+ 'display': 'flex',
4384
+ 'align-items': 'center',
4385
+ 'justify-content': 'center'
4386
+ };
4387
+ for (var gk in spacingValues) rules['.bw_gap_' + gk] = {
4388
+ 'gap': spacingValues[gk] + ' !important'
4389
+ };
4390
+
4213
4391
  // Visibility
4214
4392
  rules['.bw_visible, .visible'] = {
4215
4393
  'visibility': 'visible !important'
@@ -4288,6 +4466,26 @@
4288
4466
  return getStructuralCSS();
4289
4467
  }
4290
4468
 
4469
+ /**
4470
+ * Get CSS reset rules only (box-sizing, html/body font, reduced-motion).
4471
+ * Separate from themed/structural rules for independent injection.
4472
+ * @returns {Object} CSS rules object for the reset layer
4473
+ */
4474
+ function getResetStyles() {
4475
+ var rules = {};
4476
+ Object.assign(rules, structuralRules.base);
4477
+ // Include reduced-motion preference
4478
+ rules['@media (prefers-reduced-motion: reduce)'] = {
4479
+ '*, *::before, *::after': {
4480
+ 'animation-duration': '0.01ms !important',
4481
+ 'animation-iteration-count': '1 !important',
4482
+ 'transition-duration': '0.01ms !important',
4483
+ 'scroll-behavior': 'auto !important'
4484
+ }
4485
+ };
4486
+ return rules;
4487
+ }
4488
+
4291
4489
  // =========================================================================
4292
4490
  // defaultStyles — backward-compatible categorized view
4293
4491
  // =========================================================================
@@ -4322,57 +4520,40 @@
4322
4520
  });
4323
4521
 
4324
4522
  /**
4325
- * Generate alternate-palette CSS scoped under `.bw_theme_alt`.
4326
- * Uses the same `generateThemedCSS()` pipeline as the primary palette —
4327
- * both sides go through identical code paths.
4328
- *
4329
- * @param {string} name - Theme scope name (e.g. 'ocean'). '' for global.
4330
- * @param {Object} altPalette - From derivePalette(deriveAlternateConfig(...))
4331
- * @param {Object} layout - From resolveLayout()
4332
- * @returns {Object} CSS rules object scoped under .bw_theme_alt (+ optional .name)
4523
+ * Prefix every selector in a rules object with a scope selector.
4524
+ * Handles @media/@keyframes blocks and comma-separated selectors.
4525
+ * @param {Object} rules - CSS rules object
4526
+ * @param {string} prefix - Scope prefix (e.g. '#my-dashboard', '.bw_theme_alt')
4527
+ * @param {boolean} [compound=false] - If true, use compound selector (no space)
4528
+ * for the first segment: `#scope.bw_theme_alt .sel` vs `#scope .sel`
4529
+ * @returns {Object} New rules object with scoped selectors
4333
4530
  */
4334
- function generateAlternateCSS(name, altPalette, layout) {
4335
- // Generate themed CSS using the same pipeline as primary
4336
- var rawRules = generateThemedCSS('', altPalette, layout);
4337
-
4338
- // Re-scope every selector under .bw_theme_alt (+ optional theme name)
4339
- var altPrefix = name ? '.' + name + '.bw_theme_alt' : '.bw_theme_alt';
4340
- var altRules = {};
4341
- for (var sel in rawRules) {
4342
- if (!rawRules.hasOwnProperty(sel)) continue;
4531
+ function scopeRulesUnder(rules, prefix, compound) {
4532
+ var scoped = {};
4533
+ for (var sel in rules) {
4534
+ if (!rules.hasOwnProperty(sel)) continue;
4343
4535
  if (sel.charAt(0) === '@') {
4344
4536
  // @media / @keyframes — recurse into the block
4345
- var innerBlock = rawRules[sel];
4346
- var altInner = {};
4537
+ var innerBlock = rules[sel];
4538
+ var scopedInner = {};
4347
4539
  for (var innerSel in innerBlock) {
4348
4540
  if (!innerBlock.hasOwnProperty(innerSel)) continue;
4349
- altInner[altPrefix + ' ' + innerSel] = innerBlock[innerSel];
4541
+ scopedInner[_prefixSelector(innerSel, prefix)] = innerBlock[innerSel];
4350
4542
  }
4351
- altRules[sel] = altInner;
4543
+ scoped[sel] = scopedInner;
4352
4544
  } else {
4353
- // Regular selector — prefix with alt scope
4354
- // Handle comma-separated selectors
4355
- var parts = sel.split(',');
4356
- var scopedParts = [];
4357
- for (var i = 0; i < parts.length; i++) {
4358
- var s = parts[i].trim();
4359
- // 'body' selector gets special treatment: .bw_theme_alt body
4360
- if (s === 'body' || s.indexOf('body') === 0) {
4361
- scopedParts.push(altPrefix + ' ' + s);
4362
- } else {
4363
- scopedParts.push(altPrefix + ' ' + s);
4364
- }
4365
- }
4366
- altRules[scopedParts.join(', ')] = rawRules[sel];
4545
+ scoped[_prefixSelector(sel, prefix)] = rules[sel];
4367
4546
  }
4368
4547
  }
4369
-
4370
- // Add body-level overrides for the alternate surface
4371
- altRules[altPrefix + ' body, :root' + altPrefix + ' body'] = {
4372
- 'color': altPalette.dark.base,
4373
- 'background-color': altPalette.light.base
4374
- };
4375
- return altRules;
4548
+ return scoped;
4549
+ }
4550
+ function _prefixSelector(sel, prefix) {
4551
+ var parts = sel.split(',');
4552
+ var result = [];
4553
+ for (var i = 0; i < parts.length; i++) {
4554
+ result.push(prefix + ' ' + parts[i].trim());
4555
+ }
4556
+ return result.join(', ');
4376
4557
  }
4377
4558
 
4378
4559
  /**
@@ -5421,7 +5602,12 @@
5421
5602
  el = document.querySelector('[data-bw_id="' + id + '"]');
5422
5603
  }
5423
5604
 
5424
- // 5. Cache the result for next time
5605
+ // 5. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
5606
+ if (!el && id.indexOf('bw_uuid_') === 0) {
5607
+ el = document.querySelector('.' + id);
5608
+ }
5609
+
5610
+ // 6. Cache the result for next time
5425
5611
  if (el) {
5426
5612
  bw._nodeMap[id] = el;
5427
5613
  }
@@ -5473,6 +5659,79 @@
5473
5659
  }
5474
5660
  };
5475
5661
 
5662
+ // ===================================================================================
5663
+ // bw.assignUUID() / bw.getUUID() — Explicit UUID addressing for TACO objects
5664
+ // ===================================================================================
5665
+
5666
+ /**
5667
+ * Regex to match a bw_uuid_* token in a class string.
5668
+ * @private
5669
+ */
5670
+ var _UUID_RE = /\bbw_uuid_[a-z0-9_]+\b/;
5671
+
5672
+ /**
5673
+ * Assign a UUID to a TACO object by appending a `bw_uuid_*` token to `taco.a.class`.
5674
+ *
5675
+ * Idempotent by default — calling twice returns the same UUID. Pass `forceNew=true`
5676
+ * to replace an existing UUID (useful in loops where each TACO needs a unique ID).
5677
+ *
5678
+ * @param {Object} taco - A TACO object `{t, a, c, o}`
5679
+ * @param {boolean} [forceNew=false] - If true, replaces any existing UUID with a new one
5680
+ * @returns {string} The UUID string (e.g. 'bw_uuid_a1b2c3d4e5')
5681
+ * @category Identifiers
5682
+ * @example
5683
+ * var card = bw.makeStatCard({ value: '0', label: 'Scans' });
5684
+ * var uuid = bw.assignUUID(card); // 'bw_uuid_a1b2c3d4e5'
5685
+ * var same = bw.assignUUID(card); // same UUID (idempotent)
5686
+ * var diff = bw.assignUUID(card, true); // new UUID (forced)
5687
+ */
5688
+ bw.assignUUID = function (taco, forceNew) {
5689
+ if (!taco || !_is(taco, 'object')) return null;
5690
+
5691
+ // Ensure taco.a exists
5692
+ if (!taco.a) taco.a = {};
5693
+ if (!_is(taco.a["class"], 'string')) taco.a["class"] = taco.a["class"] ? String(taco.a["class"]) : '';
5694
+ var existing = taco.a["class"].match(_UUID_RE);
5695
+ if (existing && !forceNew) {
5696
+ return existing[0];
5697
+ }
5698
+
5699
+ // Remove old UUID if forceNew
5700
+ if (existing) {
5701
+ taco.a["class"] = taco.a["class"].replace(_UUID_RE, '').replace(/\s+/g, ' ').trim();
5702
+ }
5703
+ var uuid = bw.uuid('uuid');
5704
+ taco.a["class"] = (taco.a["class"] ? taco.a["class"] + ' ' : '') + uuid;
5705
+ return uuid;
5706
+ };
5707
+
5708
+ /**
5709
+ * Read the UUID from a TACO object or DOM element. Pure getter, no side effects.
5710
+ *
5711
+ * @param {Object|Element} tacoOrElement - A TACO object or DOM element
5712
+ * @returns {string|null} The UUID string, or null if none assigned
5713
+ * @category Identifiers
5714
+ * @example
5715
+ * bw.getUUID(card) // 'bw_uuid_a1b2c3d4e5' (from TACO)
5716
+ * bw.getUUID(domEl) // 'bw_uuid_a1b2c3d4e5' (from DOM element)
5717
+ * bw.getUUID({t:'div'}) // null (no UUID)
5718
+ */
5719
+ bw.getUUID = function (tacoOrElement) {
5720
+ if (!tacoOrElement) return null;
5721
+ var classStr;
5722
+ // DOM element: check className
5723
+ if (tacoOrElement.className !== undefined && tacoOrElement.tagName) {
5724
+ classStr = tacoOrElement.className;
5725
+ }
5726
+ // TACO object: check a.class
5727
+ else if (tacoOrElement.a && _is(tacoOrElement.a["class"], 'string')) {
5728
+ classStr = tacoOrElement.a["class"];
5729
+ }
5730
+ if (!classStr) return null;
5731
+ var match = classStr.match(_UUID_RE);
5732
+ return match ? match[0] : null;
5733
+ };
5734
+
5476
5735
  /**
5477
5736
  * Escape HTML special characters to prevent XSS.
5478
5737
  *
@@ -5525,6 +5784,45 @@
5525
5784
  };
5526
5785
  };
5527
5786
 
5787
+ /**
5788
+ * Hyperscript-style TACO constructor.
5789
+ *
5790
+ * A convenience helper that returns a canonical TACO object from positional
5791
+ * arguments. The return value is a plain object — serializable, works with
5792
+ * bwserve, and accepted everywhere TACO is accepted.
5793
+ *
5794
+ * @param {string} tag - HTML tag name (e.g. 'div', 'p', 'section')
5795
+ * @param {Object|null} [attrs] - HTML attributes object. Pass null or omit to skip.
5796
+ * @param {*} [content] - Content: string, number, TACO object, or array of children.
5797
+ * @param {Object} [options] - TACO options (state, lifecycle hooks, render fn).
5798
+ * @returns {Object} Plain TACO object {t, a?, c?, o?}
5799
+ * @category Utilities
5800
+ * @see bw.html
5801
+ * @see bw.createDOM
5802
+ * @see bw.DOM
5803
+ * @example
5804
+ * bw.h('div')
5805
+ * // => { t: 'div' }
5806
+ *
5807
+ * bw.h('p', { class: 'bw_text_muted' }, 'Hello')
5808
+ * // => { t: 'p', a: { class: 'bw_text_muted' }, c: 'Hello' }
5809
+ *
5810
+ * bw.h('ul', null, [
5811
+ * bw.h('li', null, 'one'),
5812
+ * bw.h('li', null, 'two')
5813
+ * ])
5814
+ * // => { t: 'ul', c: [{ t: 'li', c: 'one' }, { t: 'li', c: 'two' }] }
5815
+ */
5816
+ bw.h = function (tag, attrs, content, options) {
5817
+ var taco = {
5818
+ t: String(tag)
5819
+ };
5820
+ if (attrs !== null && attrs !== undefined) taco.a = attrs;
5821
+ if (content !== undefined) taco.c = content;
5822
+ if (options !== undefined) taco.o = options;
5823
+ return taco;
5824
+ };
5825
+
5528
5826
  /**
5529
5827
  * Convert a TACO object (or array of TACOs) to an HTML string.
5530
5828
  *
@@ -5799,9 +6097,7 @@
5799
6097
  if (theme) {
5800
6098
  var themeConfig = _is(theme, 'string') ? THEME_PRESETS[theme.toLowerCase()] || null : theme;
5801
6099
  if (themeConfig) {
5802
- var themeResult = bw.generateTheme('', Object.assign({}, themeConfig, {
5803
- inject: false
5804
- }));
6100
+ var themeResult = bw.makeStyles(themeConfig);
5805
6101
  themeCSS = themeResult.css;
5806
6102
  }
5807
6103
  }
@@ -5835,14 +6131,14 @@
5835
6131
  // Combine all CSS
5836
6132
  var allCSS = (themeCSS ? themeCSS + '\n' : '') + css;
5837
6133
 
5838
- // Body-end script: registry entries + optional loadDefaultStyles
6134
+ // Body-end script: registry entries + optional loadStyles
5839
6135
  var bodyEndScript = '';
5840
6136
  var bodyEndParts = [];
5841
6137
  if (registryEntries) {
5842
6138
  bodyEndParts.push(registryEntries);
5843
6139
  }
5844
6140
  if (runtime === 'inline' || runtime === 'cdn') {
5845
- bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadDefaultStyles();}');
6141
+ bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadStyles();}');
5846
6142
  }
5847
6143
  if (bodyEndParts.length > 0) {
5848
6144
  bodyEndScript = '<script>\n' + bodyEndParts.join('\n') + '\n</script>';
@@ -6016,6 +6312,14 @@
6016
6312
  bw._registerNode(el, null);
6017
6313
  }
6018
6314
 
6315
+ // Register UUID class in node cache (bw_uuid_* tokens in class string)
6316
+ if (el.className) {
6317
+ var uuidMatch = el.className.match(_UUID_RE);
6318
+ if (uuidMatch) {
6319
+ bw._nodeMap[uuidMatch[0]] = el;
6320
+ }
6321
+ }
6322
+
6019
6323
  // Handle lifecycle hooks and state
6020
6324
  if (opts.mounted || opts.unmount || opts.render || opts.state) {
6021
6325
  var id = attrs['data-bw_id'] || bw.uuid();
@@ -6375,6 +6679,16 @@
6375
6679
  bw.cleanup = function (element) {
6376
6680
  if (!bw._isBrowser || !element) return;
6377
6681
 
6682
+ // Deregister UUID classes from node cache (element + descendants)
6683
+ // Covers elements that have UUID but no data-bw_id
6684
+ var selfUuidMatch = element.className && element.className.match(_UUID_RE);
6685
+ if (selfUuidMatch) delete bw._nodeMap[selfUuidMatch[0]];
6686
+ var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
6687
+ uuidEls.forEach(function (uel) {
6688
+ var m = uel.className && uel.className.match(_UUID_RE);
6689
+ if (m) delete bw._nodeMap[m[0]];
6690
+ });
6691
+
6378
6692
  // Find all elements with data-bw_id
6379
6693
  var elements = element.querySelectorAll('[data-bw_id]');
6380
6694
  elements.forEach(function (el) {
@@ -6388,6 +6702,10 @@
6388
6702
  // Deregister from node cache
6389
6703
  bw._deregisterNode(el, id);
6390
6704
 
6705
+ // Deregister UUID class from node cache
6706
+ var uuidMatch = el.className && el.className.match(_UUID_RE);
6707
+ if (uuidMatch) delete bw._nodeMap[uuidMatch[0]];
6708
+
6391
6709
  // Clean up pub/sub subscriptions tied to this element
6392
6710
  if (el._bw_subs) {
6393
6711
  el._bw_subs.forEach(function (unsub) {
@@ -6414,6 +6732,10 @@
6414
6732
  // Deregister from node cache
6415
6733
  bw._deregisterNode(element, id);
6416
6734
 
6735
+ // Deregister UUID class from node cache
6736
+ var elemUuidMatch = element.className && element.className.match(_UUID_RE);
6737
+ if (elemUuidMatch) delete bw._nodeMap[elemUuidMatch[0]];
6738
+
6417
6739
  // Clean up pub/sub subscriptions tied to element itself
6418
6740
  if (element._bw_subs) {
6419
6741
  element._bw_subs.forEach(function (unsub) {
@@ -7030,7 +7352,7 @@
7030
7352
  willMount: o.willMount || null,
7031
7353
  mounted: o.mounted || null,
7032
7354
  willUpdate: o.willUpdate || null,
7033
- onUpdate: o.onUpdate || null,
7355
+ onUpdate: o.onUpdate || o.updated || null,
7034
7356
  unmount: o.unmount || null,
7035
7357
  willDestroy: o.willDestroy || null
7036
7358
  };
@@ -8015,7 +8337,7 @@
8015
8337
  * and calls the named method. This is the bitwrench equivalent of
8016
8338
  * Win32 SendMessage(hwnd, msg, wParam, lParam).
8017
8339
  *
8018
- * @param {string} target - Component UUID (data-bw_comp_id) or user tag (CSS class)
8340
+ * @param {string} target - Component UUID (bw_uuid_*), comp ID (data-bw_comp_id), or user tag (CSS class)
8019
8341
  * @param {string} action - Method name to call on the component
8020
8342
  * @param {*} data - Data to pass to the method
8021
8343
  * @returns {boolean} True if message was dispatched successfully
@@ -8032,9 +8354,14 @@
8032
8354
  * };
8033
8355
  */
8034
8356
  bw.message = function (target, action, data) {
8035
- // Try data-bw_comp_id attribute first, then CSS class (user tag)
8036
- var el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
8037
- if (!el) {
8357
+ // Try bw._el() first (handles UUID class, nodeMap cache, getElementById)
8358
+ var el = bw._el(target);
8359
+ // Then try data-bw_comp_id attribute
8360
+ if (!el || !el._bwComponentHandle) {
8361
+ el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
8362
+ }
8363
+ // Then try CSS class (user tag)
8364
+ if (!el || !el._bwComponentHandle) {
8038
8365
  el = bw.$('.' + target)[0];
8039
8366
  }
8040
8367
  if (!el || !el._bwComponentHandle) return false;
@@ -8048,61 +8375,24 @@
8048
8375
  };
8049
8376
 
8050
8377
  // ===================================================================================
8051
- // bw.clientApply() / bw.clientConnect() — Server-driven UI protocol
8378
+ // bw.apply() / bw.parseJSONFlex() — Server-driven UI protocol
8052
8379
  // ===================================================================================
8053
8380
 
8054
8381
  /**
8055
8382
  * Registry of named functions sent via register messages.
8056
- * Populated by clientApply({ type: 'register', name, body }).
8057
- * Invoked by clientApply({ type: 'call', name, args }).
8383
+ * Populated by bw.apply({ type: 'register', name, body }).
8384
+ * Invoked by bw.apply({ type: 'call', name, args }).
8058
8385
  * @private
8059
8386
  */
8060
8387
  bw._clientFunctions = {};
8061
8388
 
8062
8389
  /**
8063
- * Whether exec messages are allowed. Set by clientConnect opts.allowExec.
8390
+ * Whether exec messages are allowed. Set by bwclient connect opts.allowExec.
8064
8391
  * Default false — exec messages are rejected unless explicitly opted in.
8065
8392
  * @private
8066
8393
  */
8067
8394
  bw._allowExec = false;
8068
8395
 
8069
- /**
8070
- * Built-in client functions available via call() without registration.
8071
- * @private
8072
- */
8073
- bw._builtinClientFunctions = {
8074
- scrollTo: function scrollTo(selector) {
8075
- var el = bw._el(selector);
8076
- if (el) el.scrollTop = el.scrollHeight;
8077
- },
8078
- focus: function focus(selector) {
8079
- var el = bw._el(selector);
8080
- if (el && _is(el.focus, 'function')) el.focus();
8081
- },
8082
- download: function download(filename, content, mimeType) {
8083
- if (typeof document === 'undefined') return;
8084
- var blob = new Blob([content], {
8085
- type: mimeType || 'text/plain'
8086
- });
8087
- var a = document.createElement('a');
8088
- a.href = URL.createObjectURL(blob);
8089
- a.download = filename;
8090
- a.click();
8091
- URL.revokeObjectURL(a.href);
8092
- },
8093
- clipboard: function clipboard(text) {
8094
- if (typeof navigator !== 'undefined' && navigator.clipboard) {
8095
- navigator.clipboard.writeText(text);
8096
- }
8097
- },
8098
- redirect: function redirect(url) {
8099
- if (typeof window !== 'undefined') window.location.href = url;
8100
- },
8101
- log: function log() {
8102
- console.log.apply(console, arguments);
8103
- }
8104
- };
8105
-
8106
8396
  /**
8107
8397
  * Parse a bwserve protocol message string, supporting both strict JSON
8108
8398
  * and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
@@ -8117,9 +8407,9 @@
8117
8407
  * @param {string} str - JSON or r-prefixed relaxed JSON string
8118
8408
  * @returns {Object} Parsed message object
8119
8409
  * @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
8120
- * @category Server
8410
+ * @category Core
8121
8411
  */
8122
- bw.clientParse = function (str) {
8412
+ bw.parseJSONFlex = function (str) {
8123
8413
  str = (str || '').trim();
8124
8414
  if (str.charAt(0) !== 'r') return JSON.parse(str);
8125
8415
  str = str.slice(1);
@@ -8197,10 +8487,10 @@
8197
8487
  * append — target.appendChild(bw.createDOM(node))
8198
8488
  * remove — bw.cleanup(target); target.remove()
8199
8489
  * patch — bw.patch(target, content, attr)
8200
- * batch — iterate ops, call clientApply for each
8490
+ * batch — iterate ops, call bw.apply for each
8201
8491
  * message — bw.message(target, action, data)
8202
8492
  * register — store a named function for later call()
8203
- * call — invoke a registered or built-in function
8493
+ * call — invoke a registered function
8204
8494
  * exec — execute arbitrary JS (requires allowExec)
8205
8495
  *
8206
8496
  * Target resolution:
@@ -8209,9 +8499,9 @@
8209
8499
  *
8210
8500
  * @param {Object} msg - Protocol message
8211
8501
  * @returns {boolean} true if the message was applied successfully
8212
- * @category Server
8502
+ * @category Core
8213
8503
  */
8214
- bw.clientApply = function (msg) {
8504
+ bw.apply = function (msg) {
8215
8505
  if (!msg || !msg.type) return false;
8216
8506
  var type = msg.type;
8217
8507
  var target = msg.target;
@@ -8239,7 +8529,7 @@
8239
8529
  if (!_isA(msg.ops)) return false;
8240
8530
  var allOk = true;
8241
8531
  msg.ops.forEach(function (op) {
8242
- if (!bw.clientApply(op)) allOk = false;
8532
+ if (!bw.apply(op)) allOk = false;
8243
8533
  });
8244
8534
  return allOk;
8245
8535
  } else if (type === 'message') {
@@ -8255,7 +8545,7 @@
8255
8545
  }
8256
8546
  } else if (type === 'call') {
8257
8547
  if (!msg.name) return false;
8258
- var fn = bw._clientFunctions[msg.name] || bw._builtinClientFunctions[msg.name];
8548
+ var fn = bw._clientFunctions[msg.name];
8259
8549
  if (!_is(fn, 'function')) return false;
8260
8550
  try {
8261
8551
  var args = _isA(msg.args) ? msg.args : [];
@@ -8282,141 +8572,6 @@
8282
8572
  return false;
8283
8573
  };
8284
8574
 
8285
- /**
8286
- * Connect to a bwserve SSE endpoint and apply protocol messages automatically.
8287
- *
8288
- * Returns a connection object with sendAction(), on(), and close() methods.
8289
- *
8290
- * @param {string} url - SSE endpoint URL (e.g., '/__bw/events/client-1')
8291
- * @param {Object} [opts] - Connection options
8292
- * @param {string} [opts.transport='sse'] - Transport type: 'sse' (default) or 'poll'
8293
- * @param {number} [opts.interval=2000] - Poll interval in ms (only for 'poll' transport)
8294
- * @param {string} [opts.actionUrl] - POST endpoint for actions (default: derived from url)
8295
- * @param {boolean} [opts.reconnect=true] - Auto-reconnect on disconnect
8296
- * @param {boolean} [opts.allowExec=false] - Enable exec message type (arbitrary JS execution)
8297
- * @param {Function} [opts.onStatus] - Status callback: 'connecting'|'connected'|'disconnected'
8298
- * @param {Function} [opts.onMessage] - Raw message callback (before clientApply)
8299
- * @returns {Object} Connection object { sendAction, on, close, status }
8300
- * @category Server
8301
- */
8302
- bw.clientConnect = function (url, opts) {
8303
- opts = opts || {};
8304
- var transport = opts.transport || 'sse';
8305
- var actionUrl = opts.actionUrl || url.replace(/\/events\//, '/action/');
8306
- var reconnect = opts.reconnect !== false;
8307
- var onStatus = opts.onStatus || function () {};
8308
- var onMessage = opts.onMessage || null;
8309
- var handlers = {};
8310
- // Set the global allowExec flag from connection options
8311
- bw._allowExec = !!opts.allowExec;
8312
- var conn = {
8313
- status: 'connecting',
8314
- _es: null,
8315
- _pollTimer: null
8316
- };
8317
- function setStatus(s) {
8318
- conn.status = s;
8319
- onStatus(s);
8320
- }
8321
- function handleMessage(data) {
8322
- try {
8323
- var msg = _is(data, 'string') ? bw.clientParse(data) : data;
8324
- if (onMessage) onMessage(msg);
8325
- if (handlers.message) handlers.message(msg);
8326
- bw.clientApply(msg);
8327
- } catch (e) {
8328
- if (handlers.error) handlers.error(e);
8329
- }
8330
- }
8331
- if (transport === 'sse' && typeof EventSource !== 'undefined') {
8332
- setStatus('connecting');
8333
- var es = new EventSource(url);
8334
- conn._es = es;
8335
- es.onopen = function () {
8336
- setStatus('connected');
8337
- if (handlers.open) handlers.open();
8338
- };
8339
- es.onmessage = function (e) {
8340
- handleMessage(e.data);
8341
- };
8342
- es.onerror = function () {
8343
- if (conn.status === 'connected') {
8344
- setStatus('disconnected');
8345
- }
8346
- if (handlers.error) handlers.error(new Error('SSE connection error'));
8347
- if (!reconnect) {
8348
- es.close();
8349
- }
8350
- // EventSource auto-reconnects by default when reconnect=true
8351
- };
8352
- } else if (transport === 'poll') {
8353
- var interval = opts.interval || 2000;
8354
- setStatus('connected');
8355
- conn._pollTimer = setInterval(function () {
8356
- fetch(url).then(function (r) {
8357
- return r.json();
8358
- }).then(function (msgs) {
8359
- if (_isA(msgs)) {
8360
- msgs.forEach(handleMessage);
8361
- } else if (msgs && msgs.type) {
8362
- handleMessage(msgs);
8363
- }
8364
- })["catch"](function (e) {
8365
- if (handlers.error) handlers.error(e);
8366
- });
8367
- }, interval);
8368
- }
8369
-
8370
- /**
8371
- * Send an action to the server via POST.
8372
- * @param {string} action - Action name
8373
- * @param {Object} [data] - Action payload
8374
- */
8375
- conn.sendAction = function (action, data) {
8376
- var body = JSON.stringify({
8377
- type: 'action',
8378
- action: action,
8379
- data: data || {}
8380
- });
8381
- fetch(actionUrl, {
8382
- method: 'POST',
8383
- headers: {
8384
- 'Content-Type': 'application/json'
8385
- },
8386
- body: body
8387
- })["catch"](function (e) {
8388
- if (handlers.error) handlers.error(e);
8389
- });
8390
- };
8391
-
8392
- /**
8393
- * Register an event handler.
8394
- * @param {string} event - 'open'|'message'|'error'|'close'
8395
- * @param {Function} handler
8396
- */
8397
- conn.on = function (event, handler) {
8398
- handlers[event] = handler;
8399
- return conn;
8400
- };
8401
-
8402
- /**
8403
- * Close the connection.
8404
- */
8405
- conn.close = function () {
8406
- if (conn._es) {
8407
- conn._es.close();
8408
- conn._es = null;
8409
- }
8410
- if (conn._pollTimer) {
8411
- clearInterval(conn._pollTimer);
8412
- conn._pollTimer = null;
8413
- }
8414
- setStatus('disconnected');
8415
- if (handlers.close) handlers.close();
8416
- };
8417
- return conn;
8418
- };
8419
-
8420
8575
  // ===================================================================================
8421
8576
  // bw.inspect() — Debug utility
8422
8577
  // ===================================================================================
@@ -8645,7 +8800,7 @@
8645
8800
  * @returns {Element} The style element
8646
8801
  * @category CSS & Styling
8647
8802
  * @see bw.css
8648
- * @see bw.loadDefaultStyles
8803
+ * @see bw.loadStyles
8649
8804
  * @example
8650
8805
  * bw.injectCSS('.my-class { color: red; }');
8651
8806
  * bw.injectCSS({ '.card': { padding: '1rem' } }, { id: 'card-styles' });
@@ -8691,9 +8846,8 @@
8691
8846
  * @param {...Object} styles - Style objects to merge (left-to-right)
8692
8847
  * @returns {Object} Merged style object
8693
8848
  * @category CSS & Styling
8694
- * @see bw.u
8695
8849
  * @example
8696
- * var style = bw.s(bw.u.flex, bw.u.gap4, { color: 'red' });
8850
+ * var style = bw.s({ display: 'flex' }, { gap: '1rem' }, { color: 'red' });
8697
8851
  * // => { display: 'flex', gap: '1rem', color: 'red' }
8698
8852
  */
8699
8853
  bw.s = function () {
@@ -8705,216 +8859,6 @@
8705
8859
  return result;
8706
8860
  };
8707
8861
 
8708
- /**
8709
- * Pre-built CSS utility objects (like Tailwind utilities, but in JS).
8710
- *
8711
- * Compose with `bw.s()` to build inline styles without writing raw CSS strings.
8712
- * Includes flex, padding, margin, typography, color, border, and transition utilities.
8713
- *
8714
- * @category CSS & Styling
8715
- * @see bw.s
8716
- * @example
8717
- * { t: 'div', a: { style: bw.s(bw.u.flex, bw.u.gap4, bw.u.p4) },
8718
- * c: 'Flexbox with 1rem gap and padding' }
8719
- */
8720
- bw.u = {
8721
- // Display
8722
- flex: {
8723
- display: 'flex'
8724
- },
8725
- flexCol: {
8726
- display: 'flex',
8727
- flexDirection: 'column'
8728
- },
8729
- flexRow: {
8730
- display: 'flex',
8731
- flexDirection: 'row'
8732
- },
8733
- flexWrap: {
8734
- display: 'flex',
8735
- flexWrap: 'wrap'
8736
- },
8737
- block: {
8738
- display: 'block'
8739
- },
8740
- inline: {
8741
- display: 'inline'
8742
- },
8743
- hidden: {
8744
- display: 'none'
8745
- },
8746
- // Flex alignment
8747
- justifyCenter: {
8748
- justifyContent: 'center'
8749
- },
8750
- justifyBetween: {
8751
- justifyContent: 'space-between'
8752
- },
8753
- justifyEnd: {
8754
- justifyContent: 'flex-end'
8755
- },
8756
- alignCenter: {
8757
- alignItems: 'center'
8758
- },
8759
- alignStart: {
8760
- alignItems: 'flex-start'
8761
- },
8762
- alignEnd: {
8763
- alignItems: 'flex-end'
8764
- },
8765
- // Gap (0.25rem increments)
8766
- gap1: {
8767
- gap: '0.25rem'
8768
- },
8769
- gap2: {
8770
- gap: '0.5rem'
8771
- },
8772
- gap3: {
8773
- gap: '0.75rem'
8774
- },
8775
- gap4: {
8776
- gap: '1rem'
8777
- },
8778
- gap6: {
8779
- gap: '1.5rem'
8780
- },
8781
- gap8: {
8782
- gap: '2rem'
8783
- },
8784
- // Padding
8785
- p0: {
8786
- padding: '0'
8787
- },
8788
- p1: {
8789
- padding: '0.25rem'
8790
- },
8791
- p2: {
8792
- padding: '0.5rem'
8793
- },
8794
- p3: {
8795
- padding: '0.75rem'
8796
- },
8797
- p4: {
8798
- padding: '1rem'
8799
- },
8800
- p6: {
8801
- padding: '1.5rem'
8802
- },
8803
- p8: {
8804
- padding: '2rem'
8805
- },
8806
- px4: {
8807
- paddingLeft: '1rem',
8808
- paddingRight: '1rem'
8809
- },
8810
- py2: {
8811
- paddingTop: '0.5rem',
8812
- paddingBottom: '0.5rem'
8813
- },
8814
- py4: {
8815
- paddingTop: '1rem',
8816
- paddingBottom: '1rem'
8817
- },
8818
- // Margin (same scale)
8819
- m0: {
8820
- margin: '0'
8821
- },
8822
- m4: {
8823
- margin: '1rem'
8824
- },
8825
- mt2: {
8826
- marginTop: '0.5rem'
8827
- },
8828
- mt4: {
8829
- marginTop: '1rem'
8830
- },
8831
- mb2: {
8832
- marginBottom: '0.5rem'
8833
- },
8834
- mb4: {
8835
- marginBottom: '1rem'
8836
- },
8837
- mx_auto: {
8838
- marginLeft: 'auto',
8839
- marginRight: 'auto'
8840
- },
8841
- // Typography
8842
- textSm: {
8843
- fontSize: '0.875rem'
8844
- },
8845
- textBase: {
8846
- fontSize: '1rem'
8847
- },
8848
- textLg: {
8849
- fontSize: '1.125rem'
8850
- },
8851
- textXl: {
8852
- fontSize: '1.25rem'
8853
- },
8854
- text2xl: {
8855
- fontSize: '1.5rem'
8856
- },
8857
- text3xl: {
8858
- fontSize: '1.875rem'
8859
- },
8860
- bold: {
8861
- fontWeight: '700'
8862
- },
8863
- semibold: {
8864
- fontWeight: '600'
8865
- },
8866
- italic: {
8867
- fontStyle: 'italic'
8868
- },
8869
- textCenter: {
8870
- textAlign: 'center'
8871
- },
8872
- textRight: {
8873
- textAlign: 'right'
8874
- },
8875
- // Colors (from design tokens)
8876
- bgWhite: {
8877
- background: '#ffffff'
8878
- },
8879
- bgTeal: {
8880
- background: '#006666',
8881
- color: '#ffffff'
8882
- },
8883
- textWhite: {
8884
- color: '#ffffff'
8885
- },
8886
- textTeal: {
8887
- color: '#006666'
8888
- },
8889
- textMuted: {
8890
- color: '#888'
8891
- },
8892
- // Borders
8893
- rounded: {
8894
- borderRadius: '0.375rem'
8895
- },
8896
- roundedLg: {
8897
- borderRadius: '0.5rem'
8898
- },
8899
- roundedFull: {
8900
- borderRadius: '9999px'
8901
- },
8902
- border: {
8903
- border: '1px solid #d8d8d8'
8904
- },
8905
- // Sizing
8906
- wFull: {
8907
- width: '100%'
8908
- },
8909
- hFull: {
8910
- height: '100%'
8911
- },
8912
- // Transitions
8913
- transition: {
8914
- transition: 'all 0.2s ease'
8915
- }
8916
- };
8917
-
8918
8862
  /**
8919
8863
  * Generate responsive CSS with media query breakpoints.
8920
8864
  *
@@ -9040,111 +8984,48 @@
9040
8984
  };
9041
8985
  }
9042
8986
 
9043
- /**
9044
- * Load the built-in Bootstrap-inspired default stylesheet.
9045
- *
9046
- * Injects bitwrench's batteries-included CSS (buttons, cards, grids, forms,
9047
- * alerts, badges, nav, tabs, etc.) into the document head. Call once at app startup.
9048
- * Returns null in Node.js (no DOM).
9049
- *
9050
- * @param {Object} [options] - Style loading options
9051
- * @param {boolean} [options.minify=true] - Minify the CSS output
9052
- * @returns {Element|null} Style element if in browser, null in Node.js
9053
- * @category CSS & Styling
9054
- * @see bw.setTheme
9055
- * @see bw.applyTheme
9056
- * @see bw.toggleTheme
9057
- * @example
9058
- * bw.loadDefaultStyles(); // inject all default CSS
9059
- */
9060
- bw.loadDefaultStyles = function () {
9061
- var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
9062
- var _options$minify2 = options.minify,
9063
- minify = _options$minify2 === void 0 ? true : _options$minify2,
9064
- palette = options.palette;
9065
-
9066
- // 1. Inject structural CSS (layout, sizing — never changes with theme)
9067
- if (bw._isBrowser) {
9068
- var structuralCSS = bw.css(getStructuralStyles());
9069
- bw.injectCSS(structuralCSS, {
9070
- id: 'bw_structural',
9071
- append: false,
9072
- minify: minify
9073
- });
9074
- }
8987
+ // =========================================================================
8988
+ // v2.0.18 Clean Styles API makeStyles / applyStyles / loadStyles / etc.
8989
+ // =========================================================================
9075
8990
 
9076
- // 2. Inject cosmetic CSS via generateTheme (colors, shadows, radii)
9077
- var paletteConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, palette || {});
9078
- var result = bw.generateTheme('', Object.assign({}, paletteConfig, {
9079
- inject: true
9080
- }));
9081
- return result;
9082
- };
8991
+ /**
8992
+ * Convert a scope selector to a <style> element id.
8993
+ * @private
8994
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview')
8995
+ * @returns {string} Style element id (e.g. 'bw_style_my_dashboard')
8996
+ */
8997
+ function _scopeToStyleId(scope) {
8998
+ if (!scope || scope === '' || scope === 'global') return 'bw_style_global';
8999
+ if (scope === 'reset') return 'bw_style_reset';
9000
+ // Strip leading # or . and convert - to _
9001
+ var clean = scope.replace(/^[#.]/, '').replace(/-/g, '_');
9002
+ return 'bw_style_' + clean;
9003
+ }
9083
9004
 
9084
9005
  /**
9085
- * Generate a complete, scoped theme from seed colors.
9086
- *
9087
- * Produces CSS for all themed components (buttons, alerts, badges, cards,
9088
- * forms, nav, tables, tabs, list groups, pagination, progress, hero, utilities)
9089
- * scoped under `.name` class. Multiple themes can coexist in the stylesheet.
9090
- * Swap themes by changing the class on a container element.
9091
- *
9092
- * @param {string} name - CSS scope class (e.g. 'ocean'). Empty string = unscoped global.
9093
- * @param {Object} config - Theme configuration
9094
- * @param {string} config.primary - Primary brand color hex
9095
- * @param {string} config.secondary - Secondary color hex
9096
- * @param {string} [config.tertiary] - Tertiary/accent color hex (defaults to primary)
9097
- * @param {string} [config.success='#198754'] - Success color hex
9098
- * @param {string} [config.danger='#dc3545'] - Danger color hex
9099
- * @param {string} [config.warning='#ffc107'] - Warning color hex
9100
- * @param {string} [config.info='#0dcaf0'] - Info color hex
9101
- * @param {string} [config.light='#f8f9fa'] - Light color hex
9102
- * @param {string} [config.dark='#212529'] - Dark color hex
9103
- * @param {string} [config.background] - Page background hex (default: '#ffffff' light, derived dark)
9104
- * @param {string} [config.surface] - Surface/card background hex (default: '#f8f9fa' light, derived dark)
9006
+ * Generate a complete styles object from seed colors and layout config.
9007
+ * Pure function — no DOM, no state, no side effects.
9008
+ *
9009
+ * All parameters are optional. Defaults to the bitwrench default palette.
9010
+ *
9011
+ * @param {Object} [config] - Style configuration
9012
+ * @param {string} [config.primary='#006666'] - Primary brand color hex
9013
+ * @param {string} [config.secondary='#6c757d'] - Secondary color hex
9014
+ * @param {string} [config.tertiary] - Tertiary color hex (defaults to primary)
9105
9015
  * @param {string} [config.spacing='normal'] - 'compact' | 'normal' | 'spacious'
9106
9016
  * @param {string} [config.radius='md'] - 'none' | 'sm' | 'md' | 'lg' | 'pill'
9107
- * @param {number} [config.fontSize=1.0] - Base font size scale factor
9108
- * @param {string|number} [config.typeRatio='normal'] - 'tight' | 'normal' | 'relaxed' | 'dramatic' or a number
9109
- * @param {string} [config.elevation='md'] - 'flat' | 'sm' | 'md' | 'lg'
9110
- * @param {string} [config.motion='standard'] - 'reduced' | 'standard' | 'expressive'
9111
- * @param {number} [config.harmonize=0.20] - 0-1, semantic color hue shift toward primary
9112
- * @param {boolean} [config.inject=true] - Inject into DOM (browser only)
9113
- * @returns {Object} { css, palette, name, isLightPrimary, alternate: { css, palette } }
9017
+ * @returns {Object} { css, alternateCss, rules, alternateRules, palette, alternatePalette, isLightPrimary }
9114
9018
  * @category CSS & Styling
9115
- * @see bw.applyTheme
9116
- * @see bw.toggleTheme
9117
- * @see bw.loadDefaultStyles
9019
+ * @see bw.applyStyles
9020
+ * @see bw.loadStyles
9118
9021
  * @example
9119
- * // Generate and inject an ocean theme (primary + alternate)
9120
- * var theme = bw.generateTheme('ocean', {
9121
- * primary: '#0077b6',
9122
- * secondary: '#90e0ef',
9123
- * tertiary: '#00b4d8'
9124
- * });
9125
- *
9126
- * // Apply to a container
9127
- * document.getElementById('app').classList.add('ocean');
9128
- *
9129
- * // Toggle to alternate palette
9130
- * bw.toggleTheme();
9131
- *
9132
- * // Generate CSS for static export (Node.js)
9133
- * var result = bw.generateTheme('sunset', {
9134
- * primary: '#e76f51',
9135
- * secondary: '#264653',
9136
- * inject: false
9137
- * });
9138
- * fs.writeFileSync('sunset.css', result.css + result.alternate.css);
9022
+ * var styles = bw.makeStyles({ primary: '#4f46e5', secondary: '#d97706' });
9023
+ * console.log(styles.palette.primary.base); // '#4f46e5'
9024
+ * // styles.css contains all themed CSS — nothing injected
9139
9025
  */
9140
- bw.generateTheme = function (name, config) {
9141
- if (!config || !config.primary || !config.secondary) {
9142
- throw new Error('bw.generateTheme requires config.primary and config.secondary');
9143
- }
9144
-
9145
- // Merge with defaults; if user didn't supply tertiary, default to their primary
9146
- var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config);
9147
- if (!config.tertiary) fullConfig.tertiary = fullConfig.primary;
9026
+ bw.makeStyles = function (config) {
9027
+ var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config || {});
9028
+ if (config && !config.tertiary) fullConfig.tertiary = fullConfig.primary;
9148
9029
 
9149
9030
  // Derive primary palette
9150
9031
  var palette = derivePalette(fullConfig);
@@ -9152,136 +9033,211 @@
9152
9033
  // Resolve layout
9153
9034
  var layout = resolveLayout(fullConfig);
9154
9035
 
9155
- // Generate primary themed CSS rules
9156
- var themedRules = generateThemedCSS(name, palette, layout);
9036
+ // Generate primary themed CSS rules (unscoped)
9037
+ var themedRules = generateThemedCSS('', palette, layout);
9157
9038
  var cssStr = bw.css(themedRules);
9158
9039
 
9159
9040
  // Derive alternate palette (luminance-inverted)
9160
9041
  var altConfig = deriveAlternateConfig(fullConfig);
9161
9042
  var altPalette = derivePalette(altConfig);
9162
9043
 
9163
- // Generate alternate CSS scoped under .bw_theme_alt
9164
- var altRules = generateAlternateCSS(name, altPalette, layout);
9165
- var altCssStr = bw.css(altRules);
9044
+ // Generate alternate CSS rules WITHOUT .bw_theme_alt prefix (raw rules)
9045
+ // applyStyles() wraps them appropriately based on scope
9046
+ var altRawRules = generateThemedCSS('', altPalette, layout);
9047
+
9048
+ // Add body-level surface overrides for the alternate palette.
9049
+ // When .bw_theme_alt is on <html>, ".bw_theme_alt body" correctly matches.
9050
+ altRawRules['body'] = {
9051
+ 'color': altPalette.dark.base,
9052
+ 'background-color': altPalette.surface || altPalette.light.base
9053
+ };
9054
+ var altCssStr = bw.css(altRawRules);
9166
9055
 
9167
9056
  // Determine if primary is light-flavored
9168
9057
  var lightPrimary = isLightPalette(fullConfig);
9058
+ return {
9059
+ css: cssStr,
9060
+ alternateCss: altCssStr,
9061
+ rules: themedRules,
9062
+ alternateRules: altRawRules,
9063
+ palette: palette,
9064
+ alternatePalette: altPalette,
9065
+ isLightPrimary: lightPrimary
9066
+ };
9067
+ };
9169
9068
 
9170
- // Inject both CSS sets into DOM if requested
9171
- var shouldInject = config.inject !== false;
9172
- if (shouldInject && bw._isBrowser) {
9173
- var safeName = name ? name.replace(/-/g, '_') : '';
9174
- var styleId = safeName ? 'bw_theme_' + safeName : 'bw_theme_default';
9175
- var altStyleId = safeName ? 'bw_theme_' + safeName + '_alt' : 'bw_theme_default_alt';
9176
- bw.injectCSS(cssStr, {
9177
- id: styleId,
9178
- append: false
9179
- });
9180
- bw.injectCSS(altCssStr, {
9181
- id: altStyleId,
9182
- append: false
9183
- });
9184
- bw._activeThemeStyleIds = [styleId, altStyleId];
9069
+ /**
9070
+ * Inject styles into the DOM with optional scoping.
9071
+ *
9072
+ * Takes a styles object from `makeStyles()` and creates a single `<style>`
9073
+ * element in `<head>`. If a scope selector is provided, all CSS rules are
9074
+ * wrapped under that selector. Alternate CSS is wrapped under `.bw_theme_alt`.
9075
+ *
9076
+ * @param {Object} styles - Result of `bw.makeStyles()`
9077
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview'). Omit for global.
9078
+ * @returns {Element|null} The `<style>` element, or null in Node.js
9079
+ * @category CSS & Styling
9080
+ * @see bw.makeStyles
9081
+ * @see bw.loadStyles
9082
+ * @see bw.clearStyles
9083
+ * @example
9084
+ * var styles = bw.makeStyles({ primary: '#4f46e5' });
9085
+ * bw.applyStyles(styles); // global
9086
+ * bw.applyStyles(styles, '#my-dashboard'); // scoped
9087
+ */
9088
+ bw.applyStyles = function (styles, scope) {
9089
+ if (!bw._isBrowser) return null;
9090
+ if (!styles || !styles.rules) {
9091
+ _cw('bw.applyStyles: invalid styles object');
9092
+ return null;
9185
9093
  }
9094
+ var styleId = _scopeToStyleId(scope);
9186
9095
 
9187
- // Update bw.u color entries to reflect the palette
9188
- if (!name) {
9189
- bw.u.bgTeal = {
9190
- background: palette.primary.base,
9191
- color: palette.primary.textOn
9192
- };
9193
- bw.u.textTeal = {
9194
- color: palette.primary.base
9195
- };
9196
- bw.u.bgWhite = {
9197
- background: '#ffffff'
9198
- };
9199
- bw.u.textWhite = {
9200
- color: '#ffffff'
9201
- };
9096
+ // Scope the primary rules if a scope is provided
9097
+ var primaryRules = styles.rules;
9098
+ if (scope) {
9099
+ primaryRules = scopeRulesUnder(primaryRules, scope);
9202
9100
  }
9203
9101
 
9204
- // Store active theme state
9205
- var result = {
9206
- css: cssStr,
9207
- palette: palette,
9208
- name: name,
9209
- isLightPrimary: lightPrimary,
9210
- alternate: {
9211
- css: altCssStr,
9212
- palette: altPalette
9102
+ // Wrap alternate rules with .bw_theme_alt
9103
+ var altRules = styles.alternateRules;
9104
+ if (altRules) {
9105
+ if (scope) {
9106
+ // Scoped compound: #scope.bw_theme_alt .bw_card
9107
+ altRules = scopeRulesUnder(altRules, scope + '.bw_theme_alt');
9108
+ } else {
9109
+ // Global: .bw_theme_alt .bw_card
9110
+ altRules = scopeRulesUnder(altRules, '.bw_theme_alt');
9213
9111
  }
9214
- };
9215
- bw._activeTheme = result;
9216
- bw._activeThemeMode = 'primary';
9217
- return result;
9112
+ }
9113
+
9114
+ // Combine primary + alternate into one CSS string
9115
+ var combined = bw.css(primaryRules);
9116
+ if (altRules) {
9117
+ combined += '\n' + bw.css(altRules);
9118
+ }
9119
+ return bw.injectCSS(combined, {
9120
+ id: styleId,
9121
+ append: false
9122
+ });
9218
9123
  };
9219
9124
 
9220
9125
  /**
9221
- * Apply a theme mode. Switches between primary and alternate palettes
9222
- * by adding/removing the `bw_theme_alt` class on `<html>`.
9126
+ * Generate and apply styles in one call. Convenience wrapper.
9127
+ *
9128
+ * Equivalent to: `bw.applyStyles(bw.makeStyles(config), scope)`
9223
9129
  *
9224
- * @param {string} mode - 'primary' | 'alternate' | 'light' | 'dark'
9225
- * @returns {string} Active mode: 'primary' or 'alternate'
9130
+ * @param {Object} [config] - Style configuration (same as `makeStyles`)
9131
+ * @param {string} [scope] - Scope selector (same as `applyStyles`)
9132
+ * @returns {Element|null} The `<style>` element, or null in Node.js
9226
9133
  * @category CSS & Styling
9227
- * @see bw.generateTheme
9228
- * @see bw.toggleTheme
9134
+ * @see bw.makeStyles
9135
+ * @see bw.applyStyles
9229
9136
  * @example
9230
- * bw.applyTheme('alternate'); // switch to alternate palette
9231
- * bw.applyTheme('dark'); // switch to whichever palette is darker
9232
- * bw.applyTheme('primary'); // switch back to primary palette
9233
- */
9234
- bw.applyTheme = function (mode) {
9235
- if (!bw._isBrowser) return mode || 'primary';
9236
- var root = document.documentElement;
9237
- var isLight = bw._activeTheme ? bw._activeTheme.isLightPrimary : true;
9238
- var wantAlt;
9239
- if (mode === 'primary') wantAlt = false;else if (mode === 'alternate') wantAlt = true;else if (mode === 'light') wantAlt = !isLight;else if (mode === 'dark') wantAlt = isLight;else wantAlt = false;
9240
- if (wantAlt) {
9241
- root.classList.add('bw_theme_alt');
9242
- } else {
9243
- root.classList.remove('bw_theme_alt');
9137
+ * bw.loadStyles(); // defaults, global
9138
+ * bw.loadStyles({ primary: '#4f46e5' }); // custom, global
9139
+ * bw.loadStyles({ primary: '#4f46e5' }, '#my-dashboard'); // custom, scoped
9140
+ */
9141
+ bw.loadStyles = function (config, scope) {
9142
+ // Also inject structural CSS first (only once)
9143
+ if (bw._isBrowser) {
9144
+ var existing = document.getElementById('bw_structural');
9145
+ if (!existing) {
9146
+ var structuralCSS = bw.css(getStructuralStyles());
9147
+ bw.injectCSS(structuralCSS, {
9148
+ id: 'bw_structural',
9149
+ append: false
9150
+ });
9151
+ }
9244
9152
  }
9245
- bw._activeThemeMode = wantAlt ? 'alternate' : 'primary';
9246
- return bw._activeThemeMode;
9153
+ return bw.applyStyles(bw.makeStyles(config), scope);
9154
+ };
9155
+
9156
+ /**
9157
+ * Inject the CSS reset (box-sizing, html/body font, reduced-motion).
9158
+ * Idempotent — if already injected, returns the existing `<style>` element.
9159
+ *
9160
+ * @returns {Element|null} The `<style>` element, or null in Node.js
9161
+ * @category CSS & Styling
9162
+ * @see bw.loadStyles
9163
+ * @see bw.clearStyles
9164
+ * @example
9165
+ * bw.loadReset(); // inject once, safe to call multiple times
9166
+ */
9167
+ bw.loadReset = function () {
9168
+ if (!bw._isBrowser) return null;
9169
+ var existing = document.getElementById('bw_style_reset');
9170
+ if (existing) return existing;
9171
+ return bw.injectCSS(bw.css(getResetStyles()), {
9172
+ id: 'bw_style_reset',
9173
+ append: false
9174
+ });
9247
9175
  };
9248
9176
 
9249
9177
  /**
9250
- * Toggle between primary and alternate theme palettes.
9178
+ * Toggle between primary and alternate palettes.
9251
9179
  *
9180
+ * Adds/removes the `bw_theme_alt` class on the scoping element.
9181
+ * Without a scope, toggles on `<html>` (global).
9182
+ * With a scope, toggles on the first matching element.
9183
+ *
9184
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard'). Omit for global.
9252
9185
  * @returns {string} Active mode after toggle: 'primary' or 'alternate'
9253
9186
  * @category CSS & Styling
9254
- * @see bw.applyTheme
9255
- * @see bw.generateTheme
9187
+ * @see bw.applyStyles
9188
+ * @see bw.clearStyles
9256
9189
  * @example
9257
- * bw.toggleTheme(); // flip between primary and alternate
9190
+ * bw.toggleStyles(); // global toggle on <html>
9191
+ * bw.toggleStyles('#my-dashboard'); // scoped toggle
9258
9192
  */
9259
- bw.toggleTheme = function () {
9260
- var current = bw._activeThemeMode || 'primary';
9261
- return bw.applyTheme(current === 'primary' ? 'alternate' : 'primary');
9193
+ bw.toggleStyles = function (scope) {
9194
+ if (!bw._isBrowser) return 'primary';
9195
+ var target;
9196
+ if (scope) {
9197
+ var els = bw.$(scope);
9198
+ target = els[0];
9199
+ } else {
9200
+ target = document.documentElement;
9201
+ }
9202
+ if (!target) return 'primary';
9203
+ var hasAlt = target.classList.contains('bw_theme_alt');
9204
+ if (hasAlt) {
9205
+ target.classList.remove('bw_theme_alt');
9206
+ return 'primary';
9207
+ } else {
9208
+ target.classList.add('bw_theme_alt');
9209
+ return 'alternate';
9210
+ }
9262
9211
  };
9263
9212
 
9264
9213
  /**
9265
- * Remove the currently active theme's injected style elements from the DOM.
9266
- * Use this before generating a new theme with a different name to prevent
9267
- * stale CSS accumulation.
9214
+ * Remove injected styles for a given scope.
9215
+ *
9216
+ * Finds the `<style>` element by id and removes it. Also removes
9217
+ * the `bw_theme_alt` class from the relevant element.
9268
9218
  *
9219
+ * @param {string} [scope] - Scope selector. Omit to remove global styles.
9269
9220
  * @category CSS & Styling
9270
- * @see bw.generateTheme
9221
+ * @see bw.applyStyles
9222
+ * @see bw.loadStyles
9271
9223
  * @example
9272
- * bw.clearTheme(); // remove current theme styles
9273
- * bw.generateTheme('sunset', conf); // inject fresh theme
9274
- */
9275
- bw.clearTheme = function () {
9276
- if (bw._activeThemeStyleIds && bw._isBrowser) {
9277
- bw._activeThemeStyleIds.forEach(function (id) {
9278
- var el = document.getElementById(id);
9279
- if (el) el.remove();
9280
- });
9281
- bw._activeThemeStyleIds = null;
9224
+ * bw.clearStyles(); // remove global styles
9225
+ * bw.clearStyles('#my-dashboard'); // remove scoped styles
9226
+ * bw.clearStyles('reset'); // remove the CSS reset
9227
+ */
9228
+ bw.clearStyles = function (scope) {
9229
+ if (!bw._isBrowser) return;
9230
+ var styleId = _scopeToStyleId(scope);
9231
+ var el = document.getElementById(styleId);
9232
+ if (el) el.remove();
9233
+
9234
+ // Also remove bw_theme_alt from the relevant element
9235
+ if (scope && scope !== 'reset' && scope !== 'global') {
9236
+ var targets = bw.$(scope);
9237
+ if (targets[0]) targets[0].classList.remove('bw_theme_alt');
9238
+ } else if (!scope || scope === 'global') {
9239
+ document.documentElement.classList.remove('bw_theme_alt');
9282
9240
  }
9283
- bw._activeTheme = null;
9284
- bw._activeThemeMode = 'primary';
9285
9241
  };
9286
9242
 
9287
9243
  // Expose color utility functions on bw namespace
@@ -9503,10 +9459,15 @@
9503
9459
  * @param {Object} config - Table configuration
9504
9460
  * @param {Array<Object>} config.data - Array of row objects to display
9505
9461
  * @param {Array<Object>} [config.columns] - Column definitions with key, label, render
9506
- * @param {string} [config.className='table'] - CSS class for table element
9462
+ * @param {string} [config.className=''] - Additional CSS classes for table element
9507
9463
  * @param {boolean} [config.sortable=true] - Enable click-to-sort headers
9508
9464
  * @param {Function} [config.onSort] - Sort callback (column, direction)
9509
- * @returns {Object} TACO object for table
9465
+ * @param {boolean} [config.selectable=false] - Enable row selection on click
9466
+ * @param {Function} [config.onRowClick] - Row click callback (row, index, event)
9467
+ * @param {number} [config.pageSize] - Rows per page (enables pagination when set)
9468
+ * @param {number} [config.currentPage=1] - Current page number (1-based)
9469
+ * @param {Function} [config.onPageChange] - Page change callback (newPage)
9470
+ * @returns {Object} TACO object for table (with optional pagination controls)
9510
9471
  * @category Component Builders
9511
9472
  * @see bw.makeDataTable
9512
9473
  * @example
@@ -9518,7 +9479,12 @@
9518
9479
  * columns: [
9519
9480
  * { key: 'name', label: 'Name' },
9520
9481
  * { key: 'age', label: 'Age' }
9521
- * ]
9482
+ * ],
9483
+ * selectable: true,
9484
+ * onRowClick: function(row, i) { console.log('clicked', row.name); },
9485
+ * pageSize: 10,
9486
+ * currentPage: 1,
9487
+ * onPageChange: function(page) { console.log('page', page); }
9522
9488
  * });
9523
9489
  */
9524
9490
  bw.makeTable = function (config) {
@@ -9536,12 +9502,20 @@
9536
9502
  onSort = config.onSort,
9537
9503
  sortColumn = config.sortColumn,
9538
9504
  _config$sortDirection = config.sortDirection,
9539
- sortDirection = _config$sortDirection === void 0 ? 'asc' : _config$sortDirection;
9540
-
9541
- // Build class list: always include bw_table, add striped/hover, append user className
9505
+ sortDirection = _config$sortDirection === void 0 ? 'asc' : _config$sortDirection,
9506
+ _config$selectable = config.selectable,
9507
+ selectable = _config$selectable === void 0 ? false : _config$selectable,
9508
+ onRowClick = config.onRowClick,
9509
+ pageSize = config.pageSize,
9510
+ _config$currentPage = config.currentPage,
9511
+ currentPage = _config$currentPage === void 0 ? 1 : _config$currentPage,
9512
+ onPageChange = config.onPageChange;
9513
+
9514
+ // Build class list: always include bw_table, add striped/hover/selectable, append user className
9542
9515
  var cls = 'bw_table';
9543
9516
  if (striped) cls += ' bw_table_striped';
9544
- if (hover) cls += ' bw_table_hover';
9517
+ if (hover || selectable) cls += ' bw_table_hover';
9518
+ if (selectable) cls += ' bw_table_selectable';
9545
9519
  if (className) cls += ' ' + className;
9546
9520
  cls = cls.trim();
9547
9521
 
@@ -9580,6 +9554,15 @@
9580
9554
  });
9581
9555
  }
9582
9556
 
9557
+ // Pagination
9558
+ var totalRows = sortedData.length;
9559
+ var totalPages = pageSize ? Math.max(1, Math.ceil(totalRows / pageSize)) : 1;
9560
+ var page = Math.max(1, Math.min(currentPage, totalPages));
9561
+ if (pageSize) {
9562
+ var start = (page - 1) * pageSize;
9563
+ sortedData = sortedData.slice(start, start + pageSize);
9564
+ }
9565
+
9583
9566
  // Create sort handler
9584
9567
  var handleSort = function handleSort(column) {
9585
9568
  if (!sortable) return;
@@ -9625,12 +9608,28 @@
9625
9608
  }
9626
9609
  };
9627
9610
 
9628
- // Build table body
9611
+ // Build table body with selectable/onRowClick support
9629
9612
  var tbody = {
9630
9613
  t: 'tbody',
9631
- c: sortedData.map(function (row) {
9614
+ c: sortedData.map(function (row, idx) {
9615
+ var globalIdx = pageSize ? (page - 1) * pageSize + idx : idx;
9616
+ var rowAttrs = {};
9617
+ if (selectable || onRowClick) {
9618
+ rowAttrs.style = 'cursor:pointer;';
9619
+ rowAttrs.onclick = function (e) {
9620
+ if (selectable) {
9621
+ // Toggle selected class on this row
9622
+ var tr = e.currentTarget;
9623
+ tr.classList.toggle('bw_table_row_selected');
9624
+ }
9625
+ if (onRowClick) {
9626
+ onRowClick(row, globalIdx, e);
9627
+ }
9628
+ };
9629
+ }
9632
9630
  return {
9633
9631
  t: 'tr',
9632
+ a: rowAttrs,
9634
9633
  c: cols.map(function (col) {
9635
9634
  return {
9636
9635
  t: 'td',
@@ -9640,13 +9639,65 @@
9640
9639
  };
9641
9640
  })
9642
9641
  };
9643
- return {
9642
+ var table = {
9644
9643
  t: 'table',
9645
9644
  a: {
9646
9645
  "class": cls
9647
9646
  },
9648
9647
  c: [thead, tbody]
9649
9648
  };
9649
+
9650
+ // If no pagination, return table directly
9651
+ if (!pageSize) return table;
9652
+
9653
+ // Build pagination controls
9654
+ var pageButtons = [];
9655
+ // Previous button
9656
+ pageButtons.push({
9657
+ t: 'button',
9658
+ a: {
9659
+ "class": 'bw_btn bw_btn_sm',
9660
+ disabled: page <= 1 ? 'disabled' : undefined,
9661
+ onclick: page > 1 && onPageChange ? function () {
9662
+ onPageChange(page - 1);
9663
+ } : undefined
9664
+ },
9665
+ c: 'Prev'
9666
+ });
9667
+ // Page info
9668
+ pageButtons.push({
9669
+ t: 'span',
9670
+ a: {
9671
+ style: 'margin:0 0.5rem;font-size:0.875rem;'
9672
+ },
9673
+ c: 'Page ' + page + ' of ' + totalPages
9674
+ });
9675
+ // Next button
9676
+ pageButtons.push({
9677
+ t: 'button',
9678
+ a: {
9679
+ "class": 'bw_btn bw_btn_sm',
9680
+ disabled: page >= totalPages ? 'disabled' : undefined,
9681
+ onclick: page < totalPages && onPageChange ? function () {
9682
+ onPageChange(page + 1);
9683
+ } : undefined
9684
+ },
9685
+ c: 'Next'
9686
+ });
9687
+ return {
9688
+ t: 'div',
9689
+ a: {
9690
+ "class": 'bw_table_paginated'
9691
+ },
9692
+ c: [table, {
9693
+ t: 'div',
9694
+ a: {
9695
+ "class": 'bw_table_pagination',
9696
+ style: 'display:flex;align-items:center;justify-content:flex-end;padding:0.5rem 0;gap:0.25rem;'
9697
+ },
9698
+ c: pageButtons
9699
+ }]
9700
+ };
9650
9701
  };
9651
9702
 
9652
9703
  /**