bitwrench 2.0.16 → 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 (68) hide show
  1. package/README.md +127 -38
  2. package/dist/bitwrench-bccl.cjs.js +13 -9
  3. package/dist/bitwrench-bccl.cjs.min.js +2 -2
  4. package/dist/bitwrench-bccl.esm.js +13 -9
  5. package/dist/bitwrench-bccl.esm.min.js +2 -2
  6. package/dist/bitwrench-bccl.umd.js +13 -9
  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 +1438 -920
  17. package/dist/bitwrench-lean.cjs.min.js +20 -20
  18. package/dist/bitwrench-lean.es5.js +1518 -1105
  19. package/dist/bitwrench-lean.es5.min.js +18 -18
  20. package/dist/bitwrench-lean.esm.js +1437 -920
  21. package/dist/bitwrench-lean.esm.min.js +20 -20
  22. package/dist/bitwrench-lean.umd.js +1438 -920
  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 +1450 -928
  33. package/dist/bitwrench.cjs.min.js +21 -21
  34. package/dist/bitwrench.css +456 -132
  35. package/dist/bitwrench.es5.js +1563 -1140
  36. package/dist/bitwrench.es5.min.js +19 -19
  37. package/dist/bitwrench.esm.js +1450 -929
  38. package/dist/bitwrench.esm.min.js +21 -21
  39. package/dist/bitwrench.min.css +1 -1
  40. package/dist/bitwrench.umd.js +1450 -928
  41. package/dist/bitwrench.umd.min.js +21 -21
  42. package/dist/builds.json +178 -90
  43. package/dist/bwserve.cjs.js +528 -68
  44. package/dist/bwserve.esm.js +527 -69
  45. package/dist/sri.json +44 -36
  46. package/package.json +5 -2
  47. package/readme.html +136 -49
  48. package/src/bitwrench-bccl.js +12 -8
  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 +979 -630
  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 +139 -29
  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/layout-default.js +47 -32
  64. package/src/cli/serve.js +6 -2
  65. package/src/generate-css.js +11 -4
  66. package/src/vendor/html2canvas.min.js +20 -0
  67. package/src/version.js +3 -3
  68. package/src/bwserve/shell.js +0 -103
@@ -1,10 +1,11 @@
1
- /*! bitwrench-lean v2.0.16 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
1
+ /*! bitwrench-lean v2.0.18 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
2
2
  (function (global, factory) {
3
3
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
4
4
  typeof define === 'function' && define.amd ? define(factory) :
5
5
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.bw = factory());
6
6
  })(this, (function () { 'use strict';
7
7
 
8
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
8
9
  function _arrayLikeToArray(r, a) {
9
10
  (null == a || a > r.length) && (a = r.length);
10
11
  for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
@@ -189,14 +190,14 @@
189
190
  */
190
191
 
191
192
  var VERSION_INFO = {
192
- version: '2.0.16',
193
+ version: '2.0.18',
193
194
  name: 'bitwrench',
194
195
  description: 'A library for javascript UI functions.',
195
196
  license: 'BSD-2-Clause',
196
197
  homepage: 'https://deftio.github.com/bitwrench/pages',
197
198
  repository: 'git+https://github.com/deftio/bitwrench.git',
198
199
  author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
199
- buildDate: '2026-03-12T08:05:52.043Z'
200
+ buildDate: '2026-03-17T00:50:09.505Z'
200
201
  };
201
202
 
202
203
  /**
@@ -485,13 +486,16 @@
485
486
  */
486
487
  function deriveShades(hex) {
487
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);
488
492
  return {
489
493
  base: hex,
490
494
  hover: adjustLightness(hex, -10),
491
495
  active: adjustLightness(hex, -15),
492
496
  light: mixColor(hex, '#ffffff', 0.85),
493
497
  darkText: adjustLightness(hex, -40),
494
- border: mixColor(hex, '#ffffff', 0.60),
498
+ border: borderColor,
495
499
  focus: 'rgba(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ',0.25)',
496
500
  textOn: textOnColor(hex)
497
501
  };
@@ -550,18 +554,26 @@
550
554
  alt.secondary = deriveAlternateSeed(config.secondary);
551
555
  alt.tertiary = config.tertiary ? deriveAlternateSeed(config.tertiary) : alt.primary;
552
556
 
553
- // 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).
554
561
  var priHsl = hexToHsl(config.primary);
555
562
  var h = priHsl[0];
556
- var isLight = isLightPalette(config);
563
+ var primarySurface = config.surface || hslToHex([h, 8, 96]);
564
+ var isLight = relativeLuminance(primarySurface) > 0.179;
557
565
  if (isLight) {
558
- // Primary is light → alternate needs dark surfaces
566
+ // Page surface is light → alternate needs dark surfaces
559
567
  alt.light = hslToHex([h, Math.min(priHsl[1], 15), 15]);
560
568
  alt.dark = hslToHex([h, 5, 88]);
569
+ alt.surface = hslToHex([h, 12, 18]);
570
+ alt.background = hslToHex([h, 10, 14]);
561
571
  } else {
562
- // Primary is dark → alternate needs light surfaces
572
+ // Page surface is dark → alternate needs light surfaces
563
573
  alt.light = hslToHex([h, Math.min(priHsl[1], 10), 96]);
564
574
  alt.dark = hslToHex([h, 10, 18]);
575
+ alt.surface = hslToHex([h, 8, 96]);
576
+ alt.background = hslToHex([h, 6, 98]);
565
577
  }
566
578
 
567
579
  // Semantic colors: harmonize toward primary, then invert for alternate
@@ -610,10 +622,16 @@
610
622
  var darkBase = config.dark || hslToHex([h, 10, 13]);
611
623
 
612
624
  // Background & surface tokens — tinted with primary hue for theme personality.
613
- // 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.
614
627
  // User can override with config.background / config.surface.
615
- var bgBase = config.background || hslToHex([h, 6, 98]);
616
- 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)]);
617
635
  var palette = {
618
636
  primary: deriveShades(config.primary),
619
637
  secondary: deriveShades(config.secondary),
@@ -625,7 +643,8 @@
625
643
  light: deriveShades(lightBase),
626
644
  dark: deriveShades(darkBase),
627
645
  background: bgBase,
628
- surface: surfBase
646
+ surface: surfBase,
647
+ surfaceAlt: surfAlt
629
648
  };
630
649
  return palette;
631
650
  }
@@ -650,27 +669,28 @@
650
669
  5: '1.5rem',
651
670
  // 24px
652
671
  6: '2rem'};
672
+ var _S = SPACING_SCALE;
653
673
  var SPACING_PRESETS = {
654
674
  compact: {
655
- btn: SPACING_SCALE[1] + ' ' + SPACING_SCALE[3],
656
- card: SPACING_SCALE[3] + ' ' + SPACING_SCALE[4],
657
- alert: SPACING_SCALE[2] + ' ' + SPACING_SCALE[4],
658
- cell: SPACING_SCALE[2] + ' ' + SPACING_SCALE[3],
659
- 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]
660
680
  },
661
681
  normal: {
662
- btn: SPACING_SCALE[2] + ' ' + SPACING_SCALE[4],
663
- card: SPACING_SCALE[5] + ' ' + SPACING_SCALE[5],
664
- alert: SPACING_SCALE[3] + ' ' + SPACING_SCALE[5],
665
- cell: SPACING_SCALE[3] + ' ' + SPACING_SCALE[4],
666
- 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]
667
687
  },
668
688
  spacious: {
669
- btn: SPACING_SCALE[3] + ' ' + SPACING_SCALE[5],
670
- card: SPACING_SCALE[6] + ' ' + SPACING_SCALE[6],
671
- alert: SPACING_SCALE[4] + ' ' + SPACING_SCALE[5],
672
- cell: SPACING_SCALE[4] + ' ' + SPACING_SCALE[5],
673
- 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]
674
694
  }
675
695
  };
676
696
  var RADIUS_PRESETS = {
@@ -812,68 +832,13 @@
812
832
  * Built-in theme presets — named color combinations
813
833
  * Each preset provides primary, secondary, and tertiary seed colors.
814
834
  */
815
- var THEME_PRESETS = {
816
- teal: {
817
- primary: '#006666',
818
- secondary: '#6c757d',
819
- tertiary: '#006666'
820
- },
821
- ocean: {
822
- primary: '#0077b6',
823
- secondary: '#90e0ef',
824
- tertiary: '#00b4d8'
825
- },
826
- sunset: {
827
- primary: '#e76f51',
828
- secondary: '#264653',
829
- tertiary: '#e9c46a'
830
- },
831
- forest: {
832
- primary: '#2d6a4f',
833
- secondary: '#95d5b2',
834
- tertiary: '#52b788'
835
- },
836
- slate: {
837
- primary: '#343a40',
838
- secondary: '#adb5bd',
839
- tertiary: '#6c757d'
840
- },
841
- rose: {
842
- primary: '#e11d48',
843
- secondary: '#fda4af',
844
- tertiary: '#fb7185'
845
- },
846
- indigo: {
847
- primary: '#4f46e5',
848
- secondary: '#a5b4fc',
849
- tertiary: '#818cf8'
850
- },
851
- amber: {
852
- primary: '#d97706',
853
- secondary: '#fbbf24',
854
- tertiary: '#f59e0b'
855
- },
856
- emerald: {
857
- primary: '#059669',
858
- secondary: '#6ee7b7',
859
- tertiary: '#34d399'
860
- },
861
- nord: {
862
- primary: '#5e81ac',
863
- secondary: '#88c0d0',
864
- tertiary: '#81a1c1'
865
- },
866
- coral: {
867
- primary: '#ef6461',
868
- secondary: '#4a7c7e',
869
- tertiary: '#e8a87c'
870
- },
871
- midnight: {
872
- primary: '#1e3a5f',
873
- secondary: '#7c8db5',
874
- tertiary: '#3d5a80'
875
- }
876
- };
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
+ }));
877
842
 
