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,18 +1,18 @@
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
  /**
3
3
  * Auto-generated version file from package.json
4
4
  * DO NOT EDIT DIRECTLY - Use npm run generate-version
5
5
  */
6
6
 
7
7
  const VERSION_INFO = {
8
- version: '2.0.12',
8
+ version: '2.0.14',
9
9
  name: 'bitwrench',
10
10
  description: 'A library for javascript UI functions.',
11
11
  license: 'BSD-2-Clause',
12
12
  homepage: 'https://deftio.github.com/bitwrench/pages',
13
13
  repository: 'git+https://github.com/deftio/bitwrench.git',
14
14
  author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
15
- buildDate: '2026-03-07T22:31:35.755Z'
15
+ buildDate: '2026-03-08T08:04:06.572Z'
16
16
  };
17
17
 
18
18
  /**
@@ -270,6 +270,29 @@ function textOnColor(hex) {
270
270
  return relativeLuminance(hex) > 0.179 ? '#000' : '#fff';
271
271
  }
272
272
 
273
+ /**
274
+ * Shift a color's hue toward a target hue by a given amount.
275
+ * Uses shortest-arc interpolation on the hue wheel.
276
+ * @param {string} sourceHex - Color to shift
277
+ * @param {string} targetHex - Color whose hue to shift toward
278
+ * @param {number} [amount=0.20] - 0 = no shift, 1 = full shift to target hue
279
+ * @returns {string} Harmonized hex color
280
+ */
281
+ function harmonize(sourceHex, targetHex, amount) {
282
+ if (amount === undefined) amount = 0.20;
283
+ if (amount === 0) return sourceHex;
284
+ var srcHsl = hexToHsl(sourceHex);
285
+ var tgtHsl = hexToHsl(targetHex);
286
+
287
+ // Shortest-arc hue interpolation
288
+ var diff = tgtHsl[0] - srcHsl[0];
289
+ if (diff > 180) diff -= 360;
290
+ if (diff < -180) diff += 360;
291
+
292
+ var newHue = (srcHsl[0] + diff * amount + 360) % 360;
293
+ return hslToHex([newHue, srcHsl[1], srcHsl[2]]);
294
+ }
295
+
273
296
  /**
274
297
  * Derive a full shade palette for a single semantic color.
275
298
  * @param {string} hex - Base color hex
@@ -289,31 +312,128 @@ function deriveShades(hex) {
289
312
  };
290
313
  }
291
314
 
315
+ /**
316
+ * Derive the alternate (luminance-inverted) version of a single seed color.
317
+ * Preserves hue, mirrors lightness, adjusts saturation for readability.
318
+ * @param {string} hex - Seed hex color
319
+ * @returns {string} Alternate hex color
320
+ */
321
+ function deriveAlternateSeed(hex) {
322
+ var hsl = hexToHsl(hex);
323
+ var h = hsl[0], s = hsl[1], l = hsl[2];
324
+ var altL, altS;
325
+
326
+ if (l > 50) {
327
+ // Light color → make dark. Map 50-100 → 30-10 range
328
+ altL = clip(100 - l - 10, 8, 40);
329
+ // Reduce saturation slightly — vivid colors at low lightness look garish
330
+ altS = clip(s * 0.85, 0, 100);
331
+ } else {
332
+ // Dark color → make light. Map 0-50 → 65-92 range
333
+ altL = clip(100 - l + 10, 60, 92);
334
+ // Slightly increase saturation for light variant
335
+ altS = clip(s * 1.1, 0, 100);
336
+ }
337
+
338
+ return hslToHex([h, altS, altL]);
339
+ }
340
+
341
+ /**
342
+ * Determine whether a palette config is "light-flavored" based on
343
+ * the average luminance of its seed colors.
344
+ * @param {Object} config - Theme config with primary, secondary hex colors
345
+ * @returns {boolean} true if the seeds are predominantly light
346
+ */
347
+ function isLightPalette(config) {
348
+ var lum = relativeLuminance(config.primary);
349
+ if (config.secondary) lum = (lum + relativeLuminance(config.secondary)) / 2;
350
+ if (config.tertiary) lum = (lum * 2 + relativeLuminance(config.tertiary)) / 3;
351
+ return lum > 0.179;
352
+ }
353
+
354
+ /**
355
+ * Derive a complete alternate config from a primary theme config.
356
+ * Each seed color is luminance-inverted; semantic colors are adjusted for
357
+ * the new luminance context.
358
+ * @param {Object} config - Primary theme config
359
+ * @returns {Object} Alternate theme config (same shape, inverted lightness)
360
+ */
361
+ function deriveAlternateConfig(config) {
362
+ var alt = {};
363
+ // Invert the user's seed colors
364
+ alt.primary = deriveAlternateSeed(config.primary);
365
+ alt.secondary = deriveAlternateSeed(config.secondary);
366
+ alt.tertiary = config.tertiary ? deriveAlternateSeed(config.tertiary) : alt.primary;
367
+
368
+ // Derive alternate surface colors from primary hue
369
+ var priHsl = hexToHsl(config.primary);
370
+ var h = priHsl[0];
371
+ var isLight = isLightPalette(config);
372
+
373
+ if (isLight) {
374
+ // Primary is light → alternate needs dark surfaces
375
+ alt.light = hslToHex([h, Math.min(priHsl[1], 15), 15]);
376
+ alt.dark = hslToHex([h, 5, 88]);
377
+ } else {
378
+ // Primary is dark → alternate needs light surfaces
379
+ alt.light = hslToHex([h, Math.min(priHsl[1], 10), 96]);
380
+ alt.dark = hslToHex([h, 10, 18]);
381
+ }
382
+
383
+ // Semantic colors: harmonize toward primary, then invert for alternate
384
+ var amt = config.harmonize !== undefined ? config.harmonize : 0.20;
385
+ var semanticDefaults = {
386
+ success: '#198754', danger: '#dc3545',
387
+ warning: '#f0ad4e', info: '#17a2b8'
388
+ };
389
+ var semantics = ['success', 'danger', 'warning', 'info'];
390
+ for (var i = 0; i < semantics.length; i++) {
391
+ var key = semantics[i];
392
+ var seed = config[key] || semanticDefaults[key];
393
+ var harmonized = harmonize(seed, config.primary, amt);
394
+ alt[key] = deriveAlternateSeed(harmonized);
395
+ }
396
+
397
+ // Semantic colors are already harmonized+inverted — don't re-harmonize in derivePalette
398
+ alt.harmonize = 0;
399
+
400
+ return alt;
401
+ }
402
+
292
403
  /**
293
404
  * Derive complete palette from a theme config object.
405
+ * Semantic colors are harmonized toward the primary hue (configurable).
406
+ * Light/dark surface colors are tinted with the primary hue.
294
407
  * @param {Object} config - Theme config with primary, secondary, tertiary, etc.
295
- * @returns {Object} Full palette with shades for all 8 semantic colors + tertiary
408
+ * @param {number} [config.harmonize=0.20] - Hue shift amount for semantic colors (0-1)
409
+ * @returns {Object} Full palette with shades for all 9 semantic colors
296
410
  */
297
411
  function derivePalette(config) {
298
- var defaults = {
299
- success: '#198754',
300
- danger: '#dc3545',
301
- warning: '#ffc107',
302
- info: '#0dcaf0',
303
- light: '#f8f9fa',
304
- dark: '#212529'
305
- };
412
+ var amt = config.harmonize !== undefined ? config.harmonize : 0.20;
413
+ var pri = config.primary;
414
+ var priHsl = hexToHsl(pri);
415
+ var h = priHsl[0];
416
+
417
+ // Semantic defaults — harmonized toward primary hue
418
+ var successBase = harmonize(config.success || '#198754', pri, amt);
419
+ var dangerBase = harmonize(config.danger || '#dc3545', pri, amt);
420
+ var warningBase = harmonize(config.warning || '#f0ad4e', pri, amt);
421
+ var infoBase = harmonize(config.info || '#17a2b8', pri, amt);
422
+
423
+ // Light/dark: derive from primary hue with low saturation (if not user-supplied)
424
+ var lightBase = config.light || hslToHex([h, 8, 97]);
425
+ var darkBase = config.dark || hslToHex([h, 10, 13]);
306
426
 
307
427
  var palette = {
308
- primary: deriveShades(config.primary),
428
+ primary: deriveShades(config.primary),
309
429
  secondary: deriveShades(config.secondary),
310
- tertiary: deriveShades(config.tertiary),
311
- success: deriveShades(config.success || defaults.success),
312
- danger: deriveShades(config.danger || defaults.danger),
313
- warning: deriveShades(config.warning || defaults.warning),
314
- info: deriveShades(config.info || defaults.info),
315
- light: deriveShades(config.light || defaults.light),
316
- dark: deriveShades(config.dark || defaults.dark)
430
+ tertiary: deriveShades(config.tertiary),
431
+ success: deriveShades(successBase),
432
+ danger: deriveShades(dangerBase),
433
+ warning: deriveShades(warningBase),
434
+ info: deriveShades(infoBase),
435
+ light: deriveShades(lightBase),
436
+ dark: deriveShades(darkBase)
317
437
  };
318
438
 
319
439
  return palette;
@@ -362,6 +482,88 @@ var RADIUS_PRESETS = {
362
482
  pill: { btn: '50rem', card: '1rem', badge: '50rem', alert: '1rem', input: '50rem' }
363
483
  };
364
484
 
485
+ // ---- Typography scale presets ----
486
+
487
+ var TYPE_RATIO_PRESETS = {
488
+ tight: 1.125,
489
+ normal: 1.200,
490
+ relaxed: 1.250,
491
+ dramatic: 1.333
492
+ };
493
+
494
+ /**
495
+ * Generate a modular type scale from a base size and ratio.
496
+ * @param {number} base - Base font size in px (default 16)
497
+ * @param {number} ratio - Scale ratio (default 1.200)
498
+ * @returns {Object} { xs, sm, base, lg, xl, '2xl', '3xl', '4xl' } in px
499
+ */
500
+ function generateTypeScale(base, ratio) {
501
+ if (!base) base = 16;
502
+ if (!ratio) ratio = 1.200;
503
+ return {
504
+ xs: Math.round(base / (ratio * ratio)),
505
+ sm: Math.round(base / ratio),
506
+ base: base,
507
+ lg: Math.round(base * ratio),
508
+ xl: Math.round(base * ratio * ratio),
509
+ '2xl': Math.round(base * Math.pow(ratio, 3)),
510
+ '3xl': Math.round(base * Math.pow(ratio, 4)),
511
+ '4xl': Math.round(base * Math.pow(ratio, 5))
512
+ };
513
+ }
514
+
515
+ // ---- Elevation (shadow depth) presets ----
516
+
517
+ var ELEVATION_PRESETS = {
518
+ flat: {
519
+ sm: 'none',
520
+ md: 'none',
521
+ lg: 'none',
522
+ xl: 'none'
523
+ },
524
+ sm: {
525
+ sm: '0 1px 2px rgba(0,0,0,0.05)',
526
+ md: '0 1px 3px rgba(0,0,0,0.08)',
527
+ lg: '0 2px 6px rgba(0,0,0,0.10)',
528
+ xl: '0 4px 12px rgba(0,0,0,0.12)'
529
+ },
530
+ md: {
531
+ sm: '0 1px 3px rgba(0,0,0,0.08)',
532
+ md: '0 2px 6px rgba(0,0,0,0.12)',
533
+ lg: '0 4px 12px rgba(0,0,0,0.16)',
534
+ xl: '0 8px 24px rgba(0,0,0,0.20)'
535
+ },
536
+ lg: {
537
+ sm: '0 2px 4px rgba(0,0,0,0.10)',
538
+ md: '0 4px 12px rgba(0,0,0,0.16)',
539
+ lg: '0 8px 24px rgba(0,0,0,0.22)',
540
+ xl: '0 16px 48px rgba(0,0,0,0.28)'
541
+ }
542
+ };
543
+
544
+ // ---- Motion (transition) presets ----
545
+
546
+ var MOTION_PRESETS = {
547
+ reduced: {
548
+ fast: '0ms',
549
+ normal: '0ms',
550
+ slow: '0ms',
551
+ easing: 'linear'
552
+ },
553
+ standard: {
554
+ fast: '100ms',
555
+ normal: '200ms',
556
+ slow: '300ms',
557
+ easing: 'ease-out'
558
+ },
559
+ expressive: {
560
+ fast: '150ms',
561
+ normal: '300ms',
562
+ slow: '500ms',
563
+ easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)'
564
+ }
565
+ };
566
+
365
567
  /**
366
568
  * Default palette config — matches existing hardcoded colors
367
569
  */
