bitwrench 2.0.10 → 2.0.12

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.10 | BSD-2-Clause | http://deftio.com/bitwrench */
1
+ /*! bitwrench v2.0.12 | 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.10',
10
+ version: '2.0.12',
11
11
  name: 'bitwrench',
12
12
  description: 'A library for javascript UI functions.',
13
13
  license: 'BSD-2-Clause',
14
- homepage: 'http://deftio.com/bitwrench',
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-07T03:14:16.606Z'
17
+ buildDate: '2026-03-07T22:31:35.755Z'
18
18
  };
19
19
 
20
20
  /**
@@ -686,7 +686,7 @@ function generateTables(scope, palette, layout) {
686
686
  'background-color': palette.light.light
687
687
  };
688
688
  rules[scopeSelector(scope, '.bw-table-striped > tbody > tr:nth-of-type(odd) > *')] = {
689
- 'background-color': 'rgba(0, 0, 0, 0.025)'
689
+ 'background-color': 'rgba(0, 0, 0, 0.05)'
690
690
  };
691
691
  rules[scopeSelector(scope, '.bw-table-hover > tbody > tr:hover > *')] = {
692
692
  'background-color': palette.primary.focus
@@ -880,6 +880,142 @@ function generateSectionsThemed(scope, palette) {
880
880
  return rules;
881
881
  }
882
882
 
883
+ function generateAccordionThemed(scope, palette) {
884
+ var rules = {};
885
+ rules[scopeSelector(scope, '.bw-accordion-item')] = {
886
+ 'background-color': '#fff',
887
+ 'border-color': palette.light.border
888
+ };
889
+ rules[scopeSelector(scope, '.bw-accordion-button')] = {
890
+ 'color': palette.dark.base
891
+ };
892
+ rules[scopeSelector(scope, '.bw-accordion-button:not(.bw-collapsed)')] = {
893
+ 'color': palette.primary.darkText,
894
+ 'background-color': palette.primary.light
895
+ };
896
+ rules[scopeSelector(scope, '.bw-accordion-button:hover')] = {
897
+ 'background-color': palette.light.light
898
+ };
899
+ rules[scopeSelector(scope, '.bw-accordion-button:not(.bw-collapsed):hover')] = {
900
+ 'background-color': palette.primary.hover
901
+ };
902
+ rules[scopeSelector(scope, '.bw-accordion-button:focus-visible')] = {
903
+ 'box-shadow': '0 0 0 0.2rem ' + palette.primary.focus
904
+ };
905
+ rules[scopeSelector(scope, '.bw-accordion-body')] = {
906
+ 'border-top': '1px solid ' + palette.light.border
907
+ };
908
+ return rules;
909
+ }
910
+
911
+ function generateCarouselThemed(scope, palette) {
912
+ var rules = {};
913
+ rules[scopeSelector(scope, '.bw-carousel')] = {
914
+ 'background-color': palette.light.light
915
+ };
916
+ rules[scopeSelector(scope, '.bw-carousel-indicator.active')] = {
917
+ 'background-color': palette.primary.base
918
+ };
919
+ return rules;
920
+ }
921
+
922
+ function generateModalThemed(scope, palette) {
923
+ var rules = {};
924
+ rules[scopeSelector(scope, '.bw-modal-content')] = {
925
+ 'background-color': '#fff',
926
+ 'border-color': palette.light.border,
927
+ 'box-shadow': '0 0.5rem 1rem rgba(0,0,0,0.15)'
928
+ };
929
+ rules[scopeSelector(scope, '.bw-modal-header')] = {
930
+ 'border-bottom-color': palette.light.border
931
+ };
932
+ rules[scopeSelector(scope, '.bw-modal-footer')] = {
933
+ 'border-top-color': palette.light.border
934
+ };
935
+ rules[scopeSelector(scope, '.bw-modal-title')] = {
936
+ 'color': palette.dark.base
937
+ };
938
+ return rules;
939
+ }
940
+
941
+ function generateToastThemed(scope, palette) {
942
+ var rules = {};
943
+ rules[scopeSelector(scope, '.bw-toast')] = {
944
+ 'background-color': '#fff',
945
+ 'border-color': 'rgba(0,0,0,0.1)',
946
+ 'box-shadow': '0 0.5rem 1rem rgba(0,0,0,0.15)'
947
+ };
948
+ rules[scopeSelector(scope, '.bw-toast-header')] = {
949
+ 'border-bottom-color': 'rgba(0,0,0,0.05)'
950
+ };
951
+ var variants = ['primary', 'secondary', 'success', 'danger', 'warning', 'info'];
952
+ variants.forEach(function(v) {
953
+ rules[scopeSelector(scope, '.bw-toast-' + v)] = {
954
+ 'border-left': '4px solid ' + palette[v].base
955
+ };
956
+ });
957
+ return rules;
958
+ }
959
+
960
+ function generateDropdownThemed(scope, palette) {
961
+ var rules = {};
962
+ rules[scopeSelector(scope, '.bw-dropdown-menu')] = {
963
+ 'background-color': '#fff',
964
+ 'border-color': palette.light.border,
965
+ 'box-shadow': '0 0.5rem 1rem rgba(0,0,0,0.15)'
966
+ };
967
+ rules[scopeSelector(scope, '.bw-dropdown-item')] = {
968
+ 'color': palette.dark.base
969
+ };
970
+ rules[scopeSelector(scope, '.bw-dropdown-item:hover')] = {
971
+ 'color': palette.dark.hover,
972
+ 'background-color': palette.light.light
973
+ };
974
+ rules[scopeSelector(scope, '.bw-dropdown-item.disabled')] = {
975
+ 'color': palette.secondary.base
976
+ };
977
+ rules[scopeSelector(scope, '.bw-dropdown-divider')] = {
978
+ 'border-top-color': palette.light.border
979
+ };
980
+ return rules;
981
+ }
982
+
983
+ function generateSwitchThemed(scope, palette) {
984
+ var rules = {};
985
+ rules[scopeSelector(scope, '.bw-form-switch .bw-switch-input')] = {
986
+ 'background-color': palette.secondary.base,
987
+ 'border-color': palette.secondary.base
988
+ };
989
+ rules[scopeSelector(scope, '.bw-form-switch .bw-switch-input:checked')] = {
990
+ 'background-color': palette.primary.base,
991
+ 'border-color': palette.primary.base
992
+ };
993
+ rules[scopeSelector(scope, '.bw-form-switch .bw-switch-input:focus')] = {
994
+ 'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
995
+ };
996
+ return rules;
997
+ }
998
+
999
+ function generateSkeletonThemed(scope, palette) {
1000
+ var rules = {};
1001
+ rules[scopeSelector(scope, '.bw-skeleton')] = {
1002
+ 'background': 'linear-gradient(90deg, ' + palette.light.border + ' 25%, ' + palette.light.light + ' 37%, ' + palette.light.border + ' 63%)'
1003
+ };
1004
+ return rules;
1005
+ }
1006
+
1007
+ function generateAvatarThemed(scope, palette) {
1008
+ var rules = {};
1009
+ var variants = ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'];
1010
+ variants.forEach(function(v) {
1011
+ rules[scopeSelector(scope, '.bw-avatar-' + v)] = {
1012
+ 'background-color': palette[v].base,
1013
+ 'color': palette[v].textOn
1014
+ };
1015
+ });
1016
+ return rules;
1017
+ }
1018
+
883
1019
  /**
884
1020
  * Generate all themed CSS rules from a palette and layout.
885
1021
  * Returns a flat CSS rules object (selector → declarations).
@@ -909,6 +1045,14 @@ function generateThemedCSS(scopeName, palette, layout) {
909
1045
  generateSpinnerThemed(scopeName, palette),
910
1046
  generateCloseButtonThemed(scopeName, palette),
911
1047
  generateSectionsThemed(scopeName, palette),
1048
+ generateAccordionThemed(scopeName, palette),
1049
+ generateCarouselThemed(scopeName, palette),
1050
+ generateModalThemed(scopeName, palette),
1051
+ generateToastThemed(scopeName, palette),
1052
+ generateDropdownThemed(scopeName, palette),
1053
+ generateSwitchThemed(scopeName, palette),
1054
+ generateSkeletonThemed(scopeName, palette),
1055
+ generateAvatarThemed(scopeName, palette),
912
1056
  generateUtilityColors(scopeName, palette)
913
1057
  );
914
1058
  }
@@ -1178,7 +1322,7 @@ function getStructuralStyles() {
1178
1322
  'position': 'relative', 'display': 'flex', 'flex-wrap': 'wrap',
1179
1323
  'align-items': 'center', 'justify-content': 'space-between', 'padding': '0.5rem 1.5rem'
1180
1324
  };
1181
- rules['.bw-navbar > .container'] = { 'display': 'flex', 'flex-wrap': 'wrap', 'align-items': 'center', 'justify-content': 'space-between' };
1325
+ rules['.bw-navbar > .bw-container, .bw-navbar > .container'] = { 'display': 'flex', 'flex-wrap': 'wrap', 'align-items': 'center', 'justify-content': 'space-between' };
1182
1326
  rules['.bw-navbar-brand'] = {
1183
1327
  'display': 'inline-flex', 'align-items': 'center', 'gap': '0.5rem',
1184
1328
  'padding-top': '0.25rem', 'padding-bottom': '0.25rem', 'margin-right': '1.5rem',
@@ -1223,11 +1367,13 @@ function getStructuralStyles() {
1223
1367
 
1224
1368
  // Badges (structural)
1225
1369
  rules['.bw-badge'] = {
1226
- 'display': 'inline-block', 'padding': '.35em .65em', 'font-size': '.75em',
1227
- 'font-weight': '700', 'line-height': '1', 'text-align': 'center',
1370
+ 'display': 'inline-block', 'padding': '.4em .75em', 'font-size': '.875em',
1371
+ 'font-weight': '600', 'line-height': '1.3', 'text-align': 'center',
1228
1372
  'white-space': 'nowrap', 'vertical-align': 'baseline', 'border-radius': '.375rem'
1229
1373
  };
1230
1374
  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' };
1231
1377
  rules['.bw-badge-pill'] = { 'border-radius': '50rem' };
1232
1378
 
1233
1379
  // Progress (structural)
@@ -1260,6 +1406,8 @@ function getStructuralStyles() {
1260
1406
  rules['.bw-tab-content'] = { 'padding': '1.25rem 0' };
1261
1407
  rules['.bw-tab-pane'] = { 'display': 'none' };
1262
1408
  rules['.bw-tab-pane.active'] = { 'display': 'block' };
1409
+ rules['.bw-nav-scrollable'] = { 'flex-wrap': 'nowrap', 'overflow-x': 'auto', '-webkit-overflow-scrolling': 'touch', 'scrollbar-width': 'none' };
1410
+ rules['.bw-nav-scrollable .bw-nav-link'] = { 'white-space': 'nowrap' };
1263
1411
 
1264
1412
  // List groups (structural)
1265
1413
  rules['.bw-list-group'] = { 'display': 'flex', 'flex-direction': 'column', 'padding-left': '0', 'margin-bottom': '0', 'border-radius': '0.375rem' };
@@ -1294,7 +1442,8 @@ function getStructuralStyles() {
1294
1442
  rules['.bw-hero'] = { 'position': 'relative', 'overflow': 'hidden' };
1295
1443
  rules['.bw-hero-overlay'] = { 'position': 'absolute', 'top': '0', 'left': '0', 'right': '0', 'bottom': '0', 'z-index': '1' };
1296
1444
  rules['.bw-hero-content'] = { 'position': 'relative', 'z-index': '2' };
1297
- rules['.bw-hero-title'] = { 'font-weight': '300', 'letter-spacing': '-0.05rem' };
1445
+ rules['.bw-hero-title'] = { 'font-weight': '300', 'letter-spacing': '-0.05rem', 'color': 'inherit' };
1446
+ rules['.bw-hero-subtitle'] = { 'color': 'inherit' };
1298
1447
  rules['.bw-hero-actions'] = { 'display': 'flex', 'gap': '1rem', 'justify-content': 'center', 'flex-wrap': 'wrap' };
1299
1448
  rules['.bw-display-4'] = { 'font-size': 'calc(1.475rem + 2.7vw)', 'font-weight': '300', 'line-height': '1.2' };
1300
1449
  rules['.bw-lead'] = { 'font-size': '1.25rem', 'font-weight': '300' };
@@ -1367,6 +1516,179 @@ function getStructuralStyles() {
1367
1516
 
1368
1517
  // Code demo (structural)
1369
1518
  rules['.bw-code-demo'] = { 'margin-bottom': '2rem' };
1519
+ rules['.bw-code-pre'] = { 'margin': '0', 'border': 'none', 'border-radius': '6px', 'overflow-x': 'auto' };
1520
+ rules['.bw-code-block'] = { 'display': 'block', 'padding': '1.25rem', 'font-family': '"SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace', 'font-size': '0.8125rem', 'line-height': '1.6' };
1521
+ rules['.bw-code-copy-btn'] = { 'position': 'absolute', 'top': '0.5rem', 'right': '0.5rem', 'padding': '0.25rem 0.625rem', 'font-size': '0.6875rem', 'border-radius': '4px', 'cursor': 'pointer', 'font-family': 'inherit', 'transition': 'all 0.15s' };
1522
+
1523
+ // Button group (structural)
1524
+ rules['.bw-btn-group, .bw-btn-group-vertical'] = { 'position': 'relative', 'display': 'inline-flex', 'vertical-align': 'middle' };
1525
+ rules['.bw-btn-group > .bw-btn, .bw-btn-group-vertical > .bw-btn'] = { 'position': 'relative', 'flex': '1 1 auto', 'border-radius': '0', 'margin-left': '-1px' };
1526
+ rules['.bw-btn-group > .bw-btn:first-child'] = { 'margin-left': '0', 'border-top-left-radius': '6px', 'border-bottom-left-radius': '6px' };
1527
+ rules['.bw-btn-group > .bw-btn:last-child'] = { 'border-top-right-radius': '6px', 'border-bottom-right-radius': '6px' };
1528
+ rules['.bw-btn-group-vertical'] = { 'flex-direction': 'column', 'align-items': 'flex-start', 'justify-content': 'center' };
1529
+ rules['.bw-btn-group-vertical > .bw-btn'] = { 'width': '100%', 'margin-left': '0', 'margin-top': '-1px' };
1530
+ rules['.bw-btn-group-vertical > .bw-btn:first-child'] = { 'margin-top': '0', 'border-top-left-radius': '6px', 'border-top-right-radius': '6px', 'border-bottom-left-radius': '0', 'border-bottom-right-radius': '0' };
1531
+ rules['.bw-btn-group-vertical > .bw-btn:last-child'] = { 'border-top-left-radius': '0', 'border-top-right-radius': '0', 'border-bottom-left-radius': '6px', 'border-bottom-right-radius': '6px' };
1532
+
1533
+ // Accordion (structural)
1534
+ rules['.bw-accordion'] = { 'border-radius': '8px', 'overflow': 'hidden' };
1535
+ rules['.bw-accordion-item'] = { 'border': '1px solid transparent' };
1536
+ rules['.bw-accordion-item + .bw-accordion-item'] = { 'border-top': '0' };
1537
+ rules['.bw-accordion-header'] = { 'margin': '0' };
1538
+ rules['.bw-accordion-button'] = {
1539
+ 'position': 'relative', 'display': 'flex', 'align-items': 'center', 'width': '100%',
1540
+ 'padding': '1rem 1.25rem', 'font-size': '1rem', 'font-weight': '500', 'text-align': 'left',
1541
+ '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'
1543
+ };
1544
+ rules['.bw-accordion-button::after'] = {
1545
+ 'flex-shrink': '0', 'width': '1.25rem', 'height': '1.25rem', 'margin-left': 'auto',
1546
+ 'content': '""', 'background-repeat': 'no-repeat', 'background-size': '1.25rem',
1547
+ 'transition': 'transform 0.2s ease-in-out'
1548
+ };
1549
+ rules['.bw-accordion-button:not(.bw-collapsed)::after'] = { 'transform': 'rotate(-180deg)' };
1550
+ rules['.bw-accordion-collapse'] = { 'max-height': '0', 'overflow': 'hidden', 'transition': 'max-height 0.3s ease' };
1551
+ rules['.bw-accordion-collapse.bw-collapse-show'] = { 'max-height': 'none' };
1552
+ rules['.bw-accordion-body'] = { 'padding': '1rem 1.25rem' };
1553
+
1554
+ // Modal (structural)
1555
+ 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'
1558
+ };
1559
+ rules['.bw-modal.bw-modal-show'] = { 'display': 'flex', 'align-items': 'center', 'justify-content': 'center', 'opacity': '1' };
1560
+ rules['.bw-modal-dialog'] = {
1561
+ 'position': 'relative', 'width': '100%', 'max-width': '500px', 'margin': '1.75rem auto',
1562
+ 'pointer-events': 'none', 'transform': 'translateY(-20px)', 'transition': 'transform 0.2s ease-out'
1563
+ };
1564
+ rules['.bw-modal.bw-modal-show .bw-modal-dialog'] = { 'transform': 'translateY(0)' };
1565
+ rules['.bw-modal-sm'] = { 'max-width': '300px' };
1566
+ rules['.bw-modal-lg'] = { 'max-width': '800px' };
1567
+ rules['.bw-modal-xl'] = { 'max-width': '1140px' };
1568
+ rules['.bw-modal-content'] = {
1569
+ 'position': 'relative', 'display': 'flex', 'flex-direction': 'column', 'pointer-events': 'auto',
1570
+ 'background-clip': 'padding-box', 'border': '1px solid transparent', 'border-radius': '8px', 'outline': '0'
1571
+ };
1572
+ rules['.bw-modal-header'] = { 'display': 'flex', 'align-items': 'center', 'justify-content': 'space-between', 'padding': '1rem 1.5rem' };
1573
+ rules['.bw-modal-title'] = { 'margin': '0', 'font-size': '1.25rem', 'font-weight': '600', 'line-height': '1.3' };
1574
+ rules['.bw-modal-body'] = { 'position': 'relative', 'flex': '1 1 auto', 'padding': '1.5rem' };
1575
+ rules['.bw-modal-footer'] = { 'display': 'flex', 'flex-wrap': 'wrap', 'align-items': 'center', 'justify-content': 'flex-end', 'padding': '0.75rem 1.5rem', 'gap': '0.5rem' };
1576
+
1577
+ // Carousel (structural)
1578
+ rules['.bw-carousel'] = { 'position': 'relative', 'overflow': 'hidden', 'border-radius': '8px' };
1579
+ rules['.bw-carousel-track'] = { 'display': 'flex', 'transition': 'transform 0.4s ease', 'height': '100%' };
1580
+ rules['.bw-carousel-slide'] = { 'min-width': '100%', 'flex-shrink': '0', 'overflow': 'hidden', 'position': 'relative', 'display': 'flex', 'align-items': 'center', 'justify-content': 'center' };
1581
+ rules['.bw-carousel-slide img'] = { 'width': '100%', 'height': '100%', 'object-fit': 'cover' };
1582
+ rules['.bw-carousel-caption'] = { 'position': 'absolute', 'bottom': '0', 'left': '0', 'right': '0', 'padding': '0.75rem 1rem' };
1583
+ rules['.bw-carousel-control'] = {
1584
+ 'position': 'absolute', 'top': '50%', 'transform': 'translateY(-50%)', 'width': '40px', 'height': '40px',
1585
+ 'border': 'none', 'border-radius': '50%', 'cursor': 'pointer', 'display': 'flex', 'align-items': 'center',
1586
+ 'justify-content': 'center', 'z-index': '2', 'padding': '0', 'transition': 'background-color 0.2s ease'
1587
+ };
1588
+ rules['.bw-carousel-control img'] = { 'width': '20px', 'height': '20px', 'pointer-events': 'none' };
1589
+ rules['.bw-carousel-control-prev'] = { 'left': '10px' };
1590
+ rules['.bw-carousel-control-next'] = { 'right': '10px' };
1591
+ rules['.bw-carousel-indicators'] = {
1592
+ 'position': 'absolute', 'bottom': '12px', 'left': '50%', 'transform': 'translateX(-50%)',
1593
+ 'display': 'flex', 'gap': '6px', 'z-index': '2'
1594
+ };
1595
+ rules['.bw-carousel-indicator'] = {
1596
+ 'width': '10px', 'height': '10px', 'border-radius': '50%', 'border': '2px solid transparent',
1597
+ 'padding': '0', 'cursor': 'pointer', 'transition': 'opacity 0.2s ease, background-color 0.2s ease'
1598
+ };
1599
+
1600
+ // Toast (structural)
1601
+ rules['.bw-toast-container'] = {
1602
+ 'position': 'fixed', 'z-index': '1080', 'pointer-events': 'none',
1603
+ 'display': 'flex', 'flex-direction': 'column', 'gap': '0.5rem', 'padding': '1rem'
1604
+ };
1605
+ rules['.bw-toast'] = {
1606
+ 'pointer-events': 'auto', 'width': '350px', 'max-width': '100%', 'background-clip': 'padding-box',
1607
+ 'border-radius': '8px', 'opacity': '0', 'transform': 'translateY(-10px)',
1608
+ 'transition': 'opacity 0.3s ease, transform 0.3s ease'
1609
+ };
1610
+ rules['.bw-toast.bw-toast-show'] = { 'opacity': '1', 'transform': 'translateY(0)' };
1611
+ rules['.bw-toast.bw-toast-hiding'] = { 'opacity': '0', 'transform': 'translateY(-10px)' };
1612
+ rules['.bw-toast-header'] = { 'display': 'flex', 'align-items': 'center', 'justify-content': 'space-between', 'padding': '0.5rem 0.75rem', 'font-size': '0.875rem' };
1613
+ rules['.bw-toast-body'] = { 'padding': '0.75rem', 'font-size': '0.9375rem' };
1614
+
1615
+ // Dropdown (structural)
1616
+ rules['.bw-dropdown'] = { 'position': 'relative', 'display': 'inline-block' };
1617
+ rules['.bw-dropdown-toggle::after'] = {
1618
+ 'display': 'inline-block', 'margin-left': '0.255em', 'vertical-align': '0.255em',
1619
+ 'content': '""', 'border-top': '0.3em solid', 'border-right': '0.3em solid transparent',
1620
+ 'border-bottom': '0', 'border-left': '0.3em solid transparent'
1621
+ };
1622
+ rules['.bw-dropdown-menu'] = {
1623
+ 'position': 'absolute', 'top': '100%', 'left': '0', 'z-index': '1000', 'display': 'none',
1624
+ 'min-width': '10rem', 'padding': '0.5rem 0', 'margin': '0.125rem 0 0',
1625
+ 'background-clip': 'padding-box', 'border-radius': '6px'
1626
+ };
1627
+ rules['.bw-dropdown-menu.bw-dropdown-show'] = { 'display': 'block' };
1628
+ rules['.bw-dropdown-menu-end'] = { 'left': 'auto', 'right': '0' };
1629
+ rules['.bw-dropdown-item'] = {
1630
+ 'display': 'block', 'width': '100%', 'padding': '0.375rem 1rem', 'clear': 'both',
1631
+ 'font-weight': '400', 'text-align': 'inherit', 'text-decoration': 'none', 'white-space': 'nowrap',
1632
+ 'background-color': 'transparent', 'border': '0', 'font-size': '0.9375rem',
1633
+ 'transition': 'background-color 0.15s, color 0.15s'
1634
+ };
1635
+ rules['.bw-dropdown-divider'] = { 'height': '0', 'margin': '0.5rem 0', 'overflow': 'hidden', 'opacity': '1' };
1636
+
1637
+ // Switch (structural)
1638
+ rules['.bw-form-switch'] = { 'padding-left': '2.5em' };
1639
+ rules['.bw-form-switch .bw-switch-input'] = {
1640
+ 'width': '2em', 'height': '1.125em', 'margin-left': '-2.5em', 'border-radius': '2em',
1641
+ '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',
1643
+ 'cursor': 'pointer'
1644
+ };
1645
+ rules['.bw-form-switch .bw-switch-input:checked'] = { 'background-position': 'right center' };
1646
+ rules['.bw-form-switch .bw-switch-input:disabled'] = { 'opacity': '0.5', 'cursor': 'not-allowed' };
1647
+
1648
+ // Skeleton (structural)
1649
+ rules['.bw-skeleton'] = { 'border-radius': '4px', 'background-size': '400% 100%', 'animation': 'bw-skeleton-shimmer 1.4s ease infinite' };
1650
+ rules['.bw-skeleton-text'] = { 'height': '1em', 'margin-bottom': '0.5rem' };
1651
+ rules['.bw-skeleton-circle'] = { 'border-radius': '50%' };
1652
+ rules['.bw-skeleton-rect'] = { 'border-radius': '8px' };
1653
+ rules['.bw-skeleton-group'] = { 'display': 'flex', 'flex-direction': 'column' };
1654
+ rules['@keyframes bw-skeleton-shimmer'] = { '0%': { 'background-position': '100% 50%' }, '100%': { 'background-position': '0 50%' } };
1655
+
1656
+ // Avatar (structural)
1657
+ rules['.bw-avatar'] = {
1658
+ 'display': 'inline-flex', 'align-items': 'center', 'justify-content': 'center',
1659
+ 'border-radius': '50%', 'overflow': 'hidden', 'font-weight': '600',
1660
+ 'text-transform': 'uppercase', 'vertical-align': 'middle', 'object-fit': 'cover'
1661
+ };
1662
+ rules['.bw-avatar-sm'] = { 'width': '2rem', 'height': '2rem', 'font-size': '0.75rem' };
1663
+ rules['.bw-avatar-md'] = { 'width': '3rem', 'height': '3rem', 'font-size': '1rem' };
1664
+ rules['.bw-avatar-lg'] = { 'width': '4rem', 'height': '4rem', 'font-size': '1.25rem' };
1665
+ rules['.bw-avatar-xl'] = { 'width': '5rem', 'height': '5rem', 'font-size': '1.5rem' };
1666
+
1667
+ // Bar chart (structural)
1668
+ rules['.bw-bar-chart-container'] = {
1669
+ 'padding': '1rem', 'border': '1px solid transparent', 'border-radius': '8px'
1670
+ };
1671
+ rules['.bw-bar-chart'] = {
1672
+ 'display': 'flex', 'align-items': 'flex-end', 'gap': '6px', 'padding': '0 0.5rem'
1673
+ };
1674
+ rules['.bw-bar-group'] = {
1675
+ 'flex': '1', 'display': 'flex', 'flex-direction': 'column',
1676
+ 'align-items': 'center', 'height': '100%', 'justify-content': 'flex-end'
1677
+ };
1678
+ rules['.bw-bar'] = {
1679
+ 'width': '100%', 'border-radius': '3px 3px 0 0',
1680
+ 'transition': 'height 0.5s ease', 'min-height': '4px'
1681
+ };
1682
+ rules['.bw-bar:hover'] = { 'opacity': '0.85' };
1683
+ rules['.bw-bar-value'] = {
1684
+ 'font-size': '0.65rem', 'font-weight': '600', 'margin-bottom': '2px', 'text-align': 'center'
1685
+ };
1686
+ rules['.bw-bar-label'] = {
1687
+ 'font-size': '0.7rem', 'margin-top': '4px', 'text-align': 'center'
1688
+ };
1689
+ rules['.bw-bar-chart-title'] = {
1690
+ 'font-size': '1.1rem', 'font-weight': '600', 'margin': '0 0 0.75rem 0'
1691
+ };
1370
1692
 
1371
1693
  // Spacing utilities (structural)
1372
1694
  var spacingValues = { '0': '0', '1': '.25rem', '2': '.5rem', '3': '1rem', '4': '1.5rem', '5': '3rem' };
@@ -1684,6 +2006,75 @@ function generateDarkModeCSS(palette) {
1684
2006
  '.bw-dark .bw-close': {
1685
2007
  'color': textColor
1686
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
+ },
1687
2078
  '.bw-dark h1, .bw-dark h2, .bw-dark h3, .bw-dark h4, .bw-dark h5, .bw-dark h6': {
1688
2079
  'color': textColor
1689
2080
  },
@@ -2277,7 +2668,11 @@ function makeAlert(props = {}) {
2277
2668
  a: {
2278
2669
  type: 'button',
2279
2670
  class: 'bw-close',
2280
- 'aria-label': 'Close'
2671
+ 'aria-label': 'Close',
2672
+ onclick: function(e) {
2673
+ var alert = e.target.closest('.bw-alert');
2674
+ if (alert) { alert.remove(); }
2675
+ }
2281
2676
  },
2282
2677
  c: '×'
2283
2678
  }
@@ -2291,25 +2686,30 @@ function makeAlert(props = {}) {
2291
2686
  * @param {Object} [props] - Badge configuration
2292
2687
  * @param {string} [props.text] - Badge display text
2293
2688
  * @param {string} [props.variant="primary"] - Color variant
2689
+ * @param {string} [props.size] - Size variant: 'sm' or 'lg' (default is medium)
2294
2690
  * @param {boolean} [props.pill=false] - Use pill (rounded) shape
2295
2691
  * @param {string} [props.className] - Additional CSS classes
2296
2692
  * @returns {Object} TACO object representing a badge span
2297
2693
  * @category Component Builders
2298
2694
  * @example
2299
2695
  * const badge = makeBadge({ text: "New", variant: "danger", pill: true });
2696
+ * const small = makeBadge({ text: "3", variant: "info", size: "sm" });
2300
2697
  */
