bitwrench 2.0.13 → 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 v2.0.13 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
1
+ /*! bitwrench 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.13',
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:35:06.056Z'
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
1887
- */
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
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)
2302
+ */
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];
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
+ }
2083
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) {
@@ -2279,8 +2541,11 @@ function makeCard(props = {}) {
2279
2541
  * variant: "success",
2280
2542
  * onclick: () => console.log("saved")
2281
2543
  * });
2544
+ * // String shorthand:
2545
+ * const ok = makeButton("OK");
2282
2546
  */
2283
2547
  function makeButton(props = {}) {
2548
+ if (typeof props === 'string') props = { text: props };
2284
2549
  const {
2285
2550
  text,
2286
2551
  variant = 'primary',
@@ -2585,6 +2850,7 @@ function makeTabs(props = {}) {
2585
2850
  class: `bw-nav-link ${index === actualActiveIndex ? 'active' : ''}`,
2586
2851
  type: 'button',
2587
2852
  role: 'tab',
2853
+ tabindex: index === actualActiveIndex ? '0' : '-1',
2588
2854
  'aria-selected': index === actualActiveIndex ? 'true' : 'false',
2589
2855
  'data-tab-index': index,
2590
2856
  onclick: (e) => {
@@ -2595,11 +2861,13 @@ function makeTabs(props = {}) {
2595
2861
  allTabs.forEach(t => {
2596
2862
  t.classList.remove('active');
2597
2863
  t.setAttribute('aria-selected', 'false');
2864
+ t.setAttribute('tabindex', '-1');
2598
2865
  });
2599
2866
  allPanes.forEach(p => p.classList.remove('active'));
2600
2867
 
2601
2868
  e.target.classList.add('active');
2602
2869
  e.target.setAttribute('aria-selected', 'true');
2870
+ e.target.setAttribute('tabindex', '0');
2603
2871
  const targetIndex = parseInt(e.target.getAttribute('data-tab-index'));
2604
2872
  allPanes[targetIndex].classList.add('active');
2605
2873
  }
@@ -2623,7 +2891,39 @@ function makeTabs(props = {}) {
2623
2891
  ],
2624
2892
  o: {
2625
2893
  type: 'tabs',
2626
- state: { activeIndex: actualActiveIndex }
2894
+ state: { activeIndex: actualActiveIndex },
2895
+ mounted: function(el) {
2896
+ var tablist = el.querySelector('[role="tablist"]');
2897
+ if (!tablist) return;
2898
+ tablist.addEventListener('keydown', function(e) {
2899
+ var tabButtons = tablist.querySelectorAll('[role="tab"]');
2900
+ var currentIndex = -1;
2901
+ for (var i = 0; i < tabButtons.length; i++) {
2902
+ if (tabButtons[i] === e.target) { currentIndex = i; break; }
2903
+ }
2904
+ if (currentIndex === -1) return;
2905
+
2906
+ var newIndex = -1;
2907
+ if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
2908
+ e.preventDefault();
2909
+ newIndex = currentIndex > 0 ? currentIndex - 1 : tabButtons.length - 1;
2910
+ } else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
2911
+ e.preventDefault();
2912
+ newIndex = currentIndex < tabButtons.length - 1 ? currentIndex + 1 : 0;
2913
+ } else if (e.key === 'Home') {
2914
+ e.preventDefault();
2915
+ newIndex = 0;
2916
+ } else if (e.key === 'End') {
2917
+ e.preventDefault();
2918
+ newIndex = tabButtons.length - 1;
2919
+ }
2920
+
2921
+ if (newIndex >= 0) {
2922
+ tabButtons[newIndex].focus();
2923
+ tabButtons[newIndex].click();
2924
+ }
2925
+ });
2926
+ }
2627
2927
  }
2628
2928
  };
2629
2929
  }
@@ -2644,8 +2944,11 @@ function makeTabs(props = {}) {
2644
2944
  * variant: "success",
2645
2945
  * dismissible: true
2646
2946
  * });
2947
+ * // String shorthand:
2948
+ * const msg = makeAlert("Something happened");
2647
2949
  */
