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,18 +1,18 @@
1
- /*! bitwrench v2.0.17 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
1
+ /*! bitwrench v2.0.18 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
2
2
  /**
3
3
  * Auto-generated version file from package.json
4
4
  * DO NOT EDIT DIRECTLY - Use npm run generate-version
5
5
  */
6
6
 
7
7
  const VERSION_INFO = {
8
- version: '2.0.17',
8
+ version: '2.0.18',
9
9
  name: 'bitwrench',
10
10
  description: 'A library for javascript UI functions.',
11
11
  license: 'BSD-2-Clause',
12
12
  homepage: 'https://deftio.github.com/bitwrench/pages',
13
13
  repository: 'git+https://github.com/deftio/bitwrench.git',
14
14
  author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
15
- buildDate: '2026-03-13T23:15:10.823Z'
15
+ buildDate: '2026-03-17T00:50:09.505Z'
16
16
  };
17
17
 
18
18
  /**
@@ -306,13 +306,18 @@ function harmonize(sourceHex, targetHex, amount) {
306
306
  */
307
307
  function deriveShades(hex) {
308
308
  var rgb = colorParse(hex);
309
+ // For light input colors (L > 75), mixing toward white produces invisible borders.
310
+ // Darken instead so borders remain visible against light backgrounds.
311
+ var borderColor = hexToHsl(hex)[2] > 75
312
+ ? adjustLightness(hex, -18)
313
+ : mixColor(hex, '#ffffff', 0.60);
309
314
  return {
310
315
  base: hex,
311
316
  hover: adjustLightness(hex, -10),
312
317
  active: adjustLightness(hex, -15),
313
318
  light: mixColor(hex, '#ffffff', 0.85),
314
319
  darkText: adjustLightness(hex, -40),
315
- border: mixColor(hex, '#ffffff', 0.60),
320
+ border: borderColor,
316
321
  focus: 'rgba(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ',0.25)',
317
322
  textOn: textOnColor(hex)
318
323
  };
@@ -371,19 +376,27 @@ function deriveAlternateConfig(config) {
371
376
  alt.secondary = deriveAlternateSeed(config.secondary);
372
377
  alt.tertiary = config.tertiary ? deriveAlternateSeed(config.tertiary) : alt.primary;
373
378
 
374
- // Derive alternate surface colors from primary hue
379
+ // Derive alternate surface colors from primary hue.
380
+ // Check actual page surface brightness (not seed color brightness) to decide
381
+ // whether alternate should be dark or light. The page surface is what the
382
+ // user sees; seeds can be dark while the page is still light (default L=96).
375
383
  var priHsl = hexToHsl(config.primary);
376
384
  var h = priHsl[0];
377
- var isLight = isLightPalette(config);
385
+ var primarySurface = config.surface || hslToHex([h, 8, 96]);
386
+ var isLight = relativeLuminance(primarySurface) > 0.179;
378
387
 
379
388
  if (isLight) {
380
- // Primary is light → alternate needs dark surfaces
389
+ // Page surface is light → alternate needs dark surfaces
381
390
  alt.light = hslToHex([h, Math.min(priHsl[1], 15), 15]);
382
391
  alt.dark = hslToHex([h, 5, 88]);
392
+ alt.surface = hslToHex([h, 12, 18]);
393
+ alt.background = hslToHex([h, 10, 14]);
383
394
  } else {
384
- // Primary is dark → alternate needs light surfaces
395
+ // Page surface is dark → alternate needs light surfaces
385
396
  alt.light = hslToHex([h, Math.min(priHsl[1], 10), 96]);
386
397
  alt.dark = hslToHex([h, 10, 18]);
398
+ alt.surface = hslToHex([h, 8, 96]);
399
+ alt.background = hslToHex([h, 6, 98]);
387
400
  }
388
401
 
389
402
  // Semantic colors: harmonize toward primary, then invert for alternate
@@ -431,10 +444,18 @@ function derivePalette(config) {
431
444
  var darkBase = config.dark || hslToHex([h, 10, 13]);
432
445
 
433
446
  // Background & surface tokens — tinted with primary hue for theme personality.
434
- // Very subtle: bg at L=98/S=6, surface at L=96/S=8.
447
+ // Saturation high enough that the hue is visible (each theme feels distinct)
448
+ // but low enough to stay neutral and readable.
435
449
  // User can override with config.background / config.surface.
436
- var bgBase = config.background || hslToHex([h, 6, 98]);
437
- var surfBase = config.surface || hslToHex([h, 8, 96]);
450
+ var bgBase = config.background || hslToHex([h, 22, 96]);
451
+ var surfBase = config.surface || hslToHex([h, 25, 94]);
452
+
453
+ // surfaceAlt: subtle background variant for striped rows, hover states, headers.
454
+ // Slightly lighter than surface in dark mode, slightly darker in light mode.
455
+ var surfHsl = hexToHsl(surfBase);
456
+ var surfAlt = surfHsl[2] <= 50
457
+ ? hslToHex([surfHsl[0], surfHsl[1], Math.min(surfHsl[2] + 8, 100)])
458
+ : hslToHex([surfHsl[0], surfHsl[1], Math.max(surfHsl[2] - 3, 0)]);
438
459
 
439
460
  var palette = {
440
461
  primary: deriveShades(config.primary),
@@ -447,7 +468,8 @@ function derivePalette(config) {
447
468
  light: deriveShades(lightBase),
448
469
  dark: deriveShades(darkBase),
449
470
  background: bgBase,
450
- surface: surfBase
471
+ surface: surfBase,
472
+ surfaceAlt: surfAlt
451
473
  };
452
474
 
453
475
  return palette;
@@ -494,10 +516,12 @@ var SPACING_SCALE = {
494
516
  5: '1.5rem', // 24px
495
517
  6: '2rem'};
496
518
 
519
+ let _S=SPACING_SCALE;
520
+
497
521
  var SPACING_PRESETS = {
498
- 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] },
499
- 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] },
500
- 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] }
522
+ 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] },
523
+ 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] },
524
+ 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] }
501
525
  };
502
526
 