878
843
  /**
879
844
  * Resolve layout config to spacing, radius, typeScale, elevation, and motion objects.
@@ -921,6 +886,7 @@
921
886
  }).join(', ');
922
887
  return '.' + name + ' ' + sel;
923
888
  }
889
+ var _sx = scopeSelector;
924
890
 
925
891
  // =========================================================================
926
892
  // Themed CSS generators
@@ -929,12 +895,12 @@
929
895
  function generateTypographyThemed(scope, palette, layout) {
930
896
  var mot = layout.motion;
931
897
  var rules = {};
932
- rules[scopeSelector(scope, 'a')] = {
898
+ rules[_sx(scope, 'a')] = {
933
899
  'color': palette.primary.base,
934
900
  'text-decoration': 'none',
935
901
  'transition': 'color ' + mot.fast + ' ' + mot.easing
936
902
  };
937
- rules[scopeSelector(scope, 'a:hover')] = {
903
+ rules[_sx(scope, 'a:hover')] = {
938
904
  'color': palette.primary.hover,
939
905
  'text-decoration': 'underline'
940
906
  };
@@ -946,11 +912,11 @@
946
912
  var rd = layout.radius;
947
913
 
948
914
  // Base button (only when scoped — unscoped uses defaultStyles)
949
- rules[scopeSelector(scope, '.bw_btn')] = {
915
+ rules[_sx(scope, '.bw_btn')] = {
950
916
  'padding': sp.btn,
951
917
  'border-radius': rd.btn
952
918
  };
953
- rules[scopeSelector(scope, '.bw_btn:focus-visible')] = {
919
+ rules[_sx(scope, '.bw_btn:focus-visible')] = {
954
920
  'outline': '2px solid currentColor',
955
921
  'outline-offset': '2px',
956
922
  'box-shadow': '0 0 0 3px ' + palette.primary.focus
@@ -959,12 +925,12 @@
959
925
  // Variant colors handled by palette class on component root
960
926
 
961
927
  // Size variants (structural, reuse layout radius)
962
- rules[scopeSelector(scope, '.bw_btn_lg')] = {
928
+ rules[_sx(scope, '.bw_btn_lg')] = {
963
929
  'padding': '0.625rem 1.5rem',
964
930
  'font-size': '1rem',
965
931
  'border-radius': rd.btn === '50rem' ? '50rem' : parseInt(rd.btn) + 2 + 'px'
966
932
  };
967
- rules[scopeSelector(scope, '.bw_btn_sm')] = {
933
+ rules[_sx(scope, '.bw_btn_sm')] = {
968
934
  'padding': '0.25rem 0.75rem',
969
935
  'font-size': '0.8125rem',
970
936
  'border-radius': rd.btn === '50rem' ? '50rem' : Math.max(parseInt(rd.btn) - 1, 0) + 'px'
@@ -975,7 +941,7 @@
975
941
  var rules = {};
976
942
  var sp = layout.spacing;
977
943
  var rd = layout.radius;
978
- rules[scopeSelector(scope, '.bw_alert')] = {
944
+ rules[_sx(scope, '.bw_alert')] = {
979
945
  'padding': sp.alert,
980
946
  'border-radius': rd.alert
981
947
  };
@@ -993,38 +959,38 @@
993
959
  var rd = layout.radius;
994
960
  var elev = layout.elevation;
995
961
  var motion = layout.motion;
996
- rules[scopeSelector(scope, '.bw_card')] = {
962
+ rules[_sx(scope, '.bw_card')] = {
997
963
  'background-color': palette.surface || '#fff',
998
964
  'border': '1px solid ' + palette.light.border,
999
965
  'border-radius': rd.card,
1000
966
  'box-shadow': elev.sm,
1001
967
  'transition': 'box-shadow ' + motion.normal + ' ' + motion.easing + ', transform ' + motion.normal + ' ' + motion.easing
1002
968
  };
1003
- rules[scopeSelector(scope, '.bw_card:hover')] = {
969
+ rules[_sx(scope, '.bw_card:hover')] = {
1004
970
  'box-shadow': elev.md
1005
971
  };
1006
- rules[scopeSelector(scope, '.bw_card_hoverable:hover')] = {
972
+ rules[_sx(scope, '.bw_card_hoverable:hover')] = {
1007
973
  'box-shadow': elev.lg
1008
974
  };
1009
- rules[scopeSelector(scope, '.bw_card_body')] = {
975
+ rules[_sx(scope, '.bw_card_body')] = {
1010
976
  'padding': sp.card
1011
977
  };
1012
- rules[scopeSelector(scope, '.bw_card_header')] = {
978
+ rules[_sx(scope, '.bw_card_header')] = {
1013
979
  'padding': sp.card.split(' ').map(function (v) {
1014
980
  return (parseFloat(v) * 0.7).toFixed(3).replace(/\.?0+$/, '') + 'rem';
1015
981
  }).join(' '),
1016
- 'background-color': palette.light.light,
982
+ 'background-color': palette.surfaceAlt,
1017
983
  'border-bottom': '1px solid ' + palette.light.border
1018
984
  };
1019
- rules[scopeSelector(scope, '.bw_card_footer')] = {
1020
- 'background-color': palette.light.light,
985
+ rules[_sx(scope, '.bw_card_footer')] = {
986
+ 'background-color': palette.surfaceAlt,
1021
987
  'border-top': '1px solid ' + palette.light.border,
1022
988
  'color': palette.secondary.base
1023
989
  };
1024
- rules[scopeSelector(scope, '.bw_card_title')] = {
990
+ rules[_sx(scope, '.bw_card_title')] = {
1025
991
  'color': palette.dark.base
1026
992
  };
1027
- rules[scopeSelector(scope, '.bw_card_subtitle')] = {
993
+ rules[_sx(scope, '.bw_card_subtitle')] = {
1028
994
  'color': palette.secondary.base
1029
995
  };
1030
996
 
@@ -1036,101 +1002,104 @@
1036
1002
  var rules = {};
1037
1003
  var sp = layout.spacing;
1038
1004
  var rd = layout.radius;
1039
- rules[scopeSelector(scope, '.bw_form_control')] = {
1005
+ rules[_sx(scope, '.bw_form_control')] = {
1040
1006
  'padding': sp.input,
1041
1007
  'border-radius': rd.input,
1042
1008
  'color': palette.dark.base,
1043
1009
  'background-color': palette.surface || '#fff',
1044
1010
  'border-color': palette.light.border
1045
1011
  };
1046
- rules[scopeSelector(scope, '.bw_form_control:focus')] = {
1012
+ rules[_sx(scope, '.bw_form_control:focus')] = {
1047
1013
  'border-color': palette.primary.border,
1048
1014
  'outline': '2px solid ' + palette.primary.base,
1049
1015
  'outline-offset': '-1px',
1050
1016
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
1051
1017
  };
1052
- rules[scopeSelector(scope, '.bw_form_control::placeholder')] = {
1018
+ rules[_sx(scope, '.bw_form_control::placeholder')] = {
1053
1019
  'color': palette.secondary.base
1054
1020
  };
1055
- rules[scopeSelector(scope, '.bw_form_label')] = {
1021
+ rules[_sx(scope, '.bw_form_label')] = {
1056
1022
  'color': palette.dark.base
1057
1023
  };
1058
- rules[scopeSelector(scope, '.bw_form_text')] = {
1024
+ rules[_sx(scope, '.bw_form_text')] = {
1059
1025
  'color': palette.secondary.base
1060
1026
  };
1061
- rules[scopeSelector(scope, '.bw_form_check_input:checked')] = {
1027
+ rules[_sx(scope, '.bw_form_check_input:checked')] = {
1062
1028
  'background-color': palette.primary.base,
1063
1029
  'border-color': palette.primary.base
1064
1030
  };
1065
- rules[scopeSelector(scope, '.bw_form_check_input:focus')] = {
1031
+ rules[_sx(scope, '.bw_form_check_input:focus')] = {
1066
1032
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
1067
1033
  };
1068
1034
  // Validation states
1069
- rules[scopeSelector(scope, '.bw_form_control.bw_is_valid')] = {
1035
+ rules[_sx(scope, '.bw_form_control.bw_is_valid')] = {
1070
1036
  'border-color': palette.success.base
1071
1037
  };
1072
- rules[scopeSelector(scope, '.bw_form_control.bw_is_valid:focus')] = {
1038
+ rules[_sx(scope, '.bw_form_control.bw_is_valid:focus')] = {
1073
1039
  'border-color': palette.success.base,
1074
1040
  'box-shadow': '0 0 0 0.2rem ' + palette.success.focus
1075
1041
  };
1076
- rules[scopeSelector(scope, '.bw_form_control.bw_is_invalid')] = {
1042
+ rules[_sx(scope, '.bw_form_control.bw_is_invalid')] = {
1077
1043
  'border-color': palette.danger.base
1078
1044
  };
1079
- rules[scopeSelector(scope, '.bw_form_control.bw_is_invalid:focus')] = {
1045
+ rules[_sx(scope, '.bw_form_control.bw_is_invalid:focus')] = {
1080
1046
  'border-color': palette.danger.base,
1081
1047
  'box-shadow': '0 0 0 0.2rem ' + palette.danger.focus
1082
1048
  };
1083
1049
  // Form select
1084
- rules[scopeSelector(scope, '.bw_form_select')] = {
1050
+ rules[_sx(scope, '.bw_form_select')] = {
1085
1051
  'padding': sp.input,
1086
1052
  'border-radius': rd.input,
1087
1053
  'color': palette.dark.base,
1088
1054
  'background-color': palette.surface || '#fff',
1089
1055
  'border-color': palette.light.border
1090
1056
  };
1091
- rules[scopeSelector(scope, '.bw_form_select:focus')] = {
1057
+ rules[_sx(scope, '.bw_form_select:focus')] = {
1092
1058
  'border-color': palette.primary.border,
1093
1059
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
1094
1060
  };
1095
1061
  return rules;
1096
1062
  }
1097
- function generateNavigation(scope, palette) {
1063
+ function generateNavigation(scope, palette, layout) {
1098
1064
  var rules = {};
1099
- rules[scopeSelector(scope, '.bw_navbar')] = {
1100
- 'background-color': palette.light.light,
1065
+ rules[_sx(scope, '.bw_navbar')] = {
1066
+ 'background-color': palette.surfaceAlt,
1101
1067
  'border-bottom-color': palette.light.border
1102
1068
  };
1103
- rules[scopeSelector(scope, '.bw_navbar_brand')] = {
1069
+ rules[_sx(scope, '.bw_navbar_brand')] = {
1104
1070
  'color': palette.dark.base
1105
1071
  };
1106
- rules[scopeSelector(scope, '.bw_navbar_nav .bw_nav_link')] = {
1107
- '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
1108
1075
  };
1109
- rules[scopeSelector(scope, '.bw_navbar_nav .bw_nav_link:hover')] = {
1110
- '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
1111
1079
  };
1112
- rules[scopeSelector(scope, '.bw_navbar_nav .bw_nav_link.active')] = {
1080
+ rules[_sx(scope, '.bw_navbar_nav .bw_nav_link.active')] = {
1113
1081
  'color': palette.primary.base,
1114
- 'background-color': palette.primary.focus
1082
+ 'background-color': palette.primary.focus,
1083
+ 'font-weight': '600'
1115
1084
  };
1116
- rules[scopeSelector(scope, '.bw_navbar_dark')] = {
1085
+ rules[_sx(scope, '.bw_navbar_dark')] = {
1117
1086
  'background-color': palette.dark.base,
1118
1087
  'border-bottom-color': palette.dark.hover
1119
1088
  };
1120
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_navbar_brand')] = {
1089
+ rules[_sx(scope, '.bw_navbar_dark .bw_navbar_brand')] = {
1121
1090
  'color': palette.light.base
1122
1091
  };
1123
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_nav_link')] = {
1124
- 'color': 'rgba(255,255,255,.65)'
1092
+ rules[_sx(scope, '.bw_navbar_dark .bw_nav_link')] = {
1093
+ 'color': palette.light.border
1125
1094
  };
1126
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_nav_link:hover')] = {
1127
- 'color': '#fff'
1095
+ rules[_sx(scope, '.bw_navbar_dark .bw_nav_link:hover')] = {
1096
+ 'color': palette.light.base
1128
1097
  };
1129
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_nav_link.active')] = {
1130
- 'color': '#fff',
1098
+ rules[_sx(scope, '.bw_navbar_dark .bw_nav_link.active')] = {
1099
+ 'color': palette.light.base,
1131
1100
  'font-weight': '600'
1132
1101
  };
1133
- rules[scopeSelector(scope, '.bw_nav_pills .bw_nav_link.active')] = {
1102
+ rules[_sx(scope, '.bw_nav_pills .bw_nav_link.active')] = {
1134
1103
  'color': palette.primary.textOn,
1135
1104
  'background-color': palette.primary.base
1136
1105
  };
@@ -1139,47 +1108,57 @@
1139
1108
  function generateTables(scope, palette, layout) {
1140
1109
  var rules = {};
1141
1110
  var sp = layout.spacing;
1142
- rules[scopeSelector(scope, '.bw_table')] = {
1111
+ rules[_sx(scope, '.bw_table')] = {
1143
1112
  'color': palette.dark.base,
1144
1113
  'border-color': palette.light.border
1145
1114
  };
1146
- rules[scopeSelector(scope, '.bw_table > :not(caption) > * > *')] = {
1115
+ rules[_sx(scope, '.bw_table > :not(caption) > * > *')] = {
1147
1116
  'padding': sp.cell,
1148
1117
  'border-bottom-color': palette.light.border
1149
1118
  };
1150
- rules[scopeSelector(scope, '.bw_table > thead > tr > *')] = {
1119
+ rules[_sx(scope, '.bw_table > thead > tr > *')] = {
1151
1120
  'color': palette.secondary.base,
1152
1121
  'border-bottom-color': palette.light.border,
1153
- 'background-color': palette.light.light
1122
+ 'background-color': palette.surfaceAlt
1154
1123
  };
1155
- rules[scopeSelector(scope, '.bw_table_striped > tbody > tr:nth-of-type(odd) > *')] = {
1156
- '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
1157
1126
  };
1158
- rules[scopeSelector(scope, '.bw_table_hover > tbody > tr:hover > *')] = {
1127
+ rules[_sx(scope, '.bw_table_hover > tbody > tr:hover > *')] = {
1159
1128
  'background-color': palette.primary.focus
1160
1129
  };
1161
- 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')] = {
1162
1137
  'border-color': palette.light.border
1163
1138
  };
1164
- rules[scopeSelector(scope, '.bw_table caption')] = {
1139
+ rules[_sx(scope, '.bw_table caption')] = {
1165
1140
  'color': palette.secondary.base
1166
1141
  };
1167
1142
  return rules;
1168
1143
  }
1169
- function generateTabs(scope, palette) {
1170
- var rules = {};
1171
- 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')] = {
1172
1148
  'border-bottom-color': palette.light.border
1173
1149
  };
1174
- rules[scopeSelector(scope, '.bw_nav_link')] = {
1175
- '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
1176
1153
  };
1177
- rules[scopeSelector(scope, '.bw_nav_tabs .bw_nav_link:hover')] = {
1154
+ rules[_sx(scope, '.bw_nav_tabs .bw_nav_link:hover')] = {
1178
1155
  'color': palette.dark.base,
1156
+ 'background-color': palette.surfaceAlt,
1179
1157
  'border-bottom-color': palette.light.border
1180
1158
  };
1181
- rules[scopeSelector(scope, '.bw_nav_tabs .bw_nav_link.active')] = {
1159
+ rules[_sx(scope, '.bw_nav_tabs .bw_nav_link.active')] = {
1182
1160
  'color': palette.primary.base,
1161
+ 'background-color': palette.primary.focus,
1183
1162
  'border-bottom': '2px solid ' + palette.primary.base
1184
1163
  };
1185
1164
  return rules;
@@ -1187,49 +1166,62 @@
1187
1166
  function generateListGroups(scope, palette, layout) {
1188
1167
  var rules = {};
1189
1168
  var sp = layout.spacing;
1190
- rules[scopeSelector(scope, '.bw_list_group_item')] = {
1169
+ var mo = layout.motion;
1170
+ rules[_sx(scope, '.bw_list_group_item')] = {
1191
1171
  'padding': sp.cell,
1192
1172
  'color': palette.dark.base,
1193
1173
  'background-color': palette.surface || '#fff',
1194
- 'border-color': palette.light.border
1174
+ 'border-color': palette.light.border,
1175
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
1195
1176
  };
1196
- rules[scopeSelector(scope, 'a.bw_list_group_item:hover')] = {
1197
- 'background-color': palette.light.light,
1177
+ rules[_sx(scope, 'a.bw_list_group_item:hover')] = {
1178
+ 'background-color': palette.surfaceAlt,
1198
1179
  'color': palette.dark.hover
1199
1180
  };
1200
- rules[scopeSelector(scope, '.bw_list_group_item.active')] = {
1181
+ rules[_sx(scope, '.bw_list_group_item.active')] = {
1201
1182
  'color': palette.primary.textOn,
1202
1183
  'background-color': palette.primary.base,
1203
1184
  'border-color': palette.primary.base
1204
1185
  };
1205
- rules[scopeSelector(scope, '.bw_list_group_item.disabled')] = {
1186
+ rules[_sx(scope, '.bw_list_group_item.disabled')] = {
1206
1187
  'color': palette.secondary.base,
1207
1188
  'background-color': palette.surface || '#fff'
1208
1189
  };
1209
1190
  return rules;
1210
1191
  }
1211
- function generatePagination(scope, palette) {
1212
- var rules = {};
1213
- 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')] = {
1214
1205
  'color': palette.primary.base,
1215
1206
  'background-color': palette.surface || '#fff',
1216
- 'border-color': palette.light.border
1207
+ 'border-color': palette.light.border,
1208
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
1217
1209
  };
1218
- rules[scopeSelector(scope, '.bw_page_link:hover')] = {
1210
+ rules[_sx(scope, '.bw_page_link:hover')] = {
1219
1211
  'color': palette.primary.hover,
1220
- 'background-color': palette.light.light,
1212
+ 'background-color': palette.surfaceAlt,
1221
1213
  'border-color': palette.light.border
1222
1214
  };
1223
- rules[scopeSelector(scope, '.bw_page_link:focus')] = {
1215
+ rules[_sx(scope, '.bw_page_link:focus')] = {
1224
1216
  'outline': '2px solid ' + palette.primary.base,
1225
1217
  'outline-offset': '-2px'
1226
1218
  };
1227
- rules[scopeSelector(scope, '.bw_page_item.bw_active .bw_page_link')] = {
1219
+ rules[_sx(scope, '.bw_page_item.bw_active .bw_page_link')] = {
1228
1220
  'color': palette.primary.textOn,
1229
1221
  'background-color': palette.primary.base,
1230
1222
  'border-color': palette.primary.base
1231
1223
  };
1232
- rules[scopeSelector(scope, '.bw_page_item.bw_disabled .bw_page_link')] = {
1224
+ rules[_sx(scope, '.bw_page_item.bw_disabled .bw_page_link')] = {
1233
1225
  'color': palette.secondary.base,
1234
1226
  'background-color': palette.surface || '#fff',
1235
1227
  'border-color': palette.light.border
@@ -1238,12 +1230,12 @@
1238
1230
  }
1239
1231
  function generateProgress(scope, palette) {
1240
1232
  var rules = {};
1241
- rules[scopeSelector(scope, '.bw_progress')] = {
1242
- 'background-color': palette.light.light,
1233
+ rules[_sx(scope, '.bw_progress')] = {
1234
+ 'background-color': palette.surfaceAlt,
1243
1235
  'box-shadow': 'inset 0 1px 2px rgba(0,0,0,.1)'
1244
1236
  };
1245
- rules[scopeSelector(scope, '.bw_progress_bar')] = {
1246
- 'color': '#fff',
1237
+ rules[_sx(scope, '.bw_progress_bar')] = {
1238
+ 'color': palette.primary.textOn,
1247
1239
  'background-color': palette.primary.base,
1248
1240
  'box-shadow': 'inset 0 -1px 0 rgba(0,0,0,.15)'
1249
1241
  };
@@ -1262,25 +1254,31 @@
1262
1254
  'color': palette.dark.base,
1263
1255
  'background-color': bg
1264
1256
  };
1265
- rules[scopeSelector(scope, 'body')] = baseReset;
1266
- // Also apply to the scope element itself so themes work on any container, not just body
1267
- if (scope) {
1268
- rules['.' + scope] = baseReset;
1269
- }
1257
+ rules[_sx(scope, 'body')] = baseReset;
1270
1258
  return rules;
1271
1259
  }
1272
- function generateBreadcrumbThemed(scope, palette) {
1273
- var rules = {};
1274
- rules[scopeSelector(scope, '.bw_breadcrumb_item + .bw_breadcrumb_item::before')] = {
1260
+ function generateBreadcrumbThemed(scope, palette, layout) {
1261
+ var rules = {},
1262
+ mo = layout.motion;
1263
+ rules[_sx(scope, '.bw_breadcrumb')] = {
1264
+ 'background-color': palette.surfaceAlt,
1265
+ 'padding': '0.625rem 1rem',
1266
+ 'border-radius': layout.radius.btn
1267
+ };
1268
+ rules[_sx(scope, '.bw_breadcrumb_item + .bw_breadcrumb_item::before')] = {
1275
1269
  'color': palette.secondary.base
1276
1270
  };
1277
- rules[scopeSelector(scope, '.bw_breadcrumb_item.active')] = {
1278
- 'color': palette.secondary.base
1271
+ rules[_sx(scope, '.bw_breadcrumb_item a')] = {
1272
+ 'color': palette.primary.base,
1273
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing
1279
1274
  };
1280
- rules[scopeSelector(scope, '.bw_breadcrumb_item a:hover')] = {
1275
+ rules[_sx(scope, '.bw_breadcrumb_item a:hover')] = {
1281
1276
  'color': palette.primary.hover,
1282
1277
  'text-decoration': 'underline'
1283
1278
  };
1279
+ rules[_sx(scope, '.bw_breadcrumb_item.active')] = {
1280
+ 'color': palette.dark.base
1281
+ };
1284
1282
  return rules;
1285
1283
  }
1286
1284
 
@@ -1288,240 +1286,273 @@
1288
1286
 
1289
1287
  function generateCloseButtonThemed(scope, palette) {
1290
1288
  var rules = {};
1291
- rules[scopeSelector(scope, '.bw_close')] = {
1289
+ rules[_sx(scope, '.bw_close')] = {
1292
1290
  'color': palette.dark.base,
1293
1291
  'opacity': '0.5'
1294
1292
  };
1295
- rules[scopeSelector(scope, '.bw_close:focus')] = {
1293
+ rules[_sx(scope, '.bw_close:focus')] = {
1296
1294
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
1297
1295
  };
1298
1296
  return rules;
1299
1297
  }
1300
1298
  function generateSectionsThemed(scope, palette) {
1301
1299
  var rules = {};
1302
- rules[scopeSelector(scope, '.bw_section_subtitle')] = {
1300
+ rules[_sx(scope, '.bw_section_subtitle')] = {
1303
1301
  'color': palette.secondary.base
1304
1302
  };
1305
- rules[scopeSelector(scope, '.bw_feature_description')] = {
1303
+ rules[_sx(scope, '.bw_feature_description')] = {
1306
1304
  'color': palette.secondary.base
1307
1305
  };
1308
- rules[scopeSelector(scope, '.bw_cta_description')] = {
1306
+ rules[_sx(scope, '.bw_cta_description')] = {
1309
1307
  'color': palette.secondary.base
1310
1308
  };
1311
1309
  return rules;
1312
1310
  }
1313
- function generateAccordionThemed(scope, palette) {
1311
+ function generateAccordionThemed(scope, palette, layout) {
1314
1312
  var rules = {};
1315
- rules[scopeSelector(scope, '.bw_accordion_item')] = {
1313
+ var rd = layout ? layout.radius : {
1314
+ card: '8px'
1315
+ };
1316
+ rules[_sx(scope, '.bw_accordion_item')] = {
1316
1317
  'background-color': palette.surface || '#fff',
1317
1318
  'border-color': palette.light.border
1318
1319
  };
1319
- 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')] = {
1320
1329
  'color': palette.dark.base
1321
1330
  };
1322
- rules[scopeSelector(scope, '.bw_accordion_button:not(.bw_collapsed)')] = {
1331
+ rules[_sx(scope, '.bw_accordion_button:not(.bw_collapsed)')] = {
1323
1332
  'color': palette.primary.darkText,
1324
- 'background-color': palette.primary.light
1333
+ 'background-color': palette.primary.light,
1334
+ 'border-left': '3px solid ' + palette.primary.base
1325
1335
  };
1326
- rules[scopeSelector(scope, '.bw_accordion_button:hover')] = {
1327
- 'background-color': palette.light.light
1336
+ rules[_sx(scope, '.bw_accordion_button:hover')] = {
1337
+ 'background-color': palette.surfaceAlt
1328
1338
  };
1329
- rules[scopeSelector(scope, '.bw_accordion_button:not(.bw_collapsed):hover')] = {
1330
- '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
1331
1342
  };
1332
- rules[scopeSelector(scope, '.bw_accordion_button:focus-visible')] = {
1343
+ rules[_sx(scope, '.bw_accordion_button:focus-visible')] = {
1333
1344
  'box-shadow': '0 0 0 0.2rem ' + palette.primary.focus
1334
1345
  };
1335
- rules[scopeSelector(scope, '.bw_accordion_body')] = {
1336
- '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
1337
1349
  };
1338
1350
  return rules;
1339
1351
  }
1340
1352
  function generateCarouselThemed(scope, palette) {
1341
1353
  var rules = {};
1342
- rules[scopeSelector(scope, '.bw_carousel')] = {
1343
- 'background-color': palette.light.light
1354
+ rules[_sx(scope, '.bw_carousel')] = {
1355
+ 'background-color': palette.surfaceAlt
1344
1356
  };
1345
- rules[scopeSelector(scope, '.bw_carousel_indicator.active')] = {
1357
+ rules[_sx(scope, '.bw_carousel_indicator.active')] = {
1346
1358
  'background-color': palette.primary.base
1347
1359
  };
1348
- rules[scopeSelector(scope, '.bw_carousel_control')] = {
1349
- 'background-color': 'rgba(0,0,0,0.4)',
1350
- 'color': '#fff'
1360
+ rules[_sx(scope, '.bw_carousel_control')] = {
1361
+ 'background-color': palette.dark.base,
1362
+ 'color': palette.dark.textOn
1351
1363
  };
1352
- rules[scopeSelector(scope, '.bw_carousel_control:hover')] = {
1353
- 'background-color': 'rgba(0,0,0,0.6)'
1364
+ rules[_sx(scope, '.bw_carousel_control:hover')] = {
1365
+ 'background-color': palette.dark.hover
1354
1366
  };
1355
- rules[scopeSelector(scope, '.bw_carousel_caption')] = {
1356
- 'background': 'linear-gradient(transparent, rgba(0,0,0,0.6))',
1357
- 'color': '#fff'
1367
+ rules[_sx(scope, '.bw_carousel_caption')] = {
1368
+ 'background': 'linear-gradient(transparent, ' + palette.dark.base + ')',
1369
+ 'color': palette.dark.textOn
1358
1370
  };
1359
1371
  return rules;
1360
1372
  }
1361
1373
  function generateModalThemed(scope, palette, layout) {
1362
1374
  var rules = {};
1363
- rules[scopeSelector(scope, '.bw_modal_content')] = {
1375
+ rules[_sx(scope, '.bw_modal_content')] = {
1364
1376
  'background-color': palette.surface || '#fff',
1365
1377
  'border-color': palette.light.border,
1366
1378
  'box-shadow': layout.elevation.lg
1367
1379
  };
1368
- rules[scopeSelector(scope, '.bw_modal_header')] = {
1380
+ rules[_sx(scope, '.bw_modal_header')] = {
1369
1381
  'border-bottom-color': palette.light.border
1370
1382
  };
1371
- rules[scopeSelector(scope, '.bw_modal_footer')] = {
1383
+ rules[_sx(scope, '.bw_modal_footer')] = {
1372
1384
  'border-top-color': palette.light.border
1373
1385
  };
1374
- rules[scopeSelector(scope, '.bw_modal_title')] = {
1386
+ rules[_sx(scope, '.bw_modal_title')] = {
1375
1387
  'color': palette.dark.base
1376
1388
  };
1377
1389
  return rules;
1378
1390
  }
1379
1391
  function generateToastThemed(scope, palette, layout) {
1380
1392
  var rules = {};
1381
- rules[scopeSelector(scope, '.bw_toast')] = {
1393
+ rules[_sx(scope, '.bw_toast')] = {
1382
1394
  'background-color': palette.surface || '#fff',
1383
- 'border-color': 'rgba(0,0,0,0.1)',
1395
+ 'border-color': palette.light.border,
1384
1396
  'box-shadow': layout.elevation.lg
1385
1397
  };
1386
- rules[scopeSelector(scope, '.bw_toast_header')] = {
1387
- 'border-bottom-color': 'rgba(0,0,0,0.05)'
1398
+ rules[_sx(scope, '.bw_toast_header')] = {
1399
+ 'border-bottom-color': palette.light.border
1388
1400
  };
1389
1401
  // Variant toast borders handled by palette class
1390
1402
  return rules;
1391
1403
  }
1392
1404
  function generateDropdownThemed(scope, palette, layout) {
1393
1405
  var rules = {};
1394
- rules[scopeSelector(scope, '.bw_dropdown_menu')] = {
1406
+ rules[_sx(scope, '.bw_dropdown_menu')] = {
1395
1407
  'background-color': palette.surface || '#fff',
1396
1408
  'border-color': palette.light.border,
1397
1409
  'box-shadow': layout.elevation.md
1398
1410
  };
1399
- rules[scopeSelector(scope, '.bw_dropdown_item')] = {
1400
- '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
1401
1414
  };
1402
- rules[scopeSelector(scope, '.bw_dropdown_item:hover')] = {
1415
+ rules[_sx(scope, '.bw_dropdown_item:hover')] = {
1403
1416
  'color': palette.dark.hover,
1404
- 'background-color': palette.light.light
1417
+ 'background-color': palette.surfaceAlt
1405
1418
  };
1406
- rules[scopeSelector(scope, '.bw_dropdown_item.disabled')] = {
1419
+ rules[_sx(scope, '.bw_dropdown_item.disabled')] = {
1407
1420
  'color': palette.secondary.base
1408
1421
  };
1409
- rules[scopeSelector(scope, '.bw_dropdown_divider')] = {
1422
+ rules[_sx(scope, '.bw_dropdown_divider')] = {
1410
1423
  'border-top-color': palette.light.border
1411
1424
  };
1412
1425
  return rules;
1413
1426
  }
1414
1427
  function generateSwitchThemed(scope, palette) {
1415
1428
  var rules = {};
1416
- rules[scopeSelector(scope, '.bw_form_switch .bw_switch_input')] = {
1429
+ rules[_sx(scope, '.bw_form_switch .bw_switch_input')] = {
1417
1430
  'background-color': palette.secondary.base,
1418
1431
  'border-color': palette.secondary.base
1419
1432
  };
1420
- rules[scopeSelector(scope, '.bw_form_switch .bw_switch_input:checked')] = {
1433
+ rules[_sx(scope, '.bw_form_switch .bw_switch_input:checked')] = {
1421
1434
  'background-color': palette.primary.base,
1422
1435
  'border-color': palette.primary.base
1423
1436
  };
1424
- rules[scopeSelector(scope, '.bw_form_switch .bw_switch_input:focus')] = {
1437
+ rules[_sx(scope, '.bw_form_switch .bw_switch_input:focus')] = {
1425
1438
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
1426
1439
  };
1427
1440
  return rules;
1428
1441
  }
1429
1442
  function generateSkeletonThemed(scope, palette) {
1430
1443
  var rules = {};
1431
- rules[scopeSelector(scope, '.bw_skeleton')] = {
1432
- '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%)'
1433
1446
  };
1434
1447
  return rules;
1435
1448
  }
1436
1449
 
1437
1450
  // generateAvatarThemed: removed — palette class on root handles variants
1438
1451
 
1439
- function generateStatCardThemed(scope, palette) {
1440
- 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
+ };
1441
1468
  // Variant border colors handled by palette class
1442
- rules[scopeSelector(scope, '.bw_stat_change_up')] = {
1469
+ rules[_sx(scope, '.bw_stat_change_up')] = {
1443
1470
  'color': palette.success.base
1444
1471
  };
1445
- rules[scopeSelector(scope, '.bw_stat_change_down')] = {
1472
+ rules[_sx(scope, '.bw_stat_change_down')] = {
1446
1473
  'color': palette.danger.base
1447
1474
  };
1448
1475
  return rules;
1449
1476
  }
1450
1477
  function generateTimelineThemed(scope, palette) {
1451
1478
  var rules = {};
1452
- rules[scopeSelector(scope, '.bw_timeline::before')] = {
1479
+ rules[_sx(scope, '.bw_timeline::before')] = {
1453
1480
  'background-color': palette.light.border
1454
1481
  };
1455
1482
  // Variant marker colors handled by palette class
1456
- rules[scopeSelector(scope, '.bw_timeline_date')] = {
1483
+ rules[_sx(scope, '.bw_timeline_date')] = {
1457
1484
  'color': palette.secondary.base
1458
1485
  };
1459
1486
  return rules;
1460
1487
  }
1461
1488
  function generateStepperThemed(scope, palette) {
1462
1489
  var rules = {};
1463
- rules[scopeSelector(scope, '.bw_step_indicator')] = {
1464
- 'background-color': palette.light.light,
1490
+ rules[_sx(scope, '.bw_step_indicator')] = {
1491
+ 'background-color': palette.surfaceAlt,
1465
1492
  'border': '2px solid ' + palette.light.border,
1466
1493
  'color': palette.secondary.base
1467
1494
  };
1468
- rules[scopeSelector(scope, '.bw_step + .bw_step::before')] = {
1495
+ rules[_sx(scope, '.bw_step + .bw_step::before')] = {
1469
1496
  'background-color': palette.light.border
1470
1497
  };
1471
- rules[scopeSelector(scope, '.bw_step_active .bw_step_indicator')] = {
1498
+ rules[_sx(scope, '.bw_step_active .bw_step_indicator')] = {
1472
1499
  'background-color': palette.primary.base,
1473
1500
  'color': palette.primary.textOn
1474
1501
  };
1475
- rules[scopeSelector(scope, '.bw_step_active .bw_step_label')] = {
1502
+ rules[_sx(scope, '.bw_step_active .bw_step_label')] = {
1476
1503
  'color': palette.dark.base,
1477
1504
  'font-weight': '600'
1478
1505
  };
1479
- rules[scopeSelector(scope, '.bw_step_completed .bw_step_indicator')] = {
1506
+ rules[_sx(scope, '.bw_step_completed .bw_step_indicator')] = {
1480
1507
  'background-color': palette.primary.base,
1481
1508
  'color': palette.primary.textOn
1482
1509
  };
1483
- rules[scopeSelector(scope, '.bw_step_completed .bw_step_label')] = {
1510
+ rules[_sx(scope, '.bw_step_completed .bw_step_label')] = {
1484
1511
  'color': palette.primary.base
1485
1512
  };
1486
- rules[scopeSelector(scope, '.bw_step_completed + .bw_step::before')] = {
1513
+ rules[_sx(scope, '.bw_step_completed + .bw_step::before')] = {
1487
1514
  'background-color': palette.primary.base
1488
1515
  };
1489
1516
  return rules;
1490
1517
  }
1491
1518
  function generateChipInputThemed(scope, palette) {
1492
1519
  var rules = {};
1493
- rules[scopeSelector(scope, '.bw_chip_input')] = {
1494
- '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
1495
1524
  };
1496
- rules[scopeSelector(scope, '.bw_chip_input:focus-within')] = {
1525
+ rules[_sx(scope, '.bw_chip_input:focus-within')] = {
1497
1526
  'border-color': palette.primary.base,
1498
1527
  'box-shadow': '0 0 0 0.2rem ' + palette.primary.focus
1499
1528
  };
1500
- rules[scopeSelector(scope, '.bw_chip')] = {
1501
- 'background-color': palette.light.light,
1529
+ rules[_sx(scope, '.bw_chip')] = {
1530
+ 'background-color': palette.surfaceAlt,
1502
1531
  'color': palette.dark.base
1503
1532
  };
1504
- rules[scopeSelector(scope, '.bw_chip_remove:hover')] = {
1533
+ rules[_sx(scope, '.bw_chip_remove:hover')] = {
1505
1534
  'color': palette.danger.base,
1506
1535
  'background-color': palette.danger.light
1507
1536
  };
1508
1537
  return rules;
1509
1538
  }
1510
- function generateFileUploadThemed(scope, palette) {
1511
- var rules = {};
1512
- 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')] = {
1513
1543
  'border-color': palette.light.border,
1514
- 'background-color': palette.light.light
1544
+ 'background-color': palette.surfaceAlt,
1545
+ 'transition': 'border-color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
1515
1546
  };
1516
- rules[scopeSelector(scope, '.bw_file_upload:hover')] = {
1547
+ rules[_sx(scope, '.bw_file_upload:hover')] = {
1517
1548
  'border-color': palette.primary.base,
1518
1549
  'background-color': palette.primary.light
1519
1550
  };
1520
- rules[scopeSelector(scope, '.bw_file_upload:focus')] = {
1551
+ rules[_sx(scope, '.bw_file_upload:focus')] = {
1521
1552
  'outline': '2px solid ' + palette.primary.base,
1522
1553
  'outline-offset': '2px'
1523
1554
  };
1524
- rules[scopeSelector(scope, '.bw_file_upload.bw_file_upload_active')] = {
1555
+ rules[_sx(scope, '.bw_file_upload.bw_file_upload_active')] = {
1525
1556
  'border-color': palette.primary.base,
1526
1557
  'background-color': palette.primary.light,
1527
1558
  'border-style': 'solid'
@@ -1530,37 +1561,93 @@
1530
1561
  }
1531
1562
  function generateRangeThemed(scope, palette) {
1532
1563
  var rules = {};
1533
- rules[scopeSelector(scope, '.bw_range')] = {
1564
+ rules[_sx(scope, '.bw_range')] = {
1534
1565
  'background-color': palette.light.border
1535
1566
  };
1536
- rules[scopeSelector(scope, '.bw_range::-webkit-slider-thumb')] = {
1567
+ rules[_sx(scope, '.bw_range::-webkit-slider-thumb')] = {
1537
1568
  'background-color': palette.primary.base,
1538
- 'border-color': '#fff',
1569
+ 'border-color': palette.surface || '#fff',
1539
1570
  'box-shadow': '0 1px 3px rgba(0,0,0,0.2)',
1540
1571
  'transition': 'background-color 0.15s ease-out, transform 0.15s ease-out'
1541
1572
  };
1542
- rules[scopeSelector(scope, '.bw_range::-moz-range-thumb')] = {
1573
+ rules[_sx(scope, '.bw_range::-moz-range-thumb')] = {
1543
1574
  'background-color': palette.primary.base,
1544
- 'border-color': '#fff',
1575
+ 'border-color': palette.surface || '#fff',
1545
1576
  'box-shadow': '0 1px 3px rgba(0,0,0,0.2)'
1546
1577
  };
1547
1578
  return rules;
1548
1579
  }
1549
- function generateSearchThemed(scope, palette) {
1550
- var rules = {};
1551
- 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')] = {
1552
1631
  'color': palette.dark.base
1553
1632
  };
1554
1633
  return rules;
1555
1634
  }
1556
- function generateCodeDemoThemed(scope, palette) {
1635
+ function generateCodeDemoThemed(scope, palette, layout) {
1557
1636
  var rules = {};
1558
- 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')] = {
1559
1646
  'background': palette.success.base,
1560
1647
  'color': palette.success.textOn,
1561
1648
  'border-color': palette.success.base
1562
1649
  };
1563
- rules[scopeSelector(scope, '.bw_copy_btn:hover')] = {
1650
+ rules[_sx(scope, '.bw_copy_btn:hover')] = {
1564
1651
  'background': 'rgba(255,255,255,0.2)',
1565
1652
  'color': '#fff'
1566
1653
  };
@@ -1569,7 +1656,7 @@
1569
1656
  function generateNavPillsThemed(scope, palette, layout) {
1570
1657
  var rules = {};
1571
1658
  var rd = layout.radius;
1572
- rules[scopeSelector(scope, '.bw_nav_pills .bw_nav_link')] = {
1659
+ rules[_sx(scope, '.bw_nav_pills .bw_nav_link')] = {
1573
1660
  'border-radius': rd.btn
1574
1661
  };
1575
1662
  return rules;
@@ -1597,21 +1684,21 @@
1597
1684
  var s = palette[k];
1598
1685
 
1599
1686
  // --- Root palette class: sets default bg/color/border ---
1600
- rules[scopeSelector(scope, '.bw_' + k)] = {
1687
+ rules[_sx(scope, '.bw_' + k)] = {
1601
1688
  'background-color': s.base,
1602
1689
  'color': s.textOn,
1603
1690
  'border-color': s.base
1604
1691
  };
1605
1692
 
1606
1693
  // --- Pseudo-states (shared across all components) ---
1607
- rules[scopeSelector(scope, '.bw_' + k + ':hover')] = {
1694
+ rules[_sx(scope, '.bw_' + k + ':hover')] = {
1608
1695
  'background-color': s.hover,
1609
1696
  'border-color': s.active
1610
1697
  };
1611
- rules[scopeSelector(scope, '.bw_' + k + ':active')] = {
1698
+ rules[_sx(scope, '.bw_' + k + ':active')] = {
1612
1699
  'background-color': s.active
1613
1700
  };
1614
- rules[scopeSelector(scope, '.bw_' + k + ':focus-visible')] = {
1701
+ rules[_sx(scope, '.bw_' + k + ':focus-visible')] = {
1615
1702
  'box-shadow': '0 0 0 3px ' + s.focus,
1616
1703
  'outline': 'none'
1617
1704
  };
@@ -1619,71 +1706,110 @@
1619
1706
  // --- Component-specific overrides ---
1620
1707
 
1621
1708
  // Alerts: light bg, dark text, subtle border
1622
- rules[scopeSelector(scope, '.bw_alert.bw_' + k)] = {
1709
+ rules[_sx(scope, '.bw_alert.bw_' + k)] = {
1623
1710
  'background-color': s.light,
1624
1711
  'color': s.darkText,
1625
1712
  'border-color': s.border
1626
1713
  };
1627
1714
 
1628
1715
  // Toast: inherit bg, left border accent
1629
- rules[scopeSelector(scope, '.bw_toast.bw_' + k)] = {
1716
+ rules[_sx(scope, '.bw_toast.bw_' + k)] = {
1630
1717
  'background-color': 'inherit',
1631
1718
  'color': 'inherit',
1632
1719
  'border-left': '4px solid ' + s.base
1633
1720
  };
1634
1721
 
1635
1722
  // Stat card: inherit bg, left border accent
1636
- rules[scopeSelector(scope, '.bw_stat_card.bw_' + k)] = {
1723
+ rules[_sx(scope, '.bw_stat_card.bw_' + k)] = {
1637
1724
  'background-color': 'inherit',
1638
1725
  'color': 'inherit',
1639
1726
  'border-left-color': s.base
1640
1727
  };
1641
1728
 
1642
1729
  // Card accent: left border accent, inherit bg
1643
- rules[scopeSelector(scope, '.bw_card.bw_' + k)] = {
1730
+ rules[_sx(scope, '.bw_card.bw_' + k)] = {
1644
1731
  'background-color': 'inherit',
1645
1732
  'color': 'inherit',
1646
1733
  'border-left': '4px solid ' + s.base
1647
1734
  };
1648
1735
 
1649
1736
  // Timeline marker: colored dot
1650
- rules[scopeSelector(scope, '.bw_timeline_marker.bw_' + k)] = {
1737
+ rules[_sx(scope, '.bw_timeline_marker.bw_' + k)] = {
1651
1738
  'box-shadow': '0 0 0 2px ' + s.base
1652
1739
  };
1653
1740
 
1654
- // Spinner: text color only, transparent bg
1655
- 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)] = {
1656
1745
  'background-color': 'transparent',
1657
1746
  'color': s.base,
1658
- '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
1659
1758
  };
1660
1759
 
1661
1760
  // Outline button: transparent bg, colored border+text, solid on hover
1662
- rules[scopeSelector(scope, '.bw_btn_outline.bw_' + k)] = {
1761
+ rules[_sx(scope, '.bw_btn_outline.bw_' + k)] = {
1663
1762
  'background-color': 'transparent',
1664
1763
  'color': s.base,
1665
1764
  'border-color': s.base
1666
1765
  };
1667
- rules[scopeSelector(scope, '.bw_btn_outline.bw_' + k + ':hover')] = {
1766
+ rules[_sx(scope, '.bw_btn_outline.bw_' + k + ':hover')] = {
1668
1767
  'background-color': s.base,
1669
1768
  'color': s.textOn
1670
1769
  };
1671
1770
 
1672
1771
  // Hero: gradient background
1673
- rules[scopeSelector(scope, '.bw_hero.bw_' + k)] = {
1772
+ rules[_sx(scope, '.bw_hero.bw_' + k)] = {
1674
1773
  'background': 'linear-gradient(135deg, ' + s.base + ' 0%, ' + s.hover + ' 100%)',
1675
1774
  'color': s.textOn
1676
1775
  };
1677
1776
 
1678
- // Progress bar: white text on colored bg (default is fine, just ensure text)
1679
- rules[scopeSelector(scope, '.bw_progress_bar.bw_' + k)] = {
1680
- '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
1681
1791
  };
1682
1792
  });
1683
1793
 
1684
- // Text muted
1685
- rules[scopeSelector(scope, '.bw_text_muted')] = {
1686
- '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'
1687
1813
  };
1688
1814
  return rules;
1689
1815
  }
@@ -1698,7 +1824,7 @@
1698
1824
  * @returns {Object} CSS rules object
1699
1825
  */
