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) :
@@ -11,14 +11,14 @@
11
11
  */
12
12
 
13
13
  const VERSION_INFO = {
14
- version: '2.0.12',
14
+ version: '2.0.14',
15
15
  name: 'bitwrench',
16
16
  description: 'A library for javascript UI functions.',
17
17
  license: 'BSD-2-Clause',
18
18
  homepage: 'https://deftio.github.com/bitwrench/pages',
19
19
  repository: 'git+https://github.com/deftio/bitwrench.git',
20
20
  author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
21
- buildDate: '2026-03-07T22:31:35.755Z'
21
+ buildDate: '2026-03-08T08:04:06.572Z'
22
22
  };
23
23
 
24
24
  /**
@@ -276,6 +276,29 @@
276
276
  return relativeLuminance(hex) > 0.179 ? '#000' : '#fff';
277
277
  }
278
278
 
279
+ /**
280
+ * Shift a color's hue toward a target hue by a given amount.
281
+ * Uses shortest-arc interpolation on the hue wheel.
282
+ * @param {string} sourceHex - Color to shift
283
+ * @param {string} targetHex - Color whose hue to shift toward
284
+ * @param {number} [amount=0.20] - 0 = no shift, 1 = full shift to target hue
285
+ * @returns {string} Harmonized hex color
286
+ */
287
+ function harmonize(sourceHex, targetHex, amount) {
288
+ if (amount === undefined) amount = 0.20;
289
+ if (amount === 0) return sourceHex;
290
+ var srcHsl = hexToHsl(sourceHex);
291
+ var tgtHsl = hexToHsl(targetHex);
292
+
293
+ // Shortest-arc hue interpolation
294
+ var diff = tgtHsl[0] - srcHsl[0];
295
+ if (diff > 180) diff -= 360;
296
+ if (diff < -180) diff += 360;
297
+
298
+ var newHue = (srcHsl[0] + diff * amount + 360) % 360;
299
+ return hslToHex([newHue, srcHsl[1], srcHsl[2]]);
300
+ }
301
+
279
302
  /**
280
303
  * Derive a full shade palette for a single semantic color.
281
304
  * @param {string} hex - Base color hex
@@ -295,31 +318,128 @@
295
318
  };
296
319
  }
297
320
 
321
+ /**
322
+ * Derive the alternate (luminance-inverted) version of a single seed color.
323
+ * Preserves hue, mirrors lightness, adjusts saturation for readability.
324
+ * @param {string} hex - Seed hex color
325
+ * @returns {string} Alternate hex color
326
+ */
327
+ function deriveAlternateSeed(hex) {
328
+ var hsl = hexToHsl(hex);
329
+ var h = hsl[0], s = hsl[1], l = hsl[2];
330
+ var altL, altS;
331
+
332
+ if (l > 50) {
333
+ // Light color → make dark. Map 50-100 → 30-10 range
334
+ altL = clip(100 - l - 10, 8, 40);
335
+ // Reduce saturation slightly — vivid colors at low lightness look garish
336
+ altS = clip(s * 0.85, 0, 100);
337
+ } else {
338
+ // Dark color → make light. Map 0-50 → 65-92 range
339
+ altL = clip(100 - l + 10, 60, 92);
340
+ // Slightly increase saturation for light variant
341
+ altS = clip(s * 1.1, 0, 100);
342
+ }
343
+
344
+ return hslToHex([h, altS, altL]);
345
+ }
346
+
347
+ /**
348
+ * Determine whether a palette config is "light-flavored" based on
349
+ * the average luminance of its seed colors.
350
+ * @param {Object} config - Theme config with primary, secondary hex colors
351
+ * @returns {boolean} true if the seeds are predominantly light
352
+ */
353
+ function isLightPalette(config) {
354
+ var lum = relativeLuminance(config.primary);
355
+ if (config.secondary) lum = (lum + relativeLuminance(config.secondary)) / 2;
356
+ if (config.tertiary) lum = (lum * 2 + relativeLuminance(config.tertiary)) / 3;
357
+ return lum > 0.179;
358
+ }
359
+
360
+ /**
361
+ * Derive a complete alternate config from a primary theme config.
362
+ * Each seed color is luminance-inverted; semantic colors are adjusted for
363
+ * the new luminance context.
364
+ * @param {Object} config - Primary theme config
365
+ * @returns {Object} Alternate theme config (same shape, inverted lightness)
366
+ */
367
+ function deriveAlternateConfig(config) {
368
+ var alt = {};
369
+ // Invert the user's seed colors
370
+ alt.primary = deriveAlternateSeed(config.primary);
371
+ alt.secondary = deriveAlternateSeed(config.secondary);
372
+ alt.tertiary = config.tertiary ? deriveAlternateSeed(config.tertiary) : alt.primary;
373
+
374
+ // Derive alternate surface colors from primary hue
375
+ var priHsl = hexToHsl(config.primary);
376
+ var h = priHsl[0];
377
+ var isLight = isLightPalette(config);
378
+
379
+ if (isLight) {
380
+ // Primary is light → alternate needs dark surfaces
381
+ alt.light = hslToHex([h, Math.min(priHsl[1], 15), 15]);
382
+ alt.dark = hslToHex([h, 5, 88]);
383
+ } else {
384
+ // Primary is dark → alternate needs light surfaces
385
+ alt.light = hslToHex([h, Math.min(priHsl[1], 10), 96]);
386
+ alt.dark = hslToHex([h, 10, 18]);
387
+ }
388
+
389
+ // Semantic colors: harmonize toward primary, then invert for alternate
390
+ var amt = config.harmonize !== undefined ? config.harmonize : 0.20;
391
+ var semanticDefaults = {
392
+ success: '#198754', danger: '#dc3545',
393
+ warning: '#f0ad4e', info: '#17a2b8'
394
+ };
395
+ var semantics = ['success', 'danger', 'warning', 'info'];
396
+ for (var i = 0; i < semantics.length; i++) {
397
+ var key = semantics[i];
398
+ var seed = config[key] || semanticDefaults[key];
399
+ var harmonized = harmonize(seed, config.primary, amt);
400
+ alt[key] = deriveAlternateSeed(harmonized);
401
+ }
402
+
403
+ // Semantic colors are already harmonized+inverted — don't re-harmonize in derivePalette
404
+ alt.harmonize = 0;
405
+
406
+ return alt;
407
+ }
408
+
298
409
  /**
299
410
  * Derive complete palette from a theme config object.
411
+ * Semantic colors are harmonized toward the primary hue (configurable).
412
+ * Light/dark surface colors are tinted with the primary hue.
300
413
  * @param {Object} config - Theme config with primary, secondary, tertiary, etc.
301
- * @returns {Object} Full palette with shades for all 8 semantic colors + tertiary
414
+ * @param {number} [config.harmonize=0.20] - Hue shift amount for semantic colors (0-1)
415
+ * @returns {Object} Full palette with shades for all 9 semantic colors
302
416
  */
303
417
  function derivePalette(config) {
304
- var defaults = {
305
- success: '#198754',
306
- danger: '#dc3545',
307
- warning: '#ffc107',
308
- info: '#0dcaf0',
309
- light: '#f8f9fa',
310
- dark: '#212529'
311
- };
418
+ var amt = config.harmonize !== undefined ? config.harmonize : 0.20;
419
+ var pri = config.primary;
420
+ var priHsl = hexToHsl(pri);
421
+ var h = priHsl[0];
422
+
423
+ // Semantic defaults — harmonized toward primary hue
424
+ var successBase = harmonize(config.success || '#198754', pri, amt);
425
+ var dangerBase = harmonize(config.danger || '#dc3545', pri, amt);
426
+ var warningBase = harmonize(config.warning || '#f0ad4e', pri, amt);
427
+ var infoBase = harmonize(config.info || '#17a2b8', pri, amt);
428
+
429
+ // Light/dark: derive from primary hue with low saturation (if not user-supplied)
430
+ var lightBase = config.light || hslToHex([h, 8, 97]);
431
+ var darkBase = config.dark || hslToHex([h, 10, 13]);
312
432
 
313
433
  var palette = {
314
- primary: deriveShades(config.primary),
434
+ primary: deriveShades(config.primary),
315
435
  secondary: deriveShades(config.secondary),
316
- tertiary: deriveShades(config.tertiary),
317
- success: deriveShades(config.success || defaults.success),
318
- danger: deriveShades(config.danger || defaults.danger),
319
- warning: deriveShades(config.warning || defaults.warning),
320
- info: deriveShades(config.info || defaults.info),
321
- light: deriveShades(config.light || defaults.light),
322
- dark: deriveShades(config.dark || defaults.dark)
436
+ tertiary: deriveShades(config.tertiary),
437
+ success: deriveShades(successBase),
438
+ danger: deriveShades(dangerBase),
439
+ warning: deriveShades(warningBase),
440
+ info: deriveShades(infoBase),
441
+ light: deriveShades(lightBase),
442
+ dark: deriveShades(darkBase)
323
443
  };
324
444
 
325
445
  return palette;
@@ -368,6 +488,88 @@
368
488
  pill: { btn: '50rem', card: '1rem', badge: '50rem', alert: '1rem', input: '50rem' }
369
489
  };
370
490
 