@@ -371,8 +573,8 @@ var DEFAULT_PALETTE_CONFIG = {
371
573
  tertiary: '#006666',
372
574
  success: '#198754',
373
575
  danger: '#dc3545',
374
- warning: '#ffc107',
375
- info: '#0dcaf0',
576
+ warning: '#b38600',
577
+ info: '#0891b2',
376
578
  light: '#f8f9fa',
377
579
  dark: '#212529'
378
580
  };
@@ -397,18 +599,32 @@ var THEME_PRESETS = {
397
599
  };
398
600
 
399
601
  /**
400
- * Resolve layout config to spacing + radius objects
401
- * @param {Object} config - { spacing, radius, fontSize }
402
- * @returns {Object} { spacing, radius, fontSize }
602
+ * Resolve layout config to spacing, radius, typeScale, elevation, and motion objects.
603
+ * @param {Object} config - { spacing, radius, fontSize, typeRatio, elevation, motion }
604
+ * @returns {Object} { spacing, radius, fontSize, typeScale, elevation, motion }
403
605
  */
404
606
  function resolveLayout(config) {
405
607
  var sp = (config && config.spacing) || 'normal';
406
608
  var rd = (config && config.radius) || 'md';
407
609
  var fs = (config && config.fontSize) || 1.0;
610
+
611
+ // typeRatio: accept preset name or number
612
+ var tr = (config && config.typeRatio) || 'normal';
613
+ var ratioNum = typeof tr === 'string' ? (TYPE_RATIO_PRESETS[tr] || TYPE_RATIO_PRESETS.normal) : tr;
614
+
615
+ // elevation: accept preset name or object
616
+ var el = (config && config.elevation) || 'md';
617
+
618
+ // motion: accept preset name or object
619
+ var mo = (config && config.motion) || 'standard';
620
+
408
621
  return {
409
622
  spacing: typeof sp === 'string' ? (SPACING_PRESETS[sp] || SPACING_PRESETS.normal) : sp,
410
623
  radius: typeof rd === 'string' ? (RADIUS_PRESETS[rd] || RADIUS_PRESETS.md) : rd,
411
- fontSize: fs
624
+ fontSize: fs,
625
+ typeScale: generateTypeScale(16, ratioNum),
626
+ elevation: typeof el === 'string' ? (ELEVATION_PRESETS[el] || ELEVATION_PRESETS.md) : el,
627
+ motion: typeof mo === 'string' ? (MOTION_PRESETS[mo] || MOTION_PRESETS.standard) : mo
412
628
  };
413
629
  }
414
630
 
