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 v2.0.17 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
1
+ /*! bitwrench 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')] = {
1276
- 'color': palette.secondary.base
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
1277
1267
  };
1278
- rules[scopeSelector(scope, '.bw_breadcrumb_item.active')] = {
1268
+ rules[_sx(scope, '.bw_breadcrumb_item + .bw_breadcrumb_item::before')] = {
1279
1269
  'color': palette.secondary.base
1280
1270
  };
1281
- rules[scopeSelector(scope, '.bw_breadcrumb_item a:hover')] = {
1271
+ rules[_sx(scope, '.bw_breadcrumb_item a')] = {
1272
+ 'color': palette.primary.base,
1273
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing
1274
+ };
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
  /**
@@ -5414,7 +5595,7 @@
5414
5595
  if (breakpoint === 'xs') {
5415
5596
  classes.push("bw_col_".concat(value));
5416
5597
  } else {
5417
- classes.push("bw_col_".concat(breakpoint, "-").concat(value));
5598
+ classes.push("bw_col_".concat(breakpoint, "_").concat(value));
5418
5599
  }
5419
5600
  });
5420
5601
  } else if (size) {
@@ -6939,12 +7120,13 @@
6939
7120
  "class": "bw_page_item ".concat(currentPage <= 1 ? 'bw_disabled' : '').trim()
6940
7121
  },
6941
7122
  c: {
6942
- t: 'a',
7123
+ t: 'button',
6943
7124
  a: {
6944
7125
  "class": 'bw_page_link',
6945
- href: '#',
7126
+ type: 'button',
6946
7127
  onclick: handleClick(currentPage - 1),
6947
- 'aria-label': 'Previous'
7128
+ 'aria-label': 'Previous',
7129
+ disabled: currentPage <= 1 ? true : undefined
6948
7130
  },
6949
7131
  c: "\u2039"
6950
7132
  }
@@ -6959,11 +7141,12 @@
6959
7141
  "class": "bw_page_item ".concat(pageNum === currentPage ? 'bw_active' : '').trim()
6960
7142
  },
6961
7143
  c: {
6962
- t: 'a',
7144
+ t: 'button',
6963
7145
  a: {
6964
7146
  "class": 'bw_page_link',
6965
- href: '#',
6966
- onclick: handleClick(pageNum)
7147
+ type: 'button',
7148
+ onclick: handleClick(pageNum),
7149
+ 'aria-current': pageNum === currentPage ? 'page' : undefined
6967
7150
  },
6968
7151
  c: '' + pageNum
6969
7152
  }
@@ -6978,12 +7161,13 @@
6978
7161
  "class": "bw_page_item ".concat(currentPage >= pages ? 'bw_disabled' : '').trim()
6979
7162
  },
6980
7163
  c: {
6981
- t: 'a',
7164
+ t: 'button',
6982
7165
  a: {
6983
7166
  "class": 'bw_page_link',
6984
- href: '#',
7167
+ type: 'button',
6985
7168
  onclick: handleClick(currentPage + 1),
6986
- 'aria-label': 'Next'
7169
+ 'aria-label': 'Next',
7170
+ disabled: currentPage >= pages ? true : undefined
6987
7171
  },
6988
7172
  c: "\u203A"
6989
7173
  }
@@ -9496,7 +9680,12 @@
9496
9680
  el = document.querySelector('[data-bw_id="' + id + '"]');
9497
9681
  }
9498
9682
 
9499
- // 5. Cache the result for next time
9683
+ // 5. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
9684
+ if (!el && id.indexOf('bw_uuid_') === 0) {
9685
+ el = document.querySelector('.' + id);
9686
+ }
9687
+
9688
+ // 6. Cache the result for next time
9500
9689
  if (el) {
9501
9690
  bw._nodeMap[id] = el;
9502
9691
  }
@@ -9548,6 +9737,79 @@
9548
9737
  }
9549
9738
  };
9550
9739
 
9740
+ // ===================================================================================
9741
+ // bw.assignUUID() / bw.getUUID() — Explicit UUID addressing for TACO objects
9742
+ // ===================================================================================
9743
+
9744
+ /**
9745
+ * Regex to match a bw_uuid_* token in a class string.
9746
+ * @private
9747
+ */
9748
+ var _UUID_RE = /\bbw_uuid_[a-z0-9_]+\b/;
9749
+
9750
+ /**
9751
+ * Assign a UUID to a TACO object by appending a `bw_uuid_*` token to `taco.a.class`.
9752
+ *
9753
+ * Idempotent by default — calling twice returns the same UUID. Pass `forceNew=true`
9754
+ * to replace an existing UUID (useful in loops where each TACO needs a unique ID).
9755
+ *
9756
+ * @param {Object} taco - A TACO object `{t, a, c, o}`
9757
+ * @param {boolean} [forceNew=false] - If true, replaces any existing UUID with a new one
9758
+ * @returns {string} The UUID string (e.g. 'bw_uuid_a1b2c3d4e5')
9759
+ * @category Identifiers
9760
+ * @example
9761
+ * var card = bw.makeStatCard({ value: '0', label: 'Scans' });
9762
+ * var uuid = bw.assignUUID(card); // 'bw_uuid_a1b2c3d4e5'
9763
+ * var same = bw.assignUUID(card); // same UUID (idempotent)
9764
+ * var diff = bw.assignUUID(card, true); // new UUID (forced)
9765
+ */
9766
+ bw.assignUUID = function (taco, forceNew) {
9767
+ if (!taco || !_is(taco, 'object')) return null;
9768
+
9769
+ // Ensure taco.a exists
9770
+ if (!taco.a) taco.a = {};
9771
+ if (!_is(taco.a["class"], 'string')) taco.a["class"] = taco.a["class"] ? String(taco.a["class"]) : '';
9772
+ var existing = taco.a["class"].match(_UUID_RE);
9773
+ if (existing && !forceNew) {
9774
+ return existing[0];
9775
+ }
9776
+
9777
+ // Remove old UUID if forceNew
9778
+ if (existing) {
9779
+ taco.a["class"] = taco.a["class"].replace(_UUID_RE, '').replace(/\s+/g, ' ').trim();
9780
+ }
9781
+ var uuid = bw.uuid('uuid');
9782
+ taco.a["class"] = (taco.a["class"] ? taco.a["class"] + ' ' : '') + uuid;
9783
+ return uuid;
9784
+ };
9785
+
9786
+ /**
9787
+ * Read the UUID from a TACO object or DOM element. Pure getter, no side effects.
9788
+ *
9789
+ * @param {Object|Element} tacoOrElement - A TACO object or DOM element
9790
+ * @returns {string|null} The UUID string, or null if none assigned
9791
+ * @category Identifiers
9792
+ * @example
9793
+ * bw.getUUID(card) // 'bw_uuid_a1b2c3d4e5' (from TACO)
9794
+ * bw.getUUID(domEl) // 'bw_uuid_a1b2c3d4e5' (from DOM element)
9795
+ * bw.getUUID({t:'div'}) // null (no UUID)
9796
+ */
9797
+ bw.getUUID = function (tacoOrElement) {
9798
+ if (!tacoOrElement) return null;
9799
+ var classStr;
9800
+ // DOM element: check className
9801
+ if (tacoOrElement.className !== undefined && tacoOrElement.tagName) {
9802
+ classStr = tacoOrElement.className;
9803
+ }
9804
+ // TACO object: check a.class
9805
+ else if (tacoOrElement.a && _is(tacoOrElement.a["class"], 'string')) {
9806
+ classStr = tacoOrElement.a["class"];
9807
+ }
9808
+ if (!classStr) return null;
9809
+ var match = classStr.match(_UUID_RE);
9810
+ return match ? match[0] : null;
9811
+ };
9812
+
9551
9813
  /**
9552
9814
  * Escape HTML special characters to prevent XSS.
9553
9815
  *
@@ -9600,6 +9862,45 @@
9600
9862
  };
9601
9863
  };
9602
9864
 
9865
+ /**
9866
+ * Hyperscript-style TACO constructor.
9867
+ *
9868
+ * A convenience helper that returns a canonical TACO object from positional
9869
+ * arguments. The return value is a plain object — serializable, works with
9870
+ * bwserve, and accepted everywhere TACO is accepted.
9871
+ *
9872
+ * @param {string} tag - HTML tag name (e.g. 'div', 'p', 'section')
9873
+ * @param {Object|null} [attrs] - HTML attributes object. Pass null or omit to skip.
9874
+ * @param {*} [content] - Content: string, number, TACO object, or array of children.
9875
+ * @param {Object} [options] - TACO options (state, lifecycle hooks, render fn).
9876
+ * @returns {Object} Plain TACO object {t, a?, c?, o?}
9877
+ * @category Utilities
9878
+ * @see bw.html
9879
+ * @see bw.createDOM
9880
+ * @see bw.DOM
9881
+ * @example
9882
+ * bw.h('div')
9883
+ * // => { t: 'div' }
9884
+ *
9885
+ * bw.h('p', { class: 'bw_text_muted' }, 'Hello')
9886
+ * // => { t: 'p', a: { class: 'bw_text_muted' }, c: 'Hello' }
9887
+ *
9888
+ * bw.h('ul', null, [
9889
+ * bw.h('li', null, 'one'),
9890
+ * bw.h('li', null, 'two')
9891
+ * ])
9892
+ * // => { t: 'ul', c: [{ t: 'li', c: 'one' }, { t: 'li', c: 'two' }] }
9893
+ */
9894
+ bw.h = function (tag, attrs, content, options) {
9895
+ var taco = {
9896
+ t: String(tag)
9897
+ };
9898
+ if (attrs !== null && attrs !== undefined) taco.a = attrs;
9899
+ if (content !== undefined) taco.c = content;
9900
+ if (options !== undefined) taco.o = options;
9901
+ return taco;
9902
+ };
9903
+
9603
9904
  /**
9604
9905
  * Convert a TACO object (or array of TACOs) to an HTML string.
9605
9906
  *
@@ -9874,9 +10175,7 @@
9874
10175
  if (theme) {
9875
10176
  var themeConfig = _is(theme, 'string') ? THEME_PRESETS[theme.toLowerCase()] || null : theme;
9876
10177
  if (themeConfig) {
9877
- var themeResult = bw.generateTheme('', Object.assign({}, themeConfig, {
9878
- inject: false
9879
- }));
10178
+ var themeResult = bw.makeStyles(themeConfig);
9880
10179
  themeCSS = themeResult.css;
9881
10180
  }
9882
10181
  }
@@ -9910,14 +10209,14 @@
9910
10209
  // Combine all CSS
9911
10210
  var allCSS = (themeCSS ? themeCSS + '\n' : '') + css;
9912
10211
 
9913
- // Body-end script: registry entries + optional loadDefaultStyles
10212
+ // Body-end script: registry entries + optional loadStyles
9914
10213
  var bodyEndScript = '';
9915
10214
  var bodyEndParts = [];
9916
10215
  if (registryEntries) {
9917
10216
  bodyEndParts.push(registryEntries);
9918
10217
  }
9919
10218
  if (runtime === 'inline' || runtime === 'cdn') {
9920
- bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadDefaultStyles();}');
10219
+ bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadStyles();}');
9921
10220
  }
9922
10221
  if (bodyEndParts.length > 0) {
9923
10222
  bodyEndScript = '<script>\n' + bodyEndParts.join('\n') + '\n</script>';
@@ -10091,6 +10390,14 @@
10091
10390
  bw._registerNode(el, null);
10092
10391
  }
10093
10392
 
10393
+ // Register UUID class in node cache (bw_uuid_* tokens in class string)
10394
+ if (el.className) {
10395
+ var uuidMatch = el.className.match(_UUID_RE);
10396
+ if (uuidMatch) {
10397
+ bw._nodeMap[uuidMatch[0]] = el;
10398
+ }
10399
+ }
10400
+
10094
10401
  // Handle lifecycle hooks and state
10095
10402
  if (opts.mounted || opts.unmount || opts.render || opts.state) {
10096
10403
  var id = attrs['data-bw_id'] || bw.uuid();
@@ -10450,6 +10757,16 @@
10450
10757
  bw.cleanup = function (element) {
10451
10758
  if (!bw._isBrowser || !element) return;
10452
10759
 
10760
+ // Deregister UUID classes from node cache (element + descendants)
10761
+ // Covers elements that have UUID but no data-bw_id
10762
+ var selfUuidMatch = element.className && element.className.match(_UUID_RE);
10763
+ if (selfUuidMatch) delete bw._nodeMap[selfUuidMatch[0]];
10764
+ var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
10765
+ uuidEls.forEach(function (uel) {
10766
+ var m = uel.className && uel.className.match(_UUID_RE);
10767
+ if (m) delete bw._nodeMap[m[0]];
10768
+ });
10769
+
10453
10770
  // Find all elements with data-bw_id
10454
10771
  var elements = element.querySelectorAll('[data-bw_id]');
10455
10772
  elements.forEach(function (el) {
@@ -10463,6 +10780,10 @@
10463
10780
  // Deregister from node cache
10464
10781
  bw._deregisterNode(el, id);
10465
10782
 
10783
+ // Deregister UUID class from node cache
10784
+ var uuidMatch = el.className && el.className.match(_UUID_RE);
10785
+ if (uuidMatch) delete bw._nodeMap[uuidMatch[0]];
10786
+
10466
10787
  // Clean up pub/sub subscriptions tied to this element
10467
10788
  if (el._bw_subs) {
10468
10789
  el._bw_subs.forEach(function (unsub) {
@@ -10489,6 +10810,10 @@
10489
10810
  // Deregister from node cache
10490
10811
  bw._deregisterNode(element, id);
10491
10812
 
10813
+ // Deregister UUID class from node cache
10814
+ var elemUuidMatch = element.className && element.className.match(_UUID_RE);
10815
+ if (elemUuidMatch) delete bw._nodeMap[elemUuidMatch[0]];
10816
+
10492
10817
  // Clean up pub/sub subscriptions tied to element itself
10493
10818
  if (element._bw_subs) {
10494
10819
  element._bw_subs.forEach(function (unsub) {
@@ -11105,7 +11430,7 @@
11105
11430
  willMount: o.willMount || null,
11106
11431
  mounted: o.mounted || null,
11107
11432
  willUpdate: o.willUpdate || null,
11108
- onUpdate: o.onUpdate || null,
11433
+ onUpdate: o.onUpdate || o.updated || null,
11109
11434
  unmount: o.unmount || null,
11110
11435
  willDestroy: o.willDestroy || null
11111
11436
  };
@@ -12090,7 +12415,7 @@
12090
12415
  * and calls the named method. This is the bitwrench equivalent of
12091
12416
  * Win32 SendMessage(hwnd, msg, wParam, lParam).
12092
12417
  *
12093
- * @param {string} target - Component UUID (data-bw_comp_id) or user tag (CSS class)
12418
+ * @param {string} target - Component UUID (bw_uuid_*), comp ID (data-bw_comp_id), or user tag (CSS class)
12094
12419
  * @param {string} action - Method name to call on the component
12095
12420
  * @param {*} data - Data to pass to the method
12096
12421
  * @returns {boolean} True if message was dispatched successfully
@@ -12107,9 +12432,14 @@
12107
12432
  * };
12108
12433
  */
