bitwrench 2.0.17 → 2.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +127 -38
  2. package/dist/bitwrench-bccl.cjs.js +8 -8
  3. package/dist/bitwrench-bccl.cjs.min.js +3 -3
  4. package/dist/bitwrench-bccl.esm.js +8 -8
  5. package/dist/bitwrench-bccl.esm.min.js +3 -3
  6. package/dist/bitwrench-bccl.umd.js +8 -8
  7. package/dist/bitwrench-bccl.umd.min.js +2 -2
  8. package/dist/bitwrench-code-edit.cjs.js +1 -1
  9. package/dist/bitwrench-code-edit.cjs.min.js +1 -1
  10. package/dist/bitwrench-code-edit.es5.js +1 -1
  11. package/dist/bitwrench-code-edit.es5.min.js +1 -1
  12. package/dist/bitwrench-code-edit.esm.js +1 -1
  13. package/dist/bitwrench-code-edit.esm.min.js +1 -1
  14. package/dist/bitwrench-code-edit.umd.js +1 -1
  15. package/dist/bitwrench-code-edit.umd.min.js +1 -1
  16. package/dist/bitwrench-lean.cjs.js +941 -775
  17. package/dist/bitwrench-lean.cjs.min.js +20 -20
  18. package/dist/bitwrench-lean.es5.js +1012 -961
  19. package/dist/bitwrench-lean.es5.min.js +18 -18
  20. package/dist/bitwrench-lean.esm.js +941 -775
  21. package/dist/bitwrench-lean.esm.min.js +20 -20
  22. package/dist/bitwrench-lean.umd.js +941 -775
  23. package/dist/bitwrench-lean.umd.min.js +20 -20
  24. package/dist/bitwrench-util-css.cjs.js +236 -0
  25. package/dist/bitwrench-util-css.cjs.min.js +22 -0
  26. package/dist/bitwrench-util-css.es5.js +414 -0
  27. package/dist/bitwrench-util-css.es5.min.js +21 -0
  28. package/dist/bitwrench-util-css.esm.js +230 -0
  29. package/dist/bitwrench-util-css.esm.min.js +21 -0
  30. package/dist/bitwrench-util-css.umd.js +242 -0
  31. package/dist/bitwrench-util-css.umd.min.js +21 -0
  32. package/dist/bitwrench.cjs.js +948 -782
  33. package/dist/bitwrench.cjs.min.js +21 -21
  34. package/dist/bitwrench.css +456 -132
  35. package/dist/bitwrench.es5.js +1024 -970
  36. package/dist/bitwrench.es5.min.js +19 -19
  37. package/dist/bitwrench.esm.js +949 -783
  38. package/dist/bitwrench.esm.min.js +21 -21
  39. package/dist/bitwrench.min.css +1 -1
  40. package/dist/bitwrench.umd.js +948 -782
  41. package/dist/bitwrench.umd.min.js +21 -21
  42. package/dist/builds.json +178 -90
  43. package/dist/bwserve.cjs.js +514 -68
  44. package/dist/bwserve.esm.js +513 -69
  45. package/dist/sri.json +44 -36
  46. package/package.json +3 -2
  47. package/readme.html +136 -49
  48. package/src/bitwrench-bccl.js +7 -7
  49. package/src/bitwrench-color-utils.js +31 -9
  50. package/src/bitwrench-esm-entry.js +11 -0
  51. package/src/bitwrench-styles.js +439 -232
  52. package/src/bitwrench-util-css.js +229 -0
  53. package/src/bitwrench.js +483 -485
  54. package/src/bwserve/attach.js +57 -0
  55. package/src/bwserve/bwclient.js +141 -0
  56. package/src/bwserve/bwshell.js +102 -0
  57. package/src/bwserve/client.js +151 -1
  58. package/src/bwserve/index.js +127 -28
  59. package/src/cli/attach.js +555 -0
  60. package/src/cli/convert.js +2 -5
  61. package/src/cli/index.js +7 -0
  62. package/src/cli/inject.js +1 -1
  63. package/src/cli/serve.js +6 -2
  64. package/src/generate-css.js +11 -4
  65. package/src/vendor/html2canvas.min.js +20 -0
  66. package/src/version.js +3 -3
  67. package/src/bwserve/shell.js +0 -106
