bitwrench 2.0.17 → 2.0.18

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