12109
12434
  bw.message = function (target, action, data) {
12110
- // Try data-bw_comp_id attribute first, then CSS class (user tag)
12111
- var el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
12112
- if (!el) {
12435
+ // Try bw._el() first (handles UUID class, nodeMap cache, getElementById)
12436
+ var el = bw._el(target);
12437
+ // Then try data-bw_comp_id attribute
12438
+ if (!el || !el._bwComponentHandle) {
12439
+ el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
12440
+ }
12441
+ // Then try CSS class (user tag)
12442
+ if (!el || !el._bwComponentHandle) {
12113
12443
  el = bw.$('.' + target)[0];
12114
12444
  }
12115
12445
  if (!el || !el._bwComponentHandle) return false;
@@ -12123,61 +12453,24 @@
12123
12453
  };
12124
12454
 
12125
12455
  // ===================================================================================
12126
- // bw.clientApply() / bw.clientConnect() — Server-driven UI protocol
12456
+ // bw.apply() / bw.parseJSONFlex() — Server-driven UI protocol
12127
12457
  // ===================================================================================
12128
12458
 
12129
12459
  /**
12130
12460
  * Registry of named functions sent via register messages.
12131
- * Populated by clientApply({ type: 'register', name, body }).
12132
- * Invoked by clientApply({ type: 'call', name, args }).
12461
+ * Populated by bw.apply({ type: 'register', name, body }).
12462
+ * Invoked by bw.apply({ type: 'call', name, args }).
12133
12463
  * @private
12134
12464
  */
12135
12465
  bw._clientFunctions = {};
12136
12466
 
12137
12467
  /**
12138
- * Whether exec messages are allowed. Set by clientConnect opts.allowExec.
12468
+ * Whether exec messages are allowed. Set by bwclient connect opts.allowExec.
12139
12469
  * Default false — exec messages are rejected unless explicitly opted in.
12140
12470
  * @private
12141
12471
  */
12142
12472
  bw._allowExec = false;
12143
12473
 
12144
- /**
12145
- * Built-in client functions available via call() without registration.
12146
- * @private
12147
- */
12148
- bw._builtinClientFunctions = {
12149
- scrollTo: function scrollTo(selector) {
12150
- var el = bw._el(selector);
12151
- if (el) el.scrollTop = el.scrollHeight;
12152
- },
12153
- focus: function focus(selector) {
12154
- var el = bw._el(selector);
12155
- if (el && _is(el.focus, 'function')) el.focus();
12156
- },
12157
- download: function download(filename, content, mimeType) {
12158
- if (typeof document === 'undefined') return;
12159
- var blob = new Blob([content], {
12160
- type: mimeType || 'text/plain'
12161
- });
12162
- var a = document.createElement('a');
12163
- a.href = URL.createObjectURL(blob);
12164
- a.download = filename;
12165
- a.click();
12166
- URL.revokeObjectURL(a.href);
12167
- },
12168
- clipboard: function clipboard(text) {
12169
- if (typeof navigator !== 'undefined' && navigator.clipboard) {
12170
- navigator.clipboard.writeText(text);
12171
- }
12172
- },
12173
- redirect: function redirect(url) {
12174
- if (typeof window !== 'undefined') window.location.href = url;
12175
- },
12176
- log: function log() {
12177
- console.log.apply(console, arguments);
12178
- }
12179
- };
12180
-
12181
12474
  /**
12182
12475
  * Parse a bwserve protocol message string, supporting both strict JSON
12183
12476
  * and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
@@ -12192,9 +12485,9 @@
12192
12485
  * @param {string} str - JSON or r-prefixed relaxed JSON string
12193
12486
  * @returns {Object} Parsed message object
12194
12487
  * @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
12195
- * @category Server
12488
+ * @category Core
12196
12489
  */
12197
- bw.clientParse = function (str) {
12490
+ bw.parseJSONFlex = function (str) {
12198
12491
  str = (str || '').trim();
12199
12492
  if (str.charAt(0) !== 'r') return JSON.parse(str);
12200
12493
  str = str.slice(1);
@@ -12272,10 +12565,10 @@
12272
12565
  * append — target.appendChild(bw.createDOM(node))
12273
12566
  * remove — bw.cleanup(target); target.remove()
12274
12567
  * patch — bw.patch(target, content, attr)
12275
- * batch — iterate ops, call clientApply for each
12568
+ * batch — iterate ops, call bw.apply for each
12276
12569
  * message — bw.message(target, action, data)
12277
12570
  * register — store a named function for later call()
12278
- * call — invoke a registered or built-in function
12571
+ * call — invoke a registered function
12279
12572
  * exec — execute arbitrary JS (requires allowExec)
12280
12573
  *
12281
12574
  * Target resolution:
@@ -12284,9 +12577,9 @@
12284
12577
  *
12285
12578
  * @param {Object} msg - Protocol message
12286
12579
  * @returns {boolean} true if the message was applied successfully
12287
- * @category Server
12580
+ * @category Core
12288
12581
  */
12289
- bw.clientApply = function (msg) {
12582
+ bw.apply = function (msg) {
12290
12583
  if (!msg || !msg.type) return false;
12291
12584
  var type = msg.type;
12292
12585
  var target = msg.target;
@@ -12314,7 +12607,7 @@
12314
12607
  if (!_isA(msg.ops)) return false;
12315
12608
  var allOk = true;
12316
12609
  msg.ops.forEach(function (op) {
12317
- if (!bw.clientApply(op)) allOk = false;
12610
+ if (!bw.apply(op)) allOk = false;
12318
12611
  });
12319
12612
  return allOk;
12320
12613
  } else if (type === 'message') {
@@ -12330,7 +12623,7 @@
12330
12623
  }
12331
12624
  } else if (type === 'call') {
12332
12625
  if (!msg.name) return false;
12333
- var fn = bw._clientFunctions[msg.name] || bw._builtinClientFunctions[msg.name];
12626
+ var fn = bw._clientFunctions[msg.name];
12334
12627
  if (!_is(fn, 'function')) return false;
12335
12628
  try {
12336
12629
  var args = _isA(msg.args) ? msg.args : [];
@@ -12357,141 +12650,6 @@
12357
12650
  return false;
12358
12651
  };
12359
12652
 
12360
- /**
12361
- * Connect to a bwserve SSE endpoint and apply protocol messages automatically.
12362
- *
12363
- * Returns a connection object with sendAction(), on(), and close() methods.
12364
- *
12365
- * @param {string} url - SSE endpoint URL (e.g., '/__bw/events/client-1')
12366
- * @param {Object} [opts] - Connection options
12367
- * @param {string} [opts.transport='sse'] - Transport type: 'sse' (default) or 'poll'
12368
- * @param {number} [opts.interval=2000] - Poll interval in ms (only for 'poll' transport)
12369
- * @param {string} [opts.actionUrl] - POST endpoint for actions (default: derived from url)
12370
- * @param {boolean} [opts.reconnect=true] - Auto-reconnect on disconnect
12371
- * @param {boolean} [opts.allowExec=false] - Enable exec message type (arbitrary JS execution)
12372
- * @param {Function} [opts.onStatus] - Status callback: 'connecting'|'connected'|'disconnected'
12373
- * @param {Function} [opts.onMessage] - Raw message callback (before clientApply)
12374
- * @returns {Object} Connection object { sendAction, on, close, status }
12375
- * @category Server
12376
- */
12377
- bw.clientConnect = function (url, opts) {
12378
- opts = opts || {};
12379
- var transport = opts.transport || 'sse';
12380
- var actionUrl = opts.actionUrl || url.replace(/\/events\//, '/action/');
12381
- var reconnect = opts.reconnect !== false;
12382
- var onStatus = opts.onStatus || function () {};
12383
- var onMessage = opts.onMessage || null;
12384
- var handlers = {};
12385
- // Set the global allowExec flag from connection options
12386
- bw._allowExec = !!opts.allowExec;
12387
- var conn = {
12388
- status: 'connecting',
12389
- _es: null,
12390
- _pollTimer: null
12391
- };
12392
- function setStatus(s) {
12393
- conn.status = s;
12394
- onStatus(s);
12395
- }
12396
- function handleMessage(data) {
12397
- try {
12398
- var msg = _is(data, 'string') ? bw.clientParse(data) : data;
12399
- if (onMessage) onMessage(msg);
12400
- if (handlers.message) handlers.message(msg);
12401
- bw.clientApply(msg);
12402
- } catch (e) {
12403
- if (handlers.error) handlers.error(e);
12404
- }
12405
- }
12406
- if (transport === 'sse' && typeof EventSource !== 'undefined') {
12407
- setStatus('connecting');
12408
- var es = new EventSource(url);
12409
- conn._es = es;
12410
- es.onopen = function () {
12411
- setStatus('connected');
12412
- if (handlers.open) handlers.open();
12413
- };
12414
- es.onmessage = function (e) {
12415
- handleMessage(e.data);
12416
- };
12417
- es.onerror = function () {
12418
- if (conn.status === 'connected') {
12419
- setStatus('disconnected');
12420
- }
12421
- if (handlers.error) handlers.error(new Error('SSE connection error'));
12422
- if (!reconnect) {
12423
- es.close();
12424
- }
12425
- // EventSource auto-reconnects by default when reconnect=true
12426
- };
12427
- } else if (transport === 'poll') {
12428
- var interval = opts.interval || 2000;
12429
- setStatus('connected');
12430
- conn._pollTimer = setInterval(function () {
12431
- fetch(url).then(function (r) {
12432
- return r.json();
12433
- }).then(function (msgs) {
12434
- if (_isA(msgs)) {
12435
- msgs.forEach(handleMessage);
12436
- } else if (msgs && msgs.type) {
12437
- handleMessage(msgs);
12438
- }
12439
- })["catch"](function (e) {
12440
- if (handlers.error) handlers.error(e);
12441
- });
12442
- }, interval);
12443
- }
12444
-
12445
- /**
12446
- * Send an action to the server via POST.
12447
- * @param {string} action - Action name
12448
- * @param {Object} [data] - Action payload
12449
- */
12450
- conn.sendAction = function (action, data) {
12451
- var body = JSON.stringify({
12452
- type: 'action',
12453
- action: action,
12454
- data: data || {}
12455
- });
12456
- fetch(actionUrl, {
12457
- method: 'POST',
12458
- headers: {
12459
- 'Content-Type': 'application/json'
12460
- },
12461
- body: body
12462
- })["catch"](function (e) {
12463
- if (handlers.error) handlers.error(e);
12464
- });
12465
- };
12466
-
12467
- /**
12468
- * Register an event handler.
12469
- * @param {string} event - 'open'|'message'|'error'|'close'
12470
- * @param {Function} handler
12471
- */
12472
- conn.on = function (event, handler) {
12473
- handlers[event] = handler;
12474
- return conn;
12475
- };
12476
-
12477
- /**
12478
- * Close the connection.
12479
- */
12480
- conn.close = function () {
12481
- if (conn._es) {
12482
- conn._es.close();
12483
- conn._es = null;
12484
- }
12485
- if (conn._pollTimer) {
12486
- clearInterval(conn._pollTimer);
12487
- conn._pollTimer = null;
12488
- }
12489
- setStatus('disconnected');
12490
- if (handlers.close) handlers.close();
12491
- };
12492
- return conn;
12493
- };
12494
-
12495
12653
  // ===================================================================================
12496
12654
  // bw.inspect() — Debug utility
12497
12655
  // ===================================================================================
@@ -12720,7 +12878,7 @@
12720
12878
  * @returns {Element} The style element
12721
12879
  * @category CSS & Styling
12722
12880
  * @see bw.css
12723
- * @see bw.loadDefaultStyles
12881
+ * @see bw.loadStyles
12724
12882
  * @example
12725
12883
  * bw.injectCSS('.my-class { color: red; }');
12726
12884
  * bw.injectCSS({ '.card': { padding: '1rem' } }, { id: 'card-styles' });
@@ -12766,9 +12924,8 @@
12766
12924
  * @param {...Object} styles - Style objects to merge (left-to-right)
12767
12925
  * @returns {Object} Merged style object
12768
12926
  * @category CSS & Styling
12769
- * @see bw.u
12770
12927
  * @example
12771
- * var style = bw.s(bw.u.flex, bw.u.gap4, { color: 'red' });
12928
+ * var style = bw.s({ display: 'flex' }, { gap: '1rem' }, { color: 'red' });
12772
12929
  * // => { display: 'flex', gap: '1rem', color: 'red' }
12773
12930
  */
12774
12931
  bw.s = function () {
@@ -12780,216 +12937,6 @@
12780
12937
  return result;
12781
12938
  };
12782
12939
 
12783
- /**
12784
- * Pre-built CSS utility objects (like Tailwind utilities, but in JS).
12785
- *
12786
- * Compose with `bw.s()` to build inline styles without writing raw CSS strings.
12787
- * Includes flex, padding, margin, typography, color, border, and transition utilities.
12788
- *
12789
- * @category CSS & Styling
12790
- * @see bw.s
12791
- * @example
12792
- * { t: 'div', a: { style: bw.s(bw.u.flex, bw.u.gap4, bw.u.p4) },
12793
- * c: 'Flexbox with 1rem gap and padding' }
12794
- */
12795
- bw.u = {
12796
- // Display
12797
- flex: {
12798
- display: 'flex'
12799
- },
12800
- flexCol: {
12801
- display: 'flex',
12802
- flexDirection: 'column'
12803
- },
12804
- flexRow: {
12805
- display: 'flex',
12806
- flexDirection: 'row'
12807
- },
12808
- flexWrap: {
12809
- display: 'flex',
12810
- flexWrap: 'wrap'
12811
- },
12812
- block: {
12813
- display: 'block'
12814
- },
12815
- inline: {
12816
- display: 'inline'
12817
- },
12818
- hidden: {
12819
- display: 'none'
12820
- },
12821
- // Flex alignment
12822
- justifyCenter: {
12823
- justifyContent: 'center'
12824
- },
12825
- justifyBetween: {
12826
- justifyContent: 'space-between'
12827
- },
12828
- justifyEnd: {
12829
- justifyContent: 'flex-end'
12830
- },
12831
- alignCenter: {
12832
- alignItems: 'center'
12833
- },
12834
- alignStart: {
12835
- alignItems: 'flex-start'
12836
- },
12837
- alignEnd: {
12838
- alignItems: 'flex-end'
12839
- },
12840
- // Gap (0.25rem increments)
12841
- gap1: {
12842
- gap: '0.25rem'
12843
- },
12844
- gap2: {
12845
- gap: '0.5rem'
12846
- },
12847
- gap3: {
12848
- gap: '0.75rem'
12849
- },
12850
- gap4: {
12851
- gap: '1rem'
12852
- },
12853
- gap6: {
12854
- gap: '1.5rem'
12855
- },
12856
- gap8: {
12857
- gap: '2rem'
12858
- },
12859
- // Padding
12860
- p0: {
12861
- padding: '0'
12862
- },
12863
- p1: {
12864
- padding: '0.25rem'
12865
- },
12866
- p2: {
12867
- padding: '0.5rem'
12868
- },
12869
- p3: {
12870
- padding: '0.75rem'
12871
- },
12872
- p4: {
12873
- padding: '1rem'
12874
- },
12875
- p6: {
12876
- padding: '1.5rem'
12877
- },
12878
- p8: {
12879
- padding: '2rem'
12880
- },
12881
- px4: {
12882
- paddingLeft: '1rem',
12883
- paddingRight: '1rem'
12884
- },
12885
- py2: {
12886
- paddingTop: '0.5rem',
12887
- paddingBottom: '0.5rem'
12888
- },
12889
- py4: {
12890
- paddingTop: '1rem',
12891
- paddingBottom: '1rem'
12892
- },
12893
- // Margin (same scale)
12894
- m0: {
12895
- margin: '0'
12896
- },
12897
- m4: {
12898
- margin: '1rem'
12899
- },
12900
- mt2: {
12901
- marginTop: '0.5rem'
12902
- },
12903
- mt4: {
12904
- marginTop: '1rem'
12905
- },
12906
- mb2: {
12907
- marginBottom: '0.5rem'
12908
- },
12909
- mb4: {
12910
- marginBottom: '1rem'
12911
- },
12912
- mx_auto: {
12913
- marginLeft: 'auto',
12914
- marginRight: 'auto'
12915
- },
12916
- // Typography
12917
- textSm: {
12918
- fontSize: '0.875rem'
12919
- },
12920
- textBase: {
12921
- fontSize: '1rem'
12922
- },
12923
- textLg: {
12924
- fontSize: '1.125rem'
12925
- },
12926
- textXl: {
12927
- fontSize: '1.25rem'
12928
- },
12929
- text2xl: {
12930
- fontSize: '1.5rem'
12931
- },
12932
- text3xl: {
12933
- fontSize: '1.875rem'
12934
- },
12935
- bold: {
12936
- fontWeight: '700'
12937
- },
12938
- semibold: {
12939
- fontWeight: '600'
12940
- },
12941
- italic: {
12942
- fontStyle: 'italic'
12943
- },
12944
- textCenter: {
12945
- textAlign: 'center'
12946
- },
12947
- textRight: {
12948
- textAlign: 'right'
12949
- },
12950
- // Colors (from design tokens)
12951
- bgWhite: {
12952
- background: '#ffffff'
12953
- },
12954
- bgTeal: {
12955
- background: '#006666',
12956
- color: '#ffffff'
12957
- },
12958
- textWhite: {
12959
- color: '#ffffff'
12960
- },
12961
- textTeal: {
12962
- color: '#006666'
12963
- },
12964
- textMuted: {
12965
- color: '#888'
12966
- },
12967
- // Borders
12968
- rounded: {
12969
- borderRadius: '0.375rem'
12970
- },
12971
- roundedLg: {
12972
- borderRadius: '0.5rem'
12973
- },
12974
- roundedFull: {
12975
- borderRadius: '9999px'
12976
- },
12977
- border: {
12978
- border: '1px solid #d8d8d8'
12979
- },
12980
- // Sizing
12981
- wFull: {
12982
- width: '100%'
12983
- },
12984
- hFull: {
12985
- height: '100%'
12986
- },
12987
- // Transitions
12988
- transition: {
12989
- transition: 'all 0.2s ease'
12990
- }
12991
- };
12992
-
12993
12940
  /**
12994
12941
  * Generate responsive CSS with media query breakpoints.
12995
12942
  *
@@ -13115,111 +13062,48 @@
13115
13062
  };
13116
13063
  }
13117
13064
 
13065
+ // =========================================================================
13066
+ // v2.0.18 Clean Styles API — makeStyles / applyStyles / loadStyles / etc.
13067
+ // =========================================================================
13068
+
13118
13069
  /**
13119
- * Load the built-in Bootstrap-inspired default stylesheet.
13120
- *
13121
- * Injects bitwrench's batteries-included CSS (buttons, cards, grids, forms,
13122
- * alerts, badges, nav, tabs, etc.) into the document head. Call once at app startup.
13123
- * Returns null in Node.js (no DOM).
13124
- *
13125
- * @param {Object} [options] - Style loading options
13126
- * @param {boolean} [options.minify=true] - Minify the CSS output
13127
- * @returns {Element|null} Style element if in browser, null in Node.js
13128
- * @category CSS & Styling
13129
- * @see bw.setTheme
13130
- * @see bw.applyTheme
13131
- * @see bw.toggleTheme
13132
- * @example
13133
- * bw.loadDefaultStyles(); // inject all default CSS
13070
+ * Convert a scope selector to a <style> element id.
13071
+ * @private
13072
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview')
13073
+ * @returns {string} Style element id (e.g. 'bw_style_my_dashboard')
13134
13074
  */
13135
- bw.loadDefaultStyles = function () {
13136
- var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
13137
- var _options$minify2 = options.minify,
13138
- minify = _options$minify2 === void 0 ? true : _options$minify2,
13139
- palette = options.palette;
13140
-
13141
- // 1. Inject structural CSS (layout, sizing — never changes with theme)
13142
- if (bw._isBrowser) {
13143
- var structuralCSS = bw.css(getStructuralStyles());
13144
- bw.injectCSS(structuralCSS, {
13145
- id: 'bw_structural',
13146
- append: false,
13147
- minify: minify
13148
- });
13149
- }
13150
-
13151
- // 2. Inject cosmetic CSS via generateTheme (colors, shadows, radii)
13152
- var paletteConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, palette || {});
13153
- var result = bw.generateTheme('', Object.assign({}, paletteConfig, {
13154
- inject: true
13155
- }));
13156
- return result;
13157
- };
13075
+ function _scopeToStyleId(scope) {
13076
+ if (!scope || scope === '' || scope === 'global') return 'bw_style_global';
13077
+ if (scope === 'reset') return 'bw_style_reset';
13078
+ // Strip leading # or . and convert - to _
13079
+ var clean = scope.replace(/^[#.]/, '').replace(/-/g, '_');
13080
+ return 'bw_style_' + clean;
13081
+ }
13158
13082
 
13159
13083
  /**
13160
- * Generate a complete, scoped theme from seed colors.
13161
- *
13162
- * Produces CSS for all themed components (buttons, alerts, badges, cards,
13163
- * forms, nav, tables, tabs, list groups, pagination, progress, hero, utilities)
13164
- * scoped under `.name` class. Multiple themes can coexist in the stylesheet.
13165
- * Swap themes by changing the class on a container element.
13166
- *
13167
- * @param {string} name - CSS scope class (e.g. 'ocean'). Empty string = unscoped global.
13168
- * @param {Object} config - Theme configuration
13169
- * @param {string} config.primary - Primary brand color hex
13170
- * @param {string} config.secondary - Secondary color hex
13171
- * @param {string} [config.tertiary] - Tertiary/accent color hex (defaults to primary)
13172
- * @param {string} [config.success='#198754'] - Success color hex
13173
- * @param {string} [config.danger='#dc3545'] - Danger color hex
13174
- * @param {string} [config.warning='#ffc107'] - Warning color hex
13175
- * @param {string} [config.info='#0dcaf0'] - Info color hex
13176
- * @param {string} [config.light='#f8f9fa'] - Light color hex
13177
- * @param {string} [config.dark='#212529'] - Dark color hex
13178
- * @param {string} [config.background] - Page background hex (default: '#ffffff' light, derived dark)
13179
- * @param {string} [config.surface] - Surface/card background hex (default: '#f8f9fa' light, derived dark)
13084
+ * Generate a complete styles object from seed colors and layout config.
13085
+ * Pure function — no DOM, no state, no side effects.
13086
+ *
13087
+ * All parameters are optional. Defaults to the bitwrench default palette.
13088
+ *
13089
+ * @param {Object} [config] - Style configuration
13090
+ * @param {string} [config.primary='#006666'] - Primary brand color hex
13091
+ * @param {string} [config.secondary='#6c757d'] - Secondary color hex
13092
+ * @param {string} [config.tertiary] - Tertiary color hex (defaults to primary)
13180
13093
  * @param {string} [config.spacing='normal'] - 'compact' | 'normal' | 'spacious'
13181
13094
  * @param {string} [config.radius='md'] - 'none' | 'sm' | 'md' | 'lg' | 'pill'
13182
- * @param {number} [config.fontSize=1.0] - Base font size scale factor
13183
- * @param {string|number} [config.typeRatio='normal'] - 'tight' | 'normal' | 'relaxed' | 'dramatic' or a number
13184
- * @param {string} [config.elevation='md'] - 'flat' | 'sm' | 'md' | 'lg'
13185
- * @param {string} [config.motion='standard'] - 'reduced' | 'standard' | 'expressive'
13186
- * @param {number} [config.harmonize=0.20] - 0-1, semantic color hue shift toward primary
13187
- * @param {boolean} [config.inject=true] - Inject into DOM (browser only)
13188
- * @returns {Object} { css, palette, name, isLightPrimary, alternate: { css, palette } }
13095
+ * @returns {Object} { css, alternateCss, rules, alternateRules, palette, alternatePalette, isLightPrimary }
13189
13096
  * @category CSS & Styling
13190
- * @see bw.applyTheme
13191
- * @see bw.toggleTheme
13192
- * @see bw.loadDefaultStyles
13097
+ * @see bw.applyStyles
13098
+ * @see bw.loadStyles
13193
13099
  * @example
13194
- * // Generate and inject an ocean theme (primary + alternate)
13195
- * var theme = bw.generateTheme('ocean', {
13196
- * primary: '#0077b6',
13197
- * secondary: '#90e0ef',
13198
- * tertiary: '#00b4d8'
13199
- * });
13200
- *
13201
- * // Apply to a container
13202
- * document.getElementById('app').classList.add('ocean');
13203
- *
13204
- * // Toggle to alternate palette
13205
- * bw.toggleTheme();
13206
- *
13207
- * // Generate CSS for static export (Node.js)
13208
- * var result = bw.generateTheme('sunset', {
13209
- * primary: '#e76f51',
13210
- * secondary: '#264653',
13211
- * inject: false
13212
- * });
13213
- * fs.writeFileSync('sunset.css', result.css + result.alternate.css);
13100
+ * var styles = bw.makeStyles({ primary: '#4f46e5', secondary: '#d97706' });
13101
+ * console.log(styles.palette.primary.base); // '#4f46e5'
13102
+ * // styles.css contains all themed CSS — nothing injected
13214
13103
  */
13215
- bw.generateTheme = function (name, config) {
13216
- if (!config || !config.primary || !config.secondary) {
13217
- throw new Error('bw.generateTheme requires config.primary and config.secondary');
13218
- }
13219
-
13220
- // Merge with defaults; if user didn't supply tertiary, default to their primary
13221
- var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config);
13222
- if (!config.tertiary) fullConfig.tertiary = fullConfig.primary;
13104
+ bw.makeStyles = function (config) {
13105
+ var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config || {});
13106
+ if (config && !config.tertiary) fullConfig.tertiary = fullConfig.primary;
13223
13107
 
13224
13108
  // Derive primary palette
13225
13109
  var palette = derivePalette(fullConfig);
@@ -13227,136 +13111,211 @@
13227
13111
  // Resolve layout
13228
13112
  var layout = resolveLayout(fullConfig);
13229
13113
 
13230
- // Generate primary themed CSS rules
13231
- var themedRules = generateThemedCSS(name, palette, layout);
13114
+ // Generate primary themed CSS rules (unscoped)
13115
+ var themedRules = generateThemedCSS('', palette, layout);
13232
13116
  var cssStr = bw.css(themedRules);
13233
13117
 
13234
13118
  // Derive alternate palette (luminance-inverted)
13235
13119
  var altConfig = deriveAlternateConfig(fullConfig);
13236
13120
  var altPalette = derivePalette(altConfig);
13237
13121
 
13238
- // Generate alternate CSS scoped under .bw_theme_alt
13239
- var altRules = generateAlternateCSS(name, altPalette, layout);
13240
- var altCssStr = bw.css(altRules);
13122
+ // Generate alternate CSS rules WITHOUT .bw_theme_alt prefix (raw rules)
13123
+ // applyStyles() wraps them appropriately based on scope
13124
+ var altRawRules = generateThemedCSS('', altPalette, layout);
13125
+
13126
+ // Add body-level surface overrides for the alternate palette.
13127
+ // When .bw_theme_alt is on <html>, ".bw_theme_alt body" correctly matches.
13128
+ altRawRules['body'] = {
13129
+ 'color': altPalette.dark.base,
13130
+ 'background-color': altPalette.surface || altPalette.light.base
13131
+ };
13132
+ var altCssStr = bw.css(altRawRules);
13241
13133
 
13242
13134
  // Determine if primary is light-flavored
13243
13135
  var lightPrimary = isLightPalette(fullConfig);
13136
+ return {
13137
+ css: cssStr,
13138
+ alternateCss: altCssStr,
13139
+ rules: themedRules,
13140
+ alternateRules: altRawRules,
13141
+ palette: palette,
13142
+ alternatePalette: altPalette,
13143
+ isLightPrimary: lightPrimary
13144
+ };
13145
+ };
13244
13146
 
13245
- // Inject both CSS sets into DOM if requested
13246
- var shouldInject = config.inject !== false;
13247
- if (shouldInject && bw._isBrowser) {
13248
- var safeName = name ? name.replace(/-/g, '_') : '';
13249
- var styleId = safeName ? 'bw_theme_' + safeName : 'bw_theme_default';
13250
- var altStyleId = safeName ? 'bw_theme_' + safeName + '_alt' : 'bw_theme_default_alt';
13251
- bw.injectCSS(cssStr, {
13252
- id: styleId,
13253
- append: false
13254
- });
13255
- bw.injectCSS(altCssStr, {
13256
- id: altStyleId,
13257
- append: false
13258
- });
13259
- bw._activeThemeStyleIds = [styleId, altStyleId];
13147
+ /**
13148
+ * Inject styles into the DOM with optional scoping.
13149
+ *
13150
+ * Takes a styles object from `makeStyles()` and creates a single `<style>`
13151
+ * element in `<head>`. If a scope selector is provided, all CSS rules are
13152
+ * wrapped under that selector. Alternate CSS is wrapped under `.bw_theme_alt`.
13153
+ *
13154
+ * @param {Object} styles - Result of `bw.makeStyles()`
13155
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview'). Omit for global.
13156
+ * @returns {Element|null} The `<style>` element, or null in Node.js
13157
+ * @category CSS & Styling
13158
+ * @see bw.makeStyles
13159
+ * @see bw.loadStyles
13160
+ * @see bw.clearStyles
13161
+ * @example
13162
+ * var styles = bw.makeStyles({ primary: '#4f46e5' });
13163
+ * bw.applyStyles(styles); // global
13164
+ * bw.applyStyles(styles, '#my-dashboard'); // scoped
13165
+ */
13166
+ bw.applyStyles = function (styles, scope) {
13167
+ if (!bw._isBrowser) return null;
13168
+ if (!styles || !styles.rules) {
13169
+ _cw('bw.applyStyles: invalid styles object');
13170
+ return null;
13260
13171
  }
13172
+ var styleId = _scopeToStyleId(scope);
13261
13173
 
13262
- // Update bw.u color entries to reflect the palette
13263
- if (!name) {
13264
- bw.u.bgTeal = {
13265
- background: palette.primary.base,
13266
- color: palette.primary.textOn
13267
- };
13268
- bw.u.textTeal = {
13269
- color: palette.primary.base
13270
- };
13271
- bw.u.bgWhite = {
13272
- background: '#ffffff'
13273
- };
13274
- bw.u.textWhite = {
13275
- color: '#ffffff'
13276
- };
13174
+ // Scope the primary rules if a scope is provided
13175
+ var primaryRules = styles.rules;
13176
+ if (scope) {
13177
+ primaryRules = scopeRulesUnder(primaryRules, scope);
13277
13178
  }
13278
13179
 
13279
- // Store active theme state
13280
- var result = {
13281
- css: cssStr,
13282
- palette: palette,
13283
- name: name,
13284
- isLightPrimary: lightPrimary,
13285
- alternate: {
13286
- css: altCssStr,
13287
- palette: altPalette
13180
+ // Wrap alternate rules with .bw_theme_alt
13181
+ var altRules = styles.alternateRules;
13182
+ if (altRules) {
13183
+ if (scope) {
13184
+ // Scoped compound: #scope.bw_theme_alt .bw_card
13185
+ altRules = scopeRulesUnder(altRules, scope + '.bw_theme_alt');
13186
+ } else {
13187
+ // Global: .bw_theme_alt .bw_card
13188
+ altRules = scopeRulesUnder(altRules, '.bw_theme_alt');
13288
13189
  }
13289
- };
13290
- bw._activeTheme = result;
13291
- bw._activeThemeMode = 'primary';
13292
- return result;
13190
+ }
13191
+
13192
+ // Combine primary + alternate into one CSS string
13193
+ var combined = bw.css(primaryRules);
13194
+ if (altRules) {
13195
+ combined += '\n' + bw.css(altRules);
13196
+ }
13197
+ return bw.injectCSS(combined, {
13198
+ id: styleId,
13199
+ append: false
13200
+ });
13293
13201
  };
13294
13202
 
13295
13203
  /**
13296
- * Apply a theme mode. Switches between primary and alternate palettes
13297
- * by adding/removing the `bw_theme_alt` class on `<html>`.
13204
+ * Generate and apply styles in one call. Convenience wrapper.
13205
+ *
13206
+ * Equivalent to: `bw.applyStyles(bw.makeStyles(config), scope)`
13298
13207
  *
13299
- * @param {string} mode - 'primary' | 'alternate' | 'light' | 'dark'
13300
- * @returns {string} Active mode: 'primary' or 'alternate'
13208
+ * @param {Object} [config] - Style configuration (same as `makeStyles`)
13209
+ * @param {string} [scope] - Scope selector (same as `applyStyles`)
13210
+ * @returns {Element|null} The `<style>` element, or null in Node.js
13301
13211
  * @category CSS & Styling
13302
- * @see bw.generateTheme
13303
- * @see bw.toggleTheme
13212
+ * @see bw.makeStyles
13213
+ * @see bw.applyStyles
13304
13214
  * @example
13305
- * bw.applyTheme('alternate'); // switch to alternate palette
13306
- * bw.applyTheme('dark'); // switch to whichever palette is darker
13307
- * bw.applyTheme('primary'); // switch back to primary palette
13308
- */
13309
- bw.applyTheme = function (mode) {
13310
- if (!bw._isBrowser) return mode || 'primary';
13311
- var root = document.documentElement;
13312
- var isLight = bw._activeTheme ? bw._activeTheme.isLightPrimary : true;
13313
- var wantAlt;
13314
- 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;
13315
- if (wantAlt) {
13316
- root.classList.add('bw_theme_alt');
13317
- } else {
13318
- root.classList.remove('bw_theme_alt');
13215
+ * bw.loadStyles(); // defaults, global
13216
+ * bw.loadStyles({ primary: '#4f46e5' }); // custom, global
13217
+ * bw.loadStyles({ primary: '#4f46e5' }, '#my-dashboard'); // custom, scoped
13218
+ */
13219
+ bw.loadStyles = function (config, scope) {
13220
+ // Also inject structural CSS first (only once)
13221
+ if (bw._isBrowser) {
13222
+ var existing = document.getElementById('bw_structural');
13223
+ if (!existing) {
13224
+ var structuralCSS = bw.css(getStructuralStyles());
13225
+ bw.injectCSS(structuralCSS, {
13226
+ id: 'bw_structural',
13227
+ append: false
13228
+ });
13229
+ }
13319
13230
  }
13320
- bw._activeThemeMode = wantAlt ? 'alternate' : 'primary';
13321
- return bw._activeThemeMode;
13231
+ return bw.applyStyles(bw.makeStyles(config), scope);
13322
13232
  };
13323
13233
 
13324
13234
  /**
13325
- * Toggle between primary and alternate theme palettes.
13235
+ * Inject the CSS reset (box-sizing, html/body font, reduced-motion).
13236
+ * Idempotent — if already injected, returns the existing `<style>` element.
13326
13237
  *
13238
+ * @returns {Element|null} The `<style>` element, or null in Node.js
13239
+ * @category CSS & Styling
13240
+ * @see bw.loadStyles
13241
+ * @see bw.clearStyles
13242
+ * @example
13243
+ * bw.loadReset(); // inject once, safe to call multiple times
13244
+ */
13245
+ bw.loadReset = function () {
13246
+ if (!bw._isBrowser) return null;
13247
+ var existing = document.getElementById('bw_style_reset');
13248
+ if (existing) return existing;
13249
+ return bw.injectCSS(bw.css(getResetStyles()), {
13250
+ id: 'bw_style_reset',
13251
+ append: false
13252
+ });
13253
+ };
13254
+
13255
+ /**
13256
+ * Toggle between primary and alternate palettes.
13257
+ *
13258
+ * Adds/removes the `bw_theme_alt` class on the scoping element.
13259
+ * Without a scope, toggles on `<html>` (global).
13260
+ * With a scope, toggles on the first matching element.
13261
+ *
13262
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard'). Omit for global.
13327
13263
  * @returns {string} Active mode after toggle: 'primary' or 'alternate'
13328
13264
  * @category CSS & Styling
13329
- * @see bw.applyTheme
13330
- * @see bw.generateTheme
13265
+ * @see bw.applyStyles
13266
+ * @see bw.clearStyles
13331
13267
  * @example
13332
- * bw.toggleTheme(); // flip between primary and alternate
13268
+ * bw.toggleStyles(); // global toggle on <html>
13269
+ * bw.toggleStyles('#my-dashboard'); // scoped toggle
13333
13270
  */
13334
- bw.toggleTheme = function () {
13335
- var current = bw._activeThemeMode || 'primary';
13336
- return bw.applyTheme(current === 'primary' ? 'alternate' : 'primary');
13271
+ bw.toggleStyles = function (scope) {
13272
+ if (!bw._isBrowser) return 'primary';
13273
+ var target;
13274
+ if (scope) {
13275
+ var els = bw.$(scope);
13276
+ target = els[0];
13277
+ } else {
13278
+ target = document.documentElement;
13279
+ }
13280
+ if (!target) return 'primary';
13281
+ var hasAlt = target.classList.contains('bw_theme_alt');
13282
+ if (hasAlt) {
13283
+ target.classList.remove('bw_theme_alt');
13284
+ return 'primary';
13285
+ } else {
13286
+ target.classList.add('bw_theme_alt');
13287
+ return 'alternate';
13288
+ }
13337
13289
  };
13338
13290
 
13339
13291
  /**
13340
- * Remove the currently active theme's injected style elements from the DOM.
13341
- * Use this before generating a new theme with a different name to prevent
13342
- * stale CSS accumulation.
13292
+ * Remove injected styles for a given scope.
13343
13293
  *
13294
+ * Finds the `<style>` element by id and removes it. Also removes
13295
+ * the `bw_theme_alt` class from the relevant element.
13296
+ *
13297
+ * @param {string} [scope] - Scope selector. Omit to remove global styles.
13344
13298
  * @category CSS & Styling
13345
- * @see bw.generateTheme
13299
+ * @see bw.applyStyles
13300
+ * @see bw.loadStyles
13346
13301
  * @example
13347
- * bw.clearTheme(); // remove current theme styles
13348
- * bw.generateTheme('sunset', conf); // inject fresh theme
13349
- */
13350
- bw.clearTheme = function () {
13351
- if (bw._activeThemeStyleIds && bw._isBrowser) {
13352
- bw._activeThemeStyleIds.forEach(function (id) {
13353
- var el = document.getElementById(id);
13354
- if (el) el.remove();
13355
- });
13356
- bw._activeThemeStyleIds = null;
13302
+ * bw.clearStyles(); // remove global styles
13303
+ * bw.clearStyles('#my-dashboard'); // remove scoped styles
13304
+ * bw.clearStyles('reset'); // remove the CSS reset
13305
+ */
13306
+ bw.clearStyles = function (scope) {
13307
+ if (!bw._isBrowser) return;
13308
+ var styleId = _scopeToStyleId(scope);
13309
+ var el = document.getElementById(styleId);
13310
+ if (el) el.remove();
13311
+
13312
+ // Also remove bw_theme_alt from the relevant element
13313
+ if (scope && scope !== 'reset' && scope !== 'global') {
13314
+ var targets = bw.$(scope);
13315
+ if (targets[0]) targets[0].classList.remove('bw_theme_alt');
13316
+ } else if (!scope || scope === 'global') {
13317
+ document.documentElement.classList.remove('bw_theme_alt');
13357
13318
  }
13358
- bw._activeTheme = null;
13359
- bw._activeThemeMode = 'primary';
13360
13319
  };
13361
13320
 
13362
13321
  // Expose color utility functions on bw namespace
@@ -13578,10 +13537,15 @@
13578
13537
  * @param {Object} config - Table configuration
13579
13538
  * @param {Array<Object>} config.data - Array of row objects to display
13580
13539
  * @param {Array<Object>} [config.columns] - Column definitions with key, label, render
13581
- * @param {string} [config.className='table'] - CSS class for table element
13540
+ * @param {string} [config.className=''] - Additional CSS classes for table element
13582
13541
  * @param {boolean} [config.sortable=true] - Enable click-to-sort headers
13583
13542
  * @param {Function} [config.onSort] - Sort callback (column, direction)
13584
- * @returns {Object} TACO object for table
13543
+ * @param {boolean} [config.selectable=false] - Enable row selection on click
13544
+ * @param {Function} [config.onRowClick] - Row click callback (row, index, event)
13545
+ * @param {number} [config.pageSize] - Rows per page (enables pagination when set)
13546
+ * @param {number} [config.currentPage=1] - Current page number (1-based)
13547
+ * @param {Function} [config.onPageChange] - Page change callback (newPage)
13548
+ * @returns {Object} TACO object for table (with optional pagination controls)
13585
13549
  * @category Component Builders
13586
13550
  * @see bw.makeDataTable
13587
13551
  * @example
@@ -13593,7 +13557,12 @@
13593
13557
  * columns: [
13594
13558
  * { key: 'name', label: 'Name' },
13595
13559
  * { key: 'age', label: 'Age' }
13596
- * ]
13560
+ * ],
13561
+ * selectable: true,
13562
+ * onRowClick: function(row, i) { console.log('clicked', row.name); },
13563
+ * pageSize: 10,
13564
+ * currentPage: 1,
13565
+ * onPageChange: function(page) { console.log('page', page); }
13597
13566
  * });
13598
13567
  */
13599
13568
  bw.makeTable = function (config) {
@@ -13611,12 +13580,20 @@
13611
13580
  onSort = config.onSort,
13612
13581
  sortColumn = config.sortColumn,
13613
13582
  _config$sortDirection = config.sortDirection,
13614
- sortDirection = _config$sortDirection === void 0 ? 'asc' : _config$sortDirection;
13615
-
13616
- // Build class list: always include bw_table, add striped/hover, append user className
13583
+ sortDirection = _config$sortDirection === void 0 ? 'asc' : _config$sortDirection,
13584
+ _config$selectable = config.selectable,
13585
+ selectable = _config$selectable === void 0 ? false : _config$selectable,
13586
+ onRowClick = config.onRowClick,
13587
+ pageSize = config.pageSize,
13588
+ _config$currentPage = config.currentPage,
13589
+ currentPage = _config$currentPage === void 0 ? 1 : _config$currentPage,
13590
+ onPageChange = config.onPageChange;
13591
+
13592
+ // Build class list: always include bw_table, add striped/hover/selectable, append user className
13617
13593
  var cls = 'bw_table';
13618
13594
  if (striped) cls += ' bw_table_striped';
13619
- if (hover) cls += ' bw_table_hover';
13595
+ if (hover || selectable) cls += ' bw_table_hover';
13596
+ if (selectable) cls += ' bw_table_selectable';
13620
13597
  if (className) cls += ' ' + className;
13621
13598
  cls = cls.trim();
13622
13599
 
@@ -13655,6 +13632,15 @@
13655
13632
  });
13656
13633
  }
13657
13634
 
13635
+ // Pagination
13636
+ var totalRows = sortedData.length;
13637
+ var totalPages = pageSize ? Math.max(1, Math.ceil(totalRows / pageSize)) : 1;
13638
+ var page = Math.max(1, Math.min(currentPage, totalPages));
13639
+ if (pageSize) {
13640
+ var start = (page - 1) * pageSize;
13641
+ sortedData = sortedData.slice(start, start + pageSize);
13642
+ }
13643
+
13658
13644
  // Create sort handler
13659
13645
  var handleSort = function handleSort(column) {
13660
13646
  if (!sortable) return;
@@ -13700,12 +13686,28 @@
13700
13686
  }
13701
13687
  };
13702
13688
 
13703
- // Build table body
13689
+ // Build table body with selectable/onRowClick support
13704
13690
  var tbody = {
13705
13691
  t: 'tbody',
13706
- c: sortedData.map(function (row) {
13692
+ c: sortedData.map(function (row, idx) {
13693
+ var globalIdx = pageSize ? (page - 1) * pageSize + idx : idx;
13694
+ var rowAttrs = {};
13695
+ if (selectable || onRowClick) {
13696
+ rowAttrs.style = 'cursor:pointer;';
13697
+ rowAttrs.onclick = function (e) {
13698
+ if (selectable) {
13699
+ // Toggle selected class on this row
13700
+ var tr = e.currentTarget;
13701
+ tr.classList.toggle('bw_table_row_selected');
13702
+ }
13703
+ if (onRowClick) {
13704
+ onRowClick(row, globalIdx, e);
13705
+ }
13706
+ };
13707
+ }
13707
13708
  return {
13708
13709
  t: 'tr',
13710
+ a: rowAttrs,
13709
13711
  c: cols.map(function (col) {
13710
13712
  return {
13711
13713
  t: 'td',
@@ -13715,13 +13717,65 @@
13715
13717
  };
13716
13718
  })
13717
13719
  };
13718
- return {
13720
+ var table = {
13719
13721
  t: 'table',
13720
13722
  a: {
13721
13723
  "class": cls
13722
13724
  },
13723
13725
  c: [thead, tbody]
13724
13726
  };
13727
+
13728
+ // If no pagination, return table directly
13729
+ if (!pageSize) return table;
13730
+
13731
+ // Build pagination controls
13732
+ var pageButtons = [];
13733
+ // Previous button
13734
+ pageButtons.push({
13735
+ t: 'button',
13736
+ a: {
13737
+ "class": 'bw_btn bw_btn_sm',
13738
+ disabled: page <= 1 ? 'disabled' : undefined,
13739
+ onclick: page > 1 && onPageChange ? function () {
13740
+ onPageChange(page - 1);
13741
+ } : undefined
13742
+ },
13743
+ c: 'Prev'
13744
+ });
13745
+ // Page info
13746
+ pageButtons.push({
13747
+ t: 'span',
13748
+ a: {
13749
+ style: 'margin:0 0.5rem;font-size:0.875rem;'
13750
+ },
13751
+ c: 'Page ' + page + ' of ' + totalPages
13752
+ });
13753
+ // Next button
13754
+ pageButtons.push({
13755
+ t: 'button',
13756
+ a: {
13757
+ "class": 'bw_btn bw_btn_sm',
13758
+ disabled: page >= totalPages ? 'disabled' : undefined,
13759
+ onclick: page < totalPages && onPageChange ? function () {
13760
+ onPageChange(page + 1);
13761
+ } : undefined
13762
+ },
13763
+ c: 'Next'
13764
+ });
13765
+ return {
13766
+ t: 'div',
13767
+ a: {
13768
+ "class": 'bw_table_paginated'
13769
+ },
13770
+ c: [table, {
13771
+ t: 'div',
13772
+ a: {
13773
+ "class": 'bw_table_pagination',
13774
+ style: 'display:flex;align-items:center;justify-content:flex-end;padding:0.5rem 0;gap:0.25rem;'
13775
+ },
13776
+ c: pageButtons
13777
+ }]
13778
+ };
13725
13779
  };
13726
13780
 
13727
13781
  /**