2301
2698
  function makeBadge(props = {}) {
2302
2699
  const {
2303
2700
  text,
2304
2701
  variant = 'primary',
2702
+ size,
2305
2703
  pill = false,
2306
2704
  className = ''
2307
2705
  } = props;
2308
2706
 
2707
+ const sizeClass = size === 'sm' ? ' bw-badge-sm' : size === 'lg' ? ' bw-badge-lg' : '';
2708
+
2309
2709
  return {
2310
2710
  t: 'span',
2311
2711
  a: {
2312
- class: `bw-badge bw-badge-${variant} ${pill ? 'bw-badge-pill' : ''} ${className}`.trim()
2712
+ class: `bw-badge bw-badge-${variant}${sizeClass} ${pill ? 'bw-badge-pill' : ''} ${className}`.trim()
2313
2713
  },
2314
2714
  c: text
2315
2715
  };
@@ -2768,12 +3168,14 @@ function makeCheckbox(props = {}) {
2768
3168
  id,
2769
3169
  name,
2770
3170
  disabled = false,
2771
- value
3171
+ value,
3172
+ className = '',
3173
+ ...eventHandlers
2772
3174
  } = props;
2773
3175
 
2774
3176
  return {
2775
3177
  t: 'div',
2776
- a: { class: 'bw-form-check' },
3178
+ a: { class: `bw-form-check ${className}`.trim() },
2777
3179
  c: [
2778
3180
  {
2779
3181
  t: 'input',
@@ -2784,7 +3186,8 @@ function makeCheckbox(props = {}) {
2784
3186
  id,
2785
3187
  name,
2786
3188
  disabled,
2787
- value
3189
+ value,
3190
+ ...eventHandlers
2788
3191
  }
2789
3192
  },
2790
3193
  label && {
@@ -3012,8 +3415,8 @@ function makeFeatureGrid(props = {}) {
3012
3415
  feature.icon && {
3013
3416
  t: 'div',
3014
3417
  a: {
3015
- class: 'bw-feature-icon bw-mb-3',
3016
- style: `font-size: ${iconSize}; color: var(--bw-primary);`
3418
+ class: 'bw-feature-icon bw-mb-3 bw-text-primary',
3419
+ style: `font-size: ${iconSize};`
3017
3420
  },
3018
3421
  c: feature.icon
3019
3422
  },
@@ -3454,177 +3857,1201 @@ class TabsHandle {
3454
3857
  } else {
3455
3858
  item.classList.remove('active');
3456
3859
  }
3457
- });
3860
+ });
3861
+
3862
+ this.children.tabPanes.forEach((pane, i) => {
3863
+ if (i === index) {
3864
+ pane.classList.add('active');
3865
+ } else {
3866
+ pane.classList.remove('active');
3867
+ }
3868
+ });
3869
+
3870
+ this.state.activeIndex = index;
3871
+ return this;
3872
+ }
3873
+ }
3874
+
3875
+ /**
3876
+ * Create a code demo component for documentation pages
3877
+ *
3878
+ * Displays a live result alongside source code in a tabbed interface.
3879
+ * Includes a copy-to-clipboard button on the code tab.
3880
+ *
3881
+ * @param {Object} [props] - Code demo configuration
3882
+ * @param {string} [props.title] - Demo title heading
3883
+ * @param {string} [props.description] - Demo description text
3884
+ * @param {string} [props.code] - Source code to display (adds a "Code" tab when present)
3885
+ * @param {string|Object|Array} [props.result] - Live result content for the "Result" tab
3886
+ * @param {string} [props.language="javascript"] - Code language for syntax class
3887
+ * @returns {Object} TACO object representing a code demo with tabbed Result/Code views
3888
+ * @category Component Builders
3889
+ * @example
3890
+ * const demo = makeCodeDemo({
3891
+ * title: "Button Example",
3892
+ * description: "A simple primary button",
3893
+ * code: 'makeButton({ text: "Click me" })',
3894
+ * result: makeButton({ text: "Click me" })
3895
+ * });
3896
+ */
3897
+ function makeCodeDemo(props = {}) {
3898
+ const {
3899
+ title,
3900
+ description,
3901
+ code,
3902
+ result,
3903
+ language = 'javascript'
3904
+ } = props;
3905
+
3906
+ // Generate unique ID for this demo
3907
+ `demo-${Math.random().toString(36).substr(2, 9)}`;
3908
+
3909
+ const tabs = [
3910
+ {
3911
+ label: 'Result',
3912
+ active: true,
3913
+ content: result
3914
+ }
3915
+ ];
3916
+
3917
+ // Only add Code tab if code is provided
3918
+ if (code) {
3919
+ tabs.push({
3920
+ label: 'Code',
3921
+ content: {
3922
+ t: 'div',
3923
+ a: { style: 'position: relative;' },
3924
+ c: [
3925
+ {
3926
+ t: 'button',
3927
+ a: {
3928
+ 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;
3933
+ btn.textContent = 'Copied!';
3934
+ btn.style.background = '#006666';
3935
+ btn.style.color = '#fff';
3936
+ setTimeout(() => {
3937
+ btn.textContent = originalText;
3938
+ btn.style.background = 'rgba(255,255,255,0.12)';
3939
+ btn.style.color = '#aaa';
3940
+ }, 2000);
3941
+ });
3942
+ }
3943
+ },
3944
+ c: 'Copy'
3945
+ },
3946
+ (typeof globalThis !== 'undefined' && typeof globalThis.bw !== 'undefined' && typeof globalThis.bw.codeEditor === 'function')
3947
+ ? globalThis.bw.codeEditor({ code: code, lang: language === 'javascript' ? 'js' : language, readOnly: true, height: 'auto' })
3948
+ : {
3949
+ t: 'pre',
3950
+ a: { class: 'bw-code-pre' },
3951
+ c: {
3952
+ t: 'code',
3953
+ a: { class: `bw-code-block language-${language}` },
3954
+ c: code
3955
+ }
3956
+ }
3957
+ ]
3958
+ }
3959
+ });
3960
+ }
3961
+
3962
+ const content = [
3963
+ title && { t: 'h3', c: title },
3964
+ description && {
3965
+ t: 'p',
3966
+ a: { class: 'bw-text-muted', style: 'margin-bottom: 1rem;' },
3967
+ c: description
3968
+ },
3969
+ makeTabs({ tabs})
3970
+ ].filter(Boolean);
3971
+
3972
+ return {
3973
+ t: 'div',
3974
+ a: { class: 'bw-code-demo' },
3975
+ c: content
3976
+ };
3977
+ }
3978
+
3979
+ /**
3980
+ * Registry mapping component type names to their handle classes
3981
+ *
3982
+ * Used by bw.createCard(), bw.createTable(), etc. to wrap rendered
3983
+ * DOM elements in the appropriate imperative handle.
3984
+ *
3985
+ * @type {Object.<string, Function>}
3986
+ */
3987
+ // =========================================================================
3988
+ // Phase 1: Quick Wins
3989
+ // =========================================================================
3990
+
3991
+ /**
3992
+ * Create a pagination navigation component
3993
+ *
3994
+ * @param {Object} [props] - Pagination configuration
3995
+ * @param {number} [props.pages=1] - Total number of pages
3996
+ * @param {number} [props.currentPage=1] - Currently active page (1-based)
3997
+ * @param {Function} [props.onPageChange] - Callback when page changes, receives page number
3998
+ * @param {string} [props.size] - Size variant ("sm" or "lg")
3999
+ * @param {string} [props.className] - Additional CSS classes
4000
+ * @returns {Object} TACO object representing a pagination nav
4001
+ * @category Component Builders
4002
+ * @example
4003
+ * const pager = makePagination({
4004
+ * pages: 10,
4005
+ * currentPage: 3,
4006
+ * onPageChange: (page) => loadPage(page)
4007
+ * });
4008
+ */
4009
+ function makePagination(props = {}) {
4010
+ const {
4011
+ pages = 1,
4012
+ currentPage = 1,
4013
+ onPageChange,
4014
+ size,
4015
+ className = ''
4016
+ } = props;
4017
+
4018
+ function handleClick(page) {
4019
+ return function(e) {
4020
+ e.preventDefault();
4021
+ if (page < 1 || page > pages || page === currentPage) return;
4022
+ if (onPageChange) onPageChange(page);
4023
+ };
4024
+ }
4025
+
4026
+ const items = [];
4027
+
4028
+ // Previous arrow
4029
+ items.push({
4030
+ t: 'li',
4031
+ a: { class: `bw-page-item ${currentPage <= 1 ? 'bw-disabled' : ''}`.trim() },
4032
+ c: {
4033
+ t: 'a',
4034
+ a: { class: 'bw-page-link', href: '#', onclick: handleClick(currentPage - 1), 'aria-label': 'Previous' },
4035
+ c: '\u2039'
4036
+ }
4037
+ });
4038
+
4039
+ // Page numbers
4040
+ for (var i = 1; i <= pages; i++) {
4041
+ (function(pageNum) {
4042
+ items.push({
4043
+ t: 'li',
4044
+ a: { class: `bw-page-item ${pageNum === currentPage ? 'bw-active' : ''}`.trim() },
4045
+ c: {
4046
+ t: 'a',
4047
+ a: { class: 'bw-page-link', href: '#', onclick: handleClick(pageNum) },
4048
+ c: '' + pageNum
4049
+ }
4050
+ });
4051
+ })(i);
4052
+ }
4053
+
4054
+ // Next arrow
4055
+ items.push({
4056
+ t: 'li',
4057
+ a: { class: `bw-page-item ${currentPage >= pages ? 'bw-disabled' : ''}`.trim() },
4058
+ c: {
4059
+ t: 'a',
4060
+ a: { class: 'bw-page-link', href: '#', onclick: handleClick(currentPage + 1), 'aria-label': 'Next' },
4061
+ c: '\u203A'
4062
+ }
4063
+ });
4064
+
4065
+ return {
4066
+ t: 'nav',
4067
+ a: { 'aria-label': 'Pagination' },
4068
+ c: {
4069
+ t: 'ul',
4070
+ a: {
4071
+ class: `bw-pagination ${size ? 'bw-pagination-' + size : ''} ${className}`.trim()
4072
+ },
4073
+ c: items
4074
+ }
4075
+ };
4076
+ }
4077
+
4078
+ /**
4079
+ * Create a radio button input with label
4080
+ *
4081
+ * @param {Object} [props] - Radio configuration
4082
+ * @param {string} [props.label] - Radio label text
4083
+ * @param {string} [props.name] - Radio group name
4084
+ * @param {string} [props.value] - Radio value attribute
4085
+ * @param {boolean} [props.checked=false] - Whether the radio is selected
4086
+ * @param {string} [props.id] - Element ID (links label to radio)
4087
+ * @param {boolean} [props.disabled=false] - Whether the radio is disabled
4088
+ * @param {string} [props.className] - Additional CSS classes
4089
+ * @returns {Object} TACO object representing a radio form group
4090
+ * @category Component Builders
4091
+ * @example
4092
+ * const radio = makeRadio({
4093
+ * label: "Option A",
4094
+ * name: "choice",
4095
+ * value: "a",
4096
+ * checked: true
4097
+ * });
4098
+ */
4099
+ function makeRadio(props = {}) {
4100
+ const {
4101
+ label,
4102
+ name,
4103
+ value,
4104
+ checked = false,
4105
+ id,
4106
+ disabled = false,
4107
+ className = '',
4108
+ ...eventHandlers
4109
+ } = props;
4110
+
4111
+ return {
4112
+ t: 'div',
4113
+ a: { class: `bw-form-check ${className}`.trim() },
4114
+ c: [
4115
+ {
4116
+ t: 'input',
4117
+ a: {
4118
+ type: 'radio',
4119
+ class: 'bw-form-check-input',
4120
+ name,
4121
+ value,
4122
+ checked,
4123
+ id,
4124
+ disabled,
4125
+ ...eventHandlers
4126
+ }
4127
+ },
4128
+ label && {
4129
+ t: 'label',
4130
+ a: { class: 'bw-form-check-label', for: id },
4131
+ c: label
4132
+ }
4133
+ ].filter(Boolean)
4134
+ };
4135
+ }
4136
+
4137
+ /**
4138
+ * Create a button group wrapper
4139
+ *
4140
+ * @param {Object} [props] - Button group configuration
4141
+ * @param {Array} [props.children] - Button TACO objects to group
4142
+ * @param {string} [props.size] - Size variant ("sm" or "lg")
4143
+ * @param {boolean} [props.vertical=false] - Stack buttons vertically
4144
+ * @param {string} [props.className] - Additional CSS classes
4145
+ * @returns {Object} TACO object representing a button group
4146
+ * @category Component Builders
4147
+ * @example
4148
+ * const group = makeButtonGroup({
4149
+ * children: [
4150
+ * makeButton({ text: "Left", variant: "primary" }),
4151
+ * makeButton({ text: "Middle", variant: "primary" }),
4152
+ * makeButton({ text: "Right", variant: "primary" })
4153
+ * ]
4154
+ * });
4155
+ */
4156
+ function makeButtonGroup(props = {}) {
4157
+ const {
4158
+ children,
4159
+ size,
4160
+ vertical = false,
4161
+ className = ''
4162
+ } = props;
4163
+
4164
+ return {
4165
+ t: 'div',
4166
+ a: {
4167
+ class: `${vertical ? 'bw-btn-group-vertical' : 'bw-btn-group'} ${size ? 'bw-btn-group-' + size : ''} ${className}`.trim(),
4168
+ role: 'group'
4169
+ },
4170
+ c: children
4171
+ };
4172
+ }
4173
+
4174
+ // =========================================================================
4175
+ // Phase 2: Core Interactive
4176
+ // =========================================================================
4177
+
4178
+ /**
4179
+ * Create an accordion component with collapsible items
4180
+ *
4181
+ * @param {Object} [props] - Accordion configuration
4182
+ * @param {Array<Object>} [props.items=[]] - Accordion items
4183
+ * @param {string} props.items[].title - Header text for the accordion item
4184
+ * @param {string|Object|Array} props.items[].content - Collapsible content
4185
+ * @param {boolean} [props.items[].open=false] - Whether the item is initially open
4186
+ * @param {boolean} [props.multiOpen=false] - Allow multiple items open simultaneously
4187
+ * @param {string} [props.className] - Additional CSS classes
4188
+ * @returns {Object} TACO object representing an accordion
4189
+ * @category Component Builders
4190
+ * @example
4191
+ * const accordion = makeAccordion({
4192
+ * items: [
4193
+ * { title: "Section 1", content: "Content 1", open: true },
4194
+ * { title: "Section 2", content: "Content 2" }
4195
+ * ]
4196
+ * });
4197
+ */
4198
+ function makeAccordion(props = {}) {
4199
+ const {
4200
+ items = [],
4201
+ multiOpen = false,
4202
+ className = ''
4203
+ } = props;
4204
+
4205
+ return {
4206
+ t: 'div',
4207
+ a: { class: `bw-accordion ${className}`.trim() },
4208
+ c: items.map(function(item, index) {
4209
+ return {
4210
+ t: 'div',
4211
+ a: { class: 'bw-accordion-item' },
4212
+ c: [
4213
+ {
4214
+ t: 'h2',
4215
+ a: { class: 'bw-accordion-header' },
4216
+ c: {
4217
+ t: 'button',
4218
+ a: {
4219
+ class: `bw-accordion-button ${item.open ? '' : 'bw-collapsed'}`.trim(),
4220
+ type: 'button',
4221
+ 'aria-expanded': item.open ? 'true' : 'false',
4222
+ 'data-accordion-index': index,
4223
+ onclick: function(e) {
4224
+ var btn = e.target.closest('.bw-accordion-button');
4225
+ var accordionEl = btn.closest('.bw-accordion');
4226
+ var accordionItem = btn.closest('.bw-accordion-item');
4227
+ var collapse = accordionItem.querySelector('.bw-accordion-collapse');
4228
+ var isOpen = collapse.classList.contains('bw-collapse-show');
4229
+
4230
+ 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');
4241
+ }
4242
+ }
4243
+
4244
+ if (isOpen) {
4245
+ collapse.classList.remove('bw-collapse-show');
4246
+ collapse.style.maxHeight = null;
4247
+ btn.classList.add('bw-collapsed');
4248
+ btn.setAttribute('aria-expanded', 'false');
4249
+ } else {
4250
+ collapse.classList.add('bw-collapse-show');
4251
+ collapse.style.maxHeight = collapse.scrollHeight + 'px';
4252
+ btn.classList.remove('bw-collapsed');
4253
+ btn.setAttribute('aria-expanded', 'true');
4254
+ }
4255
+ }
4256
+ },
4257
+ c: item.title
4258
+ }
4259
+ },
4260
+ {
4261
+ t: 'div',
4262
+ a: { class: `bw-accordion-collapse ${item.open ? 'bw-collapse-show' : ''}`.trim() },
4263
+ c: {
4264
+ t: 'div',
4265
+ a: { class: 'bw-accordion-body' },
4266
+ c: item.content
4267
+ },
4268
+ o: item.open ? {
4269
+ mounted: function(el) {
4270
+ el.style.maxHeight = el.scrollHeight + 'px';
4271
+ }
4272
+ } : undefined
4273
+ }
4274
+ ]
4275
+ };
4276
+ }),
4277
+ o: {
4278
+ type: 'accordion',
4279
+ state: { multiOpen: multiOpen }
4280
+ }
4281
+ };
4282
+ }
4283
+
4284
+ /**
4285
+ * Imperative handle for a rendered modal component
4286
+ *
4287
+ * Provides `.show()`, `.hide()`, `.toggle()`, and `.destroy()` methods
4288
+ * for controlling the modal programmatically.
4289
+ *
4290
+ * @category Component Handles
4291
+ */
4292
+ class ModalHandle {
4293
+ /**
4294
+ * @param {Element} element - The modal backdrop DOM element
4295
+ * @param {Object} taco - The original TACO object
4296
+ */
4297
+ constructor(element, taco) {
4298
+ this.element = element;
4299
+ this._taco = taco;
4300
+ this._escHandler = null;
4301
+ }
4302
+
4303
+ /** Show the modal */
4304
+ show() {
4305
+ this.element.classList.add('bw-modal-show');
4306
+ document.body.style.overflow = 'hidden';
4307
+ return this;
4308
+ }
4309
+
4310
+ /** Hide the modal */
4311
+ hide() {
4312
+ this.element.classList.remove('bw-modal-show');
4313
+ document.body.style.overflow = '';
4314
+ return this;
4315
+ }
4316
+
4317
+ /** Toggle modal visibility */
4318
+ toggle() {
4319
+ if (this.element.classList.contains('bw-modal-show')) {
4320
+ this.hide();
4321
+ } else {
4322
+ this.show();
4323
+ }
4324
+ return this;
4325
+ }
4326
+
4327
+ /** Remove the modal from DOM and clean up */
4328
+ destroy() {
4329
+ this.hide();
4330
+ if (this._escHandler) {
4331
+ document.removeEventListener('keydown', this._escHandler);
4332
+ }
4333
+ if (this.element.parentNode) {
4334
+ this.element.parentNode.removeChild(this.element);
4335
+ }
4336
+ }
4337
+ }
4338
+
4339
+ /**
4340
+ * Create a modal dialog overlay
4341
+ *
4342
+ * @param {Object} [props] - Modal configuration
4343
+ * @param {string} [props.title] - Modal title in header
4344
+ * @param {string|Object|Array} [props.content] - Modal body content
4345
+ * @param {string|Object|Array} [props.footer] - Modal footer content
4346
+ * @param {string} [props.size] - Modal size ("sm", "lg", "xl")
4347
+ * @param {boolean} [props.closeButton=true] - Show X close button in header
4348
+ * @param {Function} [props.onClose] - Callback when modal is closed
4349
+ * @param {string} [props.className] - Additional CSS classes
4350
+ * @returns {Object} TACO object representing a modal
4351
+ * @category Component Builders
4352
+ * @example
4353
+ * const modal = makeModal({
4354
+ * title: "Confirm",
4355
+ * content: "Are you sure?",
4356
+ * footer: makeButton({ text: "OK", variant: "primary" })
4357
+ * });
4358
+ */
4359
+ function makeModal(props = {}) {
4360
+ const {
4361
+ title,
4362
+ content,
4363
+ footer,
4364
+ size,
4365
+ closeButton = true,
4366
+ onClose,
4367
+ className = ''
4368
+ } = props;
4369
+
4370
+ function closeModal(el) {
4371
+ var backdrop = el.closest('.bw-modal');
4372
+ if (backdrop) {
4373
+ backdrop.classList.remove('bw-modal-show');
4374
+ document.body.style.overflow = '';
4375
+ }
4376
+ if (onClose) onClose();
4377
+ }
4378
+
4379
+ return {
4380
+ t: 'div',
4381
+ a: { class: `bw-modal ${className}`.trim() },
4382
+ c: {
4383
+ t: 'div',
4384
+ a: { class: `bw-modal-dialog ${size ? 'bw-modal-' + size : ''}`.trim() },
4385
+ c: {
4386
+ t: 'div',
4387
+ a: { class: 'bw-modal-content' },
4388
+ c: [
4389
+ (title || closeButton) && {
4390
+ t: 'div',
4391
+ a: { class: 'bw-modal-header' },
4392
+ c: [
4393
+ title && { t: 'h5', a: { class: 'bw-modal-title' }, c: title },
4394
+ closeButton && {
4395
+ t: 'button',
4396
+ a: {
4397
+ type: 'button',
4398
+ class: 'bw-close',
4399
+ 'aria-label': 'Close',
4400
+ onclick: function(e) { closeModal(e.target); }
4401
+ },
4402
+ c: '\u00D7'
4403
+ }
4404
+ ].filter(Boolean)
4405
+ },
4406
+ content && {
4407
+ t: 'div',
4408
+ a: { class: 'bw-modal-body' },
4409
+ c: content
4410
+ },
4411
+ footer && {
4412
+ t: 'div',
4413
+ a: { class: 'bw-modal-footer' },
4414
+ c: footer
4415
+ }
4416
+ ].filter(Boolean)
4417
+ }
4418
+ },
4419
+ o: {
4420
+ type: 'modal',
4421
+ mounted: function(el) {
4422
+ // Click backdrop to close
4423
+ el.addEventListener('click', function(e) {
4424
+ if (e.target === el) closeModal(el);
4425
+ });
4426
+ // Escape key to close
4427
+ var escHandler = function(e) {
4428
+ if (e.key === 'Escape' && el.classList.contains('bw-modal-show')) {
4429
+ closeModal(el);
4430
+ }
4431
+ };
4432
+ document.addEventListener('keydown', escHandler);
4433
+ el._bw_escHandler = escHandler;
4434
+ },
4435
+ unmount: function(el) {
4436
+ if (el._bw_escHandler) {
4437
+ document.removeEventListener('keydown', el._bw_escHandler);
4438
+ }
4439
+ document.body.style.overflow = '';
4440
+ }
4441
+ }
4442
+ };
4443
+ }
4444
+
4445
+ /**
4446
+ * Create a toast notification popup
4447
+ *
4448
+ * @param {Object} [props] - Toast configuration
4449
+ * @param {string} [props.title] - Toast title
4450
+ * @param {string|Object|Array} [props.content] - Toast body content
4451
+ * @param {string} [props.variant="info"] - Color variant ("primary", "success", "danger", "warning", "info")
4452
+ * @param {boolean} [props.autoDismiss=true] - Auto-dismiss after delay
4453
+ * @param {number} [props.delay=5000] - Auto-dismiss delay in ms
4454
+ * @param {string} [props.position="top-right"] - Container position
4455
+ * @param {string} [props.className] - Additional CSS classes
4456
+ * @returns {Object} TACO object representing a toast
4457
+ * @category Component Builders
4458
+ * @example
4459
+ * const toast = makeToast({
4460
+ * title: "Success",
4461
+ * content: "File saved!",
4462
+ * variant: "success"
4463
+ * });
4464
+ */
4465
+ function makeToast(props = {}) {
4466
+ const {
4467
+ title,
4468
+ content,
4469
+ variant = 'info',
4470
+ autoDismiss = true,
4471
+ delay = 5000,
4472
+ position = 'top-right',
4473
+ className = ''
4474
+ } = props;
4475
+
4476
+ return {
4477
+ t: 'div',
4478
+ a: {
4479
+ class: `bw-toast bw-toast-${variant} ${className}`.trim(),
4480
+ role: 'alert',
4481
+ 'data-position': position
4482
+ },
4483
+ c: [
4484
+ (title) && {
4485
+ t: 'div',
4486
+ a: { class: 'bw-toast-header' },
4487
+ c: [
4488
+ { t: 'strong', c: title },
4489
+ {
4490
+ t: 'button',
4491
+ a: {
4492
+ type: 'button',
4493
+ class: 'bw-close',
4494
+ 'aria-label': 'Close',
4495
+ onclick: function(e) {
4496
+ var toast = e.target.closest('.bw-toast');
4497
+ if (toast) {
4498
+ toast.classList.add('bw-toast-hiding');
4499
+ setTimeout(function() { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 300);
4500
+ }
4501
+ }
4502
+ },
4503
+ c: '\u00D7'
4504
+ }
4505
+ ]
4506
+ },
4507
+ content && {
4508
+ t: 'div',
4509
+ a: { class: 'bw-toast-body' },
4510
+ c: content
4511
+ }
4512
+ ].filter(Boolean),
4513
+ o: {
4514
+ type: 'toast',
4515
+ mounted: function(el) {
4516
+ // Trigger show animation
4517
+ requestAnimationFrame(function() {
4518
+ el.classList.add('bw-toast-show');
4519
+ });
4520
+ // Auto-dismiss
4521
+ if (autoDismiss) {
4522
+ setTimeout(function() {
4523
+ el.classList.add('bw-toast-hiding');
4524
+ setTimeout(function() { if (el.parentNode) el.parentNode.removeChild(el); }, 300);
4525
+ }, delay);
4526
+ }
4527
+ }
4528
+ }
4529
+ };
4530
+ }
4531
+
4532
+ // =========================================================================
4533
+ // Phase 3: Essential Modern
4534
+ // =========================================================================
4535
+
4536
+ /**
4537
+ * Create a dropdown menu triggered by a button
4538
+ *
4539
+ * @param {Object} [props] - Dropdown configuration
4540
+ * @param {string|Object} [props.trigger] - Button text or TACO for the trigger
4541
+ * @param {Array<Object>} [props.items=[]] - Menu items
4542
+ * @param {string} [props.items[].text] - Item display text
4543
+ * @param {string} [props.items[].href] - Item link URL
4544
+ * @param {Function} [props.items[].onclick] - Item click handler
4545
+ * @param {boolean} [props.items[].divider] - Render as a divider line
4546
+ * @param {boolean} [props.items[].disabled] - Whether the item is disabled
4547
+ * @param {string} [props.align="start"] - Menu alignment ("start" or "end")
4548
+ * @param {string} [props.variant="primary"] - Trigger button variant
4549
+ * @param {string} [props.className] - Additional CSS classes
4550
+ * @returns {Object} TACO object representing a dropdown
4551
+ * @category Component Builders
4552
+ * @example
4553
+ * const dropdown = makeDropdown({
4554
+ * trigger: "Actions",
4555
+ * items: [
4556
+ * { text: "Edit", onclick: () => edit() },
4557
+ * { divider: true },
4558
+ * { text: "Delete", onclick: () => del() }
4559
+ * ]
4560
+ * });
4561
+ */
4562
+ function makeDropdown(props = {}) {
4563
+ const {
4564
+ trigger,
4565
+ items = [],
4566
+ align = 'start',
4567
+ variant = 'primary',
4568
+ className = ''
4569
+ } = props;
4570
+
4571
+ var triggerTaco;
4572
+ if (typeof trigger === 'string' || trigger === undefined) {
4573
+ triggerTaco = {
4574
+ t: 'button',
4575
+ a: {
4576
+ class: `bw-btn bw-btn-${variant} bw-dropdown-toggle`,
4577
+ type: 'button',
4578
+ onclick: function(e) {
4579
+ var dropdown = e.target.closest('.bw-dropdown');
4580
+ var menu = dropdown.querySelector('.bw-dropdown-menu');
4581
+ menu.classList.toggle('bw-dropdown-show');
4582
+ }
4583
+ },
4584
+ c: trigger || 'Dropdown'
4585
+ };
4586
+ } else {
4587
+ triggerTaco = trigger;
4588
+ }
4589
+
4590
+ return {
4591
+ t: 'div',
4592
+ a: { class: `bw-dropdown ${className}`.trim() },
4593
+ c: [
4594
+ triggerTaco,
4595
+ {
4596
+ t: 'div',
4597
+ a: { class: `bw-dropdown-menu ${align === 'end' ? 'bw-dropdown-menu-end' : ''}`.trim() },
4598
+ c: items.map(function(item) {
4599
+ if (item.divider) {
4600
+ return { t: 'hr', a: { class: 'bw-dropdown-divider' } };
4601
+ }
4602
+ return {
4603
+ t: 'a',
4604
+ a: {
4605
+ class: `bw-dropdown-item ${item.disabled ? 'disabled' : ''}`.trim(),
4606
+ href: item.href || '#',
4607
+ onclick: item.disabled ? undefined : function(e) {
4608
+ if (!item.href) e.preventDefault();
4609
+ var dropdown = e.target.closest('.bw-dropdown');
4610
+ var menu = dropdown.querySelector('.bw-dropdown-menu');
4611
+ menu.classList.remove('bw-dropdown-show');
4612
+ if (item.onclick) item.onclick(e);
4613
+ }
4614
+ },
4615
+ c: item.text
4616
+ };
4617
+ })
4618
+ }
4619
+ ],
4620
+ o: {
4621
+ type: 'dropdown',
4622
+ mounted: function(el) {
4623
+ // Click outside to close
4624
+ var outsideHandler = function(e) {
4625
+ if (!el.contains(e.target)) {
4626
+ var menu = el.querySelector('.bw-dropdown-menu');
4627
+ if (menu) menu.classList.remove('bw-dropdown-show');
4628
+ }
4629
+ };
4630
+ document.addEventListener('click', outsideHandler);
4631
+ el._bw_outsideHandler = outsideHandler;
4632
+ },
4633
+ unmount: function(el) {
4634
+ if (el._bw_outsideHandler) {
4635
+ document.removeEventListener('click', el._bw_outsideHandler);
4636
+ }
4637
+ }
4638
+ }
4639
+ };
4640
+ }
4641
+
4642
+ /**
4643
+ * Create a toggle switch (styled checkbox)
4644
+ *
4645
+ * @param {Object} [props] - Switch configuration
4646
+ * @param {string} [props.label] - Switch label text
4647
+ * @param {boolean} [props.checked=false] - Whether the switch is on
4648
+ * @param {string} [props.id] - Element ID (links label to switch)
4649
+ * @param {string} [props.name] - Input name attribute
4650
+ * @param {boolean} [props.disabled=false] - Whether the switch is disabled
4651
+ * @param {string} [props.className] - Additional CSS classes
4652
+ * @returns {Object} TACO object representing a toggle switch
4653
+ * @category Component Builders
4654
+ * @example
4655
+ * const toggle = makeSwitch({
4656
+ * label: "Dark mode",
4657
+ * checked: false,
4658
+ * onchange: (e) => toggleDark(e.target.checked)
4659
+ * });
4660
+ */
4661
+ function makeSwitch(props = {}) {
4662
+ const {
4663
+ label,
4664
+ checked = false,
4665
+ id,
4666
+ name,
4667
+ disabled = false,
4668
+ className = '',
4669
+ ...eventHandlers
4670
+ } = props;
4671
+
4672
+ return {
4673
+ t: 'div',
4674
+ a: { class: `bw-form-check bw-form-switch ${className}`.trim() },
4675
+ c: [
4676
+ {
4677
+ t: 'input',
4678
+ a: {
4679
+ type: 'checkbox',
4680
+ class: 'bw-form-check-input bw-switch-input',
4681
+ role: 'switch',
4682
+ checked,
4683
+ id,
4684
+ name,
4685
+ disabled,
4686
+ ...eventHandlers
4687
+ }
4688
+ },
4689
+ label && {
4690
+ t: 'label',
4691
+ a: { class: 'bw-form-check-label', for: id },
4692
+ c: label
4693
+ }
4694
+ ].filter(Boolean)
4695
+ };
4696
+ }
4697
+
4698
+ /**
4699
+ * Create a skeleton loading placeholder
4700
+ *
4701
+ * @param {Object} [props] - Skeleton configuration
4702
+ * @param {string} [props.variant="text"] - Shape variant ("text", "circle", "rect")
4703
+ * @param {string} [props.width] - Custom width (e.g. "200px", "100%")
4704
+ * @param {string} [props.height] - Custom height (e.g. "20px")
4705
+ * @param {number} [props.count=1] - Number of skeleton lines (for text variant)
4706
+ * @param {string} [props.className] - Additional CSS classes
4707
+ * @returns {Object} TACO object representing a skeleton placeholder
4708
+ * @category Component Builders
4709
+ * @example
4710
+ * const skeleton = makeSkeleton({ variant: "text", count: 3, width: "100%" });
4711
+ */
4712
+ function makeSkeleton(props = {}) {
4713
+ const {
4714
+ variant = 'text',
4715
+ width,
4716
+ height,
4717
+ count = 1,
4718
+ className = ''
4719
+ } = props;
4720
+
4721
+ if (variant === 'circle') {
4722
+ var circleSize = width || height || '3rem';
4723
+ return {
4724
+ t: 'div',
4725
+ a: {
4726
+ class: `bw-skeleton bw-skeleton-circle ${className}`.trim(),
4727
+ style: { width: circleSize, height: circleSize }
4728
+ }
4729
+ };
4730
+ }
4731
+
4732
+ if (variant === 'rect') {
4733
+ return {
4734
+ t: 'div',
4735
+ a: {
4736
+ class: `bw-skeleton bw-skeleton-rect ${className}`.trim(),
4737
+ style: {
4738
+ width: width || '100%',
4739
+ height: height || '120px'
4740
+ }
4741
+ }
4742
+ };
4743
+ }
4744
+
4745
+ // Text variant — multiple lines
4746
+ if (count === 1) {
4747
+ return {
4748
+ t: 'div',
4749
+ a: {
4750
+ class: `bw-skeleton bw-skeleton-text ${className}`.trim(),
4751
+ style: {
4752
+ width: width || '100%',
4753
+ height: height || '1em'
4754
+ }
4755
+ }
4756
+ };
4757
+ }
3458
4758
 
3459
- this.children.tabPanes.forEach((pane, i) => {
3460
- if (i === index) {
3461
- pane.classList.add('active');
3462
- } else {
3463
- pane.classList.remove('active');
4759
+ var lines = [];
4760
+ for (var i = 0; i < count; i++) {
4761
+ lines.push({
4762
+ t: 'div',
4763
+ a: {
4764
+ class: 'bw-skeleton bw-skeleton-text',
4765
+ style: {
4766
+ width: i === count - 1 ? '75%' : (width || '100%'),
4767
+ height: height || '1em'
4768
+ }
3464
4769
  }
3465
4770
  });
4771
+ }
3466
4772
 
3467
- this.state.activeIndex = index;
3468
- return this;
4773
+ return {
4774
+ t: 'div',
4775
+ a: { class: `bw-skeleton-group ${className}`.trim() },
4776
+ c: lines
4777
+ };
4778
+ }
4779
+
4780
+ /**
4781
+ * Create a user avatar with image or initials fallback
4782
+ *
4783
+ * @param {Object} [props] - Avatar configuration
4784
+ * @param {string} [props.src] - Image source URL
4785
+ * @param {string} [props.alt] - Image alt text
4786
+ * @param {string} [props.initials] - Fallback initials (e.g. "JD")
4787
+ * @param {string} [props.size="md"] - Size ("sm", "md", "lg", "xl")
4788
+ * @param {string} [props.variant="primary"] - Background color variant for initials
4789
+ * @param {string} [props.className] - Additional CSS classes
4790
+ * @returns {Object} TACO object representing an avatar
4791
+ * @category Component Builders
4792
+ * @example
4793
+ * const avatar = makeAvatar({ src: "/photo.jpg", alt: "Jane Doe", size: "lg" });
4794
+ * const avatarInitials = makeAvatar({ initials: "JD", variant: "success" });
4795
+ */
4796
+ function makeAvatar(props = {}) {
4797
+ const {
4798
+ src,
4799
+ alt = '',
4800
+ initials,
4801
+ size = 'md',
4802
+ variant = 'primary',
4803
+ className = ''
4804
+ } = props;
4805
+
4806
+ if (src) {
4807
+ return {
4808
+ t: 'img',
4809
+ a: {
4810
+ class: `bw-avatar bw-avatar-${size} ${className}`.trim(),
4811
+ src: src,
4812
+ alt: alt
4813
+ }
4814
+ };
3469
4815
  }
4816
+
4817
+ return {
4818
+ t: 'div',
4819
+ a: {
4820
+ class: `bw-avatar bw-avatar-${size} bw-avatar-${variant} ${className}`.trim()
4821
+ },
4822
+ c: initials || ''
4823
+ };
3470
4824
  }
3471
4825
 
3472
4826
  /**
3473
- * Create a code demo component for documentation pages
3474
- *
3475
- * Displays a live result alongside source code in a tabbed interface.
3476
- * Includes a copy-to-clipboard button on the code tab.
3477
- *
3478
- * @param {Object} [props] - Code demo configuration
3479
- * @param {string} [props.title] - Demo title heading
3480
- * @param {string} [props.description] - Demo description text
3481
- * @param {string} [props.code] - Source code to display (adds a "Code" tab when present)
3482
- * @param {string|Object|Array} [props.result] - Live result content for the "Result" tab
3483
- * @param {string} [props.language="javascript"] - Code language for syntax class
3484
- * @returns {Object} TACO object representing a code demo with tabbed Result/Code views
4827
+ * Create a carousel/slideshow component with slide transitions
4828
+ *
4829
+ * Supports image slides, TACO content slides, captions, prev/next controls,
4830
+ * dot indicators, and optional auto-play. Uses CSS translateX transitions.
4831
+ *
4832
+ * @param {Object} [props] - Carousel configuration
4833
+ * @param {Array<Object>} [props.items=[]] - Slide items
4834
+ * @param {string|Object} props.items[].content - Slide content (TACO, string, or img element)
4835
+ * @param {string} [props.items[].caption] - Caption text shown at bottom of slide
4836
+ * @param {boolean} [props.showControls=true] - Show prev/next arrow buttons
4837
+ * @param {boolean} [props.showIndicators=true] - Show dot navigation
4838
+ * @param {boolean} [props.autoPlay=false] - Auto-advance slides
4839
+ * @param {number} [props.interval=5000] - Auto-advance interval in ms
4840
+ * @param {string} [props.height='300px'] - Carousel height
4841
+ * @param {number} [props.startIndex=0] - Initial slide index
4842
+ * @param {string} [props.className] - Additional CSS classes
4843
+ * @returns {Object} TACO object representing a carousel
3485
4844
  * @category Component Builders
3486
4845
  * @example
3487
- * const demo = makeCodeDemo({
3488
- * title: "Button Example",
3489
- * description: "A simple primary button",
3490
- * code: 'makeButton({ text: "Click me" })',
3491
- * result: makeButton({ text: "Click me" })
4846
+ * const carousel = makeCarousel({
4847
+ * items: [
4848
+ * { content: { t: 'img', a: { src: 'photo.jpg' } }, caption: 'Photo 1' },
4849
+ * { content: { t: 'div', c: 'Text slide' } }
4850
+ * ],
4851
+ * autoPlay: true,
4852
+ * interval: 3000
3492
4853
  * });
3493
4854
  */
3494
- function makeCodeDemo(props = {}) {
4855
+ function makeCarousel(props = {}) {
3495
4856
  const {
3496
- title,
3497
- description,
3498
- code,
3499
- result,
3500
- language = 'javascript'
4857
+ items = [],
4858
+ showControls = true,
4859
+ showIndicators = true,
4860
+ autoPlay = false,
4861
+ interval = 5000,
4862
+ height = '300px',
4863
+ startIndex = 0,
4864
+ className = ''
3501
4865
  } = props;
3502
4866
 
3503
- // Generate unique ID for this demo
3504
- `demo-${Math.random().toString(36).substr(2, 9)}`;
4867
+ // Shared navigation logic
4868
+ function goToSlide(carouselEl, index) {
4869
+ var total = carouselEl.querySelectorAll('.bw-carousel-slide').length;
4870
+ if (index < 0) index = total - 1;
4871
+ if (index >= total) index = 0;
4872
+ carouselEl.setAttribute('data-carousel-index', index);
4873
+ var track = carouselEl.querySelector('.bw-carousel-track');
4874
+ track.style.transform = 'translateX(-' + (index * 100) + '%)';
4875
+ // Update indicators
4876
+ var indicators = carouselEl.querySelectorAll('.bw-carousel-indicator');
4877
+ for (var i = 0; i < indicators.length; i++) {
4878
+ if (i === index) {
4879
+ indicators[i].classList.add('active');
4880
+ } else {
4881
+ indicators[i].classList.remove('active');
4882
+ }
4883
+ }
4884
+ }
3505
4885
 
3506
- const tabs = [
4886
+ // Arrow SVGs (inline data URIs, same pattern as accordion chevrons)
4887
+ var prevArrow = "data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e";
4888
+ var nextArrow = "data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e";
4889
+
4890
+ var slides = items.map(function(item) {
4891
+ var slideContent = [
4892
+ item.content,
4893
+ item.caption && {
4894
+ t: 'div',
4895
+ a: { class: 'bw-carousel-caption' },
4896
+ c: item.caption
4897
+ }
4898
+ ].filter(Boolean);
4899
+
4900
+ return {
4901
+ t: 'div',
4902
+ a: { class: 'bw-carousel-slide' },
4903
+ c: slideContent.length === 1 ? slideContent[0] : slideContent
4904
+ };
4905
+ });
4906
+
4907
+ var children = [
4908
+ // Track
3507
4909
  {
3508
- label: 'Result',
3509
- active: true,
3510
- content: result
4910
+ t: 'div',
4911
+ a: {
4912
+ class: 'bw-carousel-track',
4913
+ style: 'transform: translateX(-' + (startIndex * 100) + '%)'
4914
+ },
4915
+ c: slides
3511
4916
  }
3512
4917
  ];
3513
4918
 
3514
- // Only add Code tab if code is provided
3515
- if (code) {
3516
- tabs.push({
3517
- label: 'Code',
3518
- content: {
3519
- t: 'div',
3520
- a: { style: 'position: relative;' },
3521
- c: [
3522
- {
3523
- t: 'button',
3524
- a: {
3525
- class: 'bw-copy-btn',
3526
- style: 'position: absolute; top: 0.5rem; right: 0.5rem; padding: 0.25rem 0.625rem; font-size: 0.6875rem; background: rgba(255,255,255,0.12); color: #aaa; border: 1px solid rgba(255,255,255,0.15); border-radius: 4px; cursor: pointer; font-family: inherit; transition: all 0.15s;',
3527
- onclick: (e) => {
3528
- navigator.clipboard.writeText(code).then(() => {
3529
- const btn = e.target;
3530
- const originalText = btn.textContent;
3531
- btn.textContent = 'Copied!';
3532
- btn.style.background = '#006666';
3533
- btn.style.color = '#fff';
3534
- setTimeout(() => {
3535
- btn.textContent = originalText;
3536
- btn.style.background = 'rgba(255,255,255,0.12)';
3537
- btn.style.color = '#aaa';
3538
- }, 2000);
3539
- });
3540
- }
3541
- },
3542
- c: 'Copy'
3543
- },
3544
- {
3545
- t: 'pre',
3546
- a: {
3547
- style: 'margin: 0; background: #1e293b; border: none; border-radius: 6px; overflow-x: auto;'
3548
- },
3549
- c: {
3550
- t: 'code',
3551
- a: {
3552
- class: `language-${language}`,
3553
- style: 'display: block; padding: 1.25rem; font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace; font-size: 0.8125rem; line-height: 1.6; color: #e2e8f0;'
3554
- },
3555
- c: code
4919
+ // Prev/Next controls
4920
+ if (showControls && items.length > 1) {
4921
+ children.push({
4922
+ t: 'button',
4923
+ a: {
4924
+ class: 'bw-carousel-control bw-carousel-control-prev',
4925
+ type: 'button',
4926
+ 'aria-label': 'Previous slide',
4927
+ onclick: function(e) {
4928
+ var carousel = e.target.closest('.bw-carousel');
4929
+ var idx = parseInt(carousel.getAttribute('data-carousel-index') || '0');
4930
+ goToSlide(carousel, idx - 1);
4931
+ }
4932
+ },
4933
+ c: { t: 'img', a: { src: prevArrow, alt: '', role: 'presentation' } }
4934
+ });
4935
+ children.push({
4936
+ t: 'button',
4937
+ a: {
4938
+ class: 'bw-carousel-control bw-carousel-control-next',
4939
+ type: 'button',
4940
+ 'aria-label': 'Next slide',
4941
+ onclick: function(e) {
4942
+ var carousel = e.target.closest('.bw-carousel');
4943
+ var idx = parseInt(carousel.getAttribute('data-carousel-index') || '0');
4944
+ goToSlide(carousel, idx + 1);
4945
+ }
4946
+ },
4947
+ c: { t: 'img', a: { src: nextArrow, alt: '', role: 'presentation' } }
4948
+ });
4949
+ }
4950
+
4951
+ // Indicators
4952
+ if (showIndicators && items.length > 1) {
4953
+ children.push({
4954
+ t: 'div',
4955
+ a: { class: 'bw-carousel-indicators' },
4956
+ c: items.map(function(_, i) {
4957
+ return {
4958
+ t: 'button',
4959
+ a: {
4960
+ class: 'bw-carousel-indicator' + (i === startIndex ? ' active' : ''),
4961
+ type: 'button',
4962
+ 'aria-label': 'Go to slide ' + (i + 1),
4963
+ 'data-slide-index': i,
4964
+ onclick: function(e) {
4965
+ var carousel = e.target.closest('.bw-carousel');
4966
+ var idx = parseInt(e.target.getAttribute('data-slide-index'));
4967
+ goToSlide(carousel, idx);
3556
4968
  }
3557
4969
  }
3558
- ]
3559
- }
4970
+ };
4971
+ })
3560
4972
  });
3561
4973
  }
3562
4974
 
3563
- const content = [
3564
- title && { t: 'h3', c: title },
3565
- description && {
3566
- t: 'p',
3567
- a: { style: 'color: #6c757d; margin-bottom: 1rem;' },
3568
- c: description
3569
- },
3570
- makeTabs({ tabs})
3571
- ].filter(Boolean);
3572
-
3573
4975
  return {
3574
4976
  t: 'div',
3575
- a: { class: 'bw-code-demo' },
3576
- c: content
4977
+ a: {
4978
+ class: ('bw-carousel ' + className).trim(),
4979
+ style: 'height: ' + height,
4980
+ 'data-carousel-index': startIndex
4981
+ },
4982
+ c: children,
4983
+ o: {
4984
+ type: 'carousel',
4985
+ state: { activeIndex: startIndex, autoPlay: autoPlay, interval: interval },
4986
+ mounted: autoPlay ? function(el) {
4987
+ var intervalId = setInterval(function() {
4988
+ 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) {
4994
+ if (el._bw_carouselInterval) {
4995
+ clearInterval(el._bw_carouselInterval);
4996
+ }
4997
+ } : undefined
4998
+ }
3577
4999
  };
3578
5000
  }
3579
5001
 
3580
- /**
3581
- * Registry mapping component type names to their handle classes
3582
- *
3583
- * Used by bw.createCard(), bw.createTable(), etc. to wrap rendered
3584
- * DOM elements in the appropriate imperative handle.
3585
- *
3586
- * @type {Object.<string, Function>}
3587
- */
3588
5002
  const componentHandles = {
3589
5003
  card: CardHandle,
3590
5004
  table: TableHandle,
3591
5005
  navbar: NavbarHandle,
3592
- tabs: TabsHandle
5006
+ tabs: TabsHandle,
5007
+ modal: ModalHandle
3593
5008
  };
3594
5009
 
3595
5010
  var components = /*#__PURE__*/Object.freeze({
3596
5011
  __proto__: null,
3597
5012
  CardHandle: CardHandle,
5013
+ ModalHandle: ModalHandle,
3598
5014
  NavbarHandle: NavbarHandle,
3599
5015
  TableHandle: TableHandle,
3600
5016
  TabsHandle: TabsHandle,
3601
5017
  componentHandles: componentHandles,
5018
+ makeAccordion: makeAccordion,
3602
5019
  makeAlert: makeAlert,
5020
+ makeAvatar: makeAvatar,
3603
5021
  makeBadge: makeBadge,
3604
5022
  makeBreadcrumb: makeBreadcrumb,
3605
5023
  makeButton: makeButton,
5024
+ makeButtonGroup: makeButtonGroup,
3606
5025
  makeCTA: makeCTA,
3607
5026
  makeCard: makeCard,
5027
+ makeCarousel: makeCarousel,
3608
5028
  makeCheckbox: makeCheckbox,
3609
5029
  makeCodeDemo: makeCodeDemo,
3610
5030
  makeCol: makeCol,
3611
5031
  makeContainer: makeContainer,
5032
+ makeDropdown: makeDropdown,
3612
5033
  makeFeatureGrid: makeFeatureGrid,
3613
5034
  makeForm: makeForm,
3614
5035
  makeFormGroup: makeFormGroup,
3615
5036
  makeHero: makeHero,
3616
5037
  makeInput: makeInput,
3617
5038
  makeListGroup: makeListGroup,
5039
+ makeModal: makeModal,
3618
5040
  makeNav: makeNav,
3619
5041
  makeNavbar: makeNavbar,
5042
+ makePagination: makePagination,
3620
5043
  makeProgress: makeProgress,
5044
+ makeRadio: makeRadio,
3621
5045
  makeRow: makeRow,
3622
5046
  makeSection: makeSection,
3623
5047
  makeSelect: makeSelect,
5048
+ makeSkeleton: makeSkeleton,
3624
5049
  makeSpinner: makeSpinner,
3625
5050
  makeStack: makeStack,
5051
+ makeSwitch: makeSwitch,
3626
5052
  makeTabs: makeTabs,
3627
- makeTextarea: makeTextarea
5053
+ makeTextarea: makeTextarea,
5054
+ makeToast: makeToast
3628
5055
  });
3629
5056
 
3630
5057
  /**
@@ -4047,6 +5474,26 @@ bw.escapeHTML = function(str) {
4047
5474
  return str.replace(/[&<>"'/]/g, (char) => escapeMap[char]);
4048
5475
  };
4049
5476
 
5477
+ /**
5478
+ * Mark a string as raw HTML so it will not be escaped by bw.html() or bw.createDOM().
5479
+ *
5480
+ * By default, bitwrench escapes all text content to prevent XSS. Use bw.raw()
5481
+ * when you need to embed pre-sanitized HTML, entities, or inline markup.
5482
+ *
5483
+ * @param {string} str - HTML string to mark as raw
5484
+ * @returns {Object} Marked object recognized by bw.html() and bw.createDOM()
5485
+ * @category DOM Generation
5486
+ * @see bw.escapeHTML
5487
+ * @see bw.html
5488
+ * @example
5489
+ * bw.raw('Hello &mdash; World')
5490
+ * // Used in TACO content:
5491
+ * { t: 'p', c: bw.raw('Price: <strong>$9.99</strong>') }
5492
+ */
5493
+ bw.raw = function(str) {
5494
+ return { __bw_raw: true, v: String(str) };
5495
+ };
5496
+
4050
5497
  /**
4051
5498
  * Normalize CSS class names by converting underscores to hyphens for bw-prefixed classes.
4052
5499
  *
@@ -4098,6 +5545,11 @@ bw.html = function(taco, options = {}) {
4098
5545
  return taco.map(t => bw.html(t, options)).join('');
4099
5546
  }
4100
5547
 
5548
+ // Handle bw.raw() marked content
5549
+ if (taco && taco.__bw_raw) {
5550
+ return taco.v;
5551
+ }
5552
+
4101
5553
  // Handle primitives and non-TACO objects
4102
5554
  if (typeof taco !== 'object' || !taco.t) {
4103
5555
  return options.raw ? String(taco) : bw.escapeHTML(String(taco));
@@ -4198,12 +5650,21 @@ bw.createDOM = function(taco, options = {}) {
4198
5650
 
4199
5651
  // Handle null/undefined
4200
5652
  if (taco == null) return document.createTextNode('');
4201
-
5653
+
5654
+ // Handle bw.raw() marked content — inject as HTML
5655
+ if (taco && taco.__bw_raw) {
5656
+ var frag = document.createDocumentFragment();
5657
+ var tmp = document.createElement('span');
5658
+ tmp.innerHTML = taco.v;
5659
+ while (tmp.firstChild) frag.appendChild(tmp.firstChild);
5660
+ return frag;
5661
+ }
5662
+
4202
5663
  // Handle text nodes
4203
5664
  if (typeof taco !== 'object' || !taco.t) {
4204
5665
  return document.createTextNode(String(taco));
4205
5666
  }
4206
-
5667
+
4207
5668
  const { t: tag, a: attrs = {}, c: content, o: opts = {} } = taco;
4208
5669
 
4209
5670
  // Create element
@@ -4268,6 +5729,9 @@ bw.createDOM = function(taco, options = {}) {
4268
5729
  }
4269
5730
  }
4270
5731
  });
5732
+ } else if (typeof content === 'object' && content.__bw_raw) {
5733
+ // Raw HTML content — inject via innerHTML
5734
+ el.innerHTML = content.v;
4271
5735
  } else if (typeof content === 'object' && content.t) {
4272
5736
  var childEl = bw.createDOM(content, options);
4273
5737
  el.appendChild(childEl);
@@ -4766,6 +6230,16 @@ bw.patch = function(id, content, attr) {
4766
6230
  if (attr) {
4767
6231
  // Patch an attribute
4768
6232
  el.setAttribute(attr, String(content));
6233
+ } else if (Array.isArray(content)) {
6234
+ // Patch with array of children (strings and/or TACOs)
6235
+ el.innerHTML = '';
6236
+ content.forEach(function(item) {
6237
+ if (typeof item === 'string' || typeof item === 'number') {
6238
+ el.appendChild(document.createTextNode(String(item)));
6239
+ } else if (item && item.t) {
6240
+ el.appendChild(bw.createDOM(item));
6241
+ }
6242
+ });
4769
6243
  } else if (typeof content === 'object' && content !== null && content.t) {
4770
6244
  // Patch with a TACO — replace children
4771
6245
  el.innerHTML = '';
@@ -5962,6 +7436,7 @@ bw.getURLParam = function(key, defaultValue) {
5962
7436
  * @see bw.makeTable
5963
7437
  */
5964
7438
  bw.htmlTable = function(data, opts = {}) {
7439
+ console.warn('bw.htmlTable() is deprecated. Use bw.makeTableFromArray() for TACO output or bw.makeTable() for object-array data.');
5965
7440
  if (bw.typeOf(data) !== "array" || data.length < 1) return "";
5966
7441
 
5967
7442
  const dopts = {
@@ -6061,6 +7536,7 @@ bw._attrsToStr = function(attrs) {
6061
7536
  * @see bw.makeTabs
6062
7537
  */
6063
7538
  bw.htmlTabs = function(tabData, opts = {}) {
7539
+ console.warn('bw.htmlTabs() is deprecated. Use bw.makeTabs() instead.');
6064
7540
  if (bw.typeOf(tabData) !== "array" || tabData.length < 1) return "";
6065
7541
 
6066
7542
  const dopts = {
@@ -6107,6 +7583,7 @@ bw.htmlTabs = function(tabData, opts = {}) {
6107
7583
  * @category Legacy (v1)
6108
7584
  */
6109
7585
  bw.selectTabContent = function(tabElement) {
7586
+ console.warn('bw.selectTabContent() is deprecated. Use bw.makeTabs() instead.');
6110
7587
  if (!bw._isBrowser || !tabElement) return;
6111
7588
 
6112
7589
  const container = tabElement.closest(".bw-tab-container");
@@ -6635,12 +8112,21 @@ bw.makeTable = function(config) {
6635
8112
  const {
6636
8113
  data = [],
6637
8114
  columns,
6638
- className = "table",
8115
+ className = '',
8116
+ striped = false,
8117
+ hover = false,
6639
8118
  sortable = true,
6640
8119
  onSort,
6641
8120
  sortColumn,
6642
8121
  sortDirection = 'asc'
6643
8122
  } = config;
8123
+
8124
+ // Build class list: always include bw-table, add striped/hover, append user className
8125
+ let cls = 'bw-table';
8126
+ if (striped) cls += ' bw-table-striped';
8127
+ if (hover) cls += ' bw-table-hover';
8128
+ if (className) cls += ' ' + className;
8129
+ cls = cls.trim();
6644
8130
 
6645
8131
  // Auto-detect columns if not provided
6646
8132
  const cols = columns || (data.length > 0
@@ -6728,11 +8214,176 @@ bw.makeTable = function(config) {
6728
8214
 
6729
8215
  return {
6730
8216
  t: 'table',
6731
- a: { class: className },
8217
+ a: { class: cls },
6732
8218
  c: [thead, tbody]
6733
8219
  };
6734
8220
  };
6735
8221
 
8222
+ /**
8223
+ * Create a table from a 2D array.
8224
+ *
8225
+ * Converts a 2D array into the object-array format that `bw.makeTable()`
8226
+ * expects, then delegates. By default, the first row is used as column
8227
+ * headers. All standard `makeTable` props (striped, hover, sortable,
8228
+ * columns, onSort, etc.) are passed through.
8229
+ *
8230
+ * @param {Object} config - Configuration object
8231
+ * @param {Array<Array>} config.data - 2D array of values
8232
+ * @param {boolean} [config.headerRow=true] - Treat first row as column headers
8233
+ * @param {boolean} [config.striped=false] - Striped rows
8234
+ * @param {boolean} [config.hover=false] - Hover highlight
8235
+ * @param {boolean} [config.sortable=true] - Enable sort
8236
+ * @param {Array<Object>} [config.columns] - Override auto-generated column defs
8237
+ * @param {string} [config.className=''] - Additional CSS classes
8238
+ * @param {Function} [config.onSort] - Sort callback
8239
+ * @param {string} [config.sortColumn] - Currently sorted column key
8240
+ * @param {string} [config.sortDirection='asc'] - Sort direction
8241
+ * @returns {Object} TACO object for table
8242
+ * @category Component Builders
8243
+ * @see bw.makeTable
8244
+ * @example
8245
+ * bw.makeTableFromArray({
8246
+ * data: [
8247
+ * ['Name', 'Role', 'Status'],
8248
+ * ['Alice', 'Engineer', 'Active'],
8249
+ * ['Bob', 'Designer', 'Away']
8250
+ * ],
8251
+ * striped: true,
8252
+ * hover: true
8253
+ * });
8254
+ */
8255
+ bw.makeTableFromArray = function(config) {
8256
+ const { data = [], headerRow = true, columns, ...rest } = config;
8257
+
8258
+ if (!Array.isArray(data) || data.length === 0) {
8259
+ return bw.makeTable({ data: [], columns: columns || [], ...rest });
8260
+ }
8261
+
8262
+ // Determine headers
8263
+ let headers;
8264
+ let rows;
8265
+ if (headerRow && data.length > 0) {
8266
+ headers = data[0].map(function(h) { return String(h); });
8267
+ rows = data.slice(1);
8268
+ } else {
8269
+ // Generate col0, col1, ... headers
8270
+ const width = data[0].length;
8271
+ headers = [];
8272
+ for (let i = 0; i < width; i++) {
8273
+ headers.push('col' + i);
8274
+ }
8275
+ rows = data;
8276
+ }
8277
+
8278
+ // Convert rows to object arrays
8279
+ const objData = rows.map(function(row) {
8280
+ const obj = {};
8281
+ headers.forEach(function(key, i) {
8282
+ obj[key] = row[i] !== undefined ? row[i] : '';
8283
+ });
8284
+ return obj;
8285
+ });
8286
+
8287
+ // Auto-generate column defs if not provided
8288
+ const cols = columns || headers.map(function(key) {
8289
+ return { key: key, label: key };
8290
+ });
8291
+
8292
+ return bw.makeTable({ data: objData, columns: cols, ...rest });
8293
+ };
8294
+
8295
+ /**
8296
+ * Create a vertical bar chart from data.
8297
+ *
8298
+ * Renders a pure-CSS bar chart using flexbox and percentage heights.
8299
+ * No canvas, SVG, or external charting library required.
8300
+ *
8301
+ * @param {Object} config - Chart configuration
8302
+ * @param {Array<Object>} config.data - Array of data objects
8303
+ * @param {string} [config.labelKey='label'] - Key for bar labels
8304
+ * @param {string} [config.valueKey='value'] - Key for bar values
8305
+ * @param {string} [config.title] - Chart title
8306
+ * @param {string} [config.color='#006666'] - Bar color (hex or CSS color)
8307
+ * @param {string} [config.height='200px'] - Height of the chart area
8308
+ * @param {Function} [config.formatValue] - Value label formatter: (value) => string
8309
+ * @param {boolean} [config.showValues=true] - Show value labels above bars
8310
+ * @param {boolean} [config.showLabels=true] - Show labels below bars
8311
+ * @param {string} [config.className=''] - Additional CSS classes
8312
+ * @returns {Object} TACO object
8313
+ * @category Component Builders
8314
+ * @example
8315
+ * bw.makeBarChart({
8316
+ * data: [
8317
+ * { label: 'Jan', value: 12400 },
8318
+ * { label: 'Feb', value: 15800 },
8319
+ * { label: 'Mar', value: 9200 }
8320
+ * ],
8321
+ * title: 'Monthly Revenue',
8322
+ * color: '#0077b6',
8323
+ * formatValue: (v) => '$' + (v / 1000).toFixed(1) + 'k'
8324
+ * });
8325
+ */
8326
+ bw.makeBarChart = function(config) {
8327
+ const {
8328
+ data = [],
8329
+ labelKey = 'label',
8330
+ valueKey = 'value',
8331
+ title,
8332
+ color = '#006666',
8333
+ height = '200px',
8334
+ formatValue,
8335
+ showValues = true,
8336
+ showLabels = true,
8337
+ className = ''
8338
+ } = config;
8339
+
8340
+ if (!Array.isArray(data) || data.length === 0) {
8341
+ return { t: 'div', a: { class: ('bw-bar-chart-container ' + className).trim() }, c: '' };
8342
+ }
8343
+
8344
+ const values = data.map(function(d) { return Number(d[valueKey]) || 0; });
8345
+ const maxVal = Math.max.apply(null, values);
8346
+
8347
+ const bars = data.map(function(d, i) {
8348
+ const val = values[i];
8349
+ const pct = maxVal > 0 ? (val / maxVal * 100) : 0;
8350
+ const formatted = formatValue ? formatValue(val) : String(val);
8351
+
8352
+ const children = [];
8353
+ if (showValues) {
8354
+ children.push({ t: 'div', a: { class: 'bw-bar-value' }, c: formatted });
8355
+ }
8356
+ children.push({
8357
+ t: 'div',
8358
+ a: {
8359
+ class: 'bw-bar',
8360
+ style: 'height:' + pct + '%;background:' + color + ';'
8361
+ }
8362
+ });
8363
+ if (showLabels) {
8364
+ children.push({ t: 'div', a: { class: 'bw-bar-label' }, c: String(d[labelKey] || '') });
8365
+ }
8366
+
8367
+ return { t: 'div', a: { class: 'bw-bar-group' }, c: children };
8368
+ });
8369
+
8370
+ const chartChildren = [];
8371
+ if (title) {
8372
+ chartChildren.push({ t: 'h3', a: { class: 'bw-bar-chart-title' }, c: title });
8373
+ }
8374
+ chartChildren.push({
8375
+ t: 'div',
8376
+ a: { class: 'bw-bar-chart', style: 'height:' + height + ';' },
8377
+ c: bars
8378
+ });
8379
+
8380
+ return {
8381
+ t: 'div',
8382
+ a: { class: ('bw-bar-chart-container ' + className).trim() },
8383
+ c: chartChildren
8384
+ };
8385
+ };
8386
+
6736
8387
  /**
6737
8388
  * Create a responsive data table with title and optional wrapper
6738
8389
  *
@@ -6743,7 +8394,9 @@ bw.makeTable = function(config) {
6743
8394
  * @param {string} [config.title] - Table title heading
6744
8395
  * @param {Array<Object>} config.data - Array of row objects
6745
8396
  * @param {Array<Object>} [config.columns] - Column definitions
6746
- * @param {string} [config.className="table table-striped table-hover"] - Table CSS class
8397
+ * @param {string} [config.className=''] - Additional CSS classes for the table
8398
+ * @param {boolean} [config.striped=true] - Add striped row styling
8399
+ * @param {boolean} [config.hover=true] - Add hover row highlighting
6747
8400
  * @param {boolean} [config.responsive=true] - Wrap table in responsive overflow div
6748
8401
  * @returns {Object} TACO object for table with wrapper
6749
8402
  * @example
@@ -6758,7 +8411,9 @@ bw.makeDataTable = function(config) {
6758
8411
  title,
6759
8412
  data,
6760
8413
  columns,
6761
- className = "table table-striped table-hover",
8414
+ className = '',
8415
+ striped = true,
8416
+ hover = true,
6762
8417
  responsive = true,
6763
8418
  ...tableConfig
6764
8419
  } = config;
@@ -6767,6 +8422,8 @@ bw.makeDataTable = function(config) {
6767
8422
  data,
6768
8423
  columns,
6769
8424
  className,
8425
+ striped,
8426
+ hover,
6770
8427
  ...tableConfig
6771
8428
  });
6772
8429