1700
1826
  function generateThemedCSS(scopeName, palette, layout) {
1701
- 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));
1702
1828
  }
1703
1829
 
1704
1830
  // =========================================================================
@@ -2193,6 +2319,12 @@
2193
2319
  'border-width': '1px',
2194
2320
  'border-style': 'solid'
2195
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
+ },
2196
2328
  '.bw_table_responsive': {
2197
2329
  'overflow-x': 'auto',
2198
2330
  '-webkit-overflow-scrolling': 'touch'
@@ -2306,6 +2438,7 @@
2306
2438
  'display': 'block',
2307
2439
  'font-size': '0.875rem',
2308
2440
  'font-weight': '500',
2441
+ 'padding': '0.625rem 1rem',
2309
2442
  'text-decoration': 'none',
2310
2443
  'cursor': 'pointer',
2311
2444
  'border': 'none',
@@ -2408,16 +2541,15 @@
2408
2541
  'padding': '0.375rem 0.75rem',
2409
2542
  'margin-left': '-1px',
2410
2543
  'line-height': '1.25',
2411
- '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'
2412
2550
  },
2413
2551
  '.bw_page_item:first-child .bw_page_link': {
2414
- 'margin-left': '0',
2415
- 'border-top-left-radius': '0.375rem',
2416
- 'border-bottom-left-radius': '0.375rem'
2417
- },
2418
- '.bw_page_item:last-child .bw_page_link': {
2419
- 'border-top-right-radius': '0.375rem',
2420
- 'border-bottom-right-radius': '0.375rem'
2552
+ 'margin-left': '0'
2421
2553
  },
2422
2554
  '.bw_page_link:focus-visible': {
2423
2555
  'z-index': '3',
@@ -2788,6 +2920,7 @@
2788
2920
  'display': 'flex',
2789
2921
  'align-items': 'center',
2790
2922
  'width': '100%',
2923
+ 'padding': '0.875rem 1.25rem',
2791
2924
  'font-size': '1rem',
2792
2925
  'font-weight': '500',
2793
2926
  'text-align': 'left',
@@ -2810,20 +2943,16 @@
2810
2943
  '.bw_accordion_button:not(.bw_collapsed)::after': {
2811
2944
  'transform': 'rotate(-180deg)'
2812
2945
  },
2946
+ '.bw_accordion_body': {
2947
+ 'padding': '1rem 1.25rem'
2948
+ },
2813
2949
  '.bw_accordion_collapse': {
2814
2950
  'max-height': '0',
2815
- 'overflow': 'hidden'
2951
+ 'overflow': 'hidden',
2952
+ 'transition': 'max-height 0.3s ease'
2816
2953
  },
2817
2954
  '.bw_accordion_collapse.bw_collapse_show': {
2818
2955
  'max-height': 'none'
2819
- },
2820
- '.bw_accordion_item:first-child': {
2821
- 'border-top-left-radius': '8px',
2822
- 'border-top-right-radius': '8px'
2823
- },
2824
- '.bw_accordion_item:last-child': {
2825
- 'border-bottom-left-radius': '8px',
2826
- 'border-bottom-right-radius': '8px'
2827
2956
  }
2828
2957
  },
2829
2958
  // ---- Carousel ----
@@ -3191,7 +3320,11 @@
3191
3320
  // ---- Stat card ----
3192
3321
  statCard: {
3193
3322
  '.bw_stat_card': {
3194
- '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'
3195
3328
  },
3196
3329
  '.bw_stat_card:hover': {
3197
3330
  'transform': 'translateY(-1px)'
@@ -4209,6 +4342,52 @@
4209
4342
  'margin-right': '.5rem'
4210
4343
  };
4211
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
+
4212
4391
  // Visibility
4213
4392
  rules['.bw_visible, .visible'] = {
4214
4393
  'visibility': 'visible !important'
@@ -4287,6 +4466,26 @@
4287
4466
  return getStructuralCSS();
4288
4467
  }
4289
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
+
4290
4489
  // =========================================================================
4291
4490
  // defaultStyles — backward-compatible categorized view
4292
4491
  // =========================================================================
@@ -4321,57 +4520,40 @@
4321
4520
  });
4322
4521
 
4323
4522
  /**
4324
- * Generate alternate-palette CSS scoped under `.bw_theme_alt`.
4325
- * Uses the same `generateThemedCSS()` pipeline as the primary palette —
4326
- * both sides go through identical code paths.
4327
- *
4328
- * @param {string} name - Theme scope name (e.g. 'ocean'). '' for global.
4329
- * @param {Object} altPalette - From derivePalette(deriveAlternateConfig(...))
4330
- * @param {Object} layout - From resolveLayout()
4331
- * @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
4332
4530
  */
4333
- function generateAlternateCSS(name, altPalette, layout) {
4334
- // Generate themed CSS using the same pipeline as primary
4335
- var rawRules = generateThemedCSS('', altPalette, layout);
4336
-
4337
- // Re-scope every selector under .bw_theme_alt (+ optional theme name)
4338
- var altPrefix = name ? '.' + name + '.bw_theme_alt' : '.bw_theme_alt';
4339
- var altRules = {};
4340
- for (var sel in rawRules) {
4341
- 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;
4342
4535
  if (sel.charAt(0) === '@') {
4343
4536
  // @media / @keyframes — recurse into the block
4344
- var innerBlock = rawRules[sel];
4345
- var altInner = {};
4537
+ var innerBlock = rules[sel];
4538
+ var scopedInner = {};
4346
4539
  for (var innerSel in innerBlock) {
4347
4540
  if (!innerBlock.hasOwnProperty(innerSel)) continue;
4348
- altInner[altPrefix + ' ' + innerSel] = innerBlock[innerSel];
4541
+ scopedInner[_prefixSelector(innerSel, prefix)] = innerBlock[innerSel];
4349
4542
  }
4350
- altRules[sel] = altInner;
4543
+ scoped[sel] = scopedInner;
4351
4544
  } else {
4352
- // Regular selector — prefix with alt scope
4353
- // Handle comma-separated selectors
4354
- var parts = sel.split(',');
4355
- var scopedParts = [];
4356
- for (var i = 0; i < parts.length; i++) {
4357
- var s = parts[i].trim();
4358
- // 'body' selector gets special treatment: .bw_theme_alt body
4359
- if (s === 'body' || s.indexOf('body') === 0) {
4360
- scopedParts.push(altPrefix + ' ' + s);
4361
- } else {
4362
- scopedParts.push(altPrefix + ' ' + s);
4363
- }
4364
- }
4365
- altRules[scopedParts.join(', ')] = rawRules[sel];
4545
+ scoped[_prefixSelector(sel, prefix)] = rules[sel];
4366
4546
  }
4367
4547
  }
4368
-
4369
- // Add body-level overrides for the alternate surface
4370
- altRules[altPrefix + ' body, :root' + altPrefix + ' body'] = {
4371
- 'color': altPalette.dark.base,
4372
- 'background-color': altPalette.light.base
4373
- };
4374
- 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(', ');
4375
4557
  }
4376
4558
 
4377
4559
  /**
@@ -5125,7 +5307,7 @@
5125
5307
  __monkey_patch_is_nodejs__: {
5126
5308
  _value: 'ignore',
5127
5309
  set: function set(x) {
5128
- this._value = typeof x === 'boolean' ? x : 'ignore';
5310
+ this._value = _is(x, 'boolean') ? x : 'ignore';
5129
5311
  },
5130
5312
  get: function get() {
5131
5313
  return this._value;
@@ -5173,6 +5355,76 @@
5173
5355
  configurable: true
5174
5356
  });
5175
5357
 
5358
+ // ── Internal aliases ─────────────────────────────────────────────────────
5359
+ // Short names for frequently-used builtins and internal methods.
5360
+ // Same pattern as v1 (_to = bw.typeOf, etc.).
5361
+ //
5362
+ // Why: Terser can't shorten global property chains (console.warn,
5363
+ // Object.prototype.hasOwnProperty, Array.isArray, document.createElement)
5364
+ // because it can't prove they're side-effect-free. We can, so we alias
5365
+ // them here. Each alias saves bytes in the minified output, and the short
5366
+ // names also reduce visual noise in the hot paths (binding pipeline,
5367
+ // createDOM, etc.).
5368
+ //
5369
+ // Alias Target Sites
5370
+ // ───────── ────────────────────────────────────── ─────
5371
+ // _hop Object.prototype.hasOwnProperty 15
5372
+ // _isA Array.isArray 25
5373
+ // _keys Object.keys 7
5374
+ // _to bw.typeOf (type string) 26
5375
+ // _is type check boolean: _is(x,'string') ~50
5376
+ // _cw console.warn 8
5377
+ // _cl console.log 11
5378
+ // _ce console.error 4
5379
+ // _chp ComponentHandle.prototype 28 (defined after constructor)
5380
+ //
5381
+ // Note: document.createElement etc. are NOT aliased because they require
5382
+ // `this === document` and .bind() would add overhead on every call.
5383
+ // Console aliases use thin wrappers (not direct refs) so test monkey-
5384
+ // patching of console.warn/log/error continues to work.
5385
+ //
5386
+ // `typeof x` for UNDECLARED globals (window, document, process, require,
5387
+ // EventSource, navigator, Promise, __filename, import.meta) MUST stay as
5388
+ // raw `typeof` — calling _to(x) when x doesn't exist throws ReferenceError.
5389
+ //
5390
+ // ── v1 functional type helpers (kept for reference, not currently used) ──
5391
+ // _toa(x, type, trueVal, falseVal) — bw.typeAssign:
5392
+ // returns trueVal if _to(x)===type, else falseVal.
5393
+ // Replaces: (typeof x === 'string') ? A : B → _toa(x,'string',A,B)
5394
+ // _toc(x, type, trueVal, falseVal) — bw.typeConvert:
5395
+ // same as _toa but if trueVal/falseVal are functions, calls them with x.
5396
+ // Replaces: typeof x === 'string' ? fn(x) : default → _toc(x,'string',fn,default)
5397
+ // Uncomment if pattern frequency justifies them:
5398
+ // var _toa = function(x, t, y, n) { return _to(x) === t ? y : n; };
5399
+ // var _toc = function(x, t, y, n) { var r = _to(x)===t; return r ? (_to(y)==='function'?y(x):y) : (_to(n)==='function'?n(x):n); };
5400
+ // ─────────────────────────────────────────────────────────────────────────
5401
+ var _hop = Object.prototype.hasOwnProperty;
5402
+ var _isA = Array.isArray;
5403
+ var _keys = Object.keys;
5404
+ var _to = typeOf; // imported from bitwrench-utils.js
5405
+ var _is = function _is(x, t) {
5406
+ var r = _to(x);
5407
+ return r === t || r.toLowerCase() === t;
5408
+ };
5409
+ // Console aliases use thin wrappers (not direct references) so that test
5410
+ // code can monkey-patch console.warn/log/error and the patches take effect.
5411
+ var _cw = function _cw() {
5412
+ console.warn.apply(console, arguments);
5413
+ };
5414
+ var _cl = function _cl() {
5415
+ console.log.apply(console, arguments);
5416
+ };
5417
+ var _ce = function _ce() {
5418
+ console.error.apply(console, arguments);
5419
+ };
5420
+
5421
+ /**
5422
+ * Debug flag. When true, emits console.warn for silent binding failures
5423
+ * (missing paths, null refs, auto-created intermediate objects).
5424
+ * @type {boolean}
5425
+ */
5426
+ bw.debug = false;
5427
+
5176
5428
  /**
5177
5429
  * Lazy-resolve Node.js `fs` module.
5178
5430
  * Tries require('fs') first (available in CJS/UMD Node.js builds),
@@ -5322,7 +5574,7 @@
5322
5574
  */
5323
5575
  bw._el = function (id) {
5324
5576
  // Pass-through for DOM elements
5325
- if (typeof id !== 'string') return id || null;
5577
+ if (!_is(id, 'string')) return id || null;
5326
5578
  if (!id) return null;
5327
5579
  if (!bw._isBrowser) return null;
5328
5580
 
@@ -5350,7 +5602,12 @@
5350
5602
  el = document.querySelector('[data-bw_id="' + id + '"]');
5351
5603
  }
5352
5604
 
5353
- // 5. Cache the result for next time
5605
+ // 5. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
5606
+ if (!el && id.indexOf('bw_uuid_') === 0) {
5607
+ el = document.querySelector('.' + id);
5608
+ }
5609
+
5610
+ // 6. Cache the result for next time
5354
5611
  if (el) {
5355
5612
  bw._nodeMap[id] = el;
5356
5613
  }
@@ -5402,6 +5659,79 @@
5402
5659
  }
5403
5660
  };
5404
5661
 
5662
+ // ===================================================================================
5663
+ // bw.assignUUID() / bw.getUUID() — Explicit UUID addressing for TACO objects
5664
+ // ===================================================================================
5665
+
5666
+ /**
5667
+ * Regex to match a bw_uuid_* token in a class string.
5668
+ * @private
5669
+ */
5670
+ var _UUID_RE = /\bbw_uuid_[a-z0-9_]+\b/;
5671
+
5672
+ /**
5673
+ * Assign a UUID to a TACO object by appending a `bw_uuid_*` token to `taco.a.class`.
5674
+ *
5675
+ * Idempotent by default — calling twice returns the same UUID. Pass `forceNew=true`
5676
+ * to replace an existing UUID (useful in loops where each TACO needs a unique ID).
5677
+ *
5678
+ * @param {Object} taco - A TACO object `{t, a, c, o}`
5679
+ * @param {boolean} [forceNew=false] - If true, replaces any existing UUID with a new one
5680
+ * @returns {string} The UUID string (e.g. 'bw_uuid_a1b2c3d4e5')
5681
+ * @category Identifiers
5682
+ * @example
5683
+ * var card = bw.makeStatCard({ value: '0', label: 'Scans' });
5684
+ * var uuid = bw.assignUUID(card); // 'bw_uuid_a1b2c3d4e5'
5685
+ * var same = bw.assignUUID(card); // same UUID (idempotent)
5686
+ * var diff = bw.assignUUID(card, true); // new UUID (forced)
5687
+ */
5688
+ bw.assignUUID = function (taco, forceNew) {
5689
+ if (!taco || !_is(taco, 'object')) return null;
5690
+
5691
+ // Ensure taco.a exists
5692
+ if (!taco.a) taco.a = {};
5693
+ if (!_is(taco.a["class"], 'string')) taco.a["class"] = taco.a["class"] ? String(taco.a["class"]) : '';
5694
+ var existing = taco.a["class"].match(_UUID_RE);
5695
+ if (existing && !forceNew) {
5696
+ return existing[0];
5697
+ }
5698
+
5699
+ // Remove old UUID if forceNew
5700
+ if (existing) {
5701
+ taco.a["class"] = taco.a["class"].replace(_UUID_RE, '').replace(/\s+/g, ' ').trim();
5702
+ }
5703
+ var uuid = bw.uuid('uuid');
5704
+ taco.a["class"] = (taco.a["class"] ? taco.a["class"] + ' ' : '') + uuid;
5705
+ return uuid;
5706
+ };
5707
+
5708
+ /**
5709
+ * Read the UUID from a TACO object or DOM element. Pure getter, no side effects.
5710
+ *
5711
+ * @param {Object|Element} tacoOrElement - A TACO object or DOM element
5712
+ * @returns {string|null} The UUID string, or null if none assigned
5713
+ * @category Identifiers
5714
+ * @example
5715
+ * bw.getUUID(card) // 'bw_uuid_a1b2c3d4e5' (from TACO)
5716
+ * bw.getUUID(domEl) // 'bw_uuid_a1b2c3d4e5' (from DOM element)
5717
+ * bw.getUUID({t:'div'}) // null (no UUID)
5718
+ */
5719
+ bw.getUUID = function (tacoOrElement) {
5720
+ if (!tacoOrElement) return null;
5721
+ var classStr;
5722
+ // DOM element: check className
5723
+ if (tacoOrElement.className !== undefined && tacoOrElement.tagName) {
5724
+ classStr = tacoOrElement.className;
5725
+ }
5726
+ // TACO object: check a.class
5727
+ else if (tacoOrElement.a && _is(tacoOrElement.a["class"], 'string')) {
5728
+ classStr = tacoOrElement.a["class"];
5729
+ }
5730
+ if (!classStr) return null;
5731
+ var match = classStr.match(_UUID_RE);
5732
+ return match ? match[0] : null;
5733
+ };
5734
+
5405
5735
  /**
5406
5736
  * Escape HTML special characters to prevent XSS.
5407
5737
  *
@@ -5417,7 +5747,7 @@
5417
5747
  * // => '&lt;b&gt;Hello&lt;&#x2F;b&gt; &amp; &quot;world&quot;'
5418
5748
  */
