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