@@ -1,4 +1,4 @@
1
- /*! bitwrench v2.0.17 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
1
+ /*! bitwrench v2.0.18 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
2
2
  (function (global, factory) {
3
3
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
4
4
  typeof define === 'function' && define.amd ? define(factory) :
@@ -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
+ };
1394
+ return rules;
1395
+ }
1396
+
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 };
1316
1422
  return rules;
1317
1423
  }
1318
1424
 
1319
- function generateCodeDemoThemed(scope, palette) {
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
  /**
@@ -3598,7 +3766,7 @@
3598
3766
  if (breakpoint === 'xs') {
3599
3767
  classes.push(`bw_col_${value}`);
3600
3768
  } else {
3601
- classes.push(`bw_col_${breakpoint}-${value}`);
3769
+ classes.push(`bw_col_${breakpoint}_${value}`);
3602
3770
  }
3603
3771
  });
3604
3772
  } else if (size) {
@@ -4981,8 +5149,8 @@
4981
5149
  t: 'li',
4982
5150
  a: { class: `bw_page_item ${currentPage <= 1 ? 'bw_disabled' : ''}`.trim() },
4983
5151
  c: {
4984
- t: 'a',
4985
- a: { class: 'bw_page_link', href: '#', onclick: handleClick(currentPage - 1), 'aria-label': 'Previous' },
5152
+ t: 'button',
5153
+ a: { class: 'bw_page_link', type: 'button', onclick: handleClick(currentPage - 1), 'aria-label': 'Previous', disabled: currentPage <= 1 ? true : undefined },
4986
5154
  c: '\u2039'
4987
5155
  }
4988
5156
  });
@@ -4994,8 +5162,8 @@
4994
5162
  t: 'li',
4995
5163
  a: { class: `bw_page_item ${pageNum === currentPage ? 'bw_active' : ''}`.trim() },
4996
5164
  c: {
4997
- t: 'a',
4998
- a: { class: 'bw_page_link', href: '#', onclick: handleClick(pageNum) },
5165
+ t: 'button',
5166
+ a: { class: 'bw_page_link', type: 'button', onclick: handleClick(pageNum), 'aria-current': pageNum === currentPage ? 'page' : undefined },
4999
5167
  c: '' + pageNum
5000
5168
  }
5001
5169
  });
@@ -5007,8 +5175,8 @@
5007
5175
  t: 'li',
5008
5176
  a: { class: `bw_page_item ${currentPage >= pages ? 'bw_disabled' : ''}`.trim() },
5009
5177
  c: {
5010
- t: 'a',
5011
- a: { class: 'bw_page_link', href: '#', onclick: handleClick(currentPage + 1), 'aria-label': 'Next' },
5178
+ t: 'button',
5179
+ a: { class: 'bw_page_link', type: 'button', onclick: handleClick(currentPage + 1), 'aria-label': 'Next', disabled: currentPage >= pages ? true : undefined },
5012
5180
  c: '\u203A'
5013
5181
  }
5014
5182
  });
@@ -7290,7 +7458,12 @@
7290
7458
  el = document.querySelector('[data-bw_id="' + id + '"]');
7291
7459
  }
7292
7460
 
7293
- // 5. Cache the result for next time
7461
+ // 5. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
7462
+ if (!el && id.indexOf('bw_uuid_') === 0) {
7463
+ el = document.querySelector('.' + id);
7464
+ }
7465
+
7466
+ // 6. Cache the result for next time
7294
7467
  if (el) {
7295
7468
  bw._nodeMap[id] = el;
7296
7469
  }
@@ -7343,6 +7516,84 @@
7343
7516
  }
7344
7517
  };
7345
7518
 
7519
+ // ===================================================================================
7520
+ // bw.assignUUID() / bw.getUUID() — Explicit UUID addressing for TACO objects
7521
+ // ===================================================================================
7522
+
7523
+ /**
7524
+ * Regex to match a bw_uuid_* token in a class string.
7525
+ * @private
7526
+ */
7527
+ var _UUID_RE = /\bbw_uuid_[a-z0-9_]+\b/;
7528
+
7529
+ /**
7530
+ * Assign a UUID to a TACO object by appending a `bw_uuid_*` token to `taco.a.class`.
7531
+ *
7532
+ * Idempotent by default — calling twice returns the same UUID. Pass `forceNew=true`
7533
+ * to replace an existing UUID (useful in loops where each TACO needs a unique ID).
7534
+ *
7535
+ * @param {Object} taco - A TACO object `{t, a, c, o}`
7536
+ * @param {boolean} [forceNew=false] - If true, replaces any existing UUID with a new one
7537
+ * @returns {string} The UUID string (e.g. 'bw_uuid_a1b2c3d4e5')
7538
+ * @category Identifiers
7539
+ * @example
7540
+ * var card = bw.makeStatCard({ value: '0', label: 'Scans' });
7541
+ * var uuid = bw.assignUUID(card); // 'bw_uuid_a1b2c3d4e5'
7542
+ * var same = bw.assignUUID(card); // same UUID (idempotent)
7543
+ * var diff = bw.assignUUID(card, true); // new UUID (forced)
7544
+ */
7545
+ bw.assignUUID = function(taco, forceNew) {
7546
+ if (!taco || !_is(taco, 'object')) return null;
7547
+
7548
+ // Ensure taco.a exists
7549
+ if (!taco.a) taco.a = {};
7550
+ if (!_is(taco.a.class, 'string')) taco.a.class = taco.a.class ? String(taco.a.class) : '';
7551
+
7552
+ var existing = taco.a.class.match(_UUID_RE);
7553
+
7554
+ if (existing && !forceNew) {
7555
+ return existing[0];
7556
+ }
7557
+
7558
+ // Remove old UUID if forceNew
7559
+ if (existing) {
7560
+ taco.a.class = taco.a.class.replace(_UUID_RE, '').replace(/\s+/g, ' ').trim();
7561
+ }
7562
+
7563
+ var uuid = bw.uuid('uuid');
7564
+ taco.a.class = (taco.a.class ? taco.a.class + ' ' : '') + uuid;
7565
+ return uuid;
7566
+ };
7567
+
7568
+ /**
7569
+ * Read the UUID from a TACO object or DOM element. Pure getter, no side effects.
7570
+ *
7571
+ * @param {Object|Element} tacoOrElement - A TACO object or DOM element
7572
+ * @returns {string|null} The UUID string, or null if none assigned
7573
+ * @category Identifiers
7574
+ * @example
7575
+ * bw.getUUID(card) // 'bw_uuid_a1b2c3d4e5' (from TACO)
7576
+ * bw.getUUID(domEl) // 'bw_uuid_a1b2c3d4e5' (from DOM element)
7577
+ * bw.getUUID({t:'div'}) // null (no UUID)
7578
+ */
7579
+ bw.getUUID = function(tacoOrElement) {
7580
+ if (!tacoOrElement) return null;
7581
+
7582
+ var classStr;
7583
+ // DOM element: check className
7584
+ if (tacoOrElement.className !== undefined && tacoOrElement.tagName) {
7585
+ classStr = tacoOrElement.className;
7586
+ }
7587
+ // TACO object: check a.class
7588
+ else if (tacoOrElement.a && _is(tacoOrElement.a.class, 'string')) {
7589
+ classStr = tacoOrElement.a.class;
7590
+ }
7591
+
7592
+ if (!classStr) return null;
7593
+ var match = classStr.match(_UUID_RE);
7594
+ return match ? match[0] : null;
7595
+ };
7596
+
7346
7597
  /**
7347
7598
  * Escape HTML special characters to prevent XSS.
7348
7599
  *
@@ -7392,6 +7643,42 @@
7392
7643
  return { __bw_raw: true, v: String(str) };
7393
7644
  };
7394
7645
 
7646
+ /**
7647
+ * Hyperscript-style TACO constructor.
7648
+ *
7649
+ * A convenience helper that returns a canonical TACO object from positional
7650
+ * arguments. The return value is a plain object — serializable, works with
7651
+ * bwserve, and accepted everywhere TACO is accepted.
7652
+ *
7653
+ * @param {string} tag - HTML tag name (e.g. 'div', 'p', 'section')
7654
+ * @param {Object|null} [attrs] - HTML attributes object. Pass null or omit to skip.
7655
+ * @param {*} [content] - Content: string, number, TACO object, or array of children.
7656
+ * @param {Object} [options] - TACO options (state, lifecycle hooks, render fn).
7657
+ * @returns {Object} Plain TACO object {t, a?, c?, o?}
7658
+ * @category Utilities
7659
+ * @see bw.html
7660
+ * @see bw.createDOM
7661
+ * @see bw.DOM
7662
+ * @example
7663
+ * bw.h('div')
7664
+ * // => { t: 'div' }
7665
+ *
7666
+ * bw.h('p', { class: 'bw_text_muted' }, 'Hello')
7667
+ * // => { t: 'p', a: { class: 'bw_text_muted' }, c: 'Hello' }
7668
+ *
7669
+ * bw.h('ul', null, [
7670
+ * bw.h('li', null, 'one'),
7671
+ * bw.h('li', null, 'two')
7672
+ * ])
7673
+ * // => { t: 'ul', c: [{ t: 'li', c: 'one' }, { t: 'li', c: 'two' }] }
7674
+ */
7675
+ bw.h = function(tag, attrs, content, options) {
7676
+ var taco = { t: String(tag) };
7677
+ if (attrs !== null && attrs !== undefined) taco.a = attrs;
7678
+ if (content !== undefined) taco.c = content;
7679
+ if (options !== undefined) taco.o = options;
7680
+ return taco;
7681
+ };
7395
7682
 
7396
7683
  /**
7397
7684
  * Convert a TACO object (or array of TACOs) to an HTML string.
@@ -7656,7 +7943,7 @@
7656
7943
  ? (THEME_PRESETS[theme.toLowerCase()] || null)
7657
7944
  : theme;
7658
7945
  if (themeConfig) {
7659
- var themeResult = bw.generateTheme('', Object.assign({}, themeConfig, { inject: false }));
7946
+ var themeResult = bw.makeStyles(themeConfig);
7660
7947
  themeCSS = themeResult.css;
7661
7948
  }
7662
7949
  }
@@ -7682,14 +7969,14 @@
7682
7969
  // Combine all CSS
7683
7970
  var allCSS = (themeCSS ? themeCSS + '\n' : '') + css;
7684
7971
 
7685
- // Body-end script: registry entries + optional loadDefaultStyles
7972
+ // Body-end script: registry entries + optional loadStyles
7686
7973
  var bodyEndScript = '';
7687
7974
  var bodyEndParts = [];
7688
7975
  if (registryEntries) {
7689
7976
  bodyEndParts.push(registryEntries);
7690
7977
  }
7691
7978
  if (runtime === 'inline' || runtime === 'cdn') {
7692
- bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadDefaultStyles();}');
7979
+ bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadStyles();}');
7693
7980
  }
7694
7981
  if (bodyEndParts.length > 0) {
7695
7982
  bodyEndScript = '<script>\n' + bodyEndParts.join('\n') + '\n</script>';
@@ -7863,6 +8150,14 @@
7863
8150
  bw._registerNode(el, null);
7864
8151
  }
7865
8152
 
8153
+ // Register UUID class in node cache (bw_uuid_* tokens in class string)
8154
+ if (el.className) {
8155
+ var uuidMatch = el.className.match(_UUID_RE);
8156
+ if (uuidMatch) {
8157
+ bw._nodeMap[uuidMatch[0]] = el;
8158
+ }
8159
+ }
8160
+
7866
8161
  // Handle lifecycle hooks and state
7867
8162
  if (opts.mounted || opts.unmount || opts.render || opts.state) {
7868
8163
  const id = attrs['data-bw_id'] || bw.uuid();
@@ -8235,6 +8530,16 @@
8235
8530
  bw.cleanup = function(element) {
8236
8531
  if (!bw._isBrowser || !element) return;
8237
8532
 
8533
+ // Deregister UUID classes from node cache (element + descendants)
8534
+ // Covers elements that have UUID but no data-bw_id
8535
+ var selfUuidMatch = element.className && element.className.match(_UUID_RE);
8536
+ if (selfUuidMatch) delete bw._nodeMap[selfUuidMatch[0]];
8537
+ var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
8538
+ uuidEls.forEach(function(uel) {
8539
+ var m = uel.className && uel.className.match(_UUID_RE);
8540
+ if (m) delete bw._nodeMap[m[0]];
8541
+ });
8542
+
8238
8543
  // Find all elements with data-bw_id
8239
8544
  const elements = element.querySelectorAll('[data-bw_id]');
8240
8545
 
@@ -8250,6 +8555,10 @@
8250
8555
  // Deregister from node cache
8251
8556
  bw._deregisterNode(el, id);
8252
8557
 
8558
+ // Deregister UUID class from node cache
8559
+ var uuidMatch = el.className && el.className.match(_UUID_RE);
8560
+ if (uuidMatch) delete bw._nodeMap[uuidMatch[0]];
8561
+
8253
8562
  // Clean up pub/sub subscriptions tied to this element
8254
8563
  if (el._bw_subs) {
8255
8564
  el._bw_subs.forEach(function(unsub) { unsub(); });
@@ -8274,6 +8583,10 @@
8274
8583
  // Deregister from node cache
8275
8584
  bw._deregisterNode(element, id);
8276
8585
 
8586
+ // Deregister UUID class from node cache
8587
+ var elemUuidMatch = element.className && element.className.match(_UUID_RE);
8588
+ if (elemUuidMatch) delete bw._nodeMap[elemUuidMatch[0]];
8589
+
8277
8590
  // Clean up pub/sub subscriptions tied to element itself
8278
8591
  if (element._bw_subs) {
8279
8592
  element._bw_subs.forEach(function(unsub) { unsub(); });
@@ -8885,7 +9198,7 @@
8885
9198
  willMount: o.willMount || null,
8886
9199
  mounted: o.mounted || null,
8887
9200
  willUpdate: o.willUpdate || null,
8888
- onUpdate: o.onUpdate || null,
9201
+ onUpdate: o.onUpdate || o.updated || null,
8889
9202
  unmount: o.unmount || null,
8890
9203
  willDestroy: o.willDestroy || null
8891
9204
  };
@@ -9824,7 +10137,7 @@
9824
10137
  * and calls the named method. This is the bitwrench equivalent of
9825
10138
  * Win32 SendMessage(hwnd, msg, wParam, lParam).
9826
10139
  *
9827
- * @param {string} target - Component UUID (data-bw_comp_id) or user tag (CSS class)
10140
+ * @param {string} target - Component UUID (bw_uuid_*), comp ID (data-bw_comp_id), or user tag (CSS class)
9828
10141
  * @param {string} action - Method name to call on the component
9829
10142
  * @param {*} data - Data to pass to the method
9830
10143
  * @returns {boolean} True if message was dispatched successfully
@@ -9841,9 +10154,14 @@
9841
10154
  * };
9842
10155
  */
