bitwrench 2.0.17 → 2.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +127 -38
  2. package/dist/bitwrench-bccl.cjs.js +8 -8
  3. package/dist/bitwrench-bccl.cjs.min.js +3 -3
  4. package/dist/bitwrench-bccl.esm.js +8 -8
  5. package/dist/bitwrench-bccl.esm.min.js +3 -3
  6. package/dist/bitwrench-bccl.umd.js +8 -8
  7. package/dist/bitwrench-bccl.umd.min.js +2 -2
  8. package/dist/bitwrench-code-edit.cjs.js +1 -1
  9. package/dist/bitwrench-code-edit.cjs.min.js +1 -1
  10. package/dist/bitwrench-code-edit.es5.js +1 -1
  11. package/dist/bitwrench-code-edit.es5.min.js +1 -1
  12. package/dist/bitwrench-code-edit.esm.js +1 -1
  13. package/dist/bitwrench-code-edit.esm.min.js +1 -1
  14. package/dist/bitwrench-code-edit.umd.js +1 -1
  15. package/dist/bitwrench-code-edit.umd.min.js +1 -1
  16. package/dist/bitwrench-lean.cjs.js +941 -775
  17. package/dist/bitwrench-lean.cjs.min.js +20 -20
  18. package/dist/bitwrench-lean.es5.js +1012 -961
  19. package/dist/bitwrench-lean.es5.min.js +18 -18
  20. package/dist/bitwrench-lean.esm.js +941 -775
  21. package/dist/bitwrench-lean.esm.min.js +20 -20
  22. package/dist/bitwrench-lean.umd.js +941 -775
  23. package/dist/bitwrench-lean.umd.min.js +20 -20
  24. package/dist/bitwrench-util-css.cjs.js +236 -0
  25. package/dist/bitwrench-util-css.cjs.min.js +22 -0
  26. package/dist/bitwrench-util-css.es5.js +414 -0
  27. package/dist/bitwrench-util-css.es5.min.js +21 -0
  28. package/dist/bitwrench-util-css.esm.js +230 -0
  29. package/dist/bitwrench-util-css.esm.min.js +21 -0
  30. package/dist/bitwrench-util-css.umd.js +242 -0
  31. package/dist/bitwrench-util-css.umd.min.js +21 -0
  32. package/dist/bitwrench.cjs.js +948 -782
  33. package/dist/bitwrench.cjs.min.js +21 -21
  34. package/dist/bitwrench.css +456 -132
  35. package/dist/bitwrench.es5.js +1024 -970
  36. package/dist/bitwrench.es5.min.js +19 -19
  37. package/dist/bitwrench.esm.js +949 -783
  38. package/dist/bitwrench.esm.min.js +21 -21
  39. package/dist/bitwrench.min.css +1 -1
  40. package/dist/bitwrench.umd.js +948 -782
  41. package/dist/bitwrench.umd.min.js +21 -21
  42. package/dist/builds.json +178 -90
  43. package/dist/bwserve.cjs.js +514 -68
  44. package/dist/bwserve.esm.js +513 -69
  45. package/dist/sri.json +44 -36
  46. package/package.json +3 -2
  47. package/readme.html +136 -49
  48. package/src/bitwrench-bccl.js +7 -7
  49. package/src/bitwrench-color-utils.js +31 -9
  50. package/src/bitwrench-esm-entry.js +11 -0
  51. package/src/bitwrench-styles.js +439 -232
  52. package/src/bitwrench-util-css.js +229 -0
  53. package/src/bitwrench.js +483 -485
  54. package/src/bwserve/attach.js +57 -0
  55. package/src/bwserve/bwclient.js +141 -0
  56. package/src/bwserve/bwshell.js +102 -0
  57. package/src/bwserve/client.js +151 -1
  58. package/src/bwserve/index.js +127 -28
  59. package/src/cli/attach.js +555 -0
  60. package/src/cli/convert.js +2 -5
  61. package/src/cli/index.js +7 -0
  62. package/src/cli/inject.js +1 -1
  63. package/src/cli/serve.js +6 -2
  64. package/src/generate-css.js +11 -4
  65. package/src/vendor/html2canvas.min.js +20 -0
  66. package/src/version.js +3 -3
  67. package/src/bwserve/shell.js +0 -106
@@ -1,4 +1,4 @@
1
- /*! bitwrench-lean v2.0.17 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
1
+ /*! bitwrench-lean v2.0.18 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
2
2
  (function (global, factory) {
3
3
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
4
4
  typeof define === 'function' && define.amd ? define(factory) :
@@ -12,14 +12,14 @@
12
12
  */
13
13
 
14
14
  const VERSION_INFO = {
15
- version: '2.0.17',
15
+ version: '2.0.18',
16
16
  name: 'bitwrench',
17
17
  description: 'A library for javascript UI functions.',
18
18
  license: 'BSD-2-Clause',
19
19
  homepage: 'https://deftio.github.com/bitwrench/pages',
20
20
  repository: 'git+https://github.com/deftio/bitwrench.git',
21
21
  author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
22
- buildDate: '2026-03-13T23:15:10.823Z'
22
+ buildDate: '2026-03-17T00:50:09.505Z'
23
23
  };
24
24
 
25
25
  /**
@@ -313,13 +313,18 @@
313
313
  */
314
314
  function deriveShades(hex) {
315
315
  var rgb = colorParse(hex);
316
+ // For light input colors (L > 75), mixing toward white produces invisible borders.
317
+ // Darken instead so borders remain visible against light backgrounds.
318
+ var borderColor = hexToHsl(hex)[2] > 75
319
+ ? adjustLightness(hex, -18)
320
+ : mixColor(hex, '#ffffff', 0.60);
316
321
  return {
317
322
  base: hex,
318
323
  hover: adjustLightness(hex, -10),
319
324
  active: adjustLightness(hex, -15),
320
325
  light: mixColor(hex, '#ffffff', 0.85),
321
326
  darkText: adjustLightness(hex, -40),
322
- border: mixColor(hex, '#ffffff', 0.60),
327
+ border: borderColor,
323
328
  focus: 'rgba(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ',0.25)',
324
329
  textOn: textOnColor(hex)
325
330
  };
@@ -378,19 +383,27 @@
378
383
  alt.secondary = deriveAlternateSeed(config.secondary);
379
384
  alt.tertiary = config.tertiary ? deriveAlternateSeed(config.tertiary) : alt.primary;
380
385
 
381
- // Derive alternate surface colors from primary hue
386
+ // Derive alternate surface colors from primary hue.
387
+ // Check actual page surface brightness (not seed color brightness) to decide
388
+ // whether alternate should be dark or light. The page surface is what the
389
+ // user sees; seeds can be dark while the page is still light (default L=96).
382
390
  var priHsl = hexToHsl(config.primary);
383
391
  var h = priHsl[0];
384
- var isLight = isLightPalette(config);
392
+ var primarySurface = config.surface || hslToHex([h, 8, 96]);
393
+ var isLight = relativeLuminance(primarySurface) > 0.179;
385
394
 
386
395
  if (isLight) {
387
- // Primary is light → alternate needs dark surfaces
396
+ // Page surface is light → alternate needs dark surfaces
388
397
  alt.light = hslToHex([h, Math.min(priHsl[1], 15), 15]);
389
398
  alt.dark = hslToHex([h, 5, 88]);
399
+ alt.surface = hslToHex([h, 12, 18]);
400
+ alt.background = hslToHex([h, 10, 14]);
390
401
  } else {
391
- // Primary is dark → alternate needs light surfaces
402
+ // Page surface is dark → alternate needs light surfaces
392
403
  alt.light = hslToHex([h, Math.min(priHsl[1], 10), 96]);
393
404
  alt.dark = hslToHex([h, 10, 18]);
405
+ alt.surface = hslToHex([h, 8, 96]);
406
+ alt.background = hslToHex([h, 6, 98]);
394
407
  }
395
408
 
396
409
  // Semantic colors: harmonize toward primary, then invert for alternate
@@ -438,10 +451,18 @@
438
451
  var darkBase = config.dark || hslToHex([h, 10, 13]);
439
452
 
440
453
  // Background & surface tokens — tinted with primary hue for theme personality.
441
- // Very subtle: bg at L=98/S=6, surface at L=96/S=8.
454
+ // Saturation high enough that the hue is visible (each theme feels distinct)
455
+ // but low enough to stay neutral and readable.
442
456
  // User can override with config.background / config.surface.
443
- var bgBase = config.background || hslToHex([h, 6, 98]);
444
- var surfBase = config.surface || hslToHex([h, 8, 96]);
457
+ var bgBase = config.background || hslToHex([h, 22, 96]);
458
+ var surfBase = config.surface || hslToHex([h, 25, 94]);
459
+
460
+ // surfaceAlt: subtle background variant for striped rows, hover states, headers.
461
+ // Slightly lighter than surface in dark mode, slightly darker in light mode.
462
+ var surfHsl = hexToHsl(surfBase);
463
+ var surfAlt = surfHsl[2] <= 50
464
+ ? hslToHex([surfHsl[0], surfHsl[1], Math.min(surfHsl[2] + 8, 100)])
465
+ : hslToHex([surfHsl[0], surfHsl[1], Math.max(surfHsl[2] - 3, 0)]);
445
466
 
446
467
  var palette = {
447
468
  primary: deriveShades(config.primary),
@@ -454,7 +475,8 @@
454
475
  light: deriveShades(lightBase),
455
476
  dark: deriveShades(darkBase),
456
477
  background: bgBase,
457
- surface: surfBase
478
+ surface: surfBase,
479
+ surfaceAlt: surfAlt
458
480
  };
459
481
 
460
482
  return palette;
@@ -501,10 +523,12 @@
501
523
  5: '1.5rem', // 24px
502
524
  6: '2rem'};
503
525
 
526
+ let _S=SPACING_SCALE;
527
+
504
528
  var SPACING_PRESETS = {
505
- compact: { btn: SPACING_SCALE[1] + ' ' + SPACING_SCALE[3], card: SPACING_SCALE[3] + ' ' + SPACING_SCALE[4], alert: SPACING_SCALE[2] + ' ' + SPACING_SCALE[4], cell: SPACING_SCALE[2] + ' ' + SPACING_SCALE[3], input: SPACING_SCALE[1] + ' ' + SPACING_SCALE[3] },
506
- normal: { btn: SPACING_SCALE[2] + ' ' + SPACING_SCALE[4], card: SPACING_SCALE[5] + ' ' + SPACING_SCALE[5], alert: SPACING_SCALE[3] + ' ' + SPACING_SCALE[5], cell: SPACING_SCALE[3] + ' ' + SPACING_SCALE[4], input: SPACING_SCALE[2] + ' ' + SPACING_SCALE[3] },
507
- spacious: { btn: SPACING_SCALE[3] + ' ' + SPACING_SCALE[5], card: SPACING_SCALE[6] + ' ' + SPACING_SCALE[6], alert: SPACING_SCALE[4] + ' ' + SPACING_SCALE[5], cell: SPACING_SCALE[4] + ' ' + SPACING_SCALE[5], input: SPACING_SCALE[3] + ' ' + SPACING_SCALE[4] }
529
+ compact: { btn: _S[1] + ' ' + _S[3], card: _S[3] + ' ' + _S[4], alert: _S[2] + ' ' + _S[4], cell: _S[2] + ' ' + _S[3], input: _S[1] + ' ' + _S[3] },
530
+ normal: { btn: _S[2] + ' ' + _S[4], card: _S[5] + ' ' + _S[5], alert: _S[3] + ' ' + _S[5], cell: _S[3] + ' ' + _S[4], input: _S[2] + ' ' + _S[3] },
531
+ spacious: { btn: _S[3] + ' ' + _S[5], card: _S[6] + ' ' + _S[6], alert: _S[4] + ' ' + _S[5], cell: _S[4] + ' ' + _S[5], input: _S[3] + ' ' + _S[4] }
508
532
  };
509
533
 
510
534
  var RADIUS_PRESETS = {
@@ -616,20 +640,14 @@
616
640
  * Built-in theme presets — named color combinations
617
641
  * Each preset provides primary, secondary, and tertiary seed colors.
618
642
  */
619
- var THEME_PRESETS = {
620
- teal: { primary: '#006666', secondary: '#6c757d', tertiary: '#006666' },
621
- ocean: { primary: '#0077b6', secondary: '#90e0ef', tertiary: '#00b4d8' },
622
- sunset: { primary: '#e76f51', secondary: '#264653', tertiary: '#e9c46a' },
623
- forest: { primary: '#2d6a4f', secondary: '#95d5b2', tertiary: '#52b788' },
624
- slate: { primary: '#343a40', secondary: '#adb5bd', tertiary: '#6c757d' },
625
- rose: { primary: '#e11d48', secondary: '#fda4af', tertiary: '#fb7185' },
626
- indigo: { primary: '#4f46e5', secondary: '#a5b4fc', tertiary: '#818cf8' },
627
- amber: { primary: '#d97706', secondary: '#fbbf24', tertiary: '#f59e0b' },
628
- emerald: { primary: '#059669', secondary: '#6ee7b7', tertiary: '#34d399' },
629
- nord: { primary: '#5e81ac', secondary: '#88c0d0', tertiary: '#81a1c1' },
630
- coral: { primary: '#ef6461', secondary: '#4a7c7e', tertiary: '#e8a87c' },
631
- midnight: { primary: '#1e3a5f', secondary: '#7c8db5', tertiary: '#3d5a80' }
632
- };
643
+ var THEME_PRESETS = Object.fromEntries([
644
+ ['teal','#006666','#6c757d','#006666'],['ocean','#0077b6','#90e0ef','#00b4d8'],
645
+ ['sunset','#e76f51','#264653','#e9c46a'],['forest','#2d6a4f','#95d5b2','#52b788'],
646
+ ['slate','#343a40','#adb5bd','#6c757d'],['rose','#e11d48','#fda4af','#fb7185'],
647
+ ['indigo','#4f46e5','#a5b4fc','#818cf8'],['amber','#d97706','#fbbf24','#f59e0b'],
648
+ ['emerald','#059669','#6ee7b7','#34d399'],['nord','#5e81ac','#88c0d0','#81a1c1'],
649
+ ['coral','#ef6461','#4a7c7e','#e8a87c'],['midnight','#1e3a5f','#7c8db5','#3d5a80']
650
+ ].map(function(e) { return [e[0], {primary:e[1],secondary:e[2],tertiary:e[3]}]; }));
633
651
 
634
652
  /**
635
653
  * Resolve layout config to spacing, radius, typeScale, elevation, and motion objects.
@@ -676,6 +694,7 @@
676
694
  if (sel.includes(',')) return sel.split(',').map(function(s) { return '.' + name + ' ' + s.trim(); }).join(', ');
677
695
  return '.' + name + ' ' + sel;
678
696
  }
697
+ var _sx=scopeSelector;
679
698
 
680
699
  // =========================================================================
681
700
  // Themed CSS generators
@@ -684,12 +703,12 @@
684
703
  function generateTypographyThemed(scope, palette, layout) {
685
704
  var mot = layout.motion;
686
705
  var rules = {};
687
- rules[scopeSelector(scope, 'a')] = {
706
+ rules[_sx(scope, 'a')] = {
688
707
  'color': palette.primary.base,
689
708
  'text-decoration': 'none',
690
709
  'transition': 'color ' + mot.fast + ' ' + mot.easing
691
710
  };
692
- rules[scopeSelector(scope, 'a:hover')] = {
711
+ rules[_sx(scope, 'a:hover')] = {
693
712
  'color': palette.primary.hover,
694
713
  'text-decoration': 'underline'
695
714
  };
@@ -702,11 +721,11 @@
702
721
  var rd = layout.radius;
703
722
 
704
723
  // Base button (only when scoped — unscoped uses defaultStyles)
705
- rules[scopeSelector(scope, '.bw_btn')] = {
724
+ rules[_sx(scope, '.bw_btn')] = {
706
725
  'padding': sp.btn,
707
726
  'border-radius': rd.btn
708
727
  };
709
- rules[scopeSelector(scope, '.bw_btn:focus-visible')] = {
728
+ rules[_sx(scope, '.bw_btn:focus-visible')] = {
710
729
  'outline': '2px solid currentColor',
711
730
  'outline-offset': '2px',
712
731
  'box-shadow': '0 0 0 3px ' + palette.primary.focus
@@ -715,12 +734,12 @@
715
734
  // Variant colors handled by palette class on component root
716
735
 
717
736
  // Size variants (structural, reuse layout radius)
718
- rules[scopeSelector(scope, '.bw_btn_lg')] = {
737
+ rules[_sx(scope, '.bw_btn_lg')] = {
719
738
  'padding': '0.625rem 1.5rem',
720
739
  'font-size': '1rem',
721
740
  'border-radius': rd.btn === '50rem' ? '50rem' : (parseInt(rd.btn) + 2) + 'px'
722
741
  };
723
- rules[scopeSelector(scope, '.bw_btn_sm')] = {
742
+ rules[_sx(scope, '.bw_btn_sm')] = {
724
743
  'padding': '0.25rem 0.75rem',
725
744
  'font-size': '0.8125rem',
726
745
  'border-radius': rd.btn === '50rem' ? '50rem' : (Math.max(parseInt(rd.btn) - 1, 0)) + 'px'
@@ -734,7 +753,7 @@
734
753
  var sp = layout.spacing;
735
754
  var rd = layout.radius;
736
755
 
737
- rules[scopeSelector(scope, '.bw_alert')] = {
756
+ rules[_sx(scope, '.bw_alert')] = {
738
757
  'padding': sp.alert,
739
758
  'border-radius': rd.alert
740
759
  };
@@ -753,36 +772,36 @@
753
772
 
754
773
  var elev = layout.elevation;
755
774
  var motion = layout.motion;
756
- rules[scopeSelector(scope, '.bw_card')] = {
775
+ rules[_sx(scope, '.bw_card')] = {
757
776
  'background-color': palette.surface || '#fff',
758
777
  'border': '1px solid ' + palette.light.border,
759
778
  'border-radius': rd.card,
760
779
  'box-shadow': elev.sm,
761
780
  'transition': 'box-shadow ' + motion.normal + ' ' + motion.easing + ', transform ' + motion.normal + ' ' + motion.easing
762
781
  };
763
- rules[scopeSelector(scope, '.bw_card:hover')] = {
782
+ rules[_sx(scope, '.bw_card:hover')] = {
764
783
  'box-shadow': elev.md
765
784
  };
766
- rules[scopeSelector(scope, '.bw_card_hoverable:hover')] = {
785
+ rules[_sx(scope, '.bw_card_hoverable:hover')] = {
767
786
  'box-shadow': elev.lg
768
787
  };
769
- rules[scopeSelector(scope, '.bw_card_body')] = {
788
+ rules[_sx(scope, '.bw_card_body')] = {
770
789
  'padding': sp.card
771
790
  };
772
- rules[scopeSelector(scope, '.bw_card_header')] = {
791
+ rules[_sx(scope, '.bw_card_header')] = {
773
792
  'padding': sp.card.split(' ').map(function(v) { return (parseFloat(v) * 0.7).toFixed(3).replace(/\.?0+$/, '') + 'rem'; }).join(' '),
774
- 'background-color': palette.light.light,
793
+ 'background-color': palette.surfaceAlt,
775
794
  'border-bottom': '1px solid ' + palette.light.border
776
795
  };
777
- rules[scopeSelector(scope, '.bw_card_footer')] = {
778
- 'background-color': palette.light.light,
796
+ rules[_sx(scope, '.bw_card_footer')] = {
797
+ 'background-color': palette.surfaceAlt,
779
798
  'border-top': '1px solid ' + palette.light.border,
780
799
  'color': palette.secondary.base
781
800
  };
782
- rules[scopeSelector(scope, '.bw_card_title')] = {
801
+ rules[_sx(scope, '.bw_card_title')] = {
783
802
  'color': palette.dark.base
784
803
  };
785
- rules[scopeSelector(scope, '.bw_card_subtitle')] = {
804
+ rules[_sx(scope, '.bw_card_subtitle')] = {
786
805
  'color': palette.secondary.base
787
806
  };
788
807
 
@@ -796,55 +815,55 @@
796
815
  var sp = layout.spacing;
797
816
  var rd = layout.radius;
798
817
 
799
- rules[scopeSelector(scope, '.bw_form_control')] = {
818
+ rules[_sx(scope, '.bw_form_control')] = {
800
819
  'padding': sp.input,
801
820
  'border-radius': rd.input,
802
821
  'color': palette.dark.base,
803
822
  'background-color': palette.surface || '#fff',
804
823
  'border-color': palette.light.border
805
824
  };
806
- rules[scopeSelector(scope, '.bw_form_control:focus')] = {
825
+ rules[_sx(scope, '.bw_form_control:focus')] = {
807
826
  'border-color': palette.primary.border,
808
827
  'outline': '2px solid ' + palette.primary.base,
809
828
  'outline-offset': '-1px',
810
829
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
811
830
  };
812
- rules[scopeSelector(scope, '.bw_form_control::placeholder')] = {
831
+ rules[_sx(scope, '.bw_form_control::placeholder')] = {
813
832
  'color': palette.secondary.base
814
833
  };
815
- rules[scopeSelector(scope, '.bw_form_label')] = {
834
+ rules[_sx(scope, '.bw_form_label')] = {
816
835
  'color': palette.dark.base
817
836
  };
818
- rules[scopeSelector(scope, '.bw_form_text')] = {
837
+ rules[_sx(scope, '.bw_form_text')] = {
819
838
  'color': palette.secondary.base
820
839
  };
821
- rules[scopeSelector(scope, '.bw_form_check_input:checked')] = {
840
+ rules[_sx(scope, '.bw_form_check_input:checked')] = {
822
841
  'background-color': palette.primary.base,
823
842
  'border-color': palette.primary.base
824
843
  };
825
- rules[scopeSelector(scope, '.bw_form_check_input:focus')] = {
844
+ rules[_sx(scope, '.bw_form_check_input:focus')] = {
826
845
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
827
846
  };
828
847
  // Validation states
829
- rules[scopeSelector(scope, '.bw_form_control.bw_is_valid')] = { 'border-color': palette.success.base };
830
- rules[scopeSelector(scope, '.bw_form_control.bw_is_valid:focus')] = {
848
+ rules[_sx(scope, '.bw_form_control.bw_is_valid')] = { 'border-color': palette.success.base };
849
+ rules[_sx(scope, '.bw_form_control.bw_is_valid:focus')] = {
831
850
  'border-color': palette.success.base,
832
851
  'box-shadow': '0 0 0 0.2rem ' + palette.success.focus
833
852
  };
834
- rules[scopeSelector(scope, '.bw_form_control.bw_is_invalid')] = { 'border-color': palette.danger.base };
835
- rules[scopeSelector(scope, '.bw_form_control.bw_is_invalid:focus')] = {
853
+ rules[_sx(scope, '.bw_form_control.bw_is_invalid')] = { 'border-color': palette.danger.base };
854
+ rules[_sx(scope, '.bw_form_control.bw_is_invalid:focus')] = {
836
855
  'border-color': palette.danger.base,
837
856
  'box-shadow': '0 0 0 0.2rem ' + palette.danger.focus
838
857
  };
839
858
  // Form select
840
- rules[scopeSelector(scope, '.bw_form_select')] = {
859
+ rules[_sx(scope, '.bw_form_select')] = {
841
860
  'padding': sp.input,
842
861
  'border-radius': rd.input,
843
862
  'color': palette.dark.base,
844
863
  'background-color': palette.surface || '#fff',
845
864
  'border-color': palette.light.border
846
865
  };
847
- rules[scopeSelector(scope, '.bw_form_select:focus')] = {
866
+ rules[_sx(scope, '.bw_form_select:focus')] = {
848
867
  'border-color': palette.primary.border,
849
868
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
850
869
  };
@@ -852,43 +871,46 @@
852
871
  return rules;
853
872
  }
854
873
 
855
- function generateNavigation(scope, palette) {
874
+ function generateNavigation(scope, palette, layout) {
856
875
  var rules = {};
857
- rules[scopeSelector(scope, '.bw_navbar')] = {
858
- 'background-color': palette.light.light,
876
+ rules[_sx(scope, '.bw_navbar')] = {
877
+ 'background-color': palette.surfaceAlt,
859
878
  'border-bottom-color': palette.light.border
860
879
  };
861
- rules[scopeSelector(scope, '.bw_navbar_brand')] = {
880
+ rules[_sx(scope, '.bw_navbar_brand')] = {
862
881
  'color': palette.dark.base
863
882
  };
864
- rules[scopeSelector(scope, '.bw_navbar_nav .bw_nav_link')] = {
865
- 'color': palette.secondary.base
883
+ rules[_sx(scope, '.bw_navbar_nav .bw_nav_link')] = {
884
+ 'color': palette.secondary.base,
885
+ 'border-radius': layout.radius.btn
866
886
  };
867
- rules[scopeSelector(scope, '.bw_navbar_nav .bw_nav_link:hover')] = {
868
- 'color': palette.dark.base
887
+ rules[_sx(scope, '.bw_navbar_nav .bw_nav_link:hover')] = {
888
+ 'color': palette.dark.base,
889
+ 'background-color': palette.surfaceAlt
869
890
  };
870
- rules[scopeSelector(scope, '.bw_navbar_nav .bw_nav_link.active')] = {
891
+ rules[_sx(scope, '.bw_navbar_nav .bw_nav_link.active')] = {
871
892
  'color': palette.primary.base,
872
- 'background-color': palette.primary.focus
893
+ 'background-color': palette.primary.focus,
894
+ 'font-weight': '600'
873
895
  };
874
- rules[scopeSelector(scope, '.bw_navbar_dark')] = {
896
+ rules[_sx(scope, '.bw_navbar_dark')] = {
875
897
  'background-color': palette.dark.base,
876
898
  'border-bottom-color': palette.dark.hover
877
899
  };
878
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_navbar_brand')] = {
900
+ rules[_sx(scope, '.bw_navbar_dark .bw_navbar_brand')] = {
879
901
  'color': palette.light.base
880
902
  };
881
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_nav_link')] = {
882
- 'color': 'rgba(255,255,255,.65)'
903
+ rules[_sx(scope, '.bw_navbar_dark .bw_nav_link')] = {
904
+ 'color': palette.light.border
883
905
  };
884
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_nav_link:hover')] = {
885
- 'color': '#fff'
906
+ rules[_sx(scope, '.bw_navbar_dark .bw_nav_link:hover')] = {
907
+ 'color': palette.light.base
886
908
  };
887
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_nav_link.active')] = {
888
- 'color': '#fff',
909
+ rules[_sx(scope, '.bw_navbar_dark .bw_nav_link.active')] = {
910
+ 'color': palette.light.base,
889
911
  'font-weight': '600'
890
912
  };
891
- rules[scopeSelector(scope, '.bw_nav_pills .bw_nav_link.active')] = {
913
+ rules[_sx(scope, '.bw_nav_pills .bw_nav_link.active')] = {
892
914
  'color': palette.primary.textOn,
893
915
  'background-color': palette.primary.base
894
916
  };
@@ -899,49 +921,58 @@
899
921
  var rules = {};
900
922
  var sp = layout.spacing;
901
923
 
902
- rules[scopeSelector(scope, '.bw_table')] = {
924
+ rules[_sx(scope, '.bw_table')] = {
903
925
  'color': palette.dark.base,
904
926
  'border-color': palette.light.border
905
927
  };
906
- rules[scopeSelector(scope, '.bw_table > :not(caption) > * > *')] = {
928
+ rules[_sx(scope, '.bw_table > :not(caption) > * > *')] = {
907
929
  'padding': sp.cell,
908
930
  'border-bottom-color': palette.light.border
909
931
  };
910
- rules[scopeSelector(scope, '.bw_table > thead > tr > *')] = {
932
+ rules[_sx(scope, '.bw_table > thead > tr > *')] = {
911
933
  'color': palette.secondary.base,
912
934
  'border-bottom-color': palette.light.border,
913
- 'background-color': palette.light.light
935
+ 'background-color': palette.surfaceAlt
914
936
  };
915
- rules[scopeSelector(scope, '.bw_table_striped > tbody > tr:nth-of-type(odd) > *')] = {
916
- 'background-color': 'rgba(0, 0, 0, 0.05)'
937
+ rules[_sx(scope, '.bw_table_striped > tbody > tr:nth-of-type(odd) > *')] = {
938
+ 'background-color': palette.surfaceAlt
917
939
  };
918
- rules[scopeSelector(scope, '.bw_table_hover > tbody > tr:hover > *')] = {
940
+ rules[_sx(scope, '.bw_table_hover > tbody > tr:hover > *')] = {
919
941
  'background-color': palette.primary.focus
920
942
  };
921
- rules[scopeSelector(scope, '.bw_table_bordered')] = {
943
+ rules[_sx(scope, '.bw_table_selectable > tbody > tr')] = {
944
+ 'cursor': 'pointer'
945
+ };
946
+ rules[_sx(scope, '.bw_table > tbody > tr.bw_table_row_selected > *')] = {
947
+ 'background-color': palette.primary.light
948
+ };
949
+ rules[_sx(scope, '.bw_table_bordered')] = {
922
950
  'border-color': palette.light.border
923
951
  };
924
- rules[scopeSelector(scope, '.bw_table caption')] = {
952
+ rules[_sx(scope, '.bw_table caption')] = {
925
953
  'color': palette.secondary.base
926
954
  };
927
955
 
928
956
  return rules;
929
957
  }
930
958
 
931
- function generateTabs(scope, palette) {
932
- var rules = {};
933
- rules[scopeSelector(scope, '.bw_nav_tabs')] = {
959
+ function generateTabs(scope, palette, layout) {
960
+ var rules = {}, mo = layout.motion;
961
+ rules[_sx(scope, '.bw_nav_tabs')] = {
934
962
  'border-bottom-color': palette.light.border
935
963
  };
936
- rules[scopeSelector(scope, '.bw_nav_link')] = {
937
- 'color': palette.secondary.base
964
+ rules[_sx(scope, '.bw_nav_link')] = {
965
+ 'color': palette.secondary.base,
966
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing + ', border-color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
938
967
  };
939
- rules[scopeSelector(scope, '.bw_nav_tabs .bw_nav_link:hover')] = {
968
+ rules[_sx(scope, '.bw_nav_tabs .bw_nav_link:hover')] = {
940
969
  'color': palette.dark.base,
970
+ 'background-color': palette.surfaceAlt,
941
971
  'border-bottom-color': palette.light.border
942
972
  };
943
- rules[scopeSelector(scope, '.bw_nav_tabs .bw_nav_link.active')] = {
973
+ rules[_sx(scope, '.bw_nav_tabs .bw_nav_link.active')] = {
944
974
  'color': palette.primary.base,
975
+ 'background-color': palette.primary.focus,
945
976
  'border-bottom': '2px solid ' + palette.primary.base
946
977
  };
947
978
  return rules;
@@ -950,23 +981,25 @@
950
981
  function generateListGroups(scope, palette, layout) {
951
982
  var rules = {};
952
983
  var sp = layout.spacing;
984
+ var mo = layout.motion;
953
985
 
954
- rules[scopeSelector(scope, '.bw_list_group_item')] = {
986
+ rules[_sx(scope, '.bw_list_group_item')] = {
955
987
  'padding': sp.cell,
956
988
  'color': palette.dark.base,
957
989
  'background-color': palette.surface || '#fff',
958
- 'border-color': palette.light.border
990
+ 'border-color': palette.light.border,
991
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
959
992
  };
960
- rules[scopeSelector(scope, 'a.bw_list_group_item:hover')] = {
961
- 'background-color': palette.light.light,
993
+ rules[_sx(scope, 'a.bw_list_group_item:hover')] = {
994
+ 'background-color': palette.surfaceAlt,
962
995
  'color': palette.dark.hover
963
996
  };
964
- rules[scopeSelector(scope, '.bw_list_group_item.active')] = {
997
+ rules[_sx(scope, '.bw_list_group_item.active')] = {
965
998
  'color': palette.primary.textOn,
966
999
  'background-color': palette.primary.base,
967
1000
  'border-color': palette.primary.base
968
1001
  };
969
- rules[scopeSelector(scope, '.bw_list_group_item.disabled')] = {
1002
+ rules[_sx(scope, '.bw_list_group_item.disabled')] = {
970
1003
  'color': palette.secondary.base,
971
1004
  'background-color': palette.surface || '#fff'
972
1005
  };
@@ -974,28 +1007,37 @@
974
1007
  return rules;
975
1008
  }
976
1009
 
977
- function generatePagination(scope, palette) {
978
- var rules = {};
979
- rules[scopeSelector(scope, '.bw_page_link')] = {
1010
+ function generatePagination(scope, palette, layout) {
1011
+ var rules = {}, mo = layout.motion, rd = layout.radius;
1012
+ rules[_sx(scope, '.bw_page_item:first-child .bw_page_link')] = {
1013
+ 'border-top-left-radius': rd.btn,
1014
+ 'border-bottom-left-radius': rd.btn
1015
+ };
1016
+ rules[_sx(scope, '.bw_page_item:last-child .bw_page_link')] = {
1017
+ 'border-top-right-radius': rd.btn,
1018
+ 'border-bottom-right-radius': rd.btn
1019
+ };
1020
+ rules[_sx(scope, '.bw_page_link')] = {
980
1021
  'color': palette.primary.base,
981
1022
  'background-color': palette.surface || '#fff',
982
- 'border-color': palette.light.border
1023
+ 'border-color': palette.light.border,
1024
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
983
1025
  };
984
- rules[scopeSelector(scope, '.bw_page_link:hover')] = {
1026
+ rules[_sx(scope, '.bw_page_link:hover')] = {
985
1027
  'color': palette.primary.hover,
986
- 'background-color': palette.light.light,
1028
+ 'background-color': palette.surfaceAlt,
987
1029
  'border-color': palette.light.border
988
1030
  };
989
- rules[scopeSelector(scope, '.bw_page_link:focus')] = {
1031
+ rules[_sx(scope, '.bw_page_link:focus')] = {
990
1032
  'outline': '2px solid ' + palette.primary.base,
991
1033
  'outline-offset': '-2px'
992
1034
  };
993
- rules[scopeSelector(scope, '.bw_page_item.bw_active .bw_page_link')] = {
1035
+ rules[_sx(scope, '.bw_page_item.bw_active .bw_page_link')] = {
994
1036
  'color': palette.primary.textOn,
995
1037
  'background-color': palette.primary.base,
996
1038
  'border-color': palette.primary.base
997
1039
  };
998
- rules[scopeSelector(scope, '.bw_page_item.bw_disabled .bw_page_link')] = {
1040
+ rules[_sx(scope, '.bw_page_item.bw_disabled .bw_page_link')] = {
999
1041
  'color': palette.secondary.base,
1000
1042
  'background-color': palette.surface || '#fff',
1001
1043
  'border-color': palette.light.border
@@ -1005,12 +1047,12 @@
1005
1047
 
1006
1048
  function generateProgress(scope, palette) {
1007
1049
  var rules = {};
1008
- rules[scopeSelector(scope, '.bw_progress')] = {
1009
- 'background-color': palette.light.light,
1050
+ rules[_sx(scope, '.bw_progress')] = {
1051
+ 'background-color': palette.surfaceAlt,
1010
1052
  'box-shadow': 'inset 0 1px 2px rgba(0,0,0,.1)'
1011
1053
  };
1012
- rules[scopeSelector(scope, '.bw_progress_bar')] = {
1013
- 'color': '#fff',
1054
+ rules[_sx(scope, '.bw_progress_bar')] = {
1055
+ 'color': palette.primary.textOn,
1014
1056
  'background-color': palette.primary.base,
1015
1057
  'box-shadow': 'inset 0 -1px 0 rgba(0,0,0,.15)'
1016
1058
  };
@@ -1029,26 +1071,31 @@
1029
1071
  'color': palette.dark.base,
1030
1072
  'background-color': bg
1031
1073
  };
1032
- rules[scopeSelector(scope, 'body')] = baseReset;
1033
- // Also apply to the scope element itself so themes work on any container, not just body
1034
- if (scope) {
1035
- rules['.' + scope] = baseReset;
1036
- }
1074
+ rules[_sx(scope, 'body')] = baseReset;
1037
1075
  return rules;
1038
1076
  }
1039
1077
 
1040
- function generateBreadcrumbThemed(scope, palette) {
1041
- var rules = {};
1042
- rules[scopeSelector(scope, '.bw_breadcrumb_item + .bw_breadcrumb_item::before')] = {
1043
- 'color': palette.secondary.base
1078
+ function generateBreadcrumbThemed(scope, palette, layout) {
1079
+ var rules = {}, mo = layout.motion;
1080
+ rules[_sx(scope, '.bw_breadcrumb')] = {
1081
+ 'background-color': palette.surfaceAlt,
1082
+ 'padding': '0.625rem 1rem',
1083
+ 'border-radius': layout.radius.btn
1044
1084
  };
1045
- rules[scopeSelector(scope, '.bw_breadcrumb_item.active')] = {
1085
+ rules[_sx(scope, '.bw_breadcrumb_item + .bw_breadcrumb_item::before')] = {
1046
1086
  'color': palette.secondary.base
1047
1087
  };
1048
- rules[scopeSelector(scope, '.bw_breadcrumb_item a:hover')] = {
1088
+ rules[_sx(scope, '.bw_breadcrumb_item a')] = {
1089
+ 'color': palette.primary.base,
1090
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing
1091
+ };
1092
+ rules[_sx(scope, '.bw_breadcrumb_item a:hover')] = {
1049
1093
  'color': palette.primary.hover,
1050
1094
  'text-decoration': 'underline'
1051
1095
  };
1096
+ rules[_sx(scope, '.bw_breadcrumb_item.active')] = {
1097
+ 'color': palette.dark.base
1098
+ };
1052
1099
  return rules;
1053
1100
  }
1054
1101
 
@@ -1056,11 +1103,11 @@
1056
1103
 
1057
1104
  function generateCloseButtonThemed(scope, palette) {
1058
1105
  var rules = {};
1059
- rules[scopeSelector(scope, '.bw_close')] = {
1106
+ rules[_sx(scope, '.bw_close')] = {
1060
1107
  'color': palette.dark.base,
1061
1108
  'opacity': '0.5'
1062
1109
  };
1063
- rules[scopeSelector(scope, '.bw_close:focus')] = {
1110
+ rules[_sx(scope, '.bw_close:focus')] = {
1064
1111
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
1065
1112
  };
1066
1113
  return rules;
@@ -1068,82 +1115,94 @@
1068
1115
 
1069
1116
  function generateSectionsThemed(scope, palette) {
1070
1117
  var rules = {};
1071
- rules[scopeSelector(scope, '.bw_section_subtitle')] = {
1118
+ rules[_sx(scope, '.bw_section_subtitle')] = {
1072
1119
  'color': palette.secondary.base
1073
1120
  };
1074
- rules[scopeSelector(scope, '.bw_feature_description')] = {
1121
+ rules[_sx(scope, '.bw_feature_description')] = {
1075
1122
  'color': palette.secondary.base
1076
1123
  };
1077
- rules[scopeSelector(scope, '.bw_cta_description')] = {
1124
+ rules[_sx(scope, '.bw_cta_description')] = {
1078
1125
  'color': palette.secondary.base
1079
1126
  };
1080
1127
  return rules;
1081
1128
  }
1082
1129
 
1083
- function generateAccordionThemed(scope, palette) {
1130
+ function generateAccordionThemed(scope, palette, layout) {
1084
1131
  var rules = {};
1085
- rules[scopeSelector(scope, '.bw_accordion_item')] = {
1132
+ var rd = layout ? layout.radius : { card: '8px' };
1133
+ rules[_sx(scope, '.bw_accordion_item')] = {
1086
1134
  'background-color': palette.surface || '#fff',
1087
1135
  'border-color': palette.light.border
1088
1136
  };
1089
- rules[scopeSelector(scope, '.bw_accordion_button')] = {
1137
+ rules[_sx(scope, '.bw_accordion_item:first-child')] = {
1138
+ 'border-top-left-radius': rd.card,
1139
+ 'border-top-right-radius': rd.card
1140
+ };
1141
+ rules[_sx(scope, '.bw_accordion_item:last-child')] = {
1142
+ 'border-bottom-left-radius': rd.card,
1143
+ 'border-bottom-right-radius': rd.card
1144
+ };
1145
+ rules[_sx(scope, '.bw_accordion_button')] = {
1090
1146
  'color': palette.dark.base
1091
1147
  };
1092
- rules[scopeSelector(scope, '.bw_accordion_button:not(.bw_collapsed)')] = {
1148
+ rules[_sx(scope, '.bw_accordion_button:not(.bw_collapsed)')] = {
1093
1149
  'color': palette.primary.darkText,
1094
- 'background-color': palette.primary.light
1150
+ 'background-color': palette.primary.light,
1151
+ 'border-left': '3px solid ' + palette.primary.base
1095
1152
  };
1096
- rules[scopeSelector(scope, '.bw_accordion_button:hover')] = {
1097
- 'background-color': palette.light.light
1153
+ rules[_sx(scope, '.bw_accordion_button:hover')] = {
1154
+ 'background-color': palette.surfaceAlt
1098
1155
  };
1099
- rules[scopeSelector(scope, '.bw_accordion_button:not(.bw_collapsed):hover')] = {
1100
- 'background-color': palette.primary.hover
1156
+ rules[_sx(scope, '.bw_accordion_button:not(.bw_collapsed):hover')] = {
1157
+ 'background-color': palette.primary.base,
1158
+ 'color': palette.primary.textOn
1101
1159
  };
1102
- rules[scopeSelector(scope, '.bw_accordion_button:focus-visible')] = {
1160
+ rules[_sx(scope, '.bw_accordion_button:focus-visible')] = {
1103
1161
  'box-shadow': '0 0 0 0.2rem ' + palette.primary.focus
1104
1162
  };
1105
- rules[scopeSelector(scope, '.bw_accordion_body')] = {
1106
- 'border-top': '1px solid ' + palette.light.border
1163
+ rules[_sx(scope, '.bw_accordion_body')] = {
1164
+ 'border-top': '1px solid ' + palette.light.border,
1165
+ 'background-color': palette.surfaceAlt
1107
1166
  };
1108
1167
  return rules;
1109
1168
  }
1110
1169
 
1111
1170
  function generateCarouselThemed(scope, palette) {
1112
1171
  var rules = {};
1113
- rules[scopeSelector(scope, '.bw_carousel')] = {
1114
- 'background-color': palette.light.light
1172
+ rules[_sx(scope, '.bw_carousel')] = {
1173
+ 'background-color': palette.surfaceAlt
1115
1174
  };
1116
- rules[scopeSelector(scope, '.bw_carousel_indicator.active')] = {
1175
+ rules[_sx(scope, '.bw_carousel_indicator.active')] = {
1117
1176
  'background-color': palette.primary.base
1118
1177
  };
1119
- rules[scopeSelector(scope, '.bw_carousel_control')] = {
1120
- 'background-color': 'rgba(0,0,0,0.4)',
1121
- 'color': '#fff'
1178
+ rules[_sx(scope, '.bw_carousel_control')] = {
1179
+ 'background-color': palette.dark.base,
1180
+ 'color': palette.dark.textOn
1122
1181
  };
1123
- rules[scopeSelector(scope, '.bw_carousel_control:hover')] = {
1124
- 'background-color': 'rgba(0,0,0,0.6)'
1182
+ rules[_sx(scope, '.bw_carousel_control:hover')] = {
1183
+ 'background-color': palette.dark.hover
1125
1184
  };
1126
- rules[scopeSelector(scope, '.bw_carousel_caption')] = {
1127
- 'background': 'linear-gradient(transparent, rgba(0,0,0,0.6))',
1128
- 'color': '#fff'
1185
+ rules[_sx(scope, '.bw_carousel_caption')] = {
1186
+ 'background': 'linear-gradient(transparent, ' + palette.dark.base + ')',
1187
+ 'color': palette.dark.textOn
1129
1188
  };
1130
1189
  return rules;
1131
1190
  }
1132
1191
 
1133
1192
  function generateModalThemed(scope, palette, layout) {
1134
1193
  var rules = {};
1135
- rules[scopeSelector(scope, '.bw_modal_content')] = {
1194
+ rules[_sx(scope, '.bw_modal_content')] = {
1136
1195
  'background-color': palette.surface || '#fff',
1137
1196
  'border-color': palette.light.border,
1138
1197
  'box-shadow': layout.elevation.lg
1139
1198
  };
1140
- rules[scopeSelector(scope, '.bw_modal_header')] = {
1199
+ rules[_sx(scope, '.bw_modal_header')] = {
1141
1200
  'border-bottom-color': palette.light.border
1142
1201
  };
1143
- rules[scopeSelector(scope, '.bw_modal_footer')] = {
1202
+ rules[_sx(scope, '.bw_modal_footer')] = {
1144
1203
  'border-top-color': palette.light.border
1145
1204
  };
1146
- rules[scopeSelector(scope, '.bw_modal_title')] = {
1205
+ rules[_sx(scope, '.bw_modal_title')] = {
1147
1206
  'color': palette.dark.base
1148
1207
  };
1149
1208
  return rules;
@@ -1151,13 +1210,13 @@
1151
1210
 
1152
1211
  function generateToastThemed(scope, palette, layout) {
1153
1212
  var rules = {};
1154
- rules[scopeSelector(scope, '.bw_toast')] = {
1213
+ rules[_sx(scope, '.bw_toast')] = {
1155
1214
  'background-color': palette.surface || '#fff',
1156
- 'border-color': 'rgba(0,0,0,0.1)',
1215
+ 'border-color': palette.light.border,
1157
1216
  'box-shadow': layout.elevation.lg
1158
1217
  };
1159
- rules[scopeSelector(scope, '.bw_toast_header')] = {
1160
- 'border-bottom-color': 'rgba(0,0,0,0.05)'
1218
+ rules[_sx(scope, '.bw_toast_header')] = {
1219
+ 'border-bottom-color': palette.light.border
1161
1220
  };
1162
1221
  // Variant toast borders handled by palette class
1163
1222
  return rules;
@@ -1165,22 +1224,23 @@
1165
1224
 
1166
1225
  function generateDropdownThemed(scope, palette, layout) {
1167
1226
  var rules = {};
1168
- rules[scopeSelector(scope, '.bw_dropdown_menu')] = {
1227
+ rules[_sx(scope, '.bw_dropdown_menu')] = {
1169
1228
  'background-color': palette.surface || '#fff',
1170
1229
  'border-color': palette.light.border,
1171
1230
  'box-shadow': layout.elevation.md
1172
1231
  };
1173
- rules[scopeSelector(scope, '.bw_dropdown_item')] = {
1174
- 'color': palette.dark.base
1232
+ rules[_sx(scope, '.bw_dropdown_item')] = {
1233
+ 'color': palette.dark.base,
1234
+ 'transition': 'background-color ' + layout.motion.fast + ' ' + layout.motion.easing
1175
1235
  };
1176
- rules[scopeSelector(scope, '.bw_dropdown_item:hover')] = {
1236
+ rules[_sx(scope, '.bw_dropdown_item:hover')] = {
1177
1237
  'color': palette.dark.hover,
1178
- 'background-color': palette.light.light
1238
+ 'background-color': palette.surfaceAlt
1179
1239
  };
1180
- rules[scopeSelector(scope, '.bw_dropdown_item.disabled')] = {
1240
+ rules[_sx(scope, '.bw_dropdown_item.disabled')] = {
1181
1241
  'color': palette.secondary.base
1182
1242
  };
1183
- rules[scopeSelector(scope, '.bw_dropdown_divider')] = {
1243
+ rules[_sx(scope, '.bw_dropdown_divider')] = {
1184
1244
  'border-top-color': palette.light.border
1185
1245
  };
1186
1246
  return rules;
@@ -1188,15 +1248,15 @@
1188
1248
 
1189
1249
  function generateSwitchThemed(scope, palette) {
1190
1250
  var rules = {};
1191
- rules[scopeSelector(scope, '.bw_form_switch .bw_switch_input')] = {
1251
+ rules[_sx(scope, '.bw_form_switch .bw_switch_input')] = {
1192
1252
  'background-color': palette.secondary.base,
1193
1253
  'border-color': palette.secondary.base
1194
1254
  };
1195
- rules[scopeSelector(scope, '.bw_form_switch .bw_switch_input:checked')] = {
1255
+ rules[_sx(scope, '.bw_form_switch .bw_switch_input:checked')] = {
1196
1256
  'background-color': palette.primary.base,
1197
1257
  'border-color': palette.primary.base
1198
1258
  };
1199
- rules[scopeSelector(scope, '.bw_form_switch .bw_switch_input:focus')] = {
1259
+ rules[_sx(scope, '.bw_form_switch .bw_switch_input:focus')] = {
1200
1260
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
1201
1261
  };
1202
1262
  return rules;
@@ -1204,88 +1264,102 @@
1204
1264
 
1205
1265
  function generateSkeletonThemed(scope, palette) {
1206
1266
  var rules = {};
1207
- rules[scopeSelector(scope, '.bw_skeleton')] = {
1208
- 'background': 'linear-gradient(90deg, ' + palette.light.border + ' 25%, ' + palette.light.light + ' 37%, ' + palette.light.border + ' 63%)'
1267
+ rules[_sx(scope, '.bw_skeleton')] = {
1268
+ 'background': 'linear-gradient(90deg, ' + palette.light.border + ' 25%, ' + palette.surfaceAlt + ' 37%, ' + palette.light.border + ' 63%)'
1209
1269
  };
1210
1270
  return rules;
1211
1271
  }
1212
1272
 
1213
1273
  // generateAvatarThemed: removed — palette class on root handles variants
1214
1274
 
1215
- function generateStatCardThemed(scope, palette) {
1216
- var rules = {};
1275
+ function generateStatCardThemed(scope, palette, layout) {
1276
+ var rules = {}, mo = layout.motion, el = layout.elevation, rd = layout.radius;
1277
+ rules[_sx(scope, '.bw_stat_card')] = {
1278
+ 'background-color': palette.surface || '#fff',
1279
+ 'color': palette.dark.base,
1280
+ 'border': '1px solid ' + palette.light.border,
1281
+ 'border-radius': rd.card,
1282
+ 'box-shadow': el.sm,
1283
+ 'transition': 'box-shadow ' + mo.fast + ' ' + mo.easing + ', transform ' + mo.fast + ' ' + mo.easing
1284
+ };
1285
+ rules[_sx(scope, '.bw_stat_card:hover')] = { 'box-shadow': el.md };
1217
1286
  // Variant border colors handled by palette class
1218
- rules[scopeSelector(scope, '.bw_stat_change_up')] = { 'color': palette.success.base };
1219
- rules[scopeSelector(scope, '.bw_stat_change_down')] = { 'color': palette.danger.base };
1287
+ rules[_sx(scope, '.bw_stat_change_up')] = { 'color': palette.success.base };
1288
+ rules[_sx(scope, '.bw_stat_change_down')] = { 'color': palette.danger.base };
1220
1289
  return rules;
1221
1290
  }
1222
1291
 
1223
1292
  function generateTimelineThemed(scope, palette) {
1224
1293
  var rules = {};
1225
- rules[scopeSelector(scope, '.bw_timeline::before')] = { 'background-color': palette.light.border };
1294
+ rules[_sx(scope, '.bw_timeline::before')] = { 'background-color': palette.light.border };
1226
1295
  // Variant marker colors handled by palette class
1227
- rules[scopeSelector(scope, '.bw_timeline_date')] = { 'color': palette.secondary.base };
1296
+ rules[_sx(scope, '.bw_timeline_date')] = { 'color': palette.secondary.base };
1228
1297
  return rules;
1229
1298
  }
1230
1299
 
1231
1300
  function generateStepperThemed(scope, palette) {
1232
1301
  var rules = {};
1233
- rules[scopeSelector(scope, '.bw_step_indicator')] = {
1234
- 'background-color': palette.light.light,
1302
+ rules[_sx(scope, '.bw_step_indicator')] = {
1303
+ 'background-color': palette.surfaceAlt,
1235
1304
  'border': '2px solid ' + palette.light.border,
1236
1305
  'color': palette.secondary.base
1237
1306
  };
1238
- rules[scopeSelector(scope, '.bw_step + .bw_step::before')] = { 'background-color': palette.light.border };
1239
- rules[scopeSelector(scope, '.bw_step_active .bw_step_indicator')] = {
1307
+ rules[_sx(scope, '.bw_step + .bw_step::before')] = { 'background-color': palette.light.border };
1308
+ rules[_sx(scope, '.bw_step_active .bw_step_indicator')] = {
1240
1309
  'background-color': palette.primary.base,
1241
1310
  'color': palette.primary.textOn
1242
1311
  };
1243
- rules[scopeSelector(scope, '.bw_step_active .bw_step_label')] = {
1312
+ rules[_sx(scope, '.bw_step_active .bw_step_label')] = {
1244
1313
  'color': palette.dark.base,
1245
1314
  'font-weight': '600'
1246
1315
  };
1247
- rules[scopeSelector(scope, '.bw_step_completed .bw_step_indicator')] = {
1316
+ rules[_sx(scope, '.bw_step_completed .bw_step_indicator')] = {
1248
1317
  'background-color': palette.primary.base,
1249
1318
  'color': palette.primary.textOn
1250
1319
  };
1251
- rules[scopeSelector(scope, '.bw_step_completed .bw_step_label')] = { 'color': palette.primary.base };
1252
- rules[scopeSelector(scope, '.bw_step_completed + .bw_step::before')] = { 'background-color': palette.primary.base };
1320
+ rules[_sx(scope, '.bw_step_completed .bw_step_label')] = { 'color': palette.primary.base };
1321
+ rules[_sx(scope, '.bw_step_completed + .bw_step::before')] = { 'background-color': palette.primary.base };
1253
1322
  return rules;
1254
1323
  }
1255
1324
 
1256
1325
  function generateChipInputThemed(scope, palette) {
1257
1326
  var rules = {};
1258
- rules[scopeSelector(scope, '.bw_chip_input')] = { 'border-color': palette.light.border };
1259
- rules[scopeSelector(scope, '.bw_chip_input:focus-within')] = {
1327
+ rules[_sx(scope, '.bw_chip_input')] = {
1328
+ 'border-color': palette.light.border,
1329
+ 'background-color': palette.surface || '#fff',
1330
+ 'color': palette.dark.base
1331
+ };
1332
+ rules[_sx(scope, '.bw_chip_input:focus-within')] = {
1260
1333
  'border-color': palette.primary.base,
1261
1334
  'box-shadow': '0 0 0 0.2rem ' + palette.primary.focus
1262
1335
  };
1263
- rules[scopeSelector(scope, '.bw_chip')] = {
1264
- 'background-color': palette.light.light,
1336
+ rules[_sx(scope, '.bw_chip')] = {
1337
+ 'background-color': palette.surfaceAlt,
1265
1338
  'color': palette.dark.base
1266
1339
  };
1267
- rules[scopeSelector(scope, '.bw_chip_remove:hover')] = {
1340
+ rules[_sx(scope, '.bw_chip_remove:hover')] = {
1268
1341
  'color': palette.danger.base,
1269
1342
  'background-color': palette.danger.light
1270
1343
  };
1271
1344
  return rules;
1272
1345
  }
1273
1346
 
1274
- function generateFileUploadThemed(scope, palette) {
1275
- var rules = {};
1276
- rules[scopeSelector(scope, '.bw_file_upload')] = {
1347
+ function generateFileUploadThemed(scope, palette, layout) {
1348
+ var rules = {}, mo = layout.motion;
1349
+ rules[_sx(scope, '.bw_file_upload')] = {
1277
1350
  'border-color': palette.light.border,
1278
- 'background-color': palette.light.light
1351
+ 'background-color': palette.surfaceAlt,
1352
+ 'transition': 'border-color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
1279
1353
  };
1280
- rules[scopeSelector(scope, '.bw_file_upload:hover')] = {
1354
+ rules[_sx(scope, '.bw_file_upload:hover')] = {
1281
1355
  'border-color': palette.primary.base,
1282
1356
  'background-color': palette.primary.light
1283
1357
  };
1284
- rules[scopeSelector(scope, '.bw_file_upload:focus')] = {
1358
+ rules[_sx(scope, '.bw_file_upload:focus')] = {
1285
1359
  'outline': '2px solid ' + palette.primary.base,
1286
1360
  'outline-offset': '2px'
1287
1361
  };
1288
- rules[scopeSelector(scope, '.bw_file_upload.bw_file_upload_active')] = {
1362
+ rules[_sx(scope, '.bw_file_upload.bw_file_upload_active')] = {
1289
1363
  'border-color': palette.primary.base,
1290
1364
  'background-color': palette.primary.light,
1291
1365
  'border-style': 'solid'
@@ -1295,35 +1369,73 @@
1295
1369
 
1296
1370
  function generateRangeThemed(scope, palette) {
1297
1371
  var rules = {};
1298
- rules[scopeSelector(scope, '.bw_range')] = { 'background-color': palette.light.border };
1299
- rules[scopeSelector(scope, '.bw_range::-webkit-slider-thumb')] = {
1372
+ rules[_sx(scope, '.bw_range')] = { 'background-color': palette.light.border };
1373
+ rules[_sx(scope, '.bw_range::-webkit-slider-thumb')] = {
1300
1374
  'background-color': palette.primary.base,
1301
- 'border-color': '#fff',
1375
+ 'border-color': palette.surface || '#fff',
1302
1376
  'box-shadow': '0 1px 3px rgba(0,0,0,0.2)',
1303
1377
  'transition': 'background-color 0.15s ease-out, transform 0.15s ease-out'
1304
1378
  };
1305
- rules[scopeSelector(scope, '.bw_range::-moz-range-thumb')] = {
1379
+ rules[_sx(scope, '.bw_range::-moz-range-thumb')] = {
1306
1380
  'background-color': palette.primary.base,
1307
- 'border-color': '#fff',
1381
+ 'border-color': palette.surface || '#fff',
1308
1382
  'box-shadow': '0 1px 3px rgba(0,0,0,0.2)'
1309
1383
  };
1310
1384
  return rules;
1311
1385
  }
1312
1386
 
1313
- function generateSearchThemed(scope, palette) {
1314
- var rules = {};
1315
- rules[scopeSelector(scope, '.bw_search_clear:hover')] = { 'color': palette.dark.base };
1387
+ function generateTooltipThemed(scope, palette, layout) {
1388
+ var rules = {}, sp = layout.spacing, rd = layout.radius, el = layout.elevation, mo = layout.motion;
1389
+ rules[_sx(scope, '.bw_tooltip')] = {
1390
+ 'background-color': palette.dark.base, 'color': palette.dark.textOn,
1391
+ 'padding': sp.input, 'border-radius': rd.badge, 'box-shadow': el.md,
1392
+ 'transition': 'opacity ' + mo.fast + ' ' + mo.easing + ', transform ' + mo.fast + ' ' + mo.easing
1393
+ };
1316
1394
  return rules;
1317
1395
  }
1318
1396
 
1319
- function generateCodeDemoThemed(scope, palette) {
1397
+ function generatePopoverThemed(scope, palette, layout) {
1398
+ var rules = {}, sp = layout.spacing, rd = layout.radius, el = layout.elevation, mo = layout.motion;
1399
+ rules[_sx(scope, '.bw_popover')] = {
1400
+ 'background-color': palette.surface || '#fff', 'color': palette.dark.base,
1401
+ 'border': '1px solid ' + palette.light.border, 'border-radius': rd.card, 'box-shadow': el.lg,
1402
+ 'transition': 'opacity ' + mo.fast + ' ' + mo.easing + ', transform ' + mo.fast + ' ' + mo.easing
1403
+ };
1404
+ rules[_sx(scope, '.bw_popover_header')] = {
1405
+ 'background-color': palette.surfaceAlt, 'border-bottom': '1px solid ' + palette.light.border,
1406
+ 'padding': sp.input
1407
+ };
1408
+ rules[_sx(scope, '.bw_popover_body')] = { 'padding': sp.card };
1409
+ return rules;
1410
+ }
1411
+
1412
+ function generateSearchThemed(scope, palette, layout) {
1413
+ var rules = {}, mo = layout.motion;
1414
+ rules[_sx(scope, '.bw_search_input')] = {
1415
+ 'background-color': palette.surface || '#fff',
1416
+ 'color': palette.dark.base
1417
+ };
1418
+ rules[_sx(scope, '.bw_search_clear')] = {
1419
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
1420
+ };
1421
+ rules[_sx(scope, '.bw_search_clear:hover')] = { 'color': palette.dark.base };
1422
+ return rules;
1423
+ }
1424
+
1425
+ function generateCodeDemoThemed(scope, palette, layout) {
1320
1426
  var rules = {};
1321
- rules[scopeSelector(scope, '.bw_code_copy_btn_copied')] = {
1427
+ var rd = layout ? layout.radius : { card: '0.375rem' };
1428
+ rules[_sx(scope, '.bw_code_demo')] = {
1429
+ 'background-color': palette.surface || '#fff',
1430
+ 'color': palette.dark.base,
1431
+ 'border-radius': rd.card
1432
+ };
1433
+ rules[_sx(scope, '.bw_code_copy_btn_copied')] = {
1322
1434
  'background': palette.success.base,
1323
1435
  'color': palette.success.textOn,
1324
1436
  'border-color': palette.success.base
1325
1437
  };
1326
- rules[scopeSelector(scope, '.bw_copy_btn:hover')] = {
1438
+ rules[_sx(scope, '.bw_copy_btn:hover')] = {
1327
1439
  'background': 'rgba(255,255,255,0.2)',
1328
1440
  'color': '#fff'
1329
1441
  };
@@ -1333,7 +1445,7 @@
1333
1445
  function generateNavPillsThemed(scope, palette, layout) {
1334
1446
  var rules = {};
1335
1447
  var rd = layout.radius;
1336
- rules[scopeSelector(scope, '.bw_nav_pills .bw_nav_link')] = { 'border-radius': rd.btn };
1448
+ rules[_sx(scope, '.bw_nav_pills .bw_nav_link')] = { 'border-radius': rd.btn };
1337
1449
  return rules;
1338
1450
  }
1339
1451
 
@@ -1359,21 +1471,21 @@
1359
1471
  var s = palette[k];
1360
1472
 
1361
1473
  // --- Root palette class: sets default bg/color/border ---
1362
- rules[scopeSelector(scope, '.bw_' + k)] = {
1474
+ rules[_sx(scope, '.bw_' + k)] = {
1363
1475
  'background-color': s.base,
1364
1476
  'color': s.textOn,
1365
1477
  'border-color': s.base
1366
1478
  };
1367
1479
 
1368
1480
  // --- Pseudo-states (shared across all components) ---
1369
- rules[scopeSelector(scope, '.bw_' + k + ':hover')] = {
1481
+ rules[_sx(scope, '.bw_' + k + ':hover')] = {
1370
1482
  'background-color': s.hover,
1371
1483
  'border-color': s.active
1372
1484
  };
1373
- rules[scopeSelector(scope, '.bw_' + k + ':active')] = {
1485
+ rules[_sx(scope, '.bw_' + k + ':active')] = {
1374
1486
  'background-color': s.active
1375
1487
  };
1376
- rules[scopeSelector(scope, '.bw_' + k + ':focus-visible')] = {
1488
+ rules[_sx(scope, '.bw_' + k + ':focus-visible')] = {
1377
1489
  'box-shadow': '0 0 0 3px ' + s.focus,
1378
1490
  'outline': 'none'
1379
1491
  };
@@ -1381,70 +1493,99 @@
1381
1493
  // --- Component-specific overrides ---
1382
1494
 
1383
1495
  // Alerts: light bg, dark text, subtle border
1384
- rules[scopeSelector(scope, '.bw_alert.bw_' + k)] = {
1496
+ rules[_sx(scope, '.bw_alert.bw_' + k)] = {
1385
1497
  'background-color': s.light,
1386
1498
  'color': s.darkText,
1387
1499
  'border-color': s.border
1388
1500
  };
1389
1501
 
1390
1502
  // Toast: inherit bg, left border accent
1391
- rules[scopeSelector(scope, '.bw_toast.bw_' + k)] = {
1503
+ rules[_sx(scope, '.bw_toast.bw_' + k)] = {
1392
1504
  'background-color': 'inherit',
1393
1505
  'color': 'inherit',
1394
1506
  'border-left': '4px solid ' + s.base
1395
1507
  };
1396
1508
 
1397
1509
  // Stat card: inherit bg, left border accent
1398
- rules[scopeSelector(scope, '.bw_stat_card.bw_' + k)] = {
1510
+ rules[_sx(scope, '.bw_stat_card.bw_' + k)] = {
1399
1511
  'background-color': 'inherit',
1400
1512
  'color': 'inherit',
1401
1513
  'border-left-color': s.base
1402
1514
  };
1403
1515
 
1404
1516
  // Card accent: left border accent, inherit bg
1405
- rules[scopeSelector(scope, '.bw_card.bw_' + k)] = {
1517
+ rules[_sx(scope, '.bw_card.bw_' + k)] = {
1406
1518
  'background-color': 'inherit',
1407
1519
  'color': 'inherit',
1408
1520
  'border-left': '4px solid ' + s.base
1409
1521
  };
1410
1522
 
1411
1523
  // Timeline marker: colored dot
1412
- rules[scopeSelector(scope, '.bw_timeline_marker.bw_' + k)] = {
1524
+ rules[_sx(scope, '.bw_timeline_marker.bw_' + k)] = {
1413
1525
  'box-shadow': '0 0 0 2px ' + s.base
1414
1526
  };
1415
1527
 
1416
- // Spinner: text color only, transparent bg
1417
- rules[scopeSelector(scope, '.bw_spinner_border.bw_' + k + ',\n' + scopeSelector(scope, '.bw_spinner_grow.bw_' + k))] = {
1528
+ // Spinner: set color, re-apply border pattern so the root palette class
1529
+ // border-color doesn't fill in the transparent gap that makes it spin.
1530
+ // Also neutralize hover/active which would override border-right-color.
1531
+ rules[_sx(scope, '.bw_spinner_border.bw_' + k)] = {
1418
1532
  'background-color': 'transparent',
1419
1533
  'color': s.base,
1420
- 'border-color': 'currentColor'
1534
+ 'border-color': s.base,
1535
+ 'border-right-color': 'transparent'
1536
+ };
1537
+ rules[_sx(scope, '.bw_spinner_border.bw_' + k + ':hover')] = {
1538
+ 'background-color': 'transparent',
1539
+ 'border-color': s.base,
1540
+ 'border-right-color': 'transparent'
1541
+ };
1542
+ rules[_sx(scope, '.bw_spinner_grow.bw_' + k)] = {
1543
+ 'background-color': s.base,
1544
+ 'color': s.base
1421
1545
  };
1422
1546
 
1423
1547
  // Outline button: transparent bg, colored border+text, solid on hover
1424
- rules[scopeSelector(scope, '.bw_btn_outline.bw_' + k)] = {
1548
+ rules[_sx(scope, '.bw_btn_outline.bw_' + k)] = {
1425
1549
  'background-color': 'transparent',
1426
1550
  'color': s.base,
1427
1551
  'border-color': s.base
1428
1552
  };
1429
- rules[scopeSelector(scope, '.bw_btn_outline.bw_' + k + ':hover')] = {
1553
+ rules[_sx(scope, '.bw_btn_outline.bw_' + k + ':hover')] = {
1430
1554
  'background-color': s.base,
1431
1555
  'color': s.textOn
1432
1556
  };
1433
1557
 
1434
1558
  // Hero: gradient background
1435
- rules[scopeSelector(scope, '.bw_hero.bw_' + k)] = {
1559
+ rules[_sx(scope, '.bw_hero.bw_' + k)] = {
1436
1560
  'background': 'linear-gradient(135deg, ' + s.base + ' 0%, ' + s.hover + ' 100%)',
1437
1561
  'color': s.textOn
1438
1562
  };
1439
1563
 
1440
- // Progress bar: white text on colored bg (default is fine, just ensure text)
1441
- rules[scopeSelector(scope, '.bw_progress_bar.bw_' + k)] = {
1442
- 'color': '#fff'
1564
+ // Progress bar: contrasting text on colored bg
1565
+ rules[_sx(scope, '.bw_progress_bar.bw_' + k)] = {
1566
+ 'color': s.textOn
1567
+ };
1568
+
1569
+ // Background utility: .bw_bg_primary, .bw_bg_secondary, etc.
1570
+ rules[_sx(scope, '.bw_bg_' + k)] = {
1571
+ 'background-color': s.base,
1572
+ 'color': s.textOn
1573
+ };
1574
+
1575
+ // Text color utility: .bw_text_primary, .bw_text_secondary, etc.
1576
+ rules[_sx(scope, '.bw_text_' + k)] = {
1577
+ 'color': s.base
1443
1578
  };
1444
1579
  });
1445
1580
 
1446
- // Text muted
1447
- rules[scopeSelector(scope, '.bw_text_muted')] = { 'color': palette.secondary.base };
1581
+ // Text muted — always a neutral gray, never a brand color
1582
+ rules[_sx(scope, '.bw_text_muted')] = { 'color': '#6c757d' };
1583
+
1584
+ // Common bg/text utilities that aren't per-variant
1585
+ rules[_sx(scope, '.bw_bg_dark')] = { 'background-color': '#212529', 'color': '#f8f9fa' };
1586
+ rules[_sx(scope, '.bw_bg_light')] = { 'background-color': '#f8f9fa', 'color': '#212529' };
1587
+ rules[_sx(scope, '.bw_text_light')] = { 'color': '#f8f9fa' };
1588
+ rules[_sx(scope, '.bw_text_dark')] = { 'color': '#212529' };
1448
1589
 
1449
1590
  return rules;
1450
1591
  }
@@ -1466,30 +1607,32 @@
1466
1607
  generateAlerts(scopeName, palette, layout),
1467
1608
  generateCards(scopeName, palette, layout),
1468
1609
  generateForms(scopeName, palette, layout),
1469
- generateNavigation(scopeName, palette),
1610
+ generateNavigation(scopeName, palette, layout),
1470
1611
  generateTables(scopeName, palette, layout),
1471
- generateTabs(scopeName, palette),
1612
+ generateTabs(scopeName, palette, layout),
1472
1613
  generateListGroups(scopeName, palette, layout),
1473
- generatePagination(scopeName, palette),
1614
+ generatePagination(scopeName, palette, layout),
1474
1615
  generateProgress(scopeName, palette),
1475
- generateBreadcrumbThemed(scopeName, palette),
1616
+ generateBreadcrumbThemed(scopeName, palette, layout),
1476
1617
  generateCloseButtonThemed(scopeName, palette),
1477
1618
  generateSectionsThemed(scopeName, palette),
1478
- generateAccordionThemed(scopeName, palette),
1619
+ generateAccordionThemed(scopeName, palette, layout),
1479
1620
  generateCarouselThemed(scopeName, palette),
1480
1621
  generateModalThemed(scopeName, palette, layout),
1481
1622
  generateToastThemed(scopeName, palette, layout),
1482
1623
  generateDropdownThemed(scopeName, palette, layout),
1483
1624
  generateSwitchThemed(scopeName, palette),
1484
1625
  generateSkeletonThemed(scopeName, palette),
1485
- generateStatCardThemed(scopeName, palette),
1626
+ generateStatCardThemed(scopeName, palette, layout),
1486
1627
  generateTimelineThemed(scopeName, palette),
1487
1628
  generateStepperThemed(scopeName, palette),
1488
1629
  generateChipInputThemed(scopeName, palette),
1489
- generateFileUploadThemed(scopeName, palette),
1630
+ generateFileUploadThemed(scopeName, palette, layout),
1490
1631
  generateRangeThemed(scopeName, palette),
1491
- generateSearchThemed(scopeName, palette),
1492
- generateCodeDemoThemed(scopeName, palette),
1632
+ generateSearchThemed(scopeName, palette, layout),
1633
+ generateTooltipThemed(scopeName, palette, layout),
1634
+ generatePopoverThemed(scopeName, palette, layout),
1635
+ generateCodeDemoThemed(scopeName, palette, layout),
1493
1636
  generateNavPillsThemed(scopeName, palette, layout),
1494
1637
  generatePaletteClasses(scopeName, palette)
1495
1638
  );
@@ -1714,6 +1857,8 @@
1714
1857
  },
1715
1858
  '.bw_table caption': { 'font-size': '0.875rem', 'caption-side': 'bottom' },
1716
1859
  '.bw_table_bordered > :not(caption) > * > *': { 'border-width': '1px', 'border-style': 'solid' },
1860
+ '.bw_table_selectable > tbody > tr': { 'cursor': 'pointer' },
1861
+ '.bw_table > tbody > tr.bw_table_row_selected > *': { 'background-color': 'rgba(0, 102, 102, 0.1)' },
1717
1862
  '.bw_table_responsive': { 'overflow-x': 'auto', '-webkit-overflow-scrolling': 'touch' }
1718
1863
  },
1719
1864
 
@@ -1767,6 +1912,7 @@
1767
1912
  '.bw_nav_tabs .bw_nav_item': { 'margin-bottom': '-2px' },
1768
1913
  '.bw_nav_link': {
1769
1914
  'display': 'block', 'font-size': '0.875rem', 'font-weight': '500',
1915
+ 'padding': '0.625rem 1rem',
1770
1916
  'text-decoration': 'none', 'cursor': 'pointer',
1771
1917
  'border': 'none', 'background': 'transparent', 'font-family': 'inherit'
1772
1918
  },
@@ -1801,10 +1947,11 @@
1801
1947
  '.bw_page_item': { 'display': 'list-item', 'list-style': 'none' },
1802
1948
  '.bw_page_link': {
1803
1949
  'position': 'relative', 'display': 'block', 'padding': '0.375rem 0.75rem',
1804
- 'margin-left': '-1px', 'line-height': '1.25', 'text-decoration': 'none'
1950
+ 'margin-left': '-1px', 'line-height': '1.25', 'text-decoration': 'none',
1951
+ 'border': '1px solid transparent', 'cursor': 'pointer',
1952
+ 'font-family': 'inherit', 'font-size': 'inherit', 'background': 'none'
1805
1953
  },
1806
- '.bw_page_item:first-child .bw_page_link': { 'margin-left': '0', 'border-top-left-radius': '0.375rem', 'border-bottom-left-radius': '0.375rem' },
1807
- '.bw_page_item:last-child .bw_page_link': { 'border-top-right-radius': '0.375rem', 'border-bottom-right-radius': '0.375rem' },
1954
+ '.bw_page_item:first-child .bw_page_link': { 'margin-left': '0' },
1808
1955
  '.bw_page_link:focus-visible': { 'z-index': '3', 'outline': '2px solid currentColor', 'outline-offset': '-2px' }
1809
1956
  },
1810
1957
 
@@ -1961,6 +2108,7 @@
1961
2108
  '.bw_accordion_header': { 'margin': '0' },
1962
2109
  '.bw_accordion_button': {
1963
2110
  'position': 'relative', 'display': 'flex', 'align-items': 'center', 'width': '100%',
2111
+ 'padding': '0.875rem 1.25rem',
1964
2112
  'font-size': '1rem', 'font-weight': '500', 'text-align': 'left',
1965
2113
  'background-color': 'transparent', 'border': '0', 'overflow-anchor': 'none', 'cursor': 'pointer',
1966
2114
  'font-family': 'inherit'
@@ -1972,10 +2120,9 @@
1972
2120
  'background-repeat': 'no-repeat', 'background-size': '1.25rem'
1973
2121
  },
1974
2122
  '.bw_accordion_button:not(.bw_collapsed)::after': { 'transform': 'rotate(-180deg)' },
1975
- '.bw_accordion_collapse': { 'max-height': '0', 'overflow': 'hidden' },
1976
- '.bw_accordion_collapse.bw_collapse_show': { 'max-height': 'none' },
1977
- '.bw_accordion_item:first-child': { 'border-top-left-radius': '8px', 'border-top-right-radius': '8px' },
1978
- '.bw_accordion_item:last-child': { 'border-bottom-left-radius': '8px', 'border-bottom-right-radius': '8px' }
2123
+ '.bw_accordion_body': { 'padding': '1rem 1.25rem' },
2124
+ '.bw_accordion_collapse': { 'max-height': '0', 'overflow': 'hidden', 'transition': 'max-height 0.3s ease' },
2125
+ '.bw_accordion_collapse.bw_collapse_show': { 'max-height': 'none' }
1979
2126
  },
1980
2127
 
1981
2128
  // ---- Carousel ----
@@ -2121,7 +2268,13 @@
2121
2268
 
2122
2269
  // ---- Stat card ----
2123
2270
  statCard: {
2124
- '.bw_stat_card': { 'border-left': '4px solid transparent' },
2271
+ '.bw_stat_card': {
2272
+ 'padding': '1.25rem',
2273
+ 'border-left': '4px solid transparent',
2274
+ 'border-radius': '0.375rem',
2275
+ 'background-color': 'inherit',
2276
+ 'transition': 'transform 0.15s ease'
2277
+ },
2125
2278
  '.bw_stat_card:hover': { 'transform': 'translateY(-1px)' },
2126
2279
  '.bw_stat_icon': { 'font-size': '1.5rem', 'margin-bottom': '0.5rem' },
2127
2280
  '.bw_stat_value': { 'font-size': '2rem', 'font-weight': '700', 'line-height': '1.2' },
@@ -2484,6 +2637,20 @@
2484
2637
  rules['.list-inline-item'] = { 'display': 'inline-block' };
2485
2638
  rules['.list-inline-item:not(:last-child)'] = { 'margin-right': '.5rem' };
2486
2639
 
2640
+ // Typography — bw_ prefixed utilities via loops
2641
+ var _imp = function(p, v) { var o = {}; o[p] = v + ' !important'; return o; };
2642
+ [['fs',{'xs':'0.75rem','sm':'0.875rem','base':'1rem','lg':'1.125rem','xl':'1.25rem','2xl':'1.5rem'},'font-size'],
2643
+ ['fw',{light:'300',normal:'400',medium:'500',semibold:'600',bold:'700'},'font-weight'],
2644
+ ['lh',{tight:'1.25',normal:'1.5',relaxed:'1.75'},'line-height']
2645
+ ].forEach(function(d) { for (var dk in d[1]) rules['.bw_'+d[0]+'_'+dk] = _imp(d[2], d[1][dk]); });
2646
+
2647
+ // Flex utilities
2648
+ rules['.bw_flex'] = { 'display': 'flex' };
2649
+ rules['.bw_flex_column'] = { 'flex-direction': 'column' };
2650
+ rules['.bw_flex_wrap'] = { 'flex-wrap': 'wrap' };
2651
+ rules['.bw_flex_center'] = { 'display': 'flex', 'align-items': 'center', 'justify-content': 'center' };
2652
+ for (var gk in spacingValues) rules['.bw_gap_' + gk] = { 'gap': spacingValues[gk] + ' !important' };
2653
+
2487
2654
  // Visibility
2488
2655
  rules['.bw_visible, .visible'] = { 'visibility': 'visible !important' };
2489
2656
  rules['.bw_invisible, .invisible'] = { 'visibility': 'hidden !important' };
@@ -2544,6 +2711,26 @@
2544
2711
  return getStructuralCSS();
2545
2712
  }
2546
2713
 
2714
+ /**
2715
+ * Get CSS reset rules only (box-sizing, html/body font, reduced-motion).
2716
+ * Separate from themed/structural rules for independent injection.
2717
+ * @returns {Object} CSS rules object for the reset layer
2718
+ */
2719
+ function getResetStyles() {
2720
+ var rules = {};
2721
+ Object.assign(rules, structuralRules.base);
2722
+ // Include reduced-motion preference
2723
+ rules['@media (prefers-reduced-motion: reduce)'] = {
2724
+ '*, *::before, *::after': {
2725
+ 'animation-duration': '0.01ms !important',
2726
+ 'animation-iteration-count': '1 !important',
2727
+ 'transition-duration': '0.01ms !important',
2728
+ 'scroll-behavior': 'auto !important'
2729
+ }
2730
+ };
2731
+ return rules;
2732
+ }
2733
+
2547
2734
  // =========================================================================
2548
2735
  // defaultStyles — backward-compatible categorized view
2549
2736
  // =========================================================================
@@ -2573,60 +2760,41 @@
2573
2760
  });
2574
2761
 
2575
2762
  /**
2576
- * Generate alternate-palette CSS scoped under `.bw_theme_alt`.
2577
- * Uses the same `generateThemedCSS()` pipeline as the primary palette —
2578
- * both sides go through identical code paths.
2579
- *
2580
- * @param {string} name - Theme scope name (e.g. 'ocean'). '' for global.
2581
- * @param {Object} altPalette - From derivePalette(deriveAlternateConfig(...))
2582
- * @param {Object} layout - From resolveLayout()
2583
- * @returns {Object} CSS rules object scoped under .bw_theme_alt (+ optional .name)
2763
+ * Prefix every selector in a rules object with a scope selector.
2764
+ * Handles @media/@keyframes blocks and comma-separated selectors.
2765
+ * @param {Object} rules - CSS rules object
2766
+ * @param {string} prefix - Scope prefix (e.g. '#my-dashboard', '.bw_theme_alt')
2767
+ * @param {boolean} [compound=false] - If true, use compound selector (no space)
2768
+ * for the first segment: `#scope.bw_theme_alt .sel` vs `#scope .sel`
2769
+ * @returns {Object} New rules object with scoped selectors
2584
2770
  */
2585
- function generateAlternateCSS(name, altPalette, layout) {
2586
- // Generate themed CSS using the same pipeline as primary
2587
- var rawRules = generateThemedCSS('', altPalette, layout);
2588
-
2589
- // Re-scope every selector under .bw_theme_alt (+ optional theme name)
2590
- var altPrefix = name ? '.' + name + '.bw_theme_alt' : '.bw_theme_alt';
2591
- var altRules = {};
2592
-
2593
- for (var sel in rawRules) {
2594
- if (!rawRules.hasOwnProperty(sel)) continue;
2595
-
2771
+ function scopeRulesUnder(rules, prefix, compound) {
2772
+ var scoped = {};
2773
+ for (var sel in rules) {
2774
+ if (!rules.hasOwnProperty(sel)) continue;
2596
2775
  if (sel.charAt(0) === '@') {
2597
2776
  // @media / @keyframes — recurse into the block
2598
- var innerBlock = rawRules[sel];
2599
- var altInner = {};
2777
+ var innerBlock = rules[sel];
2778
+ var scopedInner = {};
2600
2779
  for (var innerSel in innerBlock) {
2601
2780
  if (!innerBlock.hasOwnProperty(innerSel)) continue;
2602
- altInner[altPrefix + ' ' + innerSel] = innerBlock[innerSel];
2781
+ scopedInner[_prefixSelector(innerSel, prefix)] = innerBlock[innerSel];
2603
2782
  }
2604
- altRules[sel] = altInner;
2783
+ scoped[sel] = scopedInner;
2605
2784
  } else {
2606
- // Regular selector — prefix with alt scope
2607
- // Handle comma-separated selectors
2608
- var parts = sel.split(',');
2609
- var scopedParts = [];
2610
- for (var i = 0; i < parts.length; i++) {
2611
- var s = parts[i].trim();
2612
- // 'body' selector gets special treatment: .bw_theme_alt body
2613
- if (s === 'body' || s.indexOf('body') === 0) {
2614
- scopedParts.push(altPrefix + ' ' + s);
2615
- } else {
2616
- scopedParts.push(altPrefix + ' ' + s);
2617
- }
2618
- }
2619
- altRules[scopedParts.join(', ')] = rawRules[sel];
2785
+ scoped[_prefixSelector(sel, prefix)] = rules[sel];
2620
2786
  }
2621
2787
  }
2788
+ return scoped;
2789
+ }
2622
2790
 
2623
- // Add body-level overrides for the alternate surface
2624
- altRules[altPrefix + ' body, :root' + altPrefix + ' body'] = {
2625
- 'color': altPalette.dark.base,
2626
- 'background-color': altPalette.light.base
2627
- };
2628
-
2629
- return altRules;
2791
+ function _prefixSelector(sel, prefix) {
2792
+ var parts = sel.split(',');
2793
+ var result = [];
2794
+ for (var i = 0; i < parts.length; i++) {
2795
+ result.push(prefix + ' ' + parts[i].trim());
2796
+ }
2797
+ return result.join(', ');
2630
2798
  }
2631
2799
 
2632
2800
  /**
@@ -3634,7 +3802,12 @@
3634
3802
  el = document.querySelector('[data-bw_id="' + id + '"]');
3635
3803
  }
3636
3804
 
3637
- // 5. Cache the result for next time
3805
+ // 5. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
3806
+ if (!el && id.indexOf('bw_uuid_') === 0) {
3807
+ el = document.querySelector('.' + id);
3808
+ }
3809
+
3810
+ // 6. Cache the result for next time
3638
3811
  if (el) {
3639
3812
  bw._nodeMap[id] = el;
3640
3813
  }
@@ -3687,6 +3860,84 @@
3687
3860
  }
3688
3861
  };
3689
3862
 
3863
+ // ===================================================================================
3864
+ // bw.assignUUID() / bw.getUUID() — Explicit UUID addressing for TACO objects
3865
+ // ===================================================================================
3866
+
3867
+ /**
3868
+ * Regex to match a bw_uuid_* token in a class string.
3869
+ * @private
3870
+ */
3871
+ var _UUID_RE = /\bbw_uuid_[a-z0-9_]+\b/;
3872
+
3873
+ /**
3874
+ * Assign a UUID to a TACO object by appending a `bw_uuid_*` token to `taco.a.class`.
3875
+ *
3876
+ * Idempotent by default — calling twice returns the same UUID. Pass `forceNew=true`
3877
+ * to replace an existing UUID (useful in loops where each TACO needs a unique ID).
3878
+ *
3879
+ * @param {Object} taco - A TACO object `{t, a, c, o}`
3880
+ * @param {boolean} [forceNew=false] - If true, replaces any existing UUID with a new one
3881
+ * @returns {string} The UUID string (e.g. 'bw_uuid_a1b2c3d4e5')
3882
+ * @category Identifiers
3883
+ * @example
3884
+ * var card = bw.makeStatCard({ value: '0', label: 'Scans' });
3885
+ * var uuid = bw.assignUUID(card); // 'bw_uuid_a1b2c3d4e5'
3886
+ * var same = bw.assignUUID(card); // same UUID (idempotent)
3887
+ * var diff = bw.assignUUID(card, true); // new UUID (forced)
3888
+ */
3889
+ bw.assignUUID = function(taco, forceNew) {
3890
+ if (!taco || !_is(taco, 'object')) return null;
3891
+
3892
+ // Ensure taco.a exists
3893
+ if (!taco.a) taco.a = {};
3894
+ if (!_is(taco.a.class, 'string')) taco.a.class = taco.a.class ? String(taco.a.class) : '';
3895
+
3896
+ var existing = taco.a.class.match(_UUID_RE);
3897
+
3898
+ if (existing && !forceNew) {
3899
+ return existing[0];
3900
+ }
3901
+
3902
+ // Remove old UUID if forceNew
3903
+ if (existing) {
3904
+ taco.a.class = taco.a.class.replace(_UUID_RE, '').replace(/\s+/g, ' ').trim();
3905
+ }
3906
+
3907
+ var uuid = bw.uuid('uuid');
3908
+ taco.a.class = (taco.a.class ? taco.a.class + ' ' : '') + uuid;
3909
+ return uuid;
3910
+ };
3911
+
3912
+ /**
3913
+ * Read the UUID from a TACO object or DOM element. Pure getter, no side effects.
3914
+ *
3915
+ * @param {Object|Element} tacoOrElement - A TACO object or DOM element
3916
+ * @returns {string|null} The UUID string, or null if none assigned
3917
+ * @category Identifiers
3918
+ * @example
3919
+ * bw.getUUID(card) // 'bw_uuid_a1b2c3d4e5' (from TACO)
3920
+ * bw.getUUID(domEl) // 'bw_uuid_a1b2c3d4e5' (from DOM element)
3921
+ * bw.getUUID({t:'div'}) // null (no UUID)
3922
+ */
3923
+ bw.getUUID = function(tacoOrElement) {
3924
+ if (!tacoOrElement) return null;
3925
+
3926
+ var classStr;
3927
+ // DOM element: check className
3928
+ if (tacoOrElement.className !== undefined && tacoOrElement.tagName) {
3929
+ classStr = tacoOrElement.className;
3930
+ }
3931
+ // TACO object: check a.class
3932
+ else if (tacoOrElement.a && _is(tacoOrElement.a.class, 'string')) {
3933
+ classStr = tacoOrElement.a.class;
3934
+ }
3935
+
3936
+ if (!classStr) return null;
3937
+ var match = classStr.match(_UUID_RE);
3938
+ return match ? match[0] : null;
3939
+ };
3940
+
3690
3941
  /**
3691
3942
  * Escape HTML special characters to prevent XSS.
3692
3943
  *
@@ -3736,6 +3987,42 @@
3736
3987
  return { __bw_raw: true, v: String(str) };
3737
3988
  };
3738
3989
 
3990
+ /**
3991
+ * Hyperscript-style TACO constructor.
3992
+ *
3993
+ * A convenience helper that returns a canonical TACO object from positional
3994
+ * arguments. The return value is a plain object — serializable, works with
3995
+ * bwserve, and accepted everywhere TACO is accepted.
3996
+ *
3997
+ * @param {string} tag - HTML tag name (e.g. 'div', 'p', 'section')
3998
+ * @param {Object|null} [attrs] - HTML attributes object. Pass null or omit to skip.
3999
+ * @param {*} [content] - Content: string, number, TACO object, or array of children.
4000
+ * @param {Object} [options] - TACO options (state, lifecycle hooks, render fn).
4001
+ * @returns {Object} Plain TACO object {t, a?, c?, o?}
4002
+ * @category Utilities
4003
+ * @see bw.html
4004
+ * @see bw.createDOM
4005
+ * @see bw.DOM
4006
+ * @example
4007
+ * bw.h('div')
4008
+ * // => { t: 'div' }
4009
+ *
4010
+ * bw.h('p', { class: 'bw_text_muted' }, 'Hello')
4011
+ * // => { t: 'p', a: { class: 'bw_text_muted' }, c: 'Hello' }
4012
+ *
4013
+ * bw.h('ul', null, [
4014
+ * bw.h('li', null, 'one'),
4015
+ * bw.h('li', null, 'two')
4016
+ * ])
4017
+ * // => { t: 'ul', c: [{ t: 'li', c: 'one' }, { t: 'li', c: 'two' }] }
4018
+ */
4019
+ bw.h = function(tag, attrs, content, options) {
4020
+ var taco = { t: String(tag) };
4021
+ if (attrs !== null && attrs !== undefined) taco.a = attrs;
4022
+ if (content !== undefined) taco.c = content;
4023
+ if (options !== undefined) taco.o = options;
4024
+ return taco;
4025
+ };
3739
4026
 
3740
4027
  /**
3741
4028
  * Convert a TACO object (or array of TACOs) to an HTML string.
@@ -4000,7 +4287,7 @@
4000
4287
  ? (THEME_PRESETS[theme.toLowerCase()] || null)
4001
4288
  : theme;
4002
4289
  if (themeConfig) {
4003
- var themeResult = bw.generateTheme('', Object.assign({}, themeConfig, { inject: false }));
4290
+ var themeResult = bw.makeStyles(themeConfig);
4004
4291
  themeCSS = themeResult.css;
4005
4292
  }
4006
4293
  }
@@ -4026,14 +4313,14 @@
4026
4313
  // Combine all CSS
4027
4314
  var allCSS = (themeCSS ? themeCSS + '\n' : '') + css;
4028
4315
 
4029
- // Body-end script: registry entries + optional loadDefaultStyles
4316
+ // Body-end script: registry entries + optional loadStyles
4030
4317
  var bodyEndScript = '';
4031
4318
  var bodyEndParts = [];
4032
4319
  if (registryEntries) {
4033
4320
  bodyEndParts.push(registryEntries);
4034
4321
  }
4035
4322
  if (runtime === 'inline' || runtime === 'cdn') {
4036
- bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadDefaultStyles();}');
4323
+ bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadStyles();}');
4037
4324
  }
4038
4325
  if (bodyEndParts.length > 0) {
4039
4326
  bodyEndScript = '<script>\n' + bodyEndParts.join('\n') + '\n</script>';
@@ -4207,6 +4494,14 @@
4207
4494
  bw._registerNode(el, null);
4208
4495
  }
4209
4496
 
4497
+ // Register UUID class in node cache (bw_uuid_* tokens in class string)
4498
+ if (el.className) {
4499
+ var uuidMatch = el.className.match(_UUID_RE);
4500
+ if (uuidMatch) {
4501
+ bw._nodeMap[uuidMatch[0]] = el;
4502
+ }
4503
+ }
4504
+
4210
4505
  // Handle lifecycle hooks and state
4211
4506
  if (opts.mounted || opts.unmount || opts.render || opts.state) {
4212
4507
  const id = attrs['data-bw_id'] || bw.uuid();
@@ -4579,6 +4874,16 @@
4579
4874
  bw.cleanup = function(element) {
4580
4875
  if (!bw._isBrowser || !element) return;
4581
4876
 
4877
+ // Deregister UUID classes from node cache (element + descendants)
4878
+ // Covers elements that have UUID but no data-bw_id
4879
+ var selfUuidMatch = element.className && element.className.match(_UUID_RE);
4880
+ if (selfUuidMatch) delete bw._nodeMap[selfUuidMatch[0]];
4881
+ var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
4882
+ uuidEls.forEach(function(uel) {
4883
+ var m = uel.className && uel.className.match(_UUID_RE);
4884
+ if (m) delete bw._nodeMap[m[0]];
4885
+ });
4886
+
4582
4887
  // Find all elements with data-bw_id
4583
4888
  const elements = element.querySelectorAll('[data-bw_id]');
4584
4889
 
@@ -4594,6 +4899,10 @@
4594
4899
  // Deregister from node cache
4595
4900
  bw._deregisterNode(el, id);
4596
4901
 
4902
+ // Deregister UUID class from node cache
4903
+ var uuidMatch = el.className && el.className.match(_UUID_RE);
4904
+ if (uuidMatch) delete bw._nodeMap[uuidMatch[0]];
4905
+
4597
4906
  // Clean up pub/sub subscriptions tied to this element
4598
4907
  if (el._bw_subs) {
4599
4908
  el._bw_subs.forEach(function(unsub) { unsub(); });
@@ -4618,6 +4927,10 @@
4618
4927
  // Deregister from node cache
4619
4928
  bw._deregisterNode(element, id);
4620
4929
 
4930
+ // Deregister UUID class from node cache
4931
+ var elemUuidMatch = element.className && element.className.match(_UUID_RE);
4932
+ if (elemUuidMatch) delete bw._nodeMap[elemUuidMatch[0]];
4933
+
4621
4934
  // Clean up pub/sub subscriptions tied to element itself
4622
4935
  if (element._bw_subs) {
4623
4936
  element._bw_subs.forEach(function(unsub) { unsub(); });
@@ -5229,7 +5542,7 @@
5229
5542
  willMount: o.willMount || null,
5230
5543
  mounted: o.mounted || null,
5231
5544
  willUpdate: o.willUpdate || null,
5232
- onUpdate: o.onUpdate || null,
5545
+ onUpdate: o.onUpdate || o.updated || null,
5233
5546
  unmount: o.unmount || null,
5234
5547
  willDestroy: o.willDestroy || null
5235
5548
  };
@@ -6168,7 +6481,7 @@
6168
6481
  * and calls the named method. This is the bitwrench equivalent of
6169
6482
  * Win32 SendMessage(hwnd, msg, wParam, lParam).
6170
6483
  *
6171
- * @param {string} target - Component UUID (data-bw_comp_id) or user tag (CSS class)
6484
+ * @param {string} target - Component UUID (bw_uuid_*), comp ID (data-bw_comp_id), or user tag (CSS class)
6172
6485
  * @param {string} action - Method name to call on the component
6173
6486
  * @param {*} data - Data to pass to the method
6174
6487
  * @returns {boolean} True if message was dispatched successfully
@@ -6185,9 +6498,14 @@
6185
6498
  * };
6186
6499
  */
6187
6500
  bw.message = function(target, action, data) {
6188
- // Try data-bw_comp_id attribute first, then CSS class (user tag)
6189
- var el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
6190
- if (!el) {
6501
+ // Try bw._el() first (handles UUID class, nodeMap cache, getElementById)
6502
+ var el = bw._el(target);
6503
+ // Then try data-bw_comp_id attribute
6504
+ if (!el || !el._bwComponentHandle) {
6505
+ el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
6506
+ }
6507
+ // Then try CSS class (user tag)
6508
+ if (!el || !el._bwComponentHandle) {
6191
6509
  el = bw.$('.' + target)[0];
6192
6510
  }
6193
6511
  if (!el || !el._bwComponentHandle) return false;
@@ -6201,59 +6519,24 @@
6201
6519
  };
6202
6520
 
6203
6521
  // ===================================================================================
6204
- // bw.clientApply() / bw.clientConnect() — Server-driven UI protocol
6522
+ // bw.apply() / bw.parseJSONFlex() — Server-driven UI protocol
6205
6523
  // ===================================================================================
6206
6524
 
6207
6525
  /**
6208
6526
  * Registry of named functions sent via register messages.
6209
- * Populated by clientApply({ type: 'register', name, body }).
6210
- * Invoked by clientApply({ type: 'call', name, args }).
6527
+ * Populated by bw.apply({ type: 'register', name, body }).
6528
+ * Invoked by bw.apply({ type: 'call', name, args }).
6211
6529
  * @private
6212
6530
  */
6213
6531
  bw._clientFunctions = {};
6214
6532
 
6215
6533
  /**
6216
- * Whether exec messages are allowed. Set by clientConnect opts.allowExec.
6534
+ * Whether exec messages are allowed. Set by bwclient connect opts.allowExec.
6217
6535
  * Default false — exec messages are rejected unless explicitly opted in.
6218
6536
  * @private
6219
6537
  */
6220
6538
  bw._allowExec = false;
6221
6539
 
6222
- /**
6223
- * Built-in client functions available via call() without registration.
6224
- * @private
6225
- */
6226
- bw._builtinClientFunctions = {
6227
- scrollTo: function(selector) {
6228
- var el = bw._el(selector);
6229
- if (el) el.scrollTop = el.scrollHeight;
6230
- },
6231
- focus: function(selector) {
6232
- var el = bw._el(selector);
6233
- if (el && _is(el.focus, 'function')) el.focus();
6234
- },
6235
- download: function(filename, content, mimeType) {
6236
- if (typeof document === 'undefined') return;
6237
- var blob = new Blob([content], { type: mimeType || 'text/plain' });
6238
- var a = document.createElement('a');
6239
- a.href = URL.createObjectURL(blob);
6240
- a.download = filename;
6241
- a.click();
6242
- URL.revokeObjectURL(a.href);
6243
- },
6244
- clipboard: function(text) {
6245
- if (typeof navigator !== 'undefined' && navigator.clipboard) {
6246
- navigator.clipboard.writeText(text);
6247
- }
6248
- },
6249
- redirect: function(url) {
6250
- if (typeof window !== 'undefined') window.location.href = url;
6251
- },
6252
- log: function() {
6253
- console.log.apply(console, arguments);
6254
- }
6255
- };
6256
-
6257
6540
  /**
6258
6541
  * Parse a bwserve protocol message string, supporting both strict JSON
6259
6542
  * and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
@@ -6268,9 +6551,9 @@
6268
6551
  * @param {string} str - JSON or r-prefixed relaxed JSON string
6269
6552
  * @returns {Object} Parsed message object
6270
6553
  * @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
6271
- * @category Server
6554
+ * @category Core
6272
6555
  */
6273
- bw.clientParse = function(str) {
6556
+ bw.parseJSONFlex = function(str) {
6274
6557
  str = (str || '').trim();
6275
6558
  if (str.charAt(0) !== 'r') return JSON.parse(str);
6276
6559
  str = str.slice(1);
@@ -6355,10 +6638,10 @@
6355
6638
  * append — target.appendChild(bw.createDOM(node))
6356
6639
  * remove — bw.cleanup(target); target.remove()
6357
6640
  * patch — bw.patch(target, content, attr)
6358
- * batch — iterate ops, call clientApply for each
6641
+ * batch — iterate ops, call bw.apply for each
6359
6642
  * message — bw.message(target, action, data)
6360
6643
  * register — store a named function for later call()
6361
- * call — invoke a registered or built-in function
6644
+ * call — invoke a registered function
6362
6645
  * exec — execute arbitrary JS (requires allowExec)
6363
6646
  *
6364
6647
  * Target resolution:
@@ -6367,9 +6650,9 @@
6367
6650
  *
6368
6651
  * @param {Object} msg - Protocol message
6369
6652
  * @returns {boolean} true if the message was applied successfully
6370
- * @category Server
6653
+ * @category Core
6371
6654
  */
6372
- bw.clientApply = function(msg) {
6655
+ bw.apply = function(msg) {
6373
6656
  if (!msg || !msg.type) return false;
6374
6657
 
6375
6658
  var type = msg.type;
@@ -6403,7 +6686,7 @@
6403
6686
  if (!_isA(msg.ops)) return false;
6404
6687
  var allOk = true;
6405
6688
  msg.ops.forEach(function(op) {
6406
- if (!bw.clientApply(op)) allOk = false;
6689
+ if (!bw.apply(op)) allOk = false;
6407
6690
  });
6408
6691
  return allOk;
6409
6692
 
@@ -6422,7 +6705,7 @@
6422
6705
 
6423
6706
  } else if (type === 'call') {
6424
6707
  if (!msg.name) return false;
6425
- var fn = bw._clientFunctions[msg.name] || bw._builtinClientFunctions[msg.name];
6708
+ var fn = bw._clientFunctions[msg.name];
6426
6709
  if (!_is(fn, 'function')) return false;
6427
6710
  try {
6428
6711
  var args = _isA(msg.args) ? msg.args : [];
@@ -6451,139 +6734,6 @@
6451
6734
  return false;
6452
6735
  };
6453
6736
 
6454
- /**
6455
- * Connect to a bwserve SSE endpoint and apply protocol messages automatically.
6456
- *
6457
- * Returns a connection object with sendAction(), on(), and close() methods.
6458
- *
6459
- * @param {string} url - SSE endpoint URL (e.g., '/__bw/events/client-1')
6460
- * @param {Object} [opts] - Connection options
6461
- * @param {string} [opts.transport='sse'] - Transport type: 'sse' (default) or 'poll'
6462
- * @param {number} [opts.interval=2000] - Poll interval in ms (only for 'poll' transport)
6463
- * @param {string} [opts.actionUrl] - POST endpoint for actions (default: derived from url)
6464
- * @param {boolean} [opts.reconnect=true] - Auto-reconnect on disconnect
6465
- * @param {boolean} [opts.allowExec=false] - Enable exec message type (arbitrary JS execution)
6466
- * @param {Function} [opts.onStatus] - Status callback: 'connecting'|'connected'|'disconnected'
6467
- * @param {Function} [opts.onMessage] - Raw message callback (before clientApply)
6468
- * @returns {Object} Connection object { sendAction, on, close, status }
6469
- * @category Server
6470
- */
6471
- bw.clientConnect = function(url, opts) {
6472
- opts = opts || {};
6473
- var transport = opts.transport || 'sse';
6474
- var actionUrl = opts.actionUrl || url.replace(/\/events\//, '/action/');
6475
- var reconnect = opts.reconnect !== false;
6476
- var onStatus = opts.onStatus || function() {};
6477
- var onMessage = opts.onMessage || null;
6478
- var handlers = {};
6479
- // Set the global allowExec flag from connection options
6480
- bw._allowExec = !!opts.allowExec;
6481
- var conn = {
6482
- status: 'connecting',
6483
- _es: null,
6484
- _pollTimer: null
6485
- };
6486
-
6487
- function setStatus(s) {
6488
- conn.status = s;
6489
- onStatus(s);
6490
- }
6491
-
6492
- function handleMessage(data) {
6493
- try {
6494
- var msg = _is(data, 'string') ? bw.clientParse(data) : data;
6495
- if (onMessage) onMessage(msg);
6496
- if (handlers.message) handlers.message(msg);
6497
- bw.clientApply(msg);
6498
- } catch (e) {
6499
- if (handlers.error) handlers.error(e);
6500
- }
6501
- }
6502
-
6503
- if (transport === 'sse' && typeof EventSource !== 'undefined') {
6504
- setStatus('connecting');
6505
- var es = new EventSource(url);
6506
- conn._es = es;
6507
-
6508
- es.onopen = function() {
6509
- setStatus('connected');
6510
- if (handlers.open) handlers.open();
6511
- };
6512
-
6513
- es.onmessage = function(e) {
6514
- handleMessage(e.data);
6515
- };
6516
-
6517
- es.onerror = function() {
6518
- if (conn.status === 'connected') {
6519
- setStatus('disconnected');
6520
- }
6521
- if (handlers.error) handlers.error(new Error('SSE connection error'));
6522
- if (!reconnect) {
6523
- es.close();
6524
- }
6525
- // EventSource auto-reconnects by default when reconnect=true
6526
- };
6527
- } else if (transport === 'poll') {
6528
- var interval = opts.interval || 2000;
6529
- setStatus('connected');
6530
- conn._pollTimer = setInterval(function() {
6531
- fetch(url).then(function(r) { return r.json(); }).then(function(msgs) {
6532
- if (_isA(msgs)) {
6533
- msgs.forEach(handleMessage);
6534
- } else if (msgs && msgs.type) {
6535
- handleMessage(msgs);
6536
- }
6537
- }).catch(function(e) {
6538
- if (handlers.error) handlers.error(e);
6539
- });
6540
- }, interval);
6541
- }
6542
-
6543
- /**
6544
- * Send an action to the server via POST.
6545
- * @param {string} action - Action name
6546
- * @param {Object} [data] - Action payload
6547
- */
6548
- conn.sendAction = function(action, data) {
6549
- var body = JSON.stringify({ type: 'action', action: action, data: data || {} });
6550
- fetch(actionUrl, {
6551
- method: 'POST',
6552
- headers: { 'Content-Type': 'application/json' },
6553
- body: body
6554
- }).catch(function(e) {
6555
- if (handlers.error) handlers.error(e);
6556
- });
6557
- };
6558
-
6559
- /**
6560
- * Register an event handler.
6561
- * @param {string} event - 'open'|'message'|'error'|'close'
6562
- * @param {Function} handler
6563
- */
6564
- conn.on = function(event, handler) {
6565
- handlers[event] = handler;
6566
- return conn;
6567
- };
6568
-
6569
- /**
6570
- * Close the connection.
6571
- */
6572
- conn.close = function() {
6573
- if (conn._es) {
6574
- conn._es.close();
6575
- conn._es = null;
6576
- }
6577
- if (conn._pollTimer) {
6578
- clearInterval(conn._pollTimer);
6579
- conn._pollTimer = null;
6580
- }
6581
- setStatus('disconnected');
6582
- if (handlers.close) handlers.close();
6583
- };
6584
-
6585
- return conn;
6586
- };
6587
6737
 
6588
6738
  // ===================================================================================
6589
6739
  // bw.inspect() — Debug utility
@@ -6792,7 +6942,7 @@
6792
6942
  * @returns {Element} The style element
6793
6943
  * @category CSS & Styling
6794
6944
  * @see bw.css
6795
- * @see bw.loadDefaultStyles
6945
+ * @see bw.loadStyles
6796
6946
  * @example
6797
6947
  * bw.injectCSS('.my-class { color: red; }');
6798
6948
  * bw.injectCSS({ '.card': { padding: '1rem' } }, { id: 'card-styles' });
@@ -6837,9 +6987,8 @@
6837
6987
  * @param {...Object} styles - Style objects to merge (left-to-right)
6838
6988
  * @returns {Object} Merged style object
6839
6989
  * @category CSS & Styling
6840
- * @see bw.u
6841
6990
  * @example
6842
- * var style = bw.s(bw.u.flex, bw.u.gap4, { color: 'red' });
6991
+ * var style = bw.s({ display: 'flex' }, { gap: '1rem' }, { color: 'red' });
6843
6992
  * // => { display: 'flex', gap: '1rem', color: 'red' }
6844
6993
  */
6845
6994
  bw.s = function() {
@@ -6851,99 +7000,6 @@
6851
7000
  return result;
6852
7001
  };
6853
7002
 
6854
- /**
6855
- * Pre-built CSS utility objects (like Tailwind utilities, but in JS).
6856
- *
6857
- * Compose with `bw.s()` to build inline styles without writing raw CSS strings.
6858
- * Includes flex, padding, margin, typography, color, border, and transition utilities.
6859
- *
6860
- * @category CSS & Styling
6861
- * @see bw.s
6862
- * @example
6863
- * { t: 'div', a: { style: bw.s(bw.u.flex, bw.u.gap4, bw.u.p4) },
6864
- * c: 'Flexbox with 1rem gap and padding' }
6865
- */
6866
- bw.u = {
6867
- // Display
6868
- flex: { display: 'flex' },
6869
- flexCol: { display: 'flex', flexDirection: 'column' },
6870
- flexRow: { display: 'flex', flexDirection: 'row' },
6871
- flexWrap: { display: 'flex', flexWrap: 'wrap' },
6872
- block: { display: 'block' },
6873
- inline: { display: 'inline' },
6874
- hidden: { display: 'none' },
6875
-
6876
- // Flex alignment
6877
- justifyCenter: { justifyContent: 'center' },
6878
- justifyBetween: { justifyContent: 'space-between' },
6879
- justifyEnd: { justifyContent: 'flex-end' },
6880
- alignCenter: { alignItems: 'center' },
6881
- alignStart: { alignItems: 'flex-start' },
6882
- alignEnd: { alignItems: 'flex-end' },
6883
-
6884
- // Gap (0.25rem increments)
6885
- gap1: { gap: '0.25rem' },
6886
- gap2: { gap: '0.5rem' },
6887
- gap3: { gap: '0.75rem' },
6888
- gap4: { gap: '1rem' },
6889
- gap6: { gap: '1.5rem' },
6890
- gap8: { gap: '2rem' },
6891
-
6892
- // Padding
6893
- p0: { padding: '0' },
6894
- p1: { padding: '0.25rem' },
6895
- p2: { padding: '0.5rem' },
6896
- p3: { padding: '0.75rem' },
6897
- p4: { padding: '1rem' },
6898
- p6: { padding: '1.5rem' },
6899
- p8: { padding: '2rem' },
6900
- px4: { paddingLeft: '1rem', paddingRight: '1rem' },
6901
- py2: { paddingTop: '0.5rem', paddingBottom: '0.5rem' },
6902
- py4: { paddingTop: '1rem', paddingBottom: '1rem' },
6903
-
6904
- // Margin (same scale)
6905
- m0: { margin: '0' },
6906
- m4: { margin: '1rem' },
6907
- mt2: { marginTop: '0.5rem' },
6908
- mt4: { marginTop: '1rem' },
6909
- mb2: { marginBottom: '0.5rem' },
6910
- mb4: { marginBottom: '1rem' },
6911
- mx_auto: { marginLeft: 'auto', marginRight: 'auto' },
6912
-
6913
- // Typography
6914
- textSm: { fontSize: '0.875rem' },
6915
- textBase: { fontSize: '1rem' },
6916
- textLg: { fontSize: '1.125rem' },
6917
- textXl: { fontSize: '1.25rem' },
6918
- text2xl: { fontSize: '1.5rem' },
6919
- text3xl: { fontSize: '1.875rem' },
6920
- bold: { fontWeight: '700' },
6921
- semibold: { fontWeight: '600' },
6922
- italic: { fontStyle: 'italic' },
6923
- textCenter: { textAlign: 'center' },
6924
- textRight: { textAlign: 'right' },
6925
-
6926
- // Colors (from design tokens)
6927
- bgWhite: { background: '#ffffff' },
6928
- bgTeal: { background: '#006666', color: '#ffffff' },
6929
- textWhite: { color: '#ffffff' },
6930
- textTeal: { color: '#006666' },
6931
- textMuted: { color: '#888' },
6932
-
6933
- // Borders
6934
- rounded: { borderRadius: '0.375rem' },
6935
- roundedLg: { borderRadius: '0.5rem' },
6936
- roundedFull: { borderRadius: '9999px' },
6937
- border: { border: '1px solid #d8d8d8' },
6938
-
6939
- // Sizing
6940
- wFull: { width: '100%' },
6941
- hFull: { height: '100%' },
6942
-
6943
- // Transitions
6944
- transition: { transition: 'all 0.2s ease' }
6945
- };
6946
-
6947
7003
  /**
6948
7004
  * Generate responsive CSS with media query breakpoints.
6949
7005
  *
@@ -7065,103 +7121,49 @@
7065
7121
  };
7066
7122
  }
7067
7123
 
7068
- /**
7069
- * Load the built-in Bootstrap-inspired default stylesheet.
7070
- *
7071
- * Injects bitwrench's batteries-included CSS (buttons, cards, grids, forms,
7072
- * alerts, badges, nav, tabs, etc.) into the document head. Call once at app startup.
7073
- * Returns null in Node.js (no DOM).
7074
- *
7075
- * @param {Object} [options] - Style loading options
7076
- * @param {boolean} [options.minify=true] - Minify the CSS output
7077
- * @returns {Element|null} Style element if in browser, null in Node.js
7078
- * @category CSS & Styling
7079
- * @see bw.setTheme
7080
- * @see bw.applyTheme
7081
- * @see bw.toggleTheme
7082
- * @example
7083
- * bw.loadDefaultStyles(); // inject all default CSS
7084
- */
7085
- bw.loadDefaultStyles = function(options = {}) {
7086
- const { minify = true, palette } = options;
7087
7124
 
7088
- // 1. Inject structural CSS (layout, sizing — never changes with theme)
7089
- if (bw._isBrowser) {
7090
- var structuralCSS = bw.css(getStructuralStyles());
7091
- bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false, minify: minify });
7092
- }
7093
-
7094
- // 2. Inject cosmetic CSS via generateTheme (colors, shadows, radii)
7095
- var paletteConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, palette || {});
7096
- var result = bw.generateTheme('', Object.assign({}, paletteConfig, { inject: true }));
7097
- return result;
7098
- };
7125
+ // =========================================================================
7126
+ // v2.0.18 Clean Styles API — makeStyles / applyStyles / loadStyles / etc.
7127
+ // =========================================================================
7099
7128
 
7129
+ /**
7130
+ * Convert a scope selector to a <style> element id.
7131
+ * @private
7132
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview')
7133
+ * @returns {string} Style element id (e.g. 'bw_style_my_dashboard')
7134
+ */
7135
+ function _scopeToStyleId(scope) {
7136
+ if (!scope || scope === '' || scope === 'global') return 'bw_style_global';
7137
+ if (scope === 'reset') return 'bw_style_reset';
7138
+ // Strip leading # or . and convert - to _
7139
+ var clean = scope.replace(/^[#.]/, '').replace(/-/g, '_');
7140
+ return 'bw_style_' + clean;
7141
+ }
7100
7142
 
7101
7143
  /**
7102
- * Generate a complete, scoped theme from seed colors.
7144
+ * Generate a complete styles object from seed colors and layout config.
7145
+ * Pure function — no DOM, no state, no side effects.
7103
7146
  *
7104
- * Produces CSS for all themed components (buttons, alerts, badges, cards,
7105
- * forms, nav, tables, tabs, list groups, pagination, progress, hero, utilities)
7106
- * scoped under `.name` class. Multiple themes can coexist in the stylesheet.
7107
- * Swap themes by changing the class on a container element.
7147
+ * All parameters are optional. Defaults to the bitwrench default palette.
7108
7148
  *
7109
- * @param {string} name - CSS scope class (e.g. 'ocean'). Empty string = unscoped global.
7110
- * @param {Object} config - Theme configuration
7111
- * @param {string} config.primary - Primary brand color hex
7112
- * @param {string} config.secondary - Secondary color hex
7113
- * @param {string} [config.tertiary] - Tertiary/accent color hex (defaults to primary)
7114
- * @param {string} [config.success='#198754'] - Success color hex
7115
- * @param {string} [config.danger='#dc3545'] - Danger color hex
7116
- * @param {string} [config.warning='#ffc107'] - Warning color hex
7117
- * @param {string} [config.info='#0dcaf0'] - Info color hex
7118
- * @param {string} [config.light='#f8f9fa'] - Light color hex
7119
- * @param {string} [config.dark='#212529'] - Dark color hex
7120
- * @param {string} [config.background] - Page background hex (default: '#ffffff' light, derived dark)
7121
- * @param {string} [config.surface] - Surface/card background hex (default: '#f8f9fa' light, derived dark)
7149
+ * @param {Object} [config] - Style configuration
7150
+ * @param {string} [config.primary='#006666'] - Primary brand color hex
7151
+ * @param {string} [config.secondary='#6c757d'] - Secondary color hex
7152
+ * @param {string} [config.tertiary] - Tertiary color hex (defaults to primary)
7122
7153
  * @param {string} [config.spacing='normal'] - 'compact' | 'normal' | 'spacious'
7123
7154
  * @param {string} [config.radius='md'] - 'none' | 'sm' | 'md' | 'lg' | 'pill'
7124
- * @param {number} [config.fontSize=1.0] - Base font size scale factor
7125
- * @param {string|number} [config.typeRatio='normal'] - 'tight' | 'normal' | 'relaxed' | 'dramatic' or a number
7126
- * @param {string} [config.elevation='md'] - 'flat' | 'sm' | 'md' | 'lg'
7127
- * @param {string} [config.motion='standard'] - 'reduced' | 'standard' | 'expressive'
7128
- * @param {number} [config.harmonize=0.20] - 0-1, semantic color hue shift toward primary
7129
- * @param {boolean} [config.inject=true] - Inject into DOM (browser only)
7130
- * @returns {Object} { css, palette, name, isLightPrimary, alternate: { css, palette } }
7155
+ * @returns {Object} { css, alternateCss, rules, alternateRules, palette, alternatePalette, isLightPrimary }
7131
7156
  * @category CSS & Styling
7132
- * @see bw.applyTheme
7133
- * @see bw.toggleTheme
7134
- * @see bw.loadDefaultStyles
7157
+ * @see bw.applyStyles
7158
+ * @see bw.loadStyles
7135
7159
  * @example
7136
- * // Generate and inject an ocean theme (primary + alternate)
7137
- * var theme = bw.generateTheme('ocean', {
7138
- * primary: '#0077b6',
7139
- * secondary: '#90e0ef',
7140
- * tertiary: '#00b4d8'
7141
- * });
7142
- *
7143
- * // Apply to a container
7144
- * document.getElementById('app').classList.add('ocean');
7145
- *
7146
- * // Toggle to alternate palette
7147
- * bw.toggleTheme();
7148
- *
7149
- * // Generate CSS for static export (Node.js)
7150
- * var result = bw.generateTheme('sunset', {
7151
- * primary: '#e76f51',
7152
- * secondary: '#264653',
7153
- * inject: false
7154
- * });
7155
- * fs.writeFileSync('sunset.css', result.css + result.alternate.css);
7160
+ * var styles = bw.makeStyles({ primary: '#4f46e5', secondary: '#d97706' });
7161
+ * console.log(styles.palette.primary.base); // '#4f46e5'
7162
+ * // styles.css contains all themed CSS — nothing injected
7156
7163
  */
7157
- bw.generateTheme = function(name, config) {
7158
- if (!config || !config.primary || !config.secondary) {
7159
- throw new Error('bw.generateTheme requires config.primary and config.secondary');
7160
- }
7161
-
7162
- // Merge with defaults; if user didn't supply tertiary, default to their primary
7163
- var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config);
7164
- if (!config.tertiary) fullConfig.tertiary = fullConfig.primary;
7164
+ bw.makeStyles = function(config) {
7165
+ var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config || {});
7166
+ if (config && !config.tertiary) fullConfig.tertiary = fullConfig.primary;
7165
7167
 
7166
7168
  // Derive primary palette
7167
7169
  var palette = derivePalette(fullConfig);
@@ -7169,131 +7171,207 @@
7169
7171
  // Resolve layout
7170
7172
  var layout = resolveLayout(fullConfig);
7171
7173
 
7172
- // Generate primary themed CSS rules
7173
- var themedRules = generateThemedCSS(name, palette, layout);
7174
+ // Generate primary themed CSS rules (unscoped)
7175
+ var themedRules = generateThemedCSS('', palette, layout);
7174
7176
  var cssStr = bw.css(themedRules);
7175
7177
 
7176
7178
  // Derive alternate palette (luminance-inverted)
7177
7179
  var altConfig = deriveAlternateConfig(fullConfig);
7178
7180
  var altPalette = derivePalette(altConfig);
7179
7181
 
7180
- // Generate alternate CSS scoped under .bw_theme_alt
7181
- var altRules = generateAlternateCSS(name, altPalette, layout);
7182
- var altCssStr = bw.css(altRules);
7182
+ // Generate alternate CSS rules WITHOUT .bw_theme_alt prefix (raw rules)
7183
+ // applyStyles() wraps them appropriately based on scope
7184
+ var altRawRules = generateThemedCSS('', altPalette, layout);
7185
+
7186
+ // Add body-level surface overrides for the alternate palette.
7187
+ // When .bw_theme_alt is on <html>, ".bw_theme_alt body" correctly matches.
7188
+ altRawRules['body'] = {
7189
+ 'color': altPalette.dark.base,
7190
+ 'background-color': altPalette.surface || altPalette.light.base
7191
+ };
7192
+
7193
+ var altCssStr = bw.css(altRawRules);
7183
7194
 
7184
7195
  // Determine if primary is light-flavored
7185
7196
  var lightPrimary = isLightPalette(fullConfig);
7186
7197
 
7187
- // Inject both CSS sets into DOM if requested
7188
- var shouldInject = config.inject !== false;
7189
- if (shouldInject && bw._isBrowser) {
7190
- var safeName = name ? name.replace(/-/g, '_') : '';
7191
- var styleId = safeName ? 'bw_theme_' + safeName : 'bw_theme_default';
7192
- var altStyleId = safeName ? 'bw_theme_' + safeName + '_alt' : 'bw_theme_default_alt';
7193
-
7194
- bw.injectCSS(cssStr, { id: styleId, append: false });
7195
- bw.injectCSS(altCssStr, { id: altStyleId, append: false });
7198
+ return {
7199
+ css: cssStr,
7200
+ alternateCss: altCssStr,
7201
+ rules: themedRules,
7202
+ alternateRules: altRawRules,
7203
+ palette: palette,
7204
+ alternatePalette: altPalette,
7205
+ isLightPrimary: lightPrimary
7206
+ };
7207
+ };
7196
7208
 
7197
- bw._activeThemeStyleIds = [styleId, altStyleId];
7209
+ /**
7210
+ * Inject styles into the DOM with optional scoping.
7211
+ *
7212
+ * Takes a styles object from `makeStyles()` and creates a single `<style>`
7213
+ * element in `<head>`. If a scope selector is provided, all CSS rules are
7214
+ * wrapped under that selector. Alternate CSS is wrapped under `.bw_theme_alt`.
7215
+ *
7216
+ * @param {Object} styles - Result of `bw.makeStyles()`
7217
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview'). Omit for global.
7218
+ * @returns {Element|null} The `<style>` element, or null in Node.js
7219
+ * @category CSS & Styling
7220
+ * @see bw.makeStyles
7221
+ * @see bw.loadStyles
7222
+ * @see bw.clearStyles
7223
+ * @example
7224
+ * var styles = bw.makeStyles({ primary: '#4f46e5' });
7225
+ * bw.applyStyles(styles); // global
7226
+ * bw.applyStyles(styles, '#my-dashboard'); // scoped
7227
+ */
7228
+ bw.applyStyles = function(styles, scope) {
7229
+ if (!bw._isBrowser) return null;
7230
+ if (!styles || !styles.rules) {
7231
+ _cw('bw.applyStyles: invalid styles object');
7232
+ return null;
7198
7233
  }
7199
7234
 
7200
- // Update bw.u color entries to reflect the palette
7201
- if (!name) {
7202
- bw.u.bgTeal = { background: palette.primary.base, color: palette.primary.textOn };
7203
- bw.u.textTeal = { color: palette.primary.base };
7204
- bw.u.bgWhite = { background: '#ffffff' };
7205
- bw.u.textWhite = { color: '#ffffff' };
7235
+ var styleId = _scopeToStyleId(scope);
7236
+
7237
+ // Scope the primary rules if a scope is provided
7238
+ var primaryRules = styles.rules;
7239
+ if (scope) {
7240
+ primaryRules = scopeRulesUnder(primaryRules, scope);
7206
7241
  }
7207
7242
 
7208
- // Store active theme state
7209
- var result = {
7210
- css: cssStr,
7211
- palette: palette,
7212
- name: name,
7213
- isLightPrimary: lightPrimary,
7214
- alternate: {
7215
- css: altCssStr,
7216
- palette: altPalette
7243
+ // Wrap alternate rules with .bw_theme_alt
7244
+ var altRules = styles.alternateRules;
7245
+ if (altRules) {
7246
+ if (scope) {
7247
+ // Scoped compound: #scope.bw_theme_alt .bw_card
7248
+ altRules = scopeRulesUnder(altRules, scope + '.bw_theme_alt');
7249
+ } else {
7250
+ // Global: .bw_theme_alt .bw_card
7251
+ altRules = scopeRulesUnder(altRules, '.bw_theme_alt');
7217
7252
  }
7218
- };
7219
- bw._activeTheme = result;
7220
- bw._activeThemeMode = 'primary';
7253
+ }
7221
7254
 
7222
- return result;
7255
+ // Combine primary + alternate into one CSS string
7256
+ var combined = bw.css(primaryRules);
7257
+ if (altRules) {
7258
+ combined += '\n' + bw.css(altRules);
7259
+ }
7260
+
7261
+ return bw.injectCSS(combined, { id: styleId, append: false });
7223
7262
  };
7224
7263
 
7225
7264
  /**
7226
- * Apply a theme mode. Switches between primary and alternate palettes
7227
- * by adding/removing the `bw_theme_alt` class on `<html>`.
7265
+ * Generate and apply styles in one call. Convenience wrapper.
7266
+ *
7267
+ * Equivalent to: `bw.applyStyles(bw.makeStyles(config), scope)`
7228
7268
  *
7229
- * @param {string} mode - 'primary' | 'alternate' | 'light' | 'dark'
7230
- * @returns {string} Active mode: 'primary' or 'alternate'
7269
+ * @param {Object} [config] - Style configuration (same as `makeStyles`)
7270
+ * @param {string} [scope] - Scope selector (same as `applyStyles`)
7271
+ * @returns {Element|null} The `<style>` element, or null in Node.js
7231
7272
  * @category CSS & Styling
7232
- * @see bw.generateTheme
7233
- * @see bw.toggleTheme
7273
+ * @see bw.makeStyles
7274
+ * @see bw.applyStyles
7234
7275
  * @example
7235
- * bw.applyTheme('alternate'); // switch to alternate palette
7236
- * bw.applyTheme('dark'); // switch to whichever palette is darker
7237
- * bw.applyTheme('primary'); // switch back to primary palette
7238
- */
7239
- bw.applyTheme = function(mode) {
7240
- if (!bw._isBrowser) return mode || 'primary';
7241
- var root = document.documentElement;
7242
- var isLight = bw._activeTheme ? bw._activeTheme.isLightPrimary : true;
7243
-
7244
- var wantAlt;
7245
- if (mode === 'primary') wantAlt = false;
7246
- else if (mode === 'alternate') wantAlt = true;
7247
- else if (mode === 'light') wantAlt = !isLight;
7248
- else if (mode === 'dark') wantAlt = isLight;
7249
- else wantAlt = false;
7250
-
7251
- if (wantAlt) {
7252
- root.classList.add('bw_theme_alt');
7253
- } else {
7254
- root.classList.remove('bw_theme_alt');
7276
+ * bw.loadStyles(); // defaults, global
7277
+ * bw.loadStyles({ primary: '#4f46e5' }); // custom, global
7278
+ * bw.loadStyles({ primary: '#4f46e5' }, '#my-dashboard'); // custom, scoped
7279
+ */
7280
+ bw.loadStyles = function(config, scope) {
7281
+ // Also inject structural CSS first (only once)
7282
+ if (bw._isBrowser) {
7283
+ var existing = document.getElementById('bw_structural');
7284
+ if (!existing) {
7285
+ var structuralCSS = bw.css(getStructuralStyles());
7286
+ bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false });
7287
+ }
7255
7288
  }
7289
+ return bw.applyStyles(bw.makeStyles(config), scope);
7290
+ };
7256
7291
 
7257
- bw._activeThemeMode = wantAlt ? 'alternate' : 'primary';
7258
- return bw._activeThemeMode;
7292
+ /**
7293
+ * Inject the CSS reset (box-sizing, html/body font, reduced-motion).
7294
+ * Idempotent — if already injected, returns the existing `<style>` element.
7295
+ *
7296
+ * @returns {Element|null} The `<style>` element, or null in Node.js
7297
+ * @category CSS & Styling
7298
+ * @see bw.loadStyles
7299
+ * @see bw.clearStyles
7300
+ * @example
7301
+ * bw.loadReset(); // inject once, safe to call multiple times
7302
+ */
7303
+ bw.loadReset = function() {
7304
+ if (!bw._isBrowser) return null;
7305
+ var existing = document.getElementById('bw_style_reset');
7306
+ if (existing) return existing;
7307
+ return bw.injectCSS(bw.css(getResetStyles()), { id: 'bw_style_reset', append: false });
7259
7308
  };
7260
7309
 
7261
7310
  /**
7262
- * Toggle between primary and alternate theme palettes.
7311
+ * Toggle between primary and alternate palettes.
7263
7312
  *
7313
+ * Adds/removes the `bw_theme_alt` class on the scoping element.
7314
+ * Without a scope, toggles on `<html>` (global).
7315
+ * With a scope, toggles on the first matching element.
7316
+ *
7317
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard'). Omit for global.
7264
7318
  * @returns {string} Active mode after toggle: 'primary' or 'alternate'
7265
7319
  * @category CSS & Styling
7266
- * @see bw.applyTheme
7267
- * @see bw.generateTheme
7320
+ * @see bw.applyStyles
7321
+ * @see bw.clearStyles
7268
7322
  * @example
7269
- * bw.toggleTheme(); // flip between primary and alternate
7323
+ * bw.toggleStyles(); // global toggle on <html>
7324
+ * bw.toggleStyles('#my-dashboard'); // scoped toggle
7270
7325
  */
7271
- bw.toggleTheme = function() {
7272
- var current = bw._activeThemeMode || 'primary';
7273
- return bw.applyTheme(current === 'primary' ? 'alternate' : 'primary');
7326
+ bw.toggleStyles = function(scope) {
7327
+ if (!bw._isBrowser) return 'primary';
7328
+ var target;
7329
+ if (scope) {
7330
+ var els = bw.$(scope);
7331
+ target = els[0];
7332
+ } else {
7333
+ target = document.documentElement;
7334
+ }
7335
+ if (!target) return 'primary';
7336
+
7337
+ var hasAlt = target.classList.contains('bw_theme_alt');
7338
+ if (hasAlt) {
7339
+ target.classList.remove('bw_theme_alt');
7340
+ return 'primary';
7341
+ } else {
7342
+ target.classList.add('bw_theme_alt');
7343
+ return 'alternate';
7344
+ }
7274
7345
  };
7275
7346
 
7276
7347
  /**
7277
- * Remove the currently active theme's injected style elements from the DOM.
7278
- * Use this before generating a new theme with a different name to prevent
7279
- * stale CSS accumulation.
7348
+ * Remove injected styles for a given scope.
7349
+ *
7350
+ * Finds the `<style>` element by id and removes it. Also removes
7351
+ * the `bw_theme_alt` class from the relevant element.
7280
7352
  *
7353
+ * @param {string} [scope] - Scope selector. Omit to remove global styles.
7281
7354
  * @category CSS & Styling
7282
- * @see bw.generateTheme
7355
+ * @see bw.applyStyles
7356
+ * @see bw.loadStyles
7283
7357
  * @example
7284
- * bw.clearTheme(); // remove current theme styles
7285
- * bw.generateTheme('sunset', conf); // inject fresh theme
7286
- */
7287
- bw.clearTheme = function() {
7288
- if (bw._activeThemeStyleIds && bw._isBrowser) {
7289
- bw._activeThemeStyleIds.forEach(function(id) {
7290
- var el = document.getElementById(id);
7291
- if (el) el.remove();
7292
- });
7293
- bw._activeThemeStyleIds = null;
7358
+ * bw.clearStyles(); // remove global styles
7359
+ * bw.clearStyles('#my-dashboard'); // remove scoped styles
7360
+ * bw.clearStyles('reset'); // remove the CSS reset
7361
+ */
7362
+ bw.clearStyles = function(scope) {
7363
+ if (!bw._isBrowser) return;
7364
+ var styleId = _scopeToStyleId(scope);
7365
+ var el = document.getElementById(styleId);
7366
+ if (el) el.remove();
7367
+
7368
+ // Also remove bw_theme_alt from the relevant element
7369
+ if (scope && scope !== 'reset' && scope !== 'global') {
7370
+ var targets = bw.$(scope);
7371
+ if (targets[0]) targets[0].classList.remove('bw_theme_alt');
7372
+ } else if (!scope || scope === 'global') {
7373
+ document.documentElement.classList.remove('bw_theme_alt');
7294
7374
  }
7295
- bw._activeTheme = null;
7296
- bw._activeThemeMode = 'primary';
7297
7375
  };
7298
7376
 
7299
7377
  // Expose color utility functions on bw namespace
@@ -7516,10 +7594,15 @@
7516
7594
  * @param {Object} config - Table configuration
7517
7595
  * @param {Array<Object>} config.data - Array of row objects to display
7518
7596
  * @param {Array<Object>} [config.columns] - Column definitions with key, label, render
7519
- * @param {string} [config.className='table'] - CSS class for table element
7597
+ * @param {string} [config.className=''] - Additional CSS classes for table element
7520
7598
  * @param {boolean} [config.sortable=true] - Enable click-to-sort headers
7521
7599
  * @param {Function} [config.onSort] - Sort callback (column, direction)
7522
- * @returns {Object} TACO object for table
7600
+ * @param {boolean} [config.selectable=false] - Enable row selection on click
7601
+ * @param {Function} [config.onRowClick] - Row click callback (row, index, event)
7602
+ * @param {number} [config.pageSize] - Rows per page (enables pagination when set)
7603
+ * @param {number} [config.currentPage=1] - Current page number (1-based)
7604
+ * @param {Function} [config.onPageChange] - Page change callback (newPage)
7605
+ * @returns {Object} TACO object for table (with optional pagination controls)
7523
7606
  * @category Component Builders
7524
7607
  * @see bw.makeDataTable
7525
7608
  * @example
@@ -7531,7 +7614,12 @@
7531
7614
  * columns: [
7532
7615
  * { key: 'name', label: 'Name' },
7533
7616
  * { key: 'age', label: 'Age' }
7534
- * ]
7617
+ * ],
7618
+ * selectable: true,
7619
+ * onRowClick: function(row, i) { console.log('clicked', row.name); },
7620
+ * pageSize: 10,
7621
+ * currentPage: 1,
7622
+ * onPageChange: function(page) { console.log('page', page); }
7535
7623
  * });
7536
7624
  */
7537
7625
  bw.makeTable = function(config) {
@@ -7544,41 +7632,47 @@
7544
7632
  sortable = true,
7545
7633
  onSort,
7546
7634
  sortColumn,
7547
- sortDirection = 'asc'
7635
+ sortDirection = 'asc',
7636
+ selectable = false,
7637
+ onRowClick,
7638
+ pageSize,
7639
+ currentPage = 1,
7640
+ onPageChange
7548
7641
  } = config;
7549
7642
 
7550
- // Build class list: always include bw_table, add striped/hover, append user className
7643
+ // Build class list: always include bw_table, add striped/hover/selectable, append user className
7551
7644
  let cls = 'bw_table';
7552
7645
  if (striped) cls += ' bw_table_striped';
7553
- if (hover) cls += ' bw_table_hover';
7646
+ if (hover || selectable) cls += ' bw_table_hover';
7647
+ if (selectable) cls += ' bw_table_selectable';
7554
7648
  if (className) cls += ' ' + className;
7555
7649
  cls = cls.trim();
7556
-
7650
+
7557
7651
  // Auto-detect columns if not provided
7558
- const cols = columns || (data.length > 0
7652
+ const cols = columns || (data.length > 0
7559
7653
  ? _keys(data[0]).map(key => ({ key, label: key }))
7560
7654
  : []);
7561
-
7655
+
7562
7656
  // Current sort state
7563
7657
  let currentSortColumn = sortColumn || null;
7564
7658
  let currentSortDirection = sortDirection;
7565
-
7659
+
7566
7660
  // Sort data if column specified
7567
7661
  let sortedData = [...data];
7568
7662
  if (currentSortColumn) {
7569
7663
  sortedData.sort((a, b) => {
7570
7664
  const aVal = a[currentSortColumn];
7571
7665
  const bVal = b[currentSortColumn];
7572
-
7666
+
7573
7667
  // Handle different types
7574
7668
  if (_is(aVal, 'number') && _is(bVal, 'number')) {
7575
7669
  return currentSortDirection === 'asc' ? aVal - bVal : bVal - aVal;
7576
7670
  }
7577
-
7671
+
7578
7672
  // String comparison
7579
7673
  const aStr = String(aVal || '').toLowerCase();
7580
7674
  const bStr = String(bVal || '').toLowerCase();
7581
-
7675
+
7582
7676
  if (currentSortDirection === 'asc') {
7583
7677
  return aStr.localeCompare(bStr);
7584
7678
  } else {
@@ -7586,23 +7680,32 @@
7586
7680
  }
7587
7681
  });
7588
7682
  }
7589
-
7683
+
7684
+ // Pagination
7685
+ const totalRows = sortedData.length;
7686
+ const totalPages = pageSize ? Math.max(1, Math.ceil(totalRows / pageSize)) : 1;
7687
+ const page = Math.max(1, Math.min(currentPage, totalPages));
7688
+ if (pageSize) {
7689
+ const start = (page - 1) * pageSize;
7690
+ sortedData = sortedData.slice(start, start + pageSize);
7691
+ }
7692
+
7590
7693
  // Create sort handler
7591
7694
  const handleSort = (column) => {
7592
7695
  if (!sortable) return;
7593
-
7696
+
7594
7697
  if (currentSortColumn === column) {
7595
7698
  currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
7596
7699
  } else {
7597
7700
  currentSortColumn = column;
7598
7701
  currentSortDirection = 'asc';
7599
7702
  }
7600
-
7703
+
7601
7704
  if (onSort) {
7602
7705
  onSort(column, currentSortDirection);
7603
7706
  }
7604
7707
  };
7605
-
7708
+
7606
7709
  // Build table header
7607
7710
  const thead = {
7608
7711
  t: 'thead',
@@ -7625,24 +7728,87 @@
7625
7728
  }))
7626
7729
  }
7627
7730
  };
7628
-
7629
- // Build table body
7731
+
7732
+ // Build table body with selectable/onRowClick support
7630
7733
  const tbody = {
7631
7734
  t: 'tbody',
7632
- c: sortedData.map(row => ({
7633
- t: 'tr',
7634
- c: cols.map(col => ({
7635
- t: 'td',
7636
- c: col.render ? col.render(row[col.key], row) : String(row[col.key] || '')
7637
- }))
7638
- }))
7735
+ c: sortedData.map((row, idx) => {
7736
+ const globalIdx = pageSize ? (page - 1) * pageSize + idx : idx;
7737
+ const rowAttrs = {};
7738
+ if (selectable || onRowClick) {
7739
+ rowAttrs.style = 'cursor:pointer;';
7740
+ rowAttrs.onclick = function(e) {
7741
+ if (selectable) {
7742
+ // Toggle selected class on this row
7743
+ var tr = e.currentTarget;
7744
+ tr.classList.toggle('bw_table_row_selected');
7745
+ }
7746
+ if (onRowClick) {
7747
+ onRowClick(row, globalIdx, e);
7748
+ }
7749
+ };
7750
+ }
7751
+ return {
7752
+ t: 'tr',
7753
+ a: rowAttrs,
7754
+ c: cols.map(col => ({
7755
+ t: 'td',
7756
+ c: col.render ? col.render(row[col.key], row) : String(row[col.key] || '')
7757
+ }))
7758
+ };
7759
+ })
7639
7760
  };
7640
-
7641
- return {
7761
+
7762
+ const table = {
7642
7763
  t: 'table',
7643
7764
  a: { class: cls },
7644
7765
  c: [thead, tbody]
7645
7766
  };
7767
+
7768
+ // If no pagination, return table directly
7769
+ if (!pageSize) return table;
7770
+
7771
+ // Build pagination controls
7772
+ const pageButtons = [];
7773
+ // Previous button
7774
+ pageButtons.push({
7775
+ t: 'button',
7776
+ a: {
7777
+ class: 'bw_btn bw_btn_sm',
7778
+ disabled: page <= 1 ? 'disabled' : undefined,
7779
+ onclick: page > 1 && onPageChange ? function() { onPageChange(page - 1); } : undefined
7780
+ },
7781
+ c: 'Prev'
7782
+ });
7783
+ // Page info
7784
+ pageButtons.push({
7785
+ t: 'span',
7786
+ a: { style: 'margin:0 0.5rem;font-size:0.875rem;' },
7787
+ c: 'Page ' + page + ' of ' + totalPages
7788
+ });
7789
+ // Next button
7790
+ pageButtons.push({
7791
+ t: 'button',
7792
+ a: {
7793
+ class: 'bw_btn bw_btn_sm',
7794
+ disabled: page >= totalPages ? 'disabled' : undefined,
7795
+ onclick: page < totalPages && onPageChange ? function() { onPageChange(page + 1); } : undefined
7796
+ },
7797
+ c: 'Next'
7798
+ });
7799
+
7800
+ return {
7801
+ t: 'div',
7802
+ a: { class: 'bw_table_paginated' },
7803
+ c: [
7804
+ table,
7805
+ {
7806
+ t: 'div',
7807
+ a: { class: 'bw_table_pagination', style: 'display:flex;align-items:center;justify-content:flex-end;padding:0.5rem 0;gap:0.25rem;' },
7808
+ c: pageButtons
7809
+ }
7810
+ ]
7811
+ };
7646
7812
  };
7647
7813
 
7648
7814
  /**