bitwrench 2.0.12 → 2.0.14

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.
@@ -1,4 +1,4 @@
1
- /*! bitwrench-lean v2.0.12 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
1
+ /*! bitwrench-lean v2.0.14 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
2
2
  (function (global, factory) {
3
3
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
4
4
  typeof define === 'function' && define.amd ? define(factory) :
@@ -189,14 +189,14 @@
189
189
  */
190
190
 
191
191
  var VERSION_INFO = {
192
- version: '2.0.12',
192
+ version: '2.0.14',
193
193
  name: 'bitwrench',
194
194
  description: 'A library for javascript UI functions.',
195
195
  license: 'BSD-2-Clause',
196
196
  homepage: 'https://deftio.github.com/bitwrench/pages',
197
197
  repository: 'git+https://github.com/deftio/bitwrench.git',
198
198
  author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
199
- buildDate: '2026-03-07T22:31:35.755Z'
199
+ buildDate: '2026-03-08T08:04:06.572Z'
200
200
  };
201
201
 
202
202
  /**
@@ -445,6 +445,28 @@
445
445
  return relativeLuminance(hex) > 0.179 ? '#000' : '#fff';
446
446
  }
447
447
 
448
+ /**
449
+ * Shift a color's hue toward a target hue by a given amount.
450
+ * Uses shortest-arc interpolation on the hue wheel.
451
+ * @param {string} sourceHex - Color to shift
452
+ * @param {string} targetHex - Color whose hue to shift toward
453
+ * @param {number} [amount=0.20] - 0 = no shift, 1 = full shift to target hue
454
+ * @returns {string} Harmonized hex color
455
+ */
456
+ function harmonize(sourceHex, targetHex, amount) {
457
+ if (amount === undefined) amount = 0.20;
458
+ if (amount === 0) return sourceHex;
459
+ var srcHsl = hexToHsl(sourceHex);
460
+ var tgtHsl = hexToHsl(targetHex);
461
+
462
+ // Shortest-arc hue interpolation
463
+ var diff = tgtHsl[0] - srcHsl[0];
464
+ if (diff > 180) diff -= 360;
465
+ if (diff < -180) diff += 360;
466
+ var newHue = (srcHsl[0] + diff * amount + 360) % 360;
467
+ return hslToHex([newHue, srcHsl[1], srcHsl[2]]);
468
+ }
469
+
448
470
  /**
449
471
  * Derive a full shade palette for a single semantic color.
450
472
  * @param {string} hex - Base color hex
@@ -465,29 +487,126 @@
465
487
  }
466
488
 
467
489
  /**
468
- * Derive complete palette from a theme config object.
469
- * @param {Object} config - Theme config with primary, secondary, tertiary, etc.
470
- * @returns {Object} Full palette with shades for all 8 semantic colors + tertiary
490
+ * Derive the alternate (luminance-inverted) version of a single seed color.
491
+ * Preserves hue, mirrors lightness, adjusts saturation for readability.
492
+ * @param {string} hex - Seed hex color
493
+ * @returns {string} Alternate hex color
471
494
  */
472
- function derivePalette(config) {
473
- var defaults = {
495
+ function deriveAlternateSeed(hex) {
496
+ var hsl = hexToHsl(hex);
497
+ var h = hsl[0],
498
+ s = hsl[1],
499
+ l = hsl[2];
500
+ var altL, altS;
501
+ if (l > 50) {
502
+ // Light color → make dark. Map 50-100 → 30-10 range
503
+ altL = clip(100 - l - 10, 8, 40);
504
+ // Reduce saturation slightly — vivid colors at low lightness look garish
505
+ altS = clip(s * 0.85, 0, 100);
506
+ } else {
507
+ // Dark color → make light. Map 0-50 → 65-92 range
508
+ altL = clip(100 - l + 10, 60, 92);
509
+ // Slightly increase saturation for light variant
510
+ altS = clip(s * 1.1, 0, 100);
511
+ }
512
+ return hslToHex([h, altS, altL]);
513
+ }
514
+
515
+ /**
516
+ * Determine whether a palette config is "light-flavored" based on
517
+ * the average luminance of its seed colors.
518
+ * @param {Object} config - Theme config with primary, secondary hex colors
519
+ * @returns {boolean} true if the seeds are predominantly light
520
+ */
521
+ function isLightPalette(config) {
522
+ var lum = relativeLuminance(config.primary);
523
+ if (config.secondary) lum = (lum + relativeLuminance(config.secondary)) / 2;
524
+ if (config.tertiary) lum = (lum * 2 + relativeLuminance(config.tertiary)) / 3;
525
+ return lum > 0.179;
526
+ }
527
+
528
+ /**
529
+ * Derive a complete alternate config from a primary theme config.
530
+ * Each seed color is luminance-inverted; semantic colors are adjusted for
531
+ * the new luminance context.
532
+ * @param {Object} config - Primary theme config
533
+ * @returns {Object} Alternate theme config (same shape, inverted lightness)
534
+ */
535
+ function deriveAlternateConfig(config) {
536
+ var alt = {};
537
+ // Invert the user's seed colors
538
+ alt.primary = deriveAlternateSeed(config.primary);
539
+ alt.secondary = deriveAlternateSeed(config.secondary);
540
+ alt.tertiary = config.tertiary ? deriveAlternateSeed(config.tertiary) : alt.primary;
541
+
542
+ // Derive alternate surface colors from primary hue
543
+ var priHsl = hexToHsl(config.primary);
544
+ var h = priHsl[0];
545
+ var isLight = isLightPalette(config);
546
+ if (isLight) {
547
+ // Primary is light → alternate needs dark surfaces
548
+ alt.light = hslToHex([h, Math.min(priHsl[1], 15), 15]);
549
+ alt.dark = hslToHex([h, 5, 88]);
550
+ } else {
551
+ // Primary is dark → alternate needs light surfaces
552
+ alt.light = hslToHex([h, Math.min(priHsl[1], 10), 96]);
553
+ alt.dark = hslToHex([h, 10, 18]);
554
+ }
555
+
556
+ // Semantic colors: harmonize toward primary, then invert for alternate
557
+ var amt = config.harmonize !== undefined ? config.harmonize : 0.20;
558
+ var semanticDefaults = {
474
559
  success: '#198754',
475
560
  danger: '#dc3545',
476
- warning: '#ffc107',
477
- info: '#0dcaf0',
478
- light: '#f8f9fa',
479
- dark: '#212529'
561
+ warning: '#f0ad4e',
562
+ info: '#17a2b8'
480
563
  };
564
+ var semantics = ['success', 'danger', 'warning', 'info'];
565
+ for (var i = 0; i < semantics.length; i++) {
566
+ var key = semantics[i];
567
+ var seed = config[key] || semanticDefaults[key];
568
+ var harmonized = harmonize(seed, config.primary, amt);
569
+ alt[key] = deriveAlternateSeed(harmonized);
570
+ }
571
+
572
+ // Semantic colors are already harmonized+inverted — don't re-harmonize in derivePalette
573
+ alt.harmonize = 0;
574
+ return alt;
575
+ }
576
+
577
+ /**
578
+ * Derive complete palette from a theme config object.
579
+ * Semantic colors are harmonized toward the primary hue (configurable).
580
+ * Light/dark surface colors are tinted with the primary hue.
581
+ * @param {Object} config - Theme config with primary, secondary, tertiary, etc.
582
+ * @param {number} [config.harmonize=0.20] - Hue shift amount for semantic colors (0-1)
583
+ * @returns {Object} Full palette with shades for all 9 semantic colors
584
+ */
585
+ function derivePalette(config) {
586
+ var amt = config.harmonize !== undefined ? config.harmonize : 0.20;
587
+ var pri = config.primary;
588
+ var priHsl = hexToHsl(pri);
589
+ var h = priHsl[0];
590
+
591
+ // Semantic defaults — harmonized toward primary hue
592
+ var successBase = harmonize(config.success || '#198754', pri, amt);
593
+ var dangerBase = harmonize(config.danger || '#dc3545', pri, amt);
594
+ var warningBase = harmonize(config.warning || '#f0ad4e', pri, amt);
595
+ var infoBase = harmonize(config.info || '#17a2b8', pri, amt);
596
+
597
+ // Light/dark: derive from primary hue with low saturation (if not user-supplied)
598
+ var lightBase = config.light || hslToHex([h, 8, 97]);
599
+ var darkBase = config.dark || hslToHex([h, 10, 13]);
481
600
  var palette = {
482
601
  primary: deriveShades(config.primary),
483
602
  secondary: deriveShades(config.secondary),
484
603
  tertiary: deriveShades(config.tertiary),
485
- success: deriveShades(config.success || defaults.success),
486
- danger: deriveShades(config.danger || defaults.danger),
487
- warning: deriveShades(config.warning || defaults.warning),
488
- info: deriveShades(config.info || defaults.info),
489
- light: deriveShades(config.light || defaults.light),
490
- dark: deriveShades(config.dark || defaults.dark)
604
+ success: deriveShades(successBase),
605
+ danger: deriveShades(dangerBase),
606
+ warning: deriveShades(warningBase),
607
+ info: deriveShades(infoBase),
608
+ light: deriveShades(lightBase),
609
+ dark: deriveShades(darkBase)
491
610
  };
492
611
  return palette;
493
612
  }
@@ -559,6 +678,88 @@
559
678
  }
560
679
  };
561
680
 
