bitwrench 2.0.12 → 2.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- /*! bitwrench v2.0.12 | 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
  'use strict';
3
3
 
4
4
  /**
@@ -7,14 +7,14 @@
7
7
  */
8
8
 
9
9
  const VERSION_INFO = {
10
- version: '2.0.12',
10
+ version: '2.0.14',
11
11
  name: 'bitwrench',
12
12
  description: 'A library for javascript UI functions.',
13
13
  license: 'BSD-2-Clause',
14
14
  homepage: 'https://deftio.github.com/bitwrench/pages',
15
15
  repository: 'git+https://github.com/deftio/bitwrench.git',
16
16
  author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
17
- buildDate: '2026-03-07T22:31:35.755Z'
17
+ buildDate: '2026-03-08T08:04:06.572Z'
18
18
  };
19
19
 
20
20
  /**
@@ -272,6 +272,29 @@ function textOnColor(hex) {
272
272
  return relativeLuminance(hex) > 0.179 ? '#000' : '#fff';
273
273
  }
274
274
 
275
+ /**
276
+ * Shift a color's hue toward a target hue by a given amount.
277
+ * Uses shortest-arc interpolation on the hue wheel.
278
+ * @param {string} sourceHex - Color to shift
279
+ * @param {string} targetHex - Color whose hue to shift toward
280
+ * @param {number} [amount=0.20] - 0 = no shift, 1 = full shift to target hue
281
+ * @returns {string} Harmonized hex color
282
+ */
283
+ function harmonize(sourceHex, targetHex, amount) {
284
+ if (amount === undefined) amount = 0.20;
285
+ if (amount === 0) return sourceHex;
286
+ var srcHsl = hexToHsl(sourceHex);
287
+ var tgtHsl = hexToHsl(targetHex);
288
+
289
+ // Shortest-arc hue interpolation
290
+ var diff = tgtHsl[0] - srcHsl[0];
291
+ if (diff > 180) diff -= 360;
292
+ if (diff < -180) diff += 360;
293
+
294
+ var newHue = (srcHsl[0] + diff * amount + 360) % 360;
295
+ return hslToHex([newHue, srcHsl[1], srcHsl[2]]);
296
+ }
297
+
275
298
  /**
276
299
  * Derive a full shade palette for a single semantic color.
277
300
  * @param {string} hex - Base color hex
@@ -291,31 +314,128 @@ function deriveShades(hex) {
291
314
  };
292
315
  }
293
316
 
317
+ /**
318
+ * Derive the alternate (luminance-inverted) version of a single seed color.
319
+ * Preserves hue, mirrors lightness, adjusts saturation for readability.
320
+ * @param {string} hex - Seed hex color
321
+ * @returns {string} Alternate hex color
322
+ */
323
+ function deriveAlternateSeed(hex) {
324
+ var hsl = hexToHsl(hex);
325
+ var h = hsl[0], s = hsl[1], l = hsl[2];
326
+ var altL, altS;
327
+
328
+ if (l > 50) {
329
+ // Light color → make dark. Map 50-100 → 30-10 range
330
+ altL = clip(100 - l - 10, 8, 40);
331
+ // Reduce saturation slightly — vivid colors at low lightness look garish
332
+ altS = clip(s * 0.85, 0, 100);
333
+ } else {
334
+ // Dark color → make light. Map 0-50 → 65-92 range
335
+ altL = clip(100 - l + 10, 60, 92);
336
+ // Slightly increase saturation for light variant
337
+ altS = clip(s * 1.1, 0, 100);
338
+ }
339
+
340
+ return hslToHex([h, altS, altL]);
341
+ }
342
+
343
+ /**
344
+ * Determine whether a palette config is "light-flavored" based on
345
+ * the average luminance of its seed colors.
346
+ * @param {Object} config - Theme config with primary, secondary hex colors
347
+ * @returns {boolean} true if the seeds are predominantly light
348
+ */
349
+ function isLightPalette(config) {
350
+ var lum = relativeLuminance(config.primary);
351
+ if (config.secondary) lum = (lum + relativeLuminance(config.secondary)) / 2;
352
+ if (config.tertiary) lum = (lum * 2 + relativeLuminance(config.tertiary)) / 3;
353
+ return lum > 0.179;
354
+ }
355
+
356
+ /**
357
+ * Derive a complete alternate config from a primary theme config.
358
+ * Each seed color is luminance-inverted; semantic colors are adjusted for
359
+ * the new luminance context.
360
+ * @param {Object} config - Primary theme config
361
+ * @returns {Object} Alternate theme config (same shape, inverted lightness)
362
+ */
363
+ function deriveAlternateConfig(config) {
364
+ var alt = {};
365
+ // Invert the user's seed colors
366
+ alt.primary = deriveAlternateSeed(config.primary);
367
+ alt.secondary = deriveAlternateSeed(config.secondary);
368
+ alt.tertiary = config.tertiary ? deriveAlternateSeed(config.tertiary) : alt.primary;
369
+
370
+ // Derive alternate surface colors from primary hue
371
+ var priHsl = hexToHsl(config.primary);
372
+ var h = priHsl[0];
373
+ var isLight = isLightPalette(config);
374
+
375
+ if (isLight) {
376
+ // Primary is light → alternate needs dark surfaces
377
+ alt.light = hslToHex([h, Math.min(priHsl[1], 15), 15]);
378
+ alt.dark = hslToHex([h, 5, 88]);
379
+ } else {
380
+ // Primary is dark → alternate needs light surfaces
381
+ alt.light = hslToHex([h, Math.min(priHsl[1], 10), 96]);
382
+ alt.dark = hslToHex([h, 10, 18]);
383
+ }
384
+
385
+ // Semantic colors: harmonize toward primary, then invert for alternate
386
+ var amt = config.harmonize !== undefined ? config.harmonize : 0.20;
387
+ var semanticDefaults = {
388
+ success: '#198754', danger: '#dc3545',
389
+ warning: '#f0ad4e', info: '#17a2b8'
390
+ };
391
+ var semantics = ['success', 'danger', 'warning', 'info'];
392
+ for (var i = 0; i < semantics.length; i++) {
393
+ var key = semantics[i];
394
+ var seed = config[key] || semanticDefaults[key];
395
+ var harmonized = harmonize(seed, config.primary, amt);
396
+ alt[key] = deriveAlternateSeed(harmonized);
397
+ }
398
+
399
+ // Semantic colors are already harmonized+inverted — don't re-harmonize in derivePalette
400
+ alt.harmonize = 0;
401
+
402
+ return alt;
403
+ }
404
+
294
405
  /**
295
406
  * Derive complete palette from a theme config object.
407
+ * Semantic colors are harmonized toward the primary hue (configurable).
408
+ * Light/dark surface colors are tinted with the primary hue.
296
409
  * @param {Object} config - Theme config with primary, secondary, tertiary, etc.
297
- * @returns {Object} Full palette with shades for all 8 semantic colors + tertiary
410
+ * @param {number} [config.harmonize=0.20] - Hue shift amount for semantic colors (0-1)
411
+ * @returns {Object} Full palette with shades for all 9 semantic colors
298
412
  */
299
413
  function derivePalette(config) {
300
- var defaults = {
301
- success: '#198754',
302
- danger: '#dc3545',
303
- warning: '#ffc107',
304
- info: '#0dcaf0',
305
- light: '#f8f9fa',
306
- dark: '#212529'
307
- };
414
+ var amt = config.harmonize !== undefined ? config.harmonize : 0.20;
415
+ var pri = config.primary;
416
+ var priHsl = hexToHsl(pri);
417
+ var h = priHsl[0];
418
+
419
+ // Semantic defaults — harmonized toward primary hue
420
+ var successBase = harmonize(config.success || '#198754', pri, amt);
421
+ var dangerBase = harmonize(config.danger || '#dc3545', pri, amt);
422
+ var warningBase = harmonize(config.warning || '#f0ad4e', pri, amt);
423
+ var infoBase = harmonize(config.info || '#17a2b8', pri, amt);
424
+
425
+ // Light/dark: derive from primary hue with low saturation (if not user-supplied)
426
+ var lightBase = config.light || hslToHex([h, 8, 97]);
427
+ var darkBase = config.dark || hslToHex([h, 10, 13]);
308
428
 
309
429
  var palette = {
310
- primary: deriveShades(config.primary),
430
+ primary: deriveShades(config.primary),
311
431
  secondary: deriveShades(config.secondary),
312
- tertiary: deriveShades(config.tertiary),
313
- success: deriveShades(config.success || defaults.success),
314
- danger: deriveShades(config.danger || defaults.danger),
315
- warning: deriveShades(config.warning || defaults.warning),
316
- info: deriveShades(config.info || defaults.info),
317
- light: deriveShades(config.light || defaults.light),
318
- dark: deriveShades(config.dark || defaults.dark)
432
+ tertiary: deriveShades(config.tertiary),
433
+ success: deriveShades(successBase),
434
+ danger: deriveShades(dangerBase),
435
+ warning: deriveShades(warningBase),
436
+ info: deriveShades(infoBase),
437
+ light: deriveShades(lightBase),
438
+ dark: deriveShades(darkBase)
319
439
  };
320
440
 
321
441
  return palette;
@@ -364,6 +484,88 @@ var RADIUS_PRESETS = {
364
484
  pill: { btn: '50rem', card: '1rem', badge: '50rem', alert: '1rem', input: '50rem' }
365
485
  };
366
486
 
487
+ // ---- Typography scale presets ----
488
+
489
+ var TYPE_RATIO_PRESETS = {
490
+ tight: 1.125,
491
+ normal: 1.200,
492
+ relaxed: 1.250,
493
+ dramatic: 1.333
494
+ };
495
+
496
+ /**
497
+ * Generate a modular type scale from a base size and ratio.
498
+ * @param {number} base - Base font size in px (default 16)
499
+ * @param {number} ratio - Scale ratio (default 1.200)
500
+ * @returns {Object} { xs, sm, base, lg, xl, '2xl', '3xl', '4xl' } in px
501
+ */
502
+ function generateTypeScale(base, ratio) {
503
+ if (!base) base = 16;
504
+ if (!ratio) ratio = 1.200;
505
+ return {
506
+ xs: Math.round(base / (ratio * ratio)),
507
+ sm: Math.round(base / ratio),
508
+ base: base,
509
+ lg: Math.round(base * ratio),
510
+ xl: Math.round(base * ratio * ratio),
511
+ '2xl': Math.round(base * Math.pow(ratio, 3)),
512
+ '3xl': Math.round(base * Math.pow(ratio, 4)),
513
+ '4xl': Math.round(base * Math.pow(ratio, 5))
514
+ };
515
+ }
516
+
517
+ // ---- Elevation (shadow depth) presets ----
518
+
519
+ var ELEVATION_PRESETS = {
520
+ flat: {
521
+ sm: 'none',
522
+ md: 'none',
523
+ lg: 'none',
524
+ xl: 'none'
525
+ },
526
+ sm: {
527
+ sm: '0 1px 2px rgba(0,0,0,0.05)',
528
+ md: '0 1px 3px rgba(0,0,0,0.08)',
529
+ lg: '0 2px 6px rgba(0,0,0,0.10)',
530
+ xl: '0 4px 12px rgba(0,0,0,0.12)'
531
+ },
532
+ md: {
533
+ sm: '0 1px 3px rgba(0,0,0,0.08)',
534
+ md: '0 2px 6px rgba(0,0,0,0.12)',
535
+ lg: '0 4px 12px rgba(0,0,0,0.16)',
536
+ xl: '0 8px 24px rgba(0,0,0,0.20)'
537
+ },
538
+ lg: {
539
+ sm: '0 2px 4px rgba(0,0,0,0.10)',
540
+ md: '0 4px 12px rgba(0,0,0,0.16)',
541
+ lg: '0 8px 24px rgba(0,0,0,0.22)',
542
+ xl: '0 16px 48px rgba(0,0,0,0.28)'
543
+ }
544
+ };
545
+
546
+ // ---- Motion (transition) presets ----
547
+
548
+ var MOTION_PRESETS = {
549
+ reduced: {
550
+ fast: '0ms',
551
+ normal: '0ms',
552
+ slow: '0ms',
553
+ easing: 'linear'
554
+ },
555
+ standard: {
556
+ fast: '100ms',
557
+ normal: '200ms',
558
+ slow: '300ms',
559
+ easing: 'ease-out'
560
+ },
561
+ expressive: {
562
+ fast: '150ms',
563
+ normal: '300ms',
564
+ slow: '500ms',
565
+ easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)'
566
+ }
567
+ };
568
+
367
569
  /**
368
570
  * Default palette config — matches existing hardcoded colors
369
571
  */
@@ -373,8 +575,8 @@ var DEFAULT_PALETTE_CONFIG = {
373
575
  tertiary: '#006666',
374
576
  success: '#198754',
375
577
  danger: '#dc3545',
376
- warning: '#ffc107',
377
- info: '#0dcaf0',
578
+ warning: '#b38600',
579
+ info: '#0891b2',
378
580
  light: '#f8f9fa',
379
581
  dark: '#212529'
380
582
  };
@@ -399,18 +601,32 @@ var THEME_PRESETS = {
399
601
  };
400
602
 
401
603
  /**
402
- * Resolve layout config to spacing + radius objects
403
- * @param {Object} config - { spacing, radius, fontSize }
404
- * @returns {Object} { spacing, radius, fontSize }
604
+ * Resolve layout config to spacing, radius, typeScale, elevation, and motion objects.
605
+ * @param {Object} config - { spacing, radius, fontSize, typeRatio, elevation, motion }
606
+ * @returns {Object} { spacing, radius, fontSize, typeScale, elevation, motion }
405
607
  */
406
608
  function resolveLayout(config) {
407
609
  var sp = (config && config.spacing) || 'normal';
408
610
  var rd = (config && config.radius) || 'md';
409
611
  var fs = (config && config.fontSize) || 1.0;
612
+
613
+ // typeRatio: accept preset name or number
614
+ var tr = (config && config.typeRatio) || 'normal';
615
+ var ratioNum = typeof tr === 'string' ? (TYPE_RATIO_PRESETS[tr] || TYPE_RATIO_PRESETS.normal) : tr;
616
+
617
+ // elevation: accept preset name or object
618
+ var el = (config && config.elevation) || 'md';
619
+
620
+ // motion: accept preset name or object
621
+ var mo = (config && config.motion) || 'standard';
622
+
410
623
  return {
411
624
  spacing: typeof sp === 'string' ? (SPACING_PRESETS[sp] || SPACING_PRESETS.normal) : sp,
412
625
  radius: typeof rd === 'string' ? (RADIUS_PRESETS[rd] || RADIUS_PRESETS.md) : rd,
413
- fontSize: fs
626
+ fontSize: fs,
627
+ typeScale: generateTypeScale(16, ratioNum),
628
+ elevation: typeof el === 'string' ? (ELEVATION_PRESETS[el] || ELEVATION_PRESETS.md) : el,
629
+ motion: typeof mo === 'string' ? (MOTION_PRESETS[mo] || MOTION_PRESETS.standard) : mo
414
630
  };
415
631
  }
416
632
 