5419
5749
  bw.escapeHTML = function (str) {
5420
- if (typeof str !== 'string') return '';
5750
+ if (!_is(str, 'string')) return '';
5421
5751
  var escapeMap = {
5422
5752
  '&': '&amp;',
5423
5753
  '<': '&lt;',
@@ -5454,6 +5784,45 @@
5454
5784
  };
5455
5785
  };
5456
5786
 
5787
+ /**
5788
+ * Hyperscript-style TACO constructor.
5789
+ *
5790
+ * A convenience helper that returns a canonical TACO object from positional
5791
+ * arguments. The return value is a plain object — serializable, works with
5792
+ * bwserve, and accepted everywhere TACO is accepted.
5793
+ *
5794
+ * @param {string} tag - HTML tag name (e.g. 'div', 'p', 'section')
5795
+ * @param {Object|null} [attrs] - HTML attributes object. Pass null or omit to skip.
5796
+ * @param {*} [content] - Content: string, number, TACO object, or array of children.
5797
+ * @param {Object} [options] - TACO options (state, lifecycle hooks, render fn).
5798
+ * @returns {Object} Plain TACO object {t, a?, c?, o?}
5799
+ * @category Utilities
5800
+ * @see bw.html
5801
+ * @see bw.createDOM
5802
+ * @see bw.DOM
5803
+ * @example
5804
+ * bw.h('div')
5805
+ * // => { t: 'div' }
5806
+ *
5807
+ * bw.h('p', { class: 'bw_text_muted' }, 'Hello')
5808
+ * // => { t: 'p', a: { class: 'bw_text_muted' }, c: 'Hello' }
5809
+ *
5810
+ * bw.h('ul', null, [
5811
+ * bw.h('li', null, 'one'),
5812
+ * bw.h('li', null, 'two')
5813
+ * ])
5814
+ * // => { t: 'ul', c: [{ t: 'li', c: 'one' }, { t: 'li', c: 'two' }] }
5815
+ */
5816
+ bw.h = function (tag, attrs, content, options) {
5817
+ var taco = {
5818
+ t: String(tag)
5819
+ };
5820
+ if (attrs !== null && attrs !== undefined) taco.a = attrs;
5821
+ if (content !== undefined) taco.c = content;
5822
+ if (options !== undefined) taco.o = options;
5823
+ return taco;
5824
+ };
5825
+
5457
5826
  /**
5458
5827
  * Convert a TACO object (or array of TACOs) to an HTML string.
5459
5828
  *
@@ -5494,7 +5863,7 @@
5494
5863
  }
5495
5864
 
5496
5865
  // Handle arrays of TACOs
5497
- if (Array.isArray(taco)) {
5866
+ if (_isA(taco)) {
5498
5867
  return taco.map(function (t) {
5499
5868
  return bw.html(t, options);
5500
5869
  }).join('');
@@ -5517,17 +5886,17 @@
5517
5886
  if (taco && taco._bwEach && options.state) {
5518
5887
  var eachExpr = taco.expr.replace(/^\$\{|\}$/g, '');
5519
5888
  var arr = bw._evaluatePath(options.state, eachExpr);
5520
- if (!Array.isArray(arr)) return '';
5889
+ if (!_isA(arr)) return '';
5521
5890
  return arr.map(function (item, idx) {
5522
5891
  return bw.html(taco.factory(item, idx), options);
5523
5892
  }).join('');
5524
5893
  }
5525
5894
 
5526
5895
  // Handle primitives and non-TACO objects
5527
- if (_typeof(taco) !== 'object' || !taco.t) {
5896
+ if (!_is(taco, 'object') || !taco.t) {
5528
5897
  var str = options.raw ? String(taco) : bw.escapeHTML(String(taco));
5529
5898
  // Resolve template bindings if state provided
5530
- if (options.state && typeof str === 'string' && str.indexOf('${') >= 0) {
5899
+ if (options.state && _is(str, 'string') && str.indexOf('${') >= 0) {
5531
5900
  str = bw._resolveTemplate(str, options.state, !!options.compile);
5532
5901
  }
5533
5902
  return str;
@@ -5552,9 +5921,17 @@
5552
5921
  // Skip null, undefined, false
5553
5922
  if (value == null || value === false) continue;
5554
5923
 
5555
- // Skip event handlers (they're for DOM only)
5556
- if (key.startsWith('on')) continue;
5557
- if (key === 'style' && _typeof(value) === 'object') {
5924
+ // Serialize event handlers via funcRegister
5925
+ if (key.startsWith('on')) {
5926
+ if (_is(value, 'function')) {
5927
+ var fnId = bw.funcRegister(value);
5928
+ attrStr += ' ' + key + '="' + bw.funcGetDispatchStr(fnId, 'event') + '"';
5929
+ } else if (_is(value, 'string')) {
5930
+ attrStr += ' ' + key + '="' + bw.escapeHTML(value) + '"';
5931
+ }
5932
+ continue;
5933
+ }
5934
+ if (key === 'style' && _is(value, 'object')) {
5558
5935
  // Convert style object to string
5559
5936
  var styleStr = Object.entries(value).filter(function (_ref) {
5560
5937
  var _ref2 = _slicedToArray(_ref, 2),
@@ -5571,7 +5948,7 @@
5571
5948
  }
5572
5949
  } else if (key === 'class') {
5573
5950
  // Handle class as array or string
5574
- var classStr = Array.isArray(value) ? value.filter(Boolean).join(' ') : String(value);
5951
+ var classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
5575
5952
  if (classStr) {
5576
5953
  attrStr += " class=\"".concat(bw.escapeHTML(classStr), "\"");
5577
5954
  }
@@ -5607,12 +5984,182 @@
5607
5984
  // Process content recursively
5608
5985
  var contentStr = content != null ? bw.html(content, options) : '';
5609
5986
  // Resolve template bindings in content if state provided
5610
- if (options.state && typeof contentStr === 'string' && contentStr.indexOf('${') >= 0) {
5987
+ if (options.state && _is(contentStr, 'string') && contentStr.indexOf('${') >= 0) {
5611
5988
  contentStr = bw._resolveTemplate(contentStr, options.state, !!options.compile);
5612
5989
  }
5613
5990
  return "<".concat(tag).concat(attrStr, ">").concat(contentStr, "</").concat(tag, ">");
5614
5991
  };
5615
5992
 
5993
+ /**
5994
+ * Generate a complete, self-contained HTML document from TACO content.
5995
+ *
5996
+ * Produces a full `<!DOCTYPE html>` page with configurable runtime injection,
5997
+ * func registry emission (so serialized event handlers work), optional theme,
5998
+ * and extra head elements. Designed for static site generation, offline/airgapped
5999
+ * use, and the "static site that isn't static" workflow.
6000
+ *
6001
+ * @param {Object} [opts={}] - Page options
6002
+ * @param {Object|string|Array} [opts.body=''] - Body content: TACO, string, or array
6003
+ * @param {string} [opts.title='bitwrench'] - Page title
6004
+ * @param {Object} [opts.state] - State for ${expr} resolution in bw.html()
6005
+ * @param {string} [opts.runtime='shim'] - Runtime level: 'inline'|'cdn'|'shim'|'none'
6006
+ * @param {string} [opts.css=''] - Additional CSS for <style> block
6007
+ * @param {string|Object} [opts.theme=null] - Theme preset name or config object
6008
+ * @param {Array} [opts.head=[]] - Extra TACO elements rendered into <head>
6009
+ * @param {string} [opts.favicon=''] - Favicon URL
6010
+ * @param {string} [opts.lang='en'] - HTML lang attribute
6011
+ * @returns {string} Complete HTML document string
6012
+ * @category DOM Generation
6013
+ * @see bw.html
6014
+ * @example
6015
+ * bw.htmlPage({
6016
+ * title: 'My App',
6017
+ * body: { t: 'h1', c: 'Hello World' },
6018
+ * runtime: 'shim'
6019
+ * })
6020
+ */
6021
+ bw.htmlPage = function (opts) {
6022
+ opts = opts || {};
6023
+ var title = opts.title || 'bitwrench';
6024
+ var body = opts.body || '';
6025
+ var state = opts.state || undefined;
6026
+ var runtime = opts.runtime || 'shim';
6027
+ var css = opts.css || '';
6028
+ var theme = opts.theme || null;
6029
+ var headExtra = opts.head || [];
6030
+ var favicon = opts.favicon || '';
6031
+ var lang = opts.lang || 'en';
6032
+
6033
+ // Snapshot funcRegistry counter before rendering
6034
+ var fnCounterBefore = bw._fnIDCounter;
6035
+
6036
+ // Render body content
6037
+ var bodyHTML = '';
6038
+ if (_is(body, 'string')) {
6039
+ bodyHTML = body;
6040
+ } else {
6041
+ var htmlOpts = {};
6042
+ if (state) htmlOpts.state = state;
6043
+ bodyHTML = bw.html(body, htmlOpts);
6044
+ }
6045
+
6046
+ // Collect functions registered during this render
6047
+ var fnCounterAfter = bw._fnIDCounter;
6048
+ var registryEntries = '';
6049
+ for (var i = fnCounterBefore; i < fnCounterAfter; i++) {
6050
+ var fnKey = 'bw_fn_' + i;
6051
+ if (bw._fnRegistry[fnKey]) {
6052
+ registryEntries += 'bw._fnRegistry[\'' + fnKey + '\']=' + bw._fnRegistry[fnKey].toString() + ';\n';
6053
+ }
6054
+ }
6055
+
6056
+ // Build runtime script for <head>
6057
+ var runtimeHead = '';
6058
+ if (runtime === 'inline') {
6059
+ // Read UMD bundle synchronously if in Node.js
6060
+ var umdSource = null;
6061
+ if (bw._isNode) {
6062
+ try {
6063
+ var fs = typeof require === 'function' ? require('fs') : null;
6064
+ var pathMod = typeof require === 'function' ? require('path') : null;
6065
+ if (fs && pathMod) {
6066
+ // Resolve dist/ relative to this source file
6067
+ var srcDir = '';
6068
+ try {
6069
+ srcDir = pathMod.dirname(typeof __filename !== 'undefined' ? __filename : '');
6070
+ } catch (e2) {/* ESM: __filename not available */}
6071
+ if (!srcDir && typeof ({ url: (typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('bitwrench-lean.es5.js', document.baseURI).href)) }) !== 'undefined' && (typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('bitwrench-lean.es5.js', document.baseURI).href))) {
6072
+ var url = typeof require === 'function' ? require('url') : null;
6073
+ if (url && url.fileURLToPath) srcDir = pathMod.dirname(url.fileURLToPath((typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('bitwrench-lean.es5.js', document.baseURI).href))));
6074
+ }
6075
+ if (srcDir) {
6076
+ var distPath = pathMod.resolve(srcDir, '../dist/bitwrench.umd.min.js');
6077
+ umdSource = fs.readFileSync(distPath, 'utf8');
6078
+ }
6079
+ }
6080
+ } catch (e) {/* fall through */}
6081
+ }
6082
+ if (umdSource) {
6083
+ runtimeHead = '<script>' + umdSource + '</script>';
6084
+ } else {
6085
+ // Fallback to shim in browser or if dist not available
6086
+ runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
6087
+ }
6088
+ } else if (runtime === 'cdn') {
6089
+ runtimeHead = '<script src="https://cdn.jsdelivr.net/npm/bitwrench@2/dist/bitwrench.umd.min.js"></script>';
6090
+ } else if (runtime === 'shim') {
6091
+ runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
6092
+ }
6093
+ // runtime === 'none' → empty
6094
+
6095
+ // Theme CSS
6096
+ var themeCSS = '';
6097
+ if (theme) {
6098
+ var themeConfig = _is(theme, 'string') ? THEME_PRESETS[theme.toLowerCase()] || null : theme;
6099
+ if (themeConfig) {
6100
+ var themeResult = bw.makeStyles(themeConfig);
6101
+ themeCSS = themeResult.css;
6102
+ }
6103
+ }
6104
+
6105
+ // Extra <head> elements
6106
+ var headHTML = '';
6107
+ if (_isA(headExtra) && headExtra.length > 0) {
6108
+ headHTML = headExtra.map(function (el) {
6109
+ return bw.html(el);
6110
+ }).join('\n');
6111
+ }
6112
+
6113
+ // Favicon
6114
+ var faviconTag = '';
6115
+ if (favicon) {
6116
+ var safeFavicon = favicon.replace(/[&<>"']/g, function (c) {
6117
+ return {
6118
+ '&': '&amp;',
6119
+ '<': '&lt;',
6120
+ '>': '&gt;',
6121
+ '"': '&quot;',
6122
+ "'": '&#39;'
6123
+ }[c];
6124
+ });
6125
+ faviconTag = '<link rel="icon" href="' + safeFavicon + '">';
6126
+ }
6127
+
6128
+ // Escaped title
6129
+ var safeTitle = bw.escapeHTML(title);
6130
+
6131
+ // Combine all CSS
6132
+ var allCSS = (themeCSS ? themeCSS + '\n' : '') + css;
6133
+
6134
+ // Body-end script: registry entries + optional loadStyles
6135
+ var bodyEndScript = '';
6136
+ var bodyEndParts = [];
6137
+ if (registryEntries) {
6138
+ bodyEndParts.push(registryEntries);
6139
+ }
6140
+ if (runtime === 'inline' || runtime === 'cdn') {
6141
+ bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadStyles();}');
6142
+ }
6143
+ if (bodyEndParts.length > 0) {
6144
+ bodyEndScript = '<script>\n' + bodyEndParts.join('\n') + '\n</script>';
6145
+ }
6146
+
6147
+ // Assemble document
6148
+ var parts = ['<!DOCTYPE html>', '<html lang="' + lang + '">', '<head>', '<meta charset="UTF-8">', '<meta name="viewport" content="width=device-width, initial-scale=1">'];
6149
+ parts.push('<title>' + safeTitle + '</title>');
6150
+ if (faviconTag) parts.push(faviconTag);
6151
+ if (runtimeHead) parts.push(runtimeHead);
6152
+ if (headHTML) parts.push(headHTML);
6153
+ if (allCSS) parts.push('<style>' + allCSS + '</style>');
6154
+ parts.push('</head>');
6155
+ parts.push('<body>');
6156
+ parts.push(bodyHTML);
6157
+ if (bodyEndScript) parts.push(bodyEndScript);
6158
+ parts.push('</body>');
6159
+ parts.push('</html>');
6160
+ return parts.join('\n');
6161
+ };
6162
+
5616
6163
  /**
5617
6164
  * Create a live DOM element from a TACO object (browser only).
5618
6165
  *
@@ -5658,7 +6205,7 @@
5658
6205
  }
5659
6206
 
5660
6207
  // Handle text nodes
5661
- if (_typeof(taco) !== 'object' || !taco.t) {
6208
+ if (!_is(taco, 'object') || !taco.t) {
5662
6209
  return document.createTextNode(String(taco));
5663
6210
  }
5664
6211
  var tag = taco.t,
@@ -5677,16 +6224,16 @@
5677
6224
  key = _Object$entries2$_i[0],
5678
6225
  value = _Object$entries2$_i[1];
5679
6226
  if (value == null || value === false) continue;
5680
- if (key === 'style' && _typeof(value) === 'object') {
6227
+ if (key === 'style' && _is(value, 'object')) {
5681
6228
  // Apply styles directly
5682
6229
  Object.assign(el.style, value);
5683
6230
  } else if (key === 'class') {
5684
6231
  // Handle class as array or string
5685
- var classStr = Array.isArray(value) ? value.filter(Boolean).join(' ') : String(value);
6232
+ var classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
5686
6233
  if (classStr) {
5687
6234
  el.className = classStr;
5688
6235
  }
5689
- } else if (key.startsWith('on') && typeof value === 'function') {
6236
+ } else if (key.startsWith('on') && _is(value, 'function')) {
5690
6237
  // Event handlers
5691
6238
  var eventName = key.slice(2).toLowerCase();
5692
6239
  el.addEventListener(eventName, value);
@@ -5706,7 +6253,7 @@
5706
6253
  // Children with data-bw_id or id attributes get local refs on the parent,
5707
6254
  // so o.render functions can access them without any DOM lookup.
5708
6255
  if (content != null) {
5709
- if (Array.isArray(content)) {
6256
+ if (_isA(content)) {
5710
6257
  content.forEach(function (child) {
5711
6258
  if (child != null) {
5712
6259
  // Handle ComponentHandle in content arrays (Level 2 children)
@@ -5726,20 +6273,20 @@
5726
6273
  if (childEl._bw_refs) {
5727
6274
  if (!el._bw_refs) el._bw_refs = {};
5728
6275
  for (var rk in childEl._bw_refs) {
5729
- if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
6276
+ if (_hop.call(childEl._bw_refs, rk)) {
5730
6277
  el._bw_refs[rk] = childEl._bw_refs[rk];
5731
6278
  }
5732
6279
  }
5733
6280
  }
5734
6281
  }
5735
6282
  });
5736
- } else if (_typeof(content) === 'object' && content.__bw_raw) {
6283
+ } else if (_is(content, 'object') && content.__bw_raw) {
5737
6284
  // Raw HTML content — inject via innerHTML
5738
6285
  el.innerHTML = content.v;
5739
6286
  } else if (content._bwComponent === true) {
5740
6287
  // Single ComponentHandle as content
5741
6288
  content.mount(el);
5742
- } else if (_typeof(content) === 'object' && content.t) {
6289
+ } else if (_is(content, 'object') && content.t) {
5743
6290
  var childEl = bw.createDOM(content, options);
5744
6291
  el.appendChild(childEl);
5745
6292
  var childBwId = content.a ? content.a['data-bw_id'] || content.a.id : null;
@@ -5750,7 +6297,7 @@
5750
6297
  if (childEl._bw_refs) {
5751
6298
  if (!el._bw_refs) el._bw_refs = {};
5752
6299
  for (var rk in childEl._bw_refs) {
5753
- if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
6300
+ if (_hop.call(childEl._bw_refs, rk)) {
5754
6301
  el._bw_refs[rk] = childEl._bw_refs[rk];
5755
6302
  }
5756
6303
  }
@@ -5765,6 +6312,14 @@
5765
6312
  bw._registerNode(el, null);
5766
6313
  }
5767
6314
 
6315
+ // Register UUID class in node cache (bw_uuid_* tokens in class string)
6316
+ if (el.className) {
6317
+ var uuidMatch = el.className.match(_UUID_RE);
6318
+ if (uuidMatch) {
6319
+ bw._nodeMap[uuidMatch[0]] = el;
6320
+ }
6321
+ }
6322
+
5768
6323
  // Handle lifecycle hooks and state
5769
6324
  if (opts.mounted || opts.unmount || opts.render || opts.state) {
5770
6325
  var id = attrs['data-bw_id'] || bw.uuid();
@@ -5782,7 +6337,7 @@
5782
6337
  if (opts.render) {
5783
6338
  el._bw_render = opts.render;
5784
6339
  if (opts.mounted) {
5785
- console.warn('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
6340
+ _cw('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
5786
6341
  }
5787
6342
 
5788
6343
  // Queue initial render (same timing as mounted)
@@ -5854,7 +6409,7 @@
5854
6409
  // Get target element (use cache-backed lookup)
5855
6410
  var targetEl = bw._el(target);
5856
6411
  if (!targetEl) {
5857
- console.error('bw.DOM: Target element not found:', target);
6412
+ _ce('bw.DOM: Target element not found:', target);
5858
6413
  return null;
5859
6414
  }
5860
6415
 
@@ -5892,7 +6447,7 @@
5892
6447
  targetEl.appendChild(taco.element);
5893
6448
  }
5894
6449
  // Handle arrays
5895
- else if (Array.isArray(taco)) {
6450
+ else if (_isA(taco)) {
5896
6451
  taco.forEach(function (t) {
5897
6452
  if (t != null) {
5898
6453
  if (t._bwComponent === true) {
@@ -5927,7 +6482,7 @@
5927
6482
  bw.compileProps = function (handle) {
5928
6483
  var props = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
5929
6484
  var compiledProps = {};
5930
- Object.keys(props).forEach(function (key) {
6485
+ _keys(props).forEach(function (key) {
5931
6486
  // Create getter/setter for each prop
5932
6487
  Object.defineProperty(compiledProps, key, {
5933
6488
  get: function get() {
@@ -6124,6 +6679,16 @@
6124
6679
  bw.cleanup = function (element) {
6125
6680
  if (!bw._isBrowser || !element) return;
6126
6681
 
6682
+ // Deregister UUID classes from node cache (element + descendants)
6683
+ // Covers elements that have UUID but no data-bw_id
6684
+ var selfUuidMatch = element.className && element.className.match(_UUID_RE);
6685
+ if (selfUuidMatch) delete bw._nodeMap[selfUuidMatch[0]];
6686
+ var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
6687
+ uuidEls.forEach(function (uel) {
6688
+ var m = uel.className && uel.className.match(_UUID_RE);
6689
+ if (m) delete bw._nodeMap[m[0]];
6690
+ });
6691
+
6127
6692
  // Find all elements with data-bw_id
6128
6693
  var elements = element.querySelectorAll('[data-bw_id]');
6129
6694
  elements.forEach(function (el) {
@@ -6137,6 +6702,10 @@
6137
6702
  // Deregister from node cache
6138
6703
  bw._deregisterNode(el, id);
6139
6704
 
6705
+ // Deregister UUID class from node cache
6706
+ var uuidMatch = el.className && el.className.match(_UUID_RE);
6707
+ if (uuidMatch) delete bw._nodeMap[uuidMatch[0]];
6708
+
6140
6709
  // Clean up pub/sub subscriptions tied to this element
6141
6710
  if (el._bw_subs) {
6142
6711
  el._bw_subs.forEach(function (unsub) {
@@ -6163,6 +6732,10 @@
6163
6732
  // Deregister from node cache
6164
6733
  bw._deregisterNode(element, id);
6165
6734
 
6735
+ // Deregister UUID class from node cache
6736
+ var elemUuidMatch = element.className && element.className.match(_UUID_RE);
6737
+ if (elemUuidMatch) delete bw._nodeMap[elemUuidMatch[0]];
6738
+
6166
6739
  // Clean up pub/sub subscriptions tied to element itself
6167
6740
  if (element._bw_subs) {
6168
6741
  element._bw_subs.forEach(function (unsub) {
@@ -6238,17 +6811,17 @@
6238
6811
  if (attr) {
6239
6812
  // Patch an attribute
6240
6813
  el.setAttribute(attr, String(content));
6241
- } else if (Array.isArray(content)) {
6814
+ } else if (_isA(content)) {
6242
6815
  // Patch with array of children (strings and/or TACOs)
6243
6816
  el.innerHTML = '';
6244
6817
  content.forEach(function (item) {
6245
- if (typeof item === 'string' || typeof item === 'number') {
6818
+ if (_is(item, 'string') || _is(item, 'number')) {
6246
6819
  el.appendChild(document.createTextNode(String(item)));
6247
6820
  } else if (item && item.t) {
6248
6821
  el.appendChild(bw.createDOM(item));
6249
6822
  }
6250
6823
  });
6251
- } else if (_typeof(content) === 'object' && content !== null && content.t) {
6824
+ } else if (_is(content, 'object') && content.t) {
6252
6825
  // Patch with a TACO — replace children
6253
6826
  el.innerHTML = '';
6254
6827
  el.appendChild(bw.createDOM(content));
@@ -6279,7 +6852,7 @@
6279
6852
  bw.patchAll = function (patches) {
6280
6853
  var results = {};
6281
6854
  for (var id in patches) {
6282
- if (Object.prototype.hasOwnProperty.call(patches, id)) {
6855
+ if (_hop.call(patches, id)) {
6283
6856
  results[id] = bw.patch(id, patches[id]);
6284
6857
  }
6285
6858
  }
@@ -6376,7 +6949,7 @@
6376
6949
  snapshot[i].handler(detail);
6377
6950
  called++;
6378
6951
  } catch (err) {
6379
- console.warn('bw.pub: subscriber error on topic "' + topic + '":', err);
6952
+ _cw('bw.pub: subscriber error on topic "' + topic + '":', err);
6380
6953
  }
6381
6954
  }
6382
6955
  return called;
@@ -6477,8 +7050,8 @@
6477
7050
  * @see bw.funcGetDispatchStr
6478
7051
  */