681
+ // ---- Typography scale presets ----
682
+
683
+ var TYPE_RATIO_PRESETS = {
684
+ tight: 1.125,
685
+ normal: 1.200,
686
+ relaxed: 1.250,
687
+ dramatic: 1.333
688
+ };
689
+
690
+ /**
691
+ * Generate a modular type scale from a base size and ratio.
692
+ * @param {number} base - Base font size in px (default 16)
693
+ * @param {number} ratio - Scale ratio (default 1.200)
694
+ * @returns {Object} { xs, sm, base, lg, xl, '2xl', '3xl', '4xl' } in px
695
+ */
696
+ function generateTypeScale(base, ratio) {
697
+ if (!base) base = 16;
698
+ if (!ratio) ratio = 1.200;
699
+ return {
700
+ xs: Math.round(base / (ratio * ratio)),
701
+ sm: Math.round(base / ratio),
702
+ base: base,
703
+ lg: Math.round(base * ratio),
704
+ xl: Math.round(base * ratio * ratio),
705
+ '2xl': Math.round(base * Math.pow(ratio, 3)),
706
+ '3xl': Math.round(base * Math.pow(ratio, 4)),
707
+ '4xl': Math.round(base * Math.pow(ratio, 5))
708
+ };
709
+ }
710
+
711
+ // ---- Elevation (shadow depth) presets ----
712
+
713
+ var ELEVATION_PRESETS = {
714
+ flat: {
715
+ sm: 'none',
716
+ md: 'none',
717
+ lg: 'none',
718
+ xl: 'none'
719
+ },
720
+ sm: {
721
+ sm: '0 1px 2px rgba(0,0,0,0.05)',
722
+ md: '0 1px 3px rgba(0,0,0,0.08)',
723
+ lg: '0 2px 6px rgba(0,0,0,0.10)',
724
+ xl: '0 4px 12px rgba(0,0,0,0.12)'
725
+ },
726
+ md: {
727
+ sm: '0 1px 3px rgba(0,0,0,0.08)',
728
+ md: '0 2px 6px rgba(0,0,0,0.12)',
729
+ lg: '0 4px 12px rgba(0,0,0,0.16)',
730
+ xl: '0 8px 24px rgba(0,0,0,0.20)'
731
+ },
732
+ lg: {
733
+ sm: '0 2px 4px rgba(0,0,0,0.10)',
734
+ md: '0 4px 12px rgba(0,0,0,0.16)',
735
+ lg: '0 8px 24px rgba(0,0,0,0.22)',
736
+ xl: '0 16px 48px rgba(0,0,0,0.28)'
737
+ }
738
+ };
739
+
740
+ // ---- Motion (transition) presets ----
741
+
742
+ var MOTION_PRESETS = {
743
+ reduced: {
744
+ fast: '0ms',
745
+ normal: '0ms',
746
+ slow: '0ms',
747
+ easing: 'linear'
748
+ },
749
+ standard: {
750
+ fast: '100ms',
751
+ normal: '200ms',
752
+ slow: '300ms',
753
+ easing: 'ease-out'
754
+ },
755
+ expressive: {
756
+ fast: '150ms',
757
+ normal: '300ms',
758
+ slow: '500ms',
759
+ easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)'
760
+ }
761
+ };
762
+
562
763
  /**
563
764
  * Default palette config — matches existing hardcoded colors
564
765
  */
@@ -568,8 +769,8 @@
568
769
  tertiary: '#006666',
569
770
  success: '#198754',
570
771
  danger: '#dc3545',
571
- warning: '#ffc107',
572
- info: '#0dcaf0',
772
+ warning: '#b38600',
773
+ info: '#0891b2',
573
774
  light: '#f8f9fa',
574
775
  dark: '#212529'
575
776
  };
@@ -666,18 +867,31 @@
666
867
  };
667
868
 
668
869
  /**
669
- * Resolve layout config to spacing + radius objects
670
- * @param {Object} config - { spacing, radius, fontSize }
671
- * @returns {Object} { spacing, radius, fontSize }
870
+ * Resolve layout config to spacing, radius, typeScale, elevation, and motion objects.
871
+ * @param {Object} config - { spacing, radius, fontSize, typeRatio, elevation, motion }
872
+ * @returns {Object} { spacing, radius, fontSize, typeScale, elevation, motion }
672
873
  */
673
874
  function resolveLayout(config) {
674
875
  var sp = config && config.spacing || 'normal';
675
876
  var rd = config && config.radius || 'md';
676
877
  var fs = config && config.fontSize || 1.0;
878
+
879
+ // typeRatio: accept preset name or number
880
+ var tr = config && config.typeRatio || 'normal';
881
+ var ratioNum = typeof tr === 'string' ? TYPE_RATIO_PRESETS[tr] || TYPE_RATIO_PRESETS.normal : tr;
882
+
883
+ // elevation: accept preset name or object
884
+ var el = config && config.elevation || 'md';
885
+
886
+ // motion: accept preset name or object
887
+ var mo = config && config.motion || 'standard';
677
888
  return {
678
889
  spacing: typeof sp === 'string' ? SPACING_PRESETS[sp] || SPACING_PRESETS.normal : sp,
679
890
  radius: typeof rd === 'string' ? RADIUS_PRESETS[rd] || RADIUS_PRESETS.md : rd,
680
- fontSize: fs
891
+ fontSize: fs,
892
+ typeScale: generateTypeScale(16, ratioNum),
893
+ elevation: typeof el === 'string' ? ELEVATION_PRESETS[el] || ELEVATION_PRESETS.md : el,
894
+ motion: typeof mo === 'string' ? MOTION_PRESETS[mo] || MOTION_PRESETS.standard : mo
681
895
  };
682
896
  }
683
897
 
@@ -703,12 +917,13 @@
703
917
  // Themed CSS generators
704
918
  // =========================================================================
705
919
 
706
- function generateTypographyThemed(scope, palette) {
920
+ function generateTypographyThemed(scope, palette, layout) {
921
+ var mot = layout.motion;
707
922
  var rules = {};
708
923
  rules[scopeSelector(scope, 'a')] = {
709
924
  'color': palette.primary.base,
710
925
  'text-decoration': 'none',
711
- 'transition': 'color 0.15s'
926
+ 'transition': 'color ' + mot.fast + ' ' + mot.easing
712
927
  };
713
928
  rules[scopeSelector(scope, 'a:hover')] = {
714
929
  'color': palette.primary.hover,
@@ -727,7 +942,8 @@
727
942
  'border-radius': rd.btn
728
943
  };
729
944
  rules[scopeSelector(scope, '.bw-btn:focus-visible')] = {
730
- 'outline': '0',
945
+ 'outline': '2px solid currentColor',
946
+ 'outline-offset': '2px',
731
947
  'box-shadow': '0 0 0 3px ' + palette.primary.focus
732
948
  };
733
949
 
@@ -809,14 +1025,15 @@
809
1025
  var rules = {};
810
1026
  var sp = layout.spacing;
811
1027
  var rd = layout.radius;
1028
+ var elev = layout.elevation;
812
1029
  rules[scopeSelector(scope, '.bw-card')] = {
813
1030
  'background-color': '#fff',
814
1031
  'border': '1px solid ' + palette.light.border,
815
1032
  'border-radius': rd.card,
816
- 'box-shadow': '0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04)'
1033
+ 'box-shadow': elev.sm
817
1034
  };
818
1035
  rules[scopeSelector(scope, '.bw-card:hover')] = {
819
- 'box-shadow': '0 4px 12px rgba(0,0,0,.1), 0 2px 4px rgba(0,0,0,.06)'
1036
+ 'box-shadow': elev.md
820
1037
  };
821
1038
  rules[scopeSelector(scope, '.bw-card-body')] = {
822
1039
  'padding': sp.card
@@ -862,6 +1079,8 @@
862
1079
  };
863
1080
  rules[scopeSelector(scope, '.bw-form-control:focus')] = {
864
1081
  'border-color': palette.primary.border,
1082
+ 'outline': '2px solid ' + palette.primary.base,
1083
+ 'outline-offset': '-1px',
865
1084
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
866
1085
  };
867
1086
  rules[scopeSelector(scope, '.bw-form-control::placeholder')] = {
@@ -1009,7 +1228,8 @@
1009
1228
  'border-color': palette.light.border
1010
1229
  };
1011
1230
  rules[scopeSelector(scope, '.bw-page-link:focus')] = {
1012
- 'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
1231
+ 'outline': '2px solid ' + palette.primary.base,
1232
+ 'outline-offset': '-2px'
1013
1233
  };
1014
1234
  rules[scopeSelector(scope, '.bw-page-item.bw-active .bw-page-link')] = {
1015
1235
  'color': palette.primary.textOn,
@@ -1165,12 +1385,12 @@
1165
1385
  };
1166
1386
  return rules;
1167
1387
  }
1168
- function generateModalThemed(scope, palette) {
1388
+ function generateModalThemed(scope, palette, layout) {
1169
1389
  var rules = {};
1170
1390
  rules[scopeSelector(scope, '.bw-modal-content')] = {
1171
1391
  'background-color': '#fff',
1172
1392
  'border-color': palette.light.border,
1173
- 'box-shadow': '0 0.5rem 1rem rgba(0,0,0,0.15)'
1393
+ 'box-shadow': layout.elevation.lg
1174
1394
  };
1175
1395
  rules[scopeSelector(scope, '.bw-modal-header')] = {
1176
1396
  'border-bottom-color': palette.light.border
@@ -1183,12 +1403,12 @@
1183
1403
  };
1184
1404
  return rules;
1185
1405
  }
1186
- function generateToastThemed(scope, palette) {
1406
+ function generateToastThemed(scope, palette, layout) {
1187
1407
  var rules = {};
1188
1408
  rules[scopeSelector(scope, '.bw-toast')] = {
1189
1409
  'background-color': '#fff',
1190
1410
  'border-color': 'rgba(0,0,0,0.1)',
1191
- 'box-shadow': '0 0.5rem 1rem rgba(0,0,0,0.15)'
1411
+ 'box-shadow': layout.elevation.lg
1192
1412
  };
1193
1413
  rules[scopeSelector(scope, '.bw-toast-header')] = {
1194
1414
  'border-bottom-color': 'rgba(0,0,0,0.05)'
@@ -1201,12 +1421,12 @@
1201
1421
  });
1202
1422
  return rules;
1203
1423
  }
1204
- function generateDropdownThemed(scope, palette) {
1424
+ function generateDropdownThemed(scope, palette, layout) {
1205
1425
  var rules = {};
1206
1426
  rules[scopeSelector(scope, '.bw-dropdown-menu')] = {
1207
1427
  'background-color': '#fff',
1208
1428
  'border-color': palette.light.border,
1209
- 'box-shadow': '0 0.5rem 1rem rgba(0,0,0,0.15)'
1429
+ 'box-shadow': layout.elevation.md
1210
1430
  };
1211
1431
  rules[scopeSelector(scope, '.bw-dropdown-item')] = {
1212
1432
  'color': palette.dark.base
@@ -1267,7 +1487,7 @@
1267
1487
  * @returns {Object} CSS rules object
1268
1488
  */
1269
1489
  function generateThemedCSS(scopeName, palette, layout) {
1270
- return Object.assign({}, generateResetThemed(scopeName, palette), generateTypographyThemed(scopeName, palette), generateButtons(scopeName, palette, layout), generateAlerts(scopeName, palette, layout), generateBadges(scopeName, palette), generateCards(scopeName, palette, layout), generateForms(scopeName, palette, layout), generateNavigation(scopeName, palette), generateTables(scopeName, palette, layout), generateTabs(scopeName, palette), generateListGroups(scopeName, palette, layout), generatePagination(scopeName, palette), generateProgress(scopeName, palette), generateHero(scopeName, palette), generateBreadcrumbThemed(scopeName, palette), generateSpinnerThemed(scopeName, palette), generateCloseButtonThemed(scopeName, palette), generateSectionsThemed(scopeName, palette), generateAccordionThemed(scopeName, palette), generateCarouselThemed(scopeName, palette), generateModalThemed(scopeName, palette), generateToastThemed(scopeName, palette), generateDropdownThemed(scopeName, palette), generateSwitchThemed(scopeName, palette), generateSkeletonThemed(scopeName, palette), generateAvatarThemed(scopeName, palette), generateUtilityColors(scopeName, palette));
1490
+ return Object.assign({}, generateResetThemed(scopeName, palette), generateTypographyThemed(scopeName, palette, layout), generateButtons(scopeName, palette, layout), generateAlerts(scopeName, palette, layout), generateBadges(scopeName, palette), generateCards(scopeName, palette, layout), generateForms(scopeName, palette, layout), generateNavigation(scopeName, palette), generateTables(scopeName, palette, layout), generateTabs(scopeName, palette), generateListGroups(scopeName, palette, layout), generatePagination(scopeName, palette), generateProgress(scopeName, palette), generateHero(scopeName, palette), generateBreadcrumbThemed(scopeName, palette), generateSpinnerThemed(scopeName, palette), generateCloseButtonThemed(scopeName, palette), generateSectionsThemed(scopeName, palette), generateAccordionThemed(scopeName, palette), generateCarouselThemed(scopeName, palette), generateModalThemed(scopeName, palette, layout), generateToastThemed(scopeName, palette, layout), generateDropdownThemed(scopeName, palette, layout), generateSwitchThemed(scopeName, palette), generateSkeletonThemed(scopeName, palette), generateAvatarThemed(scopeName, palette), generateUtilityColors(scopeName, palette));
1271
1491
  }
1272
1492
 
1273
1493
  // =========================================================================
@@ -1617,11 +1837,23 @@
1617
1837
  // =========================================================================
1618
1838
 
1619
1839
  /**
1620
- * Structural styles contain only layout, sizing, spacing, and behavior
1621
- * properties. No colors, backgrounds, shadows, or border-colors.
1622
- * These never change with themes.
1840
+ * Structural styles layout, sizing, spacing, positioning, and behavior.
1623
1841
  *
1624
- * @returns {Object} CSS rules object
1842
+ * POLICY: No colors, backgrounds, shadows, or border-colors in this function.
1843
+ * All cosmetic values belong in `defaultStyles.*` sections (unthemed defaults)
1844
+ * or in `generateThemedCSS()` (theme-driven colors).
1845
+ *
1846
+ * Exception: `.bw-progress-bar-striped` uses rgba(255,255,255,.15) for the
1847
+ * stripe pattern overlay. This is theme-neutral — a semi-transparent white
1848
+ * gradient that creates visible stripes on any background color.
1849
+ *
1850
+ * Architecture:
1851
+ * getStructuralStyles() → layout-only rules (never change with themes)
1852
+ * defaultStyles.* → cosmetic defaults (colors, shadows, borders)
1853
+ * generateThemedCSS() → palette-driven cosmetics from seed colors
1854
+ * generateAlternateCSS() → alternate palette (luminance-inverted)
1855
+ *
1856
+ * @returns {Object} CSS rules object (layout-only, theme-independent)
1625
1857
  */
1626
1858
  function getStructuralStyles() {
1627
1859
  var rules = {};
@@ -1730,7 +1962,7 @@
1730
1962
  'font-size': '0.875rem',
1731
1963
  'font-family': 'inherit',
1732
1964
  'border-radius': '6px',
1733
- 'transition': 'all 0.15s cubic-bezier(0.4, 0, 0.2, 1)',
1965
+ 'transition': 'all 0.15s ease-out',
1734
1966
  'gap': '0.5rem'
1735
1967
  };
1736
1968
  rules['.bw-btn:hover'] = {
@@ -1741,7 +1973,8 @@
1741
1973
  'transform': 'translateY(0)'
1742
1974
  };
1743
1975
  rules['.bw-btn:focus-visible'] = {
1744
- 'outline': '0'
1976
+ 'outline': '2px solid currentColor',
1977
+ 'outline-offset': '2px'
1745
1978
  };
1746
1979
  rules['.bw-btn:disabled'] = {
1747
1980
  'opacity': '0.5',
@@ -1770,7 +2003,7 @@
1770
2003
  'background-clip': 'border-box',
1771
2004
  'border': '1px solid transparent',
1772
2005
  'border-radius': '8px',
1773
- 'transition': 'box-shadow 0.2s cubic-bezier(0.4,0,0.2,1), transform 0.2s cubic-bezier(0.4,0,0.2,1)',
2006
+ 'transition': 'box-shadow 0.2s ease-out, transform 0.2s ease-out',
1774
2007
  'margin-bottom': '1.5rem',
1775
2008
  'overflow': 'hidden'
1776
2009
  };
@@ -1803,7 +2036,7 @@
1803
2036
  'font-size': '0.875rem'
1804
2037
  };
1805
2038
  rules['.bw-card-hoverable'] = {
1806
- 'transition': 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)'
2039
+ 'transition': 'all 0.3s ease-out'
1807
2040
  };
1808
2041
  rules['.bw-card-img-top'] = {
1809
2042
  'width': '100%',
@@ -1841,11 +2074,12 @@
1841
2074
  'appearance': 'none',
1842
2075
  'border': '1px solid transparent',
1843
2076
  'border-radius': '6px',
1844
- 'transition': 'border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out',
2077
+ 'transition': 'border-color 0.15s ease-out, box-shadow 0.15s ease-out',
1845
2078
  'font-family': 'inherit'
1846
2079
  };
1847
2080
  rules['.bw-form-control:focus'] = {
1848
- 'outline': '0'
2081
+ 'outline': '2px solid currentColor',
2082
+ 'outline-offset': '-1px'
1849
2083
  };
1850
2084
  rules['.bw-form-control::placeholder'] = {
1851
2085
  'opacity': '1'
@@ -1874,6 +2108,18 @@
1874
2108
  'resize': 'vertical'
1875
2109
  };
1876
2110
 
2111
+ // Form validation (structural)
2112
+ rules['.bw-valid-feedback'] = {
2113
+ 'display': 'block',
2114
+ 'font-size': '0.875rem',
2115
+ 'margin-top': '0.25rem'
2116
+ };
2117
+ rules['.bw-invalid-feedback'] = {
2118
+ 'display': 'block',
2119
+ 'font-size': '0.875rem',
2120
+ 'margin-top': '0.25rem'
2121
+ };
2122
+
1877
2123
  // Form checks (structural)
1878
2124
  Object.assign(rules, {
1879
2125
  '.bw-form-check': {
@@ -2014,8 +2260,8 @@
2014
2260
  // Badges (structural)
2015
2261
  rules['.bw-badge'] = {
2016
2262
  'display': 'inline-block',
2017
- 'padding': '.4em .75em',
2018
- 'font-size': '.875em',
2263
+ 'padding': '0.375rem 0.625rem',
2264
+ 'font-size': '0.875rem',
2019
2265
  'font-weight': '600',
2020
2266
  'line-height': '1.3',
2021
2267
  'text-align': 'center',
@@ -2027,12 +2273,12 @@
2027
2273
  'display': 'none'
2028
2274
  };
2029
2275
  rules['.bw-badge-sm'] = {
2030
- 'font-size': '.75em',
2031
- 'padding': '.25em .5em'
2276
+ 'font-size': '0.75rem',
2277
+ 'padding': '0.25rem 0.5rem'
2032
2278
  };
2033
2279
  rules['.bw-badge-lg'] = {
2034
- 'font-size': '1em',
2035
- 'padding': '.5em .9em'
2280
+ 'font-size': '1rem',
2281
+ 'padding': '0.5rem 0.875rem'
2036
2282
  };
2037
2283
  rules['.bw-badge-pill'] = {
2038
2284
  'border-radius': '50rem'
@@ -2053,7 +2299,7 @@
2053
2299
  'overflow': 'hidden',
2054
2300
  'text-align': 'center',
2055
2301
  'white-space': 'nowrap',
2056
- 'transition': 'width .6s ease',
2302
+ 'transition': 'width 0.3s ease-out',
2057
2303
  'font-weight': '600'
2058
2304
  };
2059
2305
  rules['.bw-progress-bar-striped'] = {
@@ -2093,7 +2339,7 @@
2093
2339
  'cursor': 'pointer',
2094
2340
  'border': 'none',
2095
2341
  'background': 'transparent',
2096
- 'transition': 'color 0.15s, border-color 0.15s',
2342
+ 'transition': 'color 0.15s ease-out, background-color 0.15s ease-out, border-color 0.15s ease-out',
2097
2343
  'font-family': 'inherit'
2098
2344
  };
2099
2345
  rules['.bw-nav-tabs .bw-nav-link'] = {
@@ -2157,7 +2403,13 @@
2157
2403
  'pointer-events': 'none'
2158
2404
  };
2159
2405
  rules['a.bw-list-group-item'] = {
2160
- 'cursor': 'pointer'
2406
+ 'cursor': 'pointer',
2407
+ 'transition': 'background-color 0.15s ease-out, color 0.15s ease-out'
2408
+ };
2409
+ rules['a.bw-list-group-item:focus-visible, .bw-list-group-item:focus-visible'] = {
2410
+ 'z-index': '2',
2411
+ 'outline': '2px solid currentColor',
2412
+ 'outline-offset': '-2px'
2161
2413
  };
2162
2414
  rules['.bw-list-group-flush'] = {
2163
2415
  'border-radius': '0'
@@ -2188,7 +2440,7 @@
2188
2440
  'margin-left': '-1px',
2189
2441
  'line-height': '1.25',
2190
2442
  'text-decoration': 'none',
2191
- 'transition': 'color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out'
2443
+ 'transition': 'color 0.15s ease-out, background-color 0.15s ease-out, border-color 0.15s ease-out'
2192
2444
  };
2193
2445
  rules['.bw-page-item:first-child .bw-page-link'] = {
2194
2446
  'margin-left': '0',
@@ -2199,6 +2451,11 @@
2199
2451
  'border-top-right-radius': '0.375rem',
2200
2452
  'border-bottom-right-radius': '0.375rem'
2201
2453
  };
2454
+ rules['.bw-page-link:focus-visible'] = {
2455
+ 'z-index': '3',
2456
+ 'outline': '2px solid currentColor',
2457
+ 'outline-offset': '-2px'
2458
+ };
2202
2459
 
2203
2460
  // Breadcrumb (structural)
2204
2461
  rules['.bw-breadcrumb'] = {
@@ -2219,6 +2476,13 @@
2219
2476
  'padding-right': '0.5rem',
2220
2477
  'content': '"/"'
2221
2478
  };
2479
+ rules['.bw-breadcrumb-item a'] = {
2480
+ 'text-decoration': 'none',
2481
+ 'transition': 'color 0.15s ease-out'
2482
+ };
2483
+ rules['.bw-breadcrumb-item.active'] = {
2484
+ 'font-weight': '500'
2485
+ };
2222
2486
 
2223
2487
  // Hero (structural)
2224
2488
  rules['.bw-hero'] = {
@@ -2530,7 +2794,7 @@
2530
2794
  'overflow-anchor': 'none',
2531
2795
  'cursor': 'pointer',
2532
2796
  'font-family': 'inherit',
2533
- 'transition': 'color 0.15s ease-in-out, background-color 0.15s ease-in-out'
2797
+ 'transition': 'color 0.15s ease-out, background-color 0.15s ease-out'
2534
2798
  };
2535
2799
  rules['.bw-accordion-button::after'] = {
2536
2800
  'flex-shrink': '0',
@@ -2540,7 +2804,7 @@
2540
2804
  'content': '""',
2541
2805
  'background-repeat': 'no-repeat',
2542
2806
  'background-size': '1.25rem',
2543
- 'transition': 'transform 0.2s ease-in-out'
2807
+ 'transition': 'transform 0.2s ease-out'
2544
2808
  };
2545
2809
  rules['.bw-accordion-button:not(.bw-collapsed)::after'] = {
2546
2810
  'transform': 'rotate(-180deg)'
@@ -2559,7 +2823,9 @@
2559
2823
 
2560
2824
  // Modal (structural)
2561
2825
  rules['.bw-modal'] = {
2562
- 'display': 'none',
2826
+ 'display': 'flex',
2827
+ 'align-items': 'center',
2828
+ 'justify-content': 'center',
2563
2829
  'position': 'fixed',
2564
2830
  'top': '0',
2565
2831
  'left': '0',
@@ -2569,13 +2835,14 @@
2569
2835
  'overflow-x': 'hidden',
2570
2836
  'overflow-y': 'auto',
2571
2837
  'opacity': '0',
2572
- 'transition': 'opacity 0.15s linear'
2838
+ 'visibility': 'hidden',
2839
+ 'pointer-events': 'none',
2840
+ 'transition': 'opacity 0.2s ease, visibility 0.2s ease'
2573
2841
  };
2574
2842
  rules['.bw-modal.bw-modal-show'] = {
2575
- 'display': 'flex',
2576
- 'align-items': 'center',
2577
- 'justify-content': 'center',
2578
- 'opacity': '1'
2843
+ 'opacity': '1',
2844
+ 'visibility': 'visible',
2845
+ 'pointer-events': 'auto'
2579
2846
  };
2580
2847
  rules['.bw-modal-dialog'] = {
2581
2848
  'position': 'relative',
@@ -2642,7 +2909,7 @@
2642
2909
  };
2643
2910
  rules['.bw-carousel-track'] = {
2644
2911
  'display': 'flex',
2645
- 'transition': 'transform 0.4s ease',
2912
+ 'transition': 'transform 0.3s ease-out',
2646
2913
  'height': '100%'
2647
2914
  };
2648
2915
  rules['.bw-carousel-slide'] = {
@@ -2772,15 +3039,23 @@
2772
3039
  'top': '100%',
2773
3040
  'left': '0',
2774
3041
  'z-index': '1000',
2775
- 'display': 'none',
3042
+ 'display': 'block',
2776
3043
  'min-width': '10rem',
2777
3044
  'padding': '0.5rem 0',
2778
3045
  'margin': '0.125rem 0 0',
2779
3046
  'background-clip': 'padding-box',
2780
- 'border-radius': '6px'
3047
+ 'border-radius': '6px',
3048
+ 'opacity': '0',
3049
+ 'visibility': 'hidden',
3050
+ 'transform': 'translateY(-4px)',
3051
+ 'pointer-events': 'none',
3052
+ 'transition': 'opacity 0.15s ease, transform 0.15s ease, visibility 0.15s ease'
2781
3053
  };
2782
3054
  rules['.bw-dropdown-menu.bw-dropdown-show'] = {
2783
- 'display': 'block'
3055
+ 'opacity': '1',
3056
+ 'visibility': 'visible',
3057
+ 'transform': 'translateY(0)',
3058
+ 'pointer-events': 'auto'
2784
3059
  };
2785
3060
  rules['.bw-dropdown-menu-end'] = {
2786
3061
  'left': 'auto',
@@ -2800,6 +3075,10 @@
2800
3075
  'font-size': '0.9375rem',
2801
3076
  'transition': 'background-color 0.15s, color 0.15s'
2802
3077
  };
3078
+ rules['.bw-dropdown-item:focus-visible'] = {
3079
+ 'outline': '2px solid currentColor',
3080
+ 'outline-offset': '-2px'
3081
+ };
2803
3082
  rules['.bw-dropdown-divider'] = {
2804
3083
  'height': '0',
2805
3084
  'margin': '0.5rem 0',
@@ -2820,7 +3099,7 @@
2820
3099
  'background-position': 'left center',
2821
3100
  'background-repeat': 'no-repeat',
2822
3101
  'background-size': 'contain',
2823
- 'transition': 'background-position 0.15s ease-in-out, background-color 0.15s ease-in-out',
3102
+ 'transition': 'background-position 0.15s ease-out, background-color 0.15s ease-out',
2824
3103
  'cursor': 'pointer'
2825
3104
  };
2826
3105
  rules['.bw-form-switch .bw-switch-input:checked'] = {
@@ -2893,6 +3172,401 @@
2893
3172
  'font-size': '1.5rem'
2894
3173
  };
2895
3174
 
3175
+ // Stat card (structural)
3176
+ rules['.bw-stat-card'] = {
3177
+ 'border-radius': '8px',
3178
+ 'padding': '1.25rem',
3179
+ 'border-left': '4px solid transparent',
3180
+ 'transition': 'box-shadow 0.15s ease-out, transform 0.15s ease-out'
3181
+ };
3182
+ rules['.bw-stat-card:hover'] = {
3183
+ 'transform': 'translateY(-1px)'
3184
+ };
3185
+ rules['.bw-stat-icon'] = {
3186
+ 'font-size': '1.5rem',
3187
+ 'margin-bottom': '0.5rem'
3188
+ };
3189
+ rules['.bw-stat-value'] = {
3190
+ 'font-size': '2rem',
3191
+ 'font-weight': '700',
3192
+ 'line-height': '1.2'
3193
+ };
3194
+ rules['.bw-stat-label'] = {
3195
+ 'font-size': '0.875rem',
3196
+ 'margin-top': '0.25rem'
3197
+ };
3198
+ rules['.bw-stat-change'] = {
3199
+ 'font-size': '0.875rem',
3200
+ 'font-weight': '500',
3201
+ 'margin-top': '0.5rem'
3202
+ };
3203
+
3204
+ // Tooltip (structural)
3205
+ rules['.bw-tooltip-wrapper'] = {
3206
+ 'position': 'relative',
3207
+ 'display': 'inline-block'
3208
+ };
3209
+ rules['.bw-tooltip'] = {
3210
+ 'position': 'absolute',
3211
+ 'z-index': '999',
3212
+ 'padding': '0.375rem 0.75rem',
3213
+ 'border-radius': '4px',
3214
+ 'font-size': '0.875rem',
3215
+ 'white-space': 'nowrap',
3216
+ 'pointer-events': 'none',
3217
+ 'opacity': '0',
3218
+ 'visibility': 'hidden',
3219
+ 'transition': 'opacity 0.15s ease, visibility 0.15s ease, transform 0.15s ease'
3220
+ };
3221
+ rules['.bw-tooltip.bw-tooltip-show'] = {
3222
+ 'opacity': '1',
3223
+ 'visibility': 'visible'
3224
+ };
3225
+ rules['.bw-tooltip-top'] = {
3226
+ 'bottom': '100%',
3227
+ 'left': '50%',
3228
+ 'transform': 'translateX(-50%) translateY(-4px)',
3229
+ 'margin-bottom': '4px'
3230
+ };
3231
+ rules['.bw-tooltip-top.bw-tooltip-show'] = {
3232
+ 'transform': 'translateX(-50%) translateY(0)'
3233
+ };
3234
+ rules['.bw-tooltip-bottom'] = {
3235
+ 'top': '100%',
3236
+ 'left': '50%',
3237
+ 'transform': 'translateX(-50%) translateY(4px)',
3238
+ 'margin-top': '4px'
3239
+ };
3240
+ rules['.bw-tooltip-bottom.bw-tooltip-show'] = {
3241
+ 'transform': 'translateX(-50%) translateY(0)'
3242
+ };
3243
+ rules['.bw-tooltip-left'] = {
3244
+ 'right': '100%',
3245
+ 'top': '50%',
3246
+ 'transform': 'translateY(-50%) translateX(-4px)',
3247
+ 'margin-right': '4px'
3248
+ };
3249
+ rules['.bw-tooltip-left.bw-tooltip-show'] = {
3250
+ 'transform': 'translateY(-50%) translateX(0)'
3251
+ };
3252
+ rules['.bw-tooltip-right'] = {
3253
+ 'left': '100%',
3254
+ 'top': '50%',
3255
+ 'transform': 'translateY(-50%) translateX(4px)',
3256
+ 'margin-left': '4px'
3257
+ };
3258
+ rules['.bw-tooltip-right.bw-tooltip-show'] = {
3259
+ 'transform': 'translateY(-50%) translateX(0)'
3260
+ };
3261
+
3262
+ // Search input (structural)
3263
+ rules['.bw-search-input'] = {
3264
+ 'position': 'relative',
3265
+ 'display': 'flex',
3266
+ 'align-items': 'center'
3267
+ };
3268
+ rules['.bw-search-input .bw-search-field'] = {
3269
+ 'padding-right': '2.5rem'
3270
+ };
3271
+ rules['.bw-search-clear'] = {
3272
+ 'position': 'absolute',
3273
+ 'right': '0.5rem',
3274
+ 'display': 'flex',
3275
+ 'align-items': 'center',
3276
+ 'justify-content': 'center',
3277
+ 'width': '1.5rem',
3278
+ 'height': '1.5rem',
3279
+ 'border': 'none',
3280
+ 'background': 'none',
3281
+ 'font-size': '1.25rem',
3282
+ 'cursor': 'pointer',
3283
+ 'padding': '0',
3284
+ 'border-radius': '50%',
3285
+ 'transition': 'color 0.15s ease-out'
3286
+ };
3287
+
3288
+ // Range slider (structural)
3289
+ rules['.bw-range-wrapper'] = {
3290
+ 'margin-bottom': '1rem'
3291
+ };
3292
+ rules['.bw-range-label'] = {
3293
+ 'display': 'flex',
3294
+ 'justify-content': 'space-between',
3295
+ 'align-items': 'center',
3296
+ 'margin-bottom': '0.5rem',
3297
+ 'font-size': '0.875rem',
3298
+ 'font-weight': '500'
3299
+ };
3300
+ rules['.bw-range-value'] = {
3301
+ 'font-weight': '600'
3302
+ };
3303
+ rules['.bw-range'] = {
3304
+ 'width': '100%',
3305
+ 'height': '0.5rem',
3306
+ 'padding': '0',
3307
+ 'appearance': 'none',
3308
+ 'border': 'none',
3309
+ 'border-radius': '0.25rem',
3310
+ 'cursor': 'pointer',
3311
+ 'outline': 'none'
3312
+ };
3313
+ rules['.bw-range:disabled'] = {
3314
+ 'opacity': '0.5',
3315
+ 'cursor': 'not-allowed'
3316
+ };
3317
+
3318
+ // Media object (structural)
3319
+ rules['.bw-media'] = {
3320
+ 'display': 'flex',
3321
+ 'align-items': 'flex-start',
3322
+ 'gap': '1rem'
3323
+ };
3324
+ rules['.bw-media-reverse'] = {
3325
+ 'flex-direction': 'row-reverse'
3326
+ };
3327
+ rules['.bw-media-img'] = {
3328
+ 'border-radius': '50%',
3329
+ 'object-fit': 'cover',
3330
+ 'flex-shrink': '0'
3331
+ };
3332
+ rules['.bw-media-body'] = {
3333
+ 'flex': '1',
3334
+ 'min-width': '0'
3335
+ };
3336
+ rules['.bw-media-title'] = {
3337
+ 'margin': '0 0 0.25rem 0',
3338
+ 'font-size': '1rem',
3339
+ 'font-weight': '600',
3340
+ 'line-height': '1.3'
3341
+ };
3342
+
3343
+ // File upload (structural)
3344
+ rules['.bw-file-upload'] = {
3345
+ 'display': 'flex',
3346
+ 'flex-direction': 'column',
3347
+ 'align-items': 'center',
3348
+ 'justify-content': 'center',
3349
+ 'padding': '2rem',
3350
+ 'border': '2px dashed transparent',
3351
+ 'border-radius': '8px',
3352
+ 'cursor': 'pointer',
3353
+ 'text-align': 'center',
3354
+ 'position': 'relative',
3355
+ 'transition': 'border-color 0.15s ease-out, background-color 0.15s ease-out'
3356
+ };
3357
+ rules['.bw-file-upload-icon'] = {
3358
+ 'font-size': '2rem',
3359
+ 'margin-bottom': '0.5rem'
3360
+ };
3361
+ rules['.bw-file-upload-text'] = {
3362
+ 'font-size': '0.875rem'
3363
+ };
3364
+ rules['.bw-file-upload-input'] = {
3365
+ 'position': 'absolute',
3366
+ 'width': '1px',
3367
+ 'height': '1px',
3368
+ 'padding': '0',
3369
+ 'margin': '-1px',
3370
+ 'overflow': 'hidden',
3371
+ 'clip': 'rect(0,0,0,0)',
3372
+ 'border': '0'
3373
+ };
3374
+
3375
+ // Timeline (structural)
3376
+ rules['.bw-timeline'] = {
3377
+ 'position': 'relative',
3378
+ 'padding-left': '2rem'
3379
+ };
3380
+ rules['.bw-timeline-item'] = {
3381
+ 'position': 'relative',
3382
+ 'padding-bottom': '1.5rem'
3383
+ };
3384
+ rules['.bw-timeline-item:last-child'] = {
3385
+ 'padding-bottom': '0'
3386
+ };
3387
+ rules['.bw-timeline-marker'] = {
3388
+ 'position': 'absolute',
3389
+ 'left': '-1.75rem',
3390
+ 'top': '0.25rem',
3391
+ 'width': '0.75rem',
3392
+ 'height': '0.75rem',
3393
+ 'border-radius': '50%'
3394
+ };
3395
+ rules['.bw-timeline-content'] = {
3396
+ 'padding-left': '0.5rem'
3397
+ };
3398
+ rules['.bw-timeline-date'] = {
3399
+ 'font-size': '0.75rem',
3400
+ 'margin-bottom': '0.25rem',
3401
+ 'font-weight': '500'
3402
+ };
3403
+ rules['.bw-timeline-title'] = {
3404
+ 'font-size': '1rem',
3405
+ 'font-weight': '600',
3406
+ 'margin': '0 0 0.25rem 0',
3407
+ 'line-height': '1.3'
3408
+ };
3409
+ rules['.bw-timeline-text'] = {
3410
+ 'font-size': '0.875rem',
3411
+ 'margin': '0',
3412
+ 'line-height': '1.5'
3413
+ };
3414
+
3415
+ // Stepper (structural)
3416
+ rules['.bw-stepper'] = {
3417
+ 'display': 'flex',
3418
+ 'gap': '0'
3419
+ };
3420
+ rules['.bw-step'] = {
3421
+ 'flex': '1',
3422
+ 'display': 'flex',
3423
+ 'flex-direction': 'column',
3424
+ 'align-items': 'center',
3425
+ 'text-align': 'center',
3426
+ 'position': 'relative'
3427
+ };
3428
+ rules['.bw-step-indicator'] = {
3429
+ 'width': '2rem',
3430
+ 'height': '2rem',
3431
+ 'border-radius': '50%',
3432
+ 'display': 'flex',
3433
+ 'align-items': 'center',
3434
+ 'justify-content': 'center',
3435
+ 'font-size': '0.875rem',
3436
+ 'font-weight': '600',
3437
+ 'position': 'relative',
3438
+ 'z-index': '1',
3439
+ 'transition': 'background-color 0.2s ease-out, color 0.2s ease-out'
3440
+ };
3441
+ rules['.bw-step-body'] = {
3442
+ 'margin-top': '0.5rem'
3443
+ };
3444
+ rules['.bw-step-label'] = {
3445
+ 'font-size': '0.875rem',
3446
+ 'font-weight': '500'
3447
+ };
3448
+ rules['.bw-step-description'] = {
3449
+ 'font-size': '0.75rem',
3450
+ 'margin-top': '0.125rem'
3451
+ };
3452
+
3453
+ // Chip input (structural)
3454
+ rules['.bw-chip-input'] = {
3455
+ 'display': 'flex',
3456
+ 'flex-wrap': 'wrap',
3457
+ 'align-items': 'center',
3458
+ 'gap': '0.375rem',
3459
+ 'padding': '0.375rem 0.5rem',
3460
+ 'border-radius': '6px',
3461
+ 'min-height': '2.5rem',
3462
+ 'cursor': 'text',
3463
+ 'transition': 'border-color 0.15s ease-out, box-shadow 0.15s ease-out'
3464
+ };
3465
+ rules['.bw-chip'] = {
3466
+ 'display': 'inline-flex',
3467
+ 'align-items': 'center',
3468
+ 'gap': '0.25rem',
3469
+ 'padding': '0.125rem 0.5rem',
3470
+ 'border-radius': '1rem',
3471
+ 'font-size': '0.8125rem',
3472
+ 'line-height': '1.5',
3473
+ 'white-space': 'nowrap'
3474
+ };
3475
+ rules['.bw-chip-remove'] = {
3476
+ 'display': 'inline-flex',
3477
+ 'align-items': 'center',
3478
+ 'justify-content': 'center',
3479
+ 'width': '1rem',
3480
+ 'height': '1rem',
3481
+ 'border': 'none',
3482
+ 'background': 'none',
3483
+ 'font-size': '0.875rem',
3484
+ 'cursor': 'pointer',
3485
+ 'padding': '0',
3486
+ 'border-radius': '50%',
3487
+ 'transition': 'color 0.15s ease-out, background-color 0.15s ease-out'
3488
+ };
3489
+ rules['.bw-chip-field'] = {
3490
+ 'flex': '1',
3491
+ 'min-width': '80px',
3492
+ 'border': 'none',
3493
+ 'outline': 'none',
3494
+ 'font-size': '0.875rem',
3495
+ 'padding': '0.125rem 0',
3496
+ 'background': 'transparent'
3497
+ };
3498
+
3499
+ // Popover (structural)
3500
+ rules['.bw-popover-wrapper'] = {
3501
+ 'position': 'relative',
3502
+ 'display': 'inline-block'
3503
+ };
3504
+ rules['.bw-popover-trigger'] = {
3505
+ 'cursor': 'pointer'
3506
+ };
3507
+ rules['.bw-popover'] = {
3508
+ 'position': 'absolute',
3509
+ 'z-index': '1000',
3510
+ 'min-width': '200px',
3511
+ 'max-width': '320px',
3512
+ 'border-radius': '8px',
3513
+ 'pointer-events': 'none',
3514
+ 'opacity': '0',
3515
+ 'visibility': 'hidden',
3516
+ 'transition': 'opacity 0.15s ease, visibility 0.15s ease, transform 0.15s ease'
3517
+ };
3518
+ rules['.bw-popover.bw-popover-show'] = {
3519
+ 'opacity': '1',
3520
+ 'visibility': 'visible',
3521
+ 'pointer-events': 'auto'
3522
+ };
3523
+ rules['.bw-popover-header'] = {
3524
+ 'padding': '0.625rem 0.875rem',
3525
+ 'font-weight': '600',
3526
+ 'font-size': '0.9375rem'
3527
+ };
3528
+ rules['.bw-popover-body'] = {
3529
+ 'padding': '0.75rem 0.875rem',
3530
+ 'font-size': '0.875rem',
3531
+ 'line-height': '1.5'
3532
+ };
3533
+ rules['.bw-popover-top'] = {
3534
+ 'bottom': '100%',
3535
+ 'left': '50%',
3536
+ 'transform': 'translateX(-50%) translateY(-8px)',
3537
+ 'margin-bottom': '8px'
3538
+ };
3539
+ rules['.bw-popover-top.bw-popover-show'] = {
3540
+ 'transform': 'translateX(-50%) translateY(0)'
3541
+ };
3542
+ rules['.bw-popover-bottom'] = {
3543
+ 'top': '100%',
3544
+ 'left': '50%',
3545
+ 'transform': 'translateX(-50%) translateY(8px)',
3546
+ 'margin-top': '8px'
3547
+ };
3548
+ rules['.bw-popover-bottom.bw-popover-show'] = {
3549
+ 'transform': 'translateX(-50%) translateY(0)'
3550
+ };
3551
+ rules['.bw-popover-left'] = {
3552
+ 'right': '100%',
3553
+ 'top': '50%',
3554
+ 'transform': 'translateY(-50%) translateX(-8px)',
3555
+ 'margin-right': '8px'
3556
+ };
3557
+ rules['.bw-popover-left.bw-popover-show'] = {
3558
+ 'transform': 'translateY(-50%) translateX(0)'
3559
+ };
3560
+ rules['.bw-popover-right'] = {
3561
+ 'left': '100%',
3562
+ 'top': '50%',
3563
+ 'transform': 'translateY(-50%) translateX(8px)',
3564
+ 'margin-left': '8px'
3565
+ };
3566
+ rules['.bw-popover-right.bw-popover-show'] = {
3567
+ 'transform': 'translateY(-50%) translateX(0)'
3568
+ };
3569
+
2896
3570
  // Bar chart (structural)
2897
3571
  rules['.bw-bar-chart-container'] = {
2898
3572
  'padding': '1rem',
@@ -2916,7 +3590,7 @@
2916
3590
  rules['.bw-bar'] = {
2917
3591
  'width': '100%',
2918
3592
  'border-radius': '3px 3px 0 0',
2919
- 'transition': 'height 0.5s ease',
3593
+ 'transition': 'height 0.3s ease-out',
2920
3594
  'min-height': '4px'
2921
3595
  };
2922
3596
  rules['.bw-bar:hover'] = {
@@ -3215,6 +3889,16 @@
3215
3889
 
3216
3890
  // Responsive grid
3217
3891
  Object.assign(rules, defaultStyles.responsive);
3892
+
3893
+ // Accessibility: reduce motion for users who prefer it
3894
+ rules['@media (prefers-reduced-motion: reduce)'] = {
3895
+ '*, *::before, *::after': {
3896
+ 'animation-duration': '0.01ms !important',
3897
+ 'animation-iteration-count': '1 !important',
3898
+ 'transition-duration': '0.01ms !important',
3899
+ 'scroll-behavior': 'auto !important'
3900
+ }
3901
+ };
3218
3902
  return addUnderscoreAliases(rules);
3219
3903
  }
3220
3904
 
@@ -3223,9 +3907,25 @@
3223
3907
  // =========================================================================
3224
3908
 
3225
3909
  /**
3226
- * Add underscore aliases for all bw- selectors
3910
+ * Add underscore aliases for all `.bw-` selectors.
3911
+ *
3912
+ * CSS CLASS NAMING CONVENTION:
3913
+ *
3914
+ * Canonical form: `.bw-btn`, `.bw-card`, `.bw-table-hover` (hyphens)
3915
+ * Underscore alias: `.bw_btn`, `.bw_card`, `.bw_table_hover` (underscores)
3916
+ *
3917
+ * Both forms are valid in HTML and produce identical results. The hyphen
3918
+ * form is canonical (used in docs, generated CSS, component output).
3919
+ * Underscore aliases exist because:
3920
+ * 1. TACO attribute keys use underscores (`bw_id`, `bw_meta`) — no
3921
+ * quoting needed in JS object literals
3922
+ * 2. Some users prefer underscores for consistency with JS identifiers
3923
+ *
3924
+ * Use `bw.normalizeClass()` to convert underscore classes to canonical
3925
+ * hyphen form at runtime if needed.
3926
+ *
3227
3927
  * @param {Object} rules - CSS rules object
3228
- * @returns {Object} - Rules with underscore aliases added
3928
+ * @returns {Object} Rules with underscore aliases added (both forms work)
3229
3929
  */
3230
3930
  function addUnderscoreAliases(rules) {
3231
3931
  var result = {};
@@ -3245,6 +3945,27 @@
3245
3945
  // =========================================================================
3246
3946
  // Theme tokens (backwards compatible)
3247
3947
  // =========================================================================
3948
+ //
3949
+ // DESIGN NOTE — Why no CSS custom properties (CSS variables)?
3950
+ //
3951
+ // Bitwrench targets IE11 as Tier 1 (see dev/bw2x-compatibility.md).
3952
+ // CSS custom properties (var(--color-primary)) are not supported in IE11.
3953
+ //
3954
+ // Instead, bitwrench uses class-scoped CSS generation:
3955
+ // 1. `defaultStyles.*` provides hardcoded cosmetic defaults
3956
+ // 2. `generateTheme(name, config)` generates a complete set of
3957
+ // class-scoped CSS rules from 3 seed colors (primary, secondary,
3958
+ // tertiary) — all components are restyled with the new palette
3959
+ // 3. `generateAlternateCSS()` produces the alternate (dark/light)
3960
+ // variant scoped under `.bw-theme-alt`
3961
+ //
3962
+ // This achieves full theme customization without CSS variables:
3963
+ // bw.generateTheme('ocean', { primary: '#006666', secondary: '#cc6633' })
3964
+ // → generates .ocean .bw-btn-primary { background: #006666; } etc.
3965
+ //
3966
+ // When IE11 support is dropped, CSS custom properties can be added as
3967
+ // an optimization (one rule with var() instead of many scoped rules).
3968
+ // The generateTheme() API stays the same — only the output format changes.
3248
3969
 
3249
3970
  var theme = {
3250
3971
  colors: {
@@ -3252,8 +3973,8 @@
3252
3973
  secondary: '#6c757d',
3253
3974
  success: '#198754',
3254
3975
  danger: '#dc3545',
3255
- warning: '#ffc107',
3256
- info: '#0dcaf0',
3976
+ warning: '#b38600',
3977
+ info: '#0891b2',
3257
3978
  light: '#f8f9fa',
3258
3979
  dark: '#212529',
3259
3980
  white: '#fff',
@@ -3288,214 +4009,61 @@
3288
4009
  '4xl': '2.25rem',
3289
4010
  '5xl': '3rem'
3290
4011
  }
3291
- },
3292
- darkMode: false
4012
+ }
3293
4013
  };
3294
4014
 
3295
4015
  /**
3296
- * Generate theme-aware dark mode CSS from a palette.
3297
- * Derives dark variants from the palette colors instead of using hardcoded values.
4016
+ * Generate alternate-palette CSS scoped under `.bw-theme-alt`.
4017
+ * Uses the same `generateThemedCSS()` pipeline as the primary palette
4018
+ * both sides go through identical code paths.
3298
4019
  *
3299
- * @param {Object} palette - From derivePalette()
3300
- * @returns {Object} CSS rules object for dark mode
4020
+ * @param {string} name - Theme scope name (e.g. 'ocean'). '' for global.
4021
+ * @param {Object} altPalette - From derivePalette(deriveAlternateConfig(...))
4022
+ * @param {Object} layout - From resolveLayout()
4023
+ * @returns {Object} CSS rules object scoped under .bw-theme-alt (+ optional .name)
3301
4024
  */
3302
- function generateDarkModeCSS(palette) {
3303
- var darkBg = adjustLightness(palette.primary.base, -15);
3304
- var darkBgHsl = hexToHsl(darkBg);
3305
- // Make it very dark (lightness 8-12%)
3306
- var bodyBg = hslToHex([darkBgHsl[0], Math.min(darkBgHsl[1], 30), 10]);
3307
- var surfaceBg = hslToHex([darkBgHsl[0], Math.min(darkBgHsl[1], 25), 15]);
3308
- var textColor = adjustLightness(palette.light.base, 5);
3309
- var borderColor = hslToHex([darkBgHsl[0], Math.min(darkBgHsl[1], 15), 30]);
3310
- return {
3311
- ':root.bw-dark': {
3312
- '--bw-body-color': textColor,
3313
- '--bw-body-bg': bodyBg
3314
- },
3315
- '.bw-dark body, :root.bw-dark body': {
3316
- 'color': textColor,
3317
- 'background-color': bodyBg
3318
- },
3319
- '.bw-dark .bw-card': {
3320
- 'background-color': surfaceBg,
3321
- 'border-color': borderColor,
3322
- 'color': textColor
3323
- },
3324
- '.bw-dark .bw-card-header': {
3325
- 'background-color': bodyBg,
3326
- 'border-bottom-color': borderColor,
3327
- 'color': textColor
3328
- },
3329
- '.bw-dark .bw-card-footer': {
3330
- 'background-color': bodyBg,
3331
- 'border-top-color': borderColor,
3332
- 'color': textColor
3333
- },
3334
- '.bw-dark .bw-card-title': {
3335
- 'color': textColor
3336
- },
3337
- '.bw-dark .bw-navbar': {
3338
- 'background-color': surfaceBg,
3339
- 'border-bottom-color': borderColor
3340
- },
3341
- '.bw-dark .bw-navbar-brand': {
3342
- 'color': textColor
3343
- },
3344
- '.bw-dark .bw-navbar-nav .bw-nav-link': {
3345
- 'color': adjustLightness(textColor, -15)
3346
- },
3347
- '.bw-dark .bw-navbar-nav .bw-nav-link:hover': {
3348
- 'color': textColor
3349
- },
3350
- '.bw-dark .bw-form-control': {
3351
- 'background-color': surfaceBg,
3352
- 'border-color': borderColor,
3353
- 'color': textColor
3354
- },
3355
- '.bw-dark .bw-form-label': {
3356
- 'color': textColor
3357
- },
3358
- '.bw-dark .bw-form-text': {
3359
- 'color': adjustLightness(textColor, -20)
3360
- },
3361
- '.bw-dark .bw-table': {
3362
- 'color': textColor
3363
- },
3364
- '.bw-dark .bw-table > :not(caption) > * > *': {
3365
- 'border-bottom-color': borderColor
3366
- },
3367
- '.bw-dark .bw-table > thead > tr > *': {
3368
- 'background-color': bodyBg,
3369
- 'color': adjustLightness(textColor, -10),
3370
- 'border-bottom-color': borderColor
3371
- },
3372
- '.bw-dark .bw-table-striped > tbody > tr:nth-of-type(odd) > *': {
3373
- 'background-color': 'rgba(255, 255, 255, 0.05)'
3374
- },
3375
- '.bw-dark .bw-alert': {
3376
- 'border-color': borderColor
3377
- },
3378
- '.bw-dark .bw-list-group-item': {
3379
- 'background-color': surfaceBg,
3380
- 'border-color': borderColor,
3381
- 'color': textColor
3382
- },
3383
- '.bw-dark .bw-badge': {
3384
- 'color': textColor
3385
- },
3386
- '.bw-dark .bw-nav-tabs': {
3387
- 'border-bottom-color': borderColor
3388
- },
3389
- '.bw-dark .bw-nav-link': {
3390
- 'color': adjustLightness(textColor, -15)
3391
- },
3392
- '.bw-dark .bw-nav-tabs .bw-nav-link:hover': {
3393
- 'color': textColor,
3394
- 'border-bottom-color': borderColor
3395
- },
3396
- '.bw-dark .bw-pagination .bw-page-link': {
3397
- 'background-color': surfaceBg,
3398
- 'border-color': borderColor,
3399
- 'color': textColor
3400
- },
3401
- '.bw-dark .bw-breadcrumb-item + .bw-breadcrumb-item::before': {
3402
- 'color': adjustLightness(textColor, -20)
3403
- },
3404
- '.bw-dark .bw-breadcrumb-item.active': {
3405
- 'color': adjustLightness(textColor, -10)
3406
- },
3407
- '.bw-dark .bw-hero-light': {
3408
- 'background': surfaceBg,
3409
- 'color': textColor
3410
- },
3411
- '.bw-dark .bw-progress': {
3412
- 'background-color': surfaceBg
3413
- },
3414
- '.bw-dark .bw-section-subtitle': {
3415
- 'color': adjustLightness(textColor, -15)
3416
- },
3417
- '.bw-dark .bw-close': {
3418
- 'color': textColor
3419
- },
3420
- '.bw-dark .bw-accordion-item': {
3421
- 'background-color': surfaceBg,
3422
- 'border-color': borderColor
3423
- },
3424
- '.bw-dark .bw-accordion-button': {
3425
- 'color': textColor
3426
- },
3427
- '.bw-dark .bw-accordion-button:not(.bw-collapsed)': {
3428
- 'color': '#7dd3e0',
3429
- 'background-color': 'rgba(125, 211, 224, 0.1)'
3430
- },
3431
- '.bw-dark .bw-accordion-button:hover': {
3432
- 'background-color': bodyBg
3433
- },
3434
- '.bw-dark .bw-accordion-button:not(.bw-collapsed):hover': {
3435
- 'background-color': 'rgba(125, 211, 224, 0.15)'
3436
- },
3437
- '.bw-dark .bw-accordion-button:focus-visible': {
3438
- 'box-shadow': '0 0 0 0.2rem rgba(125, 211, 224, 0.3)'
3439
- },
3440
- '.bw-dark .bw-accordion-body': {
3441
- 'border-top-color': borderColor
3442
- },
3443
- '.bw-dark .bw-carousel': {
3444
- 'background-color': bodyBg
3445
- },
3446
- '.bw-dark .bw-carousel-control': {
3447
- 'background-color': 'rgba(255,255,255,0.15)'
3448
- },
3449
- '.bw-dark .bw-carousel-control:hover': {
3450
- 'background-color': 'rgba(255,255,255,0.25)'
3451
- },
3452
- '.bw-dark .bw-modal-content': {
3453
- 'background-color': surfaceBg,
3454
- 'border-color': borderColor
3455
- },
3456
- '.bw-dark .bw-modal-header': {
3457
- 'border-bottom-color': borderColor
3458
- },
3459
- '.bw-dark .bw-modal-footer': {
3460
- 'border-top-color': borderColor
3461
- },
3462
- '.bw-dark .bw-modal-title': {
3463
- 'color': textColor
3464
- },
3465
- '.bw-dark .bw-toast': {
3466
- 'background-color': surfaceBg,
3467
- 'border-color': borderColor
3468
- },
3469
- '.bw-dark .bw-toast-header': {
3470
- 'border-bottom-color': borderColor,
3471
- 'color': textColor
3472
- },
3473
- '.bw-dark .bw-dropdown-menu': {
3474
- 'background-color': surfaceBg,
3475
- 'border-color': borderColor
3476
- },
3477
- '.bw-dark .bw-dropdown-item': {
3478
- 'color': textColor
3479
- },
3480
- '.bw-dark .bw-dropdown-item:hover': {
3481
- 'background-color': bodyBg
3482
- },
3483
- '.bw-dark .bw-dropdown-divider': {
3484
- 'border-top-color': borderColor
3485
- },
3486
- '.bw-dark .bw-skeleton': {
3487
- 'background': 'linear-gradient(90deg, ' + borderColor + ' 25%, ' + surfaceBg + ' 37%, ' + borderColor + ' 63%)'
3488
- },
3489
- '.bw-dark h1, .bw-dark h2, .bw-dark h3, .bw-dark h4, .bw-dark h5, .bw-dark h6': {
3490
- 'color': textColor
3491
- },
3492
- '@media (prefers-color-scheme: dark)': {
3493
- ':root.bw-auto-dark body': {
3494
- 'color': textColor,
3495
- 'background-color': bodyBg
4025
+ function generateAlternateCSS(name, altPalette, layout) {
4026
+ // Generate themed CSS using the same pipeline as primary
4027
+ var rawRules = generateThemedCSS('', altPalette, layout);
4028
+
4029
+ // Re-scope every selector under .bw-theme-alt (+ optional theme name)
4030
+ var altPrefix = name ? '.' + name + '.bw-theme-alt' : '.bw-theme-alt';
4031
+ var altRules = {};
4032
+ for (var sel in rawRules) {
4033
+ if (!rawRules.hasOwnProperty(sel)) continue;
4034
+ if (sel.charAt(0) === '@') {
4035
+ // @media / @keyframes — recurse into the block
4036
+ var innerBlock = rawRules[sel];
4037
+ var altInner = {};
4038
+ for (var innerSel in innerBlock) {
4039
+ if (!innerBlock.hasOwnProperty(innerSel)) continue;
4040
+ altInner[altPrefix + ' ' + innerSel] = innerBlock[innerSel];
4041
+ }
4042
+ altRules[sel] = altInner;
4043
+ } else {
4044
+ // Regular selector — prefix with alt scope
4045
+ // Handle comma-separated selectors
4046
+ var parts = sel.split(',');
4047
+ var scopedParts = [];
4048
+ for (var i = 0; i < parts.length; i++) {
4049
+ var s = parts[i].trim();
4050
+ // 'body' selector gets special treatment: .bw-theme-alt body
4051
+ if (s === 'body' || s.indexOf('body') === 0) {
4052
+ scopedParts.push(altPrefix + ' ' + s);
4053
+ } else {
4054
+ scopedParts.push(altPrefix + ' ' + s);
4055
+ }
3496
4056
  }
4057
+ altRules[scopedParts.join(', ')] = rawRules[sel];
3497
4058
  }
4059
+ }
4060
+
4061
+ // Add body-level overrides for the alternate surface
4062
+ altRules[altPrefix + ' body, :root' + altPrefix + ' body'] = {
4063
+ 'color': altPalette.dark.base,
4064
+ 'background-color': altPalette.light.base
3498
4065
  };
4066
+ return altRules;
3499
4067
  }
3500
4068
  function deepMerge(target, source) {
3501
4069
  for (var _i2 = 0, _Object$keys = Object.keys(source); _i2 < _Object$keys.length; _i2++) {
@@ -5263,8 +5831,10 @@
5263
5831
  /**
5264
5832
  * Generate responsive CSS with media query breakpoints.
5265
5833
  *
5266
- * Produces a CSS string with `@media` rules for sm (640px), md (768px),
5267
- * lg (1024px), and xl (1280px) breakpoints. Pass the result to `bw.injectCSS()`.
5834
+ * Produces a CSS string with `@media (min-width)` rules for standard
5835
+ * breakpoints. These match the grid system and theme.breakpoints:
5836
+ * sm: 576px, md: 768px, lg: 992px, xl: 1200px
5837
+ * Pass the result to `bw.injectCSS()`.
5268
5838
  *
5269
5839
  * @param {string} selector - CSS selector
5270
5840
  * @param {Object} breakpoints - Object with keys: base, sm, md, lg, xl
@@ -5282,10 +5852,10 @@
5282
5852
  */
5283
5853
  bw.responsive = function (selector, breakpoints) {
5284
5854
  var sizes = {
5285
- sm: '640px',
5855
+ sm: '576px',
5286
5856
  md: '768px',
5287
- lg: '1024px',
5288
- xl: '1280px'
5857
+ lg: '992px',
5858
+ xl: '1200px'
5289
5859
  };
5290
5860
  var parts = [];
5291
5861
  Object.keys(breakpoints).forEach(function (key) {
@@ -5422,7 +5992,8 @@
5422
5992
  * @returns {Element|null} Style element if in browser, null in Node.js
5423
5993
  * @category CSS & Styling
5424
5994
  * @see bw.setTheme
5425
- * @see bw.toggleDarkMode
5995
+ * @see bw.applyTheme
5996
+ * @see bw.toggleTheme
5426
5997
  * @example
5427
5998
  * bw.loadDefaultStyles(); // inject all default CSS
5428
5999
  */
@@ -5502,50 +6073,6 @@
5502
6073
  return bw.getTheme();
5503
6074
  };
5504
6075
 
5505
- /**
5506
- * Toggle dark mode on/off.
5507
- *
5508
- * Adds/removes the `bw-dark` class on `<html>` and injects dark mode CSS
5509
- * overrides. Pass `true`/`false` to force a mode, or omit to toggle.
5510
- *
5511
- * @param {boolean} [force] - Force dark (true) or light (false). Omit to toggle.
5512
- * @returns {boolean} Whether dark mode is now active
5513
- * @category CSS & Styling
5514
- * @see bw.setTheme
5515
- * @example
5516
- * bw.toggleDarkMode(); // toggle
5517
- * bw.toggleDarkMode(true); // force dark
5518
- * bw.toggleDarkMode(false); // force light
5519
- */
5520
- bw.toggleDarkMode = function (force) {
5521
- var isDark = force !== undefined ? force : !theme.darkMode;
5522
- theme.darkMode = isDark;
5523
- if (bw._isBrowser) {
5524
- var root = document.documentElement;
5525
- if (isDark) {
5526
- root.classList.add('bw-dark');
5527
- // Generate palette-aware dark mode CSS, or fall back to default
5528
- var palette = bw._activePalette || derivePalette(DEFAULT_PALETTE_CONFIG);
5529
- var darkRules = generateDarkModeCSS(palette);
5530
- var darkCSS = bw.css(darkRules);
5531
-
5532
- // Remove existing dark styles to allow regeneration
5533
- var existing = document.getElementById('bw-dark-styles');
5534
- if (existing) existing.remove();
5535
- var styleEl = document.createElement('style');
5536
- styleEl.id = 'bw-dark-styles';
5537
- styleEl.textContent = darkCSS;
5538
- document.head.appendChild(styleEl);
5539
- } else {
5540
- root.classList.remove('bw-dark');
5541
- // Remove dark mode styles when switching to light
5542
- var darkEl = document.getElementById('bw-dark-styles');
5543
- if (darkEl) darkEl.remove();
5544
- }
5545
- }
5546
- return isDark;
5547
- };
5548
-
5549
6076
  /**
5550
6077
  * Generate a complete, scoped theme from seed colors.
5551
6078
  *
@@ -5568,13 +6095,19 @@
5568
6095
  * @param {string} [config.spacing='normal'] - 'compact' | 'normal' | 'spacious'
5569
6096
  * @param {string} [config.radius='md'] - 'none' | 'sm' | 'md' | 'lg' | 'pill'
5570
6097
  * @param {number} [config.fontSize=1.0] - Base font size scale factor
6098
+ * @param {string|number} [config.typeRatio='normal'] - 'tight' | 'normal' | 'relaxed' | 'dramatic' or a number
6099
+ * @param {string} [config.elevation='md'] - 'flat' | 'sm' | 'md' | 'lg'
6100
+ * @param {string} [config.motion='standard'] - 'reduced' | 'standard' | 'expressive'
6101
+ * @param {number} [config.harmonize=0.20] - 0-1, semantic color hue shift toward primary
5571
6102
  * @param {boolean} [config.inject=true] - Inject into DOM (browser only)
5572
- * @returns {Object} { css, palette, name }
6103
+ * @returns {Object} { css, palette, name, isLightPrimary, alternate: { css, palette } }
5573
6104
  * @category CSS & Styling
6105
+ * @see bw.applyTheme
6106
+ * @see bw.toggleTheme
5574
6107
  * @see bw.loadDefaultStyles
5575
6108
  * @example
5576
- * // Generate and inject an ocean theme
5577
- * bw.generateTheme('ocean', {
6109
+ * // Generate and inject an ocean theme (primary + alternate)
6110
+ * var theme = bw.generateTheme('ocean', {
5578
6111
  * primary: '#0077b6',
5579
6112
  * secondary: '#90e0ef',
5580
6113
  * tertiary: '#00b4d8'
@@ -5583,14 +6116,16 @@
5583
6116
  * // Apply to a container
5584
6117
  * document.getElementById('app').classList.add('ocean');
5585
6118
  *
6119
+ * // Toggle to alternate palette
6120
+ * bw.toggleTheme();
6121
+ *
5586
6122
  * // Generate CSS for static export (Node.js)
5587
6123
  * var result = bw.generateTheme('sunset', {
5588
6124
  * primary: '#e76f51',
5589
6125
  * secondary: '#264653',
5590
- * tertiary: '#e9c46a',
5591
6126
  * inject: false
5592
6127
  * });
5593
- * fs.writeFileSync('sunset.css', result.css);
6128
+ * fs.writeFileSync('sunset.css', result.css + result.alternate.css);
5594
6129
  */
5595
6130
  bw.generateTheme = function (name, config) {
5596
6131
  if (!config || !config.primary || !config.secondary) {
@@ -5601,25 +6136,30 @@
5601
6136
  var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config);
5602
6137
  if (!config.tertiary) fullConfig.tertiary = fullConfig.primary;
5603
6138
 
5604
- // Derive palette
6139
+ // Derive primary palette
5605
6140
  var palette = derivePalette(fullConfig);
5606
6141
 
5607
- // Store active palette for dark mode
5608
- bw._activePalette = palette;
5609
-
5610
6142
  // Resolve layout
5611
6143
  var layout = resolveLayout(fullConfig);
5612
6144
 
5613
- // Generate themed CSS rules
6145
+ // Generate primary themed CSS rules
5614
6146
  var themedRules = generateThemedCSS(name, palette, layout);
5615
-
5616
- // Add underscore aliases
5617
6147
  var aliasedRules = addUnderscoreAliases(themedRules);
5618
-
5619
- // Convert to CSS string
5620
6148
  var cssStr = bw.css(aliasedRules);
5621
6149
 
5622
- // Inject into DOM if requested and in browser
6150
+ // Derive alternate palette (luminance-inverted)
6151
+ var altConfig = deriveAlternateConfig(fullConfig);
6152
+ var altPalette = derivePalette(altConfig);
6153
+
6154
+ // Generate alternate CSS scoped under .bw-theme-alt
6155
+ var altRules = generateAlternateCSS(name, altPalette, layout);
6156
+ var aliasedAltRules = addUnderscoreAliases(altRules);
6157
+ var altCssStr = bw.css(aliasedAltRules);
6158
+
6159
+ // Determine if primary is light-flavored
6160
+ var lightPrimary = isLightPalette(fullConfig);
6161
+
6162
+ // Inject both CSS sets into DOM if requested
5623
6163
  var shouldInject = config.inject !== false;
5624
6164
  if (shouldInject && bw._isBrowser) {
5625
6165
  var styleId = name ? 'bw-theme-' + name : 'bw-theme-default';
@@ -5627,6 +6167,11 @@
5627
6167
  id: styleId,
5628
6168
  append: false
5629
6169
  });
6170
+ var altStyleId = name ? 'bw-theme-' + name + '-alt' : 'bw-theme-default-alt';
6171
+ bw.injectCSS(altCssStr, {
6172
+ id: altStyleId,
6173
+ append: false
6174
+ });
5630
6175
  }
5631
6176
 
5632
6177
  // Update bw.u color entries to reflect the palette
@@ -5645,11 +6190,65 @@
5645
6190
  color: '#ffffff'
5646
6191
  };
5647
6192
  }
5648
- return {
6193
+
6194
+ // Store active theme state
6195
+ var result = {
5649
6196
  css: cssStr,
5650
6197
  palette: palette,
5651
- name: name
6198
+ name: name,
6199
+ isLightPrimary: lightPrimary,
6200
+ alternate: {
6201
+ css: altCssStr,
6202
+ palette: altPalette
6203
+ }
5652
6204
  };
6205
+ bw._activeTheme = result;
6206
+ bw._activeThemeMode = 'primary';
6207
+ return result;
6208
+ };
6209
+
6210
+ /**
6211
+ * Apply a theme mode. Switches between primary and alternate palettes
6212
+ * by adding/removing the `bw-theme-alt` class on `<html>`.
6213
+ *
6214
+ * @param {string} mode - 'primary' | 'alternate' | 'light' | 'dark'
6215
+ * @returns {string} Active mode: 'primary' or 'alternate'
6216
+ * @category CSS & Styling
6217
+ * @see bw.generateTheme
6218
+ * @see bw.toggleTheme
6219
+ * @example
6220
+ * bw.applyTheme('alternate'); // switch to alternate palette
6221
+ * bw.applyTheme('dark'); // switch to whichever palette is darker
6222
+ * bw.applyTheme('primary'); // switch back to primary palette
6223
+ */
6224
+ bw.applyTheme = function (mode) {
6225
+ if (!bw._isBrowser) return mode || 'primary';
6226
+ var root = document.documentElement;
6227
+ var isLight = bw._activeTheme ? bw._activeTheme.isLightPrimary : true;
6228
+ var wantAlt;
6229
+ if (mode === 'primary') wantAlt = false;else if (mode === 'alternate') wantAlt = true;else if (mode === 'light') wantAlt = !isLight;else if (mode === 'dark') wantAlt = isLight;else wantAlt = false;
6230
+ if (wantAlt) {
6231
+ root.classList.add('bw-theme-alt');
6232
+ } else {
6233
+ root.classList.remove('bw-theme-alt');
6234
+ }
6235
+ bw._activeThemeMode = wantAlt ? 'alternate' : 'primary';
6236
+ return bw._activeThemeMode;
6237
+ };
6238
+
6239
+ /**
6240
+ * Toggle between primary and alternate theme palettes.
6241
+ *
6242
+ * @returns {string} Active mode after toggle: 'primary' or 'alternate'
6243
+ * @category CSS & Styling
6244
+ * @see bw.applyTheme
6245
+ * @see bw.generateTheme
6246
+ * @example
6247
+ * bw.toggleTheme(); // flip between primary and alternate
6248
+ */
6249
+ bw.toggleTheme = function () {
6250
+ var current = bw._activeThemeMode || 'primary';
6251
+ return bw.applyTheme(current === 'primary' ? 'alternate' : 'primary');
5653
6252
  };
5654
6253
 
5655
6254
  // Expose color utility functions on bw namespace
@@ -5661,10 +6260,18 @@
5661
6260
  bw.textOnColor = textOnColor;
5662
6261
  bw.deriveShades = deriveShades;
5663
6262
  bw.derivePalette = derivePalette;
6263
+ bw.harmonize = harmonize;
6264
+ bw.deriveAlternateSeed = deriveAlternateSeed;
6265
+ bw.deriveAlternateConfig = deriveAlternateConfig;
6266
+ bw.isLightPalette = isLightPalette;
5664
6267
 
5665
6268
  // Expose layout and theme presets
5666
6269
  bw.SPACING_PRESETS = SPACING_PRESETS;
5667
6270
  bw.RADIUS_PRESETS = RADIUS_PRESETS;
6271
+ bw.TYPE_RATIO_PRESETS = TYPE_RATIO_PRESETS;
6272
+ bw.ELEVATION_PRESETS = ELEVATION_PRESETS;
6273
+ bw.MOTION_PRESETS = MOTION_PRESETS;
6274
+ bw.generateTypeScale = generateTypeScale;
5668
6275
  bw.DEFAULT_PALETTE_CONFIG = DEFAULT_PALETTE_CONFIG;
5669
6276
  bw.THEME_PRESETS = THEME_PRESETS;
5670
6277
 
@@ -6753,9 +7360,13 @@
6753
7360
  /**
6754
7361
  * Create a sortable TACO table from an array of row objects.
6755
7362
  *
7363
+ * Returns a bare `<table>` TACO — no wrapper, title, or responsive scroll.
7364
+ * Use this when you need full control over table placement, or when embedding
7365
+ * the table inside your own layout. For a ready-to-use table with title,
7366
+ * responsive wrapper, and defaults (striped + hover), use `bw.makeDataTable()`.
7367
+ *
6756
7368
  * Auto-detects columns from data keys if not specified. Supports click-to-sort
6757
- * headers with ascending/descending indicators. Returns a TACO object —
6758
- * render with `bw.DOM()` or `bw.html()`.
7369
+ * headers with ascending/descending indicators.
6759
7370
  *
6760
7371
  * @param {Object} config - Table configuration
6761
7372
  * @param {Array<Object>} config.data - Array of row objects to display
@@ -7122,10 +7733,12 @@
7122
7733
  };
7123
7734
 
7124
7735
  /**
7125
- * Create a responsive data table with title and optional wrapper
7736
+ * Create a ready-to-use data table with title and responsive wrapper.
7126
7737
  *
7127
- * Wraps bw.makeTable() output in a responsive container div.
7128
- * Adds an optional title heading above the table.
7738
+ * Convenience wrapper around `bw.makeTable()` that adds a title heading,
7739
+ * responsive horizontal scroll container, and defaults to striped + hover.
7740
+ * Use this for the common case; use `bw.makeTable()` when you need a bare
7741
+ * table element with no wrapper.
7129
7742
  *
7130
7743
  * @param {Object} config - Table configuration
7131
7744
  * @param {string} [config.title] - Table title heading