@@ -434,12 +650,13 @@ function scopeSelector(name, sel) {
434
650
  // Themed CSS generators
435
651
  // =========================================================================
436
652
 
437
- function generateTypographyThemed(scope, palette) {
653
+ function generateTypographyThemed(scope, palette, layout) {
654
+ var mot = layout.motion;
438
655
  var rules = {};
439
656
  rules[scopeSelector(scope, 'a')] = {
440
657
  'color': palette.primary.base,
441
658
  'text-decoration': 'none',
442
- 'transition': 'color 0.15s'
659
+ 'transition': 'color ' + mot.fast + ' ' + mot.easing
443
660
  };
444
661
  rules[scopeSelector(scope, 'a:hover')] = {
445
662
  'color': palette.primary.hover,
@@ -459,7 +676,8 @@ function generateButtons(scope, palette, layout) {
459
676
  'border-radius': rd.btn
460
677
  };
461
678
  rules[scopeSelector(scope, '.bw-btn:focus-visible')] = {
462
- 'outline': '0',
679
+ 'outline': '2px solid currentColor',
680
+ 'outline-offset': '2px',
463
681
  'box-shadow': '0 0 0 3px ' + palette.primary.focus
464
682
  };
465
683
 
@@ -549,14 +767,15 @@ function generateCards(scope, palette, layout) {
549
767
  var sp = layout.spacing;
550
768
  var rd = layout.radius;
551
769
 
770
+ var elev = layout.elevation;
552
771
  rules[scopeSelector(scope, '.bw-card')] = {
553
772
  'background-color': '#fff',
554
773
  'border': '1px solid ' + palette.light.border,
555
774
  'border-radius': rd.card,
556
- 'box-shadow': '0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04)'
775
+ 'box-shadow': elev.sm
557
776
  };
558
777
  rules[scopeSelector(scope, '.bw-card:hover')] = {
559
- 'box-shadow': '0 4px 12px rgba(0,0,0,.1), 0 2px 4px rgba(0,0,0,.06)'
778
+ 'box-shadow': elev.md
560
779
  };
561
780
  rules[scopeSelector(scope, '.bw-card-body')] = {
562
781
  'padding': sp.card
@@ -603,6 +822,8 @@ function generateForms(scope, palette, layout) {
603
822
  };
604
823
  rules[scopeSelector(scope, '.bw-form-control:focus')] = {
605
824
  'border-color': palette.primary.border,
825
+ 'outline': '2px solid ' + palette.primary.base,
826
+ 'outline-offset': '-1px',
606
827
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
607
828
  };
608
829
  rules[scopeSelector(scope, '.bw-form-control::placeholder')] = {
@@ -760,7 +981,8 @@ function generatePagination(scope, palette) {
760
981
  'border-color': palette.light.border
761
982
  };
762
983
  rules[scopeSelector(scope, '.bw-page-link:focus')] = {
763
- 'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
984
+ 'outline': '2px solid ' + palette.primary.base,
985
+ 'outline-offset': '-2px'
764
986
  };
765
987
  rules[scopeSelector(scope, '.bw-page-item.bw-active .bw-page-link')] = {
766
988
  'color': palette.primary.textOn,
@@ -919,12 +1141,12 @@ function generateCarouselThemed(scope, palette) {
919
1141
  return rules;
920
1142
  }
921
1143
 
922
- function generateModalThemed(scope, palette) {
1144
+ function generateModalThemed(scope, palette, layout) {
923
1145
  var rules = {};
924
1146
  rules[scopeSelector(scope, '.bw-modal-content')] = {
925
1147
  'background-color': '#fff',
926
1148
  'border-color': palette.light.border,
927
- 'box-shadow': '0 0.5rem 1rem rgba(0,0,0,0.15)'
1149
+ 'box-shadow': layout.elevation.lg
928
1150
  };
929
1151
  rules[scopeSelector(scope, '.bw-modal-header')] = {
930
1152
  'border-bottom-color': palette.light.border
@@ -938,12 +1160,12 @@ function generateModalThemed(scope, palette) {
938
1160
  return rules;
939
1161
  }
940
1162
 
941
- function generateToastThemed(scope, palette) {
1163
+ function generateToastThemed(scope, palette, layout) {
942
1164
  var rules = {};
943
1165
  rules[scopeSelector(scope, '.bw-toast')] = {
944
1166
  'background-color': '#fff',
945
1167
  'border-color': 'rgba(0,0,0,0.1)',
946
- 'box-shadow': '0 0.5rem 1rem rgba(0,0,0,0.15)'
1168
+ 'box-shadow': layout.elevation.lg
947
1169
  };
948
1170
  rules[scopeSelector(scope, '.bw-toast-header')] = {
949
1171
  'border-bottom-color': 'rgba(0,0,0,0.05)'
@@ -957,12 +1179,12 @@ function generateToastThemed(scope, palette) {
957
1179
  return rules;
958
1180
  }
959
1181
 
960
- function generateDropdownThemed(scope, palette) {
1182
+ function generateDropdownThemed(scope, palette, layout) {
961
1183
  var rules = {};
962
1184
  rules[scopeSelector(scope, '.bw-dropdown-menu')] = {
963
1185
  'background-color': '#fff',
964
1186
  'border-color': palette.light.border,
965
- 'box-shadow': '0 0.5rem 1rem rgba(0,0,0,0.15)'
1187
+ 'box-shadow': layout.elevation.md
966
1188
  };
967
1189
  rules[scopeSelector(scope, '.bw-dropdown-item')] = {
968
1190
  'color': palette.dark.base
@@ -1028,7 +1250,7 @@ function generateAvatarThemed(scope, palette) {
1028
1250
  function generateThemedCSS(scopeName, palette, layout) {
1029
1251
  return Object.assign({},
1030
1252
  generateResetThemed(scopeName, palette),
1031
- generateTypographyThemed(scopeName, palette),
1253
+ generateTypographyThemed(scopeName, palette, layout),
1032
1254
  generateButtons(scopeName, palette, layout),
1033
1255
  generateAlerts(scopeName, palette, layout),
1034
1256
  generateBadges(scopeName, palette),
@@ -1047,9 +1269,9 @@ function generateThemedCSS(scopeName, palette, layout) {
1047
1269
  generateSectionsThemed(scopeName, palette),
1048
1270
  generateAccordionThemed(scopeName, palette),
1049
1271
  generateCarouselThemed(scopeName, palette),
1050
- generateModalThemed(scopeName, palette),
1051
- generateToastThemed(scopeName, palette),
1052
- generateDropdownThemed(scopeName, palette),
1272
+ generateModalThemed(scopeName, palette, layout),
1273
+ generateToastThemed(scopeName, palette, layout),
1274
+ generateDropdownThemed(scopeName, palette, layout),
1053
1275
  generateSwitchThemed(scopeName, palette),
1054
1276
  generateSkeletonThemed(scopeName, palette),
1055
1277
  generateAvatarThemed(scopeName, palette),
@@ -1202,11 +1424,23 @@ const defaultStyles = {
1202
1424
  // =========================================================================
1203
1425
 
1204
1426
  /**
1205
- * Structural styles contain only layout, sizing, spacing, and behavior
1206
- * properties. No colors, backgrounds, shadows, or border-colors.
1207
- * These never change with themes.
1427
+ * Structural styles layout, sizing, spacing, positioning, and behavior.
1208
1428
  *
1209
- * @returns {Object} CSS rules object
1429
+ * POLICY: No colors, backgrounds, shadows, or border-colors in this function.
1430
+ * All cosmetic values belong in `defaultStyles.*` sections (unthemed defaults)
1431
+ * or in `generateThemedCSS()` (theme-driven colors).
1432
+ *
1433
+ * Exception: `.bw-progress-bar-striped` uses rgba(255,255,255,.15) for the
1434
+ * stripe pattern overlay. This is theme-neutral — a semi-transparent white
1435
+ * gradient that creates visible stripes on any background color.
1436
+ *
1437
+ * Architecture:
1438
+ * getStructuralStyles() → layout-only rules (never change with themes)
1439
+ * defaultStyles.* → cosmetic defaults (colors, shadows, borders)
1440
+ * generateThemedCSS() → palette-driven cosmetics from seed colors
1441
+ * generateAlternateCSS() → alternate palette (luminance-inverted)
1442
+ *
1443
+ * @returns {Object} CSS rules object (layout-only, theme-independent)
1210
1444
  */
1211
1445
  function getStructuralStyles() {
1212
1446
  var rules = {};
@@ -1257,12 +1491,12 @@ function getStructuralStyles() {
1257
1491
  'text-decoration': 'none', 'vertical-align': 'middle', 'cursor': 'pointer',
1258
1492
  'user-select': 'none', 'border': '1px solid transparent',
1259
1493
  'padding': '0.5rem 1.125rem', 'font-size': '0.875rem', 'font-family': 'inherit',
1260
- 'border-radius': '6px', 'transition': 'all 0.15s cubic-bezier(0.4, 0, 0.2, 1)',
1494
+ 'border-radius': '6px', 'transition': 'all 0.15s ease-out',
1261
1495
  'gap': '0.5rem'
1262
1496
  };
1263
1497
  rules['.bw-btn:hover'] = { 'text-decoration': 'none', 'transform': 'translateY(-1px)' };
1264
1498
  rules['.bw-btn:active'] = { 'transform': 'translateY(0)' };
1265
- rules['.bw-btn:focus-visible'] = { 'outline': '0' };
1499
+ rules['.bw-btn:focus-visible'] = { 'outline': '2px solid currentColor', 'outline-offset': '2px' };
1266
1500
  rules['.bw-btn:disabled'] = { 'opacity': '0.5', 'cursor': 'not-allowed', 'pointer-events': 'none' };
1267
1501
  rules['.bw-btn-lg'] = { 'padding': '0.625rem 1.5rem', 'font-size': '1rem', 'border-radius': '8px' };
1268
1502
  rules['.bw-btn-sm'] = { 'padding': '0.25rem 0.75rem', 'font-size': '0.8125rem', 'border-radius': '5px' };
@@ -1272,7 +1506,7 @@ function getStructuralStyles() {
1272
1506
  'position': 'relative', 'display': 'flex', 'flex-direction': 'column',
1273
1507
  'min-width': '0', 'height': '100%', 'word-wrap': 'break-word',
1274
1508
  'background-clip': 'border-box', 'border': '1px solid transparent',
1275
- '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)',
1509
+ 'border-radius': '8px', 'transition': 'box-shadow 0.2s ease-out, transform 0.2s ease-out',
1276
1510
  'margin-bottom': '1.5rem', 'overflow': 'hidden'
1277
1511
  };
1278
1512
  rules['.bw-card-body'] = { 'flex': '1 1 auto', 'padding': '1.25rem 1.5rem' };
@@ -1281,7 +1515,7 @@ function getStructuralStyles() {
1281
1515
  rules['.bw-card-text'] = { 'margin-bottom': '0', 'font-size': '0.9375rem', 'line-height': '1.6' };
1282
1516
  rules['.bw-card-header'] = { 'padding': '0.875rem 1.5rem', 'margin-bottom': '0', 'font-weight': '600', 'font-size': '0.875rem' };
1283
1517
  rules['.bw-card-footer'] = { 'padding': '0.75rem 1.5rem', 'font-size': '0.875rem' };
1284
- rules['.bw-card-hoverable'] = { 'transition': 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)' };
1518
+ rules['.bw-card-hoverable'] = { 'transition': 'all 0.3s ease-out' };
1285
1519
  rules['.bw-card-img-top'] = { 'width': '100%', 'border-top-left-radius': '7px', 'border-top-right-radius': '7px' };
1286
1520
  rules['.bw-card-img-bottom'] = { 'width': '100%', 'border-bottom-left-radius': '7px', 'border-bottom-right-radius': '7px' };
1287
1521
  rules['.bw-card-img-left'] = { 'width': '40%', 'object-fit': 'cover' };
@@ -1294,10 +1528,10 @@ function getStructuralStyles() {
1294
1528
  'font-size': '0.9375rem', 'font-weight': '400', 'line-height': '1.5',
1295
1529
  'background-clip': 'padding-box', 'appearance': 'none',
1296
1530
  'border': '1px solid transparent', 'border-radius': '6px',
1297
- 'transition': 'border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out',
1531
+ 'transition': 'border-color 0.15s ease-out, box-shadow 0.15s ease-out',
1298
1532
  'font-family': 'inherit'
1299
1533
  };
1300
- rules['.bw-form-control:focus'] = { 'outline': '0' };
1534
+ rules['.bw-form-control:focus'] = { 'outline': '2px solid currentColor', 'outline-offset': '-1px' };
1301
1535
  rules['.bw-form-control::placeholder'] = { 'opacity': '1' };
1302
1536
  rules['.bw-form-label'] = { 'display': 'block', 'margin-bottom': '0.375rem', 'font-size': '0.875rem', 'font-weight': '600' };
1303
1537
  rules['.bw-form-group'] = { 'margin-bottom': '1.25rem' };
@@ -1309,6 +1543,10 @@ function getStructuralStyles() {
1309
1543
  };
1310
1544
  rules['textarea.bw-form-control'] = { 'min-height': '5rem', 'resize': 'vertical' };
1311
1545
 
1546
+ // Form validation (structural)
1547
+ rules['.bw-valid-feedback'] = { 'display': 'block', 'font-size': '0.875rem', 'margin-top': '0.25rem' };
1548
+ rules['.bw-invalid-feedback'] = { 'display': 'block', 'font-size': '0.875rem', 'margin-top': '0.25rem' };
1549
+
1312
1550
  // Form checks (structural)
1313
1551
  Object.assign(rules, {
1314
1552
  '.bw-form-check': { 'display': 'flex', 'align-items': 'center', 'gap': '0.5rem', 'min-height': '1.5rem', 'margin-bottom': '0.25rem' },
@@ -1367,13 +1605,13 @@ function getStructuralStyles() {
1367
1605
 
1368
1606
  // Badges (structural)
1369
1607
  rules['.bw-badge'] = {
1370
- 'display': 'inline-block', 'padding': '.4em .75em', 'font-size': '.875em',
1608
+ 'display': 'inline-block', 'padding': '0.375rem 0.625rem', 'font-size': '0.875rem',
1371
1609
  'font-weight': '600', 'line-height': '1.3', 'text-align': 'center',
1372
1610
  'white-space': 'nowrap', 'vertical-align': 'baseline', 'border-radius': '.375rem'
1373
1611
  };
1374
1612
  rules['.bw-badge:empty'] = { 'display': 'none' };
1375
- rules['.bw-badge-sm'] = { 'font-size': '.75em', 'padding': '.25em .5em' };
1376
- rules['.bw-badge-lg'] = { 'font-size': '1em', 'padding': '.5em .9em' };
1613
+ rules['.bw-badge-sm'] = { 'font-size': '0.75rem', 'padding': '0.25rem 0.5rem' };
1614
+ rules['.bw-badge-lg'] = { 'font-size': '1rem', 'padding': '0.5rem 0.875rem' };
1377
1615
  rules['.bw-badge-pill'] = { 'border-radius': '50rem' };
1378
1616
 
1379
1617
  // Progress (structural)
@@ -1381,7 +1619,7 @@ function getStructuralStyles() {
1381
1619
  rules['.bw-progress-bar'] = {
1382
1620
  'display': 'flex', 'flex-direction': 'column', 'justify-content': 'center',
1383
1621
  'overflow': 'hidden', 'text-align': 'center', 'white-space': 'nowrap',
1384
- 'transition': 'width .6s ease', 'font-weight': '600'
1622
+ 'transition': 'width 0.3s ease-out', 'font-weight': '600'
1385
1623
  };
1386
1624
  rules['.bw-progress-bar-striped'] = {
1387
1625
  '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)',
@@ -1398,7 +1636,7 @@ function getStructuralStyles() {
1398
1636
  'display': 'block', 'padding': '0.625rem 1rem', 'font-size': '0.875rem',
1399
1637
  'font-weight': '500', 'text-decoration': 'none', 'cursor': 'pointer',
1400
1638
  'border': 'none', 'background': 'transparent',
1401
- 'transition': 'color 0.15s, border-color 0.15s', 'font-family': 'inherit'
1639
+ 'transition': 'color 0.15s ease-out, background-color 0.15s ease-out, border-color 0.15s ease-out', 'font-family': 'inherit'
1402
1640
  };
1403
1641
  rules['.bw-nav-tabs .bw-nav-link'] = { 'border': 'none', 'border-bottom': '2px solid transparent', 'border-radius': '0', 'background-color': 'transparent' };
1404
1642
  rules['.bw-nav-pills .bw-nav-link'] = { 'border-radius': '6px' };
@@ -1416,7 +1654,8 @@ function getStructuralStyles() {
1416
1654
  rules['.bw-list-group-item:last-child'] = { 'border-bottom-right-radius': 'inherit', 'border-bottom-left-radius': 'inherit' };
1417
1655
  rules['.bw-list-group-item + .bw-list-group-item'] = { 'border-top-width': '0' };
1418
1656
  rules['.bw-list-group-item.disabled'] = { 'pointer-events': 'none' };
1419
- rules['a.bw-list-group-item'] = { 'cursor': 'pointer' };
1657
+ rules['a.bw-list-group-item'] = { 'cursor': 'pointer', 'transition': 'background-color 0.15s ease-out, color 0.15s ease-out' };
1658
+ rules['a.bw-list-group-item:focus-visible, .bw-list-group-item:focus-visible'] = { 'z-index': '2', 'outline': '2px solid currentColor', 'outline-offset': '-2px' };
1420
1659
  rules['.bw-list-group-flush'] = { 'border-radius': '0' };
1421
1660
  rules['.bw-list-group-flush > .bw-list-group-item'] = { 'border-width': '0 0 1px', 'border-radius': '0' };
1422
1661
  rules['.bw-list-group-flush > .bw-list-group-item:last-child'] = { 'border-bottom-width': '0' };
@@ -1427,16 +1666,19 @@ function getStructuralStyles() {
1427
1666
  rules['.bw-page-link'] = {
1428
1667
  'position': 'relative', 'display': 'block', 'padding': '0.375rem 0.75rem',
1429
1668
  'margin-left': '-1px', 'line-height': '1.25', 'text-decoration': 'none',
1430
- 'transition': 'color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out'
1669
+ 'transition': 'color 0.15s ease-out, background-color 0.15s ease-out, border-color 0.15s ease-out'
1431
1670
  };
1432
1671
  rules['.bw-page-item:first-child .bw-page-link'] = { 'margin-left': '0', 'border-top-left-radius': '0.375rem', 'border-bottom-left-radius': '0.375rem' };
1433
1672
  rules['.bw-page-item:last-child .bw-page-link'] = { 'border-top-right-radius': '0.375rem', 'border-bottom-right-radius': '0.375rem' };
1673
+ rules['.bw-page-link:focus-visible'] = { 'z-index': '3', 'outline': '2px solid currentColor', 'outline-offset': '-2px' };
1434
1674
 
1435
1675
  // Breadcrumb (structural)
1436
1676
  rules['.bw-breadcrumb'] = { 'display': 'flex', 'flex-wrap': 'wrap', 'padding': '0 0', 'margin-bottom': '1rem', 'list-style': 'none' };
1437
1677
  rules['.bw-breadcrumb-item'] = { 'display': 'flex' };
1438
1678
  rules['.bw-breadcrumb-item + .bw-breadcrumb-item'] = { 'padding-left': '0.5rem' };
1439
1679
  rules['.bw-breadcrumb-item + .bw-breadcrumb-item::before'] = { 'float': 'left', 'padding-right': '0.5rem', 'content': '"/"' };
1680
+ rules['.bw-breadcrumb-item a'] = { 'text-decoration': 'none', 'transition': 'color 0.15s ease-out' };
1681
+ rules['.bw-breadcrumb-item.active'] = { 'font-weight': '500' };
1440
1682
 
1441
1683
  // Hero (structural)
1442
1684
  rules['.bw-hero'] = { 'position': 'relative', 'overflow': 'hidden' };
@@ -1539,12 +1781,12 @@ function getStructuralStyles() {
1539
1781
  'position': 'relative', 'display': 'flex', 'align-items': 'center', 'width': '100%',
1540
1782
  'padding': '1rem 1.25rem', 'font-size': '1rem', 'font-weight': '500', 'text-align': 'left',
1541
1783
  'background-color': 'transparent', 'border': '0', 'overflow-anchor': 'none', 'cursor': 'pointer',
1542
- 'font-family': 'inherit', 'transition': 'color 0.15s ease-in-out, background-color 0.15s ease-in-out'
1784
+ 'font-family': 'inherit', 'transition': 'color 0.15s ease-out, background-color 0.15s ease-out'
1543
1785
  };
1544
1786
  rules['.bw-accordion-button::after'] = {
1545
1787
  'flex-shrink': '0', 'width': '1.25rem', 'height': '1.25rem', 'margin-left': 'auto',
1546
1788
  'content': '""', 'background-repeat': 'no-repeat', 'background-size': '1.25rem',
1547
- 'transition': 'transform 0.2s ease-in-out'
1789
+ 'transition': 'transform 0.2s ease-out'
1548
1790
  };
1549
1791
  rules['.bw-accordion-button:not(.bw-collapsed)::after'] = { 'transform': 'rotate(-180deg)' };
1550
1792
  rules['.bw-accordion-collapse'] = { 'max-height': '0', 'overflow': 'hidden', 'transition': 'max-height 0.3s ease' };
@@ -1553,10 +1795,13 @@ function getStructuralStyles() {
1553
1795
 
1554
1796
  // Modal (structural)
1555
1797
  rules['.bw-modal'] = {
1556
- 'display': 'none', 'position': 'fixed', 'top': '0', 'left': '0', 'width': '100%', 'height': '100%',
1557
- 'z-index': '1050', 'overflow-x': 'hidden', 'overflow-y': 'auto', 'opacity': '0', 'transition': 'opacity 0.15s linear'
1798
+ 'display': 'flex', 'align-items': 'center', 'justify-content': 'center',
1799
+ 'position': 'fixed', 'top': '0', 'left': '0', 'width': '100%', 'height': '100%',
1800
+ 'z-index': '1050', 'overflow-x': 'hidden', 'overflow-y': 'auto',
1801
+ 'opacity': '0', 'visibility': 'hidden', 'pointer-events': 'none',
1802
+ 'transition': 'opacity 0.2s ease, visibility 0.2s ease'
1558
1803
  };
1559
- rules['.bw-modal.bw-modal-show'] = { 'display': 'flex', 'align-items': 'center', 'justify-content': 'center', 'opacity': '1' };
1804
+ rules['.bw-modal.bw-modal-show'] = { 'opacity': '1', 'visibility': 'visible', 'pointer-events': 'auto' };
1560
1805
  rules['.bw-modal-dialog'] = {
1561
1806
  'position': 'relative', 'width': '100%', 'max-width': '500px', 'margin': '1.75rem auto',
1562
1807
  'pointer-events': 'none', 'transform': 'translateY(-20px)', 'transition': 'transform 0.2s ease-out'
@@ -1576,7 +1821,7 @@ function getStructuralStyles() {
1576
1821
 
1577
1822
  // Carousel (structural)
1578
1823
  rules['.bw-carousel'] = { 'position': 'relative', 'overflow': 'hidden', 'border-radius': '8px' };
1579
- rules['.bw-carousel-track'] = { 'display': 'flex', 'transition': 'transform 0.4s ease', 'height': '100%' };
1824
+ rules['.bw-carousel-track'] = { 'display': 'flex', 'transition': 'transform 0.3s ease-out', 'height': '100%' };
1580
1825
  rules['.bw-carousel-slide'] = { 'min-width': '100%', 'flex-shrink': '0', 'overflow': 'hidden', 'position': 'relative', 'display': 'flex', 'align-items': 'center', 'justify-content': 'center' };
1581
1826
  rules['.bw-carousel-slide img'] = { 'width': '100%', 'height': '100%', 'object-fit': 'cover' };
1582
1827
  rules['.bw-carousel-caption'] = { 'position': 'absolute', 'bottom': '0', 'left': '0', 'right': '0', 'padding': '0.75rem 1rem' };
@@ -1620,11 +1865,14 @@ function getStructuralStyles() {
1620
1865
  'border-bottom': '0', 'border-left': '0.3em solid transparent'
1621
1866
  };
1622
1867
  rules['.bw-dropdown-menu'] = {
1623
- 'position': 'absolute', 'top': '100%', 'left': '0', 'z-index': '1000', 'display': 'none',
1868
+ 'position': 'absolute', 'top': '100%', 'left': '0', 'z-index': '1000', 'display': 'block',
1624
1869
  'min-width': '10rem', 'padding': '0.5rem 0', 'margin': '0.125rem 0 0',
1625
- 'background-clip': 'padding-box', 'border-radius': '6px'
1870
+ 'background-clip': 'padding-box', 'border-radius': '6px',
1871
+ 'opacity': '0', 'visibility': 'hidden', 'transform': 'translateY(-4px)',
1872
+ 'pointer-events': 'none',
1873
+ 'transition': 'opacity 0.15s ease, transform 0.15s ease, visibility 0.15s ease'
1626
1874
  };
1627
- rules['.bw-dropdown-menu.bw-dropdown-show'] = { 'display': 'block' };
1875
+ rules['.bw-dropdown-menu.bw-dropdown-show'] = { 'opacity': '1', 'visibility': 'visible', 'transform': 'translateY(0)', 'pointer-events': 'auto' };
1628
1876
  rules['.bw-dropdown-menu-end'] = { 'left': 'auto', 'right': '0' };
1629
1877
  rules['.bw-dropdown-item'] = {
1630
1878
  'display': 'block', 'width': '100%', 'padding': '0.375rem 1rem', 'clear': 'both',
@@ -1632,6 +1880,7 @@ function getStructuralStyles() {
1632
1880
  'background-color': 'transparent', 'border': '0', 'font-size': '0.9375rem',
1633
1881
  'transition': 'background-color 0.15s, color 0.15s'
1634
1882
  };
1883
+ rules['.bw-dropdown-item:focus-visible'] = { 'outline': '2px solid currentColor', 'outline-offset': '-2px' };
1635
1884
  rules['.bw-dropdown-divider'] = { 'height': '0', 'margin': '0.5rem 0', 'overflow': 'hidden', 'opacity': '1' };
1636
1885
 
1637
1886
  // Switch (structural)
@@ -1639,7 +1888,7 @@ function getStructuralStyles() {
1639
1888
  rules['.bw-form-switch .bw-switch-input'] = {
1640
1889
  'width': '2em', 'height': '1.125em', 'margin-left': '-2.5em', 'border-radius': '2em',
1641
1890
  'appearance': 'none', 'background-position': 'left center', 'background-repeat': 'no-repeat',
1642
- 'background-size': 'contain', 'transition': 'background-position 0.15s ease-in-out, background-color 0.15s ease-in-out',
1891
+ 'background-size': 'contain', 'transition': 'background-position 0.15s ease-out, background-color 0.15s ease-out',
1643
1892
  'cursor': 'pointer'
1644
1893
  };
1645
1894
  rules['.bw-form-switch .bw-switch-input:checked'] = { 'background-position': 'right center' };
@@ -1664,6 +1913,123 @@ function getStructuralStyles() {
1664
1913
  rules['.bw-avatar-lg'] = { 'width': '4rem', 'height': '4rem', 'font-size': '1.25rem' };
1665
1914
  rules['.bw-avatar-xl'] = { 'width': '5rem', 'height': '5rem', 'font-size': '1.5rem' };
1666
1915
 
1916
+ // Stat card (structural)
1917
+ rules['.bw-stat-card'] = {
1918
+ 'border-radius': '8px', 'padding': '1.25rem',
1919
+ 'border-left': '4px solid transparent',
1920
+ 'transition': 'box-shadow 0.15s ease-out, transform 0.15s ease-out'
1921
+ };
1922
+ rules['.bw-stat-card:hover'] = { 'transform': 'translateY(-1px)' };
1923
+ rules['.bw-stat-icon'] = { 'font-size': '1.5rem', 'margin-bottom': '0.5rem' };
1924
+ rules['.bw-stat-value'] = { 'font-size': '2rem', 'font-weight': '700', 'line-height': '1.2' };
1925
+ rules['.bw-stat-label'] = { 'font-size': '0.875rem', 'margin-top': '0.25rem' };
1926
+ rules['.bw-stat-change'] = { 'font-size': '0.875rem', 'font-weight': '500', 'margin-top': '0.5rem' };
1927
+
1928
+ // Tooltip (structural)
1929
+ rules['.bw-tooltip-wrapper'] = { 'position': 'relative', 'display': 'inline-block' };
1930
+ rules['.bw-tooltip'] = {
1931
+ 'position': 'absolute', 'z-index': '999',
1932
+ 'padding': '0.375rem 0.75rem', 'border-radius': '4px', 'font-size': '0.875rem',
1933
+ 'white-space': 'nowrap', 'pointer-events': 'none',
1934
+ 'opacity': '0', 'visibility': 'hidden',
1935
+ 'transition': 'opacity 0.15s ease, visibility 0.15s ease, transform 0.15s ease'
1936
+ };
1937
+ rules['.bw-tooltip.bw-tooltip-show'] = { 'opacity': '1', 'visibility': 'visible' };
1938
+ rules['.bw-tooltip-top'] = { 'bottom': '100%', 'left': '50%', 'transform': 'translateX(-50%) translateY(-4px)', 'margin-bottom': '4px' };
1939
+ rules['.bw-tooltip-top.bw-tooltip-show'] = { 'transform': 'translateX(-50%) translateY(0)' };
1940
+ rules['.bw-tooltip-bottom'] = { 'top': '100%', 'left': '50%', 'transform': 'translateX(-50%) translateY(4px)', 'margin-top': '4px' };
1941
+ rules['.bw-tooltip-bottom.bw-tooltip-show'] = { 'transform': 'translateX(-50%) translateY(0)' };
1942
+ rules['.bw-tooltip-left'] = { 'right': '100%', 'top': '50%', 'transform': 'translateY(-50%) translateX(-4px)', 'margin-right': '4px' };
1943
+ rules['.bw-tooltip-left.bw-tooltip-show'] = { 'transform': 'translateY(-50%) translateX(0)' };
1944
+ rules['.bw-tooltip-right'] = { 'left': '100%', 'top': '50%', 'transform': 'translateY(-50%) translateX(4px)', 'margin-left': '4px' };
1945
+ rules['.bw-tooltip-right.bw-tooltip-show'] = { 'transform': 'translateY(-50%) translateX(0)' };
1946
+
1947
+ // Search input (structural)
1948
+ rules['.bw-search-input'] = { 'position': 'relative', 'display': 'flex', 'align-items': 'center' };
1949
+ rules['.bw-search-input .bw-search-field'] = { 'padding-right': '2.5rem' };
1950
+ rules['.bw-search-clear'] = {
1951
+ 'position': 'absolute', 'right': '0.5rem',
1952
+ 'display': 'flex', 'align-items': 'center', 'justify-content': 'center',
1953
+ 'width': '1.5rem', 'height': '1.5rem',
1954
+ 'border': 'none', 'background': 'none',
1955
+ 'font-size': '1.25rem', 'cursor': 'pointer', 'padding': '0',
1956
+ 'border-radius': '50%', 'transition': 'color 0.15s ease-out'
1957
+ };
1958
+
1959
+ // Range slider (structural)
1960
+ rules['.bw-range-wrapper'] = { 'margin-bottom': '1rem' };
1961
+ rules['.bw-range-label'] = { 'display': 'flex', 'justify-content': 'space-between', 'align-items': 'center', 'margin-bottom': '0.5rem', 'font-size': '0.875rem', 'font-weight': '500' };
1962
+ rules['.bw-range-value'] = { 'font-weight': '600' };
1963
+ rules['.bw-range'] = { 'width': '100%', 'height': '0.5rem', 'padding': '0', 'appearance': 'none', 'border': 'none', 'border-radius': '0.25rem', 'cursor': 'pointer', 'outline': 'none' };
1964
+ rules['.bw-range:disabled'] = { 'opacity': '0.5', 'cursor': 'not-allowed' };
1965
+
1966
+ // Media object (structural)
1967
+ rules['.bw-media'] = { 'display': 'flex', 'align-items': 'flex-start', 'gap': '1rem' };
1968
+ rules['.bw-media-reverse'] = { 'flex-direction': 'row-reverse' };
1969
+ rules['.bw-media-img'] = { 'border-radius': '50%', 'object-fit': 'cover', 'flex-shrink': '0' };
1970
+ rules['.bw-media-body'] = { 'flex': '1', 'min-width': '0' };
1971
+ rules['.bw-media-title'] = { 'margin': '0 0 0.25rem 0', 'font-size': '1rem', 'font-weight': '600', 'line-height': '1.3' };
1972
+
1973
+ // File upload (structural)
1974
+ rules['.bw-file-upload'] = {
1975
+ 'display': 'flex', 'flex-direction': 'column', 'align-items': 'center', 'justify-content': 'center',
1976
+ 'padding': '2rem', 'border': '2px dashed transparent', 'border-radius': '8px',
1977
+ 'cursor': 'pointer', 'text-align': 'center', 'position': 'relative',
1978
+ 'transition': 'border-color 0.15s ease-out, background-color 0.15s ease-out'
1979
+ };
1980
+ rules['.bw-file-upload-icon'] = { 'font-size': '2rem', 'margin-bottom': '0.5rem' };
1981
+ rules['.bw-file-upload-text'] = { 'font-size': '0.875rem' };
1982
+ rules['.bw-file-upload-input'] = {
1983
+ 'position': 'absolute', 'width': '1px', 'height': '1px', 'padding': '0',
1984
+ 'margin': '-1px', 'overflow': 'hidden', 'clip': 'rect(0,0,0,0)', 'border': '0'
1985
+ };
1986
+
1987
+ // Timeline (structural)
1988
+ rules['.bw-timeline'] = { 'position': 'relative', 'padding-left': '2rem' };
1989
+ rules['.bw-timeline-item'] = { 'position': 'relative', 'padding-bottom': '1.5rem' };
1990
+ rules['.bw-timeline-item:last-child'] = { 'padding-bottom': '0' };
1991
+ rules['.bw-timeline-marker'] = { 'position': 'absolute', 'left': '-1.75rem', 'top': '0.25rem', 'width': '0.75rem', 'height': '0.75rem', 'border-radius': '50%' };
1992
+ rules['.bw-timeline-content'] = { 'padding-left': '0.5rem' };
1993
+ rules['.bw-timeline-date'] = { 'font-size': '0.75rem', 'margin-bottom': '0.25rem', 'font-weight': '500' };
1994
+ rules['.bw-timeline-title'] = { 'font-size': '1rem', 'font-weight': '600', 'margin': '0 0 0.25rem 0', 'line-height': '1.3' };
1995
+ rules['.bw-timeline-text'] = { 'font-size': '0.875rem', 'margin': '0', 'line-height': '1.5' };
1996
+
1997
+ // Stepper (structural)
1998
+ rules['.bw-stepper'] = { 'display': 'flex', 'gap': '0' };
1999
+ rules['.bw-step'] = { 'flex': '1', 'display': 'flex', 'flex-direction': 'column', 'align-items': 'center', 'text-align': 'center', 'position': 'relative' };
2000
+ 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' };
2001
+ rules['.bw-step-body'] = { 'margin-top': '0.5rem' };
2002
+ rules['.bw-step-label'] = { 'font-size': '0.875rem', 'font-weight': '500' };
2003
+ rules['.bw-step-description'] = { 'font-size': '0.75rem', 'margin-top': '0.125rem' };
2004
+
2005
+ // Chip input (structural)
2006
+ 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' };
2007
+ 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' };
2008
+ 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' };
2009
+ rules['.bw-chip-field'] = { 'flex': '1', 'min-width': '80px', 'border': 'none', 'outline': 'none', 'font-size': '0.875rem', 'padding': '0.125rem 0', 'background': 'transparent' };
2010
+
2011
+ // Popover (structural)
2012
+ rules['.bw-popover-wrapper'] = { 'position': 'relative', 'display': 'inline-block' };
2013
+ rules['.bw-popover-trigger'] = { 'cursor': 'pointer' };
2014
+ rules['.bw-popover'] = {
2015
+ 'position': 'absolute', 'z-index': '1000',
2016
+ 'min-width': '200px', 'max-width': '320px',
2017
+ 'border-radius': '8px',
2018
+ 'pointer-events': 'none', 'opacity': '0', 'visibility': 'hidden',
2019
+ 'transition': 'opacity 0.15s ease, visibility 0.15s ease, transform 0.15s ease'
2020
+ };
2021
+ rules['.bw-popover.bw-popover-show'] = { 'opacity': '1', 'visibility': 'visible', 'pointer-events': 'auto' };
2022
+ rules['.bw-popover-header'] = { 'padding': '0.625rem 0.875rem', 'font-weight': '600', 'font-size': '0.9375rem' };
2023
+ rules['.bw-popover-body'] = { 'padding': '0.75rem 0.875rem', 'font-size': '0.875rem', 'line-height': '1.5' };
2024
+ rules['.bw-popover-top'] = { 'bottom': '100%', 'left': '50%', 'transform': 'translateX(-50%) translateY(-8px)', 'margin-bottom': '8px' };
2025
+ rules['.bw-popover-top.bw-popover-show'] = { 'transform': 'translateX(-50%) translateY(0)' };
2026
+ rules['.bw-popover-bottom'] = { 'top': '100%', 'left': '50%', 'transform': 'translateX(-50%) translateY(8px)', 'margin-top': '8px' };
2027
+ rules['.bw-popover-bottom.bw-popover-show'] = { 'transform': 'translateX(-50%) translateY(0)' };
2028
+ rules['.bw-popover-left'] = { 'right': '100%', 'top': '50%', 'transform': 'translateY(-50%) translateX(-8px)', 'margin-right': '8px' };
2029
+ rules['.bw-popover-left.bw-popover-show'] = { 'transform': 'translateY(-50%) translateX(0)' };
2030
+ rules['.bw-popover-right'] = { 'left': '100%', 'top': '50%', 'transform': 'translateY(-50%) translateX(8px)', 'margin-left': '8px' };
2031
+ rules['.bw-popover-right.bw-popover-show'] = { 'transform': 'translateY(-50%) translateX(0)' };
2032
+
1667
2033
  // Bar chart (structural)
1668
2034
  rules['.bw-bar-chart-container'] = {
1669
2035
  'padding': '1rem', 'border': '1px solid transparent', 'border-radius': '8px'
@@ -1677,7 +2043,7 @@ function getStructuralStyles() {
1677
2043
  };
1678
2044
  rules['.bw-bar'] = {
1679
2045
  'width': '100%', 'border-radius': '3px 3px 0 0',
1680
- 'transition': 'height 0.5s ease', 'min-height': '4px'
2046
+ 'transition': 'height 0.3s ease-out', 'min-height': '4px'
1681
2047
  };
1682
2048
  rules['.bw-bar:hover'] = { 'opacity': '0.85' };
1683
2049
  rules['.bw-bar-value'] = {
@@ -1806,6 +2172,16 @@ function getStructuralStyles() {
1806
2172
  // Responsive grid
1807
2173
  Object.assign(rules, defaultStyles.responsive);
1808
2174
 
2175
+ // Accessibility: reduce motion for users who prefer it
2176
+ rules['@media (prefers-reduced-motion: reduce)'] = {
2177
+ '*, *::before, *::after': {
2178
+ 'animation-duration': '0.01ms !important',
2179
+ 'animation-iteration-count': '1 !important',
2180
+ 'transition-duration': '0.01ms !important',
2181
+ 'scroll-behavior': 'auto !important'
2182
+ }
2183
+ };
2184
+
1809
2185
  return addUnderscoreAliases(rules);
1810
2186
  }
1811
2187
 
@@ -1814,9 +2190,25 @@ function getStructuralStyles() {
1814
2190
  // =========================================================================
1815
2191
 
1816
2192
  /**
1817
- * Add underscore aliases for all bw- selectors
2193
+ * Add underscore aliases for all `.bw-` selectors.
2194
+ *
2195
+ * CSS CLASS NAMING CONVENTION:
2196
+ *
2197
+ * Canonical form: `.bw-btn`, `.bw-card`, `.bw-table-hover` (hyphens)
2198
+ * Underscore alias: `.bw_btn`, `.bw_card`, `.bw_table_hover` (underscores)
2199
+ *
2200
+ * Both forms are valid in HTML and produce identical results. The hyphen
2201
+ * form is canonical (used in docs, generated CSS, component output).
2202
+ * Underscore aliases exist because:
2203
+ * 1. TACO attribute keys use underscores (`bw_id`, `bw_meta`) — no
2204
+ * quoting needed in JS object literals
2205
+ * 2. Some users prefer underscores for consistency with JS identifiers
2206
+ *
2207
+ * Use `bw.normalizeClass()` to convert underscore classes to canonical
2208
+ * hyphen form at runtime if needed.
2209
+ *
1818
2210
  * @param {Object} rules - CSS rules object
1819
- * @returns {Object} - Rules with underscore aliases added
2211
+ * @returns {Object} Rules with underscore aliases added (both forms work)
1820
2212
  */
1821
2213
  function addUnderscoreAliases(rules) {
1822
2214
  const result = {};
@@ -1833,6 +2225,27 @@ function addUnderscoreAliases(rules) {
1833
2225
  // =========================================================================
1834
2226
  // Theme tokens (backwards compatible)
1835
2227
  // =========================================================================
2228
+ //
2229
+ // DESIGN NOTE — Why no CSS custom properties (CSS variables)?
2230
+ //
2231
+ // Bitwrench targets IE11 as Tier 1 (see dev/bw2x-compatibility.md).
2232
+ // CSS custom properties (var(--color-primary)) are not supported in IE11.
2233
+ //
2234
+ // Instead, bitwrench uses class-scoped CSS generation:
2235
+ // 1. `defaultStyles.*` provides hardcoded cosmetic defaults
2236
+ // 2. `generateTheme(name, config)` generates a complete set of
2237
+ // class-scoped CSS rules from 3 seed colors (primary, secondary,
2238
+ // tertiary) — all components are restyled with the new palette
2239
+ // 3. `generateAlternateCSS()` produces the alternate (dark/light)
2240
+ // variant scoped under `.bw-theme-alt`
2241
+ //
2242
+ // This achieves full theme customization without CSS variables:
2243
+ // bw.generateTheme('ocean', { primary: '#006666', secondary: '#cc6633' })
2244
+ // → generates .ocean .bw-btn-primary { background: #006666; } etc.
2245
+ //
2246
+ // When IE11 support is dropped, CSS custom properties can be added as
2247
+ // an optimization (one rule with var() instead of many scoped rules).
2248
+ // The generateTheme() API stays the same — only the output format changes.
1836
2249
 
1837
2250
  let theme = {
1838
2251
  colors: {
@@ -1840,8 +2253,8 @@ let theme = {
1840
2253
  secondary: '#6c757d',
1841
2254
  success: '#198754',
1842
2255
  danger: '#dc3545',
1843
- warning: '#ffc107',
1844
- info: '#0dcaf0',
2256
+ warning: '#b38600',
2257
+ info: '#0891b2',
1845
2258
  light: '#f8f9fa',
1846
2259
  dark: '#212529',
1847
2260
  white: '#fff',
@@ -1877,214 +2290,63 @@ let theme = {
1877
2290
  '5xl': '3rem'
1878
2291
  }
1879
2292
  },
1880
- darkMode: false
1881
2293
  };
1882
2294
 
1883
2295
  /**
1884
- * Generate theme-aware dark mode CSS from a palette.
1885
- * Derives dark variants from the palette colors instead of using hardcoded values.
2296
+ * Generate alternate-palette CSS scoped under `.bw-theme-alt`.
2297
+ * Uses the same `generateThemedCSS()` pipeline as the primary palette
2298
+ * both sides go through identical code paths.
1886
2299
  *
1887
- * @param {Object} palette - From derivePalette()
1888
- * @returns {Object} CSS rules object for dark mode
1889
- */
1890
- function generateDarkModeCSS(palette) {
1891
- var darkBg = adjustLightness(palette.primary.base, -15);
1892
- var darkBgHsl = hexToHsl(darkBg);
1893
- // Make it very dark (lightness 8-12%)
1894
- var bodyBg = hslToHex([darkBgHsl[0], Math.min(darkBgHsl[1], 30), 10]);
1895
- var surfaceBg = hslToHex([darkBgHsl[0], Math.min(darkBgHsl[1], 25), 15]);
1896
- var textColor = adjustLightness(palette.light.base, 5);
1897
- var borderColor = hslToHex([darkBgHsl[0], Math.min(darkBgHsl[1], 15), 30]);
1898
-
1899
- return {
1900
- ':root.bw-dark': {
1901
- '--bw-body-color': textColor,
1902
- '--bw-body-bg': bodyBg
1903
- },
1904
- '.bw-dark body, :root.bw-dark body': {
1905
- 'color': textColor,
1906
- 'background-color': bodyBg
1907
- },
1908
- '.bw-dark .bw-card': {
1909
- 'background-color': surfaceBg,
1910
- 'border-color': borderColor,
1911
- 'color': textColor
1912
- },
1913
- '.bw-dark .bw-card-header': {
1914
- 'background-color': bodyBg,
1915
- 'border-bottom-color': borderColor,
1916
- 'color': textColor
1917
- },
1918
- '.bw-dark .bw-card-footer': {
1919
- 'background-color': bodyBg,
1920
- 'border-top-color': borderColor,
1921
- 'color': textColor
1922
- },
1923
- '.bw-dark .bw-card-title': {
1924
- 'color': textColor
1925
- },
1926
- '.bw-dark .bw-navbar': {
1927
- 'background-color': surfaceBg,
1928
- 'border-bottom-color': borderColor
1929
- },
1930
- '.bw-dark .bw-navbar-brand': {
1931
- 'color': textColor
1932
- },
1933
- '.bw-dark .bw-navbar-nav .bw-nav-link': {
1934
- 'color': adjustLightness(textColor, -15)
1935
- },
1936
- '.bw-dark .bw-navbar-nav .bw-nav-link:hover': {
1937
- 'color': textColor
1938
- },
1939
- '.bw-dark .bw-form-control': {
1940
- 'background-color': surfaceBg,
1941
- 'border-color': borderColor,
1942
- 'color': textColor
1943
- },
1944
- '.bw-dark .bw-form-label': {
1945
- 'color': textColor
1946
- },
1947
- '.bw-dark .bw-form-text': {
1948
- 'color': adjustLightness(textColor, -20)
1949
- },
1950
- '.bw-dark .bw-table': {
1951
- 'color': textColor
1952
- },
1953
- '.bw-dark .bw-table > :not(caption) > * > *': {
1954
- 'border-bottom-color': borderColor
1955
- },
1956
- '.bw-dark .bw-table > thead > tr > *': {
1957
- 'background-color': bodyBg,
1958
- 'color': adjustLightness(textColor, -10),
1959
- 'border-bottom-color': borderColor
1960
- },
1961
- '.bw-dark .bw-table-striped > tbody > tr:nth-of-type(odd) > *': {
1962
- 'background-color': 'rgba(255, 255, 255, 0.05)'
1963
- },
1964
- '.bw-dark .bw-alert': {
1965
- 'border-color': borderColor
1966
- },
1967
- '.bw-dark .bw-list-group-item': {
1968
- 'background-color': surfaceBg,
1969
- 'border-color': borderColor,
1970
- 'color': textColor
1971
- },
1972
- '.bw-dark .bw-badge': {
1973
- 'color': textColor
1974
- },
1975
- '.bw-dark .bw-nav-tabs': {
1976
- 'border-bottom-color': borderColor
1977
- },
1978
- '.bw-dark .bw-nav-link': {
1979
- 'color': adjustLightness(textColor, -15)
1980
- },
1981
- '.bw-dark .bw-nav-tabs .bw-nav-link:hover': {
1982
- 'color': textColor,
1983
- 'border-bottom-color': borderColor
1984
- },
1985
- '.bw-dark .bw-pagination .bw-page-link': {
1986
- 'background-color': surfaceBg,
1987
- 'border-color': borderColor,
1988
- 'color': textColor
1989
- },
1990
- '.bw-dark .bw-breadcrumb-item + .bw-breadcrumb-item::before': {
1991
- 'color': adjustLightness(textColor, -20)
1992
- },
1993
- '.bw-dark .bw-breadcrumb-item.active': {
1994
- 'color': adjustLightness(textColor, -10)
1995
- },
1996
- '.bw-dark .bw-hero-light': {
1997
- 'background': surfaceBg,
1998
- 'color': textColor
1999
- },
2000
- '.bw-dark .bw-progress': {
2001
- 'background-color': surfaceBg
2002
- },
2003
- '.bw-dark .bw-section-subtitle': {
2004
- 'color': adjustLightness(textColor, -15)
2005
- },
2006
- '.bw-dark .bw-close': {
2007
- 'color': textColor
2008
- },
2009
- '.bw-dark .bw-accordion-item': {
2010
- 'background-color': surfaceBg,
2011
- 'border-color': borderColor
2012
- },
2013
- '.bw-dark .bw-accordion-button': {
2014
- 'color': textColor
2015
- },
2016
- '.bw-dark .bw-accordion-button:not(.bw-collapsed)': {
2017
- 'color': '#7dd3e0',
2018
- 'background-color': 'rgba(125, 211, 224, 0.1)'
2019
- },
2020
- '.bw-dark .bw-accordion-button:hover': {
2021
- 'background-color': bodyBg
2022
- },
2023
- '.bw-dark .bw-accordion-button:not(.bw-collapsed):hover': {
2024
- 'background-color': 'rgba(125, 211, 224, 0.15)'
2025
- },
2026
- '.bw-dark .bw-accordion-button:focus-visible': {
2027
- 'box-shadow': '0 0 0 0.2rem rgba(125, 211, 224, 0.3)'
2028
- },
2029
- '.bw-dark .bw-accordion-body': {
2030
- 'border-top-color': borderColor
2031
- },
2032
- '.bw-dark .bw-carousel': {
2033
- 'background-color': bodyBg
2034
- },
2035
- '.bw-dark .bw-carousel-control': {
2036
- 'background-color': 'rgba(255,255,255,0.15)'
2037
- },
2038
- '.bw-dark .bw-carousel-control:hover': {
2039
- 'background-color': 'rgba(255,255,255,0.25)'
2040
- },
2041
- '.bw-dark .bw-modal-content': {
2042
- 'background-color': surfaceBg,
2043
- 'border-color': borderColor
2044
- },
2045
- '.bw-dark .bw-modal-header': {
2046
- 'border-bottom-color': borderColor
2047
- },
2048
- '.bw-dark .bw-modal-footer': {
2049
- 'border-top-color': borderColor
2050
- },
2051
- '.bw-dark .bw-modal-title': {
2052
- 'color': textColor
2053
- },
2054
- '.bw-dark .bw-toast': {
2055
- 'background-color': surfaceBg,
2056
- 'border-color': borderColor
2057
- },
2058
- '.bw-dark .bw-toast-header': {
2059
- 'border-bottom-color': borderColor,
2060
- 'color': textColor
2061
- },
2062
- '.bw-dark .bw-dropdown-menu': {
2063
- 'background-color': surfaceBg,
2064
- 'border-color': borderColor
2065
- },
2066
- '.bw-dark .bw-dropdown-item': {
2067
- 'color': textColor
2068
- },
2069
- '.bw-dark .bw-dropdown-item:hover': {
2070
- 'background-color': bodyBg
2071
- },
2072
- '.bw-dark .bw-dropdown-divider': {
2073
- 'border-top-color': borderColor
2074
- },
2075
- '.bw-dark .bw-skeleton': {
2076
- 'background': 'linear-gradient(90deg, ' + borderColor + ' 25%, ' + surfaceBg + ' 37%, ' + borderColor + ' 63%)'
2077
- },
2078
- '.bw-dark h1, .bw-dark h2, .bw-dark h3, .bw-dark h4, .bw-dark h5, .bw-dark h6': {
2079
- 'color': textColor
2080
- },
2081
- '@media (prefers-color-scheme: dark)': {
2082
- ':root.bw-auto-dark body': {
2083
- 'color': textColor,
2084
- 'background-color': bodyBg
2300
+ * @param {string} name - Theme scope name (e.g. 'ocean'). '' for global.
2301
+ * @param {Object} altPalette - From derivePalette(deriveAlternateConfig(...))
2302
+ * @param {Object} layout - From resolveLayout()
2303
+ * @returns {Object} CSS rules object scoped under .bw-theme-alt (+ optional .name)
2304
+ */
2305
+ function generateAlternateCSS(name, altPalette, layout) {
2306
+ // Generate themed CSS using the same pipeline as primary
2307
+ var rawRules = generateThemedCSS('', altPalette, layout);
2308
+
2309
+ // Re-scope every selector under .bw-theme-alt (+ optional theme name)
2310
+ var altPrefix = name ? '.' + name + '.bw-theme-alt' : '.bw-theme-alt';
2311
+ var altRules = {};
2312
+
2313
+ for (var sel in rawRules) {
2314
+ if (!rawRules.hasOwnProperty(sel)) continue;
2315
+
2316
+ if (sel.charAt(0) === '@') {
2317
+ // @media / @keyframes — recurse into the block
2318
+ var innerBlock = rawRules[sel];
2319
+ var altInner = {};
2320
+ for (var innerSel in innerBlock) {
2321
+ if (!innerBlock.hasOwnProperty(innerSel)) continue;
2322
+ altInner[altPrefix + ' ' + innerSel] = innerBlock[innerSel];
2323
+ }
2324
+ altRules[sel] = altInner;
2325
+ } else {
2326
+ // Regular selector — prefix with alt scope
2327
+ // Handle comma-separated selectors
2328
+ var parts = sel.split(',');
2329
+ var scopedParts = [];
2330
+ for (var i = 0; i < parts.length; i++) {
2331
+ var s = parts[i].trim();
2332
+ // 'body' selector gets special treatment: .bw-theme-alt body
2333
+ if (s === 'body' || s.indexOf('body') === 0) {
2334
+ scopedParts.push(altPrefix + ' ' + s);
2335
+ } else {
2336
+ scopedParts.push(altPrefix + ' ' + s);
2337
+ }
2085
2338
  }
2339
+ altRules[scopedParts.join(', ')] = rawRules[sel];
2086
2340
  }
2341
+ }
2342
+
2343
+ // Add body-level overrides for the alternate surface
2344
+ altRules[altPrefix + ' body, :root' + altPrefix + ' body'] = {
2345
+ 'color': altPalette.dark.base,
2346
+ 'background-color': altPalette.light.base
2087
2347
  };
2348
+
2349
+ return altRules;
2088
2350
  }
2089
2351
 
2090
2352
  function deepMerge(target, source) {
@@ -2281,8 +2543,11 @@ function makeCard(props = {}) {
2281
2543
  * variant: "success",
2282
2544
  * onclick: () => console.log("saved")
2283
2545
  * });
2546
+ * // String shorthand:
2547
+ * const ok = makeButton("OK");
2284
2548
  */
2285
2549
  function makeButton(props = {}) {
2550
+ if (typeof props === 'string') props = { text: props };
2286
2551
  const {
2287
2552
  text,
2288
2553
  variant = 'primary',
@@ -2587,6 +2852,7 @@ function makeTabs(props = {}) {
2587
2852
  class: `bw-nav-link ${index === actualActiveIndex ? 'active' : ''}`,
2588
2853
  type: 'button',
2589
2854
  role: 'tab',
2855
+ tabindex: index === actualActiveIndex ? '0' : '-1',
2590
2856
  'aria-selected': index === actualActiveIndex ? 'true' : 'false',
2591
2857
  'data-tab-index': index,
2592
2858
  onclick: (e) => {
@@ -2597,11 +2863,13 @@ function makeTabs(props = {}) {
2597
2863
  allTabs.forEach(t => {
2598
2864
  t.classList.remove('active');
2599
2865
  t.setAttribute('aria-selected', 'false');
2866
+ t.setAttribute('tabindex', '-1');
2600
2867
  });
2601
2868
  allPanes.forEach(p => p.classList.remove('active'));
2602
2869
 
2603
2870
  e.target.classList.add('active');
2604
2871
  e.target.setAttribute('aria-selected', 'true');
2872
+ e.target.setAttribute('tabindex', '0');
2605
2873
  const targetIndex = parseInt(e.target.getAttribute('data-tab-index'));
2606
2874
  allPanes[targetIndex].classList.add('active');
2607
2875
  }
@@ -2625,7 +2893,39 @@ function makeTabs(props = {}) {
2625
2893
  ],
2626
2894
  o: {
2627
2895
  type: 'tabs',
2628
- state: { activeIndex: actualActiveIndex }
2896
+ state: { activeIndex: actualActiveIndex },
2897
+ mounted: function(el) {
2898
+ var tablist = el.querySelector('[role="tablist"]');
2899
+ if (!tablist) return;
2900
+ tablist.addEventListener('keydown', function(e) {
2901
+ var tabButtons = tablist.querySelectorAll('[role="tab"]');
2902
+ var currentIndex = -1;
2903
+ for (var i = 0; i < tabButtons.length; i++) {
2904
+ if (tabButtons[i] === e.target) { currentIndex = i; break; }
2905
+ }
2906
+ if (currentIndex === -1) return;
2907
+
2908
+ var newIndex = -1;
2909
+ if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
2910
+ e.preventDefault();
2911
+ newIndex = currentIndex > 0 ? currentIndex - 1 : tabButtons.length - 1;
2912
+ } else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
2913
+ e.preventDefault();
2914
+ newIndex = currentIndex < tabButtons.length - 1 ? currentIndex + 1 : 0;
2915
+ } else if (e.key === 'Home') {
2916
+ e.preventDefault();
2917
+ newIndex = 0;
2918
+ } else if (e.key === 'End') {
2919
+ e.preventDefault();
2920
+ newIndex = tabButtons.length - 1;
2921
+ }
2922
+
2923
+ if (newIndex >= 0) {
2924
+ tabButtons[newIndex].focus();
2925
+ tabButtons[newIndex].click();
2926
+ }
2927
+ });
2928
+ }
2629
2929
  }
2630
2930
  };
2631
2931
  }
@@ -2646,8 +2946,11 @@ function makeTabs(props = {}) {
2646
2946
  * variant: "success",
2647
2947
  * dismissible: true
2648
2948
  * });
2949
+ * // String shorthand:
2950
+ * const msg = makeAlert("Something happened");
2649
2951
  */
2650
2952
  function makeAlert(props = {}) {
2953
+ if (typeof props === 'string') props = { content: props };
2651
2954
  const {
2652
2955
  content,
2653
2956
  variant = 'info',
@@ -2694,8 +2997,11 @@ function makeAlert(props = {}) {
2694
2997
  * @example
2695
2998
  * const badge = makeBadge({ text: "New", variant: "danger", pill: true });
2696
2999
  * const small = makeBadge({ text: "3", variant: "info", size: "sm" });
3000
+ * // String shorthand:
3001
+ * const tag = makeBadge("New");
2697
3002
  */
2698
3003
  function makeBadge(props = {}) {
3004
+ if (typeof props === 'string') props = { text: props };
2699
3005
  const {
2700
3006
  text,
2701
3007
  variant = 'primary',
@@ -2932,13 +3238,16 @@ function makeForm(props = {}) {
2932
3238
  }
2933
3239
 
2934
3240
  /**
2935
- * Create a form group with label, input, and optional help text
3241
+ * Create a form group with label, input, optional help text and validation feedback
2936
3242
  *
2937
3243
  * @param {Object} [props] - Form group configuration
2938
3244
  * @param {string} [props.label] - Label text
2939
3245
  * @param {Object} [props.input] - Input TACO object (from makeInput, makeSelect, etc.)
2940
3246
  * @param {string} [props.help] - Help text displayed below the input
2941
3247
  * @param {string} [props.id] - Input ID (links label to input via for/id)
3248
+ * @param {string} [props.validation] - Validation state ("valid" or "invalid")
3249
+ * @param {string} [props.feedback] - Validation feedback text shown below input
3250
+ * @param {boolean} [props.required=false] - Show required indicator (*) on label
2942
3251
  * @returns {Object} TACO object representing a form group
2943
3252
  * @category Component Builders
2944
3253
  * @example
@@ -2946,11 +3255,22 @@ function makeForm(props = {}) {
2946
3255
  * label: "Email",
2947
3256
  * id: "email",
2948
3257
  * input: makeInput({ type: "email", id: "email", placeholder: "you@example.com" }),
2949
- * help: "We'll never share your email."
3258
+ * validation: "invalid",
3259
+ * feedback: "Please enter a valid email address."
2950
3260
  * });
2951
3261
  */
2952
3262
  function makeFormGroup(props = {}) {
2953
- const { label, input, help, id } = props;
3263
+ var { label, input, help, id, validation, feedback, required } = props;
3264
+
3265
+ // Shallow-clone input TACO to add validation class without mutating original
3266
+ var styledInput = input;
3267
+ if (validation && input && input.a) {
3268
+ styledInput = { t: input.t, a: Object.assign({}, input.a), c: input.c, o: input.o };
3269
+ var validClass = validation === 'valid' ? 'bw-is-valid' : validation === 'invalid' ? 'bw-is-invalid' : '';
3270
+ if (validClass) {
3271
+ styledInput.a.class = ((styledInput.a.class || '') + ' ' + validClass).trim();
3272
+ }
3273
+ }
2954
3274
 
2955
3275
  return {
2956
3276
  t: 'div',
@@ -2959,9 +3279,14 @@ function makeFormGroup(props = {}) {
2959
3279
  label && {
2960
3280
  t: 'label',
2961
3281
  a: { for: id, class: 'bw-form-label' },
2962
- c: label
3282
+ c: required ? [label, { t: 'span', a: { class: 'bw-text-danger', style: 'margin-left: 0.25rem' }, c: '*' }] : label
3283
+ },
3284
+ styledInput,
3285
+ feedback && validation && {
3286
+ t: 'div',
3287
+ a: { class: validation === 'valid' ? 'bw-valid-feedback' : 'bw-invalid-feedback' },
3288
+ c: feedback
2963
3289
  },
2964
- input,
2965
3290
  help && {
2966
3291
  t: 'small',
2967
3292
  a: { class: 'bw-form-text bw-text-muted' },
@@ -3926,17 +4251,15 @@ function makeCodeDemo(props = {}) {
3926
4251
  t: 'button',
3927
4252
  a: {
3928
4253
  class: 'bw-copy-btn bw-code-copy-btn',
3929
- onclick: (e) => {
3930
- navigator.clipboard.writeText(code).then(() => {
3931
- const btn = e.target;
3932
- const originalText = btn.textContent;
4254
+ onclick: function(e) {
4255
+ navigator.clipboard.writeText(code).then(function() {
4256
+ var btn = e.target;
4257
+ var originalText = btn.textContent;
3933
4258
  btn.textContent = 'Copied!';
3934
- btn.style.background = '#006666';
3935
- btn.style.color = '#fff';
3936
- setTimeout(() => {
4259
+ btn.classList.add('bw-code-copy-btn-copied');
4260
+ setTimeout(function() {
3937
4261
  btn.textContent = originalText;
3938
- btn.style.background = 'rgba(255,255,255,0.12)';
3939
- btn.style.color = '#aaa';
4262
+ btn.classList.remove('bw-code-copy-btn-copied');
3940
4263
  }, 2000);
3941
4264
  });
3942
4265
  }
@@ -4228,29 +4551,47 @@ function makeAccordion(props = {}) {
4228
4551
  var isOpen = collapse.classList.contains('bw-collapse-show');
4229
4552
 
4230
4553
  if (!multiOpen) {
4231
- // Close all siblings
4232
- var allCollapses = accordionEl.querySelectorAll('.bw-accordion-collapse');
4233
- var allButtons = accordionEl.querySelectorAll('.bw-accordion-button');
4234
- for (var j = 0; j < allCollapses.length; j++) {
4235
- allCollapses[j].classList.remove('bw-collapse-show');
4236
- allCollapses[j].style.maxHeight = null;
4237
- }
4238
- for (var k = 0; k < allButtons.length; k++) {
4239
- allButtons[k].classList.add('bw-collapsed');
4240
- allButtons[k].setAttribute('aria-expanded', 'false');
4554
+ // Animate-close all other open siblings
4555
+ var allItems = accordionEl.querySelectorAll('.bw-accordion-item');
4556
+ for (var j = 0; j < allItems.length; j++) {
4557
+ if (allItems[j] === accordionItem) continue;
4558
+ var sibCollapse = allItems[j].querySelector('.bw-accordion-collapse');
4559
+ var sibBtn = allItems[j].querySelector('.bw-accordion-button');
4560
+ if (sibCollapse.classList.contains('bw-collapse-show')) {
4561
+ sibCollapse.style.maxHeight = sibCollapse.scrollHeight + 'px';
4562
+ sibCollapse.offsetHeight; // force reflow
4563
+ sibCollapse.style.maxHeight = '0px';
4564
+ sibCollapse.classList.remove('bw-collapse-show');
4565
+ sibBtn.classList.add('bw-collapsed');
4566
+ sibBtn.setAttribute('aria-expanded', 'false');
4567
+ }
4241
4568
  }
4242
4569
  }
4243
4570
 
4244
4571
  if (isOpen) {
4572
+ // Animate close
4573
+ collapse.style.maxHeight = collapse.scrollHeight + 'px';
4574
+ collapse.offsetHeight; // force reflow
4575
+ collapse.style.maxHeight = '0px';
4245
4576
  collapse.classList.remove('bw-collapse-show');
4246
- collapse.style.maxHeight = null;
4247
4577
  btn.classList.add('bw-collapsed');
4248
4578
  btn.setAttribute('aria-expanded', 'false');
4249
4579
  } else {
4580
+ // Animate open
4250
4581
  collapse.classList.add('bw-collapse-show');
4582
+ collapse.style.maxHeight = '0px';
4583
+ collapse.offsetHeight; // force reflow
4251
4584
  collapse.style.maxHeight = collapse.scrollHeight + 'px';
4252
4585
  btn.classList.remove('bw-collapsed');
4253
4586
  btn.setAttribute('aria-expanded', 'true');
4587
+ // After transition, allow dynamic content sizing
4588
+ var onEnd = function(ev) {
4589
+ if (ev.propertyName === 'max-height' && collapse.classList.contains('bw-collapse-show')) {
4590
+ collapse.style.maxHeight = 'none';
4591
+ }
4592
+ collapse.removeEventListener('transitionend', onEnd);
4593
+ };
4594
+ collapse.addEventListener('transitionend', onEnd);
4254
4595
  }
4255
4596
  }
4256
4597
  },
@@ -4267,7 +4608,7 @@ function makeAccordion(props = {}) {
4267
4608
  },
4268
4609
  o: item.open ? {
4269
4610
  mounted: function(el) {
4270
- el.style.maxHeight = el.scrollHeight + 'px';
4611
+ el.style.maxHeight = 'none';
4271
4612
  }
4272
4613
  } : undefined
4273
4614
  }
@@ -4977,104 +5318,997 @@ function makeCarousel(props = {}) {
4977
5318
  a: {
4978
5319
  class: ('bw-carousel ' + className).trim(),
4979
5320
  style: 'height: ' + height,
5321
+ tabindex: '0',
5322
+ 'aria-roledescription': 'carousel',
4980
5323
  'data-carousel-index': startIndex
4981
5324
  },
4982
5325
  c: children,
4983
5326
  o: {
4984
5327
  type: 'carousel',
4985
5328
  state: { activeIndex: startIndex, autoPlay: autoPlay, interval: interval },
4986
- mounted: autoPlay ? function(el) {
4987
- var intervalId = setInterval(function() {
5329
+ mounted: function(el) {
5330
+ // Keyboard navigation
5331
+ el.addEventListener('keydown', function(e) {
4988
5332
  var idx = parseInt(el.getAttribute('data-carousel-index') || '0');
4989
- goToSlide(el, idx + 1);
4990
- }, interval);
4991
- el._bw_carouselInterval = intervalId;
4992
- } : undefined,
4993
- unmount: autoPlay ? function(el) {
5333
+ if (e.key === 'ArrowLeft') {
5334
+ e.preventDefault();
5335
+ goToSlide(el, idx - 1);
5336
+ } else if (e.key === 'ArrowRight') {
5337
+ e.preventDefault();
5338
+ goToSlide(el, idx + 1);
5339
+ }
5340
+ });
5341
+ // Auto-play
5342
+ if (autoPlay) {
5343
+ var intervalId = setInterval(function() {
5344
+ var idx = parseInt(el.getAttribute('data-carousel-index') || '0');
5345
+ goToSlide(el, idx + 1);
5346
+ }, interval);
5347
+ el._bw_carouselInterval = intervalId;
5348
+ // Pause on hover/focus for usability
5349
+ el.addEventListener('mouseenter', function() {
5350
+ if (el._bw_carouselInterval) clearInterval(el._bw_carouselInterval);
5351
+ });
5352
+ el.addEventListener('mouseleave', function() {
5353
+ el._bw_carouselInterval = setInterval(function() {
5354
+ var idx = parseInt(el.getAttribute('data-carousel-index') || '0');
5355
+ goToSlide(el, idx + 1);
5356
+ }, interval);
5357
+ });
5358
+ }
5359
+ },
5360
+ unmount: function(el) {
4994
5361
  if (el._bw_carouselInterval) {
4995
5362
  clearInterval(el._bw_carouselInterval);
4996
5363
  }
4997
- } : undefined
5364
+ }
4998
5365
  }
4999
5366
  };
5000
5367
  }
5001
5368
 
5002
- const componentHandles = {
5003
- card: CardHandle,
5004
- table: TableHandle,
5005
- navbar: NavbarHandle,
5006
- tabs: TabsHandle,
5007
- modal: ModalHandle
5008
- };
5009
-
5010
- var components = /*#__PURE__*/Object.freeze({
5011
- __proto__: null,
5012
- CardHandle: CardHandle,
5013
- ModalHandle: ModalHandle,
5014
- NavbarHandle: NavbarHandle,
5015
- TableHandle: TableHandle,
5016
- TabsHandle: TabsHandle,
5017
- componentHandles: componentHandles,
5018
- makeAccordion: makeAccordion,
5019
- makeAlert: makeAlert,
5020
- makeAvatar: makeAvatar,
5021
- makeBadge: makeBadge,
5022
- makeBreadcrumb: makeBreadcrumb,
5023
- makeButton: makeButton,
5024
- makeButtonGroup: makeButtonGroup,
5025
- makeCTA: makeCTA,
5026
- makeCard: makeCard,
5027
- makeCarousel: makeCarousel,
5028
- makeCheckbox: makeCheckbox,
5029
- makeCodeDemo: makeCodeDemo,
5030
- makeCol: makeCol,
5031
- makeContainer: makeContainer,
5032
- makeDropdown: makeDropdown,
5033
- makeFeatureGrid: makeFeatureGrid,
5034
- makeForm: makeForm,
5035
- makeFormGroup: makeFormGroup,
5036
- makeHero: makeHero,
5037
- makeInput: makeInput,
5038
- makeListGroup: makeListGroup,
5039
- makeModal: makeModal,
5040
- makeNav: makeNav,
5041
- makeNavbar: makeNavbar,
5042
- makePagination: makePagination,
5043
- makeProgress: makeProgress,
5044
- makeRadio: makeRadio,
5045
- makeRow: makeRow,
5046
- makeSection: makeSection,
5047
- makeSelect: makeSelect,
5048
- makeSkeleton: makeSkeleton,
5049
- makeSpinner: makeSpinner,
5050
- makeStack: makeStack,
5051
- makeSwitch: makeSwitch,
5052
- makeTabs: makeTabs,
5053
- makeTextarea: makeTextarea,
5054
- makeToast: makeToast
5055
- });
5369
+ // =========================================================================
5370
+ // Phase 4: Dashboard & Data Display
5371
+ // =========================================================================
5056
5372
 
5057
5373
  /**
5058
- * Bitwrench v2 Core
5059
- * Zero-dependency UI library using JavaScript objects
5060
- * Works in browsers (IE11+) and Node.js
5061
- *
5062
- * @license BSD-2-Clause
5063
- * @author M A Chatterjee <deftio [at] deftio [dot] com>
5374
+ * Create a stat card for dashboard metrics display
5375
+ *
5376
+ * Shows a large value with a label and optional change indicator.
5377
+ * Designed for dashboard grid layouts with left-border accent.
5378
+ *
5379
+ * @param {Object|string} [props] - Stat card configuration (string shorthand sets label)
5380
+ * @param {string|number} [props.value=0] - The main stat value to display
5381
+ * @param {string} [props.label] - Descriptive label below the value
5382
+ * @param {number} [props.change] - Percentage change indicator (positive = green arrow, negative = red)
5383
+ * @param {string} [props.format] - Value format ("number", "currency", "percent")
5384
+ * @param {string} [props.prefix] - Custom prefix (e.g. "$")
5385
+ * @param {string} [props.suffix] - Custom suffix (e.g. "%")
5386
+ * @param {string} [props.icon] - Icon content (emoji or text) shown above value
5387
+ * @param {string} [props.variant] - Left-border color variant ("primary", "success", "danger", etc.)
5388
+ * @param {string} [props.className] - Additional CSS classes
5389
+ * @param {Object} [props.style] - Inline style object
5390
+ * @returns {Object} TACO object representing a stat card
5391
+ * @category Component Builders
5392
+ * @example
5393
+ * const stat = makeStatCard({
5394
+ * value: 2345,
5395
+ * label: 'Active Users',
5396
+ * change: 5.3,
5397
+ * format: 'number',
5398
+ * variant: 'primary'
5399
+ * });
5064
5400
  */
5401
+ function makeStatCard(props = {}) {
5402
+ if (typeof props === 'string') props = { label: props };
5403
+ var {
5404
+ value = 0,
5405
+ label,
5406
+ change,
5407
+ format,
5408
+ prefix,
5409
+ suffix,
5410
+ icon,
5411
+ variant,
5412
+ className = '',
5413
+ style
5414
+ } = props;
5065
5415
 
5416
+ function formatValue(val, fmt) {
5417
+ if (prefix || suffix) return (prefix || '') + val + (suffix || '');
5418
+ switch (fmt) {
5419
+ case 'currency': return '$' + Number(val).toLocaleString();
5420
+ case 'percent': return val + '%';
5421
+ case 'number': return Number(val).toLocaleString();
5422
+ default: return '' + val;
5423
+ }
5424
+ }
5066
5425
 
5067
- // Environment-aware module loader for optional Node.js built-ins (fs).
5068
- // Strategy: try require() first (CJS/UMD), fall back to import() (ESM).
5069
- // import() is wrapped in Function() to avoid parse errors in ES5/IE11 environments.
5426
+ var classes = [
5427
+ 'bw-stat-card',
5428
+ variant ? 'bw-stat-card-' + variant : '',
5429
+ className
5430
+ ].filter(Boolean).join(' ').trim();
5070
5431
 
5071
- // Core bitwrench namespace
5072
- const bw = {
5073
- // Version info from generated file
5074
- version: VERSION_INFO.version,
5075
- versionInfo: VERSION_INFO,
5076
-
5077
- /**
5432
+ var children = [];
5433
+
5434
+ if (icon) {
5435
+ children.push({
5436
+ t: 'div',
5437
+ a: { class: 'bw-stat-icon' },
5438
+ c: icon
5439
+ });
5440
+ }
5441
+
5442
+ children.push({
5443
+ t: 'div',
5444
+ a: { class: 'bw-stat-value' },
5445
+ c: formatValue(value, format)
5446
+ });
5447
+
5448
+ if (label) {
5449
+ children.push({
5450
+ t: 'div',
5451
+ a: { class: 'bw-stat-label' },
5452
+ c: label
5453
+ });
5454
+ }
5455
+
5456
+ if (change !== undefined && change !== null) {
5457
+ children.push({
5458
+ t: 'div',
5459
+ a: {
5460
+ class: 'bw-stat-change ' + (change >= 0 ? 'bw-stat-change-up' : 'bw-stat-change-down')
5461
+ },
5462
+ c: (change >= 0 ? '\u2191 +' : '\u2193 ') + change + '%'
5463
+ });
5464
+ }
5465
+
5466
+ return {
5467
+ t: 'div',
5468
+ a: { class: classes, style: style },
5469
+ c: children,
5470
+ o: { type: 'stat-card' }
5471
+ };
5472
+ }
5473
+
5474
+ // =========================================================================
5475
+ // Phase 5: Overlays & Popovers
5476
+ // =========================================================================
5477
+
5478
+ /**
5479
+ * Create a tooltip wrapper around trigger content
5480
+ *
5481
+ * Wraps the trigger element in a container that shows tooltip text
5482
+ * on hover and focus. Pure CSS-driven show/hide with JS lifecycle
5483
+ * for event binding.
5484
+ *
5485
+ * @param {Object} [props] - Tooltip configuration
5486
+ * @param {string|Object|Array} [props.content] - Trigger content (what the user hovers/focuses)
5487
+ * @param {string} [props.text=""] - Tooltip text to display
5488
+ * @param {string} [props.placement="top"] - Tooltip placement ("top", "bottom", "left", "right")
5489
+ * @param {string} [props.className] - Additional CSS classes
5490
+ * @returns {Object} TACO object representing a tooltip wrapper
5491
+ * @category Component Builders
5492
+ * @example
5493
+ * const tip = makeTooltip({
5494
+ * content: makeButton({ text: 'Hover me' }),
5495
+ * text: 'This is a tooltip!',
5496
+ * placement: 'top'
5497
+ * });
5498
+ */
5499
+ function makeTooltip(props = {}) {
5500
+ var {
5501
+ content,
5502
+ text = '',
5503
+ placement = 'top',
5504
+ className = ''
5505
+ } = props;
5506
+
5507
+ return {
5508
+ t: 'span',
5509
+ a: { class: ('bw-tooltip-wrapper ' + className).trim() },
5510
+ c: [
5511
+ content,
5512
+ {
5513
+ t: 'span',
5514
+ a: {
5515
+ class: 'bw-tooltip bw-tooltip-' + placement,
5516
+ role: 'tooltip'
5517
+ },
5518
+ c: text
5519
+ }
5520
+ ],
5521
+ o: {
5522
+ type: 'tooltip',
5523
+ mounted: function(el) {
5524
+ var tip = el.querySelector('.bw-tooltip');
5525
+ el.addEventListener('mouseenter', function() {
5526
+ tip.classList.add('bw-tooltip-show');
5527
+ });
5528
+ el.addEventListener('mouseleave', function() {
5529
+ tip.classList.remove('bw-tooltip-show');
5530
+ });
5531
+ el.addEventListener('focusin', function() {
5532
+ tip.classList.add('bw-tooltip-show');
5533
+ });
5534
+ el.addEventListener('focusout', function() {
5535
+ tip.classList.remove('bw-tooltip-show');
5536
+ });
5537
+ }
5538
+ }
5539
+ };
5540
+ }
5541
+
5542
+ /**
5543
+ * Create a popover wrapper around trigger content
5544
+ *
5545
+ * Like a tooltip but richer — supports title + body content and is
5546
+ * triggered by click rather than hover. Dismisses on click outside.
5547
+ *
5548
+ * @param {Object} [props] - Popover configuration
5549
+ * @param {string|Object|Array} [props.trigger] - Trigger content (what the user clicks)
5550
+ * @param {string} [props.title] - Popover header title
5551
+ * @param {string|Object|Array} [props.content] - Popover body content
5552
+ * @param {string} [props.placement="top"] - Placement ("top", "bottom", "left", "right")
5553
+ * @param {string} [props.className] - Additional CSS classes
5554
+ * @returns {Object} TACO object representing a popover wrapper
5555
+ * @category Component Builders
5556
+ * @example
5557
+ * const pop = makePopover({
5558
+ * trigger: makeButton({ text: 'Click me' }),
5559
+ * title: 'Popover Title',
5560
+ * content: 'Some helpful information here.',
5561
+ * placement: 'bottom'
5562
+ * });
5563
+ */
5564
+ function makePopover(props = {}) {
5565
+ var {
5566
+ trigger,
5567
+ title,
5568
+ content,
5569
+ placement = 'top',
5570
+ className = ''
5571
+ } = props;
5572
+
5573
+ var popoverContent = [
5574
+ title && {
5575
+ t: 'div',
5576
+ a: { class: 'bw-popover-header' },
5577
+ c: title
5578
+ },
5579
+ content && {
5580
+ t: 'div',
5581
+ a: { class: 'bw-popover-body' },
5582
+ c: content
5583
+ }
5584
+ ].filter(Boolean);
5585
+
5586
+ return {
5587
+ t: 'span',
5588
+ a: { class: ('bw-popover-wrapper ' + className).trim() },
5589
+ c: [
5590
+ {
5591
+ t: 'span',
5592
+ a: {
5593
+ class: 'bw-popover-trigger',
5594
+ onclick: function(e) {
5595
+ var wrapper = e.target.closest('.bw-popover-wrapper');
5596
+ var pop = wrapper.querySelector('.bw-popover');
5597
+ pop.classList.toggle('bw-popover-show');
5598
+ }
5599
+ },
5600
+ c: trigger
5601
+ },
5602
+ {
5603
+ t: 'div',
5604
+ a: {
5605
+ class: 'bw-popover bw-popover-' + placement
5606
+ },
5607
+ c: popoverContent
5608
+ }
5609
+ ],
5610
+ o: {
5611
+ type: 'popover',
5612
+ mounted: function(el) {
5613
+ // Click outside to close
5614
+ var outsideHandler = function(e) {
5615
+ if (!el.contains(e.target)) {
5616
+ var pop = el.querySelector('.bw-popover');
5617
+ if (pop) pop.classList.remove('bw-popover-show');
5618
+ }
5619
+ };
5620
+ document.addEventListener('click', outsideHandler);
5621
+ el._bw_outsideHandler = outsideHandler;
5622
+ },
5623
+ unmount: function(el) {
5624
+ if (el._bw_outsideHandler) {
5625
+ document.removeEventListener('click', el._bw_outsideHandler);
5626
+ }
5627
+ }
5628
+ }
5629
+ };
5630
+ }
5631
+
5632
+ // =========================================================================
5633
+ // Phase 6: Form Enhancements & Layout
5634
+ // =========================================================================
5635
+
5636
+ /**
5637
+ * Create a search input with clear button
5638
+ *
5639
+ * Wraps a text input with a clear (×) button that appears when
5640
+ * the field has content. Calls onSearch on Enter key.
5641
+ *
5642
+ * @param {Object} [props] - Search input configuration
5643
+ * @param {string} [props.placeholder="Search..."] - Placeholder text
5644
+ * @param {string} [props.value] - Initial value
5645
+ * @param {Function} [props.onSearch] - Callback when Enter is pressed, receives value
5646
+ * @param {Function} [props.onInput] - Callback on each keystroke, receives value
5647
+ * @param {string} [props.id] - Element ID
5648
+ * @param {string} [props.name] - Input name attribute
5649
+ * @param {string} [props.className] - Additional CSS classes
5650
+ * @returns {Object} TACO object representing a search input
5651
+ * @category Component Builders
5652
+ * @example
5653
+ * const search = makeSearchInput({
5654
+ * placeholder: 'Search users...',
5655
+ * onSearch: (val) => filterUsers(val)
5656
+ * });
5657
+ */
5658
+ function makeSearchInput(props = {}) {
5659
+ if (typeof props === 'string') props = { placeholder: props };
5660
+ var {
5661
+ placeholder = 'Search...',
5662
+ value,
5663
+ onSearch,
5664
+ onInput,
5665
+ id,
5666
+ name,
5667
+ className = ''
5668
+ } = props;
5669
+
5670
+ return {
5671
+ t: 'div',
5672
+ a: { class: ('bw-search-input ' + className).trim() },
5673
+ c: [
5674
+ {
5675
+ t: 'input',
5676
+ a: {
5677
+ type: 'search',
5678
+ class: 'bw-form-control bw-search-field',
5679
+ placeholder: placeholder,
5680
+ value: value,
5681
+ id: id,
5682
+ name: name,
5683
+ onkeydown: function(e) {
5684
+ if (e.key === 'Enter' && onSearch) {
5685
+ e.preventDefault();
5686
+ onSearch(e.target.value);
5687
+ }
5688
+ },
5689
+ oninput: function(e) {
5690
+ var wrapper = e.target.closest('.bw-search-input');
5691
+ var clearBtn = wrapper.querySelector('.bw-search-clear');
5692
+ if (clearBtn) {
5693
+ clearBtn.style.display = e.target.value ? 'flex' : 'none';
5694
+ }
5695
+ if (onInput) onInput(e.target.value);
5696
+ }
5697
+ }
5698
+ },
5699
+ {
5700
+ t: 'button',
5701
+ a: {
5702
+ type: 'button',
5703
+ class: 'bw-search-clear',
5704
+ 'aria-label': 'Clear search',
5705
+ style: value ? undefined : 'display: none',
5706
+ onclick: function(e) {
5707
+ var wrapper = e.target.closest('.bw-search-input');
5708
+ var input = wrapper.querySelector('.bw-search-field');
5709
+ input.value = '';
5710
+ e.target.style.display = 'none';
5711
+ input.focus();
5712
+ if (onInput) onInput('');
5713
+ if (onSearch) onSearch('');
5714
+ }
5715
+ },
5716
+ c: '\u00D7'
5717
+ }
5718
+ ],
5719
+ o: { type: 'search-input' }
5720
+ };
5721
+ }
5722
+
5723
+ /**
5724
+ * Create a styled range slider input
5725
+ *
5726
+ * @param {Object} [props] - Range configuration
5727
+ * @param {number} [props.min=0] - Minimum value
5728
+ * @param {number} [props.max=100] - Maximum value
5729
+ * @param {number} [props.step=1] - Step increment
5730
+ * @param {number} [props.value=50] - Current value
5731
+ * @param {string} [props.label] - Label text
5732
+ * @param {boolean} [props.showValue=false] - Show current value display
5733
+ * @param {string} [props.id] - Element ID
5734
+ * @param {string} [props.name] - Input name attribute
5735
+ * @param {boolean} [props.disabled=false] - Whether the slider is disabled
5736
+ * @param {string} [props.className] - Additional CSS classes
5737
+ * @returns {Object} TACO object representing a range input
5738
+ * @category Component Builders
5739
+ * @example
5740
+ * const slider = makeRange({
5741
+ * min: 0, max: 100, value: 50,
5742
+ * label: 'Volume',
5743
+ * showValue: true,
5744
+ * oninput: (e) => setVolume(e.target.value)
5745
+ * });
5746
+ */
5747
+ function makeRange(props = {}) {
5748
+ var {
5749
+ min = 0,
5750
+ max = 100,
5751
+ step = 1,
5752
+ value = 50,
5753
+ label,
5754
+ showValue = false,
5755
+ id,
5756
+ name,
5757
+ disabled = false,
5758
+ className = '',
5759
+ ...eventHandlers
5760
+ } = props;
5761
+
5762
+ var children = [];
5763
+
5764
+ if (label || showValue) {
5765
+ var labelContent = [];
5766
+ if (label) {
5767
+ labelContent.push({
5768
+ t: 'span',
5769
+ c: label
5770
+ });
5771
+ }
5772
+ if (showValue) {
5773
+ labelContent.push({
5774
+ t: 'span',
5775
+ a: { class: 'bw-range-value' },
5776
+ c: '' + value
5777
+ });
5778
+ }
5779
+ children.push({
5780
+ t: 'div',
5781
+ a: { class: 'bw-range-label' },
5782
+ c: labelContent
5783
+ });
5784
+ }
5785
+
5786
+ // Wrap oninput to update value display
5787
+ var userOnInput = eventHandlers.oninput;
5788
+ if (showValue) {
5789
+ eventHandlers.oninput = function(e) {
5790
+ var wrapper = e.target.closest('.bw-range-wrapper');
5791
+ var valDisplay = wrapper.querySelector('.bw-range-value');
5792
+ if (valDisplay) valDisplay.textContent = e.target.value;
5793
+ if (userOnInput) userOnInput(e);
5794
+ };
5795
+ }
5796
+
5797
+ children.push({
5798
+ t: 'input',
5799
+ a: {
5800
+ type: 'range',
5801
+ class: 'bw-range',
5802
+ min: min,
5803
+ max: max,
5804
+ step: step,
5805
+ value: value,
5806
+ id: id,
5807
+ name: name,
5808
+ disabled: disabled,
5809
+ ...eventHandlers
5810
+ }
5811
+ });
5812
+
5813
+ return {
5814
+ t: 'div',
5815
+ a: { class: ('bw-range-wrapper ' + className).trim() },
5816
+ c: children,
5817
+ o: { type: 'range' }
5818
+ };
5819
+ }
5820
+
5821
+ /**
5822
+ * Create a media object layout (image + text side-by-side)
5823
+ *
5824
+ * Classic media object pattern: image/icon on one side, text content
5825
+ * on the other, using flexbox. Supports reversed layout.
5826
+ *
5827
+ * @param {Object} [props] - Media object configuration
5828
+ * @param {string} [props.src] - Image source URL
5829
+ * @param {string} [props.alt=""] - Image alt text
5830
+ * @param {string} [props.title] - Title text
5831
+ * @param {string|Object|Array} [props.content] - Body content
5832
+ * @param {boolean} [props.reverse=false] - Put image on the right
5833
+ * @param {string} [props.imageSize="3rem"] - Image width/height
5834
+ * @param {string} [props.className] - Additional CSS classes
5835
+ * @returns {Object} TACO object representing a media object
5836
+ * @category Component Builders
5837
+ * @example
5838
+ * const media = makeMediaObject({
5839
+ * src: '/avatar.jpg',
5840
+ * title: 'Jane Doe',
5841
+ * content: 'Posted a comment 5 minutes ago.'
5842
+ * });
5843
+ */
5844
+ function makeMediaObject(props = {}) {
5845
+ var {
5846
+ src,
5847
+ alt = '',
5848
+ title,
5849
+ content,
5850
+ reverse = false,
5851
+ imageSize = '3rem',
5852
+ className = ''
5853
+ } = props;
5854
+
5855
+ var imgEl = src ? {
5856
+ t: 'img',
5857
+ a: {
5858
+ class: 'bw-media-img',
5859
+ src: src,
5860
+ alt: alt,
5861
+ style: 'width:' + imageSize + ';height:' + imageSize
5862
+ }
5863
+ } : null;
5864
+
5865
+ var bodyEl = {
5866
+ t: 'div',
5867
+ a: { class: 'bw-media-body' },
5868
+ c: [
5869
+ title && { t: 'h5', a: { class: 'bw-media-title' }, c: title },
5870
+ content
5871
+ ].filter(Boolean)
5872
+ };
5873
+
5874
+ return {
5875
+ t: 'div',
5876
+ a: { class: ('bw-media ' + (reverse ? 'bw-media-reverse ' : '') + className).trim() },
5877
+ c: reverse
5878
+ ? [bodyEl, imgEl].filter(Boolean)
5879
+ : [imgEl, bodyEl].filter(Boolean),
5880
+ o: { type: 'media-object' }
5881
+ };
5882
+ }
5883
+
5884
+ /**
5885
+ * Create a file upload zone with drag-and-drop support
5886
+ *
5887
+ * Styled drop zone with file input. Supports drag-and-drop visuals
5888
+ * and multiple file selection.
5889
+ *
5890
+ * @param {Object} [props] - File upload configuration
5891
+ * @param {string} [props.accept] - Accepted file types (e.g. "image/*", ".pdf,.doc")
5892
+ * @param {boolean} [props.multiple=false] - Allow multiple file selection
5893
+ * @param {Function} [props.onFiles] - Callback when files are selected, receives FileList
5894
+ * @param {string} [props.text="Drop files here or click to browse"] - Zone label text
5895
+ * @param {string} [props.id] - Element ID
5896
+ * @param {string} [props.className] - Additional CSS classes
5897
+ * @returns {Object} TACO object representing a file upload zone
5898
+ * @category Component Builders
5899
+ * @example
5900
+ * const upload = makeFileUpload({
5901
+ * accept: 'image/*',
5902
+ * multiple: true,
5903
+ * onFiles: (files) => uploadFiles(files)
5904
+ * });
5905
+ */
5906
+ function makeFileUpload(props = {}) {
5907
+ var {
5908
+ accept,
5909
+ multiple = false,
5910
+ onFiles,
5911
+ text = 'Drop files here or click to browse',
5912
+ id,
5913
+ className = ''
5914
+ } = props;
5915
+
5916
+ return {
5917
+ t: 'div',
5918
+ a: {
5919
+ class: ('bw-file-upload ' + className).trim(),
5920
+ tabindex: '0',
5921
+ role: 'button',
5922
+ 'aria-label': text
5923
+ },
5924
+ c: [
5925
+ { t: 'div', a: { class: 'bw-file-upload-icon' }, c: '\uD83D\uDCC1' },
5926
+ { t: 'div', a: { class: 'bw-file-upload-text' }, c: text },
5927
+ {
5928
+ t: 'input',
5929
+ a: {
5930
+ type: 'file',
5931
+ class: 'bw-file-upload-input',
5932
+ accept: accept,
5933
+ multiple: multiple,
5934
+ id: id,
5935
+ onchange: function(e) {
5936
+ if (onFiles && e.target.files.length) onFiles(e.target.files);
5937
+ }
5938
+ }
5939
+ }
5940
+ ],
5941
+ o: {
5942
+ type: 'file-upload',
5943
+ mounted: function(el) {
5944
+ var input = el.querySelector('.bw-file-upload-input');
5945
+
5946
+ // Click zone to trigger file input
5947
+ el.addEventListener('click', function(e) {
5948
+ if (e.target !== input) input.click();
5949
+ });
5950
+
5951
+ // Keyboard activation
5952
+ el.addEventListener('keydown', function(e) {
5953
+ if (e.key === 'Enter' || e.key === ' ') {
5954
+ e.preventDefault();
5955
+ input.click();
5956
+ }
5957
+ });
5958
+
5959
+ // Drag-and-drop visuals
5960
+ el.addEventListener('dragover', function(e) {
5961
+ e.preventDefault();
5962
+ el.classList.add('bw-file-upload-active');
5963
+ });
5964
+ el.addEventListener('dragleave', function() {
5965
+ el.classList.remove('bw-file-upload-active');
5966
+ });
5967
+ el.addEventListener('drop', function(e) {
5968
+ e.preventDefault();
5969
+ el.classList.remove('bw-file-upload-active');
5970
+ if (onFiles && e.dataTransfer.files.length) onFiles(e.dataTransfer.files);
5971
+ });
5972
+ }
5973
+ }
5974
+ };
5975
+ }
5976
+
5977
+ // =========================================================================
5978
+ // Phase 7: Data Display & Workflow
5979
+ // =========================================================================
5980
+
5981
+ /**
5982
+ * Create a vertical timeline for chronological event display
5983
+ *
5984
+ * Renders events as a vertical line with markers and content cards.
5985
+ * Each item can have a colored variant marker.
5986
+ *
5987
+ * @param {Object} [props] - Timeline configuration
5988
+ * @param {Array<Object>} [props.items=[]] - Timeline events
5989
+ * @param {string} [props.items[].title] - Event title
5990
+ * @param {string|Object|Array} [props.items[].content] - Event description content
5991
+ * @param {string} [props.items[].date] - Date or time label
5992
+ * @param {string} [props.items[].variant="primary"] - Marker color variant
5993
+ * @param {string} [props.className] - Additional CSS classes
5994
+ * @returns {Object} TACO object representing a timeline
5995
+ * @category Component Builders
5996
+ * @example
5997
+ * const timeline = makeTimeline({
5998
+ * items: [
5999
+ * { title: 'Project Started', date: 'Jan 2026', variant: 'primary' },
6000
+ * { title: 'Beta Release', date: 'Mar 2026', content: 'v2.0 beta shipped' },
6001
+ * { title: 'Stable Release', date: 'Jun 2026', variant: 'success' }
6002
+ * ]
6003
+ * });
6004
+ */
6005
+ function makeTimeline(props = {}) {
6006
+ var {
6007
+ items = [],
6008
+ className = ''
6009
+ } = props;
6010
+
6011
+ return {
6012
+ t: 'div',
6013
+ a: { class: ('bw-timeline ' + className).trim() },
6014
+ c: items.map(function(item) {
6015
+ return {
6016
+ t: 'div',
6017
+ a: { class: 'bw-timeline-item' },
6018
+ c: [
6019
+ {
6020
+ t: 'div',
6021
+ a: { class: 'bw-timeline-marker bw-timeline-marker-' + (item.variant || 'primary') }
6022
+ },
6023
+ {
6024
+ t: 'div',
6025
+ a: { class: 'bw-timeline-content' },
6026
+ c: [
6027
+ item.date && {
6028
+ t: 'div',
6029
+ a: { class: 'bw-timeline-date' },
6030
+ c: item.date
6031
+ },
6032
+ item.title && {
6033
+ t: 'h5',
6034
+ a: { class: 'bw-timeline-title' },
6035
+ c: item.title
6036
+ },
6037
+ item.content && (typeof item.content === 'string'
6038
+ ? { t: 'p', a: { class: 'bw-timeline-text' }, c: item.content }
6039
+ : item.content)
6040
+ ].filter(Boolean)
6041
+ }
6042
+ ]
6043
+ };
6044
+ }),
6045
+ o: { type: 'timeline' }
6046
+ };
6047
+ }
6048
+
6049
+ /**
6050
+ * Create a multi-step wizard/progress indicator
6051
+ *
6052
+ * Displays numbered steps with active and completed states.
6053
+ * Steps before currentStep are marked completed, the currentStep
6054
+ * is active, and subsequent steps are pending.
6055
+ *
6056
+ * @param {Object} [props] - Stepper configuration
6057
+ * @param {Array<Object>} [props.steps=[]] - Step definitions
6058
+ * @param {string} [props.steps[].label] - Step label text
6059
+ * @param {string} [props.steps[].description] - Optional step description
6060
+ * @param {number} [props.currentStep=0] - Zero-based index of the active step
6061
+ * @param {string} [props.className] - Additional CSS classes
6062
+ * @returns {Object} TACO object representing a stepper
6063
+ * @category Component Builders
6064
+ * @example
6065
+ * const stepper = makeStepper({
6066
+ * currentStep: 1,
6067
+ * steps: [
6068
+ * { label: 'Account', description: 'Create account' },
6069
+ * { label: 'Profile', description: 'Set up profile' },
6070
+ * { label: 'Confirm', description: 'Review & submit' }
6071
+ * ]
6072
+ * });
6073
+ */
6074
+ function makeStepper(props = {}) {
6075
+ var {
6076
+ steps = [],
6077
+ currentStep = 0,
6078
+ className = ''
6079
+ } = props;
6080
+
6081
+ return {
6082
+ t: 'div',
6083
+ a: { class: ('bw-stepper ' + className).trim(), role: 'list' },
6084
+ c: steps.map(function(step, index) {
6085
+ var state = index < currentStep ? 'completed' : index === currentStep ? 'active' : 'pending';
6086
+ return {
6087
+ t: 'div',
6088
+ a: {
6089
+ class: 'bw-step bw-step-' + state,
6090
+ role: 'listitem',
6091
+ 'aria-current': state === 'active' ? 'step' : undefined
6092
+ },
6093
+ c: [
6094
+ {
6095
+ t: 'div',
6096
+ a: { class: 'bw-step-indicator' },
6097
+ c: state === 'completed' ? '\u2713' : '' + (index + 1)
6098
+ },
6099
+ {
6100
+ t: 'div',
6101
+ a: { class: 'bw-step-body' },
6102
+ c: [
6103
+ { t: 'div', a: { class: 'bw-step-label' }, c: step.label },
6104
+ step.description && { t: 'div', a: { class: 'bw-step-description' }, c: step.description }
6105
+ ].filter(Boolean)
6106
+ }
6107
+ ]
6108
+ };
6109
+ }),
6110
+ o: { type: 'stepper' }
6111
+ };
6112
+ }
6113
+
6114
+ /**
6115
+ * Create a chip/tag input for managing a list of items
6116
+ *
6117
+ * Displays existing chips with remove buttons and an input field
6118
+ * for adding new ones. Chips are added on Enter and removed on
6119
+ * clicking the × button.
6120
+ *
6121
+ * @param {Object} [props] - Chip input configuration
6122
+ * @param {Array<string>} [props.chips=[]] - Initial chip values
6123
+ * @param {string} [props.placeholder="Add..."] - Input placeholder text
6124
+ * @param {Function} [props.onAdd] - Callback when a chip is added, receives value
6125
+ * @param {Function} [props.onRemove] - Callback when a chip is removed, receives value
6126
+ * @param {string} [props.className] - Additional CSS classes
6127
+ * @returns {Object} TACO object representing a chip input
6128
+ * @category Component Builders
6129
+ * @example
6130
+ * const tags = makeChipInput({
6131
+ * chips: ['JavaScript', 'CSS'],
6132
+ * placeholder: 'Add tag...',
6133
+ * onAdd: (val) => addTag(val),
6134
+ * onRemove: (val) => removeTag(val)
6135
+ * });
6136
+ */
6137
+ function makeChipInput(props = {}) {
6138
+ var {
6139
+ chips = [],
6140
+ placeholder = 'Add...',
6141
+ onAdd,
6142
+ onRemove,
6143
+ className = ''
6144
+ } = props;
6145
+
6146
+ function makeChipEl(text) {
6147
+ return {
6148
+ t: 'span',
6149
+ a: { class: 'bw-chip', 'data-chip-value': text },
6150
+ c: [
6151
+ text,
6152
+ {
6153
+ t: 'button',
6154
+ a: {
6155
+ type: 'button',
6156
+ class: 'bw-chip-remove',
6157
+ 'aria-label': 'Remove ' + text,
6158
+ onclick: function(e) {
6159
+ var chip = e.target.closest('.bw-chip');
6160
+ var val = chip.getAttribute('data-chip-value');
6161
+ chip.parentNode.removeChild(chip);
6162
+ if (onRemove) onRemove(val);
6163
+ }
6164
+ },
6165
+ c: '\u00D7'
6166
+ }
6167
+ ]
6168
+ };
6169
+ }
6170
+
6171
+ return {
6172
+ t: 'div',
6173
+ a: { class: ('bw-chip-input ' + className).trim() },
6174
+ c: [
6175
+ ...chips.map(makeChipEl),
6176
+ {
6177
+ t: 'input',
6178
+ a: {
6179
+ type: 'text',
6180
+ class: 'bw-chip-field',
6181
+ placeholder: placeholder,
6182
+ onkeydown: function(e) {
6183
+ if (e.key === 'Enter' && e.target.value.trim()) {
6184
+ e.preventDefault();
6185
+ var val = e.target.value.trim();
6186
+ var wrapper = e.target.closest('.bw-chip-input');
6187
+ // Insert chip before the input
6188
+ var chipEl = document.createElement('span');
6189
+ chipEl.className = 'bw-chip';
6190
+ chipEl.setAttribute('data-chip-value', val);
6191
+ chipEl.innerHTML = '';
6192
+ chipEl.textContent = val;
6193
+ var removeBtn = document.createElement('button');
6194
+ removeBtn.type = 'button';
6195
+ removeBtn.className = 'bw-chip-remove';
6196
+ removeBtn.setAttribute('aria-label', 'Remove ' + val);
6197
+ removeBtn.textContent = '\u00D7';
6198
+ removeBtn.onclick = function() {
6199
+ chipEl.parentNode.removeChild(chipEl);
6200
+ if (onRemove) onRemove(val);
6201
+ };
6202
+ chipEl.appendChild(removeBtn);
6203
+ wrapper.insertBefore(chipEl, e.target);
6204
+ e.target.value = '';
6205
+ if (onAdd) onAdd(val);
6206
+ }
6207
+ // Backspace on empty input removes last chip
6208
+ if (e.key === 'Backspace' && !e.target.value) {
6209
+ var wrapper = e.target.closest('.bw-chip-input');
6210
+ var chipEls = wrapper.querySelectorAll('.bw-chip');
6211
+ if (chipEls.length) {
6212
+ var last = chipEls[chipEls.length - 1];
6213
+ var removedVal = last.getAttribute('data-chip-value');
6214
+ last.parentNode.removeChild(last);
6215
+ if (onRemove) onRemove(removedVal);
6216
+ }
6217
+ }
6218
+ }
6219
+ }
6220
+ }
6221
+ ],
6222
+ o: { type: 'chip-input' }
6223
+ };
6224
+ }
6225
+
6226
+ const componentHandles = {
6227
+ card: CardHandle,
6228
+ table: TableHandle,
6229
+ navbar: NavbarHandle,
6230
+ tabs: TabsHandle,
6231
+ modal: ModalHandle
6232
+ };
6233
+
6234
+ var components = /*#__PURE__*/Object.freeze({
6235
+ __proto__: null,
6236
+ CardHandle: CardHandle,
6237
+ ModalHandle: ModalHandle,
6238
+ NavbarHandle: NavbarHandle,
6239
+ TableHandle: TableHandle,
6240
+ TabsHandle: TabsHandle,
6241
+ componentHandles: componentHandles,
6242
+ makeAccordion: makeAccordion,
6243
+ makeAlert: makeAlert,
6244
+ makeAvatar: makeAvatar,
6245
+ makeBadge: makeBadge,
6246
+ makeBreadcrumb: makeBreadcrumb,
6247
+ makeButton: makeButton,
6248
+ makeButtonGroup: makeButtonGroup,
6249
+ makeCTA: makeCTA,
6250
+ makeCard: makeCard,
6251
+ makeCarousel: makeCarousel,
6252
+ makeCheckbox: makeCheckbox,
6253
+ makeChipInput: makeChipInput,
6254
+ makeCodeDemo: makeCodeDemo,
6255
+ makeCol: makeCol,
6256
+ makeContainer: makeContainer,
6257
+ makeDropdown: makeDropdown,
6258
+ makeFeatureGrid: makeFeatureGrid,
6259
+ makeFileUpload: makeFileUpload,
6260
+ makeForm: makeForm,
6261
+ makeFormGroup: makeFormGroup,
6262
+ makeHero: makeHero,
6263
+ makeInput: makeInput,
6264
+ makeListGroup: makeListGroup,
6265
+ makeMediaObject: makeMediaObject,
6266
+ makeModal: makeModal,
6267
+ makeNav: makeNav,
6268
+ makeNavbar: makeNavbar,
6269
+ makePagination: makePagination,
6270
+ makePopover: makePopover,
6271
+ makeProgress: makeProgress,
6272
+ makeRadio: makeRadio,
6273
+ makeRange: makeRange,
6274
+ makeRow: makeRow,
6275
+ makeSearchInput: makeSearchInput,
6276
+ makeSection: makeSection,
6277
+ makeSelect: makeSelect,
6278
+ makeSkeleton: makeSkeleton,
6279
+ makeSpinner: makeSpinner,
6280
+ makeStack: makeStack,
6281
+ makeStatCard: makeStatCard,
6282
+ makeStepper: makeStepper,
6283
+ makeSwitch: makeSwitch,
6284
+ makeTabs: makeTabs,
6285
+ makeTextarea: makeTextarea,
6286
+ makeTimeline: makeTimeline,
6287
+ makeToast: makeToast,
6288
+ makeTooltip: makeTooltip
6289
+ });
6290
+
6291
+ /**
6292
+ * Bitwrench v2 Core
6293
+ * Zero-dependency UI library using JavaScript objects
6294
+ * Works in browsers (IE11+) and Node.js
6295
+ *
6296
+ * @license BSD-2-Clause
6297
+ * @author M A Chatterjee <deftio [at] deftio [dot] com>
6298
+ */
6299
+
6300
+
6301
+ // Environment-aware module loader for optional Node.js built-ins (fs).
6302
+ // Strategy: try require() first (CJS/UMD), fall back to import() (ESM).
6303
+ // import() is wrapped in Function() to avoid parse errors in ES5/IE11 environments.
6304
+
6305
+ // Core bitwrench namespace
6306
+ const bw = {
6307
+ // Version info from generated file
6308
+ version: VERSION_INFO.version,
6309
+ versionInfo: VERSION_INFO,
6310
+
6311
+ /**
5078
6312
  * Get version metadata object (v1-compatible callable API).
5079
6313
  *
5080
6314
  * Returns a copy of the build-time version info including version string,
@@ -6672,8 +7906,10 @@ bw.u = {
6672
7906
  /**
6673
7907
  * Generate responsive CSS with media query breakpoints.
6674
7908
  *
6675
- * Produces a CSS string with `@media` rules for sm (640px), md (768px),
6676
- * lg (1024px), and xl (1280px) breakpoints. Pass the result to `bw.injectCSS()`.
7909
+ * Produces a CSS string with `@media (min-width)` rules for standard
7910
+ * breakpoints. These match the grid system and theme.breakpoints:
7911
+ * sm: 576px, md: 768px, lg: 992px, xl: 1200px
7912
+ * Pass the result to `bw.injectCSS()`.
6677
7913
  *
6678
7914
  * @param {string} selector - CSS selector
6679
7915
  * @param {Object} breakpoints - Object with keys: base, sm, md, lg, xl
@@ -6690,7 +7926,7 @@ bw.u = {
6690
7926
  * bw.injectCSS(css);
6691
7927
  */
6692
7928
  bw.responsive = function(selector, breakpoints) {
6693
- var sizes = { sm: '640px', md: '768px', lg: '1024px', xl: '1280px' };
7929
+ var sizes = { sm: '576px', md: '768px', lg: '992px', xl: '1200px' };
6694
7930
  var parts = [];
6695
7931
  Object.keys(breakpoints).forEach(function(key) {
6696
7932
  var rules = {};
@@ -6824,7 +8060,8 @@ if (bw._isBrowser) {
6824
8060
  * @returns {Element|null} Style element if in browser, null in Node.js
6825
8061
  * @category CSS & Styling
6826
8062
  * @see bw.setTheme
6827
- * @see bw.toggleDarkMode
8063
+ * @see bw.applyTheme
8064
+ * @see bw.toggleTheme
6828
8065
  * @example
6829
8066
  * bw.loadDefaultStyles(); // inject all default CSS
6830
8067
  */
@@ -6891,53 +8128,6 @@ bw.setTheme = function(overrides, options = {}) {
6891
8128
  return bw.getTheme();
6892
8129
  };
6893
8130
 
6894
- /**
6895
- * Toggle dark mode on/off.
6896
- *
6897
- * Adds/removes the `bw-dark` class on `<html>` and injects dark mode CSS
6898
- * overrides. Pass `true`/`false` to force a mode, or omit to toggle.
6899
- *
6900
- * @param {boolean} [force] - Force dark (true) or light (false). Omit to toggle.
6901
- * @returns {boolean} Whether dark mode is now active
6902
- * @category CSS & Styling
6903
- * @see bw.setTheme
6904
- * @example
6905
- * bw.toggleDarkMode(); // toggle
6906
- * bw.toggleDarkMode(true); // force dark
6907
- * bw.toggleDarkMode(false); // force light
6908
- */
6909
- bw.toggleDarkMode = function(force) {
6910
- const isDark = force !== undefined ? force : !theme.darkMode;
6911
- theme.darkMode = isDark;
6912
-
6913
- if (bw._isBrowser) {
6914
- const root = document.documentElement;
6915
- if (isDark) {
6916
- root.classList.add('bw-dark');
6917
- // Generate palette-aware dark mode CSS, or fall back to default
6918
- var palette = bw._activePalette || derivePalette(DEFAULT_PALETTE_CONFIG);
6919
- var darkRules = generateDarkModeCSS(palette);
6920
- var darkCSS = bw.css(darkRules);
6921
-
6922
- // Remove existing dark styles to allow regeneration
6923
- var existing = document.getElementById('bw-dark-styles');
6924
- if (existing) existing.remove();
6925
-
6926
- var styleEl = document.createElement('style');
6927
- styleEl.id = 'bw-dark-styles';
6928
- styleEl.textContent = darkCSS;
6929
- document.head.appendChild(styleEl);
6930
- } else {
6931
- root.classList.remove('bw-dark');
6932
- // Remove dark mode styles when switching to light
6933
- var darkEl = document.getElementById('bw-dark-styles');
6934
- if (darkEl) darkEl.remove();
6935
- }
6936
- }
6937
-
6938
- return isDark;
6939
- };
6940
-
6941
8131
  /**
6942
8132
  * Generate a complete, scoped theme from seed colors.
6943
8133
  *
@@ -6960,13 +8150,19 @@ bw.toggleDarkMode = function(force) {
6960
8150
  * @param {string} [config.spacing='normal'] - 'compact' | 'normal' | 'spacious'
6961
8151
  * @param {string} [config.radius='md'] - 'none' | 'sm' | 'md' | 'lg' | 'pill'
6962
8152
  * @param {number} [config.fontSize=1.0] - Base font size scale factor
8153
+ * @param {string|number} [config.typeRatio='normal'] - 'tight' | 'normal' | 'relaxed' | 'dramatic' or a number
8154
+ * @param {string} [config.elevation='md'] - 'flat' | 'sm' | 'md' | 'lg'
8155
+ * @param {string} [config.motion='standard'] - 'reduced' | 'standard' | 'expressive'
8156
+ * @param {number} [config.harmonize=0.20] - 0-1, semantic color hue shift toward primary
6963
8157
  * @param {boolean} [config.inject=true] - Inject into DOM (browser only)
6964
- * @returns {Object} { css, palette, name }
8158
+ * @returns {Object} { css, palette, name, isLightPrimary, alternate: { css, palette } }
6965
8159
  * @category CSS & Styling
8160
+ * @see bw.applyTheme
8161
+ * @see bw.toggleTheme
6966
8162
  * @see bw.loadDefaultStyles
6967
8163
  * @example
6968
- * // Generate and inject an ocean theme
6969
- * bw.generateTheme('ocean', {
8164
+ * // Generate and inject an ocean theme (primary + alternate)
8165
+ * var theme = bw.generateTheme('ocean', {
6970
8166
  * primary: '#0077b6',
6971
8167
  * secondary: '#90e0ef',
6972
8168
  * tertiary: '#00b4d8'
@@ -6975,14 +8171,16 @@ bw.toggleDarkMode = function(force) {
6975
8171
  * // Apply to a container
6976
8172
  * document.getElementById('app').classList.add('ocean');
6977
8173
  *
8174
+ * // Toggle to alternate palette
8175
+ * bw.toggleTheme();
8176
+ *
6978
8177
  * // Generate CSS for static export (Node.js)
6979
8178
  * var result = bw.generateTheme('sunset', {
6980
8179
  * primary: '#e76f51',
6981
8180
  * secondary: '#264653',
6982
- * tertiary: '#e9c46a',
6983
8181
  * inject: false
6984
8182
  * });
6985
- * fs.writeFileSync('sunset.css', result.css);
8183
+ * fs.writeFileSync('sunset.css', result.css + result.alternate.css);
6986
8184
  */
6987
8185
  bw.generateTheme = function(name, config) {
6988
8186
  if (!config || !config.primary || !config.secondary) {
@@ -6993,29 +8191,37 @@ bw.generateTheme = function(name, config) {
6993
8191
  var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config);
6994
8192
  if (!config.tertiary) fullConfig.tertiary = fullConfig.primary;
6995
8193
 
6996
- // Derive palette
8194
+ // Derive primary palette
6997
8195
  var palette = derivePalette(fullConfig);
6998
8196
 
6999
- // Store active palette for dark mode
7000
- bw._activePalette = palette;
7001
-
7002
8197
  // Resolve layout
7003
8198
  var layout = resolveLayout(fullConfig);
7004
8199
 
7005
- // Generate themed CSS rules
8200
+ // Generate primary themed CSS rules
7006
8201
  var themedRules = generateThemedCSS(name, palette, layout);
7007
-
7008
- // Add underscore aliases
7009
8202
  var aliasedRules = addUnderscoreAliases(themedRules);
7010
-
7011
- // Convert to CSS string
7012
8203
  var cssStr = bw.css(aliasedRules);
7013
8204
 
7014
- // Inject into DOM if requested and in browser
8205
+ // Derive alternate palette (luminance-inverted)
8206
+ var altConfig = deriveAlternateConfig(fullConfig);
8207
+ var altPalette = derivePalette(altConfig);
8208
+
8209
+ // Generate alternate CSS scoped under .bw-theme-alt
8210
+ var altRules = generateAlternateCSS(name, altPalette, layout);
8211
+ var aliasedAltRules = addUnderscoreAliases(altRules);
8212
+ var altCssStr = bw.css(aliasedAltRules);
8213
+
8214
+ // Determine if primary is light-flavored
8215
+ var lightPrimary = isLightPalette(fullConfig);
8216
+
8217
+ // Inject both CSS sets into DOM if requested
7015
8218
  var shouldInject = config.inject !== false;
7016
8219
  if (shouldInject && bw._isBrowser) {
7017
8220
  var styleId = name ? 'bw-theme-' + name : 'bw-theme-default';
7018
8221
  bw.injectCSS(cssStr, { id: styleId, append: false });
8222
+
8223
+ var altStyleId = name ? 'bw-theme-' + name + '-alt' : 'bw-theme-default-alt';
8224
+ bw.injectCSS(altCssStr, { id: altStyleId, append: false });
7019
8225
  }
7020
8226
 
7021
8227
  // Update bw.u color entries to reflect the palette
@@ -7026,7 +8232,72 @@ bw.generateTheme = function(name, config) {
7026
8232
  bw.u.textWhite = { color: '#ffffff' };
7027
8233
  }
7028
8234
 
7029
- return { css: cssStr, palette: palette, name: name };
8235
+ // Store active theme state
8236
+ var result = {
8237
+ css: cssStr,
8238
+ palette: palette,
8239
+ name: name,
8240
+ isLightPrimary: lightPrimary,
8241
+ alternate: {
8242
+ css: altCssStr,
8243
+ palette: altPalette
8244
+ }
8245
+ };
8246
+ bw._activeTheme = result;
8247
+ bw._activeThemeMode = 'primary';
8248
+
8249
+ return result;
8250
+ };
8251
+
8252
+ /**
8253
+ * Apply a theme mode. Switches between primary and alternate palettes
8254
+ * by adding/removing the `bw-theme-alt` class on `<html>`.
8255
+ *
8256
+ * @param {string} mode - 'primary' | 'alternate' | 'light' | 'dark'
8257
+ * @returns {string} Active mode: 'primary' or 'alternate'
8258
+ * @category CSS & Styling
8259
+ * @see bw.generateTheme
8260
+ * @see bw.toggleTheme
8261
+ * @example
8262
+ * bw.applyTheme('alternate'); // switch to alternate palette
8263
+ * bw.applyTheme('dark'); // switch to whichever palette is darker
8264
+ * bw.applyTheme('primary'); // switch back to primary palette
8265
+ */
8266
+ bw.applyTheme = function(mode) {
8267
+ if (!bw._isBrowser) return mode || 'primary';
8268
+ var root = document.documentElement;
8269
+ var isLight = bw._activeTheme ? bw._activeTheme.isLightPrimary : true;
8270
+
8271
+ var wantAlt;
8272
+ if (mode === 'primary') wantAlt = false;
8273
+ else if (mode === 'alternate') wantAlt = true;
8274
+ else if (mode === 'light') wantAlt = !isLight;
8275
+ else if (mode === 'dark') wantAlt = isLight;
8276
+ else wantAlt = false;
8277
+
8278
+ if (wantAlt) {
8279
+ root.classList.add('bw-theme-alt');
8280
+ } else {
8281
+ root.classList.remove('bw-theme-alt');
8282
+ }
8283
+
8284
+ bw._activeThemeMode = wantAlt ? 'alternate' : 'primary';
8285
+ return bw._activeThemeMode;
8286
+ };
8287
+
8288
+ /**
8289
+ * Toggle between primary and alternate theme palettes.
8290
+ *
8291
+ * @returns {string} Active mode after toggle: 'primary' or 'alternate'
8292
+ * @category CSS & Styling
8293
+ * @see bw.applyTheme
8294
+ * @see bw.generateTheme
8295
+ * @example
8296
+ * bw.toggleTheme(); // flip between primary and alternate
8297
+ */
8298
+ bw.toggleTheme = function() {
8299
+ var current = bw._activeThemeMode || 'primary';
8300
+ return bw.applyTheme(current === 'primary' ? 'alternate' : 'primary');
7030
8301
  };
7031
8302
 
7032
8303
  // Expose color utility functions on bw namespace
@@ -7038,10 +8309,18 @@ bw.relativeLuminance = relativeLuminance;
7038
8309
  bw.textOnColor = textOnColor;
7039
8310
  bw.deriveShades = deriveShades;
7040
8311
  bw.derivePalette = derivePalette;
8312
+ bw.harmonize = harmonize;
8313
+ bw.deriveAlternateSeed = deriveAlternateSeed;
8314
+ bw.deriveAlternateConfig = deriveAlternateConfig;
8315
+ bw.isLightPalette = isLightPalette;
7041
8316
 
7042
8317
  // Expose layout and theme presets
7043
8318
  bw.SPACING_PRESETS = SPACING_PRESETS;
7044
8319
  bw.RADIUS_PRESETS = RADIUS_PRESETS;
8320
+ bw.TYPE_RATIO_PRESETS = TYPE_RATIO_PRESETS;
8321
+ bw.ELEVATION_PRESETS = ELEVATION_PRESETS;
8322
+ bw.MOTION_PRESETS = MOTION_PRESETS;
8323
+ bw.generateTypeScale = generateTypeScale;
7045
8324
  bw.DEFAULT_PALETTE_CONFIG = DEFAULT_PALETTE_CONFIG;
7046
8325
  bw.THEME_PRESETS = THEME_PRESETS;
7047
8326
 
@@ -8083,9 +9362,13 @@ bw.copyToClipboard = function(text) {
8083
9362
  /**
8084
9363
  * Create a sortable TACO table from an array of row objects.
8085
9364
  *
9365
+ * Returns a bare `<table>` TACO — no wrapper, title, or responsive scroll.
9366
+ * Use this when you need full control over table placement, or when embedding
9367
+ * the table inside your own layout. For a ready-to-use table with title,
9368
+ * responsive wrapper, and defaults (striped + hover), use `bw.makeDataTable()`.
9369
+ *
8086
9370
  * Auto-detects columns from data keys if not specified. Supports click-to-sort
8087
- * headers with ascending/descending indicators. Returns a TACO object —
8088
- * render with `bw.DOM()` or `bw.html()`.
9371
+ * headers with ascending/descending indicators.
8089
9372
  *
8090
9373
  * @param {Object} config - Table configuration
8091
9374
  * @param {Array<Object>} config.data - Array of row objects to display
@@ -8385,10 +9668,12 @@ bw.makeBarChart = function(config) {
8385
9668
  };
8386
9669
 
8387
9670
  /**
8388
- * Create a responsive data table with title and optional wrapper
9671
+ * Create a ready-to-use data table with title and responsive wrapper.
8389
9672
  *
8390
- * Wraps bw.makeTable() output in a responsive container div.
8391
- * Adds an optional title heading above the table.
9673
+ * Convenience wrapper around `bw.makeTable()` that adds a title heading,
9674
+ * responsive horizontal scroll container, and defaults to striped + hover.
9675
+ * Use this for the common case; use `bw.makeTable()` when you need a bare
9676
+ * table element with no wrapper.
8392
9677
  *
8393
9678
  * @param {Object} config - Table configuration
8394
9679
  * @param {string} [config.title] - Table title heading