503
527
  var RADIUS_PRESETS = {
@@ -609,20 +633,14 @@ var DEFAULT_PALETTE_CONFIG = {
609
633
  * Built-in theme presets — named color combinations
610
634
  * Each preset provides primary, secondary, and tertiary seed colors.
611
635
  */
612
- var THEME_PRESETS = {
613
- teal: { primary: '#006666', secondary: '#6c757d', tertiary: '#006666' },
614
- ocean: { primary: '#0077b6', secondary: '#90e0ef', tertiary: '#00b4d8' },
615
- sunset: { primary: '#e76f51', secondary: '#264653', tertiary: '#e9c46a' },
616
- forest: { primary: '#2d6a4f', secondary: '#95d5b2', tertiary: '#52b788' },
617
- slate: { primary: '#343a40', secondary: '#adb5bd', tertiary: '#6c757d' },
618
- rose: { primary: '#e11d48', secondary: '#fda4af', tertiary: '#fb7185' },
619
- indigo: { primary: '#4f46e5', secondary: '#a5b4fc', tertiary: '#818cf8' },
620
- amber: { primary: '#d97706', secondary: '#fbbf24', tertiary: '#f59e0b' },
621
- emerald: { primary: '#059669', secondary: '#6ee7b7', tertiary: '#34d399' },
622
- nord: { primary: '#5e81ac', secondary: '#88c0d0', tertiary: '#81a1c1' },
623
- coral: { primary: '#ef6461', secondary: '#4a7c7e', tertiary: '#e8a87c' },
624
- midnight: { primary: '#1e3a5f', secondary: '#7c8db5', tertiary: '#3d5a80' }
625
- };
636
+ var THEME_PRESETS = Object.fromEntries([
637
+ ['teal','#006666','#6c757d','#006666'],['ocean','#0077b6','#90e0ef','#00b4d8'],
638
+ ['sunset','#e76f51','#264653','#e9c46a'],['forest','#2d6a4f','#95d5b2','#52b788'],
639
+ ['slate','#343a40','#adb5bd','#6c757d'],['rose','#e11d48','#fda4af','#fb7185'],
640
+ ['indigo','#4f46e5','#a5b4fc','#818cf8'],['amber','#d97706','#fbbf24','#f59e0b'],
641
+ ['emerald','#059669','#6ee7b7','#34d399'],['nord','#5e81ac','#88c0d0','#81a1c1'],
642
+ ['coral','#ef6461','#4a7c7e','#e8a87c'],['midnight','#1e3a5f','#7c8db5','#3d5a80']
643
+ ].map(function(e) { return [e[0], {primary:e[1],secondary:e[2],tertiary:e[3]}]; }));
626
644
 
627
645
  /**
628
646
  * Resolve layout config to spacing, radius, typeScale, elevation, and motion objects.
@@ -669,6 +687,7 @@ function scopeSelector(name, sel) {
669
687
  if (sel.includes(',')) return sel.split(',').map(function(s) { return '.' + name + ' ' + s.trim(); }).join(', ');
670
688
  return '.' + name + ' ' + sel;
671
689
  }
690
+ var _sx=scopeSelector;
672
691
 
673
692
  // =========================================================================
674
693
  // Themed CSS generators
@@ -677,12 +696,12 @@ function scopeSelector(name, sel) {
677
696
  function generateTypographyThemed(scope, palette, layout) {
678
697
  var mot = layout.motion;
679
698
  var rules = {};
680
- rules[scopeSelector(scope, 'a')] = {
699
+ rules[_sx(scope, 'a')] = {
681
700
  'color': palette.primary.base,
682
701
  'text-decoration': 'none',
683
702
  'transition': 'color ' + mot.fast + ' ' + mot.easing
684
703
  };
685
- rules[scopeSelector(scope, 'a:hover')] = {
704
+ rules[_sx(scope, 'a:hover')] = {
686
705
  'color': palette.primary.hover,
687
706
  'text-decoration': 'underline'
688
707
  };
@@ -695,11 +714,11 @@ function generateButtons(scope, palette, layout) {
695
714
  var rd = layout.radius;
696
715
 
697
716
  // Base button (only when scoped — unscoped uses defaultStyles)
698
- rules[scopeSelector(scope, '.bw_btn')] = {
717
+ rules[_sx(scope, '.bw_btn')] = {
699
718
  'padding': sp.btn,
700
719
  'border-radius': rd.btn
701
720
  };
702
- rules[scopeSelector(scope, '.bw_btn:focus-visible')] = {
721
+ rules[_sx(scope, '.bw_btn:focus-visible')] = {
703
722
  'outline': '2px solid currentColor',
704
723
  'outline-offset': '2px',
705
724
  'box-shadow': '0 0 0 3px ' + palette.primary.focus
@@ -708,12 +727,12 @@ function generateButtons(scope, palette, layout) {
708
727
  // Variant colors handled by palette class on component root
709
728
 
710
729
  // Size variants (structural, reuse layout radius)
711
- rules[scopeSelector(scope, '.bw_btn_lg')] = {
730
+ rules[_sx(scope, '.bw_btn_lg')] = {
712
731
  'padding': '0.625rem 1.5rem',
713
732
  'font-size': '1rem',
714
733
  'border-radius': rd.btn === '50rem' ? '50rem' : (parseInt(rd.btn) + 2) + 'px'
715
734
  };
716
- rules[scopeSelector(scope, '.bw_btn_sm')] = {
735
+ rules[_sx(scope, '.bw_btn_sm')] = {
717
736
  'padding': '0.25rem 0.75rem',
718
737
  'font-size': '0.8125rem',
719
738
  'border-radius': rd.btn === '50rem' ? '50rem' : (Math.max(parseInt(rd.btn) - 1, 0)) + 'px'
@@ -727,7 +746,7 @@ function generateAlerts(scope, palette, layout) {
727
746
  var sp = layout.spacing;
728
747
  var rd = layout.radius;
729
748
 
730
- rules[scopeSelector(scope, '.bw_alert')] = {
749
+ rules[_sx(scope, '.bw_alert')] = {
731
750
  'padding': sp.alert,
732
751
  'border-radius': rd.alert
733
752
  };
@@ -746,36 +765,36 @@ function generateCards(scope, palette, layout) {
746
765
 
747
766
  var elev = layout.elevation;
748
767
  var motion = layout.motion;
749
- rules[scopeSelector(scope, '.bw_card')] = {
768
+ rules[_sx(scope, '.bw_card')] = {
750
769
  'background-color': palette.surface || '#fff',
751
770
  'border': '1px solid ' + palette.light.border,
752
771
  'border-radius': rd.card,
753
772
  'box-shadow': elev.sm,
754
773
  'transition': 'box-shadow ' + motion.normal + ' ' + motion.easing + ', transform ' + motion.normal + ' ' + motion.easing
755
774
  };
756
- rules[scopeSelector(scope, '.bw_card:hover')] = {
775
+ rules[_sx(scope, '.bw_card:hover')] = {
757
776
  'box-shadow': elev.md
758
777
  };
759
- rules[scopeSelector(scope, '.bw_card_hoverable:hover')] = {
778
+ rules[_sx(scope, '.bw_card_hoverable:hover')] = {
760
779
  'box-shadow': elev.lg
761
780
  };
762
- rules[scopeSelector(scope, '.bw_card_body')] = {
781
+ rules[_sx(scope, '.bw_card_body')] = {
763
782
  'padding': sp.card
764
783
  };
765
- rules[scopeSelector(scope, '.bw_card_header')] = {
784
+ rules[_sx(scope, '.bw_card_header')] = {
766
785
  'padding': sp.card.split(' ').map(function(v) { return (parseFloat(v) * 0.7).toFixed(3).replace(/\.?0+$/, '') + 'rem'; }).join(' '),
767
- 'background-color': palette.light.light,
786
+ 'background-color': palette.surfaceAlt,
768
787
  'border-bottom': '1px solid ' + palette.light.border
769
788
  };
770
- rules[scopeSelector(scope, '.bw_card_footer')] = {
771
- 'background-color': palette.light.light,
789
+ rules[_sx(scope, '.bw_card_footer')] = {
790
+ 'background-color': palette.surfaceAlt,
772
791
  'border-top': '1px solid ' + palette.light.border,
773
792
  'color': palette.secondary.base
774
793
  };
775
- rules[scopeSelector(scope, '.bw_card_title')] = {
794
+ rules[_sx(scope, '.bw_card_title')] = {
776
795
  'color': palette.dark.base
777
796
  };
778
- rules[scopeSelector(scope, '.bw_card_subtitle')] = {
797
+ rules[_sx(scope, '.bw_card_subtitle')] = {
779
798
  'color': palette.secondary.base
780
799
  };
781
800
 
@@ -789,55 +808,55 @@ function generateForms(scope, palette, layout) {
789
808
  var sp = layout.spacing;
790
809
  var rd = layout.radius;
791
810
 
792
- rules[scopeSelector(scope, '.bw_form_control')] = {
811
+ rules[_sx(scope, '.bw_form_control')] = {
793
812
  'padding': sp.input,
794
813
  'border-radius': rd.input,
795
814
  'color': palette.dark.base,
796
815
  'background-color': palette.surface || '#fff',
797
816
  'border-color': palette.light.border
798
817
  };
799
- rules[scopeSelector(scope, '.bw_form_control:focus')] = {
818
+ rules[_sx(scope, '.bw_form_control:focus')] = {
800
819
  'border-color': palette.primary.border,
801
820
  'outline': '2px solid ' + palette.primary.base,
802
821
  'outline-offset': '-1px',
803
822
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
804
823
  };
805
- rules[scopeSelector(scope, '.bw_form_control::placeholder')] = {
824
+ rules[_sx(scope, '.bw_form_control::placeholder')] = {
806
825
  'color': palette.secondary.base
807
826
  };
808
- rules[scopeSelector(scope, '.bw_form_label')] = {
827
+ rules[_sx(scope, '.bw_form_label')] = {
809
828
  'color': palette.dark.base
810
829
  };
811
- rules[scopeSelector(scope, '.bw_form_text')] = {
830
+ rules[_sx(scope, '.bw_form_text')] = {
812
831
  'color': palette.secondary.base
813
832
  };
814
- rules[scopeSelector(scope, '.bw_form_check_input:checked')] = {
833
+ rules[_sx(scope, '.bw_form_check_input:checked')] = {
815
834
  'background-color': palette.primary.base,
816
835
  'border-color': palette.primary.base
817
836
  };
818
- rules[scopeSelector(scope, '.bw_form_check_input:focus')] = {
837
+ rules[_sx(scope, '.bw_form_check_input:focus')] = {
819
838
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
820
839
  };
821
840
  // Validation states
822
- rules[scopeSelector(scope, '.bw_form_control.bw_is_valid')] = { 'border-color': palette.success.base };
823
- rules[scopeSelector(scope, '.bw_form_control.bw_is_valid:focus')] = {
841
+ rules[_sx(scope, '.bw_form_control.bw_is_valid')] = { 'border-color': palette.success.base };
842
+ rules[_sx(scope, '.bw_form_control.bw_is_valid:focus')] = {
824
843
  'border-color': palette.success.base,
825
844
  'box-shadow': '0 0 0 0.2rem ' + palette.success.focus
826
845
  };
827
- rules[scopeSelector(scope, '.bw_form_control.bw_is_invalid')] = { 'border-color': palette.danger.base };
828
- rules[scopeSelector(scope, '.bw_form_control.bw_is_invalid:focus')] = {
846
+ rules[_sx(scope, '.bw_form_control.bw_is_invalid')] = { 'border-color': palette.danger.base };
847
+ rules[_sx(scope, '.bw_form_control.bw_is_invalid:focus')] = {
829
848
  'border-color': palette.danger.base,
830
849
  'box-shadow': '0 0 0 0.2rem ' + palette.danger.focus
831
850
  };
832
851
  // Form select
833
- rules[scopeSelector(scope, '.bw_form_select')] = {
852
+ rules[_sx(scope, '.bw_form_select')] = {
834
853
  'padding': sp.input,
835
854
  'border-radius': rd.input,
836
855
  'color': palette.dark.base,
837
856
  'background-color': palette.surface || '#fff',
838
857
  'border-color': palette.light.border
839
858
  };
840
- rules[scopeSelector(scope, '.bw_form_select:focus')] = {
859
+ rules[_sx(scope, '.bw_form_select:focus')] = {
841
860
  'border-color': palette.primary.border,
842
861
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
843
862
  };
@@ -845,43 +864,46 @@ function generateForms(scope, palette, layout) {
845
864
  return rules;
846
865
  }
847
866
 
848
- function generateNavigation(scope, palette) {
867
+ function generateNavigation(scope, palette, layout) {
849
868
  var rules = {};
850
- rules[scopeSelector(scope, '.bw_navbar')] = {
851
- 'background-color': palette.light.light,
869
+ rules[_sx(scope, '.bw_navbar')] = {
870
+ 'background-color': palette.surfaceAlt,
852
871
  'border-bottom-color': palette.light.border
853
872
  };
854
- rules[scopeSelector(scope, '.bw_navbar_brand')] = {
873
+ rules[_sx(scope, '.bw_navbar_brand')] = {
855
874
  'color': palette.dark.base
856
875
  };
857
- rules[scopeSelector(scope, '.bw_navbar_nav .bw_nav_link')] = {
858
- 'color': palette.secondary.base
876
+ rules[_sx(scope, '.bw_navbar_nav .bw_nav_link')] = {
877
+ 'color': palette.secondary.base,
878
+ 'border-radius': layout.radius.btn
859
879
  };
860
- rules[scopeSelector(scope, '.bw_navbar_nav .bw_nav_link:hover')] = {
861
- 'color': palette.dark.base
880
+ rules[_sx(scope, '.bw_navbar_nav .bw_nav_link:hover')] = {
881
+ 'color': palette.dark.base,
882
+ 'background-color': palette.surfaceAlt
862
883
  };
863
- rules[scopeSelector(scope, '.bw_navbar_nav .bw_nav_link.active')] = {
884
+ rules[_sx(scope, '.bw_navbar_nav .bw_nav_link.active')] = {
864
885
  'color': palette.primary.base,
865
- 'background-color': palette.primary.focus
886
+ 'background-color': palette.primary.focus,
887
+ 'font-weight': '600'
866
888
  };
867
- rules[scopeSelector(scope, '.bw_navbar_dark')] = {
889
+ rules[_sx(scope, '.bw_navbar_dark')] = {
868
890
  'background-color': palette.dark.base,
869
891
  'border-bottom-color': palette.dark.hover
870
892
  };
871
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_navbar_brand')] = {
893
+ rules[_sx(scope, '.bw_navbar_dark .bw_navbar_brand')] = {
872
894
  'color': palette.light.base
873
895
  };
874
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_nav_link')] = {
875
- 'color': 'rgba(255,255,255,.65)'
896
+ rules[_sx(scope, '.bw_navbar_dark .bw_nav_link')] = {
897
+ 'color': palette.light.border
876
898
  };
877
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_nav_link:hover')] = {
878
- 'color': '#fff'
899
+ rules[_sx(scope, '.bw_navbar_dark .bw_nav_link:hover')] = {
900
+ 'color': palette.light.base
879
901
  };
880
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_nav_link.active')] = {
881
- 'color': '#fff',
902
+ rules[_sx(scope, '.bw_navbar_dark .bw_nav_link.active')] = {
903
+ 'color': palette.light.base,
882
904
  'font-weight': '600'
883
905
  };
884
- rules[scopeSelector(scope, '.bw_nav_pills .bw_nav_link.active')] = {
906
+ rules[_sx(scope, '.bw_nav_pills .bw_nav_link.active')] = {
885
907
  'color': palette.primary.textOn,
886
908
  'background-color': palette.primary.base
887
909
  };
@@ -892,49 +914,58 @@ function generateTables(scope, palette, layout) {
892
914
  var rules = {};
893
915
  var sp = layout.spacing;
894
916
 
895
- rules[scopeSelector(scope, '.bw_table')] = {
917
+ rules[_sx(scope, '.bw_table')] = {
896
918
  'color': palette.dark.base,
897
919
  'border-color': palette.light.border
898
920
  };
899
- rules[scopeSelector(scope, '.bw_table > :not(caption) > * > *')] = {
921
+ rules[_sx(scope, '.bw_table > :not(caption) > * > *')] = {
900
922
  'padding': sp.cell,
901
923
  'border-bottom-color': palette.light.border
902
924
  };
903
- rules[scopeSelector(scope, '.bw_table > thead > tr > *')] = {
925
+ rules[_sx(scope, '.bw_table > thead > tr > *')] = {
904
926
  'color': palette.secondary.base,
905
927
  'border-bottom-color': palette.light.border,
906
- 'background-color': palette.light.light
928
+ 'background-color': palette.surfaceAlt
907
929
  };
908
- rules[scopeSelector(scope, '.bw_table_striped > tbody > tr:nth-of-type(odd) > *')] = {
909
- 'background-color': 'rgba(0, 0, 0, 0.05)'
930
+ rules[_sx(scope, '.bw_table_striped > tbody > tr:nth-of-type(odd) > *')] = {
931
+ 'background-color': palette.surfaceAlt
910
932
  };
911
- rules[scopeSelector(scope, '.bw_table_hover > tbody > tr:hover > *')] = {
933
+ rules[_sx(scope, '.bw_table_hover > tbody > tr:hover > *')] = {
912
934
  'background-color': palette.primary.focus
913
935
  };
914
- rules[scopeSelector(scope, '.bw_table_bordered')] = {
936
+ rules[_sx(scope, '.bw_table_selectable > tbody > tr')] = {
937
+ 'cursor': 'pointer'
938
+ };
939
+ rules[_sx(scope, '.bw_table > tbody > tr.bw_table_row_selected > *')] = {
940
+ 'background-color': palette.primary.light
941
+ };
942
+ rules[_sx(scope, '.bw_table_bordered')] = {
915
943
  'border-color': palette.light.border
916
944
  };
917
- rules[scopeSelector(scope, '.bw_table caption')] = {
945
+ rules[_sx(scope, '.bw_table caption')] = {
918
946
  'color': palette.secondary.base
919
947
  };
920
948
 
921
949
  return rules;
922
950
  }
923
951
 
924
- function generateTabs(scope, palette) {
925
- var rules = {};
926
- rules[scopeSelector(scope, '.bw_nav_tabs')] = {
952
+ function generateTabs(scope, palette, layout) {
953
+ var rules = {}, mo = layout.motion;
954
+ rules[_sx(scope, '.bw_nav_tabs')] = {
927
955
  'border-bottom-color': palette.light.border
928
956
  };
929
- rules[scopeSelector(scope, '.bw_nav_link')] = {
930
- 'color': palette.secondary.base
957
+ rules[_sx(scope, '.bw_nav_link')] = {
958
+ 'color': palette.secondary.base,
959
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing + ', border-color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
931
960
  };
932
- rules[scopeSelector(scope, '.bw_nav_tabs .bw_nav_link:hover')] = {
961
+ rules[_sx(scope, '.bw_nav_tabs .bw_nav_link:hover')] = {
933
962
  'color': palette.dark.base,
963
+ 'background-color': palette.surfaceAlt,
934
964
  'border-bottom-color': palette.light.border
935
965
  };
936
- rules[scopeSelector(scope, '.bw_nav_tabs .bw_nav_link.active')] = {
966
+ rules[_sx(scope, '.bw_nav_tabs .bw_nav_link.active')] = {
937
967
  'color': palette.primary.base,
968
+ 'background-color': palette.primary.focus,
938
969
  'border-bottom': '2px solid ' + palette.primary.base
939
970
  };
940
971
  return rules;
@@ -943,23 +974,25 @@ function generateTabs(scope, palette) {
943
974
  function generateListGroups(scope, palette, layout) {
944
975
  var rules = {};
945
976
  var sp = layout.spacing;
977
+ var mo = layout.motion;
946
978
 
947
- rules[scopeSelector(scope, '.bw_list_group_item')] = {
979
+ rules[_sx(scope, '.bw_list_group_item')] = {
948
980
  'padding': sp.cell,
949
981
  'color': palette.dark.base,
950
982
  'background-color': palette.surface || '#fff',
951
- 'border-color': palette.light.border
983
+ 'border-color': palette.light.border,
984
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
952
985
  };
953
- rules[scopeSelector(scope, 'a.bw_list_group_item:hover')] = {
954
- 'background-color': palette.light.light,
986
+ rules[_sx(scope, 'a.bw_list_group_item:hover')] = {
987
+ 'background-color': palette.surfaceAlt,
955
988
  'color': palette.dark.hover
956
989
  };
957
- rules[scopeSelector(scope, '.bw_list_group_item.active')] = {
990
+ rules[_sx(scope, '.bw_list_group_item.active')] = {
958
991
  'color': palette.primary.textOn,
959
992
  'background-color': palette.primary.base,
960
993
  'border-color': palette.primary.base
961
994
  };
962
- rules[scopeSelector(scope, '.bw_list_group_item.disabled')] = {
995
+ rules[_sx(scope, '.bw_list_group_item.disabled')] = {
963
996
  'color': palette.secondary.base,
964
997
  'background-color': palette.surface || '#fff'
965
998
  };
@@ -967,28 +1000,37 @@ function generateListGroups(scope, palette, layout) {
967
1000
  return rules;
968
1001
  }
969
1002
 
970
- function generatePagination(scope, palette) {
971
- var rules = {};
972
- rules[scopeSelector(scope, '.bw_page_link')] = {
1003
+ function generatePagination(scope, palette, layout) {
1004
+ var rules = {}, mo = layout.motion, rd = layout.radius;
1005
+ rules[_sx(scope, '.bw_page_item:first-child .bw_page_link')] = {
1006
+ 'border-top-left-radius': rd.btn,
1007
+ 'border-bottom-left-radius': rd.btn
1008
+ };
1009
+ rules[_sx(scope, '.bw_page_item:last-child .bw_page_link')] = {
1010
+ 'border-top-right-radius': rd.btn,
1011
+ 'border-bottom-right-radius': rd.btn
1012
+ };
1013
+ rules[_sx(scope, '.bw_page_link')] = {
973
1014
  'color': palette.primary.base,
974
1015
  'background-color': palette.surface || '#fff',
975
- 'border-color': palette.light.border
1016
+ 'border-color': palette.light.border,
1017
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
976
1018
  };
977
- rules[scopeSelector(scope, '.bw_page_link:hover')] = {
1019
+ rules[_sx(scope, '.bw_page_link:hover')] = {
978
1020
  'color': palette.primary.hover,
979
- 'background-color': palette.light.light,
1021
+ 'background-color': palette.surfaceAlt,
980
1022
  'border-color': palette.light.border
981
1023
  };
982
- rules[scopeSelector(scope, '.bw_page_link:focus')] = {
1024
+ rules[_sx(scope, '.bw_page_link:focus')] = {
983
1025
  'outline': '2px solid ' + palette.primary.base,
984
1026
  'outline-offset': '-2px'
985
1027
  };
986
- rules[scopeSelector(scope, '.bw_page_item.bw_active .bw_page_link')] = {
1028
+ rules[_sx(scope, '.bw_page_item.bw_active .bw_page_link')] = {
987
1029
  'color': palette.primary.textOn,
988
1030
  'background-color': palette.primary.base,
989
1031
  'border-color': palette.primary.base
990
1032
  };
991
- rules[scopeSelector(scope, '.bw_page_item.bw_disabled .bw_page_link')] = {
1033
+ rules[_sx(scope, '.bw_page_item.bw_disabled .bw_page_link')] = {
992
1034
  'color': palette.secondary.base,
993
1035
  'background-color': palette.surface || '#fff',
994
1036
  'border-color': palette.light.border
@@ -998,12 +1040,12 @@ function generatePagination(scope, palette) {
998
1040
 
999
1041
  function generateProgress(scope, palette) {
1000
1042
  var rules = {};
1001
- rules[scopeSelector(scope, '.bw_progress')] = {
1002
- 'background-color': palette.light.light,
1043
+ rules[_sx(scope, '.bw_progress')] = {
1044
+ 'background-color': palette.surfaceAlt,
1003
1045
  'box-shadow': 'inset 0 1px 2px rgba(0,0,0,.1)'
1004
1046
  };
1005
- rules[scopeSelector(scope, '.bw_progress_bar')] = {
1006
- 'color': '#fff',
1047
+ rules[_sx(scope, '.bw_progress_bar')] = {
1048
+ 'color': palette.primary.textOn,
1007
1049
  'background-color': palette.primary.base,
1008
1050
  'box-shadow': 'inset 0 -1px 0 rgba(0,0,0,.15)'
1009
1051
  };
@@ -1022,26 +1064,31 @@ function generateResetThemed(scope, palette) {
1022
1064
  'color': palette.dark.base,
1023
1065
  'background-color': bg
1024
1066
  };
1025
- rules[scopeSelector(scope, 'body')] = baseReset;
1026
- // Also apply to the scope element itself so themes work on any container, not just body
1027
- if (scope) {
1028
- rules['.' + scope] = baseReset;
1029
- }
1067
+ rules[_sx(scope, 'body')] = baseReset;
1030
1068
  return rules;
1031
1069
  }
1032
1070
 
1033
- function generateBreadcrumbThemed(scope, palette) {
1034
- var rules = {};
1035
- rules[scopeSelector(scope, '.bw_breadcrumb_item + .bw_breadcrumb_item::before')] = {
1036
- 'color': palette.secondary.base
1071
+ function generateBreadcrumbThemed(scope, palette, layout) {
1072
+ var rules = {}, mo = layout.motion;
1073
+ rules[_sx(scope, '.bw_breadcrumb')] = {
1074
+ 'background-color': palette.surfaceAlt,
1075
+ 'padding': '0.625rem 1rem',
1076
+ 'border-radius': layout.radius.btn
1037
1077
  };
1038
- rules[scopeSelector(scope, '.bw_breadcrumb_item.active')] = {
1078
+ rules[_sx(scope, '.bw_breadcrumb_item + .bw_breadcrumb_item::before')] = {
1039
1079
  'color': palette.secondary.base
1040
1080
  };
1041
- rules[scopeSelector(scope, '.bw_breadcrumb_item a:hover')] = {
1081
+ rules[_sx(scope, '.bw_breadcrumb_item a')] = {
1082
+ 'color': palette.primary.base,
1083
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing
1084
+ };
1085
+ rules[_sx(scope, '.bw_breadcrumb_item a:hover')] = {
1042
1086
  'color': palette.primary.hover,
1043
1087
  'text-decoration': 'underline'
1044
1088
  };
1089
+ rules[_sx(scope, '.bw_breadcrumb_item.active')] = {
1090
+ 'color': palette.dark.base
1091
+ };
1045
1092
  return rules;
1046
1093
  }
1047
1094
 
@@ -1049,11 +1096,11 @@ function generateBreadcrumbThemed(scope, palette) {
1049
1096
 
1050
1097
  function generateCloseButtonThemed(scope, palette) {
1051
1098
  var rules = {};
1052
- rules[scopeSelector(scope, '.bw_close')] = {
1099
+ rules[_sx(scope, '.bw_close')] = {
1053
1100
  'color': palette.dark.base,
1054
1101
  'opacity': '0.5'
1055
1102
  };
1056
- rules[scopeSelector(scope, '.bw_close:focus')] = {
1103
+ rules[_sx(scope, '.bw_close:focus')] = {
1057
1104
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
1058
1105
  };
1059
1106
  return rules;
@@ -1061,82 +1108,94 @@ function generateCloseButtonThemed(scope, palette) {
1061
1108
 
1062
1109
  function generateSectionsThemed(scope, palette) {
1063
1110
  var rules = {};
1064
- rules[scopeSelector(scope, '.bw_section_subtitle')] = {
1111
+ rules[_sx(scope, '.bw_section_subtitle')] = {
1065
1112
  'color': palette.secondary.base
1066
1113
  };
1067
- rules[scopeSelector(scope, '.bw_feature_description')] = {
1114
+ rules[_sx(scope, '.bw_feature_description')] = {
1068
1115
  'color': palette.secondary.base
1069
1116
  };
1070
- rules[scopeSelector(scope, '.bw_cta_description')] = {
1117
+ rules[_sx(scope, '.bw_cta_description')] = {
1071
1118
  'color': palette.secondary.base
1072
1119
  };
1073
1120
  return rules;
1074
1121
  }
1075
1122
 
1076
- function generateAccordionThemed(scope, palette) {
1123
+ function generateAccordionThemed(scope, palette, layout) {
1077
1124
  var rules = {};
1078
- rules[scopeSelector(scope, '.bw_accordion_item')] = {
1125
+ var rd = layout ? layout.radius : { card: '8px' };
1126
+ rules[_sx(scope, '.bw_accordion_item')] = {
1079
1127
  'background-color': palette.surface || '#fff',
1080
1128
  'border-color': palette.light.border
1081
1129
  };
1082
- rules[scopeSelector(scope, '.bw_accordion_button')] = {
1130
+ rules[_sx(scope, '.bw_accordion_item:first-child')] = {
1131
+ 'border-top-left-radius': rd.card,
1132
+ 'border-top-right-radius': rd.card
1133
+ };
1134
+ rules[_sx(scope, '.bw_accordion_item:last-child')] = {
1135
+ 'border-bottom-left-radius': rd.card,
1136
+ 'border-bottom-right-radius': rd.card
1137
+ };
1138
+ rules[_sx(scope, '.bw_accordion_button')] = {
1083
1139
  'color': palette.dark.base
1084
1140
  };
1085
- rules[scopeSelector(scope, '.bw_accordion_button:not(.bw_collapsed)')] = {
1141
+ rules[_sx(scope, '.bw_accordion_button:not(.bw_collapsed)')] = {
1086
1142
  'color': palette.primary.darkText,
1087
- 'background-color': palette.primary.light
1143
+ 'background-color': palette.primary.light,
1144
+ 'border-left': '3px solid ' + palette.primary.base
1088
1145
  };
1089
- rules[scopeSelector(scope, '.bw_accordion_button:hover')] = {
1090
- 'background-color': palette.light.light
1146
+ rules[_sx(scope, '.bw_accordion_button:hover')] = {
1147
+ 'background-color': palette.surfaceAlt
1091
1148
  };
1092
- rules[scopeSelector(scope, '.bw_accordion_button:not(.bw_collapsed):hover')] = {
1093
- 'background-color': palette.primary.hover
1149
+ rules[_sx(scope, '.bw_accordion_button:not(.bw_collapsed):hover')] = {
1150
+ 'background-color': palette.primary.base,
1151
+ 'color': palette.primary.textOn
1094
1152
  };
1095
- rules[scopeSelector(scope, '.bw_accordion_button:focus-visible')] = {
1153
+ rules[_sx(scope, '.bw_accordion_button:focus-visible')] = {
1096
1154
  'box-shadow': '0 0 0 0.2rem ' + palette.primary.focus
1097
1155
  };
1098
- rules[scopeSelector(scope, '.bw_accordion_body')] = {
1099
- 'border-top': '1px solid ' + palette.light.border
1156
+ rules[_sx(scope, '.bw_accordion_body')] = {
1157
+ 'border-top': '1px solid ' + palette.light.border,
1158
+ 'background-color': palette.surfaceAlt
1100
1159
  };
1101
1160
  return rules;
1102
1161
  }
1103
1162
 
1104
1163
  function generateCarouselThemed(scope, palette) {
1105
1164
  var rules = {};
1106
- rules[scopeSelector(scope, '.bw_carousel')] = {
1107
- 'background-color': palette.light.light
1165
+ rules[_sx(scope, '.bw_carousel')] = {
1166
+ 'background-color': palette.surfaceAlt
1108
1167
  };
1109
- rules[scopeSelector(scope, '.bw_carousel_indicator.active')] = {
1168
+ rules[_sx(scope, '.bw_carousel_indicator.active')] = {
1110
1169
  'background-color': palette.primary.base
1111
1170
  };
1112
- rules[scopeSelector(scope, '.bw_carousel_control')] = {
1113
- 'background-color': 'rgba(0,0,0,0.4)',
1114
- 'color': '#fff'
1171
+ rules[_sx(scope, '.bw_carousel_control')] = {
1172
+ 'background-color': palette.dark.base,
1173
+ 'color': palette.dark.textOn
1115
1174
  };
1116
- rules[scopeSelector(scope, '.bw_carousel_control:hover')] = {
1117
- 'background-color': 'rgba(0,0,0,0.6)'
1175
+ rules[_sx(scope, '.bw_carousel_control:hover')] = {
1176
+ 'background-color': palette.dark.hover
1118
1177
  };
1119
- rules[scopeSelector(scope, '.bw_carousel_caption')] = {
1120
- 'background': 'linear-gradient(transparent, rgba(0,0,0,0.6))',
1121
- 'color': '#fff'
1178
+ rules[_sx(scope, '.bw_carousel_caption')] = {
1179
+ 'background': 'linear-gradient(transparent, ' + palette.dark.base + ')',
1180
+ 'color': palette.dark.textOn
1122
1181
  };
1123
1182
  return rules;
1124
1183
  }
1125
1184
 
1126
1185
  function generateModalThemed(scope, palette, layout) {
1127
1186
  var rules = {};
1128
- rules[scopeSelector(scope, '.bw_modal_content')] = {
1187
+ rules[_sx(scope, '.bw_modal_content')] = {
1129
1188
  'background-color': palette.surface || '#fff',
1130
1189
  'border-color': palette.light.border,
1131
1190
  'box-shadow': layout.elevation.lg
1132
1191
  };
1133
- rules[scopeSelector(scope, '.bw_modal_header')] = {
1192
+ rules[_sx(scope, '.bw_modal_header')] = {
1134
1193
  'border-bottom-color': palette.light.border
1135
1194
  };
1136
- rules[scopeSelector(scope, '.bw_modal_footer')] = {
1195
+ rules[_sx(scope, '.bw_modal_footer')] = {
1137
1196
  'border-top-color': palette.light.border
1138
1197
  };
1139
- rules[scopeSelector(scope, '.bw_modal_title')] = {
1198
+ rules[_sx(scope, '.bw_modal_title')] = {
1140
1199
  'color': palette.dark.base
1141
1200
  };
1142
1201
  return rules;
@@ -1144,13 +1203,13 @@ function generateModalThemed(scope, palette, layout) {
1144
1203
 
1145
1204
  function generateToastThemed(scope, palette, layout) {
1146
1205
  var rules = {};
1147
- rules[scopeSelector(scope, '.bw_toast')] = {
1206
+ rules[_sx(scope, '.bw_toast')] = {
1148
1207
  'background-color': palette.surface || '#fff',
1149
- 'border-color': 'rgba(0,0,0,0.1)',
1208
+ 'border-color': palette.light.border,
1150
1209
  'box-shadow': layout.elevation.lg
1151
1210
  };
1152
- rules[scopeSelector(scope, '.bw_toast_header')] = {
1153
- 'border-bottom-color': 'rgba(0,0,0,0.05)'
1211
+ rules[_sx(scope, '.bw_toast_header')] = {
1212
+ 'border-bottom-color': palette.light.border
1154
1213
  };
1155
1214
  // Variant toast borders handled by palette class
1156
1215
  return rules;
@@ -1158,22 +1217,23 @@ function generateToastThemed(scope, palette, layout) {
1158
1217
 
1159
1218
  function generateDropdownThemed(scope, palette, layout) {
1160
1219
  var rules = {};
1161
- rules[scopeSelector(scope, '.bw_dropdown_menu')] = {
1220
+ rules[_sx(scope, '.bw_dropdown_menu')] = {
1162
1221
  'background-color': palette.surface || '#fff',
1163
1222
  'border-color': palette.light.border,
1164
1223
  'box-shadow': layout.elevation.md
1165
1224
  };
1166
- rules[scopeSelector(scope, '.bw_dropdown_item')] = {
1167
- 'color': palette.dark.base
1225
+ rules[_sx(scope, '.bw_dropdown_item')] = {
1226
+ 'color': palette.dark.base,
1227
+ 'transition': 'background-color ' + layout.motion.fast + ' ' + layout.motion.easing
1168
1228
  };
1169
- rules[scopeSelector(scope, '.bw_dropdown_item:hover')] = {
1229
+ rules[_sx(scope, '.bw_dropdown_item:hover')] = {
1170
1230
  'color': palette.dark.hover,
1171
- 'background-color': palette.light.light
1231
+ 'background-color': palette.surfaceAlt
1172
1232
  };
1173
- rules[scopeSelector(scope, '.bw_dropdown_item.disabled')] = {
1233
+ rules[_sx(scope, '.bw_dropdown_item.disabled')] = {
1174
1234
  'color': palette.secondary.base
1175
1235
  };
1176
- rules[scopeSelector(scope, '.bw_dropdown_divider')] = {
1236
+ rules[_sx(scope, '.bw_dropdown_divider')] = {
1177
1237
  'border-top-color': palette.light.border
1178
1238
  };
1179
1239
  return rules;
@@ -1181,15 +1241,15 @@ function generateDropdownThemed(scope, palette, layout) {
1181
1241
 
1182
1242
  function generateSwitchThemed(scope, palette) {
1183
1243
  var rules = {};
1184
- rules[scopeSelector(scope, '.bw_form_switch .bw_switch_input')] = {
1244
+ rules[_sx(scope, '.bw_form_switch .bw_switch_input')] = {
1185
1245
  'background-color': palette.secondary.base,
1186
1246
  'border-color': palette.secondary.base
1187
1247
  };
1188
- rules[scopeSelector(scope, '.bw_form_switch .bw_switch_input:checked')] = {
1248
+ rules[_sx(scope, '.bw_form_switch .bw_switch_input:checked')] = {
1189
1249
  'background-color': palette.primary.base,
1190
1250
  'border-color': palette.primary.base
1191
1251
  };
1192
- rules[scopeSelector(scope, '.bw_form_switch .bw_switch_input:focus')] = {
1252
+ rules[_sx(scope, '.bw_form_switch .bw_switch_input:focus')] = {
1193
1253
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
1194
1254
  };
1195
1255
  return rules;
@@ -1197,88 +1257,102 @@ function generateSwitchThemed(scope, palette) {
1197
1257
 
1198
1258
  function generateSkeletonThemed(scope, palette) {
1199
1259
  var rules = {};
1200
- rules[scopeSelector(scope, '.bw_skeleton')] = {
1201
- 'background': 'linear-gradient(90deg, ' + palette.light.border + ' 25%, ' + palette.light.light + ' 37%, ' + palette.light.border + ' 63%)'
1260
+ rules[_sx(scope, '.bw_skeleton')] = {
1261
+ 'background': 'linear-gradient(90deg, ' + palette.light.border + ' 25%, ' + palette.surfaceAlt + ' 37%, ' + palette.light.border + ' 63%)'
1202
1262
  };
1203
1263
  return rules;
1204
1264
  }
1205
1265
 
1206
1266
  // generateAvatarThemed: removed — palette class on root handles variants
1207
1267
 
1208
- function generateStatCardThemed(scope, palette) {
1209
- var rules = {};
1268
+ function generateStatCardThemed(scope, palette, layout) {
1269
+ var rules = {}, mo = layout.motion, el = layout.elevation, rd = layout.radius;
1270
+ rules[_sx(scope, '.bw_stat_card')] = {
1271
+ 'background-color': palette.surface || '#fff',
1272
+ 'color': palette.dark.base,
1273
+ 'border': '1px solid ' + palette.light.border,
1274
+ 'border-radius': rd.card,
1275
+ 'box-shadow': el.sm,
1276
+ 'transition': 'box-shadow ' + mo.fast + ' ' + mo.easing + ', transform ' + mo.fast + ' ' + mo.easing
1277
+ };
1278
+ rules[_sx(scope, '.bw_stat_card:hover')] = { 'box-shadow': el.md };
1210
1279
  // Variant border colors handled by palette class
1211
- rules[scopeSelector(scope, '.bw_stat_change_up')] = { 'color': palette.success.base };
1212
- rules[scopeSelector(scope, '.bw_stat_change_down')] = { 'color': palette.danger.base };
1280
+ rules[_sx(scope, '.bw_stat_change_up')] = { 'color': palette.success.base };
1281
+ rules[_sx(scope, '.bw_stat_change_down')] = { 'color': palette.danger.base };
1213
1282
  return rules;
1214
1283
  }
1215
1284
 
1216
1285
  function generateTimelineThemed(scope, palette) {
1217
1286
  var rules = {};
1218
- rules[scopeSelector(scope, '.bw_timeline::before')] = { 'background-color': palette.light.border };
1287
+ rules[_sx(scope, '.bw_timeline::before')] = { 'background-color': palette.light.border };
1219
1288
  // Variant marker colors handled by palette class
1220
- rules[scopeSelector(scope, '.bw_timeline_date')] = { 'color': palette.secondary.base };
1289
+ rules[_sx(scope, '.bw_timeline_date')] = { 'color': palette.secondary.base };
1221
1290
  return rules;
1222
1291
  }
1223
1292
 
1224
1293
  function generateStepperThemed(scope, palette) {
1225
1294
  var rules = {};
1226
- rules[scopeSelector(scope, '.bw_step_indicator')] = {
1227
- 'background-color': palette.light.light,
1295
+ rules[_sx(scope, '.bw_step_indicator')] = {
1296
+ 'background-color': palette.surfaceAlt,
1228
1297
  'border': '2px solid ' + palette.light.border,
1229
1298
  'color': palette.secondary.base
1230
1299
  };
1231
- rules[scopeSelector(scope, '.bw_step + .bw_step::before')] = { 'background-color': palette.light.border };
1232
- rules[scopeSelector(scope, '.bw_step_active .bw_step_indicator')] = {
1300
+ rules[_sx(scope, '.bw_step + .bw_step::before')] = { 'background-color': palette.light.border };
1301
+ rules[_sx(scope, '.bw_step_active .bw_step_indicator')] = {
1233
1302
  'background-color': palette.primary.base,
1234
1303
  'color': palette.primary.textOn
1235
1304
  };
1236
- rules[scopeSelector(scope, '.bw_step_active .bw_step_label')] = {
1305
+ rules[_sx(scope, '.bw_step_active .bw_step_label')] = {
1237
1306
  'color': palette.dark.base,
1238
1307
  'font-weight': '600'
1239
1308
  };
1240
- rules[scopeSelector(scope, '.bw_step_completed .bw_step_indicator')] = {
1309
+ rules[_sx(scope, '.bw_step_completed .bw_step_indicator')] = {
1241
1310
  'background-color': palette.primary.base,
1242
1311
  'color': palette.primary.textOn
1243
1312
  };
1244
- rules[scopeSelector(scope, '.bw_step_completed .bw_step_label')] = { 'color': palette.primary.base };
1245
- rules[scopeSelector(scope, '.bw_step_completed + .bw_step::before')] = { 'background-color': palette.primary.base };
1313
+ rules[_sx(scope, '.bw_step_completed .bw_step_label')] = { 'color': palette.primary.base };
1314
+ rules[_sx(scope, '.bw_step_completed + .bw_step::before')] = { 'background-color': palette.primary.base };
1246
1315
  return rules;
1247
1316
  }
1248
1317
 
1249
1318
  function generateChipInputThemed(scope, palette) {
1250
1319
  var rules = {};
1251
- rules[scopeSelector(scope, '.bw_chip_input')] = { 'border-color': palette.light.border };
1252
- rules[scopeSelector(scope, '.bw_chip_input:focus-within')] = {
1320
+ rules[_sx(scope, '.bw_chip_input')] = {
1321
+ 'border-color': palette.light.border,
1322
+ 'background-color': palette.surface || '#fff',
1323
+ 'color': palette.dark.base
1324
+ };
1325
+ rules[_sx(scope, '.bw_chip_input:focus-within')] = {
1253
1326
  'border-color': palette.primary.base,
1254
1327
  'box-shadow': '0 0 0 0.2rem ' + palette.primary.focus
1255
1328
  };
1256
- rules[scopeSelector(scope, '.bw_chip')] = {
1257
- 'background-color': palette.light.light,
1329
+ rules[_sx(scope, '.bw_chip')] = {
1330
+ 'background-color': palette.surfaceAlt,
1258
1331
  'color': palette.dark.base
1259
1332
  };
1260
- rules[scopeSelector(scope, '.bw_chip_remove:hover')] = {
1333
+ rules[_sx(scope, '.bw_chip_remove:hover')] = {
1261
1334
  'color': palette.danger.base,
1262
1335
  'background-color': palette.danger.light
1263
1336
  };
1264
1337
  return rules;
1265
1338
  }
1266
1339
 
1267
- function generateFileUploadThemed(scope, palette) {
1268
- var rules = {};
1269
- rules[scopeSelector(scope, '.bw_file_upload')] = {
1340
+ function generateFileUploadThemed(scope, palette, layout) {
1341
+ var rules = {}, mo = layout.motion;
1342
+ rules[_sx(scope, '.bw_file_upload')] = {
1270
1343
  'border-color': palette.light.border,
1271
- 'background-color': palette.light.light
1344
+ 'background-color': palette.surfaceAlt,
1345
+ 'transition': 'border-color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
1272
1346
  };
1273
- rules[scopeSelector(scope, '.bw_file_upload:hover')] = {
1347
+ rules[_sx(scope, '.bw_file_upload:hover')] = {
1274
1348
  'border-color': palette.primary.base,
1275
1349
  'background-color': palette.primary.light
1276
1350
  };
1277
- rules[scopeSelector(scope, '.bw_file_upload:focus')] = {
1351
+ rules[_sx(scope, '.bw_file_upload:focus')] = {
1278
1352
  'outline': '2px solid ' + palette.primary.base,
1279
1353
  'outline-offset': '2px'
1280
1354
  };
1281
- rules[scopeSelector(scope, '.bw_file_upload.bw_file_upload_active')] = {
1355
+ rules[_sx(scope, '.bw_file_upload.bw_file_upload_active')] = {
1282
1356
  'border-color': palette.primary.base,
1283
1357
  'background-color': palette.primary.light,
1284
1358
  'border-style': 'solid'
@@ -1288,35 +1362,73 @@ function generateFileUploadThemed(scope, palette) {
1288
1362
 
1289
1363
  function generateRangeThemed(scope, palette) {
1290
1364
  var rules = {};
1291
- rules[scopeSelector(scope, '.bw_range')] = { 'background-color': palette.light.border };
1292
- rules[scopeSelector(scope, '.bw_range::-webkit-slider-thumb')] = {
1365
+ rules[_sx(scope, '.bw_range')] = { 'background-color': palette.light.border };
1366
+ rules[_sx(scope, '.bw_range::-webkit-slider-thumb')] = {
1293
1367
  'background-color': palette.primary.base,
1294
- 'border-color': '#fff',
1368
+ 'border-color': palette.surface || '#fff',
1295
1369
  'box-shadow': '0 1px 3px rgba(0,0,0,0.2)',
1296
1370
  'transition': 'background-color 0.15s ease-out, transform 0.15s ease-out'
1297
1371
  };
1298
- rules[scopeSelector(scope, '.bw_range::-moz-range-thumb')] = {
1372
+ rules[_sx(scope, '.bw_range::-moz-range-thumb')] = {
1299
1373
  'background-color': palette.primary.base,
1300
- 'border-color': '#fff',
1374
+ 'border-color': palette.surface || '#fff',
1301
1375
  'box-shadow': '0 1px 3px rgba(0,0,0,0.2)'
1302
1376
  };
1303
1377
  return rules;
1304
1378
  }
1305
1379
 
1306
- function generateSearchThemed(scope, palette) {
1307
- var rules = {};
1308
- rules[scopeSelector(scope, '.bw_search_clear:hover')] = { 'color': palette.dark.base };
1380
+ function generateTooltipThemed(scope, palette, layout) {
1381
+ var rules = {}, sp = layout.spacing, rd = layout.radius, el = layout.elevation, mo = layout.motion;
1382
+ rules[_sx(scope, '.bw_tooltip')] = {
1383
+ 'background-color': palette.dark.base, 'color': palette.dark.textOn,
1384
+ 'padding': sp.input, 'border-radius': rd.badge, 'box-shadow': el.md,
1385
+ 'transition': 'opacity ' + mo.fast + ' ' + mo.easing + ', transform ' + mo.fast + ' ' + mo.easing
1386
+ };
1387
+ return rules;
1388
+ }
1389
+
1390
+ function generatePopoverThemed(scope, palette, layout) {
1391
+ var rules = {}, sp = layout.spacing, rd = layout.radius, el = layout.elevation, mo = layout.motion;
1392
+ rules[_sx(scope, '.bw_popover')] = {
1393
+ 'background-color': palette.surface || '#fff', 'color': palette.dark.base,
1394
+ 'border': '1px solid ' + palette.light.border, 'border-radius': rd.card, 'box-shadow': el.lg,
1395
+ 'transition': 'opacity ' + mo.fast + ' ' + mo.easing + ', transform ' + mo.fast + ' ' + mo.easing
1396
+ };
1397
+ rules[_sx(scope, '.bw_popover_header')] = {
1398
+ 'background-color': palette.surfaceAlt, 'border-bottom': '1px solid ' + palette.light.border,
1399
+ 'padding': sp.input
1400
+ };
1401
+ rules[_sx(scope, '.bw_popover_body')] = { 'padding': sp.card };
1402
+ return rules;
1403
+ }
1404
+
1405
+ function generateSearchThemed(scope, palette, layout) {
1406
+ var rules = {}, mo = layout.motion;
1407
+ rules[_sx(scope, '.bw_search_input')] = {
1408
+ 'background-color': palette.surface || '#fff',
1409
+ 'color': palette.dark.base
1410
+ };
1411
+ rules[_sx(scope, '.bw_search_clear')] = {
1412
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
1413
+ };
1414
+ rules[_sx(scope, '.bw_search_clear:hover')] = { 'color': palette.dark.base };
1309
1415
  return rules;
1310
1416
  }
1311
1417
 
1312
- function generateCodeDemoThemed(scope, palette) {
1418
+ function generateCodeDemoThemed(scope, palette, layout) {
1313
1419
  var rules = {};
1314
- rules[scopeSelector(scope, '.bw_code_copy_btn_copied')] = {
1420
+ var rd = layout ? layout.radius : { card: '0.375rem' };
1421
+ rules[_sx(scope, '.bw_code_demo')] = {
1422
+ 'background-color': palette.surface || '#fff',
1423
+ 'color': palette.dark.base,
1424
+ 'border-radius': rd.card
1425
+ };
1426
+ rules[_sx(scope, '.bw_code_copy_btn_copied')] = {
1315
1427
  'background': palette.success.base,
1316
1428
  'color': palette.success.textOn,
1317
1429
  'border-color': palette.success.base
1318
1430
  };
1319
- rules[scopeSelector(scope, '.bw_copy_btn:hover')] = {
1431
+ rules[_sx(scope, '.bw_copy_btn:hover')] = {
1320
1432
  'background': 'rgba(255,255,255,0.2)',
1321
1433
  'color': '#fff'
1322
1434
  };
@@ -1326,7 +1438,7 @@ function generateCodeDemoThemed(scope, palette) {
1326
1438
  function generateNavPillsThemed(scope, palette, layout) {
1327
1439
  var rules = {};
1328
1440
  var rd = layout.radius;
1329
- rules[scopeSelector(scope, '.bw_nav_pills .bw_nav_link')] = { 'border-radius': rd.btn };
1441
+ rules[_sx(scope, '.bw_nav_pills .bw_nav_link')] = { 'border-radius': rd.btn };
1330
1442
  return rules;
1331
1443
  }
1332
1444
 
@@ -1352,21 +1464,21 @@ function generatePaletteClasses(scope, palette) {
1352
1464
  var s = palette[k];
1353
1465
 
1354
1466
  // --- Root palette class: sets default bg/color/border ---
1355
- rules[scopeSelector(scope, '.bw_' + k)] = {
1467
+ rules[_sx(scope, '.bw_' + k)] = {
1356
1468
  'background-color': s.base,
1357
1469
  'color': s.textOn,
1358
1470
  'border-color': s.base
1359
1471
  };
1360
1472
 
1361
1473
  // --- Pseudo-states (shared across all components) ---
1362
- rules[scopeSelector(scope, '.bw_' + k + ':hover')] = {
1474
+ rules[_sx(scope, '.bw_' + k + ':hover')] = {
1363
1475
  'background-color': s.hover,
1364
1476
  'border-color': s.active
1365
1477
  };
1366
- rules[scopeSelector(scope, '.bw_' + k + ':active')] = {
1478
+ rules[_sx(scope, '.bw_' + k + ':active')] = {
1367
1479
  'background-color': s.active
1368
1480
  };
1369
- rules[scopeSelector(scope, '.bw_' + k + ':focus-visible')] = {
1481
+ rules[_sx(scope, '.bw_' + k + ':focus-visible')] = {
1370
1482
  'box-shadow': '0 0 0 3px ' + s.focus,
1371
1483
  'outline': 'none'
1372
1484
  };
@@ -1374,70 +1486,99 @@ function generatePaletteClasses(scope, palette) {
1374
1486
  // --- Component-specific overrides ---
1375
1487
 
1376
1488
  // Alerts: light bg, dark text, subtle border
1377
- rules[scopeSelector(scope, '.bw_alert.bw_' + k)] = {
1489
+ rules[_sx(scope, '.bw_alert.bw_' + k)] = {
1378
1490
  'background-color': s.light,
1379
1491
  'color': s.darkText,
1380
1492
  'border-color': s.border
1381
1493
  };
1382
1494
 
1383
1495
  // Toast: inherit bg, left border accent
1384
- rules[scopeSelector(scope, '.bw_toast.bw_' + k)] = {
1496
+ rules[_sx(scope, '.bw_toast.bw_' + k)] = {
1385
1497
  'background-color': 'inherit',
1386
1498
  'color': 'inherit',
1387
1499
  'border-left': '4px solid ' + s.base
1388
1500
  };
1389
1501
 
1390
1502
  // Stat card: inherit bg, left border accent
1391
- rules[scopeSelector(scope, '.bw_stat_card.bw_' + k)] = {
1503
+ rules[_sx(scope, '.bw_stat_card.bw_' + k)] = {
1392
1504
  'background-color': 'inherit',
1393
1505
  'color': 'inherit',
1394
1506
  'border-left-color': s.base
1395
1507
  };
1396
1508
 
1397
1509
  // Card accent: left border accent, inherit bg
1398
- rules[scopeSelector(scope, '.bw_card.bw_' + k)] = {
1510
+ rules[_sx(scope, '.bw_card.bw_' + k)] = {
1399
1511
  'background-color': 'inherit',
1400
1512
  'color': 'inherit',
1401
1513
  'border-left': '4px solid ' + s.base
1402
1514
  };
1403
1515
 
1404
1516
  // Timeline marker: colored dot
1405
- rules[scopeSelector(scope, '.bw_timeline_marker.bw_' + k)] = {
1517
+ rules[_sx(scope, '.bw_timeline_marker.bw_' + k)] = {
1406
1518
  'box-shadow': '0 0 0 2px ' + s.base
1407
1519
  };
1408
1520
 
1409
- // Spinner: text color only, transparent bg
1410
- rules[scopeSelector(scope, '.bw_spinner_border.bw_' + k + ',\n' + scopeSelector(scope, '.bw_spinner_grow.bw_' + k))] = {
1521
+ // Spinner: set color, re-apply border pattern so the root palette class
1522
+ // border-color doesn't fill in the transparent gap that makes it spin.
1523
+ // Also neutralize hover/active which would override border-right-color.
1524
+ rules[_sx(scope, '.bw_spinner_border.bw_' + k)] = {
1411
1525
  'background-color': 'transparent',
1412
1526
  'color': s.base,
1413
- 'border-color': 'currentColor'
1527
+ 'border-color': s.base,
1528
+ 'border-right-color': 'transparent'
1529
+ };
1530
+ rules[_sx(scope, '.bw_spinner_border.bw_' + k + ':hover')] = {
1531
+ 'background-color': 'transparent',
1532
+ 'border-color': s.base,
1533
+ 'border-right-color': 'transparent'
1534
+ };
1535
+ rules[_sx(scope, '.bw_spinner_grow.bw_' + k)] = {
1536
+ 'background-color': s.base,
1537
+ 'color': s.base
1414
1538
  };
1415
1539
 
1416
1540
  // Outline button: transparent bg, colored border+text, solid on hover
1417
- rules[scopeSelector(scope, '.bw_btn_outline.bw_' + k)] = {
1541
+ rules[_sx(scope, '.bw_btn_outline.bw_' + k)] = {
1418
1542
  'background-color': 'transparent',
1419
1543
  'color': s.base,
1420
1544
  'border-color': s.base
1421
1545
  };
1422
- rules[scopeSelector(scope, '.bw_btn_outline.bw_' + k + ':hover')] = {
1546
+ rules[_sx(scope, '.bw_btn_outline.bw_' + k + ':hover')] = {
1423
1547
  'background-color': s.base,
1424
1548
  'color': s.textOn
1425
1549
  };
1426
1550
 
1427
1551
  // Hero: gradient background
1428
- rules[scopeSelector(scope, '.bw_hero.bw_' + k)] = {
1552
+ rules[_sx(scope, '.bw_hero.bw_' + k)] = {
1429
1553
  'background': 'linear-gradient(135deg, ' + s.base + ' 0%, ' + s.hover + ' 100%)',
1430
1554
  'color': s.textOn
1431
1555
  };
1432
1556
 
1433
- // Progress bar: white text on colored bg (default is fine, just ensure text)
1434
- rules[scopeSelector(scope, '.bw_progress_bar.bw_' + k)] = {
1435
- 'color': '#fff'
1557
+ // Progress bar: contrasting text on colored bg
1558
+ rules[_sx(scope, '.bw_progress_bar.bw_' + k)] = {
1559
+ 'color': s.textOn
1560
+ };
1561
+
1562
+ // Background utility: .bw_bg_primary, .bw_bg_secondary, etc.
1563
+ rules[_sx(scope, '.bw_bg_' + k)] = {
1564
+ 'background-color': s.base,
1565
+ 'color': s.textOn
1566
+ };
1567
+
1568
+ // Text color utility: .bw_text_primary, .bw_text_secondary, etc.
1569
+ rules[_sx(scope, '.bw_text_' + k)] = {
1570
+ 'color': s.base
1436
1571
  };
1437
1572
  });
1438
1573
 
1439
- // Text muted
1440
- rules[scopeSelector(scope, '.bw_text_muted')] = { 'color': palette.secondary.base };
1574
+ // Text muted — always a neutral gray, never a brand color
1575
+ rules[_sx(scope, '.bw_text_muted')] = { 'color': '#6c757d' };
1576
+
1577
+ // Common bg/text utilities that aren't per-variant
1578
+ rules[_sx(scope, '.bw_bg_dark')] = { 'background-color': '#212529', 'color': '#f8f9fa' };
1579
+ rules[_sx(scope, '.bw_bg_light')] = { 'background-color': '#f8f9fa', 'color': '#212529' };
1580
+ rules[_sx(scope, '.bw_text_light')] = { 'color': '#f8f9fa' };
1581
+ rules[_sx(scope, '.bw_text_dark')] = { 'color': '#212529' };
1441
1582
 
1442
1583
  return rules;
1443
1584
  }
@@ -1459,30 +1600,32 @@ function generateThemedCSS(scopeName, palette, layout) {
1459
1600
  generateAlerts(scopeName, palette, layout),
1460
1601
  generateCards(scopeName, palette, layout),
1461
1602
  generateForms(scopeName, palette, layout),
1462
- generateNavigation(scopeName, palette),
1603
+ generateNavigation(scopeName, palette, layout),
1463
1604
  generateTables(scopeName, palette, layout),
1464
- generateTabs(scopeName, palette),
1605
+ generateTabs(scopeName, palette, layout),
1465
1606
  generateListGroups(scopeName, palette, layout),
1466
- generatePagination(scopeName, palette),
1607
+ generatePagination(scopeName, palette, layout),
1467
1608
  generateProgress(scopeName, palette),
1468
- generateBreadcrumbThemed(scopeName, palette),
1609
+ generateBreadcrumbThemed(scopeName, palette, layout),
1469
1610
  generateCloseButtonThemed(scopeName, palette),
1470
1611
  generateSectionsThemed(scopeName, palette),
1471
- generateAccordionThemed(scopeName, palette),
1612
+ generateAccordionThemed(scopeName, palette, layout),
1472
1613
  generateCarouselThemed(scopeName, palette),
1473
1614
  generateModalThemed(scopeName, palette, layout),
1474
1615
  generateToastThemed(scopeName, palette, layout),
1475
1616
  generateDropdownThemed(scopeName, palette, layout),
1476
1617
  generateSwitchThemed(scopeName, palette),
1477
1618
  generateSkeletonThemed(scopeName, palette),
1478
- generateStatCardThemed(scopeName, palette),
1619
+ generateStatCardThemed(scopeName, palette, layout),
1479
1620
  generateTimelineThemed(scopeName, palette),
1480
1621
  generateStepperThemed(scopeName, palette),
1481
1622
  generateChipInputThemed(scopeName, palette),
1482
- generateFileUploadThemed(scopeName, palette),
1623
+ generateFileUploadThemed(scopeName, palette, layout),
1483
1624
  generateRangeThemed(scopeName, palette),
1484
- generateSearchThemed(scopeName, palette),
1485
- generateCodeDemoThemed(scopeName, palette),
1625
+ generateSearchThemed(scopeName, palette, layout),
1626
+ generateTooltipThemed(scopeName, palette, layout),
1627
+ generatePopoverThemed(scopeName, palette, layout),
1628
+ generateCodeDemoThemed(scopeName, palette, layout),
1486
1629
  generateNavPillsThemed(scopeName, palette, layout),
1487
1630
  generatePaletteClasses(scopeName, palette)
1488
1631
  );
@@ -1707,6 +1850,8 @@ var structuralRules = {
1707
1850
  },
1708
1851
  '.bw_table caption': { 'font-size': '0.875rem', 'caption-side': 'bottom' },
1709
1852
  '.bw_table_bordered > :not(caption) > * > *': { 'border-width': '1px', 'border-style': 'solid' },
1853
+ '.bw_table_selectable > tbody > tr': { 'cursor': 'pointer' },
1854
+ '.bw_table > tbody > tr.bw_table_row_selected > *': { 'background-color': 'rgba(0, 102, 102, 0.1)' },
1710
1855
  '.bw_table_responsive': { 'overflow-x': 'auto', '-webkit-overflow-scrolling': 'touch' }
1711
1856
  },
1712
1857
 
@@ -1760,6 +1905,7 @@ var structuralRules = {
1760
1905
  '.bw_nav_tabs .bw_nav_item': { 'margin-bottom': '-2px' },
1761
1906
  '.bw_nav_link': {
1762
1907
  'display': 'block', 'font-size': '0.875rem', 'font-weight': '500',
1908
+ 'padding': '0.625rem 1rem',
1763
1909
  'text-decoration': 'none', 'cursor': 'pointer',
1764
1910
  'border': 'none', 'background': 'transparent', 'font-family': 'inherit'
1765
1911
  },
@@ -1794,10 +1940,11 @@ var structuralRules = {
1794
1940
  '.bw_page_item': { 'display': 'list-item', 'list-style': 'none' },
1795
1941
  '.bw_page_link': {
1796
1942
  'position': 'relative', 'display': 'block', 'padding': '0.375rem 0.75rem',
1797
- 'margin-left': '-1px', 'line-height': '1.25', 'text-decoration': 'none'
1943
+ 'margin-left': '-1px', 'line-height': '1.25', 'text-decoration': 'none',
1944
+ 'border': '1px solid transparent', 'cursor': 'pointer',
1945
+ 'font-family': 'inherit', 'font-size': 'inherit', 'background': 'none'
1798
1946
  },
1799
- '.bw_page_item:first-child .bw_page_link': { 'margin-left': '0', 'border-top-left-radius': '0.375rem', 'border-bottom-left-radius': '0.375rem' },
1800
- '.bw_page_item:last-child .bw_page_link': { 'border-top-right-radius': '0.375rem', 'border-bottom-right-radius': '0.375rem' },
1947
+ '.bw_page_item:first-child .bw_page_link': { 'margin-left': '0' },
1801
1948
  '.bw_page_link:focus-visible': { 'z-index': '3', 'outline': '2px solid currentColor', 'outline-offset': '-2px' }
1802
1949
  },
1803
1950
 
@@ -1954,6 +2101,7 @@ var structuralRules = {
1954
2101
  '.bw_accordion_header': { 'margin': '0' },
1955
2102
  '.bw_accordion_button': {
1956
2103
  'position': 'relative', 'display': 'flex', 'align-items': 'center', 'width': '100%',
2104
+ 'padding': '0.875rem 1.25rem',
1957
2105
  'font-size': '1rem', 'font-weight': '500', 'text-align': 'left',
1958
2106
  'background-color': 'transparent', 'border': '0', 'overflow-anchor': 'none', 'cursor': 'pointer',
1959
2107
  'font-family': 'inherit'
@@ -1965,10 +2113,9 @@ var structuralRules = {
1965
2113
  'background-repeat': 'no-repeat', 'background-size': '1.25rem'
1966
2114
  },
1967
2115
  '.bw_accordion_button:not(.bw_collapsed)::after': { 'transform': 'rotate(-180deg)' },
1968
- '.bw_accordion_collapse': { 'max-height': '0', 'overflow': 'hidden' },
1969
- '.bw_accordion_collapse.bw_collapse_show': { 'max-height': 'none' },
1970
- '.bw_accordion_item:first-child': { 'border-top-left-radius': '8px', 'border-top-right-radius': '8px' },
1971
- '.bw_accordion_item:last-child': { 'border-bottom-left-radius': '8px', 'border-bottom-right-radius': '8px' }
2116
+ '.bw_accordion_body': { 'padding': '1rem 1.25rem' },
2117
+ '.bw_accordion_collapse': { 'max-height': '0', 'overflow': 'hidden', 'transition': 'max-height 0.3s ease' },
2118
+ '.bw_accordion_collapse.bw_collapse_show': { 'max-height': 'none' }
1972
2119
  },
1973
2120
 
1974
2121
  // ---- Carousel ----
@@ -2114,7 +2261,13 @@ var structuralRules = {
2114
2261
 
2115
2262
  // ---- Stat card ----
2116
2263
  statCard: {
2117
- '.bw_stat_card': { 'border-left': '4px solid transparent' },
2264
+ '.bw_stat_card': {
2265
+ 'padding': '1.25rem',
2266
+ 'border-left': '4px solid transparent',
2267
+ 'border-radius': '0.375rem',
2268
+ 'background-color': 'inherit',
2269
+ 'transition': 'transform 0.15s ease'
2270
+ },
2118
2271
  '.bw_stat_card:hover': { 'transform': 'translateY(-1px)' },
2119
2272
  '.bw_stat_icon': { 'font-size': '1.5rem', 'margin-bottom': '0.5rem' },
2120
2273
  '.bw_stat_value': { 'font-size': '2rem', 'font-weight': '700', 'line-height': '1.2' },
@@ -2477,6 +2630,20 @@ function generateUtilityRules() {
2477
2630
  rules['.list-inline-item'] = { 'display': 'inline-block' };
2478
2631
  rules['.list-inline-item:not(:last-child)'] = { 'margin-right': '.5rem' };
2479
2632
 
2633
+ // Typography — bw_ prefixed utilities via loops
2634
+ var _imp = function(p, v) { var o = {}; o[p] = v + ' !important'; return o; };
2635
+ [['fs',{'xs':'0.75rem','sm':'0.875rem','base':'1rem','lg':'1.125rem','xl':'1.25rem','2xl':'1.5rem'},'font-size'],
2636
+ ['fw',{light:'300',normal:'400',medium:'500',semibold:'600',bold:'700'},'font-weight'],
2637
+ ['lh',{tight:'1.25',normal:'1.5',relaxed:'1.75'},'line-height']
2638
+ ].forEach(function(d) { for (var dk in d[1]) rules['.bw_'+d[0]+'_'+dk] = _imp(d[2], d[1][dk]); });
2639
+
2640
+ // Flex utilities
2641
+ rules['.bw_flex'] = { 'display': 'flex' };
2642
+ rules['.bw_flex_column'] = { 'flex-direction': 'column' };
2643
+ rules['.bw_flex_wrap'] = { 'flex-wrap': 'wrap' };
2644
+ rules['.bw_flex_center'] = { 'display': 'flex', 'align-items': 'center', 'justify-content': 'center' };
2645
+ for (var gk in spacingValues) rules['.bw_gap_' + gk] = { 'gap': spacingValues[gk] + ' !important' };
2646
+
2480
2647
  // Visibility
2481
2648
  rules['.bw_visible, .visible'] = { 'visibility': 'visible !important' };
2482
2649
  rules['.bw_invisible, .invisible'] = { 'visibility': 'hidden !important' };
@@ -2537,6 +2704,26 @@ function getStructuralStyles() {
2537
2704
  return getStructuralCSS();
2538
2705
  }
2539
2706
 
2707
+ /**
2708
+ * Get CSS reset rules only (box-sizing, html/body font, reduced-motion).
2709
+ * Separate from themed/structural rules for independent injection.
2710
+ * @returns {Object} CSS rules object for the reset layer
2711
+ */
2712
+ function getResetStyles() {
2713
+ var rules = {};
2714
+ Object.assign(rules, structuralRules.base);
2715
+ // Include reduced-motion preference
2716
+ rules['@media (prefers-reduced-motion: reduce)'] = {
2717
+ '*, *::before, *::after': {
2718
+ 'animation-duration': '0.01ms !important',
2719
+ 'animation-iteration-count': '1 !important',
2720
+ 'transition-duration': '0.01ms !important',
2721
+ 'scroll-behavior': 'auto !important'
2722
+ }
2723
+ };
2724
+ return rules;
2725
+ }
2726
+
2540
2727
  // =========================================================================
2541
2728
  // defaultStyles — backward-compatible categorized view
2542
2729
  // =========================================================================
@@ -2566,60 +2753,41 @@ Object.assign({}, structuralRules, {
2566
2753
  });
2567
2754
 
2568
2755
  /**
2569
- * Generate alternate-palette CSS scoped under `.bw_theme_alt`.
2570
- * Uses the same `generateThemedCSS()` pipeline as the primary palette —
2571
- * both sides go through identical code paths.
2572
- *
2573
- * @param {string} name - Theme scope name (e.g. 'ocean'). '' for global.
2574
- * @param {Object} altPalette - From derivePalette(deriveAlternateConfig(...))
2575
- * @param {Object} layout - From resolveLayout()
2576
- * @returns {Object} CSS rules object scoped under .bw_theme_alt (+ optional .name)
2756
+ * Prefix every selector in a rules object with a scope selector.
2757
+ * Handles @media/@keyframes blocks and comma-separated selectors.
2758
+ * @param {Object} rules - CSS rules object
2759
+ * @param {string} prefix - Scope prefix (e.g. '#my-dashboard', '.bw_theme_alt')
2760
+ * @param {boolean} [compound=false] - If true, use compound selector (no space)
2761
+ * for the first segment: `#scope.bw_theme_alt .sel` vs `#scope .sel`
2762
+ * @returns {Object} New rules object with scoped selectors
2577
2763
  */
2578
- function generateAlternateCSS(name, altPalette, layout) {
2579
- // Generate themed CSS using the same pipeline as primary
2580
- var rawRules = generateThemedCSS('', altPalette, layout);
2581
-
2582
- // Re-scope every selector under .bw_theme_alt (+ optional theme name)
2583
- var altPrefix = name ? '.' + name + '.bw_theme_alt' : '.bw_theme_alt';
2584
- var altRules = {};
2585
-
2586
- for (var sel in rawRules) {
2587
- if (!rawRules.hasOwnProperty(sel)) continue;
2588
-
2764
+ function scopeRulesUnder(rules, prefix, compound) {
2765
+ var scoped = {};
2766
+ for (var sel in rules) {
2767
+ if (!rules.hasOwnProperty(sel)) continue;
2589
2768
  if (sel.charAt(0) === '@') {
2590
2769
  // @media / @keyframes — recurse into the block
2591
- var innerBlock = rawRules[sel];
2592
- var altInner = {};
2770
+ var innerBlock = rules[sel];
2771
+ var scopedInner = {};
2593
2772
  for (var innerSel in innerBlock) {
2594
2773
  if (!innerBlock.hasOwnProperty(innerSel)) continue;
2595
- altInner[altPrefix + ' ' + innerSel] = innerBlock[innerSel];
2774
+ scopedInner[_prefixSelector(innerSel, prefix)] = innerBlock[innerSel];
2596
2775
  }
2597
- altRules[sel] = altInner;
2776
+ scoped[sel] = scopedInner;
2598
2777
  } else {
2599
- // Regular selector — prefix with alt scope
2600
- // Handle comma-separated selectors
2601
- var parts = sel.split(',');
2602
- var scopedParts = [];
2603
- for (var i = 0; i < parts.length; i++) {
2604
- var s = parts[i].trim();
2605
- // 'body' selector gets special treatment: .bw_theme_alt body
2606
- if (s === 'body' || s.indexOf('body') === 0) {
2607
- scopedParts.push(altPrefix + ' ' + s);
2608
- } else {
2609
- scopedParts.push(altPrefix + ' ' + s);
2610
- }
2611
- }
2612
- altRules[scopedParts.join(', ')] = rawRules[sel];
2778
+ scoped[_prefixSelector(sel, prefix)] = rules[sel];
2613
2779
  }
2614
2780
  }
2781
+ return scoped;
2782
+ }
2615
2783
 
2616
- // Add body-level overrides for the alternate surface
2617
- altRules[altPrefix + ' body, :root' + altPrefix + ' body'] = {
2618
- 'color': altPalette.dark.base,
2619
- 'background-color': altPalette.light.base
2620
- };
2621
-
2622
- return altRules;
2784
+ function _prefixSelector(sel, prefix) {
2785
+ var parts = sel.split(',');
2786
+ var result = [];
2787
+ for (var i = 0; i < parts.length; i++) {
2788
+ result.push(prefix + ' ' + parts[i].trim());
2789
+ }
2790
+ return result.join(', ');
2623
2791
  }
2624
2792
 
2625
2793
  /**
@@ -3591,7 +3759,7 @@ function makeCol(props = {}) {
3591
3759
  if (breakpoint === 'xs') {
3592
3760
  classes.push(`bw_col_${value}`);
3593
3761
  } else {
3594
- classes.push(`bw_col_${breakpoint}-${value}`);
3762
+ classes.push(`bw_col_${breakpoint}_${value}`);
3595
3763
  }
3596
3764
  });
3597
3765
  } else if (size) {
@@ -4974,8 +5142,8 @@ function makePagination(props = {}) {
4974
5142
  t: 'li',
4975
5143
  a: { class: `bw_page_item ${currentPage <= 1 ? 'bw_disabled' : ''}`.trim() },
4976
5144
  c: {
4977
- t: 'a',
4978
- a: { class: 'bw_page_link', href: '#', onclick: handleClick(currentPage - 1), 'aria-label': 'Previous' },
5145
+ t: 'button',
5146
+ a: { class: 'bw_page_link', type: 'button', onclick: handleClick(currentPage - 1), 'aria-label': 'Previous', disabled: currentPage <= 1 ? true : undefined },
4979
5147
  c: '\u2039'
4980
5148
  }
4981
5149
  });
@@ -4987,8 +5155,8 @@ function makePagination(props = {}) {
4987
5155
  t: 'li',
4988
5156
  a: { class: `bw_page_item ${pageNum === currentPage ? 'bw_active' : ''}`.trim() },
4989
5157
  c: {
4990
- t: 'a',
4991
- a: { class: 'bw_page_link', href: '#', onclick: handleClick(pageNum) },
5158
+ t: 'button',
5159
+ a: { class: 'bw_page_link', type: 'button', onclick: handleClick(pageNum), 'aria-current': pageNum === currentPage ? 'page' : undefined },
4992
5160
  c: '' + pageNum
4993
5161
  }
4994
5162
  });
@@ -5000,8 +5168,8 @@ function makePagination(props = {}) {
5000
5168
  t: 'li',
5001
5169
  a: { class: `bw_page_item ${currentPage >= pages ? 'bw_disabled' : ''}`.trim() },
5002
5170
  c: {
5003
- t: 'a',
5004
- a: { class: 'bw_page_link', href: '#', onclick: handleClick(currentPage + 1), 'aria-label': 'Next' },
5171
+ t: 'button',
5172
+ a: { class: 'bw_page_link', type: 'button', onclick: handleClick(currentPage + 1), 'aria-label': 'Next', disabled: currentPage >= pages ? true : undefined },
5005
5173
  c: '\u203A'
5006
5174
  }
5007
5175
  });
@@ -7283,7 +7451,12 @@ bw._el = function(id) {
7283
7451
  el = document.querySelector('[data-bw_id="' + id + '"]');
7284
7452
  }
7285
7453
 
7286
- // 5. Cache the result for next time
7454
+ // 5. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
7455
+ if (!el && id.indexOf('bw_uuid_') === 0) {
7456
+ el = document.querySelector('.' + id);
7457
+ }
7458
+
7459
+ // 6. Cache the result for next time
7287
7460
  if (el) {
7288
7461
  bw._nodeMap[id] = el;
7289
7462
  }
@@ -7336,6 +7509,84 @@ bw._deregisterNode = function(el, bwId) {
7336
7509
  }
7337
7510
  };
7338
7511
 
7512
+ // ===================================================================================
7513
+ // bw.assignUUID() / bw.getUUID() — Explicit UUID addressing for TACO objects
7514
+ // ===================================================================================
7515
+
7516
+ /**
7517
+ * Regex to match a bw_uuid_* token in a class string.
7518
+ * @private
7519
+ */
7520
+ var _UUID_RE = /\bbw_uuid_[a-z0-9_]+\b/;
7521
+
7522
+ /**
7523
+ * Assign a UUID to a TACO object by appending a `bw_uuid_*` token to `taco.a.class`.
7524
+ *
7525
+ * Idempotent by default — calling twice returns the same UUID. Pass `forceNew=true`
7526
+ * to replace an existing UUID (useful in loops where each TACO needs a unique ID).
7527
+ *
7528
+ * @param {Object} taco - A TACO object `{t, a, c, o}`
7529
+ * @param {boolean} [forceNew=false] - If true, replaces any existing UUID with a new one
7530
+ * @returns {string} The UUID string (e.g. 'bw_uuid_a1b2c3d4e5')
7531
+ * @category Identifiers
7532
+ * @example
7533
+ * var card = bw.makeStatCard({ value: '0', label: 'Scans' });
7534
+ * var uuid = bw.assignUUID(card); // 'bw_uuid_a1b2c3d4e5'
7535
+ * var same = bw.assignUUID(card); // same UUID (idempotent)
7536
+ * var diff = bw.assignUUID(card, true); // new UUID (forced)
7537
+ */
7538
+ bw.assignUUID = function(taco, forceNew) {
7539
+ if (!taco || !_is(taco, 'object')) return null;
7540
+
7541
+ // Ensure taco.a exists
7542
+ if (!taco.a) taco.a = {};
7543
+ if (!_is(taco.a.class, 'string')) taco.a.class = taco.a.class ? String(taco.a.class) : '';
7544
+
7545
+ var existing = taco.a.class.match(_UUID_RE);
7546
+
7547
+ if (existing && !forceNew) {
7548
+ return existing[0];
7549
+ }
7550
+
7551
+ // Remove old UUID if forceNew
7552
+ if (existing) {
7553
+ taco.a.class = taco.a.class.replace(_UUID_RE, '').replace(/\s+/g, ' ').trim();
7554
+ }
7555
+
7556
+ var uuid = bw.uuid('uuid');
7557
+ taco.a.class = (taco.a.class ? taco.a.class + ' ' : '') + uuid;
7558
+ return uuid;
7559
+ };
7560
+
7561
+ /**
7562
+ * Read the UUID from a TACO object or DOM element. Pure getter, no side effects.
7563
+ *
7564
+ * @param {Object|Element} tacoOrElement - A TACO object or DOM element
7565
+ * @returns {string|null} The UUID string, or null if none assigned
7566
+ * @category Identifiers
7567
+ * @example
7568
+ * bw.getUUID(card) // 'bw_uuid_a1b2c3d4e5' (from TACO)
7569
+ * bw.getUUID(domEl) // 'bw_uuid_a1b2c3d4e5' (from DOM element)
7570
+ * bw.getUUID({t:'div'}) // null (no UUID)
7571
+ */
7572
+ bw.getUUID = function(tacoOrElement) {
7573
+ if (!tacoOrElement) return null;
7574
+
7575
+ var classStr;
7576
+ // DOM element: check className
7577
+ if (tacoOrElement.className !== undefined && tacoOrElement.tagName) {
7578
+ classStr = tacoOrElement.className;
7579
+ }
7580
+ // TACO object: check a.class
7581
+ else if (tacoOrElement.a && _is(tacoOrElement.a.class, 'string')) {
7582
+ classStr = tacoOrElement.a.class;
7583
+ }
7584
+
7585
+ if (!classStr) return null;
7586
+ var match = classStr.match(_UUID_RE);
7587
+ return match ? match[0] : null;
7588
+ };
7589
+
7339
7590
  /**
7340
7591
  * Escape HTML special characters to prevent XSS.
7341
7592
  *
@@ -7385,6 +7636,42 @@ bw.raw = function(str) {
7385
7636
  return { __bw_raw: true, v: String(str) };
7386
7637
  };
7387
7638
 
7639
+ /**
7640
+ * Hyperscript-style TACO constructor.
7641
+ *
7642
+ * A convenience helper that returns a canonical TACO object from positional
7643
+ * arguments. The return value is a plain object — serializable, works with
7644
+ * bwserve, and accepted everywhere TACO is accepted.
7645
+ *
7646
+ * @param {string} tag - HTML tag name (e.g. 'div', 'p', 'section')
7647
+ * @param {Object|null} [attrs] - HTML attributes object. Pass null or omit to skip.
7648
+ * @param {*} [content] - Content: string, number, TACO object, or array of children.
7649
+ * @param {Object} [options] - TACO options (state, lifecycle hooks, render fn).
7650
+ * @returns {Object} Plain TACO object {t, a?, c?, o?}
7651
+ * @category Utilities
7652
+ * @see bw.html
7653
+ * @see bw.createDOM
7654
+ * @see bw.DOM
7655
+ * @example
7656
+ * bw.h('div')
7657
+ * // => { t: 'div' }
7658
+ *
7659
+ * bw.h('p', { class: 'bw_text_muted' }, 'Hello')
7660
+ * // => { t: 'p', a: { class: 'bw_text_muted' }, c: 'Hello' }
7661
+ *
7662
+ * bw.h('ul', null, [
7663
+ * bw.h('li', null, 'one'),
7664
+ * bw.h('li', null, 'two')
7665
+ * ])
7666
+ * // => { t: 'ul', c: [{ t: 'li', c: 'one' }, { t: 'li', c: 'two' }] }
7667
+ */
7668
+ bw.h = function(tag, attrs, content, options) {
7669
+ var taco = { t: String(tag) };
7670
+ if (attrs !== null && attrs !== undefined) taco.a = attrs;
7671
+ if (content !== undefined) taco.c = content;
7672
+ if (options !== undefined) taco.o = options;
7673
+ return taco;
7674
+ };
7388
7675
 
7389
7676
  /**
7390
7677
  * Convert a TACO object (or array of TACOs) to an HTML string.
@@ -7649,7 +7936,7 @@ bw.htmlPage = function(opts) {
7649
7936
  ? (THEME_PRESETS[theme.toLowerCase()] || null)
7650
7937
  : theme;
7651
7938
  if (themeConfig) {
7652
- var themeResult = bw.generateTheme('', Object.assign({}, themeConfig, { inject: false }));
7939
+ var themeResult = bw.makeStyles(themeConfig);
7653
7940
  themeCSS = themeResult.css;
7654
7941
  }
7655
7942
  }
@@ -7675,14 +7962,14 @@ bw.htmlPage = function(opts) {
7675
7962
  // Combine all CSS
7676
7963
  var allCSS = (themeCSS ? themeCSS + '\n' : '') + css;
7677
7964
 
7678
- // Body-end script: registry entries + optional loadDefaultStyles
7965
+ // Body-end script: registry entries + optional loadStyles
7679
7966
  var bodyEndScript = '';
7680
7967
  var bodyEndParts = [];
7681
7968
  if (registryEntries) {
7682
7969
  bodyEndParts.push(registryEntries);
7683
7970
  }
7684
7971
  if (runtime === 'inline' || runtime === 'cdn') {
7685
- bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadDefaultStyles();}');
7972
+ bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadStyles();}');
7686
7973
  }
7687
7974
  if (bodyEndParts.length > 0) {
7688
7975
  bodyEndScript = '<script>\n' + bodyEndParts.join('\n') + '\n</script>';
@@ -7856,6 +8143,14 @@ bw.createDOM = function(taco, options = {}) {
7856
8143
  bw._registerNode(el, null);
7857
8144
  }
7858
8145
 
8146
+ // Register UUID class in node cache (bw_uuid_* tokens in class string)
8147
+ if (el.className) {
8148
+ var uuidMatch = el.className.match(_UUID_RE);
8149
+ if (uuidMatch) {
8150
+ bw._nodeMap[uuidMatch[0]] = el;
8151
+ }
8152
+ }
8153
+
7859
8154
  // Handle lifecycle hooks and state
7860
8155
  if (opts.mounted || opts.unmount || opts.render || opts.state) {
7861
8156
  const id = attrs['data-bw_id'] || bw.uuid();
@@ -8228,6 +8523,16 @@ bw.renderComponent = function(taco, options = {}) {
8228
8523
  bw.cleanup = function(element) {
8229
8524
  if (!bw._isBrowser || !element) return;
8230
8525
 
8526
+ // Deregister UUID classes from node cache (element + descendants)
8527
+ // Covers elements that have UUID but no data-bw_id
8528
+ var selfUuidMatch = element.className && element.className.match(_UUID_RE);
8529
+ if (selfUuidMatch) delete bw._nodeMap[selfUuidMatch[0]];
8530
+ var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
8531
+ uuidEls.forEach(function(uel) {
8532
+ var m = uel.className && uel.className.match(_UUID_RE);
8533
+ if (m) delete bw._nodeMap[m[0]];
8534
+ });
8535
+
8231
8536
  // Find all elements with data-bw_id
8232
8537
  const elements = element.querySelectorAll('[data-bw_id]');
8233
8538
 
@@ -8243,6 +8548,10 @@ bw.cleanup = function(element) {
8243
8548
  // Deregister from node cache
8244
8549
  bw._deregisterNode(el, id);
8245
8550
 
8551
+ // Deregister UUID class from node cache
8552
+ var uuidMatch = el.className && el.className.match(_UUID_RE);
8553
+ if (uuidMatch) delete bw._nodeMap[uuidMatch[0]];
8554
+
8246
8555
  // Clean up pub/sub subscriptions tied to this element
8247
8556
  if (el._bw_subs) {
8248
8557
  el._bw_subs.forEach(function(unsub) { unsub(); });
@@ -8267,6 +8576,10 @@ bw.cleanup = function(element) {
8267
8576
  // Deregister from node cache
8268
8577
  bw._deregisterNode(element, id);
8269
8578
 
8579
+ // Deregister UUID class from node cache
8580
+ var elemUuidMatch = element.className && element.className.match(_UUID_RE);
8581
+ if (elemUuidMatch) delete bw._nodeMap[elemUuidMatch[0]];
8582
+
8270
8583
  // Clean up pub/sub subscriptions tied to element itself
8271
8584
  if (element._bw_subs) {
8272
8585
  element._bw_subs.forEach(function(unsub) { unsub(); });
@@ -8878,7 +9191,7 @@ function ComponentHandle(taco) {
8878
9191
  willMount: o.willMount || null,
8879
9192
  mounted: o.mounted || null,
8880
9193
  willUpdate: o.willUpdate || null,
8881
- onUpdate: o.onUpdate || null,
9194
+ onUpdate: o.onUpdate || o.updated || null,
8882
9195
  unmount: o.unmount || null,
8883
9196
  willDestroy: o.willDestroy || null
8884
9197
  };
@@ -9817,7 +10130,7 @@ bw.component = function(taco) {
9817
10130
  * and calls the named method. This is the bitwrench equivalent of
9818
10131
  * Win32 SendMessage(hwnd, msg, wParam, lParam).
9819
10132
  *
9820
- * @param {string} target - Component UUID (data-bw_comp_id) or user tag (CSS class)
10133
+ * @param {string} target - Component UUID (bw_uuid_*), comp ID (data-bw_comp_id), or user tag (CSS class)
9821
10134
  * @param {string} action - Method name to call on the component
9822
10135
  * @param {*} data - Data to pass to the method
9823
10136
  * @returns {boolean} True if message was dispatched successfully
@@ -9834,9 +10147,14 @@ bw.component = function(taco) {
9834
10147
  * };
9835
10148
  */
9836
10149
  bw.message = function(target, action, data) {
9837
- // Try data-bw_comp_id attribute first, then CSS class (user tag)
9838
- var el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
9839
- if (!el) {
10150
+ // Try bw._el() first (handles UUID class, nodeMap cache, getElementById)
10151
+ var el = bw._el(target);
10152
+ // Then try data-bw_comp_id attribute
10153
+ if (!el || !el._bwComponentHandle) {
10154
+ el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
10155
+ }
10156
+ // Then try CSS class (user tag)
10157
+ if (!el || !el._bwComponentHandle) {
9840
10158
  el = bw.$('.' + target)[0];
9841
10159
  }
9842
10160
  if (!el || !el._bwComponentHandle) return false;
@@ -9850,59 +10168,24 @@ bw.message = function(target, action, data) {
9850
10168
  };
9851
10169
 
9852
10170
  // ===================================================================================
9853
- // bw.clientApply() / bw.clientConnect() — Server-driven UI protocol
10171
+ // bw.apply() / bw.parseJSONFlex() — Server-driven UI protocol
9854
10172
  // ===================================================================================
9855
10173
 
9856
10174
  /**
9857
10175
  * Registry of named functions sent via register messages.
9858
- * Populated by clientApply({ type: 'register', name, body }).
9859
- * Invoked by clientApply({ type: 'call', name, args }).
10176
+ * Populated by bw.apply({ type: 'register', name, body }).
10177
+ * Invoked by bw.apply({ type: 'call', name, args }).
9860
10178
  * @private
9861
10179
  */
9862
10180
  bw._clientFunctions = {};
9863
10181
 
9864
10182
  /**
9865
- * Whether exec messages are allowed. Set by clientConnect opts.allowExec.
10183
+ * Whether exec messages are allowed. Set by bwclient connect opts.allowExec.
9866
10184
  * Default false — exec messages are rejected unless explicitly opted in.
9867
10185
  * @private
9868
10186
  */
9869
10187
  bw._allowExec = false;
9870
10188
 
9871
- /**
9872
- * Built-in client functions available via call() without registration.
9873
- * @private
9874
- */
9875
- bw._builtinClientFunctions = {
9876
- scrollTo: function(selector) {
9877
- var el = bw._el(selector);
9878
- if (el) el.scrollTop = el.scrollHeight;
9879
- },
9880
- focus: function(selector) {
9881
- var el = bw._el(selector);
9882
- if (el && _is(el.focus, 'function')) el.focus();
9883
- },
9884
- download: function(filename, content, mimeType) {
9885
- if (typeof document === 'undefined') return;
9886
- var blob = new Blob([content], { type: mimeType || 'text/plain' });
9887
- var a = document.createElement('a');
9888
- a.href = URL.createObjectURL(blob);
9889
- a.download = filename;
9890
- a.click();
9891
- URL.revokeObjectURL(a.href);
9892
- },
9893
- clipboard: function(text) {
9894
- if (typeof navigator !== 'undefined' && navigator.clipboard) {
9895
- navigator.clipboard.writeText(text);
9896
- }
9897
- },
9898
- redirect: function(url) {
9899
- if (typeof window !== 'undefined') window.location.href = url;
9900
- },
9901
- log: function() {
9902
- console.log.apply(console, arguments);
9903
- }
9904
- };
9905
-
9906
10189
  /**
9907
10190
  * Parse a bwserve protocol message string, supporting both strict JSON
9908
10191
  * and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
@@ -9917,9 +10200,9 @@ bw._builtinClientFunctions = {
9917
10200
  * @param {string} str - JSON or r-prefixed relaxed JSON string
9918
10201
  * @returns {Object} Parsed message object
9919
10202
  * @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
9920
- * @category Server
10203
+ * @category Core
9921
10204
  */
9922
- bw.clientParse = function(str) {
10205
+ bw.parseJSONFlex = function(str) {
9923
10206
  str = (str || '').trim();
9924
10207
  if (str.charAt(0) !== 'r') return JSON.parse(str);
9925
10208
  str = str.slice(1);
@@ -10004,10 +10287,10 @@ bw.clientParse = function(str) {
10004
10287
  * append — target.appendChild(bw.createDOM(node))
10005
10288
  * remove — bw.cleanup(target); target.remove()
10006
10289
  * patch — bw.patch(target, content, attr)
10007
- * batch — iterate ops, call clientApply for each
10290
+ * batch — iterate ops, call bw.apply for each
10008
10291
  * message — bw.message(target, action, data)
10009
10292
  * register — store a named function for later call()
10010
- * call — invoke a registered or built-in function
10293
+ * call — invoke a registered function
10011
10294
  * exec — execute arbitrary JS (requires allowExec)
10012
10295
  *
10013
10296
  * Target resolution:
@@ -10016,9 +10299,9 @@ bw.clientParse = function(str) {
10016
10299
  *
10017
10300
  * @param {Object} msg - Protocol message
10018
10301
  * @returns {boolean} true if the message was applied successfully
10019
- * @category Server
10302
+ * @category Core
10020
10303
  */
10021
- bw.clientApply = function(msg) {
10304
+ bw.apply = function(msg) {
10022
10305
  if (!msg || !msg.type) return false;
10023
10306
 
10024
10307
  var type = msg.type;
@@ -10052,7 +10335,7 @@ bw.clientApply = function(msg) {
10052
10335
  if (!_isA(msg.ops)) return false;
10053
10336
  var allOk = true;
10054
10337
  msg.ops.forEach(function(op) {
10055
- if (!bw.clientApply(op)) allOk = false;
10338
+ if (!bw.apply(op)) allOk = false;
10056
10339
  });
10057
10340
  return allOk;
10058
10341
 
@@ -10071,7 +10354,7 @@ bw.clientApply = function(msg) {
10071
10354
 
10072
10355
  } else if (type === 'call') {
10073
10356
  if (!msg.name) return false;
10074
- var fn = bw._clientFunctions[msg.name] || bw._builtinClientFunctions[msg.name];
10357
+ var fn = bw._clientFunctions[msg.name];
10075
10358
  if (!_is(fn, 'function')) return false;
10076
10359
  try {
10077
10360
  var args = _isA(msg.args) ? msg.args : [];
@@ -10100,139 +10383,6 @@ bw.clientApply = function(msg) {
10100
10383
  return false;
10101
10384
  };
10102
10385
 
10103
- /**
10104
- * Connect to a bwserve SSE endpoint and apply protocol messages automatically.
10105
- *
10106
- * Returns a connection object with sendAction(), on(), and close() methods.
10107
- *
10108
- * @param {string} url - SSE endpoint URL (e.g., '/__bw/events/client-1')
10109
- * @param {Object} [opts] - Connection options
10110
- * @param {string} [opts.transport='sse'] - Transport type: 'sse' (default) or 'poll'
10111
- * @param {number} [opts.interval=2000] - Poll interval in ms (only for 'poll' transport)
10112
- * @param {string} [opts.actionUrl] - POST endpoint for actions (default: derived from url)
10113
- * @param {boolean} [opts.reconnect=true] - Auto-reconnect on disconnect
10114
- * @param {boolean} [opts.allowExec=false] - Enable exec message type (arbitrary JS execution)
10115
- * @param {Function} [opts.onStatus] - Status callback: 'connecting'|'connected'|'disconnected'
10116
- * @param {Function} [opts.onMessage] - Raw message callback (before clientApply)
10117
- * @returns {Object} Connection object { sendAction, on, close, status }
10118
- * @category Server
10119
- */
10120
- bw.clientConnect = function(url, opts) {
10121
- opts = opts || {};
10122
- var transport = opts.transport || 'sse';
10123
- var actionUrl = opts.actionUrl || url.replace(/\/events\//, '/action/');
10124
- var reconnect = opts.reconnect !== false;
10125
- var onStatus = opts.onStatus || function() {};
10126
- var onMessage = opts.onMessage || null;
10127
- var handlers = {};
10128
- // Set the global allowExec flag from connection options
10129
- bw._allowExec = !!opts.allowExec;
10130
- var conn = {
10131
- status: 'connecting',
10132
- _es: null,
10133
- _pollTimer: null
10134
- };
10135
-
10136
- function setStatus(s) {
10137
- conn.status = s;
10138
- onStatus(s);
10139
- }
10140
-
10141
- function handleMessage(data) {
10142
- try {
10143
- var msg = _is(data, 'string') ? bw.clientParse(data) : data;
10144
- if (onMessage) onMessage(msg);
10145
- if (handlers.message) handlers.message(msg);
10146
- bw.clientApply(msg);
10147
- } catch (e) {
10148
- if (handlers.error) handlers.error(e);
10149
- }
10150
- }
10151
-
10152
- if (transport === 'sse' && typeof EventSource !== 'undefined') {
10153
- setStatus('connecting');
10154
- var es = new EventSource(url);
10155
- conn._es = es;
10156
-
10157
- es.onopen = function() {
10158
- setStatus('connected');
10159
- if (handlers.open) handlers.open();
10160
- };
10161
-
10162
- es.onmessage = function(e) {
10163
- handleMessage(e.data);
10164
- };
10165
-
10166
- es.onerror = function() {
10167
- if (conn.status === 'connected') {
10168
- setStatus('disconnected');
10169
- }
10170
- if (handlers.error) handlers.error(new Error('SSE connection error'));
10171
- if (!reconnect) {
10172
- es.close();
10173
- }
10174
- // EventSource auto-reconnects by default when reconnect=true
10175
- };
10176
- } else if (transport === 'poll') {
10177
- var interval = opts.interval || 2000;
10178
- setStatus('connected');
10179
- conn._pollTimer = setInterval(function() {
10180
- fetch(url).then(function(r) { return r.json(); }).then(function(msgs) {
10181
- if (_isA(msgs)) {
10182
- msgs.forEach(handleMessage);
10183
- } else if (msgs && msgs.type) {
10184
- handleMessage(msgs);
10185
- }
10186
- }).catch(function(e) {
10187
- if (handlers.error) handlers.error(e);
10188
- });
10189
- }, interval);
10190
- }
10191
-
10192
- /**
10193
- * Send an action to the server via POST.
10194
- * @param {string} action - Action name
10195
- * @param {Object} [data] - Action payload
10196
- */
10197
- conn.sendAction = function(action, data) {
10198
- var body = JSON.stringify({ type: 'action', action: action, data: data || {} });
10199
- fetch(actionUrl, {
10200
- method: 'POST',
10201
- headers: { 'Content-Type': 'application/json' },
10202
- body: body
10203
- }).catch(function(e) {
10204
- if (handlers.error) handlers.error(e);
10205
- });
10206
- };
10207
-
10208
- /**
10209
- * Register an event handler.
10210
- * @param {string} event - 'open'|'message'|'error'|'close'
10211
- * @param {Function} handler
10212
- */
10213
- conn.on = function(event, handler) {
10214
- handlers[event] = handler;
10215
- return conn;
10216
- };
10217
-
10218
- /**
10219
- * Close the connection.
10220
- */
10221
- conn.close = function() {
10222
- if (conn._es) {
10223
- conn._es.close();
10224
- conn._es = null;
10225
- }
10226
- if (conn._pollTimer) {
10227
- clearInterval(conn._pollTimer);
10228
- conn._pollTimer = null;
10229
- }
10230
- setStatus('disconnected');
10231
- if (handlers.close) handlers.close();
10232
- };
10233
-
10234
- return conn;
10235
- };
10236
10386
 
10237
10387
  // ===================================================================================
10238
10388
  // bw.inspect() — Debug utility
@@ -10441,7 +10591,7 @@ bw.css = function(rules, options = {}) {
10441
10591
  * @returns {Element} The style element
10442
10592
  * @category CSS & Styling
10443
10593
  * @see bw.css
10444
- * @see bw.loadDefaultStyles
10594
+ * @see bw.loadStyles
10445
10595
  * @example
10446
10596
  * bw.injectCSS('.my-class { color: red; }');
10447
10597
  * bw.injectCSS({ '.card': { padding: '1rem' } }, { id: 'card-styles' });
@@ -10486,9 +10636,8 @@ bw.injectCSS = function(css, options = {}) {
10486
10636
  * @param {...Object} styles - Style objects to merge (left-to-right)
10487
10637
  * @returns {Object} Merged style object
10488
10638
  * @category CSS & Styling
10489
- * @see bw.u
10490
10639
  * @example
10491
- * var style = bw.s(bw.u.flex, bw.u.gap4, { color: 'red' });
10640
+ * var style = bw.s({ display: 'flex' }, { gap: '1rem' }, { color: 'red' });
10492
10641
  * // => { display: 'flex', gap: '1rem', color: 'red' }
10493
10642
  */
10494
10643
  bw.s = function() {
@@ -10500,99 +10649,6 @@ bw.s = function() {
10500
10649
  return result;
10501
10650
  };
10502
10651
 
10503
- /**
10504
- * Pre-built CSS utility objects (like Tailwind utilities, but in JS).
10505
- *
10506
- * Compose with `bw.s()` to build inline styles without writing raw CSS strings.
10507
- * Includes flex, padding, margin, typography, color, border, and transition utilities.
10508
- *
10509
- * @category CSS & Styling
10510
- * @see bw.s
10511
- * @example
10512
- * { t: 'div', a: { style: bw.s(bw.u.flex, bw.u.gap4, bw.u.p4) },
10513
- * c: 'Flexbox with 1rem gap and padding' }
10514
- */
10515
- bw.u = {
10516
- // Display
10517
- flex: { display: 'flex' },
10518
- flexCol: { display: 'flex', flexDirection: 'column' },
10519
- flexRow: { display: 'flex', flexDirection: 'row' },
10520
- flexWrap: { display: 'flex', flexWrap: 'wrap' },
10521
- block: { display: 'block' },
10522
- inline: { display: 'inline' },
10523
- hidden: { display: 'none' },
10524
-
10525
- // Flex alignment
10526
- justifyCenter: { justifyContent: 'center' },
10527
- justifyBetween: { justifyContent: 'space-between' },
10528
- justifyEnd: { justifyContent: 'flex-end' },
10529
- alignCenter: { alignItems: 'center' },
10530
- alignStart: { alignItems: 'flex-start' },
10531
- alignEnd: { alignItems: 'flex-end' },
10532
-
10533
- // Gap (0.25rem increments)
10534
- gap1: { gap: '0.25rem' },
10535
- gap2: { gap: '0.5rem' },
10536
- gap3: { gap: '0.75rem' },
10537
- gap4: { gap: '1rem' },
10538
- gap6: { gap: '1.5rem' },
10539
- gap8: { gap: '2rem' },
10540
-
10541
- // Padding
10542
- p0: { padding: '0' },
10543
- p1: { padding: '0.25rem' },
10544
- p2: { padding: '0.5rem' },
10545
- p3: { padding: '0.75rem' },
10546
- p4: { padding: '1rem' },
10547
- p6: { padding: '1.5rem' },
10548
- p8: { padding: '2rem' },
10549
- px4: { paddingLeft: '1rem', paddingRight: '1rem' },
10550
- py2: { paddingTop: '0.5rem', paddingBottom: '0.5rem' },
10551
- py4: { paddingTop: '1rem', paddingBottom: '1rem' },
10552
-
10553
- // Margin (same scale)
10554
- m0: { margin: '0' },
10555
- m4: { margin: '1rem' },
10556
- mt2: { marginTop: '0.5rem' },
10557
- mt4: { marginTop: '1rem' },
10558
- mb2: { marginBottom: '0.5rem' },
10559
- mb4: { marginBottom: '1rem' },
10560
- mx_auto: { marginLeft: 'auto', marginRight: 'auto' },
10561
-
10562
- // Typography
10563
- textSm: { fontSize: '0.875rem' },
10564
- textBase: { fontSize: '1rem' },
10565
- textLg: { fontSize: '1.125rem' },
10566
- textXl: { fontSize: '1.25rem' },
10567
- text2xl: { fontSize: '1.5rem' },
10568
- text3xl: { fontSize: '1.875rem' },
10569
- bold: { fontWeight: '700' },
10570
- semibold: { fontWeight: '600' },
10571
- italic: { fontStyle: 'italic' },
10572
- textCenter: { textAlign: 'center' },
10573
- textRight: { textAlign: 'right' },
10574
-
10575
- // Colors (from design tokens)
10576
- bgWhite: { background: '#ffffff' },
10577
- bgTeal: { background: '#006666', color: '#ffffff' },
10578
- textWhite: { color: '#ffffff' },
10579
- textTeal: { color: '#006666' },
10580
- textMuted: { color: '#888' },
10581
-
10582
- // Borders
10583
- rounded: { borderRadius: '0.375rem' },
10584
- roundedLg: { borderRadius: '0.5rem' },
10585
- roundedFull: { borderRadius: '9999px' },
10586
- border: { border: '1px solid #d8d8d8' },
10587
-
10588
- // Sizing
10589
- wFull: { width: '100%' },
10590
- hFull: { height: '100%' },
10591
-
10592
- // Transitions
10593
- transition: { transition: 'all 0.2s ease' }
10594
- };
10595
-
10596
10652
  /**
10597
10653
  * Generate responsive CSS with media query breakpoints.
10598
10654
  *
@@ -10714,103 +10770,49 @@ if (bw._isBrowser) {
10714
10770
  };
10715
10771
  }
10716
10772
 
10717
- /**
10718
- * Load the built-in Bootstrap-inspired default stylesheet.
10719
- *
10720
- * Injects bitwrench's batteries-included CSS (buttons, cards, grids, forms,
10721
- * alerts, badges, nav, tabs, etc.) into the document head. Call once at app startup.
10722
- * Returns null in Node.js (no DOM).
10723
- *
10724
- * @param {Object} [options] - Style loading options
10725
- * @param {boolean} [options.minify=true] - Minify the CSS output
10726
- * @returns {Element|null} Style element if in browser, null in Node.js
10727
- * @category CSS & Styling
10728
- * @see bw.setTheme
10729
- * @see bw.applyTheme
10730
- * @see bw.toggleTheme
10731
- * @example
10732
- * bw.loadDefaultStyles(); // inject all default CSS
10733
- */
10734
- bw.loadDefaultStyles = function(options = {}) {
10735
- const { minify = true, palette } = options;
10736
-
10737
- // 1. Inject structural CSS (layout, sizing — never changes with theme)
10738
- if (bw._isBrowser) {
10739
- var structuralCSS = bw.css(getStructuralStyles());
10740
- bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false, minify: minify });
10741
- }
10742
10773
 
10743
- // 2. Inject cosmetic CSS via generateTheme (colors, shadows, radii)
10744
- var paletteConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, palette || {});
10745
- var result = bw.generateTheme('', Object.assign({}, paletteConfig, { inject: true }));
10746
- return result;
10747
- };
10774
+ // =========================================================================
10775
+ // v2.0.18 Clean Styles API makeStyles / applyStyles / loadStyles / etc.
10776
+ // =========================================================================
10748
10777
 
10778
+ /**
10779
+ * Convert a scope selector to a <style> element id.
10780
+ * @private
10781
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview')
10782
+ * @returns {string} Style element id (e.g. 'bw_style_my_dashboard')
10783
+ */
10784
+ function _scopeToStyleId(scope) {
10785
+ if (!scope || scope === '' || scope === 'global') return 'bw_style_global';
10786
+ if (scope === 'reset') return 'bw_style_reset';
10787
+ // Strip leading # or . and convert - to _
10788
+ var clean = scope.replace(/^[#.]/, '').replace(/-/g, '_');
10789
+ return 'bw_style_' + clean;
10790
+ }
10749
10791
 
10750
10792
  /**
10751
- * Generate a complete, scoped theme from seed colors.
10793
+ * Generate a complete styles object from seed colors and layout config.
10794
+ * Pure function — no DOM, no state, no side effects.
10752
10795
  *
10753
- * Produces CSS for all themed components (buttons, alerts, badges, cards,
10754
- * forms, nav, tables, tabs, list groups, pagination, progress, hero, utilities)
10755
- * scoped under `.name` class. Multiple themes can coexist in the stylesheet.
10756
- * Swap themes by changing the class on a container element.
10796
+ * All parameters are optional. Defaults to the bitwrench default palette.
10757
10797
  *
10758
- * @param {string} name - CSS scope class (e.g. 'ocean'). Empty string = unscoped global.
10759
- * @param {Object} config - Theme configuration
10760
- * @param {string} config.primary - Primary brand color hex
10761
- * @param {string} config.secondary - Secondary color hex
10762
- * @param {string} [config.tertiary] - Tertiary/accent color hex (defaults to primary)
10763
- * @param {string} [config.success='#198754'] - Success color hex
10764
- * @param {string} [config.danger='#dc3545'] - Danger color hex
10765
- * @param {string} [config.warning='#ffc107'] - Warning color hex
10766
- * @param {string} [config.info='#0dcaf0'] - Info color hex
10767
- * @param {string} [config.light='#f8f9fa'] - Light color hex
10768
- * @param {string} [config.dark='#212529'] - Dark color hex
10769
- * @param {string} [config.background] - Page background hex (default: '#ffffff' light, derived dark)
10770
- * @param {string} [config.surface] - Surface/card background hex (default: '#f8f9fa' light, derived dark)
10798
+ * @param {Object} [config] - Style configuration
10799
+ * @param {string} [config.primary='#006666'] - Primary brand color hex
10800
+ * @param {string} [config.secondary='#6c757d'] - Secondary color hex
10801
+ * @param {string} [config.tertiary] - Tertiary color hex (defaults to primary)
10771
10802
  * @param {string} [config.spacing='normal'] - 'compact' | 'normal' | 'spacious'
10772
10803
  * @param {string} [config.radius='md'] - 'none' | 'sm' | 'md' | 'lg' | 'pill'
10773
- * @param {number} [config.fontSize=1.0] - Base font size scale factor
10774
- * @param {string|number} [config.typeRatio='normal'] - 'tight' | 'normal' | 'relaxed' | 'dramatic' or a number
10775
- * @param {string} [config.elevation='md'] - 'flat' | 'sm' | 'md' | 'lg'
10776
- * @param {string} [config.motion='standard'] - 'reduced' | 'standard' | 'expressive'
10777
- * @param {number} [config.harmonize=0.20] - 0-1, semantic color hue shift toward primary
10778
- * @param {boolean} [config.inject=true] - Inject into DOM (browser only)
10779
- * @returns {Object} { css, palette, name, isLightPrimary, alternate: { css, palette } }
10804
+ * @returns {Object} { css, alternateCss, rules, alternateRules, palette, alternatePalette, isLightPrimary }
10780
10805
  * @category CSS & Styling
10781
- * @see bw.applyTheme
10782
- * @see bw.toggleTheme
10783
- * @see bw.loadDefaultStyles
10806
+ * @see bw.applyStyles
10807
+ * @see bw.loadStyles
10784
10808
  * @example
10785
- * // Generate and inject an ocean theme (primary + alternate)
10786
- * var theme = bw.generateTheme('ocean', {
10787
- * primary: '#0077b6',
10788
- * secondary: '#90e0ef',
10789
- * tertiary: '#00b4d8'
10790
- * });
10791
- *
10792
- * // Apply to a container
10793
- * document.getElementById('app').classList.add('ocean');
10794
- *
10795
- * // Toggle to alternate palette
10796
- * bw.toggleTheme();
10797
- *
10798
- * // Generate CSS for static export (Node.js)
10799
- * var result = bw.generateTheme('sunset', {
10800
- * primary: '#e76f51',
10801
- * secondary: '#264653',
10802
- * inject: false
10803
- * });
10804
- * fs.writeFileSync('sunset.css', result.css + result.alternate.css);
10809
+ * var styles = bw.makeStyles({ primary: '#4f46e5', secondary: '#d97706' });
10810
+ * console.log(styles.palette.primary.base); // '#4f46e5'
10811
+ * // styles.css contains all themed CSS — nothing injected
10805
10812
  */
10806
- bw.generateTheme = function(name, config) {
10807
- if (!config || !config.primary || !config.secondary) {
10808
- throw new Error('bw.generateTheme requires config.primary and config.secondary');
10809
- }
10810
-
10811
- // Merge with defaults; if user didn't supply tertiary, default to their primary
10812
- var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config);
10813
- if (!config.tertiary) fullConfig.tertiary = fullConfig.primary;
10813
+ bw.makeStyles = function(config) {
10814
+ var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config || {});
10815
+ if (config && !config.tertiary) fullConfig.tertiary = fullConfig.primary;
10814
10816
 
10815
10817
  // Derive primary palette
10816
10818
  var palette = derivePalette(fullConfig);
@@ -10818,131 +10820,207 @@ bw.generateTheme = function(name, config) {
10818
10820
  // Resolve layout
10819
10821
  var layout = resolveLayout(fullConfig);
10820
10822
 
10821
- // Generate primary themed CSS rules
10822
- var themedRules = generateThemedCSS(name, palette, layout);
10823
+ // Generate primary themed CSS rules (unscoped)
10824
+ var themedRules = generateThemedCSS('', palette, layout);
10823
10825
  var cssStr = bw.css(themedRules);
10824
10826
 
10825
10827
  // Derive alternate palette (luminance-inverted)
10826
10828
  var altConfig = deriveAlternateConfig(fullConfig);
10827
10829
  var altPalette = derivePalette(altConfig);
10828
10830
 
10829
- // Generate alternate CSS scoped under .bw_theme_alt
10830
- var altRules = generateAlternateCSS(name, altPalette, layout);
10831
- var altCssStr = bw.css(altRules);
10831
+ // Generate alternate CSS rules WITHOUT .bw_theme_alt prefix (raw rules)
10832
+ // applyStyles() wraps them appropriately based on scope
10833
+ var altRawRules = generateThemedCSS('', altPalette, layout);
10834
+
10835
+ // Add body-level surface overrides for the alternate palette.
10836
+ // When .bw_theme_alt is on <html>, ".bw_theme_alt body" correctly matches.
10837
+ altRawRules['body'] = {
10838
+ 'color': altPalette.dark.base,
10839
+ 'background-color': altPalette.surface || altPalette.light.base
10840
+ };
10841
+
10842
+ var altCssStr = bw.css(altRawRules);
10832
10843
 
10833
10844
  // Determine if primary is light-flavored
10834
10845
  var lightPrimary = isLightPalette(fullConfig);
10835
10846
 
10836
- // Inject both CSS sets into DOM if requested
10837
- var shouldInject = config.inject !== false;
10838
- if (shouldInject && bw._isBrowser) {
10839
- var safeName = name ? name.replace(/-/g, '_') : '';
10840
- var styleId = safeName ? 'bw_theme_' + safeName : 'bw_theme_default';
10841
- var altStyleId = safeName ? 'bw_theme_' + safeName + '_alt' : 'bw_theme_default_alt';
10842
-
10843
- bw.injectCSS(cssStr, { id: styleId, append: false });
10844
- bw.injectCSS(altCssStr, { id: altStyleId, append: false });
10847
+ return {
10848
+ css: cssStr,
10849
+ alternateCss: altCssStr,
10850
+ rules: themedRules,
10851
+ alternateRules: altRawRules,
10852
+ palette: palette,
10853
+ alternatePalette: altPalette,
10854
+ isLightPrimary: lightPrimary
10855
+ };
10856
+ };
10845
10857
 
10846
- bw._activeThemeStyleIds = [styleId, altStyleId];
10858
+ /**
10859
+ * Inject styles into the DOM with optional scoping.
10860
+ *
10861
+ * Takes a styles object from `makeStyles()` and creates a single `<style>`
10862
+ * element in `<head>`. If a scope selector is provided, all CSS rules are
10863
+ * wrapped under that selector. Alternate CSS is wrapped under `.bw_theme_alt`.
10864
+ *
10865
+ * @param {Object} styles - Result of `bw.makeStyles()`
10866
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview'). Omit for global.
10867
+ * @returns {Element|null} The `<style>` element, or null in Node.js
10868
+ * @category CSS & Styling
10869
+ * @see bw.makeStyles
10870
+ * @see bw.loadStyles
10871
+ * @see bw.clearStyles
10872
+ * @example
10873
+ * var styles = bw.makeStyles({ primary: '#4f46e5' });
10874
+ * bw.applyStyles(styles); // global
10875
+ * bw.applyStyles(styles, '#my-dashboard'); // scoped
10876
+ */
10877
+ bw.applyStyles = function(styles, scope) {
10878
+ if (!bw._isBrowser) return null;
10879
+ if (!styles || !styles.rules) {
10880
+ _cw('bw.applyStyles: invalid styles object');
10881
+ return null;
10847
10882
  }
10848
10883
 
10849
- // Update bw.u color entries to reflect the palette
10850
- if (!name) {
10851
- bw.u.bgTeal = { background: palette.primary.base, color: palette.primary.textOn };
10852
- bw.u.textTeal = { color: palette.primary.base };
10853
- bw.u.bgWhite = { background: '#ffffff' };
10854
- bw.u.textWhite = { color: '#ffffff' };
10884
+ var styleId = _scopeToStyleId(scope);
10885
+
10886
+ // Scope the primary rules if a scope is provided
10887
+ var primaryRules = styles.rules;
10888
+ if (scope) {
10889
+ primaryRules = scopeRulesUnder(primaryRules, scope);
10855
10890
  }
10856
10891
 
10857
- // Store active theme state
10858
- var result = {
10859
- css: cssStr,
10860
- palette: palette,
10861
- name: name,
10862
- isLightPrimary: lightPrimary,
10863
- alternate: {
10864
- css: altCssStr,
10865
- palette: altPalette
10892
+ // Wrap alternate rules with .bw_theme_alt
10893
+ var altRules = styles.alternateRules;
10894
+ if (altRules) {
10895
+ if (scope) {
10896
+ // Scoped compound: #scope.bw_theme_alt .bw_card
10897
+ altRules = scopeRulesUnder(altRules, scope + '.bw_theme_alt');
10898
+ } else {
10899
+ // Global: .bw_theme_alt .bw_card
10900
+ altRules = scopeRulesUnder(altRules, '.bw_theme_alt');
10866
10901
  }
10867
- };
10868
- bw._activeTheme = result;
10869
- bw._activeThemeMode = 'primary';
10902
+ }
10870
10903
 
10871
- return result;
10904
+ // Combine primary + alternate into one CSS string
10905
+ var combined = bw.css(primaryRules);
10906
+ if (altRules) {
10907
+ combined += '\n' + bw.css(altRules);
10908
+ }
10909
+
10910
+ return bw.injectCSS(combined, { id: styleId, append: false });
10872
10911
  };
10873
10912
 
10874
10913
  /**
10875
- * Apply a theme mode. Switches between primary and alternate palettes
10876
- * by adding/removing the `bw_theme_alt` class on `<html>`.
10914
+ * Generate and apply styles in one call. Convenience wrapper.
10915
+ *
10916
+ * Equivalent to: `bw.applyStyles(bw.makeStyles(config), scope)`
10877
10917
  *
10878
- * @param {string} mode - 'primary' | 'alternate' | 'light' | 'dark'
10879
- * @returns {string} Active mode: 'primary' or 'alternate'
10918
+ * @param {Object} [config] - Style configuration (same as `makeStyles`)
10919
+ * @param {string} [scope] - Scope selector (same as `applyStyles`)
10920
+ * @returns {Element|null} The `<style>` element, or null in Node.js
10880
10921
  * @category CSS & Styling
10881
- * @see bw.generateTheme
10882
- * @see bw.toggleTheme
10922
+ * @see bw.makeStyles
10923
+ * @see bw.applyStyles
10883
10924
  * @example
10884
- * bw.applyTheme('alternate'); // switch to alternate palette
10885
- * bw.applyTheme('dark'); // switch to whichever palette is darker
10886
- * bw.applyTheme('primary'); // switch back to primary palette
10887
- */
10888
- bw.applyTheme = function(mode) {
10889
- if (!bw._isBrowser) return mode || 'primary';
10890
- var root = document.documentElement;
10891
- var isLight = bw._activeTheme ? bw._activeTheme.isLightPrimary : true;
10892
-
10893
- var wantAlt;
10894
- if (mode === 'primary') wantAlt = false;
10895
- else if (mode === 'alternate') wantAlt = true;
10896
- else if (mode === 'light') wantAlt = !isLight;
10897
- else if (mode === 'dark') wantAlt = isLight;
10898
- else wantAlt = false;
10899
-
10900
- if (wantAlt) {
10901
- root.classList.add('bw_theme_alt');
10902
- } else {
10903
- root.classList.remove('bw_theme_alt');
10925
+ * bw.loadStyles(); // defaults, global
10926
+ * bw.loadStyles({ primary: '#4f46e5' }); // custom, global
10927
+ * bw.loadStyles({ primary: '#4f46e5' }, '#my-dashboard'); // custom, scoped
10928
+ */
10929
+ bw.loadStyles = function(config, scope) {
10930
+ // Also inject structural CSS first (only once)
10931
+ if (bw._isBrowser) {
10932
+ var existing = document.getElementById('bw_structural');
10933
+ if (!existing) {
10934
+ var structuralCSS = bw.css(getStructuralStyles());
10935
+ bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false });
10936
+ }
10904
10937
  }
10938
+ return bw.applyStyles(bw.makeStyles(config), scope);
10939
+ };
10905
10940
 
10906
- bw._activeThemeMode = wantAlt ? 'alternate' : 'primary';
10907
- return bw._activeThemeMode;
10941
+ /**
10942
+ * Inject the CSS reset (box-sizing, html/body font, reduced-motion).
10943
+ * Idempotent — if already injected, returns the existing `<style>` element.
10944
+ *
10945
+ * @returns {Element|null} The `<style>` element, or null in Node.js
10946
+ * @category CSS & Styling
10947
+ * @see bw.loadStyles
10948
+ * @see bw.clearStyles
10949
+ * @example
10950
+ * bw.loadReset(); // inject once, safe to call multiple times
10951
+ */
10952
+ bw.loadReset = function() {
10953
+ if (!bw._isBrowser) return null;
10954
+ var existing = document.getElementById('bw_style_reset');
10955
+ if (existing) return existing;
10956
+ return bw.injectCSS(bw.css(getResetStyles()), { id: 'bw_style_reset', append: false });
10908
10957
  };
10909
10958
 
10910
10959
  /**
10911
- * Toggle between primary and alternate theme palettes.
10960
+ * Toggle between primary and alternate palettes.
10961
+ *
10962
+ * Adds/removes the `bw_theme_alt` class on the scoping element.
10963
+ * Without a scope, toggles on `<html>` (global).
10964
+ * With a scope, toggles on the first matching element.
10912
10965
  *
10966
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard'). Omit for global.
10913
10967
  * @returns {string} Active mode after toggle: 'primary' or 'alternate'
10914
10968
  * @category CSS & Styling
10915
- * @see bw.applyTheme
10916
- * @see bw.generateTheme
10969
+ * @see bw.applyStyles
10970
+ * @see bw.clearStyles
10917
10971
  * @example
10918
- * bw.toggleTheme(); // flip between primary and alternate
10972
+ * bw.toggleStyles(); // global toggle on <html>
10973
+ * bw.toggleStyles('#my-dashboard'); // scoped toggle
10919
10974
  */
10920
- bw.toggleTheme = function() {
10921
- var current = bw._activeThemeMode || 'primary';
10922
- return bw.applyTheme(current === 'primary' ? 'alternate' : 'primary');
10975
+ bw.toggleStyles = function(scope) {
10976
+ if (!bw._isBrowser) return 'primary';
10977
+ var target;
10978
+ if (scope) {
10979
+ var els = bw.$(scope);
10980
+ target = els[0];
10981
+ } else {
10982
+ target = document.documentElement;
10983
+ }
10984
+ if (!target) return 'primary';
10985
+
10986
+ var hasAlt = target.classList.contains('bw_theme_alt');
10987
+ if (hasAlt) {
10988
+ target.classList.remove('bw_theme_alt');
10989
+ return 'primary';
10990
+ } else {
10991
+ target.classList.add('bw_theme_alt');
10992
+ return 'alternate';
10993
+ }
10923
10994
  };
10924
10995
 
10925
10996
  /**
10926
- * Remove the currently active theme's injected style elements from the DOM.
10927
- * Use this before generating a new theme with a different name to prevent
10928
- * stale CSS accumulation.
10997
+ * Remove injected styles for a given scope.
10998
+ *
10999
+ * Finds the `<style>` element by id and removes it. Also removes
11000
+ * the `bw_theme_alt` class from the relevant element.
10929
11001
  *
11002
+ * @param {string} [scope] - Scope selector. Omit to remove global styles.
10930
11003
  * @category CSS & Styling
10931
- * @see bw.generateTheme
11004
+ * @see bw.applyStyles
11005
+ * @see bw.loadStyles
10932
11006
  * @example
10933
- * bw.clearTheme(); // remove current theme styles
10934
- * bw.generateTheme('sunset', conf); // inject fresh theme
10935
- */
10936
- bw.clearTheme = function() {
10937
- if (bw._activeThemeStyleIds && bw._isBrowser) {
10938
- bw._activeThemeStyleIds.forEach(function(id) {
10939
- var el = document.getElementById(id);
10940
- if (el) el.remove();
10941
- });
10942
- bw._activeThemeStyleIds = null;
11007
+ * bw.clearStyles(); // remove global styles
11008
+ * bw.clearStyles('#my-dashboard'); // remove scoped styles
11009
+ * bw.clearStyles('reset'); // remove the CSS reset
11010
+ */
11011
+ bw.clearStyles = function(scope) {
11012
+ if (!bw._isBrowser) return;
11013
+ var styleId = _scopeToStyleId(scope);
11014
+ var el = document.getElementById(styleId);
11015
+ if (el) el.remove();
11016
+
11017
+ // Also remove bw_theme_alt from the relevant element
11018
+ if (scope && scope !== 'reset' && scope !== 'global') {
11019
+ var targets = bw.$(scope);
11020
+ if (targets[0]) targets[0].classList.remove('bw_theme_alt');
11021
+ } else if (!scope || scope === 'global') {
11022
+ document.documentElement.classList.remove('bw_theme_alt');
10943
11023
  }
10944
- bw._activeTheme = null;
10945
- bw._activeThemeMode = 'primary';
10946
11024
  };
10947
11025
 
10948
11026
  // Expose color utility functions on bw namespace
@@ -11165,10 +11243,15 @@ bw.copyToClipboard = function(text) {
11165
11243
  * @param {Object} config - Table configuration
11166
11244
  * @param {Array<Object>} config.data - Array of row objects to display
11167
11245
  * @param {Array<Object>} [config.columns] - Column definitions with key, label, render
11168
- * @param {string} [config.className='table'] - CSS class for table element
11246
+ * @param {string} [config.className=''] - Additional CSS classes for table element
11169
11247
  * @param {boolean} [config.sortable=true] - Enable click-to-sort headers
11170
11248
  * @param {Function} [config.onSort] - Sort callback (column, direction)
11171
- * @returns {Object} TACO object for table
11249
+ * @param {boolean} [config.selectable=false] - Enable row selection on click
11250
+ * @param {Function} [config.onRowClick] - Row click callback (row, index, event)
11251
+ * @param {number} [config.pageSize] - Rows per page (enables pagination when set)
11252
+ * @param {number} [config.currentPage=1] - Current page number (1-based)
11253
+ * @param {Function} [config.onPageChange] - Page change callback (newPage)
11254
+ * @returns {Object} TACO object for table (with optional pagination controls)
11172
11255
  * @category Component Builders
11173
11256
  * @see bw.makeDataTable
11174
11257
  * @example
@@ -11180,7 +11263,12 @@ bw.copyToClipboard = function(text) {
11180
11263
  * columns: [
11181
11264
  * { key: 'name', label: 'Name' },
11182
11265
  * { key: 'age', label: 'Age' }
11183
- * ]
11266
+ * ],
11267
+ * selectable: true,
11268
+ * onRowClick: function(row, i) { console.log('clicked', row.name); },
11269
+ * pageSize: 10,
11270
+ * currentPage: 1,
11271
+ * onPageChange: function(page) { console.log('page', page); }
11184
11272
  * });
11185
11273
  */
11186
11274
  bw.makeTable = function(config) {
@@ -11193,41 +11281,47 @@ bw.makeTable = function(config) {
11193
11281
  sortable = true,
11194
11282
  onSort,
11195
11283
  sortColumn,
11196
- sortDirection = 'asc'
11284
+ sortDirection = 'asc',
11285
+ selectable = false,
11286
+ onRowClick,
11287
+ pageSize,
11288
+ currentPage = 1,
11289
+ onPageChange
11197
11290
  } = config;
11198
11291
 
11199
- // Build class list: always include bw_table, add striped/hover, append user className
11292
+ // Build class list: always include bw_table, add striped/hover/selectable, append user className
11200
11293
  let cls = 'bw_table';
11201
11294
  if (striped) cls += ' bw_table_striped';
11202
- if (hover) cls += ' bw_table_hover';
11295
+ if (hover || selectable) cls += ' bw_table_hover';
11296
+ if (selectable) cls += ' bw_table_selectable';
11203
11297
  if (className) cls += ' ' + className;
11204
11298
  cls = cls.trim();
11205
-
11299
+
11206
11300
  // Auto-detect columns if not provided
11207
- const cols = columns || (data.length > 0
11301
+ const cols = columns || (data.length > 0
11208
11302
  ? _keys(data[0]).map(key => ({ key, label: key }))
11209
11303
  : []);
11210
-
11304
+
11211
11305
  // Current sort state
11212
11306
  let currentSortColumn = sortColumn || null;
11213
11307
  let currentSortDirection = sortDirection;
11214
-
11308
+
11215
11309
  // Sort data if column specified
11216
11310
  let sortedData = [...data];
11217
11311
  if (currentSortColumn) {
11218
11312
  sortedData.sort((a, b) => {
11219
11313
  const aVal = a[currentSortColumn];
11220
11314
  const bVal = b[currentSortColumn];
11221
-
11315
+
11222
11316
  // Handle different types
11223
11317
  if (_is(aVal, 'number') && _is(bVal, 'number')) {
11224
11318
  return currentSortDirection === 'asc' ? aVal - bVal : bVal - aVal;
11225
11319
  }
11226
-
11320
+
11227
11321
  // String comparison
11228
11322
  const aStr = String(aVal || '').toLowerCase();
11229
11323
  const bStr = String(bVal || '').toLowerCase();
11230
-
11324
+
11231
11325
  if (currentSortDirection === 'asc') {
11232
11326
  return aStr.localeCompare(bStr);
11233
11327
  } else {
@@ -11235,23 +11329,32 @@ bw.makeTable = function(config) {
11235
11329
  }
11236
11330
  });
11237
11331
  }
11238
-
11332
+
11333
+ // Pagination
11334
+ const totalRows = sortedData.length;
11335
+ const totalPages = pageSize ? Math.max(1, Math.ceil(totalRows / pageSize)) : 1;
11336
+ const page = Math.max(1, Math.min(currentPage, totalPages));
11337
+ if (pageSize) {
11338
+ const start = (page - 1) * pageSize;
11339
+ sortedData = sortedData.slice(start, start + pageSize);
11340
+ }
11341
+
11239
11342
  // Create sort handler
11240
11343
  const handleSort = (column) => {
11241
11344
  if (!sortable) return;
11242
-
11345
+
11243
11346
  if (currentSortColumn === column) {
11244
11347
  currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
11245
11348
  } else {
11246
11349
  currentSortColumn = column;
11247
11350
  currentSortDirection = 'asc';
11248
11351
  }
11249
-
11352
+
11250
11353
  if (onSort) {
11251
11354
  onSort(column, currentSortDirection);
11252
11355
  }
11253
11356
  };
11254
-
11357
+
11255
11358
  // Build table header
11256
11359
  const thead = {
11257
11360
  t: 'thead',
@@ -11274,24 +11377,87 @@ bw.makeTable = function(config) {
11274
11377
  }))
11275
11378
  }
11276
11379
  };
11277
-
11278
- // Build table body
11380
+
11381
+ // Build table body with selectable/onRowClick support
11279
11382
  const tbody = {
11280
11383
  t: 'tbody',
11281
- c: sortedData.map(row => ({
11282
- t: 'tr',
11283
- c: cols.map(col => ({
11284
- t: 'td',
11285
- c: col.render ? col.render(row[col.key], row) : String(row[col.key] || '')
11286
- }))
11287
- }))
11384
+ c: sortedData.map((row, idx) => {
11385
+ const globalIdx = pageSize ? (page - 1) * pageSize + idx : idx;
11386
+ const rowAttrs = {};
11387
+ if (selectable || onRowClick) {
11388
+ rowAttrs.style = 'cursor:pointer;';
11389
+ rowAttrs.onclick = function(e) {
11390
+ if (selectable) {
11391
+ // Toggle selected class on this row
11392
+ var tr = e.currentTarget;
11393
+ tr.classList.toggle('bw_table_row_selected');
11394
+ }
11395
+ if (onRowClick) {
11396
+ onRowClick(row, globalIdx, e);
11397
+ }
11398
+ };
11399
+ }
11400
+ return {
11401
+ t: 'tr',
11402
+ a: rowAttrs,
11403
+ c: cols.map(col => ({
11404
+ t: 'td',
11405
+ c: col.render ? col.render(row[col.key], row) : String(row[col.key] || '')
11406
+ }))
11407
+ };
11408
+ })
11288
11409
  };
11289
-
11290
- return {
11410
+
11411
+ const table = {
11291
11412
  t: 'table',
11292
11413
  a: { class: cls },
11293
11414
  c: [thead, tbody]
11294
11415
  };
11416
+
11417
+ // If no pagination, return table directly
11418
+ if (!pageSize) return table;
11419
+
11420
+ // Build pagination controls
11421
+ const pageButtons = [];
11422
+ // Previous button
11423
+ pageButtons.push({
11424
+ t: 'button',
11425
+ a: {
11426
+ class: 'bw_btn bw_btn_sm',
11427
+ disabled: page <= 1 ? 'disabled' : undefined,
11428
+ onclick: page > 1 && onPageChange ? function() { onPageChange(page - 1); } : undefined
11429
+ },
11430
+ c: 'Prev'
11431
+ });
11432
+ // Page info
11433
+ pageButtons.push({
11434
+ t: 'span',
11435
+ a: { style: 'margin:0 0.5rem;font-size:0.875rem;' },
11436
+ c: 'Page ' + page + ' of ' + totalPages
11437
+ });
11438
+ // Next button
11439
+ pageButtons.push({
11440
+ t: 'button',
11441
+ a: {
11442
+ class: 'bw_btn bw_btn_sm',
11443
+ disabled: page >= totalPages ? 'disabled' : undefined,
11444
+ onclick: page < totalPages && onPageChange ? function() { onPageChange(page + 1); } : undefined
11445
+ },
11446
+ c: 'Next'
11447
+ });
11448
+
11449
+ return {
11450
+ t: 'div',
11451
+ a: { class: 'bw_table_paginated' },
11452
+ c: [
11453
+ table,
11454
+ {
11455
+ t: 'div',
11456
+ a: { class: 'bw_table_pagination', style: 'display:flex;align-items:center;justify-content:flex-end;padding:0.5rem 0;gap:0.25rem;' },
11457
+ c: pageButtons
11458
+ }
11459
+ ]
11460
+ };
11295
11461
  };
11296
11462
 
11297
11463
  /**
@@ -11869,5 +12035,5 @@ if (bw._isBrowser && typeof window !== 'undefined') {
11869
12035
  window.bw = bw;
11870
12036
  }
11871
12037
 
11872
- export { bw as default };
12038
+ export { BCCL, bw as default, make, makeAccordion, makeAlert, makeAvatar, makeBadge, makeBreadcrumb, makeButton, makeButtonGroup, makeCTA, makeCard, makeCarousel, makeCheckbox, makeChipInput, makeCodeDemo, makeCol, makeContainer, makeDropdown, makeFeatureGrid, makeFileUpload, makeForm, makeFormGroup, makeHero, makeInput, makeListGroup, makeMediaObject, makeModal, makeNav, makeNavbar, makePagination, makePopover, makeProgress, makeRadio, makeRange, makeRow, makeSearchInput, makeSection, makeSelect, makeSkeleton, makeSpinner, makeStack, makeStatCard, makeStepper, makeSwitch, makeTabs, makeTextarea, makeTimeline, makeToast, makeTooltip, variantClass };
11873
12039
  //# sourceMappingURL=bitwrench.esm.js.map