2648
2950
  function makeAlert(props = {}) {
2951
+ if (typeof props === 'string') props = { content: props };
2649
2952
  const {
2650
2953
  content,
2651
2954
  variant = 'info',
@@ -2692,8 +2995,11 @@ function makeAlert(props = {}) {
2692
2995
  * @example
2693
2996
  * const badge = makeBadge({ text: "New", variant: "danger", pill: true });
2694
2997
  * const small = makeBadge({ text: "3", variant: "info", size: "sm" });
2998
+ * // String shorthand:
2999
+ * const tag = makeBadge("New");
2695
3000
  */
2696
3001
  function makeBadge(props = {}) {
3002
+ if (typeof props === 'string') props = { text: props };
2697
3003
  const {
2698
3004
  text,
2699
3005
  variant = 'primary',
@@ -2930,13 +3236,16 @@ function makeForm(props = {}) {
2930
3236
  }
2931
3237
 
2932
3238
  /**
2933
- * Create a form group with label, input, and optional help text
3239
+ * Create a form group with label, input, optional help text and validation feedback
2934
3240
  *
2935
3241
  * @param {Object} [props] - Form group configuration
2936
3242
  * @param {string} [props.label] - Label text
2937
3243
  * @param {Object} [props.input] - Input TACO object (from makeInput, makeSelect, etc.)
2938
3244
  * @param {string} [props.help] - Help text displayed below the input
2939
3245
  * @param {string} [props.id] - Input ID (links label to input via for/id)
3246
+ * @param {string} [props.validation] - Validation state ("valid" or "invalid")
3247
+ * @param {string} [props.feedback] - Validation feedback text shown below input
3248
+ * @param {boolean} [props.required=false] - Show required indicator (*) on label
2940
3249
  * @returns {Object} TACO object representing a form group
2941
3250
  * @category Component Builders
2942
3251
  * @example
@@ -2944,11 +3253,22 @@ function makeForm(props = {}) {
2944
3253
  * label: "Email",
2945
3254
  * id: "email",
2946
3255
  * input: makeInput({ type: "email", id: "email", placeholder: "you@example.com" }),
2947
- * help: "We'll never share your email."
3256
+ * validation: "invalid",
3257
+ * feedback: "Please enter a valid email address."
2948
3258
  * });
2949
3259
  */
2950
3260
  function makeFormGroup(props = {}) {
2951
- const { label, input, help, id } = props;
3261
+ var { label, input, help, id, validation, feedback, required } = props;
3262
+
3263
+ // Shallow-clone input TACO to add validation class without mutating original
3264
+ var styledInput = input;
3265
+ if (validation && input && input.a) {
3266
+ styledInput = { t: input.t, a: Object.assign({}, input.a), c: input.c, o: input.o };
3267
+ var validClass = validation === 'valid' ? 'bw-is-valid' : validation === 'invalid' ? 'bw-is-invalid' : '';
3268
+ if (validClass) {
3269
+ styledInput.a.class = ((styledInput.a.class || '') + ' ' + validClass).trim();
3270
+ }
3271
+ }
2952
3272
 
2953
3273
  return {
2954
3274
  t: 'div',
@@ -2957,9 +3277,14 @@ function makeFormGroup(props = {}) {
2957
3277
  label && {
2958
3278
  t: 'label',
2959
3279
  a: { for: id, class: 'bw-form-label' },
2960
- c: label
3280
+ c: required ? [label, { t: 'span', a: { class: 'bw-text-danger', style: 'margin-left: 0.25rem' }, c: '*' }] : label
3281
+ },
3282
+ styledInput,
3283
+ feedback && validation && {
3284
+ t: 'div',
3285
+ a: { class: validation === 'valid' ? 'bw-valid-feedback' : 'bw-invalid-feedback' },
3286
+ c: feedback
2961
3287
  },
2962
- input,
2963
3288
  help && {
2964
3289
  t: 'small',
2965
3290
  a: { class: 'bw-form-text bw-text-muted' },
@@ -3924,17 +4249,15 @@ function makeCodeDemo(props = {}) {
3924
4249
  t: 'button',
3925
4250
  a: {
3926
4251
  class: 'bw-copy-btn bw-code-copy-btn',
3927
- onclick: (e) => {
3928
- navigator.clipboard.writeText(code).then(() => {
3929
- const btn = e.target;
3930
- const originalText = btn.textContent;
4252
+ onclick: function(e) {
4253
+ navigator.clipboard.writeText(code).then(function() {
4254
+ var btn = e.target;
4255
+ var originalText = btn.textContent;
3931
4256
  btn.textContent = 'Copied!';
3932
- btn.style.background = '#006666';
3933
- btn.style.color = '#fff';
3934
- setTimeout(() => {
4257
+ btn.classList.add('bw-code-copy-btn-copied');
4258
+ setTimeout(function() {
3935
4259
  btn.textContent = originalText;
3936
- btn.style.background = 'rgba(255,255,255,0.12)';
3937
- btn.style.color = '#aaa';
4260
+ btn.classList.remove('bw-code-copy-btn-copied');
3938
4261
  }, 2000);
3939
4262
  });
3940
4263
  }
@@ -4226,29 +4549,47 @@ function makeAccordion(props = {}) {
4226
4549
  var isOpen = collapse.classList.contains('bw-collapse-show');
4227
4550
 
4228
4551
  if (!multiOpen) {
4229
- // Close all siblings
4230
- var allCollapses = accordionEl.querySelectorAll('.bw-accordion-collapse');
4231
- var allButtons = accordionEl.querySelectorAll('.bw-accordion-button');
4232
- for (var j = 0; j < allCollapses.length; j++) {
4233
- allCollapses[j].classList.remove('bw-collapse-show');
4234
- allCollapses[j].style.maxHeight = null;
4235
- }
4236
- for (var k = 0; k < allButtons.length; k++) {
4237
- allButtons[k].classList.add('bw-collapsed');
4238
- allButtons[k].setAttribute('aria-expanded', 'false');
4552
+ // Animate-close all other open siblings
4553
+ var allItems = accordionEl.querySelectorAll('.bw-accordion-item');
4554
+ for (var j = 0; j < allItems.length; j++) {
4555
+ if (allItems[j] === accordionItem) continue;
4556
+ var sibCollapse = allItems[j].querySelector('.bw-accordion-collapse');
4557
+ var sibBtn = allItems[j].querySelector('.bw-accordion-button');
4558
+ if (sibCollapse.classList.contains('bw-collapse-show')) {
4559
+ sibCollapse.style.maxHeight = sibCollapse.scrollHeight + 'px';
4560
+ sibCollapse.offsetHeight; // force reflow
4561
+ sibCollapse.style.maxHeight = '0px';
4562
+ sibCollapse.classList.remove('bw-collapse-show');
4563
+ sibBtn.classList.add('bw-collapsed');
4564
+ sibBtn.setAttribute('aria-expanded', 'false');
4565
+ }
4239
4566
  }
4240
4567
  }
4241
4568
 
4242
4569
  if (isOpen) {
4570
+ // Animate close
4571
+ collapse.style.maxHeight = collapse.scrollHeight + 'px';
4572
+ collapse.offsetHeight; // force reflow
4573
+ collapse.style.maxHeight = '0px';
4243
4574
  collapse.classList.remove('bw-collapse-show');
4244
- collapse.style.maxHeight = null;
4245
4575
  btn.classList.add('bw-collapsed');
4246
4576
  btn.setAttribute('aria-expanded', 'false');
4247
4577
  } else {
4578
+ // Animate open
4248
4579
  collapse.classList.add('bw-collapse-show');
4580
+ collapse.style.maxHeight = '0px';
4581
+ collapse.offsetHeight; // force reflow
4249
4582
  collapse.style.maxHeight = collapse.scrollHeight + 'px';
4250
4583
  btn.classList.remove('bw-collapsed');
4251
4584
  btn.setAttribute('aria-expanded', 'true');
4585
+ // After transition, allow dynamic content sizing
4586
+ var onEnd = function(ev) {
4587
+ if (ev.propertyName === 'max-height' && collapse.classList.contains('bw-collapse-show')) {
4588
+ collapse.style.maxHeight = 'none';
4589
+ }
4590
+ collapse.removeEventListener('transitionend', onEnd);
4591
+ };
4592
+ collapse.addEventListener('transitionend', onEnd);
4252
4593
  }
4253
4594
  }
4254
4595
  },
@@ -4265,7 +4606,7 @@ function makeAccordion(props = {}) {
4265
4606
  },
4266
4607
  o: item.open ? {
4267
4608
  mounted: function(el) {
4268
- el.style.maxHeight = el.scrollHeight + 'px';
4609
+ el.style.maxHeight = 'none';
4269
4610
  }
4270
4611
  } : undefined
4271
4612
  }
@@ -4975,104 +5316,997 @@ function makeCarousel(props = {}) {
4975
5316
  a: {
4976
5317
  class: ('bw-carousel ' + className).trim(),
4977
5318
  style: 'height: ' + height,
5319
+ tabindex: '0',
5320
+ 'aria-roledescription': 'carousel',
4978
5321
  'data-carousel-index': startIndex
4979
5322
  },
4980
5323
  c: children,
4981
5324
  o: {
4982
5325
  type: 'carousel',
4983
5326
  state: { activeIndex: startIndex, autoPlay: autoPlay, interval: interval },
4984
- mounted: autoPlay ? function(el) {
4985
- var intervalId = setInterval(function() {
5327
+ mounted: function(el) {
5328
+ // Keyboard navigation
5329
+ el.addEventListener('keydown', function(e) {
4986
5330
  var idx = parseInt(el.getAttribute('data-carousel-index') || '0');
4987
- goToSlide(el, idx + 1);
4988
- }, interval);
4989
- el._bw_carouselInterval = intervalId;
4990
- } : undefined,
4991
- unmount: autoPlay ? function(el) {
5331
+ if (e.key === 'ArrowLeft') {
5332
+ e.preventDefault();
5333
+ goToSlide(el, idx - 1);
5334
+ } else if (e.key === 'ArrowRight') {
5335
+ e.preventDefault();
5336
+ goToSlide(el, idx + 1);
5337
+ }
5338
+ });
5339
+ // Auto-play
5340
+ if (autoPlay) {
5341
+ var intervalId = setInterval(function() {
5342
+ var idx = parseInt(el.getAttribute('data-carousel-index') || '0');
5343
+ goToSlide(el, idx + 1);
5344
+ }, interval);
5345
+ el._bw_carouselInterval = intervalId;
5346
+ // Pause on hover/focus for usability
5347
+ el.addEventListener('mouseenter', function() {
5348
+ if (el._bw_carouselInterval) clearInterval(el._bw_carouselInterval);
5349
+ });
5350
+ el.addEventListener('mouseleave', function() {
5351
+ el._bw_carouselInterval = setInterval(function() {
5352
+ var idx = parseInt(el.getAttribute('data-carousel-index') || '0');
5353
+ goToSlide(el, idx + 1);
5354
+ }, interval);
5355
+ });
5356
+ }
5357
+ },
5358
+ unmount: function(el) {
4992
5359
  if (el._bw_carouselInterval) {
4993
5360
  clearInterval(el._bw_carouselInterval);
4994
5361
  }
4995
- } : undefined
5362
+ }
4996
5363
  }
4997
5364
  };
4998
5365
  }
4999
5366
 
5000
- const componentHandles = {
5001
- card: CardHandle,
5002
- table: TableHandle,
5003
- navbar: NavbarHandle,
5004
- tabs: TabsHandle,
5005
- modal: ModalHandle
5006
- };
5007
-
5008
- var components = /*#__PURE__*/Object.freeze({
5009
- __proto__: null,
5010
- CardHandle: CardHandle,
5011
- ModalHandle: ModalHandle,
5012
- NavbarHandle: NavbarHandle,
5013
- TableHandle: TableHandle,
5014
- TabsHandle: TabsHandle,
5015
- componentHandles: componentHandles,
5016
- makeAccordion: makeAccordion,
5017
- makeAlert: makeAlert,
5018
- makeAvatar: makeAvatar,
5019
- makeBadge: makeBadge,
5020
- makeBreadcrumb: makeBreadcrumb,
5021
- makeButton: makeButton,
5022
- makeButtonGroup: makeButtonGroup,
5023
- makeCTA: makeCTA,
5024
- makeCard: makeCard,
5025
- makeCarousel: makeCarousel,
5026
- makeCheckbox: makeCheckbox,
5027
- makeCodeDemo: makeCodeDemo,
5028
- makeCol: makeCol,
5029
- makeContainer: makeContainer,
5030
- makeDropdown: makeDropdown,
5031
- makeFeatureGrid: makeFeatureGrid,
5032
- makeForm: makeForm,
5033
- makeFormGroup: makeFormGroup,
5034
- makeHero: makeHero,
5035
- makeInput: makeInput,
5036
- makeListGroup: makeListGroup,
5037
- makeModal: makeModal,
5038
- makeNav: makeNav,
5039
- makeNavbar: makeNavbar,
5040
- makePagination: makePagination,
5041
- makeProgress: makeProgress,
5042
- makeRadio: makeRadio,
5043
- makeRow: makeRow,
5044
- makeSection: makeSection,
5045
- makeSelect: makeSelect,
5046
- makeSkeleton: makeSkeleton,
5047
- makeSpinner: makeSpinner,
5048
- makeStack: makeStack,
5049
- makeSwitch: makeSwitch,
5050
- makeTabs: makeTabs,
5051
- makeTextarea: makeTextarea,
5052
- makeToast: makeToast
5053
- });
5367
+ // =========================================================================
5368
+ // Phase 4: Dashboard & Data Display
5369
+ // =========================================================================
5054
5370
 
5055
5371
  /**
5056
- * Bitwrench v2 Core
5057
- * Zero-dependency UI library using JavaScript objects
5058
- * Works in browsers (IE11+) and Node.js
5059
- *
5060
- * @license BSD-2-Clause
5061
- * @author M A Chatterjee <deftio [at] deftio [dot] com>
5372
+ * Create a stat card for dashboard metrics display
5373
+ *
5374
+ * Shows a large value with a label and optional change indicator.
5375
+ * Designed for dashboard grid layouts with left-border accent.
5376
+ *
5377
+ * @param {Object|string} [props] - Stat card configuration (string shorthand sets label)
5378
+ * @param {string|number} [props.value=0] - The main stat value to display
5379
+ * @param {string} [props.label] - Descriptive label below the value
5380
+ * @param {number} [props.change] - Percentage change indicator (positive = green arrow, negative = red)
5381
+ * @param {string} [props.format] - Value format ("number", "currency", "percent")
5382
+ * @param {string} [props.prefix] - Custom prefix (e.g. "$")
5383
+ * @param {string} [props.suffix] - Custom suffix (e.g. "%")
5384
+ * @param {string} [props.icon] - Icon content (emoji or text) shown above value
5385
+ * @param {string} [props.variant] - Left-border color variant ("primary", "success", "danger", etc.)
5386
+ * @param {string} [props.className] - Additional CSS classes
5387
+ * @param {Object} [props.style] - Inline style object
5388
+ * @returns {Object} TACO object representing a stat card
5389
+ * @category Component Builders
5390
+ * @example
5391
+ * const stat = makeStatCard({
5392
+ * value: 2345,
5393
+ * label: 'Active Users',
5394
+ * change: 5.3,
5395
+ * format: 'number',
5396
+ * variant: 'primary'
5397
+ * });
5062
5398
  */
5399
+ function makeStatCard(props = {}) {
5400
+ if (typeof props === 'string') props = { label: props };
5401
+ var {
5402
+ value = 0,
5403
+ label,
5404
+ change,
5405
+ format,
5406
+ prefix,
5407
+ suffix,
5408
+ icon,
5409
+ variant,
5410
+ className = '',
5411
+ style
5412
+ } = props;
5063
5413
 
5414
+ function formatValue(val, fmt) {
5415
+ if (prefix || suffix) return (prefix || '') + val + (suffix || '');
5416
+ switch (fmt) {
5417
+ case 'currency': return '$' + Number(val).toLocaleString();
5418
+ case 'percent': return val + '%';
5419
+ case 'number': return Number(val).toLocaleString();
5420
+ default: return '' + val;
5421
+ }
5422
+ }
5064
5423
 
5065
- // Environment-aware module loader for optional Node.js built-ins (fs).
5066
- // Strategy: try require() first (CJS/UMD), fall back to import() (ESM).
5067
- // import() is wrapped in Function() to avoid parse errors in ES5/IE11 environments.
5424
+ var classes = [
5425
+ 'bw-stat-card',
5426
+ variant ? 'bw-stat-card-' + variant : '',
5427
+ className
5428
+ ].filter(Boolean).join(' ').trim();
5068
5429
 
5069
- // Core bitwrench namespace
5070
- const bw = {
5071
- // Version info from generated file
5072
- version: VERSION_INFO.version,
5073
- versionInfo: VERSION_INFO,
5074
-
5075
- /**
5430
+ var children = [];
5431
+
5432
+ if (icon) {
5433
+ children.push({
5434
+ t: 'div',
5435
+ a: { class: 'bw-stat-icon' },
5436
+ c: icon
5437
+ });
5438
+ }
5439
+
5440
+ children.push({
5441
+ t: 'div',
5442
+ a: { class: 'bw-stat-value' },
5443
+ c: formatValue(value, format)
5444
+ });
5445
+
5446
+ if (label) {
5447
+ children.push({
5448
+ t: 'div',
5449
+ a: { class: 'bw-stat-label' },
5450
+ c: label
5451
+ });
5452
+ }
5453
+
5454
+ if (change !== undefined && change !== null) {
5455
+ children.push({
5456
+ t: 'div',
5457
+ a: {
5458
+ class: 'bw-stat-change ' + (change >= 0 ? 'bw-stat-change-up' : 'bw-stat-change-down')
5459
+ },
5460
+ c: (change >= 0 ? '\u2191 +' : '\u2193 ') + change + '%'
5461
+ });
5462
+ }
5463
+
5464
+ return {
5465
+ t: 'div',
5466
+ a: { class: classes, style: style },
5467
+ c: children,
5468
+ o: { type: 'stat-card' }
5469
+ };
5470
+ }
5471
+
5472
+ // =========================================================================
5473
+ // Phase 5: Overlays & Popovers
5474
+ // =========================================================================
5475
+
5476
+ /**
5477
+ * Create a tooltip wrapper around trigger content
5478
+ *
5479
+ * Wraps the trigger element in a container that shows tooltip text
5480
+ * on hover and focus. Pure CSS-driven show/hide with JS lifecycle
5481
+ * for event binding.
5482
+ *
5483
+ * @param {Object} [props] - Tooltip configuration
5484
+ * @param {string|Object|Array} [props.content] - Trigger content (what the user hovers/focuses)
5485
+ * @param {string} [props.text=""] - Tooltip text to display
5486
+ * @param {string} [props.placement="top"] - Tooltip placement ("top", "bottom", "left", "right")
5487
+ * @param {string} [props.className] - Additional CSS classes
5488
+ * @returns {Object} TACO object representing a tooltip wrapper
5489
+ * @category Component Builders
5490
+ * @example
5491
+ * const tip = makeTooltip({
5492
+ * content: makeButton({ text: 'Hover me' }),
5493
+ * text: 'This is a tooltip!',
5494
+ * placement: 'top'
5495
+ * });
5496
+ */
5497
+ function makeTooltip(props = {}) {
5498
+ var {
5499
+ content,
5500
+ text = '',
5501
+ placement = 'top',
5502
+ className = ''
5503
+ } = props;
5504
+
5505
+ return {
5506
+ t: 'span',
5507
+ a: { class: ('bw-tooltip-wrapper ' + className).trim() },
5508
+ c: [
5509
+ content,
5510
+ {
5511
+ t: 'span',
5512
+ a: {
5513
+ class: 'bw-tooltip bw-tooltip-' + placement,
5514
+ role: 'tooltip'
5515
+ },
5516
+ c: text
5517
+ }
5518
+ ],
5519
+ o: {
5520
+ type: 'tooltip',
5521
+ mounted: function(el) {
5522
+ var tip = el.querySelector('.bw-tooltip');
5523
+ el.addEventListener('mouseenter', function() {
5524
+ tip.classList.add('bw-tooltip-show');
5525
+ });
5526
+ el.addEventListener('mouseleave', function() {
5527
+ tip.classList.remove('bw-tooltip-show');
5528
+ });
5529
+ el.addEventListener('focusin', function() {
5530
+ tip.classList.add('bw-tooltip-show');
5531
+ });
5532
+ el.addEventListener('focusout', function() {
5533
+ tip.classList.remove('bw-tooltip-show');
5534
+ });
5535
+ }
5536
+ }
5537
+ };
5538
+ }
5539
+
5540
+ /**
5541
+ * Create a popover wrapper around trigger content
5542
+ *
5543
+ * Like a tooltip but richer — supports title + body content and is
5544
+ * triggered by click rather than hover. Dismisses on click outside.
5545
+ *
5546
+ * @param {Object} [props] - Popover configuration
5547
+ * @param {string|Object|Array} [props.trigger] - Trigger content (what the user clicks)
5548
+ * @param {string} [props.title] - Popover header title
5549
+ * @param {string|Object|Array} [props.content] - Popover body content
5550
+ * @param {string} [props.placement="top"] - Placement ("top", "bottom", "left", "right")
5551
+ * @param {string} [props.className] - Additional CSS classes
5552
+ * @returns {Object} TACO object representing a popover wrapper
5553
+ * @category Component Builders
5554
+ * @example
5555
+ * const pop = makePopover({
5556
+ * trigger: makeButton({ text: 'Click me' }),
5557
+ * title: 'Popover Title',
5558
+ * content: 'Some helpful information here.',
5559
+ * placement: 'bottom'
5560
+ * });
5561
+ */
5562
+ function makePopover(props = {}) {
5563
+ var {
5564
+ trigger,
5565
+ title,
5566
+ content,
5567
+ placement = 'top',
5568
+ className = ''
5569
+ } = props;
5570
+
5571
+ var popoverContent = [
5572
+ title && {
5573
+ t: 'div',
5574
+ a: { class: 'bw-popover-header' },
5575
+ c: title
5576
+ },
5577
+ content && {
5578
+ t: 'div',
5579
+ a: { class: 'bw-popover-body' },
5580
+ c: content
5581
+ }
5582
+ ].filter(Boolean);
5583
+
5584
+ return {
5585
+ t: 'span',
5586
+ a: { class: ('bw-popover-wrapper ' + className).trim() },
5587
+ c: [
5588
+ {
5589
+ t: 'span',
5590
+ a: {
5591
+ class: 'bw-popover-trigger',
5592
+ onclick: function(e) {
5593
+ var wrapper = e.target.closest('.bw-popover-wrapper');
5594
+ var pop = wrapper.querySelector('.bw-popover');
5595
+ pop.classList.toggle('bw-popover-show');
5596
+ }
5597
+ },
5598
+ c: trigger
5599
+ },
5600
+ {
5601
+ t: 'div',
5602
+ a: {
5603
+ class: 'bw-popover bw-popover-' + placement
5604
+ },
5605
+ c: popoverContent
5606
+ }
5607
+ ],
5608
+ o: {
5609
+ type: 'popover',
5610
+ mounted: function(el) {
5611
+ // Click outside to close
5612
+ var outsideHandler = function(e) {
5613
+ if (!el.contains(e.target)) {
5614
+ var pop = el.querySelector('.bw-popover');
5615
+ if (pop) pop.classList.remove('bw-popover-show');
5616
+ }
5617
+ };
5618
+ document.addEventListener('click', outsideHandler);
5619
+ el._bw_outsideHandler = outsideHandler;
5620
+ },
5621
+ unmount: function(el) {
5622
+ if (el._bw_outsideHandler) {
5623
+ document.removeEventListener('click', el._bw_outsideHandler);
5624
+ }
5625
+ }
5626
+ }
5627
+ };
5628
+ }
5629
+
5630
+ // =========================================================================
5631
+ // Phase 6: Form Enhancements & Layout
5632
+ // =========================================================================
5633
+
5634
+ /**
5635
+ * Create a search input with clear button
5636
+ *
5637
+ * Wraps a text input with a clear (×) button that appears when
5638
+ * the field has content. Calls onSearch on Enter key.
5639
+ *
5640
+ * @param {Object} [props] - Search input configuration
5641
+ * @param {string} [props.placeholder="Search..."] - Placeholder text
5642
+ * @param {string} [props.value] - Initial value
5643
+ * @param {Function} [props.onSearch] - Callback when Enter is pressed, receives value
5644
+ * @param {Function} [props.onInput] - Callback on each keystroke, receives value
5645
+ * @param {string} [props.id] - Element ID
5646
+ * @param {string} [props.name] - Input name attribute
5647
+ * @param {string} [props.className] - Additional CSS classes
5648
+ * @returns {Object} TACO object representing a search input
5649
+ * @category Component Builders
5650
+ * @example
5651
+ * const search = makeSearchInput({
5652
+ * placeholder: 'Search users...',
5653
+ * onSearch: (val) => filterUsers(val)
5654
+ * });
5655
+ */
5656
+ function makeSearchInput(props = {}) {
5657
+ if (typeof props === 'string') props = { placeholder: props };
5658
+ var {
5659
+ placeholder = 'Search...',
5660
+ value,
5661
+ onSearch,
5662
+ onInput,
5663
+ id,
5664
+ name,
5665
+ className = ''
5666
+ } = props;
5667
+
5668
+ return {
5669
+ t: 'div',
5670
+ a: { class: ('bw-search-input ' + className).trim() },
5671
+ c: [
5672
+ {
5673
+ t: 'input',
5674
+ a: {
5675
+ type: 'search',
5676
+ class: 'bw-form-control bw-search-field',
5677
+ placeholder: placeholder,
5678
+ value: value,
5679
+ id: id,
5680
+ name: name,
5681
+ onkeydown: function(e) {
5682
+ if (e.key === 'Enter' && onSearch) {
5683
+ e.preventDefault();
5684
+ onSearch(e.target.value);
5685
+ }
5686
+ },
5687
+ oninput: function(e) {
5688
+ var wrapper = e.target.closest('.bw-search-input');
5689
+ var clearBtn = wrapper.querySelector('.bw-search-clear');
5690
+ if (clearBtn) {
5691
+ clearBtn.style.display = e.target.value ? 'flex' : 'none';
5692
+ }
5693
+ if (onInput) onInput(e.target.value);
5694
+ }
5695
+ }
5696
+ },
5697
+ {
5698
+ t: 'button',
5699
+ a: {
5700
+ type: 'button',
5701
+ class: 'bw-search-clear',
5702
+ 'aria-label': 'Clear search',
5703
+ style: value ? undefined : 'display: none',
5704
+ onclick: function(e) {
5705
+ var wrapper = e.target.closest('.bw-search-input');
5706
+ var input = wrapper.querySelector('.bw-search-field');
5707
+ input.value = '';
5708
+ e.target.style.display = 'none';
5709
+ input.focus();
5710
+ if (onInput) onInput('');
5711
+ if (onSearch) onSearch('');
5712
+ }
5713
+ },
5714
+ c: '\u00D7'
5715
+ }
5716
+ ],
5717
+ o: { type: 'search-input' }
5718
+ };
5719
+ }
5720
+
5721
+ /**
5722
+ * Create a styled range slider input
5723
+ *
5724
+ * @param {Object} [props] - Range configuration
5725
+ * @param {number} [props.min=0] - Minimum value
5726
+ * @param {number} [props.max=100] - Maximum value
5727
+ * @param {number} [props.step=1] - Step increment
5728
+ * @param {number} [props.value=50] - Current value
5729
+ * @param {string} [props.label] - Label text
5730
+ * @param {boolean} [props.showValue=false] - Show current value display
5731
+ * @param {string} [props.id] - Element ID
5732
+ * @param {string} [props.name] - Input name attribute
5733
+ * @param {boolean} [props.disabled=false] - Whether the slider is disabled
5734
+ * @param {string} [props.className] - Additional CSS classes
5735
+ * @returns {Object} TACO object representing a range input
5736
+ * @category Component Builders
5737
+ * @example
5738
+ * const slider = makeRange({
5739
+ * min: 0, max: 100, value: 50,
5740
+ * label: 'Volume',
5741
+ * showValue: true,
5742
+ * oninput: (e) => setVolume(e.target.value)
5743
+ * });
5744
+ */
5745
+ function makeRange(props = {}) {
5746
+ var {
5747
+ min = 0,
5748
+ max = 100,
5749
+ step = 1,
5750
+ value = 50,
5751
+ label,
5752
+ showValue = false,
5753
+ id,
5754
+ name,
5755
+ disabled = false,
5756
+ className = '',
5757
+ ...eventHandlers
5758
+ } = props;
5759
+
5760
+ var children = [];
5761
+
5762
+ if (label || showValue) {
5763
+ var labelContent = [];
5764
+ if (label) {
5765
+ labelContent.push({
5766
+ t: 'span',
5767
+ c: label
5768
+ });
5769
+ }
5770
+ if (showValue) {
5771
+ labelContent.push({
5772
+ t: 'span',
5773
+ a: { class: 'bw-range-value' },
5774
+ c: '' + value
5775
+ });
5776
+ }
5777
+ children.push({
5778
+ t: 'div',
5779
+ a: { class: 'bw-range-label' },
5780
+ c: labelContent
5781
+ });
5782
+ }
5783
+
5784
+ // Wrap oninput to update value display
5785
+ var userOnInput = eventHandlers.oninput;
5786
+ if (showValue) {
5787
+ eventHandlers.oninput = function(e) {
5788
+ var wrapper = e.target.closest('.bw-range-wrapper');
5789
+ var valDisplay = wrapper.querySelector('.bw-range-value');
5790
+ if (valDisplay) valDisplay.textContent = e.target.value;
5791
+ if (userOnInput) userOnInput(e);
5792
+ };
5793
+ }
5794
+
5795
+ children.push({
5796
+ t: 'input',
5797
+ a: {
5798
+ type: 'range',
5799
+ class: 'bw-range',
5800
+ min: min,
5801
+ max: max,
5802
+ step: step,
5803
+ value: value,
5804
+ id: id,
5805
+ name: name,
5806
+ disabled: disabled,
5807
+ ...eventHandlers
5808
+ }
5809
+ });
5810
+
5811
+ return {
5812
+ t: 'div',
5813
+ a: { class: ('bw-range-wrapper ' + className).trim() },
5814
+ c: children,
5815
+ o: { type: 'range' }
5816
+ };
5817
+ }
5818
+
5819
+ /**
5820
+ * Create a media object layout (image + text side-by-side)
5821
+ *
5822
+ * Classic media object pattern: image/icon on one side, text content
5823
+ * on the other, using flexbox. Supports reversed layout.
5824
+ *
5825
+ * @param {Object} [props] - Media object configuration
5826
+ * @param {string} [props.src] - Image source URL
5827
+ * @param {string} [props.alt=""] - Image alt text
5828
+ * @param {string} [props.title] - Title text
5829
+ * @param {string|Object|Array} [props.content] - Body content
5830
+ * @param {boolean} [props.reverse=false] - Put image on the right
5831
+ * @param {string} [props.imageSize="3rem"] - Image width/height
5832
+ * @param {string} [props.className] - Additional CSS classes
5833
+ * @returns {Object} TACO object representing a media object
5834
+ * @category Component Builders
5835
+ * @example
5836
+ * const media = makeMediaObject({
5837
+ * src: '/avatar.jpg',
5838
+ * title: 'Jane Doe',
5839
+ * content: 'Posted a comment 5 minutes ago.'
5840
+ * });
5841
+ */
5842
+ function makeMediaObject(props = {}) {
5843
+ var {
5844
+ src,
5845
+ alt = '',
5846
+ title,
5847
+ content,
5848
+ reverse = false,
5849
+ imageSize = '3rem',
5850
+ className = ''
5851
+ } = props;
5852
+
5853
+ var imgEl = src ? {
5854
+ t: 'img',
5855
+ a: {
5856
+ class: 'bw-media-img',
5857
+ src: src,
5858
+ alt: alt,
5859
+ style: 'width:' + imageSize + ';height:' + imageSize
5860
+ }
5861
+ } : null;
5862
+
5863
+ var bodyEl = {
5864
+ t: 'div',
5865
+ a: { class: 'bw-media-body' },
5866
+ c: [
5867
+ title && { t: 'h5', a: { class: 'bw-media-title' }, c: title },
5868
+ content
5869
+ ].filter(Boolean)
5870
+ };
5871
+
5872
+ return {
5873
+ t: 'div',
5874
+ a: { class: ('bw-media ' + (reverse ? 'bw-media-reverse ' : '') + className).trim() },
5875
+ c: reverse
5876
+ ? [bodyEl, imgEl].filter(Boolean)
5877
+ : [imgEl, bodyEl].filter(Boolean),
5878
+ o: { type: 'media-object' }
5879
+ };
5880
+ }
5881
+
5882
+ /**
5883
+ * Create a file upload zone with drag-and-drop support
5884
+ *
5885
+ * Styled drop zone with file input. Supports drag-and-drop visuals
5886
+ * and multiple file selection.
5887
+ *
5888
+ * @param {Object} [props] - File upload configuration
5889
+ * @param {string} [props.accept] - Accepted file types (e.g. "image/*", ".pdf,.doc")
5890
+ * @param {boolean} [props.multiple=false] - Allow multiple file selection
5891
+ * @param {Function} [props.onFiles] - Callback when files are selected, receives FileList
5892
+ * @param {string} [props.text="Drop files here or click to browse"] - Zone label text
5893
+ * @param {string} [props.id] - Element ID
5894
+ * @param {string} [props.className] - Additional CSS classes
5895
+ * @returns {Object} TACO object representing a file upload zone
5896
+ * @category Component Builders
5897
+ * @example
5898
+ * const upload = makeFileUpload({
5899
+ * accept: 'image/*',
5900
+ * multiple: true,
5901
+ * onFiles: (files) => uploadFiles(files)
5902
+ * });
5903
+ */
5904
+ function makeFileUpload(props = {}) {
5905
+ var {
5906
+ accept,
5907
+ multiple = false,
5908
+ onFiles,
5909
+ text = 'Drop files here or click to browse',
5910
+ id,
5911
+ className = ''
5912
+ } = props;
5913
+
5914
+ return {
5915
+ t: 'div',
5916
+ a: {
5917
+ class: ('bw-file-upload ' + className).trim(),
5918
+ tabindex: '0',
5919
+ role: 'button',
5920
+ 'aria-label': text
5921
+ },
5922
+ c: [
5923
+ { t: 'div', a: { class: 'bw-file-upload-icon' }, c: '\uD83D\uDCC1' },
5924
+ { t: 'div', a: { class: 'bw-file-upload-text' }, c: text },
5925
+ {
5926
+ t: 'input',
5927
+ a: {
5928
+ type: 'file',
5929
+ class: 'bw-file-upload-input',
5930
+ accept: accept,
5931
+ multiple: multiple,
5932
+ id: id,
5933
+ onchange: function(e) {
5934
+ if (onFiles && e.target.files.length) onFiles(e.target.files);
5935
+ }
5936
+ }
5937
+ }
5938
+ ],
5939
+ o: {
5940
+ type: 'file-upload',
5941
+ mounted: function(el) {
5942
+ var input = el.querySelector('.bw-file-upload-input');
5943
+
5944
+ // Click zone to trigger file input
5945
+ el.addEventListener('click', function(e) {
5946
+ if (e.target !== input) input.click();
5947
+ });
5948
+
5949
+ // Keyboard activation
5950
+ el.addEventListener('keydown', function(e) {
5951
+ if (e.key === 'Enter' || e.key === ' ') {
5952
+ e.preventDefault();
5953
+ input.click();
5954
+ }
5955
+ });
5956
+
5957
+ // Drag-and-drop visuals
5958
+ el.addEventListener('dragover', function(e) {
5959
+ e.preventDefault();
5960
+ el.classList.add('bw-file-upload-active');
5961
+ });
5962
+ el.addEventListener('dragleave', function() {
5963
+ el.classList.remove('bw-file-upload-active');
5964
+ });
5965
+ el.addEventListener('drop', function(e) {
5966
+ e.preventDefault();
5967
+ el.classList.remove('bw-file-upload-active');
5968
+ if (onFiles && e.dataTransfer.files.length) onFiles(e.dataTransfer.files);
5969
+ });
5970
+ }
5971
+ }
5972
+ };
5973
+ }
5974
+
5975
+ // =========================================================================
5976
+ // Phase 7: Data Display & Workflow
5977
+ // =========================================================================
5978
+
5979
+ /**
5980
+ * Create a vertical timeline for chronological event display
5981
+ *
5982
+ * Renders events as a vertical line with markers and content cards.
5983
+ * Each item can have a colored variant marker.
5984
+ *
5985
+ * @param {Object} [props] - Timeline configuration
5986
+ * @param {Array<Object>} [props.items=[]] - Timeline events
5987
+ * @param {string} [props.items[].title] - Event title
5988
+ * @param {string|Object|Array} [props.items[].content] - Event description content
5989
+ * @param {string} [props.items[].date] - Date or time label
5990
+ * @param {string} [props.items[].variant="primary"] - Marker color variant
5991
+ * @param {string} [props.className] - Additional CSS classes
5992
+ * @returns {Object} TACO object representing a timeline
5993
+ * @category Component Builders
5994
+ * @example
5995
+ * const timeline = makeTimeline({
5996
+ * items: [
5997
+ * { title: 'Project Started', date: 'Jan 2026', variant: 'primary' },
5998
+ * { title: 'Beta Release', date: 'Mar 2026', content: 'v2.0 beta shipped' },
5999
+ * { title: 'Stable Release', date: 'Jun 2026', variant: 'success' }
6000
+ * ]
6001
+ * });
6002
+ */
6003
+ function makeTimeline(props = {}) {
6004
+ var {
6005
+ items = [],
6006
+ className = ''
6007
+ } = props;
6008
+
6009
+ return {
6010
+ t: 'div',
6011
+ a: { class: ('bw-timeline ' + className).trim() },
6012
+ c: items.map(function(item) {
6013
+ return {
6014
+ t: 'div',
6015
+ a: { class: 'bw-timeline-item' },
6016
+ c: [
6017
+ {
6018
+ t: 'div',
6019
+ a: { class: 'bw-timeline-marker bw-timeline-marker-' + (item.variant || 'primary') }
6020
+ },
6021
+ {
6022
+ t: 'div',
6023
+ a: { class: 'bw-timeline-content' },
6024
+ c: [
6025
+ item.date && {
6026
+ t: 'div',
6027
+ a: { class: 'bw-timeline-date' },
6028
+ c: item.date
6029
+ },
6030
+ item.title && {
6031
+ t: 'h5',
6032
+ a: { class: 'bw-timeline-title' },
6033
+ c: item.title
6034
+ },
6035
+ item.content && (typeof item.content === 'string'
6036
+ ? { t: 'p', a: { class: 'bw-timeline-text' }, c: item.content }
6037
+ : item.content)
6038
+ ].filter(Boolean)
6039
+ }
6040
+ ]
6041
+ };
6042
+ }),
6043
+ o: { type: 'timeline' }
6044
+ };
6045
+ }
6046
+
6047
+ /**
6048
+ * Create a multi-step wizard/progress indicator
6049
+ *
6050
+ * Displays numbered steps with active and completed states.
6051
+ * Steps before currentStep are marked completed, the currentStep
6052
+ * is active, and subsequent steps are pending.
6053
+ *
6054
+ * @param {Object} [props] - Stepper configuration
6055
+ * @param {Array<Object>} [props.steps=[]] - Step definitions
6056
+ * @param {string} [props.steps[].label] - Step label text
6057
+ * @param {string} [props.steps[].description] - Optional step description
6058
+ * @param {number} [props.currentStep=0] - Zero-based index of the active step
6059
+ * @param {string} [props.className] - Additional CSS classes
6060
+ * @returns {Object} TACO object representing a stepper
6061
+ * @category Component Builders
6062
+ * @example
6063
+ * const stepper = makeStepper({
6064
+ * currentStep: 1,
6065
+ * steps: [
6066
+ * { label: 'Account', description: 'Create account' },
6067
+ * { label: 'Profile', description: 'Set up profile' },
6068
+ * { label: 'Confirm', description: 'Review & submit' }
6069
+ * ]
6070
+ * });
6071
+ */
6072
+ function makeStepper(props = {}) {
6073
+ var {
6074
+ steps = [],
6075
+ currentStep = 0,
6076
+ className = ''
6077
+ } = props;
6078
+
6079
+ return {
6080
+ t: 'div',
6081
+ a: { class: ('bw-stepper ' + className).trim(), role: 'list' },
6082
+ c: steps.map(function(step, index) {
6083
+ var state = index < currentStep ? 'completed' : index === currentStep ? 'active' : 'pending';
6084
+ return {
6085
+ t: 'div',
6086
+ a: {
6087
+ class: 'bw-step bw-step-' + state,
6088
+ role: 'listitem',
6089
+ 'aria-current': state === 'active' ? 'step' : undefined
6090
+ },
6091
+ c: [
6092
+ {
6093
+ t: 'div',
6094
+ a: { class: 'bw-step-indicator' },
6095
+ c: state === 'completed' ? '\u2713' : '' + (index + 1)
6096
+ },
6097
+ {
6098
+ t: 'div',
6099
+ a: { class: 'bw-step-body' },
6100
+ c: [
6101
+ { t: 'div', a: { class: 'bw-step-label' }, c: step.label },
6102
+ step.description && { t: 'div', a: { class: 'bw-step-description' }, c: step.description }
6103
+ ].filter(Boolean)
6104
+ }
6105
+ ]
6106
+ };
6107
+ }),
6108
+ o: { type: 'stepper' }
6109
+ };
6110
+ }
6111
+
6112
+ /**
6113
+ * Create a chip/tag input for managing a list of items
6114
+ *
6115
+ * Displays existing chips with remove buttons and an input field
6116
+ * for adding new ones. Chips are added on Enter and removed on
6117
+ * clicking the × button.
6118
+ *
6119
+ * @param {Object} [props] - Chip input configuration
6120
+ * @param {Array<string>} [props.chips=[]] - Initial chip values
6121
+ * @param {string} [props.placeholder="Add..."] - Input placeholder text
6122
+ * @param {Function} [props.onAdd] - Callback when a chip is added, receives value
6123
+ * @param {Function} [props.onRemove] - Callback when a chip is removed, receives value
6124
+ * @param {string} [props.className] - Additional CSS classes
6125
+ * @returns {Object} TACO object representing a chip input
6126
+ * @category Component Builders
6127
+ * @example
6128
+ * const tags = makeChipInput({
6129
+ * chips: ['JavaScript', 'CSS'],
6130
+ * placeholder: 'Add tag...',
6131
+ * onAdd: (val) => addTag(val),
6132
+ * onRemove: (val) => removeTag(val)
6133
+ * });
6134
+ */
6135
+ function makeChipInput(props = {}) {
6136
+ var {
6137
+ chips = [],
6138
+ placeholder = 'Add...',
6139
+ onAdd,
6140
+ onRemove,
6141
+ className = ''
6142
+ } = props;
6143
+
6144
+ function makeChipEl(text) {
6145
+ return {
6146
+ t: 'span',
6147
+ a: { class: 'bw-chip', 'data-chip-value': text },
6148
+ c: [
6149
+ text,
6150
+ {
6151
+ t: 'button',
6152
+ a: {
6153
+ type: 'button',
6154
+ class: 'bw-chip-remove',
6155
+ 'aria-label': 'Remove ' + text,
6156
+ onclick: function(e) {
6157
+ var chip = e.target.closest('.bw-chip');
6158
+ var val = chip.getAttribute('data-chip-value');
6159
+ chip.parentNode.removeChild(chip);
6160
+ if (onRemove) onRemove(val);
6161
+ }
6162
+ },
6163
+ c: '\u00D7'
6164
+ }
6165
+ ]
6166
+ };
6167
+ }
6168
+
6169
+ return {
6170
+ t: 'div',
6171
+ a: { class: ('bw-chip-input ' + className).trim() },
6172
+ c: [
6173
+ ...chips.map(makeChipEl),
6174
+ {
6175
+ t: 'input',
6176
+ a: {
6177
+ type: 'text',
6178
+ class: 'bw-chip-field',
6179
+ placeholder: placeholder,
6180
+ onkeydown: function(e) {
6181
+ if (e.key === 'Enter' && e.target.value.trim()) {
6182
+ e.preventDefault();
6183
+ var val = e.target.value.trim();
6184
+ var wrapper = e.target.closest('.bw-chip-input');
6185
+ // Insert chip before the input
6186
+ var chipEl = document.createElement('span');
6187
+ chipEl.className = 'bw-chip';
6188
+ chipEl.setAttribute('data-chip-value', val);
6189
+ chipEl.innerHTML = '';
6190
+ chipEl.textContent = val;
6191
+ var removeBtn = document.createElement('button');
6192
+ removeBtn.type = 'button';
6193
+ removeBtn.className = 'bw-chip-remove';
6194
+ removeBtn.setAttribute('aria-label', 'Remove ' + val);
6195
+ removeBtn.textContent = '\u00D7';
6196
+ removeBtn.onclick = function() {
6197
+ chipEl.parentNode.removeChild(chipEl);
6198
+ if (onRemove) onRemove(val);
6199
+ };
6200
+ chipEl.appendChild(removeBtn);
6201
+ wrapper.insertBefore(chipEl, e.target);
6202
+ e.target.value = '';
6203
+ if (onAdd) onAdd(val);
6204
+ }
6205
+ // Backspace on empty input removes last chip
6206
+ if (e.key === 'Backspace' && !e.target.value) {
6207
+ var wrapper = e.target.closest('.bw-chip-input');
6208
+ var chipEls = wrapper.querySelectorAll('.bw-chip');
6209
+ if (chipEls.length) {
6210
+ var last = chipEls[chipEls.length - 1];
6211
+ var removedVal = last.getAttribute('data-chip-value');
6212
+ last.parentNode.removeChild(last);
6213
+ if (onRemove) onRemove(removedVal);
6214
+ }
6215
+ }
6216
+ }
6217
+ }
6218
+ }
6219
+ ],
6220
+ o: { type: 'chip-input' }
6221
+ };
6222
+ }
6223
+
6224
+ const componentHandles = {
6225
+ card: CardHandle,
6226
+ table: TableHandle,
6227
+ navbar: NavbarHandle,
6228
+ tabs: TabsHandle,
6229
+ modal: ModalHandle
6230
+ };
6231
+
6232
+ var components = /*#__PURE__*/Object.freeze({
6233
+ __proto__: null,
6234
+ CardHandle: CardHandle,
6235
+ ModalHandle: ModalHandle,
6236
+ NavbarHandle: NavbarHandle,
6237
+ TableHandle: TableHandle,
6238
+ TabsHandle: TabsHandle,
6239
+ componentHandles: componentHandles,
6240
+ makeAccordion: makeAccordion,
6241
+ makeAlert: makeAlert,
6242
+ makeAvatar: makeAvatar,
6243
+ makeBadge: makeBadge,
6244
+ makeBreadcrumb: makeBreadcrumb,
6245
+ makeButton: makeButton,
6246
+ makeButtonGroup: makeButtonGroup,
6247
+ makeCTA: makeCTA,
6248
+ makeCard: makeCard,
6249
+ makeCarousel: makeCarousel,
6250
+ makeCheckbox: makeCheckbox,
6251
+ makeChipInput: makeChipInput,
6252
+ makeCodeDemo: makeCodeDemo,
6253
+ makeCol: makeCol,
6254
+ makeContainer: makeContainer,
6255
+ makeDropdown: makeDropdown,
6256
+ makeFeatureGrid: makeFeatureGrid,
6257
+ makeFileUpload: makeFileUpload,
6258
+ makeForm: makeForm,
6259
+ makeFormGroup: makeFormGroup,
6260
+ makeHero: makeHero,
6261
+ makeInput: makeInput,
6262
+ makeListGroup: makeListGroup,
6263
+ makeMediaObject: makeMediaObject,
6264
+ makeModal: makeModal,
6265
+ makeNav: makeNav,
6266
+ makeNavbar: makeNavbar,
6267
+ makePagination: makePagination,
6268
+ makePopover: makePopover,
6269
+ makeProgress: makeProgress,
6270
+ makeRadio: makeRadio,
6271
+ makeRange: makeRange,
6272
+ makeRow: makeRow,
6273
+ makeSearchInput: makeSearchInput,
6274
+ makeSection: makeSection,
6275
+ makeSelect: makeSelect,
6276
+ makeSkeleton: makeSkeleton,
6277
+ makeSpinner: makeSpinner,
6278
+ makeStack: makeStack,
6279
+ makeStatCard: makeStatCard,
6280
+ makeStepper: makeStepper,
6281
+ makeSwitch: makeSwitch,
6282
+ makeTabs: makeTabs,
6283
+ makeTextarea: makeTextarea,
6284
+ makeTimeline: makeTimeline,
6285
+ makeToast: makeToast,
6286
+ makeTooltip: makeTooltip
6287
+ });
6288
+
6289
+ /**
6290
+ * Bitwrench v2 Core
6291
+ * Zero-dependency UI library using JavaScript objects
6292
+ * Works in browsers (IE11+) and Node.js
6293
+ *
6294
+ * @license BSD-2-Clause
6295
+ * @author M A Chatterjee <deftio [at] deftio [dot] com>
6296
+ */
6297
+
6298
+
6299
+ // Environment-aware module loader for optional Node.js built-ins (fs).
6300
+ // Strategy: try require() first (CJS/UMD), fall back to import() (ESM).
6301
+ // import() is wrapped in Function() to avoid parse errors in ES5/IE11 environments.
6302
+
6303
+ // Core bitwrench namespace
6304
+ const bw = {
6305
+ // Version info from generated file
6306
+ version: VERSION_INFO.version,
6307
+ versionInfo: VERSION_INFO,
6308
+
6309
+ /**
5076
6310
  * Get version metadata object (v1-compatible callable API).
5077
6311
  *
5078
6312
  * Returns a copy of the build-time version info including version string,
@@ -6670,8 +7904,10 @@ bw.u = {
6670
7904
  /**
6671
7905
  * Generate responsive CSS with media query breakpoints.
6672
7906
  *
6673
- * Produces a CSS string with `@media` rules for sm (640px), md (768px),
6674
- * lg (1024px), and xl (1280px) breakpoints. Pass the result to `bw.injectCSS()`.
7907
+ * Produces a CSS string with `@media (min-width)` rules for standard
7908
+ * breakpoints. These match the grid system and theme.breakpoints:
7909
+ * sm: 576px, md: 768px, lg: 992px, xl: 1200px
7910
+ * Pass the result to `bw.injectCSS()`.
6675
7911
  *
6676
7912
  * @param {string} selector - CSS selector
6677
7913
  * @param {Object} breakpoints - Object with keys: base, sm, md, lg, xl
@@ -6688,7 +7924,7 @@ bw.u = {
6688
7924
  * bw.injectCSS(css);
6689
7925
  */
6690
7926
  bw.responsive = function(selector, breakpoints) {
6691
- var sizes = { sm: '640px', md: '768px', lg: '1024px', xl: '1280px' };
7927
+ var sizes = { sm: '576px', md: '768px', lg: '992px', xl: '1200px' };
6692
7928
  var parts = [];
6693
7929
  Object.keys(breakpoints).forEach(function(key) {
6694
7930
  var rules = {};
@@ -6822,7 +8058,8 @@ if (bw._isBrowser) {
6822
8058
  * @returns {Element|null} Style element if in browser, null in Node.js
6823
8059
  * @category CSS & Styling
6824
8060
  * @see bw.setTheme
6825
- * @see bw.toggleDarkMode
8061
+ * @see bw.applyTheme
8062
+ * @see bw.toggleTheme
6826
8063
  * @example
6827
8064
  * bw.loadDefaultStyles(); // inject all default CSS
6828
8065
  */
@@ -6889,53 +8126,6 @@ bw.setTheme = function(overrides, options = {}) {
6889
8126
  return bw.getTheme();
6890
8127
  };
6891
8128
 
6892
- /**
6893
- * Toggle dark mode on/off.
6894
- *
6895
- * Adds/removes the `bw-dark` class on `<html>` and injects dark mode CSS
6896
- * overrides. Pass `true`/`false` to force a mode, or omit to toggle.
6897
- *
6898
- * @param {boolean} [force] - Force dark (true) or light (false). Omit to toggle.
6899
- * @returns {boolean} Whether dark mode is now active
6900
- * @category CSS & Styling
6901
- * @see bw.setTheme
6902
- * @example
6903
- * bw.toggleDarkMode(); // toggle
6904
- * bw.toggleDarkMode(true); // force dark
6905
- * bw.toggleDarkMode(false); // force light
6906
- */
6907
- bw.toggleDarkMode = function(force) {
6908
- const isDark = force !== undefined ? force : !theme.darkMode;
6909
- theme.darkMode = isDark;
6910
-
6911
- if (bw._isBrowser) {
6912
- const root = document.documentElement;
6913
- if (isDark) {
6914
- root.classList.add('bw-dark');
6915
- // Generate palette-aware dark mode CSS, or fall back to default
6916
- var palette = bw._activePalette || derivePalette(DEFAULT_PALETTE_CONFIG);
6917
- var darkRules = generateDarkModeCSS(palette);
6918
- var darkCSS = bw.css(darkRules);
6919
-
6920
- // Remove existing dark styles to allow regeneration
6921
- var existing = document.getElementById('bw-dark-styles');
6922
- if (existing) existing.remove();
6923
-
6924
- var styleEl = document.createElement('style');
6925
- styleEl.id = 'bw-dark-styles';
6926
- styleEl.textContent = darkCSS;
6927
- document.head.appendChild(styleEl);
6928
- } else {
6929
- root.classList.remove('bw-dark');
6930
- // Remove dark mode styles when switching to light
6931
- var darkEl = document.getElementById('bw-dark-styles');
6932
- if (darkEl) darkEl.remove();
6933
- }
6934
- }
6935
-
6936
- return isDark;
6937
- };
6938
-
6939
8129
  /**
6940
8130
  * Generate a complete, scoped theme from seed colors.
6941
8131
  *
@@ -6958,13 +8148,19 @@ bw.toggleDarkMode = function(force) {
6958
8148
  * @param {string} [config.spacing='normal'] - 'compact' | 'normal' | 'spacious'
6959
8149
  * @param {string} [config.radius='md'] - 'none' | 'sm' | 'md' | 'lg' | 'pill'
6960
8150
  * @param {number} [config.fontSize=1.0] - Base font size scale factor
8151
+ * @param {string|number} [config.typeRatio='normal'] - 'tight' | 'normal' | 'relaxed' | 'dramatic' or a number
8152
+ * @param {string} [config.elevation='md'] - 'flat' | 'sm' | 'md' | 'lg'
8153
+ * @param {string} [config.motion='standard'] - 'reduced' | 'standard' | 'expressive'
8154
+ * @param {number} [config.harmonize=0.20] - 0-1, semantic color hue shift toward primary
6961
8155
  * @param {boolean} [config.inject=true] - Inject into DOM (browser only)
6962
- * @returns {Object} { css, palette, name }
8156
+ * @returns {Object} { css, palette, name, isLightPrimary, alternate: { css, palette } }
6963
8157
  * @category CSS & Styling
8158
+ * @see bw.applyTheme
8159
+ * @see bw.toggleTheme
6964
8160
  * @see bw.loadDefaultStyles
6965
8161
  * @example
6966
- * // Generate and inject an ocean theme
6967
- * bw.generateTheme('ocean', {
8162
+ * // Generate and inject an ocean theme (primary + alternate)
8163
+ * var theme = bw.generateTheme('ocean', {
6968
8164
  * primary: '#0077b6',
6969
8165
  * secondary: '#90e0ef',
6970
8166
  * tertiary: '#00b4d8'
@@ -6973,14 +8169,16 @@ bw.toggleDarkMode = function(force) {
6973
8169
  * // Apply to a container
6974
8170
  * document.getElementById('app').classList.add('ocean');
6975
8171
  *
8172
+ * // Toggle to alternate palette
8173
+ * bw.toggleTheme();
8174
+ *
6976
8175
  * // Generate CSS for static export (Node.js)
6977
8176
  * var result = bw.generateTheme('sunset', {
6978
8177
  * primary: '#e76f51',
6979
8178
  * secondary: '#264653',
6980
- * tertiary: '#e9c46a',
6981
8179
  * inject: false
6982
8180
  * });
6983
- * fs.writeFileSync('sunset.css', result.css);
8181
+ * fs.writeFileSync('sunset.css', result.css + result.alternate.css);
6984
8182
  */
6985
8183
  bw.generateTheme = function(name, config) {
6986
8184
  if (!config || !config.primary || !config.secondary) {
@@ -6991,29 +8189,37 @@ bw.generateTheme = function(name, config) {
6991
8189
  var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config);
6992
8190
  if (!config.tertiary) fullConfig.tertiary = fullConfig.primary;
6993
8191
 
6994
- // Derive palette
8192
+ // Derive primary palette
6995
8193
  var palette = derivePalette(fullConfig);
6996
8194
 
6997
- // Store active palette for dark mode
6998
- bw._activePalette = palette;
6999
-
7000
8195
  // Resolve layout
7001
8196
  var layout = resolveLayout(fullConfig);
7002
8197
 
7003
- // Generate themed CSS rules
8198
+ // Generate primary themed CSS rules
7004
8199
  var themedRules = generateThemedCSS(name, palette, layout);
7005
-
7006
- // Add underscore aliases
7007
8200
  var aliasedRules = addUnderscoreAliases(themedRules);
7008
-
7009
- // Convert to CSS string
7010
8201
  var cssStr = bw.css(aliasedRules);
7011
8202
 
7012
- // Inject into DOM if requested and in browser
8203
+ // Derive alternate palette (luminance-inverted)
8204
+ var altConfig = deriveAlternateConfig(fullConfig);
8205
+ var altPalette = derivePalette(altConfig);
8206
+
8207
+ // Generate alternate CSS scoped under .bw-theme-alt
8208
+ var altRules = generateAlternateCSS(name, altPalette, layout);
8209
+ var aliasedAltRules = addUnderscoreAliases(altRules);
8210
+ var altCssStr = bw.css(aliasedAltRules);
8211
+
8212
+ // Determine if primary is light-flavored
8213
+ var lightPrimary = isLightPalette(fullConfig);
8214
+
8215
+ // Inject both CSS sets into DOM if requested
7013
8216
  var shouldInject = config.inject !== false;
7014
8217
  if (shouldInject && bw._isBrowser) {
7015
8218
  var styleId = name ? 'bw-theme-' + name : 'bw-theme-default';
7016
8219
  bw.injectCSS(cssStr, { id: styleId, append: false });
8220
+
8221
+ var altStyleId = name ? 'bw-theme-' + name + '-alt' : 'bw-theme-default-alt';
8222
+ bw.injectCSS(altCssStr, { id: altStyleId, append: false });
7017
8223
  }
7018
8224
 
7019
8225
  // Update bw.u color entries to reflect the palette
@@ -7024,7 +8230,72 @@ bw.generateTheme = function(name, config) {
7024
8230
  bw.u.textWhite = { color: '#ffffff' };
7025
8231
  }
7026
8232
 
7027
- return { css: cssStr, palette: palette, name: name };
8233
+ // Store active theme state
8234
+ var result = {
8235
+ css: cssStr,
8236
+ palette: palette,
8237
+ name: name,
8238
+ isLightPrimary: lightPrimary,
8239
+ alternate: {
8240
+ css: altCssStr,
8241
+ palette: altPalette
8242
+ }
8243
+ };
8244
+ bw._activeTheme = result;
8245
+ bw._activeThemeMode = 'primary';
8246
+
8247
+ return result;
8248
+ };
8249
+
8250
+ /**
8251
+ * Apply a theme mode. Switches between primary and alternate palettes
8252
+ * by adding/removing the `bw-theme-alt` class on `<html>`.
8253
+ *
8254
+ * @param {string} mode - 'primary' | 'alternate' | 'light' | 'dark'
8255
+ * @returns {string} Active mode: 'primary' or 'alternate'
8256
+ * @category CSS & Styling
8257
+ * @see bw.generateTheme
8258
+ * @see bw.toggleTheme
8259
+ * @example
8260
+ * bw.applyTheme('alternate'); // switch to alternate palette
8261
+ * bw.applyTheme('dark'); // switch to whichever palette is darker
8262
+ * bw.applyTheme('primary'); // switch back to primary palette
8263
+ */
8264
+ bw.applyTheme = function(mode) {
8265
+ if (!bw._isBrowser) return mode || 'primary';
8266
+ var root = document.documentElement;
8267
+ var isLight = bw._activeTheme ? bw._activeTheme.isLightPrimary : true;
8268
+
8269
+ var wantAlt;
8270
+ if (mode === 'primary') wantAlt = false;
8271
+ else if (mode === 'alternate') wantAlt = true;
8272
+ else if (mode === 'light') wantAlt = !isLight;
8273
+ else if (mode === 'dark') wantAlt = isLight;
8274
+ else wantAlt = false;
8275
+
8276
+ if (wantAlt) {
8277
+ root.classList.add('bw-theme-alt');
8278
+ } else {
8279
+ root.classList.remove('bw-theme-alt');
8280
+ }
8281
+
8282
+ bw._activeThemeMode = wantAlt ? 'alternate' : 'primary';
8283
+ return bw._activeThemeMode;
8284
+ };
8285
+
8286
+ /**
8287
+ * Toggle between primary and alternate theme palettes.
8288
+ *
8289
+ * @returns {string} Active mode after toggle: 'primary' or 'alternate'
8290
+ * @category CSS & Styling
8291
+ * @see bw.applyTheme
8292
+ * @see bw.generateTheme
8293
+ * @example
8294
+ * bw.toggleTheme(); // flip between primary and alternate
8295
+ */
8296
+ bw.toggleTheme = function() {
8297
+ var current = bw._activeThemeMode || 'primary';
8298
+ return bw.applyTheme(current === 'primary' ? 'alternate' : 'primary');
7028
8299
  };
7029
8300
 
7030
8301
  // Expose color utility functions on bw namespace
@@ -7036,10 +8307,18 @@ bw.relativeLuminance = relativeLuminance;
7036
8307
  bw.textOnColor = textOnColor;
7037
8308
  bw.deriveShades = deriveShades;
7038
8309
  bw.derivePalette = derivePalette;
8310
+ bw.harmonize = harmonize;
8311
+ bw.deriveAlternateSeed = deriveAlternateSeed;
8312
+ bw.deriveAlternateConfig = deriveAlternateConfig;
8313
+ bw.isLightPalette = isLightPalette;
7039
8314
 
7040
8315
  // Expose layout and theme presets
7041
8316
  bw.SPACING_PRESETS = SPACING_PRESETS;
7042
8317
  bw.RADIUS_PRESETS = RADIUS_PRESETS;
8318
+ bw.TYPE_RATIO_PRESETS = TYPE_RATIO_PRESETS;
8319
+ bw.ELEVATION_PRESETS = ELEVATION_PRESETS;
8320
+ bw.MOTION_PRESETS = MOTION_PRESETS;
8321
+ bw.generateTypeScale = generateTypeScale;
7043
8322
  bw.DEFAULT_PALETTE_CONFIG = DEFAULT_PALETTE_CONFIG;
7044
8323
  bw.THEME_PRESETS = THEME_PRESETS;
7045
8324
 
@@ -8081,9 +9360,13 @@ bw.copyToClipboard = function(text) {
8081
9360
  /**
8082
9361
  * Create a sortable TACO table from an array of row objects.
8083
9362
  *
9363
+ * Returns a bare `<table>` TACO — no wrapper, title, or responsive scroll.
9364
+ * Use this when you need full control over table placement, or when embedding
9365
+ * the table inside your own layout. For a ready-to-use table with title,
9366
+ * responsive wrapper, and defaults (striped + hover), use `bw.makeDataTable()`.
9367
+ *
8084
9368
  * Auto-detects columns from data keys if not specified. Supports click-to-sort
8085
- * headers with ascending/descending indicators. Returns a TACO object —
8086
- * render with `bw.DOM()` or `bw.html()`.
9369
+ * headers with ascending/descending indicators.
8087
9370
  *
8088
9371
  * @param {Object} config - Table configuration
8089
9372
  * @param {Array<Object>} config.data - Array of row objects to display
@@ -8383,10 +9666,12 @@ bw.makeBarChart = function(config) {
8383
9666
  };
8384
9667
 
8385
9668
  /**
8386
- * Create a responsive data table with title and optional wrapper
9669
+ * Create a ready-to-use data table with title and responsive wrapper.
8387
9670
  *
8388
- * Wraps bw.makeTable() output in a responsive container div.
8389
- * Adds an optional title heading above the table.
9671
+ * Convenience wrapper around `bw.makeTable()` that adds a title heading,
9672
+ * responsive horizontal scroll container, and defaults to striped + hover.
9673
+ * Use this for the common case; use `bw.makeTable()` when you need a bare
9674
+ * table element with no wrapper.
8390
9675
  *
8391
9676
  * @param {Object} config - Table configuration
8392
9677
  * @param {string} [config.title] - Table title heading