491
+ // ---- Typography scale presets ----
492
+
493
+ var TYPE_RATIO_PRESETS = {
494
+ tight: 1.125,
495
+ normal: 1.200,
496
+ relaxed: 1.250,
497
+ dramatic: 1.333
498
+ };
499
+
500
+ /**
501
+ * Generate a modular type scale from a base size and ratio.
502
+ * @param {number} base - Base font size in px (default 16)
503
+ * @param {number} ratio - Scale ratio (default 1.200)
504
+ * @returns {Object} { xs, sm, base, lg, xl, '2xl', '3xl', '4xl' } in px
505
+ */
506
+ function generateTypeScale(base, ratio) {
507
+ if (!base) base = 16;
508
+ if (!ratio) ratio = 1.200;
509
+ return {
510
+ xs: Math.round(base / (ratio * ratio)),
511
+ sm: Math.round(base / ratio),
512
+ base: base,
513
+ lg: Math.round(base * ratio),
514
+ xl: Math.round(base * ratio * ratio),
515
+ '2xl': Math.round(base * Math.pow(ratio, 3)),
516
+ '3xl': Math.round(base * Math.pow(ratio, 4)),
517
+ '4xl': Math.round(base * Math.pow(ratio, 5))
518
+ };
519
+ }
520
+
521
+ // ---- Elevation (shadow depth) presets ----
522
+
523
+ var ELEVATION_PRESETS = {
524
+ flat: {
525
+ sm: 'none',
526
+ md: 'none',
527
+ lg: 'none',
528
+ xl: 'none'
529
+ },
530
+ sm: {
531
+ sm: '0 1px 2px rgba(0,0,0,0.05)',
532
+ md: '0 1px 3px rgba(0,0,0,0.08)',
533
+ lg: '0 2px 6px rgba(0,0,0,0.10)',
534
+ xl: '0 4px 12px rgba(0,0,0,0.12)'
535
+ },
536
+ md: {
537
+ sm: '0 1px 3px rgba(0,0,0,0.08)',
538
+ md: '0 2px 6px rgba(0,0,0,0.12)',
539
+ lg: '0 4px 12px rgba(0,0,0,0.16)',
540
+ xl: '0 8px 24px rgba(0,0,0,0.20)'
541
+ },
542
+ lg: {
543
+ sm: '0 2px 4px rgba(0,0,0,0.10)',
544
+ md: '0 4px 12px rgba(0,0,0,0.16)',
545
+ lg: '0 8px 24px rgba(0,0,0,0.22)',
546
+ xl: '0 16px 48px rgba(0,0,0,0.28)'
547
+ }
548
+ };
549
+
550
+ // ---- Motion (transition) presets ----
551
+
552
+ var MOTION_PRESETS = {
553
+ reduced: {
554
+ fast: '0ms',
555
+ normal: '0ms',
556
+ slow: '0ms',
557
+ easing: 'linear'
558
+ },
559
+ standard: {
560
+ fast: '100ms',
561
+ normal: '200ms',
562
+ slow: '300ms',
563
+ easing: 'ease-out'
564
+ },
565
+ expressive: {
566
+ fast: '150ms',
567
+ normal: '300ms',
568
+ slow: '500ms',
569
+ easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)'
570
+ }
571
+ };
572
+
371
573
  /**
372
574
  * Default palette config — matches existing hardcoded colors
373
575
  */
@@ -377,8 +579,8 @@
377
579
  tertiary: '#006666',
378
580
  success: '#198754',
379
581
  danger: '#dc3545',
380
- warning: '#ffc107',
381
- info: '#0dcaf0',
582
+ warning: '#b38600',
583
+ info: '#0891b2',
382
584
  light: '#f8f9fa',
383
585
  dark: '#212529'
384
586
  };
@@ -403,18 +605,32 @@
403
605
  };
404
606
 
405
607
  /**
406
- * Resolve layout config to spacing + radius objects
407
- * @param {Object} config - { spacing, radius, fontSize }
408
- * @returns {Object} { spacing, radius, fontSize }
608
+ * Resolve layout config to spacing, radius, typeScale, elevation, and motion objects.
609
+ * @param {Object} config - { spacing, radius, fontSize, typeRatio, elevation, motion }
610
+ * @returns {Object} { spacing, radius, fontSize, typeScale, elevation, motion }
409
611
  */
410
612
  function resolveLayout(config) {
411
613
  var sp = (config && config.spacing) || 'normal';
412
614
  var rd = (config && config.radius) || 'md';
413
615
  var fs = (config && config.fontSize) || 1.0;
616
+
617
+ // typeRatio: accept preset name or number
618
+ var tr = (config && config.typeRatio) || 'normal';
619
+ var ratioNum = typeof tr === 'string' ? (TYPE_RATIO_PRESETS[tr] || TYPE_RATIO_PRESETS.normal) : tr;
620
+
621
+ // elevation: accept preset name or object
622
+ var el = (config && config.elevation) || 'md';
623
+
624
+ // motion: accept preset name or object
625
+ var mo = (config && config.motion) || 'standard';
626
+
414
627
  return {
415
628
  spacing: typeof sp === 'string' ? (SPACING_PRESETS[sp] || SPACING_PRESETS.normal) : sp,
416
629
  radius: typeof rd === 'string' ? (RADIUS_PRESETS[rd] || RADIUS_PRESETS.md) : rd,
417
- fontSize: fs
630
+ fontSize: fs,
631
+ typeScale: generateTypeScale(16, ratioNum),
632
+ elevation: typeof el === 'string' ? (ELEVATION_PRESETS[el] || ELEVATION_PRESETS.md) : el,
633
+ motion: typeof mo === 'string' ? (MOTION_PRESETS[mo] || MOTION_PRESETS.standard) : mo
418
634
  };
419
635
  }
420
636
 
@@ -438,12 +654,13 @@
438
654
  // Themed CSS generators
439
655
  // =========================================================================
440
656
 
441
- function generateTypographyThemed(scope, palette) {
657
+ function generateTypographyThemed(scope, palette, layout) {
658
+ var mot = layout.motion;
442
659
  var rules = {};
443
660
  rules[scopeSelector(scope, 'a')] = {
444
661
  'color': palette.primary.base,
445
662
  'text-decoration': 'none',
446
- 'transition': 'color 0.15s'
663
+ 'transition': 'color ' + mot.fast + ' ' + mot.easing
447
664
  };
448
665
  rules[scopeSelector(scope, 'a:hover')] = {
449
666
  'color': palette.primary.hover,
@@ -463,7 +680,8 @@
463
680
  'border-radius': rd.btn
464
681
  };
465
682
  rules[scopeSelector(scope, '.bw-btn:focus-visible')] = {
466
- 'outline': '0',
683
+ 'outline': '2px solid currentColor',
684
+ 'outline-offset': '2px',
467
685
  'box-shadow': '0 0 0 3px ' + palette.primary.focus
468
686
  };
469
687
 
@@ -553,14 +771,15 @@
553
771
  var sp = layout.spacing;
554
772
  var rd = layout.radius;
555
773
 
774
+ var elev = layout.elevation;
556
775
  rules[scopeSelector(scope, '.bw-card')] = {
557
776
  'background-color': '#fff',
558
777
  'border': '1px solid ' + palette.light.border,
559
778
  'border-radius': rd.card,
560
- 'box-shadow': '0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04)'
779
+ 'box-shadow': elev.sm
561
780
  };
562
781
  rules[scopeSelector(scope, '.bw-card:hover')] = {
563
- 'box-shadow': '0 4px 12px rgba(0,0,0,.1), 0 2px 4px rgba(0,0,0,.06)'
782
+ 'box-shadow': elev.md
564
783
  };
565
784
  rules[scopeSelector(scope, '.bw-card-body')] = {
566
785
  'padding': sp.card
@@ -607,6 +826,8 @@
607
826
  };
608
827
  rules[scopeSelector(scope, '.bw-form-control:focus')] = {
609
828
  'border-color': palette.primary.border,
829
+ 'outline': '2px solid ' + palette.primary.base,
830
+ 'outline-offset': '-1px',
610
831
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
611
832
  };
612
833
  rules[scopeSelector(scope, '.bw-form-control::placeholder')] = {
@@ -764,7 +985,8 @@
764
985
  'border-color': palette.light.border
765
986
  };
766
987
  rules[scopeSelector(scope, '.bw-page-link:focus')] = {
767
- 'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
988
+ 'outline': '2px solid ' + palette.primary.base,
989
+ 'outline-offset': '-2px'
768
990
  };
769
991
  rules[scopeSelector(scope, '.bw-page-item.bw-active .bw-page-link')] = {
770
992
  'color': palette.primary.textOn,
@@ -923,12 +1145,12 @@
923
1145
  return rules;
924
1146
  }
925
1147
 
926
- function generateModalThemed(scope, palette) {
1148
+ function generateModalThemed(scope, palette, layout) {
927
1149
  var rules = {};
928
1150
  rules[scopeSelector(scope, '.bw-modal-content')] = {
929
1151
  'background-color': '#fff',
930
1152
  'border-color': palette.light.border,
931
- 'box-shadow': '0 0.5rem 1rem rgba(0,0,0,0.15)'
1153
+ 'box-shadow': layout.elevation.lg
932
1154
  };
933
1155
  rules[scopeSelector(scope, '.bw-modal-header')] = {
934
1156
  'border-bottom-color': palette.light.border
@@ -942,12 +1164,12 @@
942
1164
  return rules;
943
1165
  }
944
1166
 
945
- function generateToastThemed(scope, palette) {
1167
+ function generateToastThemed(scope, palette, layout) {
946
1168
  var rules = {};
947
1169
  rules[scopeSelector(scope, '.bw-toast')] = {
948
1170
  'background-color': '#fff',
949
1171
  'border-color': 'rgba(0,0,0,0.1)',
950
- 'box-shadow': '0 0.5rem 1rem rgba(0,0,0,0.15)'
1172
+ 'box-shadow': layout.elevation.lg
951
1173
  };
952
1174
  rules[scopeSelector(scope, '.bw-toast-header')] = {
953
1175
  'border-bottom-color': 'rgba(0,0,0,0.05)'
@@ -961,12 +1183,12 @@
961
1183
  return rules;
962
1184
  }
963
1185
 
964
- function generateDropdownThemed(scope, palette) {
1186
+ function generateDropdownThemed(scope, palette, layout) {
965
1187
  var rules = {};
966
1188
  rules[scopeSelector(scope, '.bw-dropdown-menu')] = {
967
1189
  'background-color': '#fff',
968
1190
  'border-color': palette.light.border,
969
- 'box-shadow': '0 0.5rem 1rem rgba(0,0,0,0.15)'
1191
+ 'box-shadow': layout.elevation.md
970
1192
  };
971
1193
  rules[scopeSelector(scope, '.bw-dropdown-item')] = {
972
1194
  'color': palette.dark.base
@@ -1032,7 +1254,7 @@
1032
1254
  function generateThemedCSS(scopeName, palette, layout) {
1033
1255
  return Object.assign({},
1034
1256
  generateResetThemed(scopeName, palette),
1035
- generateTypographyThemed(scopeName, palette),
1257
+ generateTypographyThemed(scopeName, palette, layout),
1036
1258
  generateButtons(scopeName, palette, layout),
1037
1259
  generateAlerts(scopeName, palette, layout),
1038
1260
  generateBadges(scopeName, palette),
@@ -1051,9 +1273,9 @@
1051
1273
  generateSectionsThemed(scopeName, palette),
1052
1274
  generateAccordionThemed(scopeName, palette),
1053
1275
  generateCarouselThemed(scopeName, palette),
1054
- generateModalThemed(scopeName, palette),
1055
- generateToastThemed(scopeName, palette),
1056
- generateDropdownThemed(scopeName, palette),
1276
+ generateModalThemed(scopeName, palette, layout),
1277
+ generateToastThemed(scopeName, palette, layout),
1278
+ generateDropdownThemed(scopeName, palette, layout),
1057
1279
  generateSwitchThemed(scopeName, palette),
1058
1280
  generateSkeletonThemed(scopeName, palette),
1059
1281
  generateAvatarThemed(scopeName, palette),
@@ -1206,11 +1428,23 @@
1206
1428
  // =========================================================================
1207
1429
 
1208
1430
  /**
1209
- * Structural styles contain only layout, sizing, spacing, and behavior
1210
- * properties. No colors, backgrounds, shadows, or border-colors.
1211
- * These never change with themes.
1431
+ * Structural styles layout, sizing, spacing, positioning, and behavior.
1212
1432
  *
1213
- * @returns {Object} CSS rules object
1433
+ * POLICY: No colors, backgrounds, shadows, or border-colors in this function.
1434
+ * All cosmetic values belong in `defaultStyles.*` sections (unthemed defaults)
1435
+ * or in `generateThemedCSS()` (theme-driven colors).
1436
+ *
1437
+ * Exception: `.bw-progress-bar-striped` uses rgba(255,255,255,.15) for the
1438
+ * stripe pattern overlay. This is theme-neutral — a semi-transparent white
1439
+ * gradient that creates visible stripes on any background color.
1440
+ *
1441
+ * Architecture:
1442
+ * getStructuralStyles() → layout-only rules (never change with themes)
1443
+ * defaultStyles.* → cosmetic defaults (colors, shadows, borders)
1444
+ * generateThemedCSS() → palette-driven cosmetics from seed colors
1445
+ * generateAlternateCSS() → alternate palette (luminance-inverted)
1446
+ *
1447
+ * @returns {Object} CSS rules object (layout-only, theme-independent)
1214
1448
  */
1215
1449
  function getStructuralStyles() {
1216
1450
  var rules = {};
@@ -1261,12 +1495,12 @@
1261
1495
  'text-decoration': 'none', 'vertical-align': 'middle', 'cursor': 'pointer',
1262
1496
  'user-select': 'none', 'border': '1px solid transparent',
1263
1497
  'padding': '0.5rem 1.125rem', 'font-size': '0.875rem', 'font-family': 'inherit',
1264
- 'border-radius': '6px', 'transition': 'all 0.15s cubic-bezier(0.4, 0, 0.2, 1)',
1498
+ 'border-radius': '6px', 'transition': 'all 0.15s ease-out',
1265
1499
  'gap': '0.5rem'
1266
1500
  };
1267
1501
  rules['.bw-btn:hover'] = { 'text-decoration': 'none', 'transform': 'translateY(-1px)' };
1268
1502
  rules['.bw-btn:active'] = { 'transform': 'translateY(0)' };
1269
- rules['.bw-btn:focus-visible'] = { 'outline': '0' };
1503
+ rules['.bw-btn:focus-visible'] = { 'outline': '2px solid currentColor', 'outline-offset': '2px' };
1270
1504
  rules['.bw-btn:disabled'] = { 'opacity': '0.5', 'cursor': 'not-allowed', 'pointer-events': 'none' };
1271
1505
  rules['.bw-btn-lg'] = { 'padding': '0.625rem 1.5rem', 'font-size': '1rem', 'border-radius': '8px' };
1272
1506
  rules['.bw-btn-sm'] = { 'padding': '0.25rem 0.75rem', 'font-size': '0.8125rem', 'border-radius': '5px' };
@@ -1276,7 +1510,7 @@
1276
1510
  'position': 'relative', 'display': 'flex', 'flex-direction': 'column',
1277
1511
  'min-width': '0', 'height': '100%', 'word-wrap': 'break-word',
1278
1512
  'background-clip': 'border-box', 'border': '1px solid transparent',
1279
- 'border-radius': '8px', 'transition': 'box-shadow 0.2s cubic-bezier(0.4,0,0.2,1), transform 0.2s cubic-bezier(0.4,0,0.2,1)',
1513
+ 'border-radius': '8px', 'transition': 'box-shadow 0.2s ease-out, transform 0.2s ease-out',
1280
1514
  'margin-bottom': '1.5rem', 'overflow': 'hidden'
1281
1515
  };
1282
1516
  rules['.bw-card-body'] = { 'flex': '1 1 auto', 'padding': '1.25rem 1.5rem' };
@@ -1285,7 +1519,7 @@
1285
1519
  rules['.bw-card-text'] = { 'margin-bottom': '0', 'font-size': '0.9375rem', 'line-height': '1.6' };
1286
1520
  rules['.bw-card-header'] = { 'padding': '0.875rem 1.5rem', 'margin-bottom': '0', 'font-weight': '600', 'font-size': '0.875rem' };
1287
1521
  rules['.bw-card-footer'] = { 'padding': '0.75rem 1.5rem', 'font-size': '0.875rem' };
1288
- rules['.bw-card-hoverable'] = { 'transition': 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)' };
1522
+ rules['.bw-card-hoverable'] = { 'transition': 'all 0.3s ease-out' };
1289
1523
  rules['.bw-card-img-top'] = { 'width': '100%', 'border-top-left-radius': '7px', 'border-top-right-radius': '7px' };
1290
1524
  rules['.bw-card-img-bottom'] = { 'width': '100%', 'border-bottom-left-radius': '7px', 'border-bottom-right-radius': '7px' };
1291
1525
  rules['.bw-card-img-left'] = { 'width': '40%', 'object-fit': 'cover' };
@@ -1298,10 +1532,10 @@
1298
1532
  'font-size': '0.9375rem', 'font-weight': '400', 'line-height': '1.5',
1299
1533
  'background-clip': 'padding-box', 'appearance': 'none',
1300
1534
  'border': '1px solid transparent', 'border-radius': '6px',
1301
- 'transition': 'border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out',
1535
+ 'transition': 'border-color 0.15s ease-out, box-shadow 0.15s ease-out',
1302
1536
  'font-family': 'inherit'
1303
1537
  };
1304
- rules['.bw-form-control:focus'] = { 'outline': '0' };
1538
+ rules['.bw-form-control:focus'] = { 'outline': '2px solid currentColor', 'outline-offset': '-1px' };
1305
1539
  rules['.bw-form-control::placeholder'] = { 'opacity': '1' };
1306
1540
  rules['.bw-form-label'] = { 'display': 'block', 'margin-bottom': '0.375rem', 'font-size': '0.875rem', 'font-weight': '600' };
1307
1541
  rules['.bw-form-group'] = { 'margin-bottom': '1.25rem' };
@@ -1313,6 +1547,10 @@
1313
1547
  };
1314
1548
  rules['textarea.bw-form-control'] = { 'min-height': '5rem', 'resize': 'vertical' };
1315
1549
 
1550
+ // Form validation (structural)
1551
+ rules['.bw-valid-feedback'] = { 'display': 'block', 'font-size': '0.875rem', 'margin-top': '0.25rem' };
1552
+ rules['.bw-invalid-feedback'] = { 'display': 'block', 'font-size': '0.875rem', 'margin-top': '0.25rem' };
1553
+
1316
1554
  // Form checks (structural)
1317
1555
  Object.assign(rules, {
1318
1556
  '.bw-form-check': { 'display': 'flex', 'align-items': 'center', 'gap': '0.5rem', 'min-height': '1.5rem', 'margin-bottom': '0.25rem' },
@@ -1371,13 +1609,13 @@
1371
1609
 
1372
1610
  // Badges (structural)
1373
1611
  rules['.bw-badge'] = {
1374
- 'display': 'inline-block', 'padding': '.4em .75em', 'font-size': '.875em',
1612
+ 'display': 'inline-block', 'padding': '0.375rem 0.625rem', 'font-size': '0.875rem',
1375
1613
  'font-weight': '600', 'line-height': '1.3', 'text-align': 'center',
1376
1614
  'white-space': 'nowrap', 'vertical-align': 'baseline', 'border-radius': '.375rem'
1377
1615
  };
1378
1616
  rules['.bw-badge:empty'] = { 'display': 'none' };
1379
- rules['.bw-badge-sm'] = { 'font-size': '.75em', 'padding': '.25em .5em' };
1380
- rules['.bw-badge-lg'] = { 'font-size': '1em', 'padding': '.5em .9em' };
1617
+ rules['.bw-badge-sm'] = { 'font-size': '0.75rem', 'padding': '0.25rem 0.5rem' };
1618
+ rules['.bw-badge-lg'] = { 'font-size': '1rem', 'padding': '0.5rem 0.875rem' };
1381
1619
  rules['.bw-badge-pill'] = { 'border-radius': '50rem' };
1382
1620
 
1383
1621
  // Progress (structural)
@@ -1385,7 +1623,7 @@
1385
1623
  rules['.bw-progress-bar'] = {
1386
1624
  'display': 'flex', 'flex-direction': 'column', 'justify-content': 'center',
1387
1625
  'overflow': 'hidden', 'text-align': 'center', 'white-space': 'nowrap',
1388
- 'transition': 'width .6s ease', 'font-weight': '600'
1626
+ 'transition': 'width 0.3s ease-out', 'font-weight': '600'
1389
1627
  };
1390
1628
  rules['.bw-progress-bar-striped'] = {
1391
1629
  'background-image': 'linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)',
@@ -1402,7 +1640,7 @@
1402
1640
  'display': 'block', 'padding': '0.625rem 1rem', 'font-size': '0.875rem',
1403
1641
  'font-weight': '500', 'text-decoration': 'none', 'cursor': 'pointer',
1404
1642
  'border': 'none', 'background': 'transparent',
1405
- 'transition': 'color 0.15s, border-color 0.15s', 'font-family': 'inherit'
1643
+ 'transition': 'color 0.15s ease-out, background-color 0.15s ease-out, border-color 0.15s ease-out', 'font-family': 'inherit'
1406
1644
  };
1407
1645
  rules['.bw-nav-tabs .bw-nav-link'] = { 'border': 'none', 'border-bottom': '2px solid transparent', 'border-radius': '0', 'background-color': 'transparent' };
1408
1646
  rules['.bw-nav-pills .bw-nav-link'] = { 'border-radius': '6px' };
@@ -1420,7 +1658,8 @@
1420
1658
  rules['.bw-list-group-item:last-child'] = { 'border-bottom-right-radius': 'inherit', 'border-bottom-left-radius': 'inherit' };
1421
1659
  rules['.bw-list-group-item + .bw-list-group-item'] = { 'border-top-width': '0' };
1422
1660
  rules['.bw-list-group-item.disabled'] = { 'pointer-events': 'none' };
1423
- rules['a.bw-list-group-item'] = { 'cursor': 'pointer' };
1661
+ rules['a.bw-list-group-item'] = { 'cursor': 'pointer', 'transition': 'background-color 0.15s ease-out, color 0.15s ease-out' };
1662
+ rules['a.bw-list-group-item:focus-visible, .bw-list-group-item:focus-visible'] = { 'z-index': '2', 'outline': '2px solid currentColor', 'outline-offset': '-2px' };
1424
1663
  rules['.bw-list-group-flush'] = { 'border-radius': '0' };
1425
1664
  rules['.bw-list-group-flush > .bw-list-group-item'] = { 'border-width': '0 0 1px', 'border-radius': '0' };
1426
1665
  rules['.bw-list-group-flush > .bw-list-group-item:last-child'] = { 'border-bottom-width': '0' };
@@ -1431,16 +1670,19 @@
1431
1670
  rules['.bw-page-link'] = {
1432
1671
  'position': 'relative', 'display': 'block', 'padding': '0.375rem 0.75rem',
1433
1672
  'margin-left': '-1px', 'line-height': '1.25', 'text-decoration': 'none',
1434
- 'transition': 'color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out'
1673
+ 'transition': 'color 0.15s ease-out, background-color 0.15s ease-out, border-color 0.15s ease-out'
1435
1674
  };
1436
1675
  rules['.bw-page-item:first-child .bw-page-link'] = { 'margin-left': '0', 'border-top-left-radius': '0.375rem', 'border-bottom-left-radius': '0.375rem' };
1437
1676
  rules['.bw-page-item:last-child .bw-page-link'] = { 'border-top-right-radius': '0.375rem', 'border-bottom-right-radius': '0.375rem' };
1677
+ rules['.bw-page-link:focus-visible'] = { 'z-index': '3', 'outline': '2px solid currentColor', 'outline-offset': '-2px' };
1438
1678
 
1439
1679
  // Breadcrumb (structural)
1440
1680
  rules['.bw-breadcrumb'] = { 'display': 'flex', 'flex-wrap': 'wrap', 'padding': '0 0', 'margin-bottom': '1rem', 'list-style': 'none' };
1441
1681
  rules['.bw-breadcrumb-item'] = { 'display': 'flex' };
1442
1682
  rules['.bw-breadcrumb-item + .bw-breadcrumb-item'] = { 'padding-left': '0.5rem' };
1443
1683
  rules['.bw-breadcrumb-item + .bw-breadcrumb-item::before'] = { 'float': 'left', 'padding-right': '0.5rem', 'content': '"/"' };
1684
+ rules['.bw-breadcrumb-item a'] = { 'text-decoration': 'none', 'transition': 'color 0.15s ease-out' };
1685
+ rules['.bw-breadcrumb-item.active'] = { 'font-weight': '500' };
1444
1686
 
1445
1687
  // Hero (structural)
1446
1688
  rules['.bw-hero'] = { 'position': 'relative', 'overflow': 'hidden' };
@@ -1543,12 +1785,12 @@
1543
1785
  'position': 'relative', 'display': 'flex', 'align-items': 'center', 'width': '100%',
1544
1786
  'padding': '1rem 1.25rem', 'font-size': '1rem', 'font-weight': '500', 'text-align': 'left',
1545
1787
  'background-color': 'transparent', 'border': '0', 'overflow-anchor': 'none', 'cursor': 'pointer',
1546
- 'font-family': 'inherit', 'transition': 'color 0.15s ease-in-out, background-color 0.15s ease-in-out'
1788
+ 'font-family': 'inherit', 'transition': 'color 0.15s ease-out, background-color 0.15s ease-out'
1547
1789
  };
1548
1790
  rules['.bw-accordion-button::after'] = {
1549
1791
  'flex-shrink': '0', 'width': '1.25rem', 'height': '1.25rem', 'margin-left': 'auto',
1550
1792
  'content': '""', 'background-repeat': 'no-repeat', 'background-size': '1.25rem',
1551
- 'transition': 'transform 0.2s ease-in-out'
1793
+ 'transition': 'transform 0.2s ease-out'
1552
1794
  };
1553
1795
  rules['.bw-accordion-button:not(.bw-collapsed)::after'] = { 'transform': 'rotate(-180deg)' };
1554
1796
  rules['.bw-accordion-collapse'] = { 'max-height': '0', 'overflow': 'hidden', 'transition': 'max-height 0.3s ease' };
@@ -1557,10 +1799,13 @@
1557
1799
 
1558
1800
  // Modal (structural)
1559
1801
  rules['.bw-modal'] = {
1560
- 'display': 'none', 'position': 'fixed', 'top': '0', 'left': '0', 'width': '100%', 'height': '100%',
1561
- 'z-index': '1050', 'overflow-x': 'hidden', 'overflow-y': 'auto', 'opacity': '0', 'transition': 'opacity 0.15s linear'
1802
+ 'display': 'flex', 'align-items': 'center', 'justify-content': 'center',
1803
+ 'position': 'fixed', 'top': '0', 'left': '0', 'width': '100%', 'height': '100%',
1804
+ 'z-index': '1050', 'overflow-x': 'hidden', 'overflow-y': 'auto',
1805
+ 'opacity': '0', 'visibility': 'hidden', 'pointer-events': 'none',
1806
+ 'transition': 'opacity 0.2s ease, visibility 0.2s ease'
1562
1807
  };
1563
- rules['.bw-modal.bw-modal-show'] = { 'display': 'flex', 'align-items': 'center', 'justify-content': 'center', 'opacity': '1' };
1808
+ rules['.bw-modal.bw-modal-show'] = { 'opacity': '1', 'visibility': 'visible', 'pointer-events': 'auto' };
1564
1809
  rules['.bw-modal-dialog'] = {
1565
1810
  'position': 'relative', 'width': '100%', 'max-width': '500px', 'margin': '1.75rem auto',
1566
1811
  'pointer-events': 'none', 'transform': 'translateY(-20px)', 'transition': 'transform 0.2s ease-out'
@@ -1580,7 +1825,7 @@
1580
1825
 
1581
1826
  // Carousel (structural)
1582
1827
  rules['.bw-carousel'] = { 'position': 'relative', 'overflow': 'hidden', 'border-radius': '8px' };
1583
- rules['.bw-carousel-track'] = { 'display': 'flex', 'transition': 'transform 0.4s ease', 'height': '100%' };
1828
+ rules['.bw-carousel-track'] = { 'display': 'flex', 'transition': 'transform 0.3s ease-out', 'height': '100%' };
1584
1829
  rules['.bw-carousel-slide'] = { 'min-width': '100%', 'flex-shrink': '0', 'overflow': 'hidden', 'position': 'relative', 'display': 'flex', 'align-items': 'center', 'justify-content': 'center' };
1585
1830
  rules['.bw-carousel-slide img'] = { 'width': '100%', 'height': '100%', 'object-fit': 'cover' };
1586
1831
  rules['.bw-carousel-caption'] = { 'position': 'absolute', 'bottom': '0', 'left': '0', 'right': '0', 'padding': '0.75rem 1rem' };
@@ -1624,11 +1869,14 @@
1624
1869
  'border-bottom': '0', 'border-left': '0.3em solid transparent'
1625
1870
  };
1626
1871
  rules['.bw-dropdown-menu'] = {
1627
- 'position': 'absolute', 'top': '100%', 'left': '0', 'z-index': '1000', 'display': 'none',
1872
+ 'position': 'absolute', 'top': '100%', 'left': '0', 'z-index': '1000', 'display': 'block',
1628
1873
  'min-width': '10rem', 'padding': '0.5rem 0', 'margin': '0.125rem 0 0',
1629
- 'background-clip': 'padding-box', 'border-radius': '6px'
1874
+ 'background-clip': 'padding-box', 'border-radius': '6px',
1875
+ 'opacity': '0', 'visibility': 'hidden', 'transform': 'translateY(-4px)',
1876
+ 'pointer-events': 'none',
1877
+ 'transition': 'opacity 0.15s ease, transform 0.15s ease, visibility 0.15s ease'
1630
1878
  };
1631
- rules['.bw-dropdown-menu.bw-dropdown-show'] = { 'display': 'block' };
1879
+ rules['.bw-dropdown-menu.bw-dropdown-show'] = { 'opacity': '1', 'visibility': 'visible', 'transform': 'translateY(0)', 'pointer-events': 'auto' };
1632
1880
  rules['.bw-dropdown-menu-end'] = { 'left': 'auto', 'right': '0' };
1633
1881
  rules['.bw-dropdown-item'] = {
1634
1882
  'display': 'block', 'width': '100%', 'padding': '0.375rem 1rem', 'clear': 'both',
@@ -1636,6 +1884,7 @@
1636
1884
  'background-color': 'transparent', 'border': '0', 'font-size': '0.9375rem',
1637
1885
  'transition': 'background-color 0.15s, color 0.15s'
1638
1886
  };
1887
+ rules['.bw-dropdown-item:focus-visible'] = { 'outline': '2px solid currentColor', 'outline-offset': '-2px' };
1639
1888
  rules['.bw-dropdown-divider'] = { 'height': '0', 'margin': '0.5rem 0', 'overflow': 'hidden', 'opacity': '1' };
1640
1889
 
1641
1890
  // Switch (structural)
@@ -1643,7 +1892,7 @@
1643
1892
  rules['.bw-form-switch .bw-switch-input'] = {
1644
1893
  'width': '2em', 'height': '1.125em', 'margin-left': '-2.5em', 'border-radius': '2em',
1645
1894
  'appearance': 'none', 'background-position': 'left center', 'background-repeat': 'no-repeat',
1646
- 'background-size': 'contain', 'transition': 'background-position 0.15s ease-in-out, background-color 0.15s ease-in-out',
1895
+ 'background-size': 'contain', 'transition': 'background-position 0.15s ease-out, background-color 0.15s ease-out',
1647
1896
  'cursor': 'pointer'
1648
1897
  };
1649
1898
  rules['.bw-form-switch .bw-switch-input:checked'] = { 'background-position': 'right center' };
@@ -1668,6 +1917,123 @@
1668
1917
  rules['.bw-avatar-lg'] = { 'width': '4rem', 'height': '4rem', 'font-size': '1.25rem' };
1669
1918
  rules['.bw-avatar-xl'] = { 'width': '5rem', 'height': '5rem', 'font-size': '1.5rem' };
1670
1919
 
1920
+ // Stat card (structural)
1921
+ rules['.bw-stat-card'] = {
1922
+ 'border-radius': '8px', 'padding': '1.25rem',
1923
+ 'border-left': '4px solid transparent',
1924
+ 'transition': 'box-shadow 0.15s ease-out, transform 0.15s ease-out'
1925
+ };
1926
+ rules['.bw-stat-card:hover'] = { 'transform': 'translateY(-1px)' };
1927
+ rules['.bw-stat-icon'] = { 'font-size': '1.5rem', 'margin-bottom': '0.5rem' };
1928
+ rules['.bw-stat-value'] = { 'font-size': '2rem', 'font-weight': '700', 'line-height': '1.2' };
1929
+ rules['.bw-stat-label'] = { 'font-size': '0.875rem', 'margin-top': '0.25rem' };
1930
+ rules['.bw-stat-change'] = { 'font-size': '0.875rem', 'font-weight': '500', 'margin-top': '0.5rem' };
1931
+
1932
+ // Tooltip (structural)
1933
+ rules['.bw-tooltip-wrapper'] = { 'position': 'relative', 'display': 'inline-block' };
1934
+ rules['.bw-tooltip'] = {
1935
+ 'position': 'absolute', 'z-index': '999',
1936
+ 'padding': '0.375rem 0.75rem', 'border-radius': '4px', 'font-size': '0.875rem',
1937
+ 'white-space': 'nowrap', 'pointer-events': 'none',
1938
+ 'opacity': '0', 'visibility': 'hidden',
1939
+ 'transition': 'opacity 0.15s ease, visibility 0.15s ease, transform 0.15s ease'
1940
+ };
1941
+ rules['.bw-tooltip.bw-tooltip-show'] = { 'opacity': '1', 'visibility': 'visible' };
1942
+ rules['.bw-tooltip-top'] = { 'bottom': '100%', 'left': '50%', 'transform': 'translateX(-50%) translateY(-4px)', 'margin-bottom': '4px' };
1943
+ rules['.bw-tooltip-top.bw-tooltip-show'] = { 'transform': 'translateX(-50%) translateY(0)' };
1944
+ rules['.bw-tooltip-bottom'] = { 'top': '100%', 'left': '50%', 'transform': 'translateX(-50%) translateY(4px)', 'margin-top': '4px' };
1945
+ rules['.bw-tooltip-bottom.bw-tooltip-show'] = { 'transform': 'translateX(-50%) translateY(0)' };
1946
+ rules['.bw-tooltip-left'] = { 'right': '100%', 'top': '50%', 'transform': 'translateY(-50%) translateX(-4px)', 'margin-right': '4px' };
1947
+ rules['.bw-tooltip-left.bw-tooltip-show'] = { 'transform': 'translateY(-50%) translateX(0)' };
1948
+ rules['.bw-tooltip-right'] = { 'left': '100%', 'top': '50%', 'transform': 'translateY(-50%) translateX(4px)', 'margin-left': '4px' };
1949
+ rules['.bw-tooltip-right.bw-tooltip-show'] = { 'transform': 'translateY(-50%) translateX(0)' };
1950
+
1951
+ // Search input (structural)
1952
+ rules['.bw-search-input'] = { 'position': 'relative', 'display': 'flex', 'align-items': 'center' };
1953
+ rules['.bw-search-input .bw-search-field'] = { 'padding-right': '2.5rem' };
1954
+ rules['.bw-search-clear'] = {
1955
+ 'position': 'absolute', 'right': '0.5rem',
1956
+ 'display': 'flex', 'align-items': 'center', 'justify-content': 'center',
1957
+ 'width': '1.5rem', 'height': '1.5rem',
1958
+ 'border': 'none', 'background': 'none',
1959
+ 'font-size': '1.25rem', 'cursor': 'pointer', 'padding': '0',
1960
+ 'border-radius': '50%', 'transition': 'color 0.15s ease-out'
1961
+ };
1962
+
1963
+ // Range slider (structural)
1964
+ rules['.bw-range-wrapper'] = { 'margin-bottom': '1rem' };
1965
+ rules['.bw-range-label'] = { 'display': 'flex', 'justify-content': 'space-between', 'align-items': 'center', 'margin-bottom': '0.5rem', 'font-size': '0.875rem', 'font-weight': '500' };
1966
+ rules['.bw-range-value'] = { 'font-weight': '600' };
1967
+ rules['.bw-range'] = { 'width': '100%', 'height': '0.5rem', 'padding': '0', 'appearance': 'none', 'border': 'none', 'border-radius': '0.25rem', 'cursor': 'pointer', 'outline': 'none' };
1968
+ rules['.bw-range:disabled'] = { 'opacity': '0.5', 'cursor': 'not-allowed' };
1969
+
1970
+ // Media object (structural)
1971
+ rules['.bw-media'] = { 'display': 'flex', 'align-items': 'flex-start', 'gap': '1rem' };
1972
+ rules['.bw-media-reverse'] = { 'flex-direction': 'row-reverse' };
1973
+ rules['.bw-media-img'] = { 'border-radius': '50%', 'object-fit': 'cover', 'flex-shrink': '0' };
1974
+ rules['.bw-media-body'] = { 'flex': '1', 'min-width': '0' };
1975
+ rules['.bw-media-title'] = { 'margin': '0 0 0.25rem 0', 'font-size': '1rem', 'font-weight': '600', 'line-height': '1.3' };
1976
+
1977
+ // File upload (structural)
1978
+ rules['.bw-file-upload'] = {
1979
+ 'display': 'flex', 'flex-direction': 'column', 'align-items': 'center', 'justify-content': 'center',
1980
+ 'padding': '2rem', 'border': '2px dashed transparent', 'border-radius': '8px',
1981
+ 'cursor': 'pointer', 'text-align': 'center', 'position': 'relative',
1982
+ 'transition': 'border-color 0.15s ease-out, background-color 0.15s ease-out'
1983
+ };
1984
+ rules['.bw-file-upload-icon'] = { 'font-size': '2rem', 'margin-bottom': '0.5rem' };
1985
+ rules['.bw-file-upload-text'] = { 'font-size': '0.875rem' };
1986
+ rules['.bw-file-upload-input'] = {
1987
+ 'position': 'absolute', 'width': '1px', 'height': '1px', 'padding': '0',
1988
+ 'margin': '-1px', 'overflow': 'hidden', 'clip': 'rect(0,0,0,0)', 'border': '0'
1989
+ };
1990
+
1991
+ // Timeline (structural)
1992
+ rules['.bw-timeline'] = { 'position': 'relative', 'padding-left': '2rem' };
1993
+ rules['.bw-timeline-item'] = { 'position': 'relative', 'padding-bottom': '1.5rem' };
1994
+ rules['.bw-timeline-item:last-child'] = { 'padding-bottom': '0' };
1995
+ rules['.bw-timeline-marker'] = { 'position': 'absolute', 'left': '-1.75rem', 'top': '0.25rem', 'width': '0.75rem', 'height': '0.75rem', 'border-radius': '50%' };
1996
+ rules['.bw-timeline-content'] = { 'padding-left': '0.5rem' };
1997
+ rules['.bw-timeline-date'] = { 'font-size': '0.75rem', 'margin-bottom': '0.25rem', 'font-weight': '500' };
1998
+ rules['.bw-timeline-title'] = { 'font-size': '1rem', 'font-weight': '600', 'margin': '0 0 0.25rem 0', 'line-height': '1.3' };
1999
+ rules['.bw-timeline-text'] = { 'font-size': '0.875rem', 'margin': '0', 'line-height': '1.5' };
2000
+
2001
+ // Stepper (structural)
2002
+ rules['.bw-stepper'] = { 'display': 'flex', 'gap': '0' };
2003
+ rules['.bw-step'] = { 'flex': '1', 'display': 'flex', 'flex-direction': 'column', 'align-items': 'center', 'text-align': 'center', 'position': 'relative' };
2004
+ rules['.bw-step-indicator'] = { 'width': '2rem', 'height': '2rem', 'border-radius': '50%', 'display': 'flex', 'align-items': 'center', 'justify-content': 'center', 'font-size': '0.875rem', 'font-weight': '600', 'position': 'relative', 'z-index': '1', 'transition': 'background-color 0.2s ease-out, color 0.2s ease-out' };
2005
+ rules['.bw-step-body'] = { 'margin-top': '0.5rem' };
2006
+ rules['.bw-step-label'] = { 'font-size': '0.875rem', 'font-weight': '500' };
2007
+ rules['.bw-step-description'] = { 'font-size': '0.75rem', 'margin-top': '0.125rem' };
2008
+
2009
+ // Chip input (structural)
2010
+ rules['.bw-chip-input'] = { 'display': 'flex', 'flex-wrap': 'wrap', 'align-items': 'center', 'gap': '0.375rem', 'padding': '0.375rem 0.5rem', 'border-radius': '6px', 'min-height': '2.5rem', 'cursor': 'text', 'transition': 'border-color 0.15s ease-out, box-shadow 0.15s ease-out' };
2011
+ rules['.bw-chip'] = { 'display': 'inline-flex', 'align-items': 'center', 'gap': '0.25rem', 'padding': '0.125rem 0.5rem', 'border-radius': '1rem', 'font-size': '0.8125rem', 'line-height': '1.5', 'white-space': 'nowrap' };
2012
+ rules['.bw-chip-remove'] = { 'display': 'inline-flex', 'align-items': 'center', 'justify-content': 'center', 'width': '1rem', 'height': '1rem', 'border': 'none', 'background': 'none', 'font-size': '0.875rem', 'cursor': 'pointer', 'padding': '0', 'border-radius': '50%', 'transition': 'color 0.15s ease-out, background-color 0.15s ease-out' };
2013
+ rules['.bw-chip-field'] = { 'flex': '1', 'min-width': '80px', 'border': 'none', 'outline': 'none', 'font-size': '0.875rem', 'padding': '0.125rem 0', 'background': 'transparent' };
2014
+
2015
+ // Popover (structural)
2016
+ rules['.bw-popover-wrapper'] = { 'position': 'relative', 'display': 'inline-block' };
2017
+ rules['.bw-popover-trigger'] = { 'cursor': 'pointer' };
2018
+ rules['.bw-popover'] = {
2019
+ 'position': 'absolute', 'z-index': '1000',
2020
+ 'min-width': '200px', 'max-width': '320px',
2021
+ 'border-radius': '8px',
2022
+ 'pointer-events': 'none', 'opacity': '0', 'visibility': 'hidden',
2023
+ 'transition': 'opacity 0.15s ease, visibility 0.15s ease, transform 0.15s ease'
2024
+ };
2025
+ rules['.bw-popover.bw-popover-show'] = { 'opacity': '1', 'visibility': 'visible', 'pointer-events': 'auto' };
2026
+ rules['.bw-popover-header'] = { 'padding': '0.625rem 0.875rem', 'font-weight': '600', 'font-size': '0.9375rem' };
2027
+ rules['.bw-popover-body'] = { 'padding': '0.75rem 0.875rem', 'font-size': '0.875rem', 'line-height': '1.5' };
2028
+ rules['.bw-popover-top'] = { 'bottom': '100%', 'left': '50%', 'transform': 'translateX(-50%) translateY(-8px)', 'margin-bottom': '8px' };
2029
+ rules['.bw-popover-top.bw-popover-show'] = { 'transform': 'translateX(-50%) translateY(0)' };
2030
+ rules['.bw-popover-bottom'] = { 'top': '100%', 'left': '50%', 'transform': 'translateX(-50%) translateY(8px)', 'margin-top': '8px' };
2031
+ rules['.bw-popover-bottom.bw-popover-show'] = { 'transform': 'translateX(-50%) translateY(0)' };
2032
+ rules['.bw-popover-left'] = { 'right': '100%', 'top': '50%', 'transform': 'translateY(-50%) translateX(-8px)', 'margin-right': '8px' };
2033
+ rules['.bw-popover-left.bw-popover-show'] = { 'transform': 'translateY(-50%) translateX(0)' };
2034
+ rules['.bw-popover-right'] = { 'left': '100%', 'top': '50%', 'transform': 'translateY(-50%) translateX(8px)', 'margin-left': '8px' };
2035
+ rules['.bw-popover-right.bw-popover-show'] = { 'transform': 'translateY(-50%) translateX(0)' };
2036
+
1671
2037
  // Bar chart (structural)
1672
2038
  rules['.bw-bar-chart-container'] = {
1673
2039
  'padding': '1rem', 'border': '1px solid transparent', 'border-radius': '8px'
@@ -1681,7 +2047,7 @@
1681
2047
  };
1682
2048
  rules['.bw-bar'] = {
1683
2049
  'width': '100%', 'border-radius': '3px 3px 0 0',
1684
- 'transition': 'height 0.5s ease', 'min-height': '4px'
2050
+ 'transition': 'height 0.3s ease-out', 'min-height': '4px'
1685
2051
  };
1686
2052
  rules['.bw-bar:hover'] = { 'opacity': '0.85' };
1687
2053
  rules['.bw-bar-value'] = {
@@ -1810,6 +2176,16 @@
1810
2176
  // Responsive grid
1811
2177
  Object.assign(rules, defaultStyles.responsive);
1812
2178
 
2179
+ // Accessibility: reduce motion for users who prefer it
2180
+ rules['@media (prefers-reduced-motion: reduce)'] = {
2181
+ '*, *::before, *::after': {
2182
+ 'animation-duration': '0.01ms !important',
2183
+ 'animation-iteration-count': '1 !important',
2184
+ 'transition-duration': '0.01ms !important',
2185
+ 'scroll-behavior': 'auto !important'
2186
+ }
2187
+ };
2188
+
1813
2189
  return addUnderscoreAliases(rules);
1814
2190
  }
1815
2191
 
@@ -1818,9 +2194,25 @@
1818
2194
  // =========================================================================
1819
2195
 
1820
2196
  /**
1821
- * Add underscore aliases for all bw- selectors
2197
+ * Add underscore aliases for all `.bw-` selectors.
2198
+ *
2199
+ * CSS CLASS NAMING CONVENTION:
2200
+ *
2201
+ * Canonical form: `.bw-btn`, `.bw-card`, `.bw-table-hover` (hyphens)
2202
+ * Underscore alias: `.bw_btn`, `.bw_card`, `.bw_table_hover` (underscores)
2203
+ *
2204
+ * Both forms are valid in HTML and produce identical results. The hyphen
2205
+ * form is canonical (used in docs, generated CSS, component output).
2206
+ * Underscore aliases exist because:
2207
+ * 1. TACO attribute keys use underscores (`bw_id`, `bw_meta`) — no
2208
+ * quoting needed in JS object literals
2209
+ * 2. Some users prefer underscores for consistency with JS identifiers
2210
+ *
2211
+ * Use `bw.normalizeClass()` to convert underscore classes to canonical
2212
+ * hyphen form at runtime if needed.
2213
+ *
1822
2214
  * @param {Object} rules - CSS rules object
1823
- * @returns {Object} - Rules with underscore aliases added
2215
+ * @returns {Object} Rules with underscore aliases added (both forms work)
1824
2216
  */
1825
2217
  function addUnderscoreAliases(rules) {
1826
2218
  const result = {};
@@ -1837,6 +2229,27 @@
1837
2229
  // =========================================================================
1838
2230
  // Theme tokens (backwards compatible)
1839
2231
  // =========================================================================
2232
+ //
2233
+ // DESIGN NOTE — Why no CSS custom properties (CSS variables)?
2234
+ //
2235
+ // Bitwrench targets IE11 as Tier 1 (see dev/bw2x-compatibility.md).
2236
+ // CSS custom properties (var(--color-primary)) are not supported in IE11.
2237
+ //
2238
+ // Instead, bitwrench uses class-scoped CSS generation:
2239
+ // 1. `defaultStyles.*` provides hardcoded cosmetic defaults
2240
+ // 2. `generateTheme(name, config)` generates a complete set of
2241
+ // class-scoped CSS rules from 3 seed colors (primary, secondary,
2242
+ // tertiary) — all components are restyled with the new palette
2243
+ // 3. `generateAlternateCSS()` produces the alternate (dark/light)
2244
+ // variant scoped under `.bw-theme-alt`
2245
+ //
2246
+ // This achieves full theme customization without CSS variables:
2247
+ // bw.generateTheme('ocean', { primary: '#006666', secondary: '#cc6633' })
2248
+ // → generates .ocean .bw-btn-primary { background: #006666; } etc.
2249
+ //
2250
+ // When IE11 support is dropped, CSS custom properties can be added as
2251
+ // an optimization (one rule with var() instead of many scoped rules).
2252
+ // The generateTheme() API stays the same — only the output format changes.
1840
2253
 
1841
2254
  let theme = {
1842
2255
  colors: {
@@ -1844,8 +2257,8 @@
1844
2257
  secondary: '#6c757d',
1845
2258
  success: '#198754',
1846
2259
  danger: '#dc3545',
1847
- warning: '#ffc107',
1848
- info: '#0dcaf0',
2260
+ warning: '#b38600',
2261
+ info: '#0891b2',
1849
2262
  light: '#f8f9fa',
1850
2263
  dark: '#212529',
1851
2264
  white: '#fff',
@@ -1881,214 +2294,63 @@
1881
2294
  '5xl': '3rem'
1882
2295
  }
1883
2296
  },
1884
- darkMode: false
1885
2297
  };
1886
2298
 
1887
2299
  /**
1888
- * Generate theme-aware dark mode CSS from a palette.
1889
- * Derives dark variants from the palette colors instead of using hardcoded values.
2300
+ * Generate alternate-palette CSS scoped under `.bw-theme-alt`.
2301
+ * Uses the same `generateThemedCSS()` pipeline as the primary palette
2302
+ * both sides go through identical code paths.
1890
2303
  *
1891
- * @param {Object} palette - From derivePalette()
1892
- * @returns {Object} CSS rules object for dark mode
2304
+ * @param {string} name - Theme scope name (e.g. 'ocean'). '' for global.
2305
+ * @param {Object} altPalette - From derivePalette(deriveAlternateConfig(...))
2306
+ * @param {Object} layout - From resolveLayout()
2307
+ * @returns {Object} CSS rules object scoped under .bw-theme-alt (+ optional .name)
1893
2308
  */
1894
- function generateDarkModeCSS(palette) {
1895
- var darkBg = adjustLightness(palette.primary.base, -15);
1896
- var darkBgHsl = hexToHsl(darkBg);
1897
- // Make it very dark (lightness 8-12%)
1898
- var bodyBg = hslToHex([darkBgHsl[0], Math.min(darkBgHsl[1], 30), 10]);
1899
- var surfaceBg = hslToHex([darkBgHsl[0], Math.min(darkBgHsl[1], 25), 15]);
1900
- var textColor = adjustLightness(palette.light.base, 5);
1901
- var borderColor = hslToHex([darkBgHsl[0], Math.min(darkBgHsl[1], 15), 30]);
1902
-
1903
- return {
1904
- ':root.bw-dark': {
1905
- '--bw-body-color': textColor,
1906
- '--bw-body-bg': bodyBg
1907
- },
1908
- '.bw-dark body, :root.bw-dark body': {
1909
- 'color': textColor,
1910
- 'background-color': bodyBg
1911
- },
1912
- '.bw-dark .bw-card': {
1913
- 'background-color': surfaceBg,
1914
- 'border-color': borderColor,
1915
- 'color': textColor
1916
- },
1917
- '.bw-dark .bw-card-header': {
1918
- 'background-color': bodyBg,
1919
- 'border-bottom-color': borderColor,
1920
- 'color': textColor
1921
- },
1922
- '.bw-dark .bw-card-footer': {
1923
- 'background-color': bodyBg,
1924
- 'border-top-color': borderColor,
1925
- 'color': textColor
1926
- },
1927
- '.bw-dark .bw-card-title': {
1928
- 'color': textColor
1929
- },
1930
- '.bw-dark .bw-navbar': {
1931
- 'background-color': surfaceBg,
1932
- 'border-bottom-color': borderColor
1933
- },
1934
- '.bw-dark .bw-navbar-brand': {
1935
- 'color': textColor
1936
- },
1937
- '.bw-dark .bw-navbar-nav .bw-nav-link': {
1938
- 'color': adjustLightness(textColor, -15)
1939
- },
1940
- '.bw-dark .bw-navbar-nav .bw-nav-link:hover': {
1941
- 'color': textColor
1942
- },
1943
- '.bw-dark .bw-form-control': {
1944
- 'background-color': surfaceBg,
1945
- 'border-color': borderColor,
1946
- 'color': textColor
1947
- },
1948
- '.bw-dark .bw-form-label': {
1949
- 'color': textColor
1950
- },
1951
- '.bw-dark .bw-form-text': {
1952
- 'color': adjustLightness(textColor, -20)
1953
- },
1954
- '.bw-dark .bw-table': {
1955
- 'color': textColor
1956
- },
1957
- '.bw-dark .bw-table > :not(caption) > * > *': {
1958
- 'border-bottom-color': borderColor
1959
- },
1960
- '.bw-dark .bw-table > thead > tr > *': {
1961
- 'background-color': bodyBg,
1962
- 'color': adjustLightness(textColor, -10),
1963
- 'border-bottom-color': borderColor
1964
- },
1965
- '.bw-dark .bw-table-striped > tbody > tr:nth-of-type(odd) > *': {
1966
- 'background-color': 'rgba(255, 255, 255, 0.05)'
1967
- },
1968
- '.bw-dark .bw-alert': {
1969
- 'border-color': borderColor
1970
- },
1971
- '.bw-dark .bw-list-group-item': {
1972
- 'background-color': surfaceBg,
1973
- 'border-color': borderColor,
1974
- 'color': textColor
1975
- },
1976
- '.bw-dark .bw-badge': {
1977
- 'color': textColor
1978
- },
1979
- '.bw-dark .bw-nav-tabs': {
1980
- 'border-bottom-color': borderColor
1981
- },
1982
- '.bw-dark .bw-nav-link': {
1983
- 'color': adjustLightness(textColor, -15)
1984
- },
1985
- '.bw-dark .bw-nav-tabs .bw-nav-link:hover': {
1986
- 'color': textColor,
1987
- 'border-bottom-color': borderColor
1988
- },
1989
- '.bw-dark .bw-pagination .bw-page-link': {
1990
- 'background-color': surfaceBg,
1991
- 'border-color': borderColor,
1992
- 'color': textColor
1993
- },
1994
- '.bw-dark .bw-breadcrumb-item + .bw-breadcrumb-item::before': {
1995
- 'color': adjustLightness(textColor, -20)
1996
- },
1997
- '.bw-dark .bw-breadcrumb-item.active': {
1998
- 'color': adjustLightness(textColor, -10)
1999
- },
2000
- '.bw-dark .bw-hero-light': {
2001
- 'background': surfaceBg,
2002
- 'color': textColor
2003
- },
2004
- '.bw-dark .bw-progress': {
2005
- 'background-color': surfaceBg
2006
- },
2007
- '.bw-dark .bw-section-subtitle': {
2008
- 'color': adjustLightness(textColor, -15)
2009
- },
2010
- '.bw-dark .bw-close': {
2011
- 'color': textColor
2012
- },
2013
- '.bw-dark .bw-accordion-item': {
2014
- 'background-color': surfaceBg,
2015
- 'border-color': borderColor
2016
- },
2017
- '.bw-dark .bw-accordion-button': {
2018
- 'color': textColor
2019
- },
2020
- '.bw-dark .bw-accordion-button:not(.bw-collapsed)': {
2021
- 'color': '#7dd3e0',
2022
- 'background-color': 'rgba(125, 211, 224, 0.1)'
2023
- },
2024
- '.bw-dark .bw-accordion-button:hover': {
2025
- 'background-color': bodyBg
2026
- },
2027
- '.bw-dark .bw-accordion-button:not(.bw-collapsed):hover': {
2028
- 'background-color': 'rgba(125, 211, 224, 0.15)'
2029
- },
2030
- '.bw-dark .bw-accordion-button:focus-visible': {
2031
- 'box-shadow': '0 0 0 0.2rem rgba(125, 211, 224, 0.3)'
2032
- },
2033
- '.bw-dark .bw-accordion-body': {
2034
- 'border-top-color': borderColor
2035
- },
2036
- '.bw-dark .bw-carousel': {
2037
- 'background-color': bodyBg
2038
- },
2039
- '.bw-dark .bw-carousel-control': {
2040
- 'background-color': 'rgba(255,255,255,0.15)'
2041
- },
2042
- '.bw-dark .bw-carousel-control:hover': {
2043
- 'background-color': 'rgba(255,255,255,0.25)'
2044
- },
2045
- '.bw-dark .bw-modal-content': {
2046
- 'background-color': surfaceBg,
2047
- 'border-color': borderColor
2048
- },
2049
- '.bw-dark .bw-modal-header': {
2050
- 'border-bottom-color': borderColor
2051
- },
2052
- '.bw-dark .bw-modal-footer': {
2053
- 'border-top-color': borderColor
2054
- },
2055
- '.bw-dark .bw-modal-title': {
2056
- 'color': textColor
2057
- },
2058
- '.bw-dark .bw-toast': {
2059
- 'background-color': surfaceBg,
2060
- 'border-color': borderColor
2061
- },
2062
- '.bw-dark .bw-toast-header': {
2063
- 'border-bottom-color': borderColor,
2064
- 'color': textColor
2065
- },
2066
- '.bw-dark .bw-dropdown-menu': {
2067
- 'background-color': surfaceBg,
2068
- 'border-color': borderColor
2069
- },
2070
- '.bw-dark .bw-dropdown-item': {
2071
- 'color': textColor
2072
- },
2073
- '.bw-dark .bw-dropdown-item:hover': {
2074
- 'background-color': bodyBg
2075
- },
2076
- '.bw-dark .bw-dropdown-divider': {
2077
- 'border-top-color': borderColor
2078
- },
2079
- '.bw-dark .bw-skeleton': {
2080
- 'background': 'linear-gradient(90deg, ' + borderColor + ' 25%, ' + surfaceBg + ' 37%, ' + borderColor + ' 63%)'
2081
- },
2082
- '.bw-dark h1, .bw-dark h2, .bw-dark h3, .bw-dark h4, .bw-dark h5, .bw-dark h6': {
2083
- 'color': textColor
2084
- },
2085
- '@media (prefers-color-scheme: dark)': {
2086
- ':root.bw-auto-dark body': {
2087
- 'color': textColor,
2088
- 'background-color': bodyBg
2309
+ function generateAlternateCSS(name, altPalette, layout) {
2310
+ // Generate themed CSS using the same pipeline as primary
2311
+ var rawRules = generateThemedCSS('', altPalette, layout);
2312
+
2313
+ // Re-scope every selector under .bw-theme-alt (+ optional theme name)
2314
+ var altPrefix = name ? '.' + name + '.bw-theme-alt' : '.bw-theme-alt';
2315
+ var altRules = {};
2316
+
2317
+ for (var sel in rawRules) {
2318
+ if (!rawRules.hasOwnProperty(sel)) continue;
2319
+
2320
+ if (sel.charAt(0) === '@') {
2321
+ // @media / @keyframes — recurse into the block
2322
+ var innerBlock = rawRules[sel];
2323
+ var altInner = {};
2324
+ for (var innerSel in innerBlock) {
2325
+ if (!innerBlock.hasOwnProperty(innerSel)) continue;
2326
+ altInner[altPrefix + ' ' + innerSel] = innerBlock[innerSel];
2089
2327
  }
2328
+ altRules[sel] = altInner;
2329
+ } else {
2330
+ // Regular selector — prefix with alt scope
2331
+ // Handle comma-separated selectors
2332
+ var parts = sel.split(',');
2333
+ var scopedParts = [];
2334
+ for (var i = 0; i < parts.length; i++) {
2335
+ var s = parts[i].trim();
2336
+ // 'body' selector gets special treatment: .bw-theme-alt body
2337
+ if (s === 'body' || s.indexOf('body') === 0) {
2338
+ scopedParts.push(altPrefix + ' ' + s);
2339
+ } else {
2340
+ scopedParts.push(altPrefix + ' ' + s);
2341
+ }
2342
+ }
2343
+ altRules[scopedParts.join(', ')] = rawRules[sel];
2090
2344
  }
2345
+ }
2346
+
2347
+ // Add body-level overrides for the alternate surface
2348
+ altRules[altPrefix + ' body, :root' + altPrefix + ' body'] = {
2349
+ 'color': altPalette.dark.base,
2350
+ 'background-color': altPalette.light.base
2091
2351
  };
2352
+
2353
+ return altRules;
2092
2354
  }
2093
2355
 
2094
2356
  function deepMerge(target, source) {
@@ -3736,8 +3998,10 @@
3736
3998
  /**
3737
3999
  * Generate responsive CSS with media query breakpoints.
3738
4000
  *
3739
- * Produces a CSS string with `@media` rules for sm (640px), md (768px),
3740
- * lg (1024px), and xl (1280px) breakpoints. Pass the result to `bw.injectCSS()`.
4001
+ * Produces a CSS string with `@media (min-width)` rules for standard
4002
+ * breakpoints. These match the grid system and theme.breakpoints:
4003
+ * sm: 576px, md: 768px, lg: 992px, xl: 1200px
4004
+ * Pass the result to `bw.injectCSS()`.
3741
4005
  *
3742
4006
  * @param {string} selector - CSS selector
3743
4007
  * @param {Object} breakpoints - Object with keys: base, sm, md, lg, xl
@@ -3754,7 +4018,7 @@
3754
4018
  * bw.injectCSS(css);
3755
4019
  */
3756
4020
  bw.responsive = function(selector, breakpoints) {
3757
- var sizes = { sm: '640px', md: '768px', lg: '1024px', xl: '1280px' };
4021
+ var sizes = { sm: '576px', md: '768px', lg: '992px', xl: '1200px' };
3758
4022
  var parts = [];
3759
4023
  Object.keys(breakpoints).forEach(function(key) {
3760
4024
  var rules = {};
@@ -3888,7 +4152,8 @@
3888
4152
  * @returns {Element|null} Style element if in browser, null in Node.js
3889
4153
  * @category CSS & Styling
3890
4154
  * @see bw.setTheme
3891
- * @see bw.toggleDarkMode
4155
+ * @see bw.applyTheme
4156
+ * @see bw.toggleTheme
3892
4157
  * @example
3893
4158
  * bw.loadDefaultStyles(); // inject all default CSS
3894
4159
  */
@@ -3955,53 +4220,6 @@
3955
4220
  return bw.getTheme();
3956
4221
  };
3957
4222
 
3958
- /**
3959
- * Toggle dark mode on/off.
3960
- *
3961
- * Adds/removes the `bw-dark` class on `<html>` and injects dark mode CSS
3962
- * overrides. Pass `true`/`false` to force a mode, or omit to toggle.
3963
- *
3964
- * @param {boolean} [force] - Force dark (true) or light (false). Omit to toggle.
3965
- * @returns {boolean} Whether dark mode is now active
3966
- * @category CSS & Styling
3967
- * @see bw.setTheme
3968
- * @example
3969
- * bw.toggleDarkMode(); // toggle
3970
- * bw.toggleDarkMode(true); // force dark
3971
- * bw.toggleDarkMode(false); // force light
3972
- */
3973
- bw.toggleDarkMode = function(force) {
3974
- const isDark = force !== undefined ? force : !theme.darkMode;
3975
- theme.darkMode = isDark;
3976
-
3977
- if (bw._isBrowser) {
3978
- const root = document.documentElement;
3979
- if (isDark) {
3980
- root.classList.add('bw-dark');
3981
- // Generate palette-aware dark mode CSS, or fall back to default
3982
- var palette = bw._activePalette || derivePalette(DEFAULT_PALETTE_CONFIG);
3983
- var darkRules = generateDarkModeCSS(palette);
3984
- var darkCSS = bw.css(darkRules);
3985
-
3986
- // Remove existing dark styles to allow regeneration
3987
- var existing = document.getElementById('bw-dark-styles');
3988
- if (existing) existing.remove();
3989
-
3990
- var styleEl = document.createElement('style');
3991
- styleEl.id = 'bw-dark-styles';
3992
- styleEl.textContent = darkCSS;
3993
- document.head.appendChild(styleEl);
3994
- } else {
3995
- root.classList.remove('bw-dark');
3996
- // Remove dark mode styles when switching to light
3997
- var darkEl = document.getElementById('bw-dark-styles');
3998
- if (darkEl) darkEl.remove();
3999
- }
4000
- }
4001
-
4002
- return isDark;
4003
- };
4004
-
4005
4223
  /**
4006
4224
  * Generate a complete, scoped theme from seed colors.
4007
4225
  *
@@ -4024,13 +4242,19 @@
4024
4242
  * @param {string} [config.spacing='normal'] - 'compact' | 'normal' | 'spacious'
4025
4243
  * @param {string} [config.radius='md'] - 'none' | 'sm' | 'md' | 'lg' | 'pill'
4026
4244
  * @param {number} [config.fontSize=1.0] - Base font size scale factor
4245
+ * @param {string|number} [config.typeRatio='normal'] - 'tight' | 'normal' | 'relaxed' | 'dramatic' or a number
4246
+ * @param {string} [config.elevation='md'] - 'flat' | 'sm' | 'md' | 'lg'
4247
+ * @param {string} [config.motion='standard'] - 'reduced' | 'standard' | 'expressive'
4248
+ * @param {number} [config.harmonize=0.20] - 0-1, semantic color hue shift toward primary
4027
4249
  * @param {boolean} [config.inject=true] - Inject into DOM (browser only)
4028
- * @returns {Object} { css, palette, name }
4250
+ * @returns {Object} { css, palette, name, isLightPrimary, alternate: { css, palette } }
4029
4251
  * @category CSS & Styling
4252
+ * @see bw.applyTheme
4253
+ * @see bw.toggleTheme
4030
4254
  * @see bw.loadDefaultStyles
4031
4255
  * @example
4032
- * // Generate and inject an ocean theme
4033
- * bw.generateTheme('ocean', {
4256
+ * // Generate and inject an ocean theme (primary + alternate)
4257
+ * var theme = bw.generateTheme('ocean', {
4034
4258
  * primary: '#0077b6',
4035
4259
  * secondary: '#90e0ef',
4036
4260
  * tertiary: '#00b4d8'
@@ -4039,14 +4263,16 @@
4039
4263
  * // Apply to a container
4040
4264
  * document.getElementById('app').classList.add('ocean');
4041
4265
  *
4266
+ * // Toggle to alternate palette
4267
+ * bw.toggleTheme();
4268
+ *
4042
4269
  * // Generate CSS for static export (Node.js)
4043
4270
  * var result = bw.generateTheme('sunset', {
4044
4271
  * primary: '#e76f51',
4045
4272
  * secondary: '#264653',
4046
- * tertiary: '#e9c46a',
4047
4273
  * inject: false
4048
4274
  * });
4049
- * fs.writeFileSync('sunset.css', result.css);
4275
+ * fs.writeFileSync('sunset.css', result.css + result.alternate.css);
4050
4276
  */
4051
4277
  bw.generateTheme = function(name, config) {
4052
4278
  if (!config || !config.primary || !config.secondary) {
@@ -4057,29 +4283,37 @@
4057
4283
  var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config);
4058
4284
  if (!config.tertiary) fullConfig.tertiary = fullConfig.primary;
4059
4285
 
4060
- // Derive palette
4286
+ // Derive primary palette
4061
4287
  var palette = derivePalette(fullConfig);
4062
4288
 
4063
- // Store active palette for dark mode
4064
- bw._activePalette = palette;
4065
-
4066
4289
  // Resolve layout
4067
4290
  var layout = resolveLayout(fullConfig);
4068
4291
 
4069
- // Generate themed CSS rules
4292
+ // Generate primary themed CSS rules
4070
4293
  var themedRules = generateThemedCSS(name, palette, layout);
4071
-
4072
- // Add underscore aliases
4073
4294
  var aliasedRules = addUnderscoreAliases(themedRules);
4074
-
4075
- // Convert to CSS string
4076
4295
  var cssStr = bw.css(aliasedRules);
4077
4296
 
4078
- // Inject into DOM if requested and in browser
4297
+ // Derive alternate palette (luminance-inverted)
4298
+ var altConfig = deriveAlternateConfig(fullConfig);
4299
+ var altPalette = derivePalette(altConfig);
4300
+
4301
+ // Generate alternate CSS scoped under .bw-theme-alt
4302
+ var altRules = generateAlternateCSS(name, altPalette, layout);
4303
+ var aliasedAltRules = addUnderscoreAliases(altRules);
4304
+ var altCssStr = bw.css(aliasedAltRules);
4305
+
4306
+ // Determine if primary is light-flavored
4307
+ var lightPrimary = isLightPalette(fullConfig);
4308
+
4309
+ // Inject both CSS sets into DOM if requested
4079
4310
  var shouldInject = config.inject !== false;
4080
4311
  if (shouldInject && bw._isBrowser) {
4081
4312
  var styleId = name ? 'bw-theme-' + name : 'bw-theme-default';
4082
4313
  bw.injectCSS(cssStr, { id: styleId, append: false });
4314
+
4315
+ var altStyleId = name ? 'bw-theme-' + name + '-alt' : 'bw-theme-default-alt';
4316
+ bw.injectCSS(altCssStr, { id: altStyleId, append: false });
4083
4317
  }
4084
4318
 
4085
4319
  // Update bw.u color entries to reflect the palette
@@ -4090,7 +4324,72 @@
4090
4324
  bw.u.textWhite = { color: '#ffffff' };
4091
4325
  }
4092
4326
 
4093
- return { css: cssStr, palette: palette, name: name };
4327
+ // Store active theme state
4328
+ var result = {
4329
+ css: cssStr,
4330
+ palette: palette,
4331
+ name: name,
4332
+ isLightPrimary: lightPrimary,
4333
+ alternate: {
4334
+ css: altCssStr,
4335
+ palette: altPalette
4336
+ }
4337
+ };
4338
+ bw._activeTheme = result;
4339
+ bw._activeThemeMode = 'primary';
4340
+
4341
+ return result;
4342
+ };
4343
+
4344
+ /**
4345
+ * Apply a theme mode. Switches between primary and alternate palettes
4346
+ * by adding/removing the `bw-theme-alt` class on `<html>`.
4347
+ *
4348
+ * @param {string} mode - 'primary' | 'alternate' | 'light' | 'dark'
4349
+ * @returns {string} Active mode: 'primary' or 'alternate'
4350
+ * @category CSS & Styling
4351
+ * @see bw.generateTheme
4352
+ * @see bw.toggleTheme
4353
+ * @example
4354
+ * bw.applyTheme('alternate'); // switch to alternate palette
4355
+ * bw.applyTheme('dark'); // switch to whichever palette is darker
4356
+ * bw.applyTheme('primary'); // switch back to primary palette
4357
+ */
4358
+ bw.applyTheme = function(mode) {
4359
+ if (!bw._isBrowser) return mode || 'primary';
4360
+ var root = document.documentElement;
4361
+ var isLight = bw._activeTheme ? bw._activeTheme.isLightPrimary : true;
4362
+
4363
+ var wantAlt;
4364
+ if (mode === 'primary') wantAlt = false;
4365
+ else if (mode === 'alternate') wantAlt = true;
4366
+ else if (mode === 'light') wantAlt = !isLight;
4367
+ else if (mode === 'dark') wantAlt = isLight;
4368
+ else wantAlt = false;
4369
+
4370
+ if (wantAlt) {
4371
+ root.classList.add('bw-theme-alt');
4372
+ } else {
4373
+ root.classList.remove('bw-theme-alt');
4374
+ }
4375
+
4376
+ bw._activeThemeMode = wantAlt ? 'alternate' : 'primary';
4377
+ return bw._activeThemeMode;
4378
+ };
4379
+
4380
+ /**
4381
+ * Toggle between primary and alternate theme palettes.
4382
+ *
4383
+ * @returns {string} Active mode after toggle: 'primary' or 'alternate'
4384
+ * @category CSS & Styling
4385
+ * @see bw.applyTheme
4386
+ * @see bw.generateTheme
4387
+ * @example
4388
+ * bw.toggleTheme(); // flip between primary and alternate
4389
+ */
4390
+ bw.toggleTheme = function() {
4391
+ var current = bw._activeThemeMode || 'primary';
4392
+ return bw.applyTheme(current === 'primary' ? 'alternate' : 'primary');
4094
4393
  };
4095
4394
 
4096
4395
  // Expose color utility functions on bw namespace
@@ -4102,10 +4401,18 @@
4102
4401
  bw.textOnColor = textOnColor;
4103
4402
  bw.deriveShades = deriveShades;
4104
4403
  bw.derivePalette = derivePalette;
4404
+ bw.harmonize = harmonize;
4405
+ bw.deriveAlternateSeed = deriveAlternateSeed;
4406
+ bw.deriveAlternateConfig = deriveAlternateConfig;
4407
+ bw.isLightPalette = isLightPalette;
4105
4408
 
4106
4409
  // Expose layout and theme presets
4107
4410
  bw.SPACING_PRESETS = SPACING_PRESETS;
4108
4411
  bw.RADIUS_PRESETS = RADIUS_PRESETS;
4412
+ bw.TYPE_RATIO_PRESETS = TYPE_RATIO_PRESETS;
4413
+ bw.ELEVATION_PRESETS = ELEVATION_PRESETS;
4414
+ bw.MOTION_PRESETS = MOTION_PRESETS;
4415
+ bw.generateTypeScale = generateTypeScale;
4109
4416
  bw.DEFAULT_PALETTE_CONFIG = DEFAULT_PALETTE_CONFIG;
4110
4417
  bw.THEME_PRESETS = THEME_PRESETS;
4111
4418
 
@@ -5147,9 +5454,13 @@
5147
5454
  /**
5148
5455
  * Create a sortable TACO table from an array of row objects.
5149
5456
  *
5457
+ * Returns a bare `<table>` TACO — no wrapper, title, or responsive scroll.
5458
+ * Use this when you need full control over table placement, or when embedding
5459
+ * the table inside your own layout. For a ready-to-use table with title,
5460
+ * responsive wrapper, and defaults (striped + hover), use `bw.makeDataTable()`.
5461
+ *
5150
5462
  * Auto-detects columns from data keys if not specified. Supports click-to-sort
5151
- * headers with ascending/descending indicators. Returns a TACO object —
5152
- * render with `bw.DOM()` or `bw.html()`.
5463
+ * headers with ascending/descending indicators.
5153
5464
  *
5154
5465
  * @param {Object} config - Table configuration
5155
5466
  * @param {Array<Object>} config.data - Array of row objects to display
@@ -5449,10 +5760,12 @@
5449
5760
  };
5450
5761
 
5451
5762
  /**
5452
- * Create a responsive data table with title and optional wrapper
5763
+ * Create a ready-to-use data table with title and responsive wrapper.
5453
5764
  *
5454
- * Wraps bw.makeTable() output in a responsive container div.
5455
- * Adds an optional title heading above the table.
5765
+ * Convenience wrapper around `bw.makeTable()` that adds a title heading,
5766
+ * responsive horizontal scroll container, and defaults to striped + hover.
5767
+ * Use this for the common case; use `bw.makeTable()` when you need a bare
5768
+ * table element with no wrapper.
5456
5769
  *
5457
5770
  * @param {Object} config - Table configuration
5458
5771
  * @param {string} [config.title] - Table title heading