6479
7052
  bw.funcRegister = function (fn, name) {
6480
- if (typeof fn !== 'function') return '';
6481
- var fnID = typeof name === 'string' && name.length > 0 ? name : 'bw_fn_' + bw._fnIDCounter++;
7053
+ if (!_is(fn, 'function')) return '';
7054
+ var fnID = _is(name, 'string') && name.length > 0 ? name : 'bw_fn_' + bw._fnIDCounter++;
6482
7055
  bw._fnRegistry[fnID] = fn;
6483
7056
  return fnID;
6484
7057
  };
@@ -6497,8 +7070,8 @@
6497
7070
  bw.funcGetById = function (name, errFn) {
6498
7071
  name = String(name);
6499
7072
  if (name in bw._fnRegistry) return bw._fnRegistry[name];
6500
- return typeof errFn === 'function' ? errFn : function () {
6501
- console.warn('bw.funcGetById: unregistered fn "' + name + '"');
7073
+ return _is(errFn, 'function') ? errFn : function () {
7074
+ _cw('bw.funcGetById: unregistered fn "' + name + '"');
6502
7075
  };
6503
7076
  };
6504
7077
 
@@ -6540,13 +7113,23 @@
6540
7113
  bw.funcGetRegistry = function () {
6541
7114
  var copy = {};
6542
7115
  for (var k in bw._fnRegistry) {
6543
- if (Object.prototype.hasOwnProperty.call(bw._fnRegistry, k)) {
7116
+ if (_hop.call(bw._fnRegistry, k)) {
6544
7117
  copy[k] = bw._fnRegistry[k];
6545
7118
  }
6546
7119
  }
6547
7120
  return copy;
6548
7121
  };
6549
7122
 
7123
+ /**
7124
+ * Minimal runtime shim for funcRegister dispatch in static HTML.
7125
+ * When embedded in a `<script>` tag, provides just enough infrastructure
7126
+ * for `bw.funcGetById()` calls to resolve. The actual function bodies
7127
+ * are emitted separately as `bw._fnRegistry['bw_fn_X'] = ...;` assignments.
7128
+ * @type {string}
7129
+ * @category Function Registry
7130
+ */
7131
+ bw._FUNC_REGISTRY_SHIM = '(function(){var bw=window.bw||(window.bw={});' + 'if(!bw._fnRegistry)bw._fnRegistry={};' + 'bw.funcGetById=function(n){return bw._fnRegistry[n]||function(){' + 'console.warn("bw: unregistered fn "+n)};};' + 'bw.funcRegister=function(fn,name){' + 'var id=name||("bw_fn_"+(bw._fnIDCounter=(bw._fnIDCounter||0)+1));' + 'bw._fnRegistry[id]=fn;return id;};' + 'window.bw=bw;})();';
7132
+
6550
7133
  // ===================================================================================
6551
7134
  // Template Binding Utilities
6552
7135
  // ===================================================================================
@@ -6578,7 +7161,10 @@
6578
7161
  var parts = path.split('.');
6579
7162
  var val = state;
6580
7163
  for (var i = 0; i < parts.length; i++) {
6581
- if (val == null) return '';
7164
+ if (val == null) {
7165
+ if (bw.debug) _cw('bw.debug: _evaluatePath — null at key "' + parts[i] + '" in path "' + path + '"');
7166
+ return '';
7167
+ }
6582
7168
  val = val[parts[i]];
6583
7169
  }
6584
7170
  return val == null ? '' : val;
@@ -6598,7 +7184,7 @@
6598
7184
  */
6599
7185
  bw._compiledExprs = {};
6600
7186
  bw._resolveTemplate = function (str, state, compile) {
6601
- if (typeof str !== 'string' || str.indexOf('${') < 0) return str;
7187
+ if (!_is(str, 'string') || str.indexOf('${') < 0) return str;
6602
7188
  var bindings = bw._parseBindings(str);
6603
7189
  if (bindings.length === 0) return str;
6604
7190
  var result = '';
@@ -6621,6 +7207,7 @@
6621
7207
  try {
6622
7208
  val = bw._compiledExprs[b.expr](state);
6623
7209
  } catch (e) {
7210
+ if (bw.debug) _cw('bw.debug: _resolveTemplate — Tier 2 eval failed for "${' + b.expr + '}":', e.message);
6624
7211
  val = '';
6625
7212
  }
6626
7213
  } else {
@@ -6728,7 +7315,7 @@
6728
7315
  this._state = {};
6729
7316
  if (o.state) {
6730
7317
  for (var k in o.state) {
6731
- if (Object.prototype.hasOwnProperty.call(o.state, k)) {
7318
+ if (_hop.call(o.state, k)) {
6732
7319
  this._state[k] = o.state[k];
6733
7320
  }
6734
7321
  }
@@ -6737,7 +7324,7 @@
6737
7324
  this._actions = {};
6738
7325
  if (o.actions) {
6739
7326
  for (var k2 in o.actions) {
6740
- if (Object.prototype.hasOwnProperty.call(o.actions, k2)) {
7327
+ if (_hop.call(o.actions, k2)) {
6741
7328
  this._actions[k2] = o.actions[k2];
6742
7329
  }
6743
7330
  }
@@ -6747,7 +7334,7 @@
6747
7334
  if (o.methods) {
6748
7335
  var self = this;
6749
7336
  for (var k3 in o.methods) {
6750
- if (Object.prototype.hasOwnProperty.call(o.methods, k3)) {
7337
+ if (_hop.call(o.methods, k3)) {
6751
7338
  this._methods[k3] = o.methods[k3];
6752
7339
  (function (methodName, methodFn) {
6753
7340
  self[methodName] = function () {
@@ -6765,7 +7352,7 @@
6765
7352
  willMount: o.willMount || null,
6766
7353
  mounted: o.mounted || null,
6767
7354
  willUpdate: o.willUpdate || null,
6768
- onUpdate: o.onUpdate || null,
7355
+ onUpdate: o.onUpdate || o.updated || null,
6769
7356
  unmount: o.unmount || null,
6770
7357
  willDestroy: o.willDestroy || null
6771
7358
  };
@@ -6780,14 +7367,23 @@
6780
7367
  this._compile = !!o.compile;
6781
7368
  this._bw_refs = {};
6782
7369
  this._refCounter = 0;
7370
+ // Child component ownership (Bug #5)
7371
+ this._children = [];
7372
+ this._parent = null;
7373
+ // Factory metadata for BCCL rebuild (Bug #6)
7374
+ this._factory = taco._bwFactory || null;
6783
7375
  }
6784
7376
 
7377
+ // Short alias for ComponentHandle.prototype (see alias block at top of file).
7378
+ // 28 method definitions × 25 chars = ~700B raw savings in minified output.
7379
+ var _chp = ComponentHandle.prototype;
7380
+
6785
7381
  // ── State Methods ──
6786
7382
 
6787
7383
  /**
6788
7384
  * Get a state value. Dot-path supported: `get('user.name')`
6789
7385
  */
6790
- ComponentHandle.prototype.get = function (key) {
7386
+ _chp.get = function (key) {
6791
7387
  return bw._evaluatePath(this._state, key);
6792
7388
  };
6793
7389
 
@@ -6797,12 +7393,13 @@
6797
7393
  * @param {*} value - New value
6798
7394
  * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
6799
7395
  */
6800
- ComponentHandle.prototype.set = function (key, value, opts) {
7396
+ _chp.set = function (key, value, opts) {
6801
7397
  // Dot-path set
6802
7398
  var parts = key.split('.');
6803
7399
  var obj = this._state;
6804
7400
  for (var i = 0; i < parts.length - 1; i++) {
6805
- if (obj[parts[i]] == null || _typeof(obj[parts[i]]) !== 'object') {
7401
+ if (!_is(obj[parts[i]], 'object')) {
7402
+ if (bw.debug) _cw('bw.debug: set() — auto-creating intermediate "' + parts[i] + '" in path "' + key + '"');
6806
7403
  obj[parts[i]] = {};
6807
7404
  }
6808
7405
  obj = obj[parts[i]];
@@ -6822,10 +7419,10 @@
6822
7419
  /**
6823
7420
  * Get a shallow clone of the full state.
6824
7421
  */
6825
- ComponentHandle.prototype.getState = function () {
7422
+ _chp.getState = function () {
6826
7423
  var clone = {};
6827
7424
  for (var k in this._state) {
6828
- if (Object.prototype.hasOwnProperty.call(this._state, k)) {
7425
+ if (_hop.call(this._state, k)) {
6829
7426
  clone[k] = this._state[k];
6830
7427
  }
6831
7428
  }
@@ -6837,9 +7434,9 @@
6837
7434
  * @param {Object} updates - Key-value pairs to merge
6838
7435
  * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
6839
7436
  */
6840
- ComponentHandle.prototype.setState = function (updates, opts) {
7437
+ _chp.setState = function (updates, opts) {
6841
7438
  for (var k in updates) {
6842
- if (Object.prototype.hasOwnProperty.call(updates, k)) {
7439
+ if (_hop.call(updates, k)) {
6843
7440
  this._state[k] = updates[k];
6844
7441
  this._dirtyKeys[k] = true;
6845
7442
  }
@@ -6856,9 +7453,9 @@
6856
7453
  /**
6857
7454
  * Push a value onto an array in state. Clones the array.
6858
7455
  */
6859
- ComponentHandle.prototype.push = function (key, val) {
7456
+ _chp.push = function (key, val) {
6860
7457
  var arr = this.get(key);
6861
- var newArr = Array.isArray(arr) ? arr.slice() : [];
7458
+ var newArr = _isA(arr) ? arr.slice() : [];
6862
7459
  newArr.push(val);
6863
7460
  this.set(key, newArr);
6864
7461
  };
@@ -6866,9 +7463,9 @@
6866
7463
  /**
6867
7464
  * Splice an array in state. Clones the array.
6868
7465
  */
6869
- ComponentHandle.prototype.splice = function (key, start, deleteCount) {
7466
+ _chp.splice = function (key, start, deleteCount) {
6870
7467
  var arr = this.get(key);
6871
- var newArr = Array.isArray(arr) ? arr.slice() : [];
7468
+ var newArr = _isA(arr) ? arr.slice() : [];
6872
7469
  var args = [start, deleteCount].concat(Array.prototype.slice.call(arguments, 3));
6873
7470
  Array.prototype.splice.apply(newArr, args);
6874
7471
  this.set(key, newArr);
@@ -6876,7 +7473,7 @@
6876
7473
 
6877
7474
  // ── Scheduling ──
6878
7475
 
6879
- ComponentHandle.prototype._scheduleDirty = function () {
7476
+ _chp._scheduleDirty = function () {
6880
7477
  if (!this._scheduled) {
6881
7478
  this._scheduled = true;
6882
7479
  bw._dirtyComponents.push(this);
@@ -6891,16 +7488,16 @@
6891
7488
  * Creates binding descriptors with refIds for targeted DOM updates.
6892
7489
  * @private
6893
7490
  */
6894
- ComponentHandle.prototype._compileBindings = function () {
7491
+ _chp._compileBindings = function () {
6895
7492
  this._bindings = [];
6896
7493
  this._refCounter = 0;
6897
- var stateKeys = Object.keys(this._state);
7494
+ var stateKeys = _keys(this._state);
6898
7495
  var self = this;
6899
7496
  function walkTaco(taco, path) {
6900
- if (taco == null || _typeof(taco) !== 'object' || !taco.t) return taco;
7497
+ if (!_is(taco, 'object') || !taco.t) return taco;
6901
7498
 
6902
7499
  // Check content for bindings
6903
- if (typeof taco.c === 'string' && taco.c.indexOf('${') >= 0) {
7500
+ if (_is(taco.c, 'string') && taco.c.indexOf('${') >= 0) {
6904
7501
  var refId = 'bw_ref_' + self._refCounter++;
6905
7502
  var parsed = bw._parseBindings(taco.c);
6906
7503
  var deps = [];
@@ -6922,10 +7519,10 @@
6922
7519
  // Check attributes for bindings
6923
7520
  if (taco.a) {
6924
7521
  for (var attrName in taco.a) {
6925
- if (!Object.prototype.hasOwnProperty.call(taco.a, attrName)) continue;
7522
+ if (!_hop.call(taco.a, attrName)) continue;
6926
7523
  if (attrName === 'data-bw_ref') continue;
6927
7524
  var attrVal = taco.a[attrName];
6928
- if (typeof attrVal === 'string' && attrVal.indexOf('${') >= 0) {
7525
+ if (_is(attrVal, 'string') && attrVal.indexOf('${') >= 0) {
6929
7526
  var refId2 = 'bw_ref_' + self._refCounter++;
6930
7527
  var parsed2 = bw._parseBindings(attrVal);
6931
7528
  var deps2 = [];
@@ -6951,9 +7548,34 @@
6951
7548
  }
6952
7549
 
6953
7550
  // Recurse into children
6954
- if (Array.isArray(taco.c)) {
7551
+ if (_isA(taco.c)) {
6955
7552
  for (var i = 0; i < taco.c.length; i++) {
6956
- if (taco.c[i] && _typeof(taco.c[i]) === 'object' && taco.c[i].t) {
7553
+ // Wrap string children with ${expr} in a span so patches target the span, not the parent
7554
+ if (_is(taco.c[i], 'string') && taco.c[i].indexOf('${') >= 0) {
7555
+ var mixedRefId = 'bw_ref_' + self._refCounter++;
7556
+ var mixedParsed = bw._parseBindings(taco.c[i]);
7557
+ var mixedDeps = [];
7558
+ for (var mi = 0; mi < mixedParsed.length; mi++) {
7559
+ mixedDeps = mixedDeps.concat(bw._extractDeps(mixedParsed[mi].expr, stateKeys));
7560
+ }
7561
+ self._bindings.push({
7562
+ expr: taco.c[i],
7563
+ type: 'content',
7564
+ refId: mixedRefId,
7565
+ deps: mixedDeps,
7566
+ template: taco.c[i]
7567
+ });
7568
+ // Replace string with a span wrapper so textContent targets the span only
7569
+ taco.c[i] = {
7570
+ t: 'span',
7571
+ a: {
7572
+ 'data-bw_ref': mixedRefId,
7573
+ style: 'display:contents'
7574
+ },
7575
+ c: taco.c[i]
7576
+ };
7577
+ }
7578
+ if (_is(taco.c[i], 'object') && taco.c[i].t) {
6957
7579
  walkTaco(taco.c[i], path.concat(i));
6958
7580
  }
6959
7581
  // Handle bw.when/bw.each markers
@@ -6988,7 +7610,7 @@
6988
7610
  taco.c[i]._refId = eachRefId;
6989
7611
  }
6990
7612
  }
6991
- } else if (taco.c && _typeof(taco.c) === 'object' && taco.c.t) {
7613
+ } else if (_is(taco.c, 'object') && taco.c.t) {
6992
7614
  walkTaco(taco.c, path.concat(0));
6993
7615
  }
6994
7616
  return taco;
@@ -7002,7 +7624,7 @@
7002
7624
  * Build ref map from the live DOM after createDOM.
7003
7625
  * @private
7004
7626
  */
7005
- ComponentHandle.prototype._collectRefs = function () {
7627
+ _chp._collectRefs = function () {
7006
7628
  this._bw_refs = {};
7007
7629
  if (!this.element) return;
7008
7630
  var els = this.element.querySelectorAll('[data-bw_ref]');
@@ -7023,7 +7645,7 @@
7023
7645
  * Creates DOM, compiles bindings, registers actions, and calls lifecycle hooks.
7024
7646
  * @param {Element} parentEl - DOM element to mount into
7025
7647
  */
7026
- ComponentHandle.prototype.mount = function (parentEl) {
7648
+ _chp.mount = function (parentEl) {
7027
7649
  // willMount hook
7028
7650
  if (this._hooks.willMount) this._hooks.willMount(this);
7029
7651
 
@@ -7045,7 +7667,7 @@
7045
7667
  // Register named actions in function registry
7046
7668
  var self = this;
7047
7669
  for (var actionName in this._actions) {
7048
- if (Object.prototype.hasOwnProperty.call(this._actions, actionName)) {
7670
+ if (_hop.call(this._actions, actionName)) {
7049
7671
  var registeredName = this._bwId + '_' + actionName;
7050
7672
  (function (aName) {
7051
7673
  bw.funcRegister(function (evt) {
@@ -7064,6 +7686,11 @@
7064
7686
  this.element = bw.createDOM(tacoForDOM);
7065
7687
  this.element._bwComponentHandle = this;
7066
7688
  this.element.setAttribute('data-bw_comp_id', this._bwId);
7689
+
7690
+ // Restore o.render from original TACO (stripped by _tacoForDOM)
7691
+ if (this.taco.o && this.taco.o.render) {
7692
+ this.element._bw_render = this.taco.o.render;
7693
+ }
7067
7694
  if (this._userTag) {
7068
7695
  this.element.classList.add(this._userTag);
7069
7696
  }
@@ -7078,6 +7705,16 @@
7078
7705
  this._resolveAndApplyAll();
7079
7706
  this.mounted = true;
7080
7707
 
7708
+ // Scan for child ComponentHandles and link parent/child (Bug #5)
7709
+ var childEls = this.element.querySelectorAll('[data-bw_comp_id]');
7710
+ for (var ci = 0; ci < childEls.length; ci++) {
7711
+ var ch = childEls[ci]._bwComponentHandle;
7712
+ if (ch && ch !== this && !ch._parent) {
7713
+ ch._parent = this;
7714
+ this._children.push(ch);
7715
+ }
7716
+ }
7717
+
7081
7718
  // mounted hook (backward compat: fn.length === 2 wraps (el, state))
7082
7719
  if (this._hooks.mounted) {
7083
7720
  if (this._hooks.mounted.length === 2) {
@@ -7086,15 +7723,20 @@
7086
7723
  this._hooks.mounted(this);
7087
7724
  }
7088
7725
  }
7726
+
7727
+ // Invoke o.render on initial mount (if present)
7728
+ if (this.element._bw_render) {
7729
+ this.element._bw_render(this.element, this._state);
7730
+ }
7089
7731
  };
7090
7732
 
7091
7733
  /**
7092
7734
  * Prepare TACO for initial render: resolve when/each markers.
7093
7735
  * @private
7094
7736
  */
7095
- ComponentHandle.prototype._prepareTaco = function (taco) {
7096
- if (!taco || _typeof(taco) !== 'object') return;
7097
- if (Array.isArray(taco.c)) {
7737
+ _chp._prepareTaco = function (taco) {
7738
+ if (!_is(taco, 'object')) return;
7739
+ if (_isA(taco.c)) {
7098
7740
  for (var i = taco.c.length - 1; i >= 0; i--) {
7099
7741
  var child = taco.c[i];
7100
7742
  if (child && child._bwWhen) {
@@ -7135,7 +7777,7 @@
7135
7777
  var eachExprStr = child.expr.replace(/^\$\{|\}$/g, '');
7136
7778
  var arr = bw._evaluatePath(this._state, eachExprStr);
7137
7779
  var items = [];
7138
- if (Array.isArray(arr)) {
7780
+ if (_isA(arr)) {
7139
7781
  for (var j = 0; j < arr.length; j++) {
7140
7782
  items.push(child.factory(arr[j], j));
7141
7783
  }
@@ -7149,11 +7791,11 @@
7149
7791
  c: items
7150
7792
  };
7151
7793
  }
7152
- if (taco.c[i] && _typeof(taco.c[i]) === 'object' && taco.c[i].t) {
7794
+ if (_is(taco.c[i], 'object') && taco.c[i].t) {
7153
7795
  this._prepareTaco(taco.c[i]);
7154
7796
  }
7155
7797
  }
7156
- } else if (taco.c && _typeof(taco.c) === 'object' && taco.c.t) {
7798
+ } else if (_is(taco.c, 'object') && taco.c.t) {
7157
7799
  this._prepareTaco(taco.c);
7158
7800
  }
7159
7801
  };
@@ -7162,12 +7804,12 @@
7162
7804
  * Wire action name strings (in onclick etc.) to dispatch function calls.
7163
7805
  * @private
7164
7806
  */
7165
- ComponentHandle.prototype._wireActions = function (taco) {
7166
- if (!taco || _typeof(taco) !== 'object' || !taco.t) return;
7807
+ _chp._wireActions = function (taco) {
7808
+ if (!_is(taco, 'object') || !taco.t) return;
7167
7809
  if (taco.a) {
7168
7810
  for (var key in taco.a) {
7169
- if (!Object.prototype.hasOwnProperty.call(taco.a, key)) continue;
7170
- if (key.startsWith('on') && typeof taco.a[key] === 'string') {
7811
+ if (!_hop.call(taco.a, key)) continue;
7812
+ if (key.startsWith('on') && _is(taco.a[key], 'string')) {
7171
7813
  var actionName = taco.a[key];
7172
7814
  if (actionName in this._actions) {
7173
7815
  var registeredName = this._bwId + '_' + actionName;
@@ -7181,11 +7823,11 @@
7181
7823
  }
7182
7824
  }
7183
7825
  }
7184
- if (Array.isArray(taco.c)) {
7826
+ if (_isA(taco.c)) {
7185
7827
  for (var i = 0; i < taco.c.length; i++) {
7186
7828
  this._wireActions(taco.c[i]);
7187
7829
  }
7188
- } else if (taco.c && _typeof(taco.c) === 'object' && taco.c.t) {
7830
+ } else if (_is(taco.c, 'object') && taco.c.t) {
7189
7831
  this._wireActions(taco.c);
7190
7832
  }
7191
7833
  };
@@ -7194,7 +7836,7 @@
7194
7836
  * Deep-clone a TACO tree, preserving _bwWhen/_bwEach markers and their factories.
7195
7837
  * @private
7196
7838
  */
7197
- ComponentHandle.prototype._deepCloneTaco = function (taco) {
7839
+ _chp._deepCloneTaco = function (taco) {
7198
7840
  if (taco == null) return taco;
7199
7841
  // Preserve _bwWhen / _bwEach markers (contain functions)
7200
7842
  if (taco._bwWhen) {
@@ -7213,22 +7855,22 @@
7213
7855
  _refId: taco._refId
7214
7856
  };
7215
7857
  }
7216
- if (_typeof(taco) !== 'object' || !taco.t) return taco;
7858
+ if (!_is(taco, 'object') || !taco.t) return taco;
7217
7859
  var result = {
7218
7860
  t: taco.t
7219
7861
  };
7220
7862
  if (taco.a) {
7221
7863
  result.a = {};
7222
7864
  for (var k in taco.a) {
7223
- if (Object.prototype.hasOwnProperty.call(taco.a, k)) result.a[k] = taco.a[k];
7865
+ if (_hop.call(taco.a, k)) result.a[k] = taco.a[k];
7224
7866
  }
7225
7867
  }
7226
7868
  if (taco.c != null) {
7227
- if (Array.isArray(taco.c)) {
7869
+ if (_isA(taco.c)) {
7228
7870
  result.c = taco.c.map(function (child) {
7229
7871
  return this._deepCloneTaco(child);
7230
7872
  }.bind(this));
7231
- } else if (_typeof(taco.c) === 'object') {
7873
+ } else if (_is(taco.c, 'object')) {
7232
7874
  result.c = this._deepCloneTaco(taco.c);
7233
7875
  } else {
7234
7876
  result.c = taco.c;
@@ -7242,31 +7884,34 @@
7242
7884
  * Create a copy of TACO suitable for createDOM (strips o to prevent double lifecycle).
7243
7885
  * @private
7244
7886
  */
7245
- ComponentHandle.prototype._tacoForDOM = function (taco) {
7246
- if (!taco || _typeof(taco) !== 'object' || !taco.t) return taco;
7887
+ _chp._tacoForDOM = function (taco) {
7888
+ if (!_is(taco, 'object') || !taco.t) return taco;
7247
7889
  var result = {
7248
7890
  t: taco.t
7249
7891
  };
7250
7892
  if (taco.a) result.a = taco.a;
7251
7893
  if (taco.c != null) {
7252
- if (Array.isArray(taco.c)) {
7894
+ if (_isA(taco.c)) {
7253
7895
  result.c = taco.c.map(function (child) {
7254
7896
  return this._tacoForDOM(child);
7255
7897
  }.bind(this));
7256
- } else if (_typeof(taco.c) === 'object' && taco.c.t) {
7898
+ } else if (_is(taco.c, 'object') && taco.c.t) {
7257
7899
  result.c = this._tacoForDOM(taco.c);
7258
7900
  } else {
7259
7901
  result.c = taco.c;
7260
7902
  }
7261
7903
  }
7262
7904
  // Intentionally strip o (no mounted/unmount/state/render on sub-elements)
7905
+ if (taco.o && (taco.o.mounted || taco.o.render || taco.o.unmount)) {
7906
+ _cw('bw: _tacoForDOM stripped o.mounted/render/unmount from child <' + taco.t + '>. Use onclick attribute or bw.component() for child interactivity.');
7907
+ }
7263
7908
  return result;
7264
7909
  };
7265
7910
 
7266
7911
  /**
7267
7912
  * Unmount: remove from DOM, deactivate, preserve state for re-mount.
7268
7913
  */
7269
- ComponentHandle.prototype.unmount = function () {
7914
+ _chp.unmount = function () {
7270
7915
  if (!this.mounted) return;
7271
7916
 
7272
7917
  // unmount hook
@@ -7300,11 +7945,22 @@
7300
7945
  /**
7301
7946
  * Destroy: unmount + clear state + unregister actions.
7302
7947
  */
7303
- ComponentHandle.prototype.destroy = function () {
7948
+ _chp.destroy = function () {
7304
7949
  // willDestroy hook
7305
7950
  if (this._hooks.willDestroy) {
7306
7951
  this._hooks.willDestroy(this);
7307
7952
  }
7953
+
7954
+ // Cascade destroy to children depth-first (Bug #5)
7955
+ for (var ci = this._children.length - 1; ci >= 0; ci--) {
7956
+ this._children[ci].destroy();
7957
+ }
7958
+ this._children = [];
7959
+ if (this._parent) {
7960
+ var idx = this._parent._children.indexOf(this);
7961
+ if (idx >= 0) this._parent._children.splice(idx, 1);
7962
+ this._parent = null;
7963
+ }
7308
7964
  this.unmount();
7309
7965
 
7310
7966
  // Unregister actions from function registry
@@ -7331,12 +7987,37 @@
7331
7987
  * Flush dirty state: resolve changed bindings and apply to DOM.
7332
7988
  * @private
7333
7989
  */
7334
- ComponentHandle.prototype._flush = function () {
7990
+ _chp._flush = function () {
7335
7991
  this._scheduled = false;
7336
- var changedKeys = Object.keys(this._dirtyKeys);
7992
+ var changedKeys = _keys(this._dirtyKeys);
7337
7993
  this._dirtyKeys = {};
7338
7994
  if (changedKeys.length === 0 || !this.mounted) return;
7339
7995
 
7996
+ // Factory rebuild: if a BCCL factory exists and changed keys overlap factory props,
7997
+ // rebuild the TACO from the factory with merged state (Bug #6)
7998
+ if (this._factory) {
7999
+ var rebuildNeeded = false;
8000
+ for (var fi = 0; fi < changedKeys.length; fi++) {
8001
+ if (_hop.call(this._factory.props, changedKeys[fi])) {
8002
+ rebuildNeeded = true;
8003
+ break;
8004
+ }
8005
+ }
8006
+ if (rebuildNeeded) {
8007
+ var merged = {};
8008
+ for (var mk in this._factory.props) if (_hop.call(this._factory.props, mk)) merged[mk] = this._factory.props[mk];
8009
+ for (var sk in this._state) if (_hop.call(this._state, sk)) merged[sk] = this._state[sk];
8010
+ this._factory.props = merged;
8011
+ var newTaco = bw.make(this._factory.type, merged);
8012
+ newTaco._bwFactory = this._factory;
8013
+ this.taco = newTaco;
8014
+ this._originalTaco = this._deepCloneTaco(newTaco);
8015
+ this._render();
8016
+ if (this._hooks.onUpdate) this._hooks.onUpdate(this, changedKeys);
8017
+ return;
8018
+ }
8019
+ }
8020
+
7340
8021
  // willUpdate hook
7341
8022
  if (this._hooks.willUpdate) {
7342
8023
  this._hooks.willUpdate(this, changedKeys);
@@ -7374,7 +8055,7 @@
7374
8055
  * Returns list of patches to apply.
7375
8056
  * @private
7376
8057
  */
7377
- ComponentHandle.prototype._resolveBindings = function (changedKeys) {
8058
+ _chp._resolveBindings = function (changedKeys) {
7378
8059
  var patches = [];
7379
8060
  for (var i = 0; i < this._bindings.length; i++) {
7380
8061
  var b = this._bindings[i];
@@ -7410,11 +8091,14 @@
7410
8091
  * Apply patches to DOM.
7411
8092
  * @private
7412
8093
  */
7413
- ComponentHandle.prototype._applyPatches = function (patches) {
8094
+ _chp._applyPatches = function (patches) {
7414
8095
  for (var i = 0; i < patches.length; i++) {
7415
8096
  var p = patches[i];
7416
8097
  var el = this._bw_refs[p.refId];
7417
- if (!el) continue;
8098
+ if (!el) {
8099
+ if (bw.debug) _cw('bw.debug: _applyPatches — ref "' + p.refId + '" not found in DOM');
8100
+ continue;
8101
+ }
7418
8102
  if (p.type === 'content') {
7419
8103
  el.textContent = p.value;
7420
8104
  } else if (p.type === 'attribute') {
@@ -7431,7 +8115,7 @@
7431
8115
  * Resolve all bindings and apply (used for initial render).
7432
8116
  * @private
7433
8117
  */
7434
- ComponentHandle.prototype._resolveAndApplyAll = function () {
8118
+ _chp._resolveAndApplyAll = function () {
7435
8119
  var patches = [];
7436
8120
  for (var i = 0; i < this._bindings.length; i++) {
7437
8121
  var b = this._bindings[i];
@@ -7453,7 +8137,7 @@
7453
8137
  * Full re-render for structural changes (when/each branch switches).
7454
8138
  * @private
7455
8139
  */
7456
- ComponentHandle.prototype._render = function () {
8140
+ _chp._render = function () {
7457
8141
  if (!this.element || !this.element.parentNode) return;
7458
8142
  var parent = this.element.parentNode;
7459
8143
  var nextSibling = this.element.nextSibling;
@@ -7492,7 +8176,7 @@
7492
8176
  * @param {string} event - Event name (e.g., 'click')
7493
8177
  * @param {Function} handler - Event handler
7494
8178
  */
7495
- ComponentHandle.prototype.on = function (event, handler) {
8179
+ _chp.on = function (event, handler) {
7496
8180
  if (this.element) {
7497
8181
  this.element.addEventListener(event, handler);
7498
8182
  }
@@ -7507,7 +8191,7 @@
7507
8191
  * @param {string} event - Event name
7508
8192
  * @param {Function} handler - Handler to remove
7509
8193
  */
7510
- ComponentHandle.prototype.off = function (event, handler) {
8194
+ _chp.off = function (event, handler) {
7511
8195
  if (this.element) {
7512
8196
  this.element.removeEventListener(event, handler);
7513
8197
  }
@@ -7522,7 +8206,7 @@
7522
8206
  * @param {Function} handler - Handler function
7523
8207
  * @returns {Function} Unsubscribe function
7524
8208
  */
7525
- ComponentHandle.prototype.sub = function (topic, handler) {
8209
+ _chp.sub = function (topic, handler) {
7526
8210
  var unsub = bw.sub(topic, handler);
7527
8211
  this._subs.push(unsub);
7528
8212
  return unsub;
@@ -7533,10 +8217,10 @@
7533
8217
  * @param {string} name - Action name
7534
8218
  * @param {...*} args - Arguments passed after comp
7535
8219
  */
7536
- ComponentHandle.prototype.action = function (name) {
8220
+ _chp.action = function (name) {
7537
8221
  var fn = this._actions[name];
7538
8222
  if (!fn) {
7539
- console.warn('ComponentHandle.action: unknown action "' + name + '"');
8223
+ _cw('ComponentHandle.action: unknown action "' + name + '"');
7540
8224
  return;
7541
8225
  }
7542
8226
  var args = [this].concat(Array.prototype.slice.call(arguments, 1));
@@ -7548,7 +8232,7 @@
7548
8232
  * @param {string} sel - CSS selector
7549
8233
  * @returns {Element|null}
7550
8234
  */
7551
- ComponentHandle.prototype.select = function (sel) {
8235
+ _chp.select = function (sel) {
7552
8236
  return this.element ? this.element.querySelector(sel) : null;
7553
8237
  };
7554
8238
 
@@ -7557,7 +8241,7 @@
7557
8241
  * @param {string} sel - CSS selector
7558
8242
  * @returns {Element[]}
7559
8243
  */
7560
- ComponentHandle.prototype.selectAll = function (sel) {
8244
+ _chp.selectAll = function (sel) {
7561
8245
  if (!this.element) return [];
7562
8246
  return Array.prototype.slice.call(this.element.querySelectorAll(sel));
7563
8247
  };
@@ -7568,7 +8252,7 @@
7568
8252
  * @param {string} tag - User-defined identifier (e.g. 'dashboard_prod_east')
7569
8253
  * @returns {ComponentHandle} this (for chaining)
7570
8254
  */
7571
- ComponentHandle.prototype.userTag = function (tag) {
8255
+ _chp.userTag = function (tag) {
7572
8256
  this._userTag = tag;
7573
8257
  if (this.element) {
7574
8258
  this.element.classList.add(tag);
@@ -7653,7 +8337,7 @@
7653
8337
  * and calls the named method. This is the bitwrench equivalent of
7654
8338
  * Win32 SendMessage(hwnd, msg, wParam, lParam).
7655
8339
  *
7656
- * @param {string} target - Component UUID (data-bw_comp_id) or user tag (CSS class)
8340
+ * @param {string} target - Component UUID (bw_uuid_*), comp ID (data-bw_comp_id), or user tag (CSS class)
7657
8341
  * @param {string} action - Method name to call on the component
7658
8342
  * @param {*} data - Data to pass to the method
7659
8343
  * @returns {boolean} True if message was dispatched successfully
@@ -7670,15 +8354,20 @@
7670
8354
  * };
7671
8355
  */
7672
8356
  bw.message = function (target, action, data) {
7673
- // Try data-bw_comp_id attribute first, then CSS class (user tag)
7674
- var el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
7675
- if (!el) {
8357
+ // Try bw._el() first (handles UUID class, nodeMap cache, getElementById)
8358
+ var el = bw._el(target);
8359
+ // Then try data-bw_comp_id attribute
8360
+ if (!el || !el._bwComponentHandle) {
8361
+ el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
8362
+ }
8363
+ // Then try CSS class (user tag)
8364
+ if (!el || !el._bwComponentHandle) {
7676
8365
  el = bw.$('.' + target)[0];
7677
8366
  }
7678
8367
  if (!el || !el._bwComponentHandle) return false;
7679
8368
  var comp = el._bwComponentHandle;
7680
- if (typeof comp[action] !== 'function') {
7681
- console.warn('bw.message: unknown action "' + action + '" on component ' + target);
8369
+ if (!_is(comp[action], 'function')) {
8370
+ _cw('bw.message: unknown action "' + action + '" on component ' + target);
7682
8371
  return false;
7683
8372
  }
7684
8373
  comp[action](data);
@@ -7686,61 +8375,24 @@
7686
8375
  };
7687
8376
 
7688
8377
  // ===================================================================================
7689
- // bw.clientApply() / bw.clientConnect() — Server-driven UI protocol
8378
+ // bw.apply() / bw.parseJSONFlex() — Server-driven UI protocol
7690
8379
  // ===================================================================================
7691
8380
 
7692
8381
  /**
7693
8382
  * Registry of named functions sent via register messages.
7694
- * Populated by clientApply({ type: 'register', name, body }).
7695
- * Invoked by clientApply({ type: 'call', name, args }).
8383
+ * Populated by bw.apply({ type: 'register', name, body }).
8384
+ * Invoked by bw.apply({ type: 'call', name, args }).
7696
8385
  * @private
7697
8386
  */
7698
8387
  bw._clientFunctions = {};
7699
8388
 
7700
8389
  /**
7701
- * Whether exec messages are allowed. Set by clientConnect opts.allowExec.
8390
+ * Whether exec messages are allowed. Set by bwclient connect opts.allowExec.
7702
8391
  * Default false — exec messages are rejected unless explicitly opted in.
7703
8392
  * @private
7704
8393
  */
7705
8394
  bw._allowExec = false;
7706
8395
 
7707
- /**
7708
- * Built-in client functions available via call() without registration.
7709
- * @private
7710
- */
7711
- bw._builtinClientFunctions = {
7712
- scrollTo: function scrollTo(selector) {
7713
- var el = bw._el(selector);
7714
- if (el) el.scrollTop = el.scrollHeight;
7715
- },
7716
- focus: function focus(selector) {
7717
- var el = bw._el(selector);
7718
- if (el && typeof el.focus === 'function') el.focus();
7719
- },
7720
- download: function download(filename, content, mimeType) {
7721
- if (typeof document === 'undefined') return;
7722
- var blob = new Blob([content], {
7723
- type: mimeType || 'text/plain'
7724
- });
7725
- var a = document.createElement('a');
7726
- a.href = URL.createObjectURL(blob);
7727
- a.download = filename;
7728
- a.click();
7729
- URL.revokeObjectURL(a.href);
7730
- },
7731
- clipboard: function clipboard(text) {
7732
- if (typeof navigator !== 'undefined' && navigator.clipboard) {
7733
- navigator.clipboard.writeText(text);
7734
- }
7735
- },
7736
- redirect: function redirect(url) {
7737
- if (typeof window !== 'undefined') window.location.href = url;
7738
- },
7739
- log: function log() {
7740
- console.log.apply(console, arguments);
7741
- }
7742
- };
7743
-
7744
8396
  /**
7745
8397
  * Parse a bwserve protocol message string, supporting both strict JSON
7746
8398
  * and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
@@ -7755,9 +8407,9 @@
7755
8407
  * @param {string} str - JSON or r-prefixed relaxed JSON string
7756
8408
  * @returns {Object} Parsed message object
7757
8409
  * @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
7758
- * @category Server
8410
+ * @category Core
7759
8411
  */
7760
- bw.clientParse = function (str) {
8412
+ bw.parseJSONFlex = function (str) {
7761
8413
  str = (str || '').trim();
7762
8414
  if (str.charAt(0) !== 'r') return JSON.parse(str);
7763
8415
  str = str.slice(1);
@@ -7835,10 +8487,10 @@
7835
8487
  * append — target.appendChild(bw.createDOM(node))
7836
8488
  * remove — bw.cleanup(target); target.remove()
7837
8489
  * patch — bw.patch(target, content, attr)
7838
- * batch — iterate ops, call clientApply for each
8490
+ * batch — iterate ops, call bw.apply for each
7839
8491
  * message — bw.message(target, action, data)
7840
8492
  * register — store a named function for later call()
7841
- * call — invoke a registered or built-in function
8493
+ * call — invoke a registered function
7842
8494
  * exec — execute arbitrary JS (requires allowExec)
7843
8495
  *
7844
8496
  * Target resolution:
@@ -7847,9 +8499,9 @@
7847
8499
  *
7848
8500
  * @param {Object} msg - Protocol message
7849
8501
  * @returns {boolean} true if the message was applied successfully
7850
- * @category Server
8502
+ * @category Core
7851
8503
  */
7852
- bw.clientApply = function (msg) {
8504
+ bw.apply = function (msg) {
7853
8505
  if (!msg || !msg.type) return false;
7854
8506
  var type = msg.type;
7855
8507
  var target = msg.target;
@@ -7870,14 +8522,14 @@
7870
8522
  } else if (type === 'remove') {
7871
8523
  var toRemove = bw._el(target);
7872
8524
  if (!toRemove) return false;
7873
- if (typeof bw.cleanup === 'function') bw.cleanup(toRemove);
8525
+ if (_is(bw.cleanup, 'function')) bw.cleanup(toRemove);
7874
8526
  toRemove.remove();
7875
8527
  return true;
7876
8528
  } else if (type === 'batch') {
7877
- if (!Array.isArray(msg.ops)) return false;
8529
+ if (!_isA(msg.ops)) return false;
7878
8530
  var allOk = true;
7879
8531
  msg.ops.forEach(function (op) {
7880
- if (!bw.clientApply(op)) allOk = false;
8532
+ if (!bw.apply(op)) allOk = false;
7881
8533
  });
7882
8534
  return allOk;
7883
8535
  } else if (type === 'message') {
@@ -7888,24 +8540,24 @@
7888
8540
  bw._clientFunctions[msg.name] = new Function('return ' + msg.body)();
7889
8541
  return true;
7890
8542
  } catch (e) {
7891
- console.error('[bw] register error:', msg.name, e);
8543
+ _ce('[bw] register error:', msg.name, e);
7892
8544
  return false;
7893
8545
  }
7894
8546
  } else if (type === 'call') {
7895
8547
  if (!msg.name) return false;
7896
- var fn = bw._clientFunctions[msg.name] || bw._builtinClientFunctions[msg.name];
7897
- if (typeof fn !== 'function') return false;
8548
+ var fn = bw._clientFunctions[msg.name];
8549
+ if (!_is(fn, 'function')) return false;
7898
8550
  try {
7899
- var args = Array.isArray(msg.args) ? msg.args : [];
8551
+ var args = _isA(msg.args) ? msg.args : [];
7900
8552
  fn.apply(null, args);
7901
8553
  return true;
7902
8554
  } catch (e) {
7903
- console.error('[bw] call error:', msg.name, e);
8555
+ _ce('[bw] call error:', msg.name, e);
7904
8556
  return false;
7905
8557
  }
7906
8558
  } else if (type === 'exec') {
7907
8559
  if (!bw._allowExec) {
7908
- console.warn('[bw] exec rejected: allowExec is not enabled');
8560
+ _cw('[bw] exec rejected: allowExec is not enabled');
7909
8561
  return false;
7910
8562
  }
7911
8563
  if (!msg.code) return false;
@@ -7913,148 +8565,13 @@
7913
8565
  new Function(msg.code)();
7914
8566
  return true;
7915
8567
  } catch (e) {
7916
- console.error('[bw] exec error:', e);
8568
+ _ce('[bw] exec error:', e);
7917
8569
  return false;
7918
8570
  }
7919
8571
  }
7920
8572
  return false;
7921
8573
  };
7922
8574
 
7923
- /**
7924
- * Connect to a bwserve SSE endpoint and apply protocol messages automatically.
7925
- *
7926
- * Returns a connection object with sendAction(), on(), and close() methods.
7927
- *
7928
- * @param {string} url - SSE endpoint URL (e.g., '/__bw/events/client-1')
7929
- * @param {Object} [opts] - Connection options
7930
- * @param {string} [opts.transport='sse'] - Transport type: 'sse' (default) or 'poll'
7931
- * @param {number} [opts.interval=2000] - Poll interval in ms (only for 'poll' transport)
7932
- * @param {string} [opts.actionUrl] - POST endpoint for actions (default: derived from url)
7933
- * @param {boolean} [opts.reconnect=true] - Auto-reconnect on disconnect
7934
- * @param {boolean} [opts.allowExec=false] - Enable exec message type (arbitrary JS execution)
7935
- * @param {Function} [opts.onStatus] - Status callback: 'connecting'|'connected'|'disconnected'
7936
- * @param {Function} [opts.onMessage] - Raw message callback (before clientApply)
7937
- * @returns {Object} Connection object { sendAction, on, close, status }
7938
- * @category Server
7939
- */
7940
- bw.clientConnect = function (url, opts) {
7941
- opts = opts || {};
7942
- var transport = opts.transport || 'sse';
7943
- var actionUrl = opts.actionUrl || url.replace(/\/events\//, '/action/');
7944
- var reconnect = opts.reconnect !== false;
7945
- var onStatus = opts.onStatus || function () {};
7946
- var onMessage = opts.onMessage || null;
7947
- var handlers = {};
7948
- // Set the global allowExec flag from connection options
7949
- bw._allowExec = !!opts.allowExec;
7950
- var conn = {
7951
- status: 'connecting',
7952
- _es: null,
7953
- _pollTimer: null
7954
- };
7955
- function setStatus(s) {
7956
- conn.status = s;
7957
- onStatus(s);
7958
- }
7959
- function handleMessage(data) {
7960
- try {
7961
- var msg = typeof data === 'string' ? bw.clientParse(data) : data;
7962
- if (onMessage) onMessage(msg);
7963
- if (handlers.message) handlers.message(msg);
7964
- bw.clientApply(msg);
7965
- } catch (e) {
7966
- if (handlers.error) handlers.error(e);
7967
- }
7968
- }
7969
- if (transport === 'sse' && typeof EventSource !== 'undefined') {
7970
- setStatus('connecting');
7971
- var es = new EventSource(url);
7972
- conn._es = es;
7973
- es.onopen = function () {
7974
- setStatus('connected');
7975
- if (handlers.open) handlers.open();
7976
- };
7977
- es.onmessage = function (e) {
7978
- handleMessage(e.data);
7979
- };
7980
- es.onerror = function () {
7981
- if (conn.status === 'connected') {
7982
- setStatus('disconnected');
7983
- }
7984
- if (handlers.error) handlers.error(new Error('SSE connection error'));
7985
- if (!reconnect) {
7986
- es.close();
7987
- }
7988
- // EventSource auto-reconnects by default when reconnect=true
7989
- };
7990
- } else if (transport === 'poll') {
7991
- var interval = opts.interval || 2000;
7992
- setStatus('connected');
7993
- conn._pollTimer = setInterval(function () {
7994
- fetch(url).then(function (r) {
7995
- return r.json();
7996
- }).then(function (msgs) {
7997
- if (Array.isArray(msgs)) {
7998
- msgs.forEach(handleMessage);
7999
- } else if (msgs && msgs.type) {
8000
- handleMessage(msgs);
8001
- }
8002
- })["catch"](function (e) {
8003
- if (handlers.error) handlers.error(e);
8004
- });
8005
- }, interval);
8006
- }
8007
-
8008
- /**
8009
- * Send an action to the server via POST.
8010
- * @param {string} action - Action name
8011
- * @param {Object} [data] - Action payload
8012
- */
8013
- conn.sendAction = function (action, data) {
8014
- var body = JSON.stringify({
8015
- type: 'action',
8016
- action: action,
8017
- data: data || {}
8018
- });
8019
- fetch(actionUrl, {
8020
- method: 'POST',
8021
- headers: {
8022
- 'Content-Type': 'application/json'
8023
- },
8024
- body: body
8025
- })["catch"](function (e) {
8026
- if (handlers.error) handlers.error(e);
8027
- });
8028
- };
8029
-
8030
- /**
8031
- * Register an event handler.
8032
- * @param {string} event - 'open'|'message'|'error'|'close'
8033
- * @param {Function} handler
8034
- */
8035
- conn.on = function (event, handler) {
8036
- handlers[event] = handler;
8037
- return conn;
8038
- };
8039
-
8040
- /**
8041
- * Close the connection.
8042
- */
8043
- conn.close = function () {
8044
- if (conn._es) {
8045
- conn._es.close();
8046
- conn._es = null;
8047
- }
8048
- if (conn._pollTimer) {
8049
- clearInterval(conn._pollTimer);
8050
- conn._pollTimer = null;
8051
- }
8052
- setStatus('disconnected');
8053
- if (handlers.close) handlers.close();
8054
- };
8055
- return conn;
8056
- };
8057
-
8058
8575
  // ===================================================================================
8059
8576
  // bw.inspect() — Debug utility
8060
8577
  // ===================================================================================
@@ -8081,20 +8598,20 @@
8081
8598
  el = target.element;
8082
8599
  comp = target;
8083
8600
  } else {
8084
- if (typeof target === 'string') {
8601
+ if (_is(target, 'string')) {
8085
8602
  el = bw.$(target)[0];
8086
8603
  }
8087
8604
  if (!el) {
8088
- console.warn('bw.inspect: element not found');
8605
+ _cw('bw.inspect: element not found');
8089
8606
  return null;
8090
8607
  }
8091
8608
  comp = el._bwComponentHandle;
8092
8609
  }
8093
8610
  if (!comp) {
8094
- console.log('bw.inspect: no ComponentHandle on this element');
8095
- console.log(' Tag:', el.tagName);
8096
- console.log(' Classes:', el.className);
8097
- console.log(' _bw_state:', el._bw_state || '(none)');
8611
+ _cl('bw.inspect: no ComponentHandle on this element');
8612
+ _cl(' Tag:', el.tagName);
8613
+ _cl(' Classes:', el.className);
8614
+ _cl(' _bw_state:', el._bw_state || '(none)');
8098
8615
  return null;
8099
8616
  }
8100
8617
  var deps = comp._bindings.reduce(function (s, b) {
@@ -8103,13 +8620,13 @@
8103
8620
  return a.indexOf(v) === i;
8104
8621
  });
8105
8622
  console.group('Component: ' + comp._bwId);
8106
- console.log('State:', comp._state);
8107
- console.log('Bindings:', comp._bindings.length, '(deps:', deps, ')');
8108
- console.log('Methods:', Object.keys(comp._methods));
8109
- console.log('Actions:', Object.keys(comp._actions));
8110
- console.log('User tag:', comp._userTag || '(none)');
8111
- console.log('Mounted:', comp.mounted);
8112
- console.log('Element:', comp.element);
8623
+ _cl('State:', comp._state);
8624
+ _cl('Bindings:', comp._bindings.length, '(deps:', deps, ')');
8625
+ _cl('Methods:', _keys(comp._methods));
8626
+ _cl('Actions:', _keys(comp._actions));
8627
+ _cl('User tag:', comp._userTag || '(none)');
8628
+ _cl('Mounted:', comp.mounted);
8629
+ _cl('Element:', comp.element);
8113
8630
  console.groupEnd();
8114
8631
  return comp;
8115
8632
  };
@@ -8132,8 +8649,8 @@
8132
8649
  // Pre-extract all binding expressions
8133
8650
  var precompiled = [];
8134
8651
  function walkExpressions(node) {
8135
- if (!node || _typeof(node) !== 'object') return;
8136
- if (typeof node.c === 'string' && node.c.indexOf('${') >= 0) {
8652
+ if (!_is(node, 'object')) return;
8653
+ if (_is(node.c, 'string') && node.c.indexOf('${') >= 0) {
8137
8654
  var parsed = bw._parseBindings(node.c);
8138
8655
  for (var i = 0; i < parsed.length; i++) {
8139
8656
  try {
@@ -8153,9 +8670,9 @@
8153
8670
  }
8154
8671
  if (node.a) {
8155
8672
  for (var key in node.a) {
8156
- if (Object.prototype.hasOwnProperty.call(node.a, key)) {
8673
+ if (_hop.call(node.a, key)) {
8157
8674
  var v = node.a[key];
8158
- if (typeof v === 'string' && v.indexOf('${') >= 0) {
8675
+ if (_is(v, 'string') && v.indexOf('${') >= 0) {
8159
8676
  var parsed2 = bw._parseBindings(v);
8160
8677
  for (var j = 0; j < parsed2.length; j++) {
8161
8678
  try {
@@ -8176,9 +8693,9 @@
8176
8693
  }
8177
8694
  }
8178
8695
  }
8179
- if (Array.isArray(node.c)) {
8696
+ if (_isA(node.c)) {
8180
8697
  for (var k = 0; k < node.c.length; k++) walkExpressions(node.c[k]);
8181
- } else if (node.c && _typeof(node.c) === 'object' && node.c.t) {
8698
+ } else if (_is(node.c, 'object') && node.c.t) {
8182
8699
  walkExpressions(node.c);
8183
8700
  }
8184
8701
  }
@@ -8189,7 +8706,7 @@
8189
8706
  handle._precompiledBindings = precompiled;
8190
8707
  if (initialState) {
8191
8708
  for (var k in initialState) {
8192
- if (Object.prototype.hasOwnProperty.call(initialState, k)) {
8709
+ if (_hop.call(initialState, k)) {
8193
8710
  handle._state[k] = initialState[k];
8194
8711
  }
8195
8712
  }
@@ -8223,21 +8740,21 @@
8223
8740
  minify = _options$minify === void 0 ? false : _options$minify,
8224
8741
  _options$pretty = options.pretty,
8225
8742
  pretty = _options$pretty === void 0 ? !minify : _options$pretty;
8226
- if (typeof rules === 'string') return rules;
8743
+ if (_is(rules, 'string')) return rules;
8227
8744
  var css = '';
8228
8745
  var indent = pretty ? ' ' : '';
8229
8746
  var newline = pretty ? '\n' : '';
8230
8747
  var space = pretty ? ' ' : '';
8231
- if (Array.isArray(rules)) {
8748
+ if (_isA(rules)) {
8232
8749
  css = rules.map(function (rule) {
8233
8750
  return bw.css(rule, options);
8234
8751
  }).join(newline);
8235
- } else if (_typeof(rules) === 'object') {
8752
+ } else if (_is(rules, 'object')) {
8236
8753
  Object.entries(rules).forEach(function (_ref5) {
8237
8754
  var _ref6 = _slicedToArray(_ref5, 2),
8238
8755
  selector = _ref6[0],
8239
8756
  styles = _ref6[1];
8240
- if (_typeof(styles) === 'object' && !Array.isArray(styles)) {
8757
+ if (_is(styles, 'object')) {
8241
8758
  // Handle @media, @keyframes, @supports — recurse into nested block
8242
8759
  if (selector.charAt(0) === '@') {
8243
8760
  var inner = bw.css(styles, options);
@@ -8283,7 +8800,7 @@
8283
8800
  * @returns {Element} The style element
8284
8801
  * @category CSS & Styling
8285
8802
  * @see bw.css
8286
- * @see bw.loadDefaultStyles
8803
+ * @see bw.loadStyles
8287
8804
  * @example
8288
8805
  * bw.injectCSS('.my-class { color: red; }');
8289
8806
  * bw.injectCSS({ '.card': { padding: '1rem' } }, { id: 'card-styles' });
@@ -8291,7 +8808,7 @@
8291
8808
  bw.injectCSS = function (css) {
8292
8809
  var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
8293
8810
  if (!bw._isBrowser) {
8294
- console.warn('bw.injectCSS requires a DOM environment');
8811
+ _cw('bw.injectCSS requires a DOM environment');
8295
8812
  return null;
8296
8813
  }
8297
8814
  var _options$id = options.id,
@@ -8309,7 +8826,7 @@
8309
8826
  }
8310
8827
 
8311
8828
  // Convert CSS if needed
8312
- var cssStr = typeof css === 'string' ? css : bw.css(css, options);
8829
+ var cssStr = _is(css, 'string') ? css : bw.css(css, options);
8313
8830
 
8314
8831
  // Set or append CSS
8315
8832
  if (append && styleEl.textContent) {
@@ -8329,230 +8846,19 @@
8329
8846
  * @param {...Object} styles - Style objects to merge (left-to-right)
8330
8847
  * @returns {Object} Merged style object
8331
8848
  * @category CSS & Styling
8332
- * @see bw.u
8333
8849
  * @example
8334
- * var style = bw.s(bw.u.flex, bw.u.gap4, { color: 'red' });
8850
+ * var style = bw.s({ display: 'flex' }, { gap: '1rem' }, { color: 'red' });
8335
8851
  * // => { display: 'flex', gap: '1rem', color: 'red' }
8336
8852
  */
8337
8853
  bw.s = function () {
8338
8854
  var result = {};
8339
8855
  for (var i = 0; i < arguments.length; i++) {
8340
8856
  var arg = arguments[i];
8341
- if (arg && _typeof(arg) === 'object') Object.assign(result, arg);
8857
+ if (_is(arg, 'object')) Object.assign(result, arg);
8342
8858
  }
8343
8859
  return result;
8344
8860
  };
8345
8861
 
8346
- /**
8347
- * Pre-built CSS utility objects (like Tailwind utilities, but in JS).
8348
- *
8349
- * Compose with `bw.s()` to build inline styles without writing raw CSS strings.
8350
- * Includes flex, padding, margin, typography, color, border, and transition utilities.
8351
- *
8352
- * @category CSS & Styling
8353
- * @see bw.s
8354
- * @example
8355
- * { t: 'div', a: { style: bw.s(bw.u.flex, bw.u.gap4, bw.u.p4) },
8356
- * c: 'Flexbox with 1rem gap and padding' }
8357
- */
8358
- bw.u = {
8359
- // Display
8360
- flex: {
8361
- display: 'flex'
8362
- },
8363
- flexCol: {
8364
- display: 'flex',
8365
- flexDirection: 'column'
8366
- },
8367
- flexRow: {
8368
- display: 'flex',
8369
- flexDirection: 'row'
8370
- },
8371
- flexWrap: {
8372
- display: 'flex',
8373
- flexWrap: 'wrap'
8374
- },
8375
- block: {
8376
- display: 'block'
8377
- },
8378
- inline: {
8379
- display: 'inline'
8380
- },
8381
- hidden: {
8382
- display: 'none'
8383
- },
8384
- // Flex alignment
8385
- justifyCenter: {
8386
- justifyContent: 'center'
8387
- },
8388
- justifyBetween: {
8389
- justifyContent: 'space-between'
8390
- },
8391
- justifyEnd: {
8392
- justifyContent: 'flex-end'
8393
- },
8394
- alignCenter: {
8395
- alignItems: 'center'
8396
- },
8397
- alignStart: {
8398
- alignItems: 'flex-start'
8399
- },
8400
- alignEnd: {
8401
- alignItems: 'flex-end'
8402
- },
8403
- // Gap (0.25rem increments)
8404
- gap1: {
8405
- gap: '0.25rem'
8406
- },
8407
- gap2: {
8408
- gap: '0.5rem'
8409
- },
8410
- gap3: {
8411
- gap: '0.75rem'
8412
- },
8413
- gap4: {
8414
- gap: '1rem'
8415
- },
8416
- gap6: {
8417
- gap: '1.5rem'
8418
- },
8419
- gap8: {
8420
- gap: '2rem'
8421
- },
8422
- // Padding
8423
- p0: {
8424
- padding: '0'
8425
- },
8426
- p1: {
8427
- padding: '0.25rem'
8428
- },
8429
- p2: {
8430
- padding: '0.5rem'
8431
- },
8432
- p3: {
8433
- padding: '0.75rem'
8434
- },
8435
- p4: {
8436
- padding: '1rem'
8437
- },
8438
- p6: {
8439
- padding: '1.5rem'
8440
- },
8441
- p8: {
8442
- padding: '2rem'
8443
- },
8444
- px4: {
8445
- paddingLeft: '1rem',
8446
- paddingRight: '1rem'
8447
- },
8448
- py2: {
8449
- paddingTop: '0.5rem',
8450
- paddingBottom: '0.5rem'
8451
- },
8452
- py4: {
8453
- paddingTop: '1rem',
8454
- paddingBottom: '1rem'
8455
- },
8456
- // Margin (same scale)
8457
- m0: {
8458
- margin: '0'
8459
- },
8460
- m4: {
8461
- margin: '1rem'
8462
- },
8463
- mt2: {
8464
- marginTop: '0.5rem'
8465
- },
8466
- mt4: {
8467
- marginTop: '1rem'
8468
- },
8469
- mb2: {
8470
- marginBottom: '0.5rem'
8471
- },
8472
- mb4: {
8473
- marginBottom: '1rem'
8474
- },
8475
- mx_auto: {
8476
- marginLeft: 'auto',
8477
- marginRight: 'auto'
8478
- },
8479
- // Typography
8480
- textSm: {
8481
- fontSize: '0.875rem'
8482
- },
8483
- textBase: {
8484
- fontSize: '1rem'
8485
- },
8486
- textLg: {
8487
- fontSize: '1.125rem'
8488
- },
8489
- textXl: {
8490
- fontSize: '1.25rem'
8491
- },
8492
- text2xl: {
8493
- fontSize: '1.5rem'
8494
- },
8495
- text3xl: {
8496
- fontSize: '1.875rem'
8497
- },
8498
- bold: {
8499
- fontWeight: '700'
8500
- },
8501
- semibold: {
8502
- fontWeight: '600'
8503
- },
8504
- italic: {
8505
- fontStyle: 'italic'
8506
- },
8507
- textCenter: {
8508
- textAlign: 'center'
8509
- },
8510
- textRight: {
8511
- textAlign: 'right'
8512
- },
8513
- // Colors (from design tokens)
8514
- bgWhite: {
8515
- background: '#ffffff'
8516
- },
8517
- bgTeal: {
8518
- background: '#006666',
8519
- color: '#ffffff'
8520
- },
8521
- textWhite: {
8522
- color: '#ffffff'
8523
- },
8524
- textTeal: {
8525
- color: '#006666'
8526
- },
8527
- textMuted: {
8528
- color: '#888'
8529
- },
8530
- // Borders
8531
- rounded: {
8532
- borderRadius: '0.375rem'
8533
- },
8534
- roundedLg: {
8535
- borderRadius: '0.5rem'
8536
- },
8537
- roundedFull: {
8538
- borderRadius: '9999px'
8539
- },
8540
- border: {
8541
- border: '1px solid #d8d8d8'
8542
- },
8543
- // Sizing
8544
- wFull: {
8545
- width: '100%'
8546
- },
8547
- hFull: {
8548
- height: '100%'
8549
- },
8550
- // Transitions
8551
- transition: {
8552
- transition: 'all 0.2s ease'
8553
- }
8554
- };
8555
-
8556
8862
  /**
8557
8863
  * Generate responsive CSS with media query breakpoints.
8558
8864
  *
@@ -8583,7 +8889,7 @@
8583
8889
  xl: '1200px'
8584
8890
  };
8585
8891
  var parts = [];
8586
- Object.keys(breakpoints).forEach(function (key) {
8892
+ _keys(breakpoints).forEach(function (key) {
8587
8893
  var rules = {};
8588
8894
  if (key === 'base') {
8589
8895
  rules[selector] = breakpoints[key];
@@ -8655,18 +8961,18 @@
8655
8961
  if (!selector) return [];
8656
8962
 
8657
8963
  // Already an array
8658
- if (Array.isArray(selector)) return selector;
8964
+ if (_isA(selector)) return selector;
8659
8965
 
8660
8966
  // Single element
8661
8967
  if (selector.nodeType) return [selector];
8662
8968
 
8663
8969
  // NodeList or HTMLCollection
8664
- if (selector.length !== undefined && typeof selector !== 'string') {
8970
+ if (selector.length !== undefined && !_is(selector, 'string')) {
8665
8971
  return Array.from(selector);
8666
8972
  }
8667
8973
 
8668
8974
  // CSS selector string
8669
- if (typeof selector === 'string') {
8975
+ if (_is(selector, 'string')) {
8670
8976
  return Array.from(document.querySelectorAll(selector));
8671
8977
  }
8672
8978
  return [];
@@ -8678,111 +8984,48 @@
8678
8984
  };
8679
8985
  }
8680
8986
 
8681
- /**
8682
- * Load the built-in Bootstrap-inspired default stylesheet.
8683
- *
8684
- * Injects bitwrench's batteries-included CSS (buttons, cards, grids, forms,
8685
- * alerts, badges, nav, tabs, etc.) into the document head. Call once at app startup.
8686
- * Returns null in Node.js (no DOM).
8687
- *
8688
- * @param {Object} [options] - Style loading options
8689
- * @param {boolean} [options.minify=true] - Minify the CSS output
8690
- * @returns {Element|null} Style element if in browser, null in Node.js
8691
- * @category CSS & Styling
8692
- * @see bw.setTheme
8693
- * @see bw.applyTheme
8694
- * @see bw.toggleTheme
8695
- * @example
8696
- * bw.loadDefaultStyles(); // inject all default CSS
8697
- */
8698
- bw.loadDefaultStyles = function () {
8699
- var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
8700
- var _options$minify2 = options.minify,
8701
- minify = _options$minify2 === void 0 ? true : _options$minify2,
8702
- palette = options.palette;
8703
-
8704
- // 1. Inject structural CSS (layout, sizing — never changes with theme)
8705
- if (bw._isBrowser) {
8706
- var structuralCSS = bw.css(getStructuralStyles());
8707
- bw.injectCSS(structuralCSS, {
8708
- id: 'bw_structural',
8709
- append: false,
8710
- minify: minify
8711
- });
8712
- }
8987
+ // =========================================================================
8988
+ // v2.0.18 Clean Styles API makeStyles / applyStyles / loadStyles / etc.
8989
+ // =========================================================================
8713
8990
 
8714
- // 2. Inject cosmetic CSS via generateTheme (colors, shadows, radii)
8715
- var paletteConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, palette || {});
8716
- var result = bw.generateTheme('', Object.assign({}, paletteConfig, {
8717
- inject: true
8718
- }));
8719
- return result;
8720
- };
8991
+ /**
8992
+ * Convert a scope selector to a <style> element id.
8993
+ * @private
8994
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview')
8995
+ * @returns {string} Style element id (e.g. 'bw_style_my_dashboard')
8996
+ */
8997
+ function _scopeToStyleId(scope) {
8998
+ if (!scope || scope === '' || scope === 'global') return 'bw_style_global';
8999
+ if (scope === 'reset') return 'bw_style_reset';
9000
+ // Strip leading # or . and convert - to _
9001
+ var clean = scope.replace(/^[#.]/, '').replace(/-/g, '_');
9002
+ return 'bw_style_' + clean;
9003
+ }
8721
9004
 
8722
9005
  /**
8723
- * Generate a complete, scoped theme from seed colors.
8724
- *
8725
- * Produces CSS for all themed components (buttons, alerts, badges, cards,
8726
- * forms, nav, tables, tabs, list groups, pagination, progress, hero, utilities)
8727
- * scoped under `.name` class. Multiple themes can coexist in the stylesheet.
8728
- * Swap themes by changing the class on a container element.
8729
- *
8730
- * @param {string} name - CSS scope class (e.g. 'ocean'). Empty string = unscoped global.
8731
- * @param {Object} config - Theme configuration
8732
- * @param {string} config.primary - Primary brand color hex
8733
- * @param {string} config.secondary - Secondary color hex
8734
- * @param {string} [config.tertiary] - Tertiary/accent color hex (defaults to primary)
8735
- * @param {string} [config.success='#198754'] - Success color hex
8736
- * @param {string} [config.danger='#dc3545'] - Danger color hex
8737
- * @param {string} [config.warning='#ffc107'] - Warning color hex
8738
- * @param {string} [config.info='#0dcaf0'] - Info color hex
8739
- * @param {string} [config.light='#f8f9fa'] - Light color hex
8740
- * @param {string} [config.dark='#212529'] - Dark color hex
8741
- * @param {string} [config.background] - Page background hex (default: '#ffffff' light, derived dark)
8742
- * @param {string} [config.surface] - Surface/card background hex (default: '#f8f9fa' light, derived dark)
9006
+ * Generate a complete styles object from seed colors and layout config.
9007
+ * Pure function — no DOM, no state, no side effects.
9008
+ *
9009
+ * All parameters are optional. Defaults to the bitwrench default palette.
9010
+ *
9011
+ * @param {Object} [config] - Style configuration
9012
+ * @param {string} [config.primary='#006666'] - Primary brand color hex
9013
+ * @param {string} [config.secondary='#6c757d'] - Secondary color hex
9014
+ * @param {string} [config.tertiary] - Tertiary color hex (defaults to primary)
8743
9015
  * @param {string} [config.spacing='normal'] - 'compact' | 'normal' | 'spacious'
8744
9016
  * @param {string} [config.radius='md'] - 'none' | 'sm' | 'md' | 'lg' | 'pill'
8745
- * @param {number} [config.fontSize=1.0] - Base font size scale factor
8746
- * @param {string|number} [config.typeRatio='normal'] - 'tight' | 'normal' | 'relaxed' | 'dramatic' or a number
8747
- * @param {string} [config.elevation='md'] - 'flat' | 'sm' | 'md' | 'lg'
8748
- * @param {string} [config.motion='standard'] - 'reduced' | 'standard' | 'expressive'
8749
- * @param {number} [config.harmonize=0.20] - 0-1, semantic color hue shift toward primary
8750
- * @param {boolean} [config.inject=true] - Inject into DOM (browser only)
8751
- * @returns {Object} { css, palette, name, isLightPrimary, alternate: { css, palette } }
9017
+ * @returns {Object} { css, alternateCss, rules, alternateRules, palette, alternatePalette, isLightPrimary }
8752
9018
  * @category CSS & Styling
8753
- * @see bw.applyTheme
8754
- * @see bw.toggleTheme
8755
- * @see bw.loadDefaultStyles
9019
+ * @see bw.applyStyles
9020
+ * @see bw.loadStyles
8756
9021
  * @example
8757
- * // Generate and inject an ocean theme (primary + alternate)
8758
- * var theme = bw.generateTheme('ocean', {
8759
- * primary: '#0077b6',
8760
- * secondary: '#90e0ef',
8761
- * tertiary: '#00b4d8'
8762
- * });
8763
- *
8764
- * // Apply to a container
8765
- * document.getElementById('app').classList.add('ocean');
8766
- *
8767
- * // Toggle to alternate palette
8768
- * bw.toggleTheme();
8769
- *
8770
- * // Generate CSS for static export (Node.js)
8771
- * var result = bw.generateTheme('sunset', {
8772
- * primary: '#e76f51',
8773
- * secondary: '#264653',
8774
- * inject: false
8775
- * });
8776
- * fs.writeFileSync('sunset.css', result.css + result.alternate.css);
9022
+ * var styles = bw.makeStyles({ primary: '#4f46e5', secondary: '#d97706' });
9023
+ * console.log(styles.palette.primary.base); // '#4f46e5'
9024
+ * // styles.css contains all themed CSS — nothing injected
8777
9025
  */
8778
- bw.generateTheme = function (name, config) {
8779
- if (!config || !config.primary || !config.secondary) {
8780
- throw new Error('bw.generateTheme requires config.primary and config.secondary');
8781
- }
8782
-
8783
- // Merge with defaults; if user didn't supply tertiary, default to their primary
8784
- var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config);
8785
- if (!config.tertiary) fullConfig.tertiary = fullConfig.primary;
9026
+ bw.makeStyles = function (config) {
9027
+ var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config || {});
9028
+ if (config && !config.tertiary) fullConfig.tertiary = fullConfig.primary;
8786
9029
 
8787
9030
  // Derive primary palette
8788
9031
  var palette = derivePalette(fullConfig);
@@ -8790,136 +9033,211 @@
8790
9033
  // Resolve layout
8791
9034
  var layout = resolveLayout(fullConfig);
8792
9035
 
8793
- // Generate primary themed CSS rules
8794
- var themedRules = generateThemedCSS(name, palette, layout);
9036
+ // Generate primary themed CSS rules (unscoped)
9037
+ var themedRules = generateThemedCSS('', palette, layout);
8795
9038
  var cssStr = bw.css(themedRules);
8796
9039
 
8797
9040
  // Derive alternate palette (luminance-inverted)
8798
9041
  var altConfig = deriveAlternateConfig(fullConfig);
8799
9042
  var altPalette = derivePalette(altConfig);
8800
9043
 
8801
- // Generate alternate CSS scoped under .bw_theme_alt
8802
- var altRules = generateAlternateCSS(name, altPalette, layout);
8803
- var altCssStr = bw.css(altRules);
9044
+ // Generate alternate CSS rules WITHOUT .bw_theme_alt prefix (raw rules)
9045
+ // applyStyles() wraps them appropriately based on scope
9046
+ var altRawRules = generateThemedCSS('', altPalette, layout);
9047
+
9048
+ // Add body-level surface overrides for the alternate palette.
9049
+ // When .bw_theme_alt is on <html>, ".bw_theme_alt body" correctly matches.
9050
+ altRawRules['body'] = {
9051
+ 'color': altPalette.dark.base,
9052
+ 'background-color': altPalette.surface || altPalette.light.base
9053
+ };
9054
+ var altCssStr = bw.css(altRawRules);
8804
9055
 
8805
9056
  // Determine if primary is light-flavored
8806
9057
  var lightPrimary = isLightPalette(fullConfig);
9058
+ return {
9059
+ css: cssStr,
9060
+ alternateCss: altCssStr,
9061
+ rules: themedRules,
9062
+ alternateRules: altRawRules,
9063
+ palette: palette,
9064
+ alternatePalette: altPalette,
9065
+ isLightPrimary: lightPrimary
9066
+ };
9067
+ };
8807
9068
 
8808
- // Inject both CSS sets into DOM if requested
8809
- var shouldInject = config.inject !== false;
8810
- if (shouldInject && bw._isBrowser) {
8811
- var safeName = name ? name.replace(/-/g, '_') : '';
8812
- var styleId = safeName ? 'bw_theme_' + safeName : 'bw_theme_default';
8813
- var altStyleId = safeName ? 'bw_theme_' + safeName + '_alt' : 'bw_theme_default_alt';
8814
- bw.injectCSS(cssStr, {
8815
- id: styleId,
8816
- append: false
8817
- });
8818
- bw.injectCSS(altCssStr, {
8819
- id: altStyleId,
8820
- append: false
8821
- });
8822
- bw._activeThemeStyleIds = [styleId, altStyleId];
9069
+ /**
9070
+ * Inject styles into the DOM with optional scoping.
9071
+ *
9072
+ * Takes a styles object from `makeStyles()` and creates a single `<style>`
9073
+ * element in `<head>`. If a scope selector is provided, all CSS rules are
9074
+ * wrapped under that selector. Alternate CSS is wrapped under `.bw_theme_alt`.
9075
+ *
9076
+ * @param {Object} styles - Result of `bw.makeStyles()`
9077
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview'). Omit for global.
9078
+ * @returns {Element|null} The `<style>` element, or null in Node.js
9079
+ * @category CSS & Styling
9080
+ * @see bw.makeStyles
9081
+ * @see bw.loadStyles
9082
+ * @see bw.clearStyles
9083
+ * @example
9084
+ * var styles = bw.makeStyles({ primary: '#4f46e5' });
9085
+ * bw.applyStyles(styles); // global
9086
+ * bw.applyStyles(styles, '#my-dashboard'); // scoped
9087
+ */
9088
+ bw.applyStyles = function (styles, scope) {
9089
+ if (!bw._isBrowser) return null;
9090
+ if (!styles || !styles.rules) {
9091
+ _cw('bw.applyStyles: invalid styles object');
9092
+ return null;
8823
9093
  }
9094
+ var styleId = _scopeToStyleId(scope);
8824
9095
 
8825
- // Update bw.u color entries to reflect the palette
8826
- if (!name) {
8827
- bw.u.bgTeal = {
8828
- background: palette.primary.base,
8829
- color: palette.primary.textOn
8830
- };
8831
- bw.u.textTeal = {
8832
- color: palette.primary.base
8833
- };
8834
- bw.u.bgWhite = {
8835
- background: '#ffffff'
8836
- };
8837
- bw.u.textWhite = {
8838
- color: '#ffffff'
8839
- };
9096
+ // Scope the primary rules if a scope is provided
9097
+ var primaryRules = styles.rules;
9098
+ if (scope) {
9099
+ primaryRules = scopeRulesUnder(primaryRules, scope);
8840
9100
  }
8841
9101
 
8842
- // Store active theme state
8843
- var result = {
8844
- css: cssStr,
8845
- palette: palette,
8846
- name: name,
8847
- isLightPrimary: lightPrimary,
8848
- alternate: {
8849
- css: altCssStr,
8850
- palette: altPalette
9102
+ // Wrap alternate rules with .bw_theme_alt
9103
+ var altRules = styles.alternateRules;
9104
+ if (altRules) {
9105
+ if (scope) {
9106
+ // Scoped compound: #scope.bw_theme_alt .bw_card
9107
+ altRules = scopeRulesUnder(altRules, scope + '.bw_theme_alt');
9108
+ } else {
9109
+ // Global: .bw_theme_alt .bw_card
9110
+ altRules = scopeRulesUnder(altRules, '.bw_theme_alt');
8851
9111
  }
8852
- };
8853
- bw._activeTheme = result;
8854
- bw._activeThemeMode = 'primary';
8855
- return result;
9112
+ }
9113
+
9114
+ // Combine primary + alternate into one CSS string
9115
+ var combined = bw.css(primaryRules);
9116
+ if (altRules) {
9117
+ combined += '\n' + bw.css(altRules);
9118
+ }
9119
+ return bw.injectCSS(combined, {
9120
+ id: styleId,
9121
+ append: false
9122
+ });
8856
9123
  };
8857
9124
 
8858
9125
  /**
8859
- * Apply a theme mode. Switches between primary and alternate palettes
8860
- * by adding/removing the `bw_theme_alt` class on `<html>`.
9126
+ * Generate and apply styles in one call. Convenience wrapper.
8861
9127
  *
8862
- * @param {string} mode - 'primary' | 'alternate' | 'light' | 'dark'
8863
- * @returns {string} Active mode: 'primary' or 'alternate'
9128
+ * Equivalent to: `bw.applyStyles(bw.makeStyles(config), scope)`
9129
+ *
9130
+ * @param {Object} [config] - Style configuration (same as `makeStyles`)
9131
+ * @param {string} [scope] - Scope selector (same as `applyStyles`)
9132
+ * @returns {Element|null} The `<style>` element, or null in Node.js
8864
9133
  * @category CSS & Styling
8865
- * @see bw.generateTheme
8866
- * @see bw.toggleTheme
9134
+ * @see bw.makeStyles
9135
+ * @see bw.applyStyles
8867
9136
  * @example
8868
- * bw.applyTheme('alternate'); // switch to alternate palette
8869
- * bw.applyTheme('dark'); // switch to whichever palette is darker
8870
- * bw.applyTheme('primary'); // switch back to primary palette
8871
- */
8872
- bw.applyTheme = function (mode) {
8873
- if (!bw._isBrowser) return mode || 'primary';
8874
- var root = document.documentElement;
8875
- var isLight = bw._activeTheme ? bw._activeTheme.isLightPrimary : true;
8876
- var wantAlt;
8877
- 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;
8878
- if (wantAlt) {
8879
- root.classList.add('bw_theme_alt');
8880
- } else {
8881
- root.classList.remove('bw_theme_alt');
9137
+ * bw.loadStyles(); // defaults, global
9138
+ * bw.loadStyles({ primary: '#4f46e5' }); // custom, global
9139
+ * bw.loadStyles({ primary: '#4f46e5' }, '#my-dashboard'); // custom, scoped
9140
+ */
9141
+ bw.loadStyles = function (config, scope) {
9142
+ // Also inject structural CSS first (only once)
9143
+ if (bw._isBrowser) {
9144
+ var existing = document.getElementById('bw_structural');
9145
+ if (!existing) {
9146
+ var structuralCSS = bw.css(getStructuralStyles());
9147
+ bw.injectCSS(structuralCSS, {
9148
+ id: 'bw_structural',
9149
+ append: false
9150
+ });
9151
+ }
8882
9152
  }
8883
- bw._activeThemeMode = wantAlt ? 'alternate' : 'primary';
8884
- return bw._activeThemeMode;
9153
+ return bw.applyStyles(bw.makeStyles(config), scope);
8885
9154
  };
8886
9155
 
8887
9156
  /**
8888
- * Toggle between primary and alternate theme palettes.
9157
+ * Inject the CSS reset (box-sizing, html/body font, reduced-motion).
9158
+ * Idempotent — if already injected, returns the existing `<style>` element.
8889
9159
  *
9160
+ * @returns {Element|null} The `<style>` element, or null in Node.js
9161
+ * @category CSS & Styling
9162
+ * @see bw.loadStyles
9163
+ * @see bw.clearStyles
9164
+ * @example
9165
+ * bw.loadReset(); // inject once, safe to call multiple times
9166
+ */
9167
+ bw.loadReset = function () {
9168
+ if (!bw._isBrowser) return null;
9169
+ var existing = document.getElementById('bw_style_reset');
9170
+ if (existing) return existing;
9171
+ return bw.injectCSS(bw.css(getResetStyles()), {
9172
+ id: 'bw_style_reset',
9173
+ append: false
9174
+ });
9175
+ };
9176
+
9177
+ /**
9178
+ * Toggle between primary and alternate palettes.
9179
+ *
9180
+ * Adds/removes the `bw_theme_alt` class on the scoping element.
9181
+ * Without a scope, toggles on `<html>` (global).
9182
+ * With a scope, toggles on the first matching element.
9183
+ *
9184
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard'). Omit for global.
8890
9185
  * @returns {string} Active mode after toggle: 'primary' or 'alternate'
8891
9186
  * @category CSS & Styling
8892
- * @see bw.applyTheme
8893
- * @see bw.generateTheme
9187
+ * @see bw.applyStyles
9188
+ * @see bw.clearStyles
8894
9189
  * @example
8895
- * bw.toggleTheme(); // flip between primary and alternate
9190
+ * bw.toggleStyles(); // global toggle on <html>
9191
+ * bw.toggleStyles('#my-dashboard'); // scoped toggle
8896
9192
  */
8897
- bw.toggleTheme = function () {
8898
- var current = bw._activeThemeMode || 'primary';
8899
- return bw.applyTheme(current === 'primary' ? 'alternate' : 'primary');
9193
+ bw.toggleStyles = function (scope) {
9194
+ if (!bw._isBrowser) return 'primary';
9195
+ var target;
9196
+ if (scope) {
9197
+ var els = bw.$(scope);
9198
+ target = els[0];
9199
+ } else {
9200
+ target = document.documentElement;
9201
+ }
9202
+ if (!target) return 'primary';
9203
+ var hasAlt = target.classList.contains('bw_theme_alt');
9204
+ if (hasAlt) {
9205
+ target.classList.remove('bw_theme_alt');
9206
+ return 'primary';
9207
+ } else {
9208
+ target.classList.add('bw_theme_alt');
9209
+ return 'alternate';
9210
+ }
8900
9211
  };
8901
9212
 
8902
9213
  /**
8903
- * Remove the currently active theme's injected style elements from the DOM.
8904
- * Use this before generating a new theme with a different name to prevent
8905
- * stale CSS accumulation.
9214
+ * Remove injected styles for a given scope.
8906
9215
  *
9216
+ * Finds the `<style>` element by id and removes it. Also removes
9217
+ * the `bw_theme_alt` class from the relevant element.
9218
+ *
9219
+ * @param {string} [scope] - Scope selector. Omit to remove global styles.
8907
9220
  * @category CSS & Styling
8908
- * @see bw.generateTheme
9221
+ * @see bw.applyStyles
9222
+ * @see bw.loadStyles
8909
9223
  * @example
8910
- * bw.clearTheme(); // remove current theme styles
8911
- * bw.generateTheme('sunset', conf); // inject fresh theme
8912
- */
8913
- bw.clearTheme = function () {
8914
- if (bw._activeThemeStyleIds && bw._isBrowser) {
8915
- bw._activeThemeStyleIds.forEach(function (id) {
8916
- var el = document.getElementById(id);
8917
- if (el) el.remove();
8918
- });
8919
- bw._activeThemeStyleIds = null;
9224
+ * bw.clearStyles(); // remove global styles
9225
+ * bw.clearStyles('#my-dashboard'); // remove scoped styles
9226
+ * bw.clearStyles('reset'); // remove the CSS reset
9227
+ */
9228
+ bw.clearStyles = function (scope) {
9229
+ if (!bw._isBrowser) return;
9230
+ var styleId = _scopeToStyleId(scope);
9231
+ var el = document.getElementById(styleId);
9232
+ if (el) el.remove();
9233
+
9234
+ // Also remove bw_theme_alt from the relevant element
9235
+ if (scope && scope !== 'reset' && scope !== 'global') {
9236
+ var targets = bw.$(scope);
9237
+ if (targets[0]) targets[0].classList.remove('bw_theme_alt');
9238
+ } else if (!scope || scope === 'global') {
9239
+ document.documentElement.classList.remove('bw_theme_alt');
8920
9240
  }
8921
- bw._activeTheme = null;
8922
- bw._activeThemeMode = 'primary';
8923
9241
  };
8924
9242
 
8925
9243
  // Expose color utility functions on bw namespace
@@ -9141,10 +9459,15 @@
9141
9459
  * @param {Object} config - Table configuration
9142
9460
  * @param {Array<Object>} config.data - Array of row objects to display
9143
9461
  * @param {Array<Object>} [config.columns] - Column definitions with key, label, render
9144
- * @param {string} [config.className='table'] - CSS class for table element
9462
+ * @param {string} [config.className=''] - Additional CSS classes for table element
9145
9463
  * @param {boolean} [config.sortable=true] - Enable click-to-sort headers
9146
9464
  * @param {Function} [config.onSort] - Sort callback (column, direction)
9147
- * @returns {Object} TACO object for table
9465
+ * @param {boolean} [config.selectable=false] - Enable row selection on click
9466
+ * @param {Function} [config.onRowClick] - Row click callback (row, index, event)
9467
+ * @param {number} [config.pageSize] - Rows per page (enables pagination when set)
9468
+ * @param {number} [config.currentPage=1] - Current page number (1-based)
9469
+ * @param {Function} [config.onPageChange] - Page change callback (newPage)
9470
+ * @returns {Object} TACO object for table (with optional pagination controls)
9148
9471
  * @category Component Builders
9149
9472
  * @see bw.makeDataTable
9150
9473
  * @example
@@ -9156,7 +9479,12 @@
9156
9479
  * columns: [
9157
9480
  * { key: 'name', label: 'Name' },
9158
9481
  * { key: 'age', label: 'Age' }
9159
- * ]
9482
+ * ],
9483
+ * selectable: true,
9484
+ * onRowClick: function(row, i) { console.log('clicked', row.name); },
9485
+ * pageSize: 10,
9486
+ * currentPage: 1,
9487
+ * onPageChange: function(page) { console.log('page', page); }
9160
9488
  * });
9161
9489
  */
9162
9490
  bw.makeTable = function (config) {
@@ -9174,17 +9502,25 @@
9174
9502
  onSort = config.onSort,
9175
9503
  sortColumn = config.sortColumn,
9176
9504
  _config$sortDirection = config.sortDirection,
9177
- sortDirection = _config$sortDirection === void 0 ? 'asc' : _config$sortDirection;
9178
-
9179
- // Build class list: always include bw_table, add striped/hover, append user className
9505
+ sortDirection = _config$sortDirection === void 0 ? 'asc' : _config$sortDirection,
9506
+ _config$selectable = config.selectable,
9507
+ selectable = _config$selectable === void 0 ? false : _config$selectable,
9508
+ onRowClick = config.onRowClick,
9509
+ pageSize = config.pageSize,
9510
+ _config$currentPage = config.currentPage,
9511
+ currentPage = _config$currentPage === void 0 ? 1 : _config$currentPage,
9512
+ onPageChange = config.onPageChange;
9513
+
9514
+ // Build class list: always include bw_table, add striped/hover/selectable, append user className
9180
9515
  var cls = 'bw_table';
9181
9516
  if (striped) cls += ' bw_table_striped';
9182
- if (hover) cls += ' bw_table_hover';
9517
+ if (hover || selectable) cls += ' bw_table_hover';
9518
+ if (selectable) cls += ' bw_table_selectable';
9183
9519
  if (className) cls += ' ' + className;
9184
9520
  cls = cls.trim();
9185
9521
 
9186
9522
  // Auto-detect columns if not provided
9187
- var cols = columns || (data.length > 0 ? Object.keys(data[0]).map(function (key) {
9523
+ var cols = columns || (data.length > 0 ? _keys(data[0]).map(function (key) {
9188
9524
  return {
9189
9525
  key: key,
9190
9526
  label: key
@@ -9203,7 +9539,7 @@
9203
9539
  var bVal = b[currentSortColumn];
9204
9540
 
9205
9541
  // Handle different types
9206
- if (typeof aVal === 'number' && typeof bVal === 'number') {
9542
+ if (_is(aVal, 'number') && _is(bVal, 'number')) {
9207
9543
  return currentSortDirection === 'asc' ? aVal - bVal : bVal - aVal;
9208
9544
  }
9209
9545
 
@@ -9218,6 +9554,15 @@
9218
9554
  });
9219
9555
  }
9220
9556
 
9557
+ // Pagination
9558
+ var totalRows = sortedData.length;
9559
+ var totalPages = pageSize ? Math.max(1, Math.ceil(totalRows / pageSize)) : 1;
9560
+ var page = Math.max(1, Math.min(currentPage, totalPages));
9561
+ if (pageSize) {
9562
+ var start = (page - 1) * pageSize;
9563
+ sortedData = sortedData.slice(start, start + pageSize);
9564
+ }
9565
+
9221
9566
  // Create sort handler
9222
9567
  var handleSort = function handleSort(column) {
9223
9568
  if (!sortable) return;
@@ -9263,12 +9608,28 @@
9263
9608
  }
9264
9609
  };
9265
9610
 
9266
- // Build table body
9611
+ // Build table body with selectable/onRowClick support
9267
9612
  var tbody = {
9268
9613
  t: 'tbody',
9269
- c: sortedData.map(function (row) {
9614
+ c: sortedData.map(function (row, idx) {
9615
+ var globalIdx = pageSize ? (page - 1) * pageSize + idx : idx;
9616
+ var rowAttrs = {};
9617
+ if (selectable || onRowClick) {
9618
+ rowAttrs.style = 'cursor:pointer;';
9619
+ rowAttrs.onclick = function (e) {
9620
+ if (selectable) {
9621
+ // Toggle selected class on this row
9622
+ var tr = e.currentTarget;
9623
+ tr.classList.toggle('bw_table_row_selected');
9624
+ }
9625
+ if (onRowClick) {
9626
+ onRowClick(row, globalIdx, e);
9627
+ }
9628
+ };
9629
+ }
9270
9630
  return {
9271
9631
  t: 'tr',
9632
+ a: rowAttrs,
9272
9633
  c: cols.map(function (col) {
9273
9634
  return {
9274
9635
  t: 'td',
@@ -9278,13 +9639,65 @@
9278
9639
  };
9279
9640
  })
9280
9641
  };
9281
- return {
9642
+ var table = {
9282
9643
  t: 'table',
9283
9644
  a: {
9284
9645
  "class": cls
9285
9646
  },
9286
9647
  c: [thead, tbody]
9287
9648
  };
9649
+
9650
+ // If no pagination, return table directly
9651
+ if (!pageSize) return table;
9652
+
9653
+ // Build pagination controls
9654
+ var pageButtons = [];
9655
+ // Previous button
9656
+ pageButtons.push({
9657
+ t: 'button',
9658
+ a: {
9659
+ "class": 'bw_btn bw_btn_sm',
9660
+ disabled: page <= 1 ? 'disabled' : undefined,
9661
+ onclick: page > 1 && onPageChange ? function () {
9662
+ onPageChange(page - 1);
9663
+ } : undefined
9664
+ },
9665
+ c: 'Prev'
9666
+ });
9667
+ // Page info
9668
+ pageButtons.push({
9669
+ t: 'span',
9670
+ a: {
9671
+ style: 'margin:0 0.5rem;font-size:0.875rem;'
9672
+ },
9673
+ c: 'Page ' + page + ' of ' + totalPages
9674
+ });
9675
+ // Next button
9676
+ pageButtons.push({
9677
+ t: 'button',
9678
+ a: {
9679
+ "class": 'bw_btn bw_btn_sm',
9680
+ disabled: page >= totalPages ? 'disabled' : undefined,
9681
+ onclick: page < totalPages && onPageChange ? function () {
9682
+ onPageChange(page + 1);
9683
+ } : undefined
9684
+ },
9685
+ c: 'Next'
9686
+ });
9687
+ return {
9688
+ t: 'div',
9689
+ a: {
9690
+ "class": 'bw_table_paginated'
9691
+ },
9692
+ c: [table, {
9693
+ t: 'div',
9694
+ a: {
9695
+ "class": 'bw_table_pagination',
9696
+ style: 'display:flex;align-items:center;justify-content:flex-end;padding:0.5rem 0;gap:0.25rem;'
9697
+ },
9698
+ c: pageButtons
9699
+ }]
9700
+ };
9288
9701
  };
9289
9702
 
9290
9703
  /**
@@ -9327,7 +9740,7 @@
9327
9740
  headerRow = _config$headerRow === void 0 ? true : _config$headerRow,
9328
9741
  columns = config.columns,
9329
9742
  rest = _objectWithoutProperties(config, _excluded);
9330
- if (!Array.isArray(data) || data.length === 0) {
9743
+ if (!_isA(data) || data.length === 0) {
9331
9744
  return bw.makeTable(_objectSpread2({
9332
9745
  data: [],
9333
9746
  columns: columns || []
@@ -9424,7 +9837,7 @@
9424
9837
  showLabels = _config$showLabels === void 0 ? true : _config$showLabels,
9425
9838
  _config$className2 = config.className,
9426
9839
  className = _config$className2 === void 0 ? '' : _config$className2;
9427
- if (!Array.isArray(data) || data.length === 0) {
9840
+ if (!_isA(data) || data.length === 0) {
9428
9841
  return {
9429
9842
  t: 'div',
9430
9843
  a: {
@@ -9607,7 +10020,7 @@
9607
10020
  bw.render = function (element, position, taco) {
9608
10021
  var _taco$o4, _taco$o5, _taco$o6;
9609
10022
  // Get target element
9610
- var targetEl = typeof element === 'string' ? document.querySelector(element) : element;
10023
+ var targetEl = _is(element, 'string') ? document.querySelector(element) : element;
9611
10024
  if (!targetEl) {
9612
10025
  return {
9613
10026
  object_type: 'error',
@@ -9745,7 +10158,7 @@
9745
10158
  setContent: function setContent(content) {
9746
10159
  this._taco.c = content;
9747
10160
  if (this.element) {
9748
- if (typeof content === 'string') {
10161
+ if (_is(content, 'string')) {
9749
10162
  this.element.textContent = content;
9750
10163
  } else {
9751
10164
  // Re-render for complex content