@@ -432,12 +648,13 @@ function scopeSelector(name, sel) {
432
648
  // Themed CSS generators
433
649
  // =========================================================================
434
650
 
435
- function generateTypographyThemed(scope, palette) {
651
+ function generateTypographyThemed(scope, palette, layout) {
652
+ var mot = layout.motion;
436
653
  var rules = {};
437
654
  rules[scopeSelector(scope, 'a')] = {
438
655
  'color': palette.primary.base,
439
656
  'text-decoration': 'none',
440
- 'transition': 'color 0.15s'
657
+ 'transition': 'color ' + mot.fast + ' ' + mot.easing
441
658
  };
442
659
  rules[scopeSelector(scope, 'a:hover')] = {
443
660
  'color': palette.primary.hover,
@@ -457,7 +674,8 @@ function generateButtons(scope, palette, layout) {
457
674
  'border-radius': rd.btn
458
675
  };
459
676
  rules[scopeSelector(scope, '.bw-btn:focus-visible')] = {
460
- 'outline': '0',
677
+ 'outline': '2px solid currentColor',
678
+ 'outline-offset': '2px',
461
679
  'box-shadow': '0 0 0 3px ' + palette.primary.focus
462
680
  };
463
681
 
@@ -547,14 +765,15 @@ function generateCards(scope, palette, layout) {
547
765
  var sp = layout.spacing;
548
766
  var rd = layout.radius;
549
767
 
768
+ var elev = layout.elevation;
550
769
  rules[scopeSelector(scope, '.bw-card')] = {
551
770
  'background-color': '#fff',
552
771
  'border': '1px solid ' + palette.light.border,
553
772
  'border-radius': rd.card,
554
- 'box-shadow': '0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04)'
773
+ 'box-shadow': elev.sm
555
774
  };
556
775
  rules[scopeSelector(scope, '.bw-card:hover')] = {
557
- 'box-shadow': '0 4px 12px rgba(0,0,0,.1), 0 2px 4px rgba(0,0,0,.06)'
776
+ 'box-shadow': elev.md
558
777
  };
559
778
  rules[scopeSelector(scope, '.bw-card-body')] = {
560
779
  'padding': sp.card
@@ -601,6 +820,8 @@ function generateForms(scope, palette, layout) {
601
820
  };
602
821
  rules[scopeSelector(scope, '.bw-form-control:focus')] = {
603
822
  'border-color': palette.primary.border,
823
+ 'outline': '2px solid ' + palette.primary.base,
824
+ 'outline-offset': '-1px',
604
825
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
605
826
  };
606
827
  rules[scopeSelector(scope, '.bw-form-control::placeholder')] = {
@@ -758,7 +979,8 @@ function generatePagination(scope, palette) {
758
979
  'border-color': palette.light.border
759
980
  };
760
981
  rules[scopeSelector(scope, '.bw-page-link:focus')] = {
761
- 'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
982
+ 'outline': '2px solid ' + palette.primary.base,
983
+ 'outline-offset': '-2px'
762
984
  };
763
985
  rules[scopeSelector(scope, '.bw-page-item.bw-active .bw-page-link')] = {
764
986
  'color': palette.primary.textOn,
@@ -917,12 +1139,12 @@ function generateCarouselThemed(scope, palette) {
917
1139
  return rules;
918
1140
  }
919
1141
 
920
- function generateModalThemed(scope, palette) {
1142
+ function generateModalThemed(scope, palette, layout) {
921
1143
  var rules = {};
922
1144
  rules[scopeSelector(scope, '.bw-modal-content')] = {
923
1145
  'background-color': '#fff',
924
1146
  'border-color': palette.light.border,
925
- 'box-shadow': '0 0.5rem 1rem rgba(0,0,0,0.15)'
1147
+ 'box-shadow': layout.elevation.lg
926
1148
  };
927
1149
  rules[scopeSelector(scope, '.bw-modal-header')] = {
928
1150
  'border-bottom-color': palette.light.border
@@ -936,12 +1158,12 @@ function generateModalThemed(scope, palette) {
936
1158
  return rules;
937
1159
  }
938
1160
 
939
- function generateToastThemed(scope, palette) {
1161
+ function generateToastThemed(scope, palette, layout) {
940
1162
  var rules = {};
941
1163
  rules[scopeSelector(scope, '.bw-toast')] = {
942
1164
  'background-color': '#fff',
943
1165
  'border-color': 'rgba(0,0,0,0.1)',
944
- 'box-shadow': '0 0.5rem 1rem rgba(0,0,0,0.15)'
1166
+ 'box-shadow': layout.elevation.lg
945
1167
  };
946
1168
  rules[scopeSelector(scope, '.bw-toast-header')] = {
947
1169
  'border-bottom-color': 'rgba(0,0,0,0.05)'
@@ -955,12 +1177,12 @@ function generateToastThemed(scope, palette) {
955
1177
  return rules;
956
1178
  }
957
1179
 
958
- function generateDropdownThemed(scope, palette) {
1180
+ function generateDropdownThemed(scope, palette, layout) {
959
1181
  var rules = {};
960
1182
  rules[scopeSelector(scope, '.bw-dropdown-menu')] = {
961
1183
  'background-color': '#fff',
962
1184
  'border-color': palette.light.border,
963
- 'box-shadow': '0 0.5rem 1rem rgba(0,0,0,0.15)'
1185
+ 'box-shadow': layout.elevation.md
964
1186
  };
965
1187
  rules[scopeSelector(scope, '.bw-dropdown-item')] = {
966
1188
  'color': palette.dark.base
@@ -1026,7 +1248,7 @@ function generateAvatarThemed(scope, palette) {
1026
1248
  function generateThemedCSS(scopeName, palette, layout) {
1027
1249
  return Object.assign({},
1028
1250
  generateResetThemed(scopeName, palette),
1029
- generateTypographyThemed(scopeName, palette),
1251
+ generateTypographyThemed(scopeName, palette, layout),
1030
1252
  generateButtons(scopeName, palette, layout),
1031
1253
  generateAlerts(scopeName, palette, layout),
1032
1254
  generateBadges(scopeName, palette),
@@ -1045,9 +1267,9 @@ function generateThemedCSS(scopeName, palette, layout) {
1045
1267
  generateSectionsThemed(scopeName, palette),
1046
1268
  generateAccordionThemed(scopeName, palette),
1047
1269
  generateCarouselThemed(scopeName, palette),
1048
- generateModalThemed(scopeName, palette),
1049
- generateToastThemed(scopeName, palette),
1050
- generateDropdownThemed(scopeName, palette),
1270
+ generateModalThemed(scopeName, palette, layout),
1271
+ generateToastThemed(scopeName, palette, layout),
1272
+ generateDropdownThemed(scopeName, palette, layout),
1051
1273
  generateSwitchThemed(scopeName, palette),
1052
1274
  generateSkeletonThemed(scopeName, palette),
1053
1275
  generateAvatarThemed(scopeName, palette),
@@ -1200,11 +1422,23 @@ const defaultStyles = {
1200
1422
  // =========================================================================
1201
1423
 
1202
1424
  /**
1203
- * Structural styles contain only layout, sizing, spacing, and behavior
1204
- * properties. No colors, backgrounds, shadows, or border-colors.
1205
- * These never change with themes.
1425
+ * Structural styles layout, sizing, spacing, positioning, and behavior.
1206
1426
  *
1207
- * @returns {Object} CSS rules object
1427
+ * POLICY: No colors, backgrounds, shadows, or border-colors in this function.
1428
+ * All cosmetic values belong in `defaultStyles.*` sections (unthemed defaults)
1429
+ * or in `generateThemedCSS()` (theme-driven colors).
1430
+ *
1431
+ * Exception: `.bw-progress-bar-striped` uses rgba(255,255,255,.15) for the
1432
+ * stripe pattern overlay. This is theme-neutral — a semi-transparent white
1433
+ * gradient that creates visible stripes on any background color.
1434
+ *
1435
+ * Architecture:
1436
+ * getStructuralStyles() → layout-only rules (never change with themes)
1437
+ * defaultStyles.* → cosmetic defaults (colors, shadows, borders)
1438
+ * generateThemedCSS() → palette-driven cosmetics from seed colors
1439
+ * generateAlternateCSS() → alternate palette (luminance-inverted)
1440
+ *
1441
+ * @returns {Object} CSS rules object (layout-only, theme-independent)
1208
1442
  */
1209
1443
  function getStructuralStyles() {
1210
1444
  var rules = {};
@@ -1255,12 +1489,12 @@ function getStructuralStyles() {
1255
1489
  'text-decoration': 'none', 'vertical-align': 'middle', 'cursor': 'pointer',
1256
1490
  'user-select': 'none', 'border': '1px solid transparent',
1257
1491
  'padding': '0.5rem 1.125rem', 'font-size': '0.875rem', 'font-family': 'inherit',
1258
- 'border-radius': '6px', 'transition': 'all 0.15s cubic-bezier(0.4, 0, 0.2, 1)',
1492
+ 'border-radius': '6px', 'transition': 'all 0.15s ease-out',
1259
1493
  'gap': '0.5rem'
1260
1494
  };
1261
1495
  rules['.bw-btn:hover'] = { 'text-decoration': 'none', 'transform': 'translateY(-1px)' };
1262
1496
  rules['.bw-btn:active'] = { 'transform': 'translateY(0)' };
1263
- rules['.bw-btn:focus-visible'] = { 'outline': '0' };
1497
+ rules['.bw-btn:focus-visible'] = { 'outline': '2px solid currentColor', 'outline-offset': '2px' };
1264
1498
  rules['.bw-btn:disabled'] = { 'opacity': '0.5', 'cursor': 'not-allowed', 'pointer-events': 'none' };
1265
1499
  rules['.bw-btn-lg'] = { 'padding': '0.625rem 1.5rem', 'font-size': '1rem', 'border-radius': '8px' };
1266
1500
  rules['.bw-btn-sm'] = { 'padding': '0.25rem 0.75rem', 'font-size': '0.8125rem', 'border-radius': '5px' };
@@ -1270,7 +1504,7 @@ function getStructuralStyles() {
1270
1504
  'position': 'relative', 'display': 'flex', 'flex-direction': 'column',
1271
1505
  'min-width': '0', 'height': '100%', 'word-wrap': 'break-word',
1272
1506
  'background-clip': 'border-box', 'border': '1px solid transparent',
1273
- '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)',
1507
+ 'border-radius': '8px', 'transition': 'box-shadow 0.2s ease-out, transform 0.2s ease-out',
1274
1508
  'margin-bottom': '1.5rem', 'overflow': 'hidden'
1275
1509
  };
1276
1510
  rules['.bw-card-body'] = { 'flex': '1 1 auto', 'padding': '1.25rem 1.5rem' };
@@ -1279,7 +1513,7 @@ function getStructuralStyles() {
1279
1513
  rules['.bw-card-text'] = { 'margin-bottom': '0', 'font-size': '0.9375rem', 'line-height': '1.6' };
1280
1514
  rules['.bw-card-header'] = { 'padding': '0.875rem 1.5rem', 'margin-bottom': '0', 'font-weight': '600', 'font-size': '0.875rem' };
1281
1515
  rules['.bw-card-footer'] = { 'padding': '0.75rem 1.5rem', 'font-size': '0.875rem' };
1282
- rules['.bw-card-hoverable'] = { 'transition': 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)' };
1516
+ rules['.bw-card-hoverable'] = { 'transition': 'all 0.3s ease-out' };
1283
1517
  rules['.bw-card-img-top'] = { 'width': '100%', 'border-top-left-radius': '7px', 'border-top-right-radius': '7px' };
1284
1518
  rules['.bw-card-img-bottom'] = { 'width': '100%', 'border-bottom-left-radius': '7px', 'border-bottom-right-radius': '7px' };
1285
1519
  rules['.bw-card-img-left'] = { 'width': '40%', 'object-fit': 'cover' };
@@ -1292,10 +1526,10 @@ function getStructuralStyles() {
1292
1526
  'font-size': '0.9375rem', 'font-weight': '400', 'line-height': '1.5',
1293
1527
  'background-clip': 'padding-box', 'appearance': 'none',
1294
1528
  'border': '1px solid transparent', 'border-radius': '6px',
1295
- 'transition': 'border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out',
1529
+ 'transition': 'border-color 0.15s ease-out, box-shadow 0.15s ease-out',
1296
1530
  'font-family': 'inherit'
1297
1531
  };
1298
- rules['.bw-form-control:focus'] = { 'outline': '0' };
1532
+ rules['.bw-form-control:focus'] = { 'outline': '2px solid currentColor', 'outline-offset': '-1px' };
1299
1533
  rules['.bw-form-control::placeholder'] = { 'opacity': '1' };
1300
1534
  rules['.bw-form-label'] = { 'display': 'block', 'margin-bottom': '0.375rem', 'font-size': '0.875rem', 'font-weight': '600' };
1301
1535
  rules['.bw-form-group'] = { 'margin-bottom': '1.25rem' };
@@ -1307,6 +1541,10 @@ function getStructuralStyles() {
1307
1541
  };
1308
1542
  rules['textarea.bw-form-control'] = { 'min-height': '5rem', 'resize': 'vertical' };
1309
1543
 
1544
+ // Form validation (structural)
1545
+ rules['.bw-valid-feedback'] = { 'display': 'block', 'font-size': '0.875rem', 'margin-top': '0.25rem' };
1546
+ rules['.bw-invalid-feedback'] = { 'display': 'block', 'font-size': '0.875rem', 'margin-top': '0.25rem' };
1547
+
1310
1548
  // Form checks (structural)
1311
1549
  Object.assign(rules, {
1312
1550
  '.bw-form-check': { 'display': 'flex', 'align-items': 'center', 'gap': '0.5rem', 'min-height': '1.5rem', 'margin-bottom': '0.25rem' },
@@ -1365,13 +1603,13 @@ function getStructuralStyles() {
1365
1603
 
1366
1604
  // Badges (structural)
1367
1605
  rules['.bw-badge'] = {
1368
- 'display': 'inline-block', 'padding': '.4em .75em', 'font-size': '.875em',
1606
+ 'display': 'inline-block', 'padding': '0.375rem 0.625rem', 'font-size': '0.875rem',
1369
1607
  'font-weight': '600', 'line-height': '1.3', 'text-align': 'center',
1370
1608
  'white-space': 'nowrap', 'vertical-align': 'baseline', 'border-radius': '.375rem'
1371
1609
  };
1372
1610
  rules['.bw-badge:empty'] = { 'display': 'none' };
1373
- rules['.bw-badge-sm'] = { 'font-size': '.75em', 'padding': '.25em .5em' };
1374
- rules['.bw-badge-lg'] = { 'font-size': '1em', 'padding': '.5em .9em' };
1611
+ rules['.bw-badge-sm'] = { 'font-size': '0.75rem', 'padding': '0.25rem 0.5rem' };
1612
+ rules['.bw-badge-lg'] = { 'font-size': '1rem', 'padding': '0.5rem 0.875rem' };
1375
1613
  rules['.bw-badge-pill'] = { 'border-radius': '50rem' };
1376
1614
 
1377
1615
  // Progress (structural)
@@ -1379,7 +1617,7 @@ function getStructuralStyles() {
1379
1617
  rules['.bw-progress-bar'] = {
1380
1618
  'display': 'flex', 'flex-direction': 'column', 'justify-content': 'center',
1381
1619
  'overflow': 'hidden', 'text-align': 'center', 'white-space': 'nowrap',
1382
- 'transition': 'width .6s ease', 'font-weight': '600'
1620
+ 'transition': 'width 0.3s ease-out', 'font-weight': '600'
1383
1621
  };
1384
1622
  rules['.bw-progress-bar-striped'] = {
1385
1623
  '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)',
@@ -1396,7 +1634,7 @@ function getStructuralStyles() {
1396
1634
  'display': 'block', 'padding': '0.625rem 1rem', 'font-size': '0.875rem',
1397
1635
  'font-weight': '500', 'text-decoration': 'none', 'cursor': 'pointer',
1398
1636
  'border': 'none', 'background': 'transparent',
1399
- 'transition': 'color 0.15s, border-color 0.15s', 'font-family': 'inherit'
1637
+ 'transition': 'color 0.15s ease-out, background-color 0.15s ease-out, border-color 0.15s ease-out', 'font-family': 'inherit'
1400
1638
  };
1401
1639
  rules['.bw-nav-tabs .bw-nav-link'] = { 'border': 'none', 'border-bottom': '2px solid transparent', 'border-radius': '0', 'background-color': 'transparent' };
1402
1640
  rules['.bw-nav-pills .bw-nav-link'] = { 'border-radius': '6px' };
@@ -1414,7 +1652,8 @@ function getStructuralStyles() {
1414
1652
  rules['.bw-list-group-item:last-child'] = { 'border-bottom-right-radius': 'inherit', 'border-bottom-left-radius': 'inherit' };
1415
1653
  rules['.bw-list-group-item + .bw-list-group-item'] = { 'border-top-width': '0' };
1416
1654
  rules['.bw-list-group-item.disabled'] = { 'pointer-events': 'none' };
1417
- rules['a.bw-list-group-item'] = { 'cursor': 'pointer' };
1655
+ rules['a.bw-list-group-item'] = { 'cursor': 'pointer', 'transition': 'background-color 0.15s ease-out, color 0.15s ease-out' };
1656
+ rules['a.bw-list-group-item:focus-visible, .bw-list-group-item:focus-visible'] = { 'z-index': '2', 'outline': '2px solid currentColor', 'outline-offset': '-2px' };
1418
1657
  rules['.bw-list-group-flush'] = { 'border-radius': '0' };
1419
1658
  rules['.bw-list-group-flush > .bw-list-group-item'] = { 'border-width': '0 0 1px', 'border-radius': '0' };
1420
1659
  rules['.bw-list-group-flush > .bw-list-group-item:last-child'] = { 'border-bottom-width': '0' };
@@ -1425,16 +1664,19 @@ function getStructuralStyles() {
1425
1664
  rules['.bw-page-link'] = {
1426
1665
  'position': 'relative', 'display': 'block', 'padding': '0.375rem 0.75rem',
1427
1666
  'margin-left': '-1px', 'line-height': '1.25', 'text-decoration': 'none',
1428
- 'transition': 'color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out'
1667
+ 'transition': 'color 0.15s ease-out, background-color 0.15s ease-out, border-color 0.15s ease-out'
1429
1668
  };
1430
1669
  rules['.bw-page-item:first-child .bw-page-link'] = { 'margin-left': '0', 'border-top-left-radius': '0.375rem', 'border-bottom-left-radius': '0.375rem' };
1431
1670
  rules['.bw-page-item:last-child .bw-page-link'] = { 'border-top-right-radius': '0.375rem', 'border-bottom-right-radius': '0.375rem' };
1671
+ rules['.bw-page-link:focus-visible'] = { 'z-index': '3', 'outline': '2px solid currentColor', 'outline-offset': '-2px' };
1432
1672
 
1433
1673
  // Breadcrumb (structural)
1434
1674
  rules['.bw-breadcrumb'] = { 'display': 'flex', 'flex-wrap': 'wrap', 'padding': '0 0', 'margin-bottom': '1rem', 'list-style': 'none' };
1435
1675
  rules['.bw-breadcrumb-item'] = { 'display': 'flex' };
1436
1676
  rules['.bw-breadcrumb-item + .bw-breadcrumb-item'] = { 'padding-left': '0.5rem' };
1437
1677
  rules['.bw-breadcrumb-item + .bw-breadcrumb-item::before'] = { 'float': 'left', 'padding-right': '0.5rem', 'content': '"/"' };
1678
+ rules['.bw-breadcrumb-item a'] = { 'text-decoration': 'none', 'transition': 'color 0.15s ease-out' };
1679
+ rules['.bw-breadcrumb-item.active'] = { 'font-weight': '500' };
1438
1680
 
1439
1681
  // Hero (structural)
1440
1682
  rules['.bw-hero'] = { 'position': 'relative', 'overflow': 'hidden' };
@@ -1537,12 +1779,12 @@ function getStructuralStyles() {
1537
1779
  'position': 'relative', 'display': 'flex', 'align-items': 'center', 'width': '100%',
1538
1780
  'padding': '1rem 1.25rem', 'font-size': '1rem', 'font-weight': '500', 'text-align': 'left',
1539
1781
  'background-color': 'transparent', 'border': '0', 'overflow-anchor': 'none', 'cursor': 'pointer',
1540
- 'font-family': 'inherit', 'transition': 'color 0.15s ease-in-out, background-color 0.15s ease-in-out'
1782
+ 'font-family': 'inherit', 'transition': 'color 0.15s ease-out, background-color 0.15s ease-out'
1541
1783
  };
1542
1784
  rules['.bw-accordion-button::after'] = {
1543
1785
  'flex-shrink': '0', 'width': '1.25rem', 'height': '1.25rem', 'margin-left': 'auto',
1544
1786
  'content': '""', 'background-repeat': 'no-repeat', 'background-size': '1.25rem',
1545
- 'transition': 'transform 0.2s ease-in-out'
1787
+ 'transition': 'transform 0.2s ease-out'
1546
1788
  };
1547
1789
  rules['.bw-accordion-button:not(.bw-collapsed)::after'] = { 'transform': 'rotate(-180deg)' };
1548
1790
  rules['.bw-accordion-collapse'] = { 'max-height': '0', 'overflow': 'hidden', 'transition': 'max-height 0.3s ease' };
@@ -1551,10 +1793,13 @@ function getStructuralStyles() {
1551
1793
 
1552
1794
  // Modal (structural)
1553
1795
  rules['.bw-modal'] = {
1554
- 'display': 'none', 'position': 'fixed', 'top': '0', 'left': '0', 'width': '100%', 'height': '100%',
1555
- 'z-index': '1050', 'overflow-x': 'hidden', 'overflow-y': 'auto', 'opacity': '0', 'transition': 'opacity 0.15s linear'
1796
+ 'display': 'flex', 'align-items': 'center', 'justify-content': 'center',
1797
+ 'position': 'fixed', 'top': '0', 'left': '0', 'width': '100%', 'height': '100%',
1798
+ 'z-index': '1050', 'overflow-x': 'hidden', 'overflow-y': 'auto',
1799
+ 'opacity': '0', 'visibility': 'hidden', 'pointer-events': 'none',
1800
+ 'transition': 'opacity 0.2s ease, visibility 0.2s ease'
1556
1801
  };
1557
- rules['.bw-modal.bw-modal-show'] = { 'display': 'flex', 'align-items': 'center', 'justify-content': 'center', 'opacity': '1' };
1802
+ rules['.bw-modal.bw-modal-show'] = { 'opacity': '1', 'visibility': 'visible', 'pointer-events': 'auto' };
1558
1803
  rules['.bw-modal-dialog'] = {
1559
1804
  'position': 'relative', 'width': '100%', 'max-width': '500px', 'margin': '1.75rem auto',
1560
1805
  'pointer-events': 'none', 'transform': 'translateY(-20px)', 'transition': 'transform 0.2s ease-out'
@@ -1574,7 +1819,7 @@ function getStructuralStyles() {
1574
1819
 
1575
1820
  // Carousel (structural)
1576
1821
  rules['.bw-carousel'] = { 'position': 'relative', 'overflow': 'hidden', 'border-radius': '8px' };
1577
- rules['.bw-carousel-track'] = { 'display': 'flex', 'transition': 'transform 0.4s ease', 'height': '100%' };
1822
+ rules['.bw-carousel-track'] = { 'display': 'flex', 'transition': 'transform 0.3s ease-out', 'height': '100%' };
1578
1823
  rules['.bw-carousel-slide'] = { 'min-width': '100%', 'flex-shrink': '0', 'overflow': 'hidden', 'position': 'relative', 'display': 'flex', 'align-items': 'center', 'justify-content': 'center' };
1579
1824
  rules['.bw-carousel-slide img'] = { 'width': '100%', 'height': '100%', 'object-fit': 'cover' };
1580
1825
  rules['.bw-carousel-caption'] = { 'position': 'absolute', 'bottom': '0', 'left': '0', 'right': '0', 'padding': '0.75rem 1rem' };
@@ -1618,11 +1863,14 @@ function getStructuralStyles() {
1618
1863
  'border-bottom': '0', 'border-left': '0.3em solid transparent'
1619
1864
  };
1620
1865
  rules['.bw-dropdown-menu'] = {
1621
- 'position': 'absolute', 'top': '100%', 'left': '0', 'z-index': '1000', 'display': 'none',
1866
+ 'position': 'absolute', 'top': '100%', 'left': '0', 'z-index': '1000', 'display': 'block',
1622
1867
  'min-width': '10rem', 'padding': '0.5rem 0', 'margin': '0.125rem 0 0',
1623
- 'background-clip': 'padding-box', 'border-radius': '6px'
1868
+ 'background-clip': 'padding-box', 'border-radius': '6px',
1869
+ 'opacity': '0', 'visibility': 'hidden', 'transform': 'translateY(-4px)',
1870
+ 'pointer-events': 'none',
1871
+ 'transition': 'opacity 0.15s ease, transform 0.15s ease, visibility 0.15s ease'
1624
1872
  };
1625
- rules['.bw-dropdown-menu.bw-dropdown-show'] = { 'display': 'block' };
1873
+ rules['.bw-dropdown-menu.bw-dropdown-show'] = { 'opacity': '1', 'visibility': 'visible', 'transform': 'translateY(0)', 'pointer-events': 'auto' };
1626
1874
  rules['.bw-dropdown-menu-end'] = { 'left': 'auto', 'right': '0' };
1627
1875
  rules['.bw-dropdown-item'] = {
1628
1876
  'display': 'block', 'width': '100%', 'padding': '0.375rem 1rem', 'clear': 'both',
@@ -1630,6 +1878,7 @@ function getStructuralStyles() {
1630
1878
  'background-color': 'transparent', 'border': '0', 'font-size': '0.9375rem',
1631
1879
  'transition': 'background-color 0.15s, color 0.15s'
1632
1880
  };
1881
+ rules['.bw-dropdown-item:focus-visible'] = { 'outline': '2px solid currentColor', 'outline-offset': '-2px' };
1633
1882
  rules['.bw-dropdown-divider'] = { 'height': '0', 'margin': '0.5rem 0', 'overflow': 'hidden', 'opacity': '1' };
1634
1883
 
1635
1884
  // Switch (structural)
@@ -1637,7 +1886,7 @@ function getStructuralStyles() {
1637
1886
  rules['.bw-form-switch .bw-switch-input'] = {
1638
1887
  'width': '2em', 'height': '1.125em', 'margin-left': '-2.5em', 'border-radius': '2em',
1639
1888
  'appearance': 'none', 'background-position': 'left center', 'background-repeat': 'no-repeat',
1640
- 'background-size': 'contain', 'transition': 'background-position 0.15s ease-in-out, background-color 0.15s ease-in-out',
1889
+ 'background-size': 'contain', 'transition': 'background-position 0.15s ease-out, background-color 0.15s ease-out',
1641
1890
  'cursor': 'pointer'
1642
1891
  };
1643
1892
  rules['.bw-form-switch .bw-switch-input:checked'] = { 'background-position': 'right center' };
@@ -1662,6 +1911,123 @@ function getStructuralStyles() {
1662
1911
  rules['.bw-avatar-lg'] = { 'width': '4rem', 'height': '4rem', 'font-size': '1.25rem' };
1663
1912
  rules['.bw-avatar-xl'] = { 'width': '5rem', 'height': '5rem', 'font-size': '1.5rem' };
1664
1913
 
1914
+ // Stat card (structural)
1915
+ rules['.bw-stat-card'] = {
1916
+ 'border-radius': '8px', 'padding': '1.25rem',
1917
+ 'border-left': '4px solid transparent',
1918
+ 'transition': 'box-shadow 0.15s ease-out, transform 0.15s ease-out'
1919
+ };
1920
+ rules['.bw-stat-card:hover'] = { 'transform': 'translateY(-1px)' };
1921
+ rules['.bw-stat-icon'] = { 'font-size': '1.5rem', 'margin-bottom': '0.5rem' };
1922
+ rules['.bw-stat-value'] = { 'font-size': '2rem', 'font-weight': '700', 'line-height': '1.2' };
1923
+ rules['.bw-stat-label'] = { 'font-size': '0.875rem', 'margin-top': '0.25rem' };
1924
+ rules['.bw-stat-change'] = { 'font-size': '0.875rem', 'font-weight': '500', 'margin-top': '0.5rem' };
1925
+
1926
+ // Tooltip (structural)
1927
+ rules['.bw-tooltip-wrapper'] = { 'position': 'relative', 'display': 'inline-block' };
1928
+ rules['.bw-tooltip'] = {
1929
+ 'position': 'absolute', 'z-index': '999',
1930
+ 'padding': '0.375rem 0.75rem', 'border-radius': '4px', 'font-size': '0.875rem',
1931
+ 'white-space': 'nowrap', 'pointer-events': 'none',
1932
+ 'opacity': '0', 'visibility': 'hidden',
1933
+ 'transition': 'opacity 0.15s ease, visibility 0.15s ease, transform 0.15s ease'
1934
+ };
1935
+ rules['.bw-tooltip.bw-tooltip-show'] = { 'opacity': '1', 'visibility': 'visible' };
1936
+ rules['.bw-tooltip-top'] = { 'bottom': '100%', 'left': '50%', 'transform': 'translateX(-50%) translateY(-4px)', 'margin-bottom': '4px' };
1937
+ rules['.bw-tooltip-top.bw-tooltip-show'] = { 'transform': 'translateX(-50%) translateY(0)' };
1938
+ rules['.bw-tooltip-bottom'] = { 'top': '100%', 'left': '50%', 'transform': 'translateX(-50%) translateY(4px)', 'margin-top': '4px' };
1939
+ rules['.bw-tooltip-bottom.bw-tooltip-show'] = { 'transform': 'translateX(-50%) translateY(0)' };
1940
+ rules['.bw-tooltip-left'] = { 'right': '100%', 'top': '50%', 'transform': 'translateY(-50%) translateX(-4px)', 'margin-right': '4px' };
1941
+ rules['.bw-tooltip-left.bw-tooltip-show'] = { 'transform': 'translateY(-50%) translateX(0)' };
1942
+ rules['.bw-tooltip-right'] = { 'left': '100%', 'top': '50%', 'transform': 'translateY(-50%) translateX(4px)', 'margin-left': '4px' };
1943
+ rules['.bw-tooltip-right.bw-tooltip-show'] = { 'transform': 'translateY(-50%) translateX(0)' };
1944
+
1945
+ // Search input (structural)
1946
+ rules['.bw-search-input'] = { 'position': 'relative', 'display': 'flex', 'align-items': 'center' };
1947
+ rules['.bw-search-input .bw-search-field'] = { 'padding-right': '2.5rem' };
1948
+ rules['.bw-search-clear'] = {
1949
+ 'position': 'absolute', 'right': '0.5rem',
1950
+ 'display': 'flex', 'align-items': 'center', 'justify-content': 'center',
1951
+ 'width': '1.5rem', 'height': '1.5rem',
1952
+ 'border': 'none', 'background': 'none',
1953
+ 'font-size': '1.25rem', 'cursor': 'pointer', 'padding': '0',
1954
+ 'border-radius': '50%', 'transition': 'color 0.15s ease-out'
1955
+ };
1956
+
1957
+ // Range slider (structural)
1958
+ rules['.bw-range-wrapper'] = { 'margin-bottom': '1rem' };
1959
+ rules['.bw-range-label'] = { 'display': 'flex', 'justify-content': 'space-between', 'align-items': 'center', 'margin-bottom': '0.5rem', 'font-size': '0.875rem', 'font-weight': '500' };
1960
+ rules['.bw-range-value'] = { 'font-weight': '600' };
1961
+ rules['.bw-range'] = { 'width': '100%', 'height': '0.5rem', 'padding': '0', 'appearance': 'none', 'border': 'none', 'border-radius': '0.25rem', 'cursor': 'pointer', 'outline': 'none' };
1962
+ rules['.bw-range:disabled'] = { 'opacity': '0.5', 'cursor': 'not-allowed' };
1963
+
1964
+ // Media object (structural)
1965
+ rules['.bw-media'] = { 'display': 'flex', 'align-items': 'flex-start', 'gap': '1rem' };
1966
+ rules['.bw-media-reverse'] = { 'flex-direction': 'row-reverse' };
1967
+ rules['.bw-media-img'] = { 'border-radius': '50%', 'object-fit': 'cover', 'flex-shrink': '0' };
1968
+ rules['.bw-media-body'] = { 'flex': '1', 'min-width': '0' };
1969
+ rules['.bw-media-title'] = { 'margin': '0 0 0.25rem 0', 'font-size': '1rem', 'font-weight': '600', 'line-height': '1.3' };
1970
+
1971
+ // File upload (structural)
1972
+ rules['.bw-file-upload'] = {
1973
+ 'display': 'flex', 'flex-direction': 'column', 'align-items': 'center', 'justify-content': 'center',
1974
+ 'padding': '2rem', 'border': '2px dashed transparent', 'border-radius': '8px',
1975
+ 'cursor': 'pointer', 'text-align': 'center', 'position': 'relative',
1976
+ 'transition': 'border-color 0.15s ease-out, background-color 0.15s ease-out'
1977
+ };
1978
+ rules['.bw-file-upload-icon'] = { 'font-size': '2rem', 'margin-bottom': '0.5rem' };
1979
+ rules['.bw-file-upload-text'] = { 'font-size': '0.875rem' };
1980
+ rules['.bw-file-upload-input'] = {
1981
+ 'position': 'absolute', 'width': '1px', 'height': '1px', 'padding': '0',
1982
+ 'margin': '-1px', 'overflow': 'hidden', 'clip': 'rect(0,0,0,0)', 'border': '0'
1983
+ };
1984
+
1985
+ // Timeline (structural)
1986
+ rules['.bw-timeline'] = { 'position': 'relative', 'padding-left': '2rem' };
1987
+ rules['.bw-timeline-item'] = { 'position': 'relative', 'padding-bottom': '1.5rem' };
1988
+ rules['.bw-timeline-item:last-child'] = { 'padding-bottom': '0' };
1989
+ rules['.bw-timeline-marker'] = { 'position': 'absolute', 'left': '-1.75rem', 'top': '0.25rem', 'width': '0.75rem', 'height': '0.75rem', 'border-radius': '50%' };
1990
+ rules['.bw-timeline-content'] = { 'padding-left': '0.5rem' };
1991
+ rules['.bw-timeline-date'] = { 'font-size': '0.75rem', 'margin-bottom': '0.25rem', 'font-weight': '500' };
1992
+ rules['.bw-timeline-title'] = { 'font-size': '1rem', 'font-weight': '600', 'margin': '0 0 0.25rem 0', 'line-height': '1.3' };
1993
+ rules['.bw-timeline-text'] = { 'font-size': '0.875rem', 'margin': '0', 'line-height': '1.5' };
1994
+
1995
+ // Stepper (structural)
1996
+ rules['.bw-stepper'] = { 'display': 'flex', 'gap': '0' };
1997
+ rules['.bw-step'] = { 'flex': '1', 'display': 'flex', 'flex-direction': 'column', 'align-items': 'center', 'text-align': 'center', 'position': 'relative' };
1998
+ 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' };
1999
+ rules['.bw-step-body'] = { 'margin-top': '0.5rem' };
2000
+ rules['.bw-step-label'] = { 'font-size': '0.875rem', 'font-weight': '500' };
2001
+ rules['.bw-step-description'] = { 'font-size': '0.75rem', 'margin-top': '0.125rem' };
2002
+
2003
+ // Chip input (structural)
2004
+ 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' };
2005
+ 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' };
2006
+ 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' };
2007
+ rules['.bw-chip-field'] = { 'flex': '1', 'min-width': '80px', 'border': 'none', 'outline': 'none', 'font-size': '0.875rem', 'padding': '0.125rem 0', 'background': 'transparent' };
2008
+
2009
+ // Popover (structural)
2010
+ rules['.bw-popover-wrapper'] = { 'position': 'relative', 'display': 'inline-block' };
2011
+ rules['.bw-popover-trigger'] = { 'cursor': 'pointer' };
2012
+ rules['.bw-popover'] = {
2013
+ 'position': 'absolute', 'z-index': '1000',
2014
+ 'min-width': '200px', 'max-width': '320px',
2015
+ 'border-radius': '8px',
2016
+ 'pointer-events': 'none', 'opacity': '0', 'visibility': 'hidden',
2017
+ 'transition': 'opacity 0.15s ease, visibility 0.15s ease, transform 0.15s ease'
2018
+ };
2019
+ rules['.bw-popover.bw-popover-show'] = { 'opacity': '1', 'visibility': 'visible', 'pointer-events': 'auto' };
2020
+ rules['.bw-popover-header'] = { 'padding': '0.625rem 0.875rem', 'font-weight': '600', 'font-size': '0.9375rem' };
2021
+ rules['.bw-popover-body'] = { 'padding': '0.75rem 0.875rem', 'font-size': '0.875rem', 'line-height': '1.5' };
2022
+ rules['.bw-popover-top'] = { 'bottom': '100%', 'left': '50%', 'transform': 'translateX(-50%) translateY(-8px)', 'margin-bottom': '8px' };
2023
+ rules['.bw-popover-top.bw-popover-show'] = { 'transform': 'translateX(-50%) translateY(0)' };
2024
+ rules['.bw-popover-bottom'] = { 'top': '100%', 'left': '50%', 'transform': 'translateX(-50%) translateY(8px)', 'margin-top': '8px' };
2025
+ rules['.bw-popover-bottom.bw-popover-show'] = { 'transform': 'translateX(-50%) translateY(0)' };
2026
+ rules['.bw-popover-left'] = { 'right': '100%', 'top': '50%', 'transform': 'translateY(-50%) translateX(-8px)', 'margin-right': '8px' };
2027
+ rules['.bw-popover-left.bw-popover-show'] = { 'transform': 'translateY(-50%) translateX(0)' };
2028
+ rules['.bw-popover-right'] = { 'left': '100%', 'top': '50%', 'transform': 'translateY(-50%) translateX(8px)', 'margin-left': '8px' };
2029
+ rules['.bw-popover-right.bw-popover-show'] = { 'transform': 'translateY(-50%) translateX(0)' };
2030
+
1665
2031
  // Bar chart (structural)
1666
2032
  rules['.bw-bar-chart-container'] = {
1667
2033
  'padding': '1rem', 'border': '1px solid transparent', 'border-radius': '8px'
@@ -1675,7 +2041,7 @@ function getStructuralStyles() {
1675
2041
  };
1676
2042
  rules['.bw-bar'] = {
1677
2043
  'width': '100%', 'border-radius': '3px 3px 0 0',
1678
- 'transition': 'height 0.5s ease', 'min-height': '4px'
2044
+ 'transition': 'height 0.3s ease-out', 'min-height': '4px'
1679
2045
  };
1680
2046
  rules['.bw-bar:hover'] = { 'opacity': '0.85' };
1681
2047
  rules['.bw-bar-value'] = {
@@ -1804,6 +2170,16 @@ function getStructuralStyles() {
1804
2170
  // Responsive grid
1805
2171
  Object.assign(rules, defaultStyles.responsive);
1806
2172
 
2173
+ // Accessibility: reduce motion for users who prefer it
2174
+ rules['@media (prefers-reduced-motion: reduce)'] = {
2175
+ '*, *::before, *::after': {
2176
+ 'animation-duration': '0.01ms !important',
2177
+ 'animation-iteration-count': '1 !important',
2178
+ 'transition-duration': '0.01ms !important',
2179
+ 'scroll-behavior': 'auto !important'
2180
+ }
2181
+ };
2182
+
1807
2183
  return addUnderscoreAliases(rules);
1808
2184
  }
1809
2185
 
@@ -1812,9 +2188,25 @@ function getStructuralStyles() {
1812
2188
  // =========================================================================
1813
2189
 
1814
2190
  /**
1815
- * Add underscore aliases for all bw- selectors
2191
+ * Add underscore aliases for all `.bw-` selectors.
2192
+ *
2193
+ * CSS CLASS NAMING CONVENTION:
2194
+ *
2195
+ * Canonical form: `.bw-btn`, `.bw-card`, `.bw-table-hover` (hyphens)
2196
+ * Underscore alias: `.bw_btn`, `.bw_card`, `.bw_table_hover` (underscores)
2197
+ *
2198
+ * Both forms are valid in HTML and produce identical results. The hyphen
2199
+ * form is canonical (used in docs, generated CSS, component output).
2200
+ * Underscore aliases exist because:
2201
+ * 1. TACO attribute keys use underscores (`bw_id`, `bw_meta`) — no
2202
+ * quoting needed in JS object literals
2203
+ * 2. Some users prefer underscores for consistency with JS identifiers
2204
+ *
2205
+ * Use `bw.normalizeClass()` to convert underscore classes to canonical
2206
+ * hyphen form at runtime if needed.
2207
+ *
1816
2208
  * @param {Object} rules - CSS rules object
1817
- * @returns {Object} - Rules with underscore aliases added
2209
+ * @returns {Object} Rules with underscore aliases added (both forms work)
1818
2210
  */
1819
2211
  function addUnderscoreAliases(rules) {
1820
2212
  const result = {};
@@ -1831,6 +2223,27 @@ function addUnderscoreAliases(rules) {
1831
2223
  // =========================================================================
1832
2224
  // Theme tokens (backwards compatible)
1833
2225
  // =========================================================================
2226
+ //
2227
+ // DESIGN NOTE — Why no CSS custom properties (CSS variables)?
2228
+ //
2229
+ // Bitwrench targets IE11 as Tier 1 (see dev/bw2x-compatibility.md).
2230
+ // CSS custom properties (var(--color-primary)) are not supported in IE11.
2231
+ //
2232
+ // Instead, bitwrench uses class-scoped CSS generation:
2233
+ // 1. `defaultStyles.*` provides hardcoded cosmetic defaults
2234
+ // 2. `generateTheme(name, config)` generates a complete set of
2235
+ // class-scoped CSS rules from 3 seed colors (primary, secondary,
2236
+ // tertiary) — all components are restyled with the new palette
2237
+ // 3. `generateAlternateCSS()` produces the alternate (dark/light)
2238
+ // variant scoped under `.bw-theme-alt`
2239
+ //
2240
+ // This achieves full theme customization without CSS variables:
2241
+ // bw.generateTheme('ocean', { primary: '#006666', secondary: '#cc6633' })
2242
+ // → generates .ocean .bw-btn-primary { background: #006666; } etc.
2243
+ //
2244
+ // When IE11 support is dropped, CSS custom properties can be added as
2245
+ // an optimization (one rule with var() instead of many scoped rules).
2246
+ // The generateTheme() API stays the same — only the output format changes.
1834
2247
 
1835
2248
  let theme = {
1836
2249
  colors: {
@@ -1838,8 +2251,8 @@ let theme = {
1838
2251
  secondary: '#6c757d',
1839
2252
  success: '#198754',
1840
2253
  danger: '#dc3545',
1841
- warning: '#ffc107',
1842
- info: '#0dcaf0',
2254
+ warning: '#b38600',
2255
+ info: '#0891b2',
1843
2256
  light: '#f8f9fa',
1844
2257
  dark: '#212529',
1845
2258
  white: '#fff',
@@ -1875,214 +2288,63 @@ let theme = {
1875
2288
  '5xl': '3rem'
1876
2289
  }
1877
2290
  },
1878
- darkMode: false
1879
2291
  };
1880
2292
 
1881
2293
  /**
1882
- * Generate theme-aware dark mode CSS from a palette.
1883
- * Derives dark variants from the palette colors instead of using hardcoded values.
2294
+ * Generate alternate-palette CSS scoped under `.bw-theme-alt`.
2295
+ * Uses the same `generateThemedCSS()` pipeline as the primary palette
2296
+ * both sides go through identical code paths.
1884
2297
  *
1885
- * @param {Object} palette - From derivePalette()
1886
- * @returns {Object} CSS rules object for dark mode
2298
+ * @param {string} name - Theme scope name (e.g. 'ocean'). '' for global.
2299
+ * @param {Object} altPalette - From derivePalette(deriveAlternateConfig(...))
2300
+ * @param {Object} layout - From resolveLayout()
2301
+ * @returns {Object} CSS rules object scoped under .bw-theme-alt (+ optional .name)
1887
2302
  */
1888
- function generateDarkModeCSS(palette) {
1889
- var darkBg = adjustLightness(palette.primary.base, -15);
1890
- var darkBgHsl = hexToHsl(darkBg);
1891
- // Make it very dark (lightness 8-12%)
1892
- var bodyBg = hslToHex([darkBgHsl[0], Math.min(darkBgHsl[1], 30), 10]);
1893
- var surfaceBg = hslToHex([darkBgHsl[0], Math.min(darkBgHsl[1], 25), 15]);
1894
- var textColor = adjustLightness(palette.light.base, 5);
1895
- var borderColor = hslToHex([darkBgHsl[0], Math.min(darkBgHsl[1], 15), 30]);
1896
-
1897
- return {
1898
- ':root.bw-dark': {
1899
- '--bw-body-color': textColor,
1900
- '--bw-body-bg': bodyBg
1901
- },
1902
- '.bw-dark body, :root.bw-dark body': {
1903
- 'color': textColor,
1904
- 'background-color': bodyBg
1905
- },
1906
- '.bw-dark .bw-card': {
1907
- 'background-color': surfaceBg,
1908
- 'border-color': borderColor,
1909
- 'color': textColor
1910
- },
1911
- '.bw-dark .bw-card-header': {
1912
- 'background-color': bodyBg,
1913
- 'border-bottom-color': borderColor,
1914
- 'color': textColor
1915
- },
1916
- '.bw-dark .bw-card-footer': {
1917
- 'background-color': bodyBg,
1918
- 'border-top-color': borderColor,
1919
- 'color': textColor
1920
- },
1921
- '.bw-dark .bw-card-title': {
1922
- 'color': textColor
1923
- },
1924
- '.bw-dark .bw-navbar': {
1925
- 'background-color': surfaceBg,
1926
- 'border-bottom-color': borderColor
1927
- },
1928
- '.bw-dark .bw-navbar-brand': {
1929
- 'color': textColor
1930
- },
1931
- '.bw-dark .bw-navbar-nav .bw-nav-link': {
1932
- 'color': adjustLightness(textColor, -15)
1933
- },
1934
- '.bw-dark .bw-navbar-nav .bw-nav-link:hover': {
1935
- 'color': textColor
1936
- },
1937
- '.bw-dark .bw-form-control': {
1938
- 'background-color': surfaceBg,
1939
- 'border-color': borderColor,
1940
- 'color': textColor
1941
- },
1942
- '.bw-dark .bw-form-label': {
1943
- 'color': textColor
1944
- },
1945
- '.bw-dark .bw-form-text': {
1946
- 'color': adjustLightness(textColor, -20)
1947
- },
1948
- '.bw-dark .bw-table': {
1949
- 'color': textColor
1950
- },
1951
- '.bw-dark .bw-table > :not(caption) > * > *': {
1952
- 'border-bottom-color': borderColor
1953
- },
1954
- '.bw-dark .bw-table > thead > tr > *': {
1955
- 'background-color': bodyBg,
1956
- 'color': adjustLightness(textColor, -10),
1957
- 'border-bottom-color': borderColor
1958
- },
1959
- '.bw-dark .bw-table-striped > tbody > tr:nth-of-type(odd) > *': {
1960
- 'background-color': 'rgba(255, 255, 255, 0.05)'
1961
- },
1962
- '.bw-dark .bw-alert': {
1963
- 'border-color': borderColor
1964
- },
1965
- '.bw-dark .bw-list-group-item': {
1966
- 'background-color': surfaceBg,
1967
- 'border-color': borderColor,
1968
- 'color': textColor
1969
- },
1970
- '.bw-dark .bw-badge': {
1971
- 'color': textColor
1972
- },
1973
- '.bw-dark .bw-nav-tabs': {
1974
- 'border-bottom-color': borderColor
1975
- },
1976
- '.bw-dark .bw-nav-link': {
1977
- 'color': adjustLightness(textColor, -15)
1978
- },
1979
- '.bw-dark .bw-nav-tabs .bw-nav-link:hover': {
1980
- 'color': textColor,
1981
- 'border-bottom-color': borderColor
1982
- },
1983
- '.bw-dark .bw-pagination .bw-page-link': {
1984
- 'background-color': surfaceBg,
1985
- 'border-color': borderColor,
1986
- 'color': textColor
1987
- },
1988
- '.bw-dark .bw-breadcrumb-item + .bw-breadcrumb-item::before': {
1989
- 'color': adjustLightness(textColor, -20)
1990
- },
1991
- '.bw-dark .bw-breadcrumb-item.active': {
1992
- 'color': adjustLightness(textColor, -10)
1993
- },
1994
- '.bw-dark .bw-hero-light': {
1995
- 'background': surfaceBg,
1996
- 'color': textColor
1997
- },
1998
- '.bw-dark .bw-progress': {
1999
- 'background-color': surfaceBg
2000
- },
2001
- '.bw-dark .bw-section-subtitle': {
2002
- 'color': adjustLightness(textColor, -15)
2003
- },
2004
- '.bw-dark .bw-close': {
2005
- 'color': textColor
2006
- },
2007
- '.bw-dark .bw-accordion-item': {
2008
- 'background-color': surfaceBg,
2009
- 'border-color': borderColor
2010
- },
2011
- '.bw-dark .bw-accordion-button': {
2012
- 'color': textColor
2013
- },
2014
- '.bw-dark .bw-accordion-button:not(.bw-collapsed)': {
2015
- 'color': '#7dd3e0',
2016
- 'background-color': 'rgba(125, 211, 224, 0.1)'
2017
- },
2018
- '.bw-dark .bw-accordion-button:hover': {
2019
- 'background-color': bodyBg
2020
- },
2021
- '.bw-dark .bw-accordion-button:not(.bw-collapsed):hover': {
2022
- 'background-color': 'rgba(125, 211, 224, 0.15)'
2023
- },
2024
- '.bw-dark .bw-accordion-button:focus-visible': {
2025
- 'box-shadow': '0 0 0 0.2rem rgba(125, 211, 224, 0.3)'
2026
- },
2027
- '.bw-dark .bw-accordion-body': {
2028
- 'border-top-color': borderColor
2029
- },
2030
- '.bw-dark .bw-carousel': {
2031
- 'background-color': bodyBg
2032
- },
2033
- '.bw-dark .bw-carousel-control': {
2034
- 'background-color': 'rgba(255,255,255,0.15)'
2035
- },
2036
- '.bw-dark .bw-carousel-control:hover': {
2037
- 'background-color': 'rgba(255,255,255,0.25)'
2038
- },
2039
- '.bw-dark .bw-modal-content': {
2040
- 'background-color': surfaceBg,
2041
- 'border-color': borderColor
2042
- },
2043
- '.bw-dark .bw-modal-header': {
2044
- 'border-bottom-color': borderColor
2045
- },
2046
- '.bw-dark .bw-modal-footer': {
2047
- 'border-top-color': borderColor
2048
- },
2049
- '.bw-dark .bw-modal-title': {
2050
- 'color': textColor
2051
- },
2052
- '.bw-dark .bw-toast': {
2053
- 'background-color': surfaceBg,
2054
- 'border-color': borderColor
2055
- },
2056
- '.bw-dark .bw-toast-header': {
2057
- 'border-bottom-color': borderColor,
2058
- 'color': textColor
2059
- },
2060
- '.bw-dark .bw-dropdown-menu': {
2061
- 'background-color': surfaceBg,
2062
- 'border-color': borderColor
2063
- },
2064
- '.bw-dark .bw-dropdown-item': {
2065
- 'color': textColor
2066
- },
2067
- '.bw-dark .bw-dropdown-item:hover': {
2068
- 'background-color': bodyBg
2069
- },
2070
- '.bw-dark .bw-dropdown-divider': {
2071
- 'border-top-color': borderColor
2072
- },
2073
- '.bw-dark .bw-skeleton': {
2074
- 'background': 'linear-gradient(90deg, ' + borderColor + ' 25%, ' + surfaceBg + ' 37%, ' + borderColor + ' 63%)'
2075
- },
2076
- '.bw-dark h1, .bw-dark h2, .bw-dark h3, .bw-dark h4, .bw-dark h5, .bw-dark h6': {
2077
- 'color': textColor
2078
- },
2079
- '@media (prefers-color-scheme: dark)': {
2080
- ':root.bw-auto-dark body': {
2081
- 'color': textColor,
2082
- 'background-color': bodyBg
2303
+ function generateAlternateCSS(name, altPalette, layout) {
2304
+ // Generate themed CSS using the same pipeline as primary
2305
+ var rawRules = generateThemedCSS('', altPalette, layout);
2306
+
2307
+ // Re-scope every selector under .bw-theme-alt (+ optional theme name)
2308
+ var altPrefix = name ? '.' + name + '.bw-theme-alt' : '.bw-theme-alt';
2309
+ var altRules = {};
2310
+
2311
+ for (var sel in rawRules) {
2312
+ if (!rawRules.hasOwnProperty(sel)) continue;
2313
+
2314
+ if (sel.charAt(0) === '@') {
2315
+ // @media / @keyframes — recurse into the block
2316
+ var innerBlock = rawRules[sel];
2317
+ var altInner = {};
2318
+ for (var innerSel in innerBlock) {
2319
+ if (!innerBlock.hasOwnProperty(innerSel)) continue;
2320
+ altInner[altPrefix + ' ' + innerSel] = innerBlock[innerSel];
2083
2321
  }
2322
+ altRules[sel] = altInner;
2323
+ } else {
2324
+ // Regular selector — prefix with alt scope
2325
+ // Handle comma-separated selectors
2326
+ var parts = sel.split(',');
2327
+ var scopedParts = [];
2328
+ for (var i = 0; i < parts.length; i++) {
2329
+ var s = parts[i].trim();
2330
+ // 'body' selector gets special treatment: .bw-theme-alt body
2331
+ if (s === 'body' || s.indexOf('body') === 0) {
2332
+ scopedParts.push(altPrefix + ' ' + s);
2333
+ } else {
2334
+ scopedParts.push(altPrefix + ' ' + s);
2335
+ }
2336
+ }
2337
+ altRules[scopedParts.join(', ')] = rawRules[sel];
2084
2338
  }
2339
+ }
2340
+
2341
+ // Add body-level overrides for the alternate surface
2342
+ altRules[altPrefix + ' body, :root' + altPrefix + ' body'] = {
2343
+ 'color': altPalette.dark.base,
2344
+ 'background-color': altPalette.light.base
2085
2345
  };
2346
+
2347
+ return altRules;
2086
2348
  }
2087
2349
 
2088
2350
  function deepMerge(target, source) {
@@ -3730,8 +3992,10 @@ bw.u = {
3730
3992
  /**
3731
3993
  * Generate responsive CSS with media query breakpoints.
3732
3994
  *
3733
- * Produces a CSS string with `@media` rules for sm (640px), md (768px),
3734
- * lg (1024px), and xl (1280px) breakpoints. Pass the result to `bw.injectCSS()`.
3995
+ * Produces a CSS string with `@media (min-width)` rules for standard
3996
+ * breakpoints. These match the grid system and theme.breakpoints:
3997
+ * sm: 576px, md: 768px, lg: 992px, xl: 1200px
3998
+ * Pass the result to `bw.injectCSS()`.
3735
3999
  *
3736
4000
  * @param {string} selector - CSS selector
3737
4001
  * @param {Object} breakpoints - Object with keys: base, sm, md, lg, xl
@@ -3748,7 +4012,7 @@ bw.u = {
3748
4012
  * bw.injectCSS(css);
3749
4013
  */
3750
4014
  bw.responsive = function(selector, breakpoints) {
3751
- var sizes = { sm: '640px', md: '768px', lg: '1024px', xl: '1280px' };
4015
+ var sizes = { sm: '576px', md: '768px', lg: '992px', xl: '1200px' };
3752
4016
  var parts = [];
3753
4017
  Object.keys(breakpoints).forEach(function(key) {
3754
4018
  var rules = {};
@@ -3882,7 +4146,8 @@ if (bw._isBrowser) {
3882
4146
  * @returns {Element|null} Style element if in browser, null in Node.js
3883
4147
  * @category CSS & Styling
3884
4148
  * @see bw.setTheme
3885
- * @see bw.toggleDarkMode
4149
+ * @see bw.applyTheme
4150
+ * @see bw.toggleTheme
3886
4151
  * @example
3887
4152
  * bw.loadDefaultStyles(); // inject all default CSS
3888
4153
  */
@@ -3949,53 +4214,6 @@ bw.setTheme = function(overrides, options = {}) {
3949
4214
  return bw.getTheme();
3950
4215
  };
3951
4216
 
3952
- /**
3953
- * Toggle dark mode on/off.
3954
- *
3955
- * Adds/removes the `bw-dark` class on `<html>` and injects dark mode CSS
3956
- * overrides. Pass `true`/`false` to force a mode, or omit to toggle.
3957
- *
3958
- * @param {boolean} [force] - Force dark (true) or light (false). Omit to toggle.
3959
- * @returns {boolean} Whether dark mode is now active
3960
- * @category CSS & Styling
3961
- * @see bw.setTheme
3962
- * @example
3963
- * bw.toggleDarkMode(); // toggle
3964
- * bw.toggleDarkMode(true); // force dark
3965
- * bw.toggleDarkMode(false); // force light
3966
- */
3967
- bw.toggleDarkMode = function(force) {
3968
- const isDark = force !== undefined ? force : !theme.darkMode;
3969
- theme.darkMode = isDark;
3970
-
3971
- if (bw._isBrowser) {
3972
- const root = document.documentElement;
3973
- if (isDark) {
3974
- root.classList.add('bw-dark');
3975
- // Generate palette-aware dark mode CSS, or fall back to default
3976
- var palette = bw._activePalette || derivePalette(DEFAULT_PALETTE_CONFIG);
3977
- var darkRules = generateDarkModeCSS(palette);
3978
- var darkCSS = bw.css(darkRules);
3979
-
3980
- // Remove existing dark styles to allow regeneration
3981
- var existing = document.getElementById('bw-dark-styles');
3982
- if (existing) existing.remove();
3983
-
3984
- var styleEl = document.createElement('style');
3985
- styleEl.id = 'bw-dark-styles';
3986
- styleEl.textContent = darkCSS;
3987
- document.head.appendChild(styleEl);
3988
- } else {
3989
- root.classList.remove('bw-dark');
3990
- // Remove dark mode styles when switching to light
3991
- var darkEl = document.getElementById('bw-dark-styles');
3992
- if (darkEl) darkEl.remove();
3993
- }
3994
- }
3995
-
3996
- return isDark;
3997
- };
3998
-
3999
4217
  /**
4000
4218
  * Generate a complete, scoped theme from seed colors.
4001
4219
  *
@@ -4018,13 +4236,19 @@ bw.toggleDarkMode = function(force) {
4018
4236
  * @param {string} [config.spacing='normal'] - 'compact' | 'normal' | 'spacious'
4019
4237
  * @param {string} [config.radius='md'] - 'none' | 'sm' | 'md' | 'lg' | 'pill'
4020
4238
  * @param {number} [config.fontSize=1.0] - Base font size scale factor
4239
+ * @param {string|number} [config.typeRatio='normal'] - 'tight' | 'normal' | 'relaxed' | 'dramatic' or a number
4240
+ * @param {string} [config.elevation='md'] - 'flat' | 'sm' | 'md' | 'lg'
4241
+ * @param {string} [config.motion='standard'] - 'reduced' | 'standard' | 'expressive'
4242
+ * @param {number} [config.harmonize=0.20] - 0-1, semantic color hue shift toward primary
4021
4243
  * @param {boolean} [config.inject=true] - Inject into DOM (browser only)
4022
- * @returns {Object} { css, palette, name }
4244
+ * @returns {Object} { css, palette, name, isLightPrimary, alternate: { css, palette } }
4023
4245
  * @category CSS & Styling
4246
+ * @see bw.applyTheme
4247
+ * @see bw.toggleTheme
4024
4248
  * @see bw.loadDefaultStyles
4025
4249
  * @example
4026
- * // Generate and inject an ocean theme
4027
- * bw.generateTheme('ocean', {
4250
+ * // Generate and inject an ocean theme (primary + alternate)
4251
+ * var theme = bw.generateTheme('ocean', {
4028
4252
  * primary: '#0077b6',
4029
4253
  * secondary: '#90e0ef',
4030
4254
  * tertiary: '#00b4d8'
@@ -4033,14 +4257,16 @@ bw.toggleDarkMode = function(force) {
4033
4257
  * // Apply to a container
4034
4258
  * document.getElementById('app').classList.add('ocean');
4035
4259
  *
4260
+ * // Toggle to alternate palette
4261
+ * bw.toggleTheme();
4262
+ *
4036
4263
  * // Generate CSS for static export (Node.js)
4037
4264
  * var result = bw.generateTheme('sunset', {
4038
4265
  * primary: '#e76f51',
4039
4266
  * secondary: '#264653',
4040
- * tertiary: '#e9c46a',
4041
4267
  * inject: false
4042
4268
  * });
4043
- * fs.writeFileSync('sunset.css', result.css);
4269
+ * fs.writeFileSync('sunset.css', result.css + result.alternate.css);
4044
4270
  */
4045
4271
  bw.generateTheme = function(name, config) {
4046
4272
  if (!config || !config.primary || !config.secondary) {
@@ -4051,29 +4277,37 @@ bw.generateTheme = function(name, config) {
4051
4277
  var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config);
4052
4278
  if (!config.tertiary) fullConfig.tertiary = fullConfig.primary;
4053
4279
 
4054
- // Derive palette
4280
+ // Derive primary palette
4055
4281
  var palette = derivePalette(fullConfig);
4056
4282
 
4057
- // Store active palette for dark mode
4058
- bw._activePalette = palette;
4059
-
4060
4283
  // Resolve layout
4061
4284
  var layout = resolveLayout(fullConfig);
4062
4285
 
4063
- // Generate themed CSS rules
4286
+ // Generate primary themed CSS rules
4064
4287
  var themedRules = generateThemedCSS(name, palette, layout);
4065
-
4066
- // Add underscore aliases
4067
4288
  var aliasedRules = addUnderscoreAliases(themedRules);
4068
-
4069
- // Convert to CSS string
4070
4289
  var cssStr = bw.css(aliasedRules);
4071
4290
 
4072
- // Inject into DOM if requested and in browser
4291
+ // Derive alternate palette (luminance-inverted)
4292
+ var altConfig = deriveAlternateConfig(fullConfig);
4293
+ var altPalette = derivePalette(altConfig);
4294
+
4295
+ // Generate alternate CSS scoped under .bw-theme-alt
4296
+ var altRules = generateAlternateCSS(name, altPalette, layout);
4297
+ var aliasedAltRules = addUnderscoreAliases(altRules);
4298
+ var altCssStr = bw.css(aliasedAltRules);
4299
+
4300
+ // Determine if primary is light-flavored
4301
+ var lightPrimary = isLightPalette(fullConfig);
4302
+
4303
+ // Inject both CSS sets into DOM if requested
4073
4304
  var shouldInject = config.inject !== false;
4074
4305
  if (shouldInject && bw._isBrowser) {
4075
4306
  var styleId = name ? 'bw-theme-' + name : 'bw-theme-default';
4076
4307
  bw.injectCSS(cssStr, { id: styleId, append: false });
4308
+
4309
+ var altStyleId = name ? 'bw-theme-' + name + '-alt' : 'bw-theme-default-alt';
4310
+ bw.injectCSS(altCssStr, { id: altStyleId, append: false });
4077
4311
  }
4078
4312
 
4079
4313
  // Update bw.u color entries to reflect the palette
@@ -4084,7 +4318,72 @@ bw.generateTheme = function(name, config) {
4084
4318
  bw.u.textWhite = { color: '#ffffff' };
4085
4319
  }
4086
4320
 
4087
- return { css: cssStr, palette: palette, name: name };
4321
+ // Store active theme state
4322
+ var result = {
4323
+ css: cssStr,
4324
+ palette: palette,
4325
+ name: name,
4326
+ isLightPrimary: lightPrimary,
4327
+ alternate: {
4328
+ css: altCssStr,
4329
+ palette: altPalette
4330
+ }
4331
+ };
4332
+ bw._activeTheme = result;
4333
+ bw._activeThemeMode = 'primary';
4334
+
4335
+ return result;
4336
+ };
4337
+
4338
+ /**
4339
+ * Apply a theme mode. Switches between primary and alternate palettes
4340
+ * by adding/removing the `bw-theme-alt` class on `<html>`.
4341
+ *
4342
+ * @param {string} mode - 'primary' | 'alternate' | 'light' | 'dark'
4343
+ * @returns {string} Active mode: 'primary' or 'alternate'
4344
+ * @category CSS & Styling
4345
+ * @see bw.generateTheme
4346
+ * @see bw.toggleTheme
4347
+ * @example
4348
+ * bw.applyTheme('alternate'); // switch to alternate palette
4349
+ * bw.applyTheme('dark'); // switch to whichever palette is darker
4350
+ * bw.applyTheme('primary'); // switch back to primary palette
4351
+ */
4352
+ bw.applyTheme = function(mode) {
4353
+ if (!bw._isBrowser) return mode || 'primary';
4354
+ var root = document.documentElement;
4355
+ var isLight = bw._activeTheme ? bw._activeTheme.isLightPrimary : true;
4356
+
4357
+ var wantAlt;
4358
+ if (mode === 'primary') wantAlt = false;
4359
+ else if (mode === 'alternate') wantAlt = true;
4360
+ else if (mode === 'light') wantAlt = !isLight;
4361
+ else if (mode === 'dark') wantAlt = isLight;
4362
+ else wantAlt = false;
4363
+
4364
+ if (wantAlt) {
4365
+ root.classList.add('bw-theme-alt');
4366
+ } else {
4367
+ root.classList.remove('bw-theme-alt');
4368
+ }
4369
+
4370
+ bw._activeThemeMode = wantAlt ? 'alternate' : 'primary';
4371
+ return bw._activeThemeMode;
4372
+ };
4373
+
4374
+ /**
4375
+ * Toggle between primary and alternate theme palettes.
4376
+ *
4377
+ * @returns {string} Active mode after toggle: 'primary' or 'alternate'
4378
+ * @category CSS & Styling
4379
+ * @see bw.applyTheme
4380
+ * @see bw.generateTheme
4381
+ * @example
4382
+ * bw.toggleTheme(); // flip between primary and alternate
4383
+ */
4384
+ bw.toggleTheme = function() {
4385
+ var current = bw._activeThemeMode || 'primary';
4386
+ return bw.applyTheme(current === 'primary' ? 'alternate' : 'primary');
4088
4387
  };
4089
4388
 
4090
4389
  // Expose color utility functions on bw namespace
@@ -4096,10 +4395,18 @@ bw.relativeLuminance = relativeLuminance;
4096
4395
  bw.textOnColor = textOnColor;
4097
4396
  bw.deriveShades = deriveShades;
4098
4397
  bw.derivePalette = derivePalette;
4398
+ bw.harmonize = harmonize;
4399
+ bw.deriveAlternateSeed = deriveAlternateSeed;
4400
+ bw.deriveAlternateConfig = deriveAlternateConfig;
4401
+ bw.isLightPalette = isLightPalette;
4099
4402
 
4100
4403
  // Expose layout and theme presets
4101
4404
  bw.SPACING_PRESETS = SPACING_PRESETS;
4102
4405
  bw.RADIUS_PRESETS = RADIUS_PRESETS;
4406
+ bw.TYPE_RATIO_PRESETS = TYPE_RATIO_PRESETS;
4407
+ bw.ELEVATION_PRESETS = ELEVATION_PRESETS;
4408
+ bw.MOTION_PRESETS = MOTION_PRESETS;
4409
+ bw.generateTypeScale = generateTypeScale;
4103
4410
  bw.DEFAULT_PALETTE_CONFIG = DEFAULT_PALETTE_CONFIG;
4104
4411
  bw.THEME_PRESETS = THEME_PRESETS;
4105
4412
 
@@ -5141,9 +5448,13 @@ bw.copyToClipboard = function(text) {
5141
5448
  /**
5142
5449
  * Create a sortable TACO table from an array of row objects.
5143
5450
  *
5451
+ * Returns a bare `<table>` TACO — no wrapper, title, or responsive scroll.
5452
+ * Use this when you need full control over table placement, or when embedding
5453
+ * the table inside your own layout. For a ready-to-use table with title,
5454
+ * responsive wrapper, and defaults (striped + hover), use `bw.makeDataTable()`.
5455
+ *
5144
5456
  * Auto-detects columns from data keys if not specified. Supports click-to-sort
5145
- * headers with ascending/descending indicators. Returns a TACO object —
5146
- * render with `bw.DOM()` or `bw.html()`.
5457
+ * headers with ascending/descending indicators.
5147
5458
  *
5148
5459
  * @param {Object} config - Table configuration
5149
5460
  * @param {Array<Object>} config.data - Array of row objects to display
@@ -5443,10 +5754,12 @@ bw.makeBarChart = function(config) {
5443
5754
  };
5444
5755
 
5445
5756
  /**
5446
- * Create a responsive data table with title and optional wrapper
5757
+ * Create a ready-to-use data table with title and responsive wrapper.
5447
5758
  *
5448
- * Wraps bw.makeTable() output in a responsive container div.
5449
- * Adds an optional title heading above the table.
5759
+ * Convenience wrapper around `bw.makeTable()` that adds a title heading,
5760
+ * responsive horizontal scroll container, and defaults to striped + hover.
5761
+ * Use this for the common case; use `bw.makeTable()` when you need a bare
5762
+ * table element with no wrapper.
5450
5763
  *
5451
5764
  * @param {Object} config - Table configuration
5452
5765
  * @param {string} [config.title] - Table title heading