9843
10156
  bw.message = function(target, action, data) {
9844
- // Try data-bw_comp_id attribute first, then CSS class (user tag)
9845
- var el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
9846
- if (!el) {
10157
+ // Try bw._el() first (handles UUID class, nodeMap cache, getElementById)
10158
+ var el = bw._el(target);
10159
+ // Then try data-bw_comp_id attribute
10160
+ if (!el || !el._bwComponentHandle) {
10161
+ el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
10162
+ }
10163
+ // Then try CSS class (user tag)
10164
+ if (!el || !el._bwComponentHandle) {
9847
10165
  el = bw.$('.' + target)[0];
9848
10166
  }
9849
10167
  if (!el || !el._bwComponentHandle) return false;
@@ -9857,59 +10175,24 @@
9857
10175
  };
9858
10176
 
9859
10177
  // ===================================================================================
9860
- // bw.clientApply() / bw.clientConnect() — Server-driven UI protocol
10178
+ // bw.apply() / bw.parseJSONFlex() — Server-driven UI protocol
9861
10179
  // ===================================================================================
9862
10180
 
9863
10181
  /**
9864
10182
  * Registry of named functions sent via register messages.
9865
- * Populated by clientApply({ type: 'register', name, body }).
9866
- * Invoked by clientApply({ type: 'call', name, args }).
10183
+ * Populated by bw.apply({ type: 'register', name, body }).
10184
+ * Invoked by bw.apply({ type: 'call', name, args }).
9867
10185
  * @private
9868
10186
  */
9869
10187
  bw._clientFunctions = {};
9870
10188
 
9871
10189
  /**
9872
- * Whether exec messages are allowed. Set by clientConnect opts.allowExec.
10190
+ * Whether exec messages are allowed. Set by bwclient connect opts.allowExec.
9873
10191
  * Default false — exec messages are rejected unless explicitly opted in.
9874
10192
  * @private
9875
10193
  */
9876
10194
  bw._allowExec = false;
9877
10195
 
9878
- /**
9879
- * Built-in client functions available via call() without registration.
9880
- * @private
9881
- */
9882
- bw._builtinClientFunctions = {
9883
- scrollTo: function(selector) {
9884
- var el = bw._el(selector);
9885
- if (el) el.scrollTop = el.scrollHeight;
9886
- },
9887
- focus: function(selector) {
9888
- var el = bw._el(selector);
9889
- if (el && _is(el.focus, 'function')) el.focus();
9890
- },
9891
- download: function(filename, content, mimeType) {
9892
- if (typeof document === 'undefined') return;
9893
- var blob = new Blob([content], { type: mimeType || 'text/plain' });
9894
- var a = document.createElement('a');
9895
- a.href = URL.createObjectURL(blob);
9896
- a.download = filename;
9897
- a.click();
9898
- URL.revokeObjectURL(a.href);
9899
- },
9900
- clipboard: function(text) {
9901
- if (typeof navigator !== 'undefined' && navigator.clipboard) {
9902
- navigator.clipboard.writeText(text);
9903
- }
9904
- },
9905
- redirect: function(url) {
9906
- if (typeof window !== 'undefined') window.location.href = url;
9907
- },
9908
- log: function() {
9909
- console.log.apply(console, arguments);
9910
- }
9911
- };
9912
-
9913
10196
  /**
9914
10197
  * Parse a bwserve protocol message string, supporting both strict JSON
9915
10198
  * and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
@@ -9924,9 +10207,9 @@
9924
10207
  * @param {string} str - JSON or r-prefixed relaxed JSON string
9925
10208
  * @returns {Object} Parsed message object
9926
10209
  * @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
9927
- * @category Server
10210
+ * @category Core
9928
10211
  */
9929
- bw.clientParse = function(str) {
10212
+ bw.parseJSONFlex = function(str) {
9930
10213
  str = (str || '').trim();
9931
10214
  if (str.charAt(0) !== 'r') return JSON.parse(str);
9932
10215
  str = str.slice(1);
@@ -10011,10 +10294,10 @@
10011
10294
  * append — target.appendChild(bw.createDOM(node))
10012
10295
  * remove — bw.cleanup(target); target.remove()
10013
10296
  * patch — bw.patch(target, content, attr)
10014
- * batch — iterate ops, call clientApply for each
10297
+ * batch — iterate ops, call bw.apply for each
10015
10298
  * message — bw.message(target, action, data)
10016
10299
  * register — store a named function for later call()
10017
- * call — invoke a registered or built-in function
10300
+ * call — invoke a registered function
10018
10301
  * exec — execute arbitrary JS (requires allowExec)
10019
10302
  *
10020
10303
  * Target resolution:
@@ -10023,9 +10306,9 @@
10023
10306
  *
10024
10307
  * @param {Object} msg - Protocol message
10025
10308
  * @returns {boolean} true if the message was applied successfully
10026
- * @category Server
10309
+ * @category Core
10027
10310
  */
10028
- bw.clientApply = function(msg) {
10311
+ bw.apply = function(msg) {
10029
10312
  if (!msg || !msg.type) return false;
10030
10313
 
10031
10314
  var type = msg.type;
@@ -10059,7 +10342,7 @@
10059
10342
  if (!_isA(msg.ops)) return false;
10060
10343
  var allOk = true;
10061
10344
  msg.ops.forEach(function(op) {
10062
- if (!bw.clientApply(op)) allOk = false;
10345
+ if (!bw.apply(op)) allOk = false;
10063
10346
  });
10064
10347
  return allOk;
10065
10348
 
@@ -10078,7 +10361,7 @@
10078
10361
 
10079
10362
  } else if (type === 'call') {
10080
10363
  if (!msg.name) return false;
10081
- var fn = bw._clientFunctions[msg.name] || bw._builtinClientFunctions[msg.name];
10364
+ var fn = bw._clientFunctions[msg.name];
10082
10365
  if (!_is(fn, 'function')) return false;
10083
10366
  try {
10084
10367
  var args = _isA(msg.args) ? msg.args : [];
@@ -10107,139 +10390,6 @@
10107
10390
  return false;
10108
10391
  };
10109
10392
 
10110
- /**
10111
- * Connect to a bwserve SSE endpoint and apply protocol messages automatically.
10112
- *
10113
- * Returns a connection object with sendAction(), on(), and close() methods.
10114
- *
10115
- * @param {string} url - SSE endpoint URL (e.g., '/__bw/events/client-1')
10116
- * @param {Object} [opts] - Connection options
10117
- * @param {string} [opts.transport='sse'] - Transport type: 'sse' (default) or 'poll'
10118
- * @param {number} [opts.interval=2000] - Poll interval in ms (only for 'poll' transport)
10119
- * @param {string} [opts.actionUrl] - POST endpoint for actions (default: derived from url)
10120
- * @param {boolean} [opts.reconnect=true] - Auto-reconnect on disconnect
10121
- * @param {boolean} [opts.allowExec=false] - Enable exec message type (arbitrary JS execution)
10122
- * @param {Function} [opts.onStatus] - Status callback: 'connecting'|'connected'|'disconnected'
10123
- * @param {Function} [opts.onMessage] - Raw message callback (before clientApply)
10124
- * @returns {Object} Connection object { sendAction, on, close, status }
10125
- * @category Server
10126
- */
10127
- bw.clientConnect = function(url, opts) {
10128
- opts = opts || {};
10129
- var transport = opts.transport || 'sse';
10130
- var actionUrl = opts.actionUrl || url.replace(/\/events\//, '/action/');
10131
- var reconnect = opts.reconnect !== false;
10132
- var onStatus = opts.onStatus || function() {};
10133
- var onMessage = opts.onMessage || null;
10134
- var handlers = {};
10135
- // Set the global allowExec flag from connection options
10136
- bw._allowExec = !!opts.allowExec;
10137
- var conn = {
10138
- status: 'connecting',
10139
- _es: null,
10140
- _pollTimer: null
10141
- };
10142
-
10143
- function setStatus(s) {
10144
- conn.status = s;
10145
- onStatus(s);
10146
- }
10147
-
10148
- function handleMessage(data) {
10149
- try {
10150
- var msg = _is(data, 'string') ? bw.clientParse(data) : data;
10151
- if (onMessage) onMessage(msg);
10152
- if (handlers.message) handlers.message(msg);
10153
- bw.clientApply(msg);
10154
- } catch (e) {
10155
- if (handlers.error) handlers.error(e);
10156
- }
10157
- }
10158
-
10159
- if (transport === 'sse' && typeof EventSource !== 'undefined') {
10160
- setStatus('connecting');
10161
- var es = new EventSource(url);
10162
- conn._es = es;
10163
-
10164
- es.onopen = function() {
10165
- setStatus('connected');
10166
- if (handlers.open) handlers.open();
10167
- };
10168
-
10169
- es.onmessage = function(e) {
10170
- handleMessage(e.data);
10171
- };
10172
-
10173
- es.onerror = function() {
10174
- if (conn.status === 'connected') {
10175
- setStatus('disconnected');
10176
- }
10177
- if (handlers.error) handlers.error(new Error('SSE connection error'));
10178
- if (!reconnect) {
10179
- es.close();
10180
- }
10181
- // EventSource auto-reconnects by default when reconnect=true
10182
- };
10183
- } else if (transport === 'poll') {
10184
- var interval = opts.interval || 2000;
10185
- setStatus('connected');
10186
- conn._pollTimer = setInterval(function() {
10187
- fetch(url).then(function(r) { return r.json(); }).then(function(msgs) {
10188
- if (_isA(msgs)) {
10189
- msgs.forEach(handleMessage);
10190
- } else if (msgs && msgs.type) {
10191
- handleMessage(msgs);
10192
- }
10193
- }).catch(function(e) {
10194
- if (handlers.error) handlers.error(e);
10195
- });
10196
- }, interval);
10197
- }
10198
-
10199
- /**
10200
- * Send an action to the server via POST.
10201
- * @param {string} action - Action name
10202
- * @param {Object} [data] - Action payload
10203
- */
10204
- conn.sendAction = function(action, data) {
10205
- var body = JSON.stringify({ type: 'action', action: action, data: data || {} });
10206
- fetch(actionUrl, {
10207
- method: 'POST',
10208
- headers: { 'Content-Type': 'application/json' },
10209
- body: body
10210
- }).catch(function(e) {
10211
- if (handlers.error) handlers.error(e);
10212
- });
10213
- };
10214
-
10215
- /**
10216
- * Register an event handler.
10217
- * @param {string} event - 'open'|'message'|'error'|'close'
10218
- * @param {Function} handler
10219
- */
10220
- conn.on = function(event, handler) {
10221
- handlers[event] = handler;
10222
- return conn;
10223
- };
10224
-
10225
- /**
10226
- * Close the connection.
10227
- */
10228
- conn.close = function() {
10229
- if (conn._es) {
10230
- conn._es.close();
10231
- conn._es = null;
10232
- }
10233
- if (conn._pollTimer) {
10234
- clearInterval(conn._pollTimer);
10235
- conn._pollTimer = null;
10236
- }
10237
- setStatus('disconnected');
10238
- if (handlers.close) handlers.close();
10239
- };
10240
-
10241
- return conn;
10242
- };
10243
10393
 
10244
10394
  // ===================================================================================
10245
10395
  // bw.inspect() — Debug utility
@@ -10448,7 +10598,7 @@
10448
10598
  * @returns {Element} The style element
10449
10599
  * @category CSS & Styling
10450
10600
  * @see bw.css
10451
- * @see bw.loadDefaultStyles
10601
+ * @see bw.loadStyles
10452
10602
  * @example
10453
10603
  * bw.injectCSS('.my-class { color: red; }');
10454
10604
  * bw.injectCSS({ '.card': { padding: '1rem' } }, { id: 'card-styles' });
@@ -10493,9 +10643,8 @@
10493
10643
  * @param {...Object} styles - Style objects to merge (left-to-right)
10494
10644
  * @returns {Object} Merged style object
10495
10645
  * @category CSS & Styling
10496
- * @see bw.u
10497
10646
  * @example
10498
- * var style = bw.s(bw.u.flex, bw.u.gap4, { color: 'red' });
10647
+ * var style = bw.s({ display: 'flex' }, { gap: '1rem' }, { color: 'red' });
10499
10648
  * // => { display: 'flex', gap: '1rem', color: 'red' }
10500
10649
  */
10501
10650
  bw.s = function() {
@@ -10507,99 +10656,6 @@
10507
10656
  return result;
10508
10657
  };
10509
10658
 
10510
- /**
10511
- * Pre-built CSS utility objects (like Tailwind utilities, but in JS).
10512
- *
10513
- * Compose with `bw.s()` to build inline styles without writing raw CSS strings.
10514
- * Includes flex, padding, margin, typography, color, border, and transition utilities.
10515
- *
10516
- * @category CSS & Styling
10517
- * @see bw.s
10518
- * @example
10519
- * { t: 'div', a: { style: bw.s(bw.u.flex, bw.u.gap4, bw.u.p4) },
10520
- * c: 'Flexbox with 1rem gap and padding' }
10521
- */
10522
- bw.u = {
10523
- // Display
10524
- flex: { display: 'flex' },
10525
- flexCol: { display: 'flex', flexDirection: 'column' },
10526
- flexRow: { display: 'flex', flexDirection: 'row' },
10527
- flexWrap: { display: 'flex', flexWrap: 'wrap' },
10528
- block: { display: 'block' },
10529
- inline: { display: 'inline' },
10530
- hidden: { display: 'none' },
10531
-
10532
- // Flex alignment
10533
- justifyCenter: { justifyContent: 'center' },
10534
- justifyBetween: { justifyContent: 'space-between' },
10535
- justifyEnd: { justifyContent: 'flex-end' },
10536
- alignCenter: { alignItems: 'center' },
10537
- alignStart: { alignItems: 'flex-start' },
10538
- alignEnd: { alignItems: 'flex-end' },
10539
-
10540
- // Gap (0.25rem increments)
10541
- gap1: { gap: '0.25rem' },
10542
- gap2: { gap: '0.5rem' },
10543
- gap3: { gap: '0.75rem' },
10544
- gap4: { gap: '1rem' },
10545
- gap6: { gap: '1.5rem' },
10546
- gap8: { gap: '2rem' },
10547
-
10548
- // Padding
10549
- p0: { padding: '0' },
10550
- p1: { padding: '0.25rem' },
10551
- p2: { padding: '0.5rem' },
10552
- p3: { padding: '0.75rem' },
10553
- p4: { padding: '1rem' },
10554
- p6: { padding: '1.5rem' },
10555
- p8: { padding: '2rem' },
10556
- px4: { paddingLeft: '1rem', paddingRight: '1rem' },
10557
- py2: { paddingTop: '0.5rem', paddingBottom: '0.5rem' },
10558
- py4: { paddingTop: '1rem', paddingBottom: '1rem' },
10559
-
10560
- // Margin (same scale)
10561
- m0: { margin: '0' },
10562
- m4: { margin: '1rem' },
10563
- mt2: { marginTop: '0.5rem' },
10564
- mt4: { marginTop: '1rem' },
10565
- mb2: { marginBottom: '0.5rem' },
10566
- mb4: { marginBottom: '1rem' },
10567
- mx_auto: { marginLeft: 'auto', marginRight: 'auto' },
10568
-
10569
- // Typography
10570
- textSm: { fontSize: '0.875rem' },
10571
- textBase: { fontSize: '1rem' },
10572
- textLg: { fontSize: '1.125rem' },
10573
- textXl: { fontSize: '1.25rem' },
10574
- text2xl: { fontSize: '1.5rem' },
10575
- text3xl: { fontSize: '1.875rem' },
10576
- bold: { fontWeight: '700' },
10577
- semibold: { fontWeight: '600' },
10578
- italic: { fontStyle: 'italic' },
10579
- textCenter: { textAlign: 'center' },
10580
- textRight: { textAlign: 'right' },
10581
-
10582
- // Colors (from design tokens)
10583
- bgWhite: { background: '#ffffff' },
10584
- bgTeal: { background: '#006666', color: '#ffffff' },
10585
- textWhite: { color: '#ffffff' },
10586
- textTeal: { color: '#006666' },
10587
- textMuted: { color: '#888' },
10588
-
10589
- // Borders
10590
- rounded: { borderRadius: '0.375rem' },
10591
- roundedLg: { borderRadius: '0.5rem' },
10592
- roundedFull: { borderRadius: '9999px' },
10593
- border: { border: '1px solid #d8d8d8' },
10594
-
10595
- // Sizing
10596
- wFull: { width: '100%' },
10597
- hFull: { height: '100%' },
10598
-
10599
- // Transitions
10600
- transition: { transition: 'all 0.2s ease' }
10601
- };
10602
-
10603
10659
  /**
10604
10660
  * Generate responsive CSS with media query breakpoints.
10605
10661
  *
@@ -10721,103 +10777,49 @@
10721
10777
  };
10722
10778
  }
10723
10779
 
10724
- /**
10725
- * Load the built-in Bootstrap-inspired default stylesheet.
10726
- *
10727
- * Injects bitwrench's batteries-included CSS (buttons, cards, grids, forms,
10728
- * alerts, badges, nav, tabs, etc.) into the document head. Call once at app startup.
10729
- * Returns null in Node.js (no DOM).
10730
- *
10731
- * @param {Object} [options] - Style loading options
10732
- * @param {boolean} [options.minify=true] - Minify the CSS output
10733
- * @returns {Element|null} Style element if in browser, null in Node.js
10734
- * @category CSS & Styling
10735
- * @see bw.setTheme
10736
- * @see bw.applyTheme
10737
- * @see bw.toggleTheme
10738
- * @example
10739
- * bw.loadDefaultStyles(); // inject all default CSS
10740
- */
10741
- bw.loadDefaultStyles = function(options = {}) {
10742
- const { minify = true, palette } = options;
10743
-
10744
- // 1. Inject structural CSS (layout, sizing — never changes with theme)
10745
- if (bw._isBrowser) {
10746
- var structuralCSS = bw.css(getStructuralStyles());
10747
- bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false, minify: minify });
10748
- }
10749
10780
 
10750
- // 2. Inject cosmetic CSS via generateTheme (colors, shadows, radii)
10751
- var paletteConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, palette || {});
10752
- var result = bw.generateTheme('', Object.assign({}, paletteConfig, { inject: true }));
10753
- return result;
10754
- };
10781
+ // =========================================================================
10782
+ // v2.0.18 Clean Styles API makeStyles / applyStyles / loadStyles / etc.
10783
+ // =========================================================================
10755
10784
 
10785
+ /**
10786
+ * Convert a scope selector to a <style> element id.
10787
+ * @private
10788
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview')
10789
+ * @returns {string} Style element id (e.g. 'bw_style_my_dashboard')
10790
+ */
10791
+ function _scopeToStyleId(scope) {
10792
+ if (!scope || scope === '' || scope === 'global') return 'bw_style_global';
10793
+ if (scope === 'reset') return 'bw_style_reset';
10794
+ // Strip leading # or . and convert - to _
10795
+ var clean = scope.replace(/^[#.]/, '').replace(/-/g, '_');
10796
+ return 'bw_style_' + clean;
10797
+ }
10756
10798
 
10757
10799
  /**
10758
- * Generate a complete, scoped theme from seed colors.
10800
+ * Generate a complete styles object from seed colors and layout config.
10801
+ * Pure function — no DOM, no state, no side effects.
10759
10802
  *
10760
- * Produces CSS for all themed components (buttons, alerts, badges, cards,
10761
- * forms, nav, tables, tabs, list groups, pagination, progress, hero, utilities)
10762
- * scoped under `.name` class. Multiple themes can coexist in the stylesheet.
10763
- * Swap themes by changing the class on a container element.
10803
+ * All parameters are optional. Defaults to the bitwrench default palette.
10764
10804
  *
10765
- * @param {string} name - CSS scope class (e.g. 'ocean'). Empty string = unscoped global.
10766
- * @param {Object} config - Theme configuration
10767
- * @param {string} config.primary - Primary brand color hex
10768
- * @param {string} config.secondary - Secondary color hex
10769
- * @param {string} [config.tertiary] - Tertiary/accent color hex (defaults to primary)
10770
- * @param {string} [config.success='#198754'] - Success color hex
10771
- * @param {string} [config.danger='#dc3545'] - Danger color hex
10772
- * @param {string} [config.warning='#ffc107'] - Warning color hex
10773
- * @param {string} [config.info='#0dcaf0'] - Info color hex
10774
- * @param {string} [config.light='#f8f9fa'] - Light color hex
10775
- * @param {string} [config.dark='#212529'] - Dark color hex
10776
- * @param {string} [config.background] - Page background hex (default: '#ffffff' light, derived dark)
10777
- * @param {string} [config.surface] - Surface/card background hex (default: '#f8f9fa' light, derived dark)
10805
+ * @param {Object} [config] - Style configuration
10806
+ * @param {string} [config.primary='#006666'] - Primary brand color hex
10807
+ * @param {string} [config.secondary='#6c757d'] - Secondary color hex
10808
+ * @param {string} [config.tertiary] - Tertiary color hex (defaults to primary)
10778
10809
  * @param {string} [config.spacing='normal'] - 'compact' | 'normal' | 'spacious'
10779
10810
  * @param {string} [config.radius='md'] - 'none' | 'sm' | 'md' | 'lg' | 'pill'
10780
- * @param {number} [config.fontSize=1.0] - Base font size scale factor
10781
- * @param {string|number} [config.typeRatio='normal'] - 'tight' | 'normal' | 'relaxed' | 'dramatic' or a number
10782
- * @param {string} [config.elevation='md'] - 'flat' | 'sm' | 'md' | 'lg'
10783
- * @param {string} [config.motion='standard'] - 'reduced' | 'standard' | 'expressive'
10784
- * @param {number} [config.harmonize=0.20] - 0-1, semantic color hue shift toward primary
10785
- * @param {boolean} [config.inject=true] - Inject into DOM (browser only)
10786
- * @returns {Object} { css, palette, name, isLightPrimary, alternate: { css, palette } }
10811
+ * @returns {Object} { css, alternateCss, rules, alternateRules, palette, alternatePalette, isLightPrimary }
10787
10812
  * @category CSS & Styling
10788
- * @see bw.applyTheme
10789
- * @see bw.toggleTheme
10790
- * @see bw.loadDefaultStyles
10813
+ * @see bw.applyStyles
10814
+ * @see bw.loadStyles
10791
10815
  * @example
10792
- * // Generate and inject an ocean theme (primary + alternate)
10793
- * var theme = bw.generateTheme('ocean', {
10794
- * primary: '#0077b6',
10795
- * secondary: '#90e0ef',
10796
- * tertiary: '#00b4d8'
10797
- * });
10798
- *
10799
- * // Apply to a container
10800
- * document.getElementById('app').classList.add('ocean');
10801
- *
10802
- * // Toggle to alternate palette
10803
- * bw.toggleTheme();
10804
- *
10805
- * // Generate CSS for static export (Node.js)
10806
- * var result = bw.generateTheme('sunset', {
10807
- * primary: '#e76f51',
10808
- * secondary: '#264653',
10809
- * inject: false
10810
- * });
10811
- * fs.writeFileSync('sunset.css', result.css + result.alternate.css);
10816
+ * var styles = bw.makeStyles({ primary: '#4f46e5', secondary: '#d97706' });
10817
+ * console.log(styles.palette.primary.base); // '#4f46e5'
10818
+ * // styles.css contains all themed CSS — nothing injected
10812
10819
  */
10813
- bw.generateTheme = function(name, config) {
10814
- if (!config || !config.primary || !config.secondary) {
10815
- throw new Error('bw.generateTheme requires config.primary and config.secondary');
10816
- }
10817
-
10818
- // Merge with defaults; if user didn't supply tertiary, default to their primary
10819
- var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config);
10820
- if (!config.tertiary) fullConfig.tertiary = fullConfig.primary;
10820
+ bw.makeStyles = function(config) {
10821
+ var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config || {});
10822
+ if (config && !config.tertiary) fullConfig.tertiary = fullConfig.primary;
10821
10823
 
10822
10824
  // Derive primary palette
10823
10825
  var palette = derivePalette(fullConfig);
@@ -10825,131 +10827,207 @@
10825
10827
  // Resolve layout
10826
10828
  var layout = resolveLayout(fullConfig);
10827
10829
 
10828
- // Generate primary themed CSS rules
10829
- var themedRules = generateThemedCSS(name, palette, layout);
10830
+ // Generate primary themed CSS rules (unscoped)
10831
+ var themedRules = generateThemedCSS('', palette, layout);
10830
10832
  var cssStr = bw.css(themedRules);
10831
10833
 
10832
10834
  // Derive alternate palette (luminance-inverted)
10833
10835
  var altConfig = deriveAlternateConfig(fullConfig);
10834
10836
  var altPalette = derivePalette(altConfig);
10835
10837
 
10836
- // Generate alternate CSS scoped under .bw_theme_alt
10837
- var altRules = generateAlternateCSS(name, altPalette, layout);
10838
- var altCssStr = bw.css(altRules);
10838
+ // Generate alternate CSS rules WITHOUT .bw_theme_alt prefix (raw rules)
10839
+ // applyStyles() wraps them appropriately based on scope
10840
+ var altRawRules = generateThemedCSS('', altPalette, layout);
10841
+
10842
+ // Add body-level surface overrides for the alternate palette.
10843
+ // When .bw_theme_alt is on <html>, ".bw_theme_alt body" correctly matches.
10844
+ altRawRules['body'] = {
10845
+ 'color': altPalette.dark.base,
10846
+ 'background-color': altPalette.surface || altPalette.light.base
10847
+ };
10848
+
10849
+ var altCssStr = bw.css(altRawRules);
10839
10850
 
10840
10851
  // Determine if primary is light-flavored
10841
10852
  var lightPrimary = isLightPalette(fullConfig);
10842
10853
 
10843
- // Inject both CSS sets into DOM if requested
10844
- var shouldInject = config.inject !== false;
10845
- if (shouldInject && bw._isBrowser) {
10846
- var safeName = name ? name.replace(/-/g, '_') : '';
10847
- var styleId = safeName ? 'bw_theme_' + safeName : 'bw_theme_default';
10848
- var altStyleId = safeName ? 'bw_theme_' + safeName + '_alt' : 'bw_theme_default_alt';
10849
-
10850
- bw.injectCSS(cssStr, { id: styleId, append: false });
10851
- bw.injectCSS(altCssStr, { id: altStyleId, append: false });
10854
+ return {
10855
+ css: cssStr,
10856
+ alternateCss: altCssStr,
10857
+ rules: themedRules,
10858
+ alternateRules: altRawRules,
10859
+ palette: palette,
10860
+ alternatePalette: altPalette,
10861
+ isLightPrimary: lightPrimary
10862
+ };
10863
+ };
10852
10864
 
10853
- bw._activeThemeStyleIds = [styleId, altStyleId];
10865
+ /**
10866
+ * Inject styles into the DOM with optional scoping.
10867
+ *
10868
+ * Takes a styles object from `makeStyles()` and creates a single `<style>`
10869
+ * element in `<head>`. If a scope selector is provided, all CSS rules are
10870
+ * wrapped under that selector. Alternate CSS is wrapped under `.bw_theme_alt`.
10871
+ *
10872
+ * @param {Object} styles - Result of `bw.makeStyles()`
10873
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview'). Omit for global.
10874
+ * @returns {Element|null} The `<style>` element, or null in Node.js
10875
+ * @category CSS & Styling
10876
+ * @see bw.makeStyles
10877
+ * @see bw.loadStyles
10878
+ * @see bw.clearStyles
10879
+ * @example
10880
+ * var styles = bw.makeStyles({ primary: '#4f46e5' });
10881
+ * bw.applyStyles(styles); // global
10882
+ * bw.applyStyles(styles, '#my-dashboard'); // scoped
10883
+ */
10884
+ bw.applyStyles = function(styles, scope) {
10885
+ if (!bw._isBrowser) return null;
10886
+ if (!styles || !styles.rules) {
10887
+ _cw('bw.applyStyles: invalid styles object');
10888
+ return null;
10854
10889
  }
10855
10890
 
10856
- // Update bw.u color entries to reflect the palette
10857
- if (!name) {
10858
- bw.u.bgTeal = { background: palette.primary.base, color: palette.primary.textOn };
10859
- bw.u.textTeal = { color: palette.primary.base };
10860
- bw.u.bgWhite = { background: '#ffffff' };
10861
- bw.u.textWhite = { color: '#ffffff' };
10891
+ var styleId = _scopeToStyleId(scope);
10892
+
10893
+ // Scope the primary rules if a scope is provided
10894
+ var primaryRules = styles.rules;
10895
+ if (scope) {
10896
+ primaryRules = scopeRulesUnder(primaryRules, scope);
10862
10897
  }
10863
10898
 
10864
- // Store active theme state
10865
- var result = {
10866
- css: cssStr,
10867
- palette: palette,
10868
- name: name,
10869
- isLightPrimary: lightPrimary,
10870
- alternate: {
10871
- css: altCssStr,
10872
- palette: altPalette
10899
+ // Wrap alternate rules with .bw_theme_alt
10900
+ var altRules = styles.alternateRules;
10901
+ if (altRules) {
10902
+ if (scope) {
10903
+ // Scoped compound: #scope.bw_theme_alt .bw_card
10904
+ altRules = scopeRulesUnder(altRules, scope + '.bw_theme_alt');
10905
+ } else {
10906
+ // Global: .bw_theme_alt .bw_card
10907
+ altRules = scopeRulesUnder(altRules, '.bw_theme_alt');
10873
10908
  }
10874
- };
10875
- bw._activeTheme = result;
10876
- bw._activeThemeMode = 'primary';
10909
+ }
10877
10910
 
10878
- return result;
10911
+ // Combine primary + alternate into one CSS string
10912
+ var combined = bw.css(primaryRules);
10913
+ if (altRules) {
10914
+ combined += '\n' + bw.css(altRules);
10915
+ }
10916
+
10917
+ return bw.injectCSS(combined, { id: styleId, append: false });
10879
10918
  };
10880
10919
 
10881
10920
  /**
10882
- * Apply a theme mode. Switches between primary and alternate palettes
10883
- * by adding/removing the `bw_theme_alt` class on `<html>`.
10921
+ * Generate and apply styles in one call. Convenience wrapper.
10922
+ *
10923
+ * Equivalent to: `bw.applyStyles(bw.makeStyles(config), scope)`
10884
10924
  *
10885
- * @param {string} mode - 'primary' | 'alternate' | 'light' | 'dark'
10886
- * @returns {string} Active mode: 'primary' or 'alternate'
10925
+ * @param {Object} [config] - Style configuration (same as `makeStyles`)
10926
+ * @param {string} [scope] - Scope selector (same as `applyStyles`)
10927
+ * @returns {Element|null} The `<style>` element, or null in Node.js
10887
10928
  * @category CSS & Styling
10888
- * @see bw.generateTheme
10889
- * @see bw.toggleTheme
10929
+ * @see bw.makeStyles
10930
+ * @see bw.applyStyles
10890
10931
  * @example
10891
- * bw.applyTheme('alternate'); // switch to alternate palette
10892
- * bw.applyTheme('dark'); // switch to whichever palette is darker
10893
- * bw.applyTheme('primary'); // switch back to primary palette
10894
- */
10895
- bw.applyTheme = function(mode) {
10896
- if (!bw._isBrowser) return mode || 'primary';
10897
- var root = document.documentElement;
10898
- var isLight = bw._activeTheme ? bw._activeTheme.isLightPrimary : true;
10899
-
10900
- var wantAlt;
10901
- if (mode === 'primary') wantAlt = false;
10902
- else if (mode === 'alternate') wantAlt = true;
10903
- else if (mode === 'light') wantAlt = !isLight;
10904
- else if (mode === 'dark') wantAlt = isLight;
10905
- else wantAlt = false;
10906
-
10907
- if (wantAlt) {
10908
- root.classList.add('bw_theme_alt');
10909
- } else {
10910
- root.classList.remove('bw_theme_alt');
10932
+ * bw.loadStyles(); // defaults, global
10933
+ * bw.loadStyles({ primary: '#4f46e5' }); // custom, global
10934
+ * bw.loadStyles({ primary: '#4f46e5' }, '#my-dashboard'); // custom, scoped
10935
+ */
10936
+ bw.loadStyles = function(config, scope) {
10937
+ // Also inject structural CSS first (only once)
10938
+ if (bw._isBrowser) {
10939
+ var existing = document.getElementById('bw_structural');
10940
+ if (!existing) {
10941
+ var structuralCSS = bw.css(getStructuralStyles());
10942
+ bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false });
10943
+ }
10911
10944
  }
10945
+ return bw.applyStyles(bw.makeStyles(config), scope);
10946
+ };
10912
10947
 
10913
- bw._activeThemeMode = wantAlt ? 'alternate' : 'primary';
10914
- return bw._activeThemeMode;
10948
+ /**
10949
+ * Inject the CSS reset (box-sizing, html/body font, reduced-motion).
10950
+ * Idempotent — if already injected, returns the existing `<style>` element.
10951
+ *
10952
+ * @returns {Element|null} The `<style>` element, or null in Node.js
10953
+ * @category CSS & Styling
10954
+ * @see bw.loadStyles
10955
+ * @see bw.clearStyles
10956
+ * @example
10957
+ * bw.loadReset(); // inject once, safe to call multiple times
10958
+ */
10959
+ bw.loadReset = function() {
10960
+ if (!bw._isBrowser) return null;
10961
+ var existing = document.getElementById('bw_style_reset');
10962
+ if (existing) return existing;
10963
+ return bw.injectCSS(bw.css(getResetStyles()), { id: 'bw_style_reset', append: false });
10915
10964
  };
10916
10965
 
10917
10966
  /**
10918
- * Toggle between primary and alternate theme palettes.
10967
+ * Toggle between primary and alternate palettes.
10968
+ *
10969
+ * Adds/removes the `bw_theme_alt` class on the scoping element.
10970
+ * Without a scope, toggles on `<html>` (global).
10971
+ * With a scope, toggles on the first matching element.
10919
10972
  *
10973
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard'). Omit for global.
10920
10974
  * @returns {string} Active mode after toggle: 'primary' or 'alternate'
10921
10975
  * @category CSS & Styling
10922
- * @see bw.applyTheme
10923
- * @see bw.generateTheme
10976
+ * @see bw.applyStyles
10977
+ * @see bw.clearStyles
10924
10978
  * @example
10925
- * bw.toggleTheme(); // flip between primary and alternate
10979
+ * bw.toggleStyles(); // global toggle on <html>
10980
+ * bw.toggleStyles('#my-dashboard'); // scoped toggle
10926
10981
  */
10927
- bw.toggleTheme = function() {
10928
- var current = bw._activeThemeMode || 'primary';
10929
- return bw.applyTheme(current === 'primary' ? 'alternate' : 'primary');
10982
+ bw.toggleStyles = function(scope) {
10983
+ if (!bw._isBrowser) return 'primary';
10984
+ var target;
10985
+ if (scope) {
10986
+ var els = bw.$(scope);
10987
+ target = els[0];
10988
+ } else {
10989
+ target = document.documentElement;
10990
+ }
10991
+ if (!target) return 'primary';
10992
+
10993
+ var hasAlt = target.classList.contains('bw_theme_alt');
10994
+ if (hasAlt) {
10995
+ target.classList.remove('bw_theme_alt');
10996
+ return 'primary';
10997
+ } else {
10998
+ target.classList.add('bw_theme_alt');
10999
+ return 'alternate';
11000
+ }
10930
11001
  };
10931
11002
 
10932
11003
  /**
10933
- * Remove the currently active theme's injected style elements from the DOM.
10934
- * Use this before generating a new theme with a different name to prevent
10935
- * stale CSS accumulation.
11004
+ * Remove injected styles for a given scope.
11005
+ *
11006
+ * Finds the `<style>` element by id and removes it. Also removes
11007
+ * the `bw_theme_alt` class from the relevant element.
10936
11008
  *
11009
+ * @param {string} [scope] - Scope selector. Omit to remove global styles.
10937
11010
  * @category CSS & Styling
10938
- * @see bw.generateTheme
11011
+ * @see bw.applyStyles
11012
+ * @see bw.loadStyles
10939
11013
  * @example
10940
- * bw.clearTheme(); // remove current theme styles
10941
- * bw.generateTheme('sunset', conf); // inject fresh theme
10942
- */
10943
- bw.clearTheme = function() {
10944
- if (bw._activeThemeStyleIds && bw._isBrowser) {
10945
- bw._activeThemeStyleIds.forEach(function(id) {
10946
- var el = document.getElementById(id);
10947
- if (el) el.remove();
10948
- });
10949
- bw._activeThemeStyleIds = null;
11014
+ * bw.clearStyles(); // remove global styles
11015
+ * bw.clearStyles('#my-dashboard'); // remove scoped styles
11016
+ * bw.clearStyles('reset'); // remove the CSS reset
11017
+ */
11018
+ bw.clearStyles = function(scope) {
11019
+ if (!bw._isBrowser) return;
11020
+ var styleId = _scopeToStyleId(scope);
11021
+ var el = document.getElementById(styleId);
11022
+ if (el) el.remove();
11023
+
11024
+ // Also remove bw_theme_alt from the relevant element
11025
+ if (scope && scope !== 'reset' && scope !== 'global') {
11026
+ var targets = bw.$(scope);
11027
+ if (targets[0]) targets[0].classList.remove('bw_theme_alt');
11028
+ } else if (!scope || scope === 'global') {
11029
+ document.documentElement.classList.remove('bw_theme_alt');
10950
11030
  }
10951
- bw._activeTheme = null;
10952
- bw._activeThemeMode = 'primary';
10953
11031
  };
10954
11032
 
10955
11033
  // Expose color utility functions on bw namespace
@@ -11172,10 +11250,15 @@
11172
11250
  * @param {Object} config - Table configuration
11173
11251
  * @param {Array<Object>} config.data - Array of row objects to display
11174
11252
  * @param {Array<Object>} [config.columns] - Column definitions with key, label, render
11175
- * @param {string} [config.className='table'] - CSS class for table element
11253
+ * @param {string} [config.className=''] - Additional CSS classes for table element
11176
11254
  * @param {boolean} [config.sortable=true] - Enable click-to-sort headers
11177
11255
  * @param {Function} [config.onSort] - Sort callback (column, direction)
11178
- * @returns {Object} TACO object for table
11256
+ * @param {boolean} [config.selectable=false] - Enable row selection on click
11257
+ * @param {Function} [config.onRowClick] - Row click callback (row, index, event)
11258
+ * @param {number} [config.pageSize] - Rows per page (enables pagination when set)
11259
+ * @param {number} [config.currentPage=1] - Current page number (1-based)
11260
+ * @param {Function} [config.onPageChange] - Page change callback (newPage)
11261
+ * @returns {Object} TACO object for table (with optional pagination controls)
11179
11262
  * @category Component Builders
11180
11263
  * @see bw.makeDataTable
11181
11264
  * @example
@@ -11187,7 +11270,12 @@
11187
11270
  * columns: [
11188
11271
  * { key: 'name', label: 'Name' },
11189
11272
  * { key: 'age', label: 'Age' }
11190
- * ]
11273
+ * ],
11274
+ * selectable: true,
11275
+ * onRowClick: function(row, i) { console.log('clicked', row.name); },
11276
+ * pageSize: 10,
11277
+ * currentPage: 1,
11278
+ * onPageChange: function(page) { console.log('page', page); }
11191
11279
  * });
11192
11280
  */
11193
11281
  bw.makeTable = function(config) {
@@ -11200,41 +11288,47 @@
11200
11288
  sortable = true,
11201
11289
  onSort,
11202
11290
  sortColumn,
11203
- sortDirection = 'asc'
11291
+ sortDirection = 'asc',
11292
+ selectable = false,
11293
+ onRowClick,
11294
+ pageSize,
11295
+ currentPage = 1,
11296
+ onPageChange
11204
11297
  } = config;
11205
11298
 
11206
- // Build class list: always include bw_table, add striped/hover, append user className
11299
+ // Build class list: always include bw_table, add striped/hover/selectable, append user className
11207
11300
  let cls = 'bw_table';
11208
11301
  if (striped) cls += ' bw_table_striped';
11209
- if (hover) cls += ' bw_table_hover';
11302
+ if (hover || selectable) cls += ' bw_table_hover';
11303
+ if (selectable) cls += ' bw_table_selectable';
11210
11304
  if (className) cls += ' ' + className;
11211
11305
  cls = cls.trim();
11212
-
11306
+
11213
11307
  // Auto-detect columns if not provided
11214
- const cols = columns || (data.length > 0
11308
+ const cols = columns || (data.length > 0
11215
11309
  ? _keys(data[0]).map(key => ({ key, label: key }))
11216
11310
  : []);
11217
-
11311
+
11218
11312
  // Current sort state
11219
11313
  let currentSortColumn = sortColumn || null;
11220
11314
  let currentSortDirection = sortDirection;
11221
-
11315
+
11222
11316
  // Sort data if column specified
11223
11317
  let sortedData = [...data];
11224
11318
  if (currentSortColumn) {
11225
11319
  sortedData.sort((a, b) => {
11226
11320
  const aVal = a[currentSortColumn];
11227
11321
  const bVal = b[currentSortColumn];
11228
-
11322
+
11229
11323
  // Handle different types
11230
11324
  if (_is(aVal, 'number') && _is(bVal, 'number')) {
11231
11325
  return currentSortDirection === 'asc' ? aVal - bVal : bVal - aVal;
11232
11326
  }
11233
-
11327
+
11234
11328
  // String comparison
11235
11329
  const aStr = String(aVal || '').toLowerCase();
11236
11330
  const bStr = String(bVal || '').toLowerCase();
11237
-
11331
+
11238
11332
  if (currentSortDirection === 'asc') {
11239
11333
  return aStr.localeCompare(bStr);
11240
11334
  } else {
@@ -11242,23 +11336,32 @@
11242
11336
  }
11243
11337
  });
11244
11338
  }
11245
-
11339
+
11340
+ // Pagination
11341
+ const totalRows = sortedData.length;
11342
+ const totalPages = pageSize ? Math.max(1, Math.ceil(totalRows / pageSize)) : 1;
11343
+ const page = Math.max(1, Math.min(currentPage, totalPages));
11344
+ if (pageSize) {
11345
+ const start = (page - 1) * pageSize;
11346
+ sortedData = sortedData.slice(start, start + pageSize);
11347
+ }
11348
+
11246
11349
  // Create sort handler
11247
11350
  const handleSort = (column) => {
11248
11351
  if (!sortable) return;
11249
-
11352
+
11250
11353
  if (currentSortColumn === column) {
11251
11354
  currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
11252
11355
  } else {
11253
11356
  currentSortColumn = column;
11254
11357
  currentSortDirection = 'asc';
11255
11358
  }
11256
-
11359
+
11257
11360
  if (onSort) {
11258
11361
  onSort(column, currentSortDirection);
11259
11362
  }
11260
11363
  };
11261
-
11364
+
11262
11365
  // Build table header
11263
11366
  const thead = {
11264
11367
  t: 'thead',
@@ -11281,24 +11384,87 @@
11281
11384
  }))
11282
11385
  }
11283
11386
  };
11284
-
11285
- // Build table body
11387
+
11388
+ // Build table body with selectable/onRowClick support
11286
11389
  const tbody = {
11287
11390
  t: 'tbody',
11288
- c: sortedData.map(row => ({
11289
- t: 'tr',
11290
- c: cols.map(col => ({
11291
- t: 'td',
11292
- c: col.render ? col.render(row[col.key], row) : String(row[col.key] || '')
11293
- }))
11294
- }))
11391
+ c: sortedData.map((row, idx) => {
11392
+ const globalIdx = pageSize ? (page - 1) * pageSize + idx : idx;
11393
+ const rowAttrs = {};
11394
+ if (selectable || onRowClick) {
11395
+ rowAttrs.style = 'cursor:pointer;';
11396
+ rowAttrs.onclick = function(e) {
11397
+ if (selectable) {
11398
+ // Toggle selected class on this row
11399
+ var tr = e.currentTarget;
11400
+ tr.classList.toggle('bw_table_row_selected');
11401
+ }
11402
+ if (onRowClick) {
11403
+ onRowClick(row, globalIdx, e);
11404
+ }
11405
+ };
11406
+ }
11407
+ return {
11408
+ t: 'tr',
11409
+ a: rowAttrs,
11410
+ c: cols.map(col => ({
11411
+ t: 'td',
11412
+ c: col.render ? col.render(row[col.key], row) : String(row[col.key] || '')
11413
+ }))
11414
+ };
11415
+ })
11295
11416
  };
11296
-
11297
- return {
11417
+
11418
+ const table = {
11298
11419
  t: 'table',
11299
11420
  a: { class: cls },
11300
11421
  c: [thead, tbody]
11301
11422
  };
11423
+
11424
+ // If no pagination, return table directly
11425
+ if (!pageSize) return table;
11426
+
11427
+ // Build pagination controls
11428
+ const pageButtons = [];
11429
+ // Previous button
11430
+ pageButtons.push({
11431
+ t: 'button',
11432
+ a: {
11433
+ class: 'bw_btn bw_btn_sm',
11434
+ disabled: page <= 1 ? 'disabled' : undefined,
11435
+ onclick: page > 1 && onPageChange ? function() { onPageChange(page - 1); } : undefined
11436
+ },
11437
+ c: 'Prev'
11438
+ });
11439
+ // Page info
11440
+ pageButtons.push({
11441
+ t: 'span',
11442
+ a: { style: 'margin:0 0.5rem;font-size:0.875rem;' },
11443
+ c: 'Page ' + page + ' of ' + totalPages
11444
+ });
11445
+ // Next button
11446
+ pageButtons.push({
11447
+ t: 'button',
11448
+ a: {
11449
+ class: 'bw_btn bw_btn_sm',
11450
+ disabled: page >= totalPages ? 'disabled' : undefined,
11451
+ onclick: page < totalPages && onPageChange ? function() { onPageChange(page + 1); } : undefined
11452
+ },
11453
+ c: 'Next'
11454
+ });
11455
+
11456
+ return {
11457
+ t: 'div',
11458
+ a: { class: 'bw_table_paginated' },
11459
+ c: [
11460
+ table,
11461
+ {
11462
+ t: 'div',
11463
+ a: { class: 'bw_table_pagination', style: 'display:flex;align-items:center;justify-content:flex-end;padding:0.5rem 0;gap:0.25rem;' },
11464
+ c: pageButtons
11465
+ }
11466
+ ]
11467
+ };
11302
11468
  };
11303
11469
 
11304
11470
  /**