bitwrench 2.0.10 → 2.0.11

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.11 | 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.11',
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-07T11:05:08.522Z'
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,121 @@ 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:hover')] = {
893
+ 'background-color': palette.light.light
894
+ };
895
+ rules[scopeSelector(scope, '.bw-accordion-body')] = {
896
+ 'border-top': '1px solid ' + palette.light.border
897
+ };
898
+ return rules;
899
+ }
900
+
901
+ function generateModalThemed(scope, palette) {
902
+ var rules = {};
903
+ rules[scopeSelector(scope, '.bw-modal-content')] = {
904
+ 'background-color': '#fff',
905
+ 'border-color': palette.light.border,
906
+ 'box-shadow': '0 0.5rem 1rem rgba(0,0,0,0.15)'
907
+ };
908
+ rules[scopeSelector(scope, '.bw-modal-header')] = {
909
+ 'border-bottom-color': palette.light.border
910
+ };
911
+ rules[scopeSelector(scope, '.bw-modal-footer')] = {
912
+ 'border-top-color': palette.light.border
913
+ };
914
+ rules[scopeSelector(scope, '.bw-modal-title')] = {
915
+ 'color': palette.dark.base
916
+ };
917
+ return rules;
918
+ }
919
+
920
+ function generateToastThemed(scope, palette) {
921
+ var rules = {};
922
+ rules[scopeSelector(scope, '.bw-toast')] = {
923
+ 'background-color': '#fff',
924
+ 'border-color': 'rgba(0,0,0,0.1)',
925
+ 'box-shadow': '0 0.5rem 1rem rgba(0,0,0,0.15)'
926
+ };
927
+ rules[scopeSelector(scope, '.bw-toast-header')] = {
928
+ 'border-bottom-color': 'rgba(0,0,0,0.05)'
929
+ };
930
+ var variants = ['primary', 'secondary', 'success', 'danger', 'warning', 'info'];
931
+ variants.forEach(function(v) {
932
+ rules[scopeSelector(scope, '.bw-toast-' + v)] = {
933
+ 'border-left': '4px solid ' + palette[v].base
934
+ };
935
+ });
936
+ return rules;
937
+ }
938
+
939
+ function generateDropdownThemed(scope, palette) {
940
+ var rules = {};
941
+ rules[scopeSelector(scope, '.bw-dropdown-menu')] = {
942
+ 'background-color': '#fff',
943
+ 'border-color': palette.light.border,
944
+ 'box-shadow': '0 0.5rem 1rem rgba(0,0,0,0.15)'
945
+ };
946
+ rules[scopeSelector(scope, '.bw-dropdown-item')] = {
947
+ 'color': palette.dark.base
948
+ };
949
+ rules[scopeSelector(scope, '.bw-dropdown-item:hover')] = {
950
+ 'color': palette.dark.hover,
951
+ 'background-color': palette.light.light
952
+ };
953
+ rules[scopeSelector(scope, '.bw-dropdown-item.disabled')] = {
954
+ 'color': palette.secondary.base
955
+ };
956
+ rules[scopeSelector(scope, '.bw-dropdown-divider')] = {
957
+ 'border-top-color': palette.light.border
958
+ };
959
+ return rules;
960
+ }
961
+
962
+ function generateSwitchThemed(scope, palette) {
963
+ var rules = {};
964
+ rules[scopeSelector(scope, '.bw-form-switch .bw-switch-input')] = {
965
+ 'background-color': palette.secondary.base,
966
+ 'border-color': palette.secondary.base
967
+ };
968
+ rules[scopeSelector(scope, '.bw-form-switch .bw-switch-input:checked')] = {
969
+ 'background-color': palette.primary.base,
970
+ 'border-color': palette.primary.base
971
+ };
972
+ rules[scopeSelector(scope, '.bw-form-switch .bw-switch-input:focus')] = {
973
+ 'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
974
+ };
975
+ return rules;
976
+ }
977
+
978
+ function generateSkeletonThemed(scope, palette) {
979
+ var rules = {};
980
+ rules[scopeSelector(scope, '.bw-skeleton')] = {
981
+ 'background-color': palette.light.border
982
+ };
983
+ return rules;
984
+ }
985
+
986
+ function generateAvatarThemed(scope, palette) {
987
+ var rules = {};
988
+ var variants = ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'];
989
+ variants.forEach(function(v) {
990
+ rules[scopeSelector(scope, '.bw-avatar-' + v)] = {
991
+ 'background-color': palette[v].base,
992
+ 'color': palette[v].textOn
993
+ };
994
+ });
995
+ return rules;
996
+ }
997
+
883
998
  /**
884
999
  * Generate all themed CSS rules from a palette and layout.
885
1000
  * Returns a flat CSS rules object (selector → declarations).
@@ -909,6 +1024,13 @@ function generateThemedCSS(scopeName, palette, layout) {
909
1024
  generateSpinnerThemed(scopeName, palette),
910
1025
  generateCloseButtonThemed(scopeName, palette),
911
1026
  generateSectionsThemed(scopeName, palette),
1027
+ generateAccordionThemed(scopeName, palette),
1028
+ generateModalThemed(scopeName, palette),
1029
+ generateToastThemed(scopeName, palette),
1030
+ generateDropdownThemed(scopeName, palette),
1031
+ generateSwitchThemed(scopeName, palette),
1032
+ generateSkeletonThemed(scopeName, palette),
1033
+ generateAvatarThemed(scopeName, palette),
912
1034
  generateUtilityColors(scopeName, palette)
913
1035
  );
914
1036
  }
@@ -1178,7 +1300,7 @@ function getStructuralStyles() {
1178
1300
  'position': 'relative', 'display': 'flex', 'flex-wrap': 'wrap',
1179
1301
  'align-items': 'center', 'justify-content': 'space-between', 'padding': '0.5rem 1.5rem'
1180
1302
  };
1181
- rules['.bw-navbar > .container'] = { 'display': 'flex', 'flex-wrap': 'wrap', 'align-items': 'center', 'justify-content': 'space-between' };
1303
+ rules['.bw-navbar > .bw-container, .bw-navbar > .container'] = { 'display': 'flex', 'flex-wrap': 'wrap', 'align-items': 'center', 'justify-content': 'space-between' };
1182
1304
  rules['.bw-navbar-brand'] = {
1183
1305
  'display': 'inline-flex', 'align-items': 'center', 'gap': '0.5rem',
1184
1306
  'padding-top': '0.25rem', 'padding-bottom': '0.25rem', 'margin-right': '1.5rem',
@@ -1223,11 +1345,13 @@ function getStructuralStyles() {
1223
1345
 
1224
1346
  // Badges (structural)
1225
1347
  rules['.bw-badge'] = {
1226
- 'display': 'inline-block', 'padding': '.35em .65em', 'font-size': '.75em',
1227
- 'font-weight': '700', 'line-height': '1', 'text-align': 'center',
1348
+ 'display': 'inline-block', 'padding': '.4em .75em', 'font-size': '.875em',
1349
+ 'font-weight': '600', 'line-height': '1.3', 'text-align': 'center',
1228
1350
  'white-space': 'nowrap', 'vertical-align': 'baseline', 'border-radius': '.375rem'
1229
1351
  };
1230
1352
  rules['.bw-badge:empty'] = { 'display': 'none' };
1353
+ rules['.bw-badge-sm'] = { 'font-size': '.75em', 'padding': '.25em .5em' };
1354
+ rules['.bw-badge-lg'] = { 'font-size': '1em', 'padding': '.5em .9em' };
1231
1355
  rules['.bw-badge-pill'] = { 'border-radius': '50rem' };
1232
1356
 
1233
1357
  // Progress (structural)
@@ -1294,7 +1418,8 @@ function getStructuralStyles() {
1294
1418
  rules['.bw-hero'] = { 'position': 'relative', 'overflow': 'hidden' };
1295
1419
  rules['.bw-hero-overlay'] = { 'position': 'absolute', 'top': '0', 'left': '0', 'right': '0', 'bottom': '0', 'z-index': '1' };
1296
1420
  rules['.bw-hero-content'] = { 'position': 'relative', 'z-index': '2' };
1297
- rules['.bw-hero-title'] = { 'font-weight': '300', 'letter-spacing': '-0.05rem' };
1421
+ rules['.bw-hero-title'] = { 'font-weight': '300', 'letter-spacing': '-0.05rem', 'color': 'inherit' };
1422
+ rules['.bw-hero-subtitle'] = { 'color': 'inherit' };
1298
1423
  rules['.bw-hero-actions'] = { 'display': 'flex', 'gap': '1rem', 'justify-content': 'center', 'flex-wrap': 'wrap' };
1299
1424
  rules['.bw-display-4'] = { 'font-size': 'calc(1.475rem + 2.7vw)', 'font-weight': '300', 'line-height': '1.2' };
1300
1425
  rules['.bw-lead'] = { 'font-size': '1.25rem', 'font-weight': '300' };
@@ -1367,6 +1492,156 @@ function getStructuralStyles() {
1367
1492
 
1368
1493
  // Code demo (structural)
1369
1494
  rules['.bw-code-demo'] = { 'margin-bottom': '2rem' };
1495
+ rules['.bw-code-pre'] = { 'margin': '0', 'border': 'none', 'border-radius': '6px', 'overflow-x': 'auto' };
1496
+ 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' };
1497
+ 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' };
1498
+
1499
+ // Button group (structural)
1500
+ rules['.bw-btn-group, .bw-btn-group-vertical'] = { 'position': 'relative', 'display': 'inline-flex', 'vertical-align': 'middle' };
1501
+ rules['.bw-btn-group > .bw-btn, .bw-btn-group-vertical > .bw-btn'] = { 'position': 'relative', 'flex': '1 1 auto', 'border-radius': '0', 'margin-left': '-1px' };
1502
+ rules['.bw-btn-group > .bw-btn:first-child'] = { 'margin-left': '0', 'border-top-left-radius': '6px', 'border-bottom-left-radius': '6px' };
1503
+ rules['.bw-btn-group > .bw-btn:last-child'] = { 'border-top-right-radius': '6px', 'border-bottom-right-radius': '6px' };
1504
+ rules['.bw-btn-group-vertical'] = { 'flex-direction': 'column', 'align-items': 'flex-start', 'justify-content': 'center' };
1505
+ rules['.bw-btn-group-vertical > .bw-btn'] = { 'width': '100%', 'margin-left': '0', 'margin-top': '-1px' };
1506
+ 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' };
1507
+ 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' };
1508
+
1509
+ // Accordion (structural)
1510
+ rules['.bw-accordion'] = { 'border-radius': '8px', 'overflow': 'hidden' };
1511
+ rules['.bw-accordion-item'] = { 'border': '1px solid transparent' };
1512
+ rules['.bw-accordion-item + .bw-accordion-item'] = { 'border-top': '0' };
1513
+ rules['.bw-accordion-header'] = { 'margin': '0' };
1514
+ rules['.bw-accordion-button'] = {
1515
+ 'position': 'relative', 'display': 'flex', 'align-items': 'center', 'width': '100%',
1516
+ 'padding': '1rem 1.25rem', 'font-size': '1rem', 'font-weight': '500', 'text-align': 'left',
1517
+ 'background-color': 'transparent', 'border': '0', 'overflow-anchor': 'none', 'cursor': 'pointer',
1518
+ 'font-family': 'inherit', 'transition': 'color 0.15s ease-in-out, background-color 0.15s ease-in-out'
1519
+ };
1520
+ rules['.bw-accordion-button::after'] = {
1521
+ 'flex-shrink': '0', 'width': '1.25rem', 'height': '1.25rem', 'margin-left': 'auto',
1522
+ 'content': '""', 'background-repeat': 'no-repeat', 'background-size': '1.25rem',
1523
+ 'transition': 'transform 0.2s ease-in-out'
1524
+ };
1525
+ rules['.bw-accordion-button:not(.bw-collapsed)::after'] = { 'transform': 'rotate(-180deg)' };
1526
+ rules['.bw-accordion-collapse'] = { 'max-height': '0', 'overflow': 'hidden', 'transition': 'max-height 0.3s ease' };
1527
+ rules['.bw-accordion-collapse.bw-collapse-show'] = { 'max-height': 'none' };
1528
+ rules['.bw-accordion-body'] = { 'padding': '1rem 1.25rem' };
1529
+
1530
+ // Modal (structural)
1531
+ rules['.bw-modal'] = {
1532
+ 'display': 'none', 'position': 'fixed', 'top': '0', 'left': '0', 'width': '100%', 'height': '100%',
1533
+ 'z-index': '1050', 'overflow-x': 'hidden', 'overflow-y': 'auto', 'opacity': '0', 'transition': 'opacity 0.15s linear'
1534
+ };
1535
+ rules['.bw-modal.bw-modal-show'] = { 'display': 'flex', 'align-items': 'center', 'justify-content': 'center', 'opacity': '1' };
1536
+ rules['.bw-modal-dialog'] = {
1537
+ 'position': 'relative', 'width': '100%', 'max-width': '500px', 'margin': '1.75rem auto',
1538
+ 'pointer-events': 'none', 'transform': 'translateY(-20px)', 'transition': 'transform 0.2s ease-out'
1539
+ };
1540
+ rules['.bw-modal.bw-modal-show .bw-modal-dialog'] = { 'transform': 'translateY(0)' };
1541
+ rules['.bw-modal-sm'] = { 'max-width': '300px' };
1542
+ rules['.bw-modal-lg'] = { 'max-width': '800px' };
1543
+ rules['.bw-modal-xl'] = { 'max-width': '1140px' };
1544
+ rules['.bw-modal-content'] = {
1545
+ 'position': 'relative', 'display': 'flex', 'flex-direction': 'column', 'pointer-events': 'auto',
1546
+ 'background-clip': 'padding-box', 'border': '1px solid transparent', 'border-radius': '8px', 'outline': '0'
1547
+ };
1548
+ rules['.bw-modal-header'] = { 'display': 'flex', 'align-items': 'center', 'justify-content': 'space-between', 'padding': '1rem 1.5rem' };
1549
+ rules['.bw-modal-title'] = { 'margin': '0', 'font-size': '1.25rem', 'font-weight': '600', 'line-height': '1.3' };
1550
+ rules['.bw-modal-body'] = { 'position': 'relative', 'flex': '1 1 auto', 'padding': '1.5rem' };
1551
+ rules['.bw-modal-footer'] = { 'display': 'flex', 'flex-wrap': 'wrap', 'align-items': 'center', 'justify-content': 'flex-end', 'padding': '0.75rem 1.5rem', 'gap': '0.5rem' };
1552
+
1553
+ // Toast (structural)
1554
+ rules['.bw-toast-container'] = {
1555
+ 'position': 'fixed', 'z-index': '1080', 'pointer-events': 'none',
1556
+ 'display': 'flex', 'flex-direction': 'column', 'gap': '0.5rem', 'padding': '1rem'
1557
+ };
1558
+ rules['.bw-toast'] = {
1559
+ 'pointer-events': 'auto', 'width': '350px', 'max-width': '100%', 'background-clip': 'padding-box',
1560
+ 'border-radius': '8px', 'opacity': '0', 'transform': 'translateY(-10px)',
1561
+ 'transition': 'opacity 0.3s ease, transform 0.3s ease'
1562
+ };
1563
+ rules['.bw-toast.bw-toast-show'] = { 'opacity': '1', 'transform': 'translateY(0)' };
1564
+ rules['.bw-toast.bw-toast-hiding'] = { 'opacity': '0', 'transform': 'translateY(-10px)' };
1565
+ rules['.bw-toast-header'] = { 'display': 'flex', 'align-items': 'center', 'justify-content': 'space-between', 'padding': '0.5rem 0.75rem', 'font-size': '0.875rem' };
1566
+ rules['.bw-toast-body'] = { 'padding': '0.75rem', 'font-size': '0.9375rem' };
1567
+
1568
+ // Dropdown (structural)
1569
+ rules['.bw-dropdown'] = { 'position': 'relative', 'display': 'inline-block' };
1570
+ rules['.bw-dropdown-toggle::after'] = {
1571
+ 'display': 'inline-block', 'margin-left': '0.255em', 'vertical-align': '0.255em',
1572
+ 'content': '""', 'border-top': '0.3em solid', 'border-right': '0.3em solid transparent',
1573
+ 'border-bottom': '0', 'border-left': '0.3em solid transparent'
1574
+ };
1575
+ rules['.bw-dropdown-menu'] = {
1576
+ 'position': 'absolute', 'top': '100%', 'left': '0', 'z-index': '1000', 'display': 'none',
1577
+ 'min-width': '10rem', 'padding': '0.5rem 0', 'margin': '0.125rem 0 0',
1578
+ 'background-clip': 'padding-box', 'border-radius': '6px'
1579
+ };
1580
+ rules['.bw-dropdown-menu.bw-dropdown-show'] = { 'display': 'block' };
1581
+ rules['.bw-dropdown-menu-end'] = { 'left': 'auto', 'right': '0' };
1582
+ rules['.bw-dropdown-item'] = {
1583
+ 'display': 'block', 'width': '100%', 'padding': '0.375rem 1rem', 'clear': 'both',
1584
+ 'font-weight': '400', 'text-align': 'inherit', 'text-decoration': 'none', 'white-space': 'nowrap',
1585
+ 'background-color': 'transparent', 'border': '0', 'font-size': '0.9375rem',
1586
+ 'transition': 'background-color 0.15s, color 0.15s'
1587
+ };
1588
+ rules['.bw-dropdown-divider'] = { 'height': '0', 'margin': '0.5rem 0', 'overflow': 'hidden', 'opacity': '1' };
1589
+
1590
+ // Switch (structural)
1591
+ rules['.bw-form-switch'] = { 'padding-left': '2.5em' };
1592
+ rules['.bw-form-switch .bw-switch-input'] = {
1593
+ 'width': '2em', 'height': '1.125em', 'margin-left': '-2.5em', 'border-radius': '2em',
1594
+ 'appearance': 'none', 'background-position': 'left center', 'background-repeat': 'no-repeat',
1595
+ 'background-size': 'contain', 'transition': 'background-position 0.15s ease-in-out, background-color 0.15s ease-in-out',
1596
+ 'cursor': 'pointer'
1597
+ };
1598
+ rules['.bw-form-switch .bw-switch-input:checked'] = { 'background-position': 'right center' };
1599
+ rules['.bw-form-switch .bw-switch-input:disabled'] = { 'opacity': '0.5', 'cursor': 'not-allowed' };
1600
+
1601
+ // Skeleton (structural)
1602
+ rules['.bw-skeleton'] = { 'border-radius': '4px', 'animation': 'bw-skeleton-pulse 1.5s ease-in-out infinite' };
1603
+ rules['.bw-skeleton-text'] = { 'height': '1em', 'margin-bottom': '0.5rem' };
1604
+ rules['.bw-skeleton-circle'] = { 'border-radius': '50%' };
1605
+ rules['.bw-skeleton-rect'] = { 'border-radius': '8px' };
1606
+ rules['.bw-skeleton-group'] = { 'display': 'flex', 'flex-direction': 'column' };
1607
+ rules['@keyframes bw-skeleton-pulse'] = { '0%': { 'opacity': '1' }, '50%': { 'opacity': '0.4' }, '100%': { 'opacity': '1' } };
1608
+
1609
+ // Avatar (structural)
1610
+ rules['.bw-avatar'] = {
1611
+ 'display': 'inline-flex', 'align-items': 'center', 'justify-content': 'center',
1612
+ 'border-radius': '50%', 'overflow': 'hidden', 'font-weight': '600',
1613
+ 'text-transform': 'uppercase', 'vertical-align': 'middle', 'object-fit': 'cover'
1614
+ };
1615
+ rules['.bw-avatar-sm'] = { 'width': '2rem', 'height': '2rem', 'font-size': '0.75rem' };
1616
+ rules['.bw-avatar-md'] = { 'width': '3rem', 'height': '3rem', 'font-size': '1rem' };
1617
+ rules['.bw-avatar-lg'] = { 'width': '4rem', 'height': '4rem', 'font-size': '1.25rem' };
1618
+ rules['.bw-avatar-xl'] = { 'width': '5rem', 'height': '5rem', 'font-size': '1.5rem' };
1619
+
1620
+ // Bar chart (structural)
1621
+ rules['.bw-bar-chart-container'] = {
1622
+ 'padding': '1rem', 'border': '1px solid transparent', 'border-radius': '8px'
1623
+ };
1624
+ rules['.bw-bar-chart'] = {
1625
+ 'display': 'flex', 'align-items': 'flex-end', 'gap': '6px', 'padding': '0 0.5rem'
1626
+ };
1627
+ rules['.bw-bar-group'] = {
1628
+ 'flex': '1', 'display': 'flex', 'flex-direction': 'column',
1629
+ 'align-items': 'center', 'height': '100%', 'justify-content': 'flex-end'
1630
+ };
1631
+ rules['.bw-bar'] = {
1632
+ 'width': '100%', 'border-radius': '3px 3px 0 0',
1633
+ 'transition': 'height 0.5s ease', 'min-height': '4px'
1634
+ };
1635
+ rules['.bw-bar:hover'] = { 'opacity': '0.85' };
1636
+ rules['.bw-bar-value'] = {
1637
+ 'font-size': '0.65rem', 'font-weight': '600', 'margin-bottom': '2px', 'text-align': 'center'
1638
+ };
1639
+ rules['.bw-bar-label'] = {
1640
+ 'font-size': '0.7rem', 'margin-top': '4px', 'text-align': 'center'
1641
+ };
1642
+ rules['.bw-bar-chart-title'] = {
1643
+ 'font-size': '1.1rem', 'font-weight': '600', 'margin': '0 0 0.75rem 0'
1644
+ };
1370
1645
 
1371
1646
  // Spacing utilities (structural)
1372
1647
  var spacingValues = { '0': '0', '1': '.25rem', '2': '.5rem', '3': '1rem', '4': '1.5rem', '5': '3rem' };
@@ -1684,6 +1959,56 @@ function generateDarkModeCSS(palette) {
1684
1959
  '.bw-dark .bw-close': {
1685
1960
  'color': textColor
1686
1961
  },
1962
+ '.bw-dark .bw-accordion-item': {
1963
+ 'background-color': surfaceBg,
1964
+ 'border-color': borderColor
1965
+ },
1966
+ '.bw-dark .bw-accordion-button': {
1967
+ 'color': textColor
1968
+ },
1969
+ '.bw-dark .bw-accordion-button:hover': {
1970
+ 'background-color': bodyBg
1971
+ },
1972
+ '.bw-dark .bw-accordion-body': {
1973
+ 'border-top-color': borderColor
1974
+ },
1975
+ '.bw-dark .bw-modal-content': {
1976
+ 'background-color': surfaceBg,
1977
+ 'border-color': borderColor
1978
+ },
1979
+ '.bw-dark .bw-modal-header': {
1980
+ 'border-bottom-color': borderColor
1981
+ },
1982
+ '.bw-dark .bw-modal-footer': {
1983
+ 'border-top-color': borderColor
1984
+ },
1985
+ '.bw-dark .bw-modal-title': {
1986
+ 'color': textColor
1987
+ },
1988
+ '.bw-dark .bw-toast': {
1989
+ 'background-color': surfaceBg,
1990
+ 'border-color': borderColor
1991
+ },
1992
+ '.bw-dark .bw-toast-header': {
1993
+ 'border-bottom-color': borderColor,
1994
+ 'color': textColor
1995
+ },
1996
+ '.bw-dark .bw-dropdown-menu': {
1997
+ 'background-color': surfaceBg,
1998
+ 'border-color': borderColor
1999
+ },
2000
+ '.bw-dark .bw-dropdown-item': {
2001
+ 'color': textColor
2002
+ },
2003
+ '.bw-dark .bw-dropdown-item:hover': {
2004
+ 'background-color': bodyBg
2005
+ },
2006
+ '.bw-dark .bw-dropdown-divider': {
2007
+ 'border-top-color': borderColor
2008
+ },
2009
+ '.bw-dark .bw-skeleton': {
2010
+ 'background-color': borderColor
2011
+ },
1687
2012
  '.bw-dark h1, .bw-dark h2, .bw-dark h3, .bw-dark h4, .bw-dark h5, .bw-dark h6': {
1688
2013
  'color': textColor
1689
2014
  },
@@ -2277,7 +2602,11 @@ function makeAlert(props = {}) {
2277
2602
  a: {
2278
2603
  type: 'button',
2279
2604
  class: 'bw-close',
2280
- 'aria-label': 'Close'
2605
+ 'aria-label': 'Close',
2606
+ onclick: function(e) {
2607
+ var alert = e.target.closest('.bw-alert');
2608
+ if (alert) { alert.remove(); }
2609
+ }
2281
2610
  },
2282
2611
  c: '×'
2283
2612
  }
@@ -2291,25 +2620,30 @@ function makeAlert(props = {}) {
2291
2620
  * @param {Object} [props] - Badge configuration
2292
2621
  * @param {string} [props.text] - Badge display text
2293
2622
  * @param {string} [props.variant="primary"] - Color variant
2623
+ * @param {string} [props.size] - Size variant: 'sm' or 'lg' (default is medium)
2294
2624
  * @param {boolean} [props.pill=false] - Use pill (rounded) shape
2295
2625
  * @param {string} [props.className] - Additional CSS classes
2296
2626
  * @returns {Object} TACO object representing a badge span
2297
2627
  * @category Component Builders
2298
2628
  * @example
2299
2629
  * const badge = makeBadge({ text: "New", variant: "danger", pill: true });
2630
+ * const small = makeBadge({ text: "3", variant: "info", size: "sm" });
2300
2631
  */
2301
2632
  function makeBadge(props = {}) {
2302
2633
  const {
2303
2634
  text,
2304
2635
  variant = 'primary',
2636
+ size,
2305
2637
  pill = false,
2306
2638
  className = ''
2307
2639
  } = props;
2308
2640
 
2641
+ const sizeClass = size === 'sm' ? ' bw-badge-sm' : size === 'lg' ? ' bw-badge-lg' : '';
2642
+
2309
2643
  return {
2310
2644
  t: 'span',
2311
2645
  a: {
2312
- class: `bw-badge bw-badge-${variant} ${pill ? 'bw-badge-pill' : ''} ${className}`.trim()
2646
+ class: `bw-badge bw-badge-${variant}${sizeClass} ${pill ? 'bw-badge-pill' : ''} ${className}`.trim()
2313
2647
  },
2314
2648
  c: text
2315
2649
  };
@@ -2768,12 +3102,14 @@ function makeCheckbox(props = {}) {
2768
3102
  id,
2769
3103
  name,
2770
3104
  disabled = false,
2771
- value
3105
+ value,
3106
+ className = '',
3107
+ ...eventHandlers
2772
3108
  } = props;
2773
3109
 
2774
3110
  return {
2775
3111
  t: 'div',
2776
- a: { class: 'bw-form-check' },
3112
+ a: { class: `bw-form-check ${className}`.trim() },
2777
3113
  c: [
2778
3114
  {
2779
3115
  t: 'input',
@@ -2784,7 +3120,8 @@ function makeCheckbox(props = {}) {
2784
3120
  id,
2785
3121
  name,
2786
3122
  disabled,
2787
- value
3123
+ value,
3124
+ ...eventHandlers
2788
3125
  }
2789
3126
  },
2790
3127
  label && {
@@ -3012,8 +3349,8 @@ function makeFeatureGrid(props = {}) {
3012
3349
  feature.icon && {
3013
3350
  t: 'div',
3014
3351
  a: {
3015
- class: 'bw-feature-icon bw-mb-3',
3016
- style: `font-size: ${iconSize}; color: var(--bw-primary);`
3352
+ class: 'bw-feature-icon bw-mb-3 bw-text-primary',
3353
+ style: `font-size: ${iconSize};`
3017
3354
  },
3018
3355
  c: feature.icon
3019
3356
  },
@@ -3402,229 +3739,1076 @@ class NavbarHandle {
3402
3739
  }
3403
3740
 
3404
3741
  /**
3405
- * Imperative handle for a rendered tabs component
3406
- *
3407
- * Provides programmatic tab switching. Sets up click handlers
3408
- * on tab buttons and manages active states on both buttons and panes.
3409
- * Created automatically when using bw.createTabs().
3742
+ * Imperative handle for a rendered tabs component
3743
+ *
3744
+ * Provides programmatic tab switching. Sets up click handlers
3745
+ * on tab buttons and manages active states on both buttons and panes.
3746
+ * Created automatically when using bw.createTabs().
3747
+ *
3748
+ * @category Component Handles
3749
+ */
3750
+ class TabsHandle {
3751
+ /**
3752
+ * @param {Element} element - The tabs container DOM element
3753
+ * @param {Object} taco - The original TACO object used to create the tabs
3754
+ */
3755
+ constructor(element, taco) {
3756
+ this.element = element;
3757
+ this._taco = taco;
3758
+ this.state = taco.o?.state || {};
3759
+
3760
+ this.children = {
3761
+ navItems: element.querySelectorAll('.bw-nav-link'),
3762
+ tabPanes: element.querySelectorAll('.bw-tab-pane')
3763
+ };
3764
+
3765
+ this._setupTabs();
3766
+ }
3767
+
3768
+ /**
3769
+ * Attach click handlers to tab navigation buttons
3770
+ * @private
3771
+ */
3772
+ _setupTabs() {
3773
+ this.children.navItems.forEach((navItem, index) => {
3774
+ navItem.onclick = (e) => {
3775
+ e.preventDefault();
3776
+ this.switchTo(index);
3777
+ };
3778
+ });
3779
+ }
3780
+
3781
+ /**
3782
+ * Programmatically switch to a tab by index
3783
+ *
3784
+ * @param {number} index - Zero-based tab index to activate
3785
+ * @returns {TabsHandle} this (for chaining)
3786
+ */
3787
+ switchTo(index) {
3788
+ this.children.navItems.forEach((item, i) => {
3789
+ if (i === index) {
3790
+ item.classList.add('active');
3791
+ } else {
3792
+ item.classList.remove('active');
3793
+ }
3794
+ });
3795
+
3796
+ this.children.tabPanes.forEach((pane, i) => {
3797
+ if (i === index) {
3798
+ pane.classList.add('active');
3799
+ } else {
3800
+ pane.classList.remove('active');
3801
+ }
3802
+ });
3803
+
3804
+ this.state.activeIndex = index;
3805
+ return this;
3806
+ }
3807
+ }
3808
+
3809
+ /**
3810
+ * Create a code demo component for documentation pages
3811
+ *
3812
+ * Displays a live result alongside source code in a tabbed interface.
3813
+ * Includes a copy-to-clipboard button on the code tab.
3814
+ *
3815
+ * @param {Object} [props] - Code demo configuration
3816
+ * @param {string} [props.title] - Demo title heading
3817
+ * @param {string} [props.description] - Demo description text
3818
+ * @param {string} [props.code] - Source code to display (adds a "Code" tab when present)
3819
+ * @param {string|Object|Array} [props.result] - Live result content for the "Result" tab
3820
+ * @param {string} [props.language="javascript"] - Code language for syntax class
3821
+ * @returns {Object} TACO object representing a code demo with tabbed Result/Code views
3822
+ * @category Component Builders
3823
+ * @example
3824
+ * const demo = makeCodeDemo({
3825
+ * title: "Button Example",
3826
+ * description: "A simple primary button",
3827
+ * code: 'makeButton({ text: "Click me" })',
3828
+ * result: makeButton({ text: "Click me" })
3829
+ * });
3830
+ */
3831
+ function makeCodeDemo(props = {}) {
3832
+ const {
3833
+ title,
3834
+ description,
3835
+ code,
3836
+ result,
3837
+ language = 'javascript'
3838
+ } = props;
3839
+
3840
+ // Generate unique ID for this demo
3841
+ `demo-${Math.random().toString(36).substr(2, 9)}`;
3842
+
3843
+ const tabs = [
3844
+ {
3845
+ label: 'Result',
3846
+ active: true,
3847
+ content: result
3848
+ }
3849
+ ];
3850
+
3851
+ // Only add Code tab if code is provided
3852
+ if (code) {
3853
+ tabs.push({
3854
+ label: 'Code',
3855
+ content: {
3856
+ t: 'div',
3857
+ a: { style: 'position: relative;' },
3858
+ c: [
3859
+ {
3860
+ t: 'button',
3861
+ a: {
3862
+ class: 'bw-copy-btn bw-code-copy-btn',
3863
+ onclick: (e) => {
3864
+ navigator.clipboard.writeText(code).then(() => {
3865
+ const btn = e.target;
3866
+ const originalText = btn.textContent;
3867
+ btn.textContent = 'Copied!';
3868
+ btn.style.background = '#006666';
3869
+ btn.style.color = '#fff';
3870
+ setTimeout(() => {
3871
+ btn.textContent = originalText;
3872
+ btn.style.background = 'rgba(255,255,255,0.12)';
3873
+ btn.style.color = '#aaa';
3874
+ }, 2000);
3875
+ });
3876
+ }
3877
+ },
3878
+ c: 'Copy'
3879
+ },
3880
+ (typeof globalThis !== 'undefined' && typeof globalThis.bw !== 'undefined' && typeof globalThis.bw.codeEditor === 'function')
3881
+ ? globalThis.bw.codeEditor({ code: code, lang: language === 'javascript' ? 'js' : language, readOnly: true, height: 'auto' })
3882
+ : {
3883
+ t: 'pre',
3884
+ a: { class: 'bw-code-pre' },
3885
+ c: {
3886
+ t: 'code',
3887
+ a: { class: `bw-code-block language-${language}` },
3888
+ c: code
3889
+ }
3890
+ }
3891
+ ]
3892
+ }
3893
+ });
3894
+ }
3895
+
3896
+ const content = [
3897
+ title && { t: 'h3', c: title },
3898
+ description && {
3899
+ t: 'p',
3900
+ a: { class: 'bw-text-muted', style: 'margin-bottom: 1rem;' },
3901
+ c: description
3902
+ },
3903
+ makeTabs({ tabs})
3904
+ ].filter(Boolean);
3905
+
3906
+ return {
3907
+ t: 'div',
3908
+ a: { class: 'bw-code-demo' },
3909
+ c: content
3910
+ };
3911
+ }
3912
+
3913
+ /**
3914
+ * Registry mapping component type names to their handle classes
3915
+ *
3916
+ * Used by bw.createCard(), bw.createTable(), etc. to wrap rendered
3917
+ * DOM elements in the appropriate imperative handle.
3918
+ *
3919
+ * @type {Object.<string, Function>}
3920
+ */
3921
+ // =========================================================================
3922
+ // Phase 1: Quick Wins
3923
+ // =========================================================================
3924
+
3925
+ /**
3926
+ * Create a pagination navigation component
3927
+ *
3928
+ * @param {Object} [props] - Pagination configuration
3929
+ * @param {number} [props.pages=1] - Total number of pages
3930
+ * @param {number} [props.currentPage=1] - Currently active page (1-based)
3931
+ * @param {Function} [props.onPageChange] - Callback when page changes, receives page number
3932
+ * @param {string} [props.size] - Size variant ("sm" or "lg")
3933
+ * @param {string} [props.className] - Additional CSS classes
3934
+ * @returns {Object} TACO object representing a pagination nav
3935
+ * @category Component Builders
3936
+ * @example
3937
+ * const pager = makePagination({
3938
+ * pages: 10,
3939
+ * currentPage: 3,
3940
+ * onPageChange: (page) => loadPage(page)
3941
+ * });
3942
+ */
3943
+ function makePagination(props = {}) {
3944
+ const {
3945
+ pages = 1,
3946
+ currentPage = 1,
3947
+ onPageChange,
3948
+ size,
3949
+ className = ''
3950
+ } = props;
3951
+
3952
+ function handleClick(page) {
3953
+ return function(e) {
3954
+ e.preventDefault();
3955
+ if (page < 1 || page > pages || page === currentPage) return;
3956
+ if (onPageChange) onPageChange(page);
3957
+ };
3958
+ }
3959
+
3960
+ const items = [];
3961
+
3962
+ // Previous arrow
3963
+ items.push({
3964
+ t: 'li',
3965
+ a: { class: `bw-page-item ${currentPage <= 1 ? 'bw-disabled' : ''}`.trim() },
3966
+ c: {
3967
+ t: 'a',
3968
+ a: { class: 'bw-page-link', href: '#', onclick: handleClick(currentPage - 1), 'aria-label': 'Previous' },
3969
+ c: '\u2039'
3970
+ }
3971
+ });
3972
+
3973
+ // Page numbers
3974
+ for (var i = 1; i <= pages; i++) {
3975
+ (function(pageNum) {
3976
+ items.push({
3977
+ t: 'li',
3978
+ a: { class: `bw-page-item ${pageNum === currentPage ? 'bw-active' : ''}`.trim() },
3979
+ c: {
3980
+ t: 'a',
3981
+ a: { class: 'bw-page-link', href: '#', onclick: handleClick(pageNum) },
3982
+ c: '' + pageNum
3983
+ }
3984
+ });
3985
+ })(i);
3986
+ }
3987
+
3988
+ // Next arrow
3989
+ items.push({
3990
+ t: 'li',
3991
+ a: { class: `bw-page-item ${currentPage >= pages ? 'bw-disabled' : ''}`.trim() },
3992
+ c: {
3993
+ t: 'a',
3994
+ a: { class: 'bw-page-link', href: '#', onclick: handleClick(currentPage + 1), 'aria-label': 'Next' },
3995
+ c: '\u203A'
3996
+ }
3997
+ });
3998
+
3999
+ return {
4000
+ t: 'nav',
4001
+ a: { 'aria-label': 'Pagination' },
4002
+ c: {
4003
+ t: 'ul',
4004
+ a: {
4005
+ class: `bw-pagination ${size ? 'bw-pagination-' + size : ''} ${className}`.trim()
4006
+ },
4007
+ c: items
4008
+ }
4009
+ };
4010
+ }
4011
+
4012
+ /**
4013
+ * Create a radio button input with label
4014
+ *
4015
+ * @param {Object} [props] - Radio configuration
4016
+ * @param {string} [props.label] - Radio label text
4017
+ * @param {string} [props.name] - Radio group name
4018
+ * @param {string} [props.value] - Radio value attribute
4019
+ * @param {boolean} [props.checked=false] - Whether the radio is selected
4020
+ * @param {string} [props.id] - Element ID (links label to radio)
4021
+ * @param {boolean} [props.disabled=false] - Whether the radio is disabled
4022
+ * @param {string} [props.className] - Additional CSS classes
4023
+ * @returns {Object} TACO object representing a radio form group
4024
+ * @category Component Builders
4025
+ * @example
4026
+ * const radio = makeRadio({
4027
+ * label: "Option A",
4028
+ * name: "choice",
4029
+ * value: "a",
4030
+ * checked: true
4031
+ * });
4032
+ */
4033
+ function makeRadio(props = {}) {
4034
+ const {
4035
+ label,
4036
+ name,
4037
+ value,
4038
+ checked = false,
4039
+ id,
4040
+ disabled = false,
4041
+ className = '',
4042
+ ...eventHandlers
4043
+ } = props;
4044
+
4045
+ return {
4046
+ t: 'div',
4047
+ a: { class: `bw-form-check ${className}`.trim() },
4048
+ c: [
4049
+ {
4050
+ t: 'input',
4051
+ a: {
4052
+ type: 'radio',
4053
+ class: 'bw-form-check-input',
4054
+ name,
4055
+ value,
4056
+ checked,
4057
+ id,
4058
+ disabled,
4059
+ ...eventHandlers
4060
+ }
4061
+ },
4062
+ label && {
4063
+ t: 'label',
4064
+ a: { class: 'bw-form-check-label', for: id },
4065
+ c: label
4066
+ }
4067
+ ].filter(Boolean)
4068
+ };
4069
+ }
4070
+
4071
+ /**
4072
+ * Create a button group wrapper
4073
+ *
4074
+ * @param {Object} [props] - Button group configuration
4075
+ * @param {Array} [props.children] - Button TACO objects to group
4076
+ * @param {string} [props.size] - Size variant ("sm" or "lg")
4077
+ * @param {boolean} [props.vertical=false] - Stack buttons vertically
4078
+ * @param {string} [props.className] - Additional CSS classes
4079
+ * @returns {Object} TACO object representing a button group
4080
+ * @category Component Builders
4081
+ * @example
4082
+ * const group = makeButtonGroup({
4083
+ * children: [
4084
+ * makeButton({ text: "Left", variant: "primary" }),
4085
+ * makeButton({ text: "Middle", variant: "primary" }),
4086
+ * makeButton({ text: "Right", variant: "primary" })
4087
+ * ]
4088
+ * });
4089
+ */
4090
+ function makeButtonGroup(props = {}) {
4091
+ const {
4092
+ children,
4093
+ size,
4094
+ vertical = false,
4095
+ className = ''
4096
+ } = props;
4097
+
4098
+ return {
4099
+ t: 'div',
4100
+ a: {
4101
+ class: `${vertical ? 'bw-btn-group-vertical' : 'bw-btn-group'} ${size ? 'bw-btn-group-' + size : ''} ${className}`.trim(),
4102
+ role: 'group'
4103
+ },
4104
+ c: children
4105
+ };
4106
+ }
4107
+
4108
+ // =========================================================================
4109
+ // Phase 2: Core Interactive
4110
+ // =========================================================================
4111
+
4112
+ /**
4113
+ * Create an accordion component with collapsible items
4114
+ *
4115
+ * @param {Object} [props] - Accordion configuration
4116
+ * @param {Array<Object>} [props.items=[]] - Accordion items
4117
+ * @param {string} props.items[].title - Header text for the accordion item
4118
+ * @param {string|Object|Array} props.items[].content - Collapsible content
4119
+ * @param {boolean} [props.items[].open=false] - Whether the item is initially open
4120
+ * @param {boolean} [props.multiOpen=false] - Allow multiple items open simultaneously
4121
+ * @param {string} [props.className] - Additional CSS classes
4122
+ * @returns {Object} TACO object representing an accordion
4123
+ * @category Component Builders
4124
+ * @example
4125
+ * const accordion = makeAccordion({
4126
+ * items: [
4127
+ * { title: "Section 1", content: "Content 1", open: true },
4128
+ * { title: "Section 2", content: "Content 2" }
4129
+ * ]
4130
+ * });
4131
+ */
4132
+ function makeAccordion(props = {}) {
4133
+ const {
4134
+ items = [],
4135
+ multiOpen = false,
4136
+ className = ''
4137
+ } = props;
4138
+
4139
+ return {
4140
+ t: 'div',
4141
+ a: { class: `bw-accordion ${className}`.trim() },
4142
+ c: items.map(function(item, index) {
4143
+ return {
4144
+ t: 'div',
4145
+ a: { class: 'bw-accordion-item' },
4146
+ c: [
4147
+ {
4148
+ t: 'h2',
4149
+ a: { class: 'bw-accordion-header' },
4150
+ c: {
4151
+ t: 'button',
4152
+ a: {
4153
+ class: `bw-accordion-button ${item.open ? '' : 'bw-collapsed'}`.trim(),
4154
+ type: 'button',
4155
+ 'aria-expanded': item.open ? 'true' : 'false',
4156
+ 'data-accordion-index': index,
4157
+ onclick: function(e) {
4158
+ var btn = e.target.closest('.bw-accordion-button');
4159
+ var accordionEl = btn.closest('.bw-accordion');
4160
+ var accordionItem = btn.closest('.bw-accordion-item');
4161
+ var collapse = accordionItem.querySelector('.bw-accordion-collapse');
4162
+ var isOpen = collapse.classList.contains('bw-collapse-show');
4163
+
4164
+ if (!multiOpen) {
4165
+ // Close all siblings
4166
+ var allCollapses = accordionEl.querySelectorAll('.bw-accordion-collapse');
4167
+ var allButtons = accordionEl.querySelectorAll('.bw-accordion-button');
4168
+ for (var j = 0; j < allCollapses.length; j++) {
4169
+ allCollapses[j].classList.remove('bw-collapse-show');
4170
+ allCollapses[j].style.maxHeight = null;
4171
+ }
4172
+ for (var k = 0; k < allButtons.length; k++) {
4173
+ allButtons[k].classList.add('bw-collapsed');
4174
+ allButtons[k].setAttribute('aria-expanded', 'false');
4175
+ }
4176
+ }
4177
+
4178
+ if (isOpen) {
4179
+ collapse.classList.remove('bw-collapse-show');
4180
+ collapse.style.maxHeight = null;
4181
+ btn.classList.add('bw-collapsed');
4182
+ btn.setAttribute('aria-expanded', 'false');
4183
+ } else {
4184
+ collapse.classList.add('bw-collapse-show');
4185
+ collapse.style.maxHeight = collapse.scrollHeight + 'px';
4186
+ btn.classList.remove('bw-collapsed');
4187
+ btn.setAttribute('aria-expanded', 'true');
4188
+ }
4189
+ }
4190
+ },
4191
+ c: item.title
4192
+ }
4193
+ },
4194
+ {
4195
+ t: 'div',
4196
+ a: { class: `bw-accordion-collapse ${item.open ? 'bw-collapse-show' : ''}`.trim() },
4197
+ c: {
4198
+ t: 'div',
4199
+ a: { class: 'bw-accordion-body' },
4200
+ c: item.content
4201
+ },
4202
+ o: item.open ? {
4203
+ mounted: function(el) {
4204
+ el.style.maxHeight = el.scrollHeight + 'px';
4205
+ }
4206
+ } : undefined
4207
+ }
4208
+ ]
4209
+ };
4210
+ }),
4211
+ o: {
4212
+ type: 'accordion',
4213
+ state: { multiOpen: multiOpen }
4214
+ }
4215
+ };
4216
+ }
4217
+
4218
+ /**
4219
+ * Imperative handle for a rendered modal component
4220
+ *
4221
+ * Provides `.show()`, `.hide()`, `.toggle()`, and `.destroy()` methods
4222
+ * for controlling the modal programmatically.
4223
+ *
4224
+ * @category Component Handles
4225
+ */
4226
+ class ModalHandle {
4227
+ /**
4228
+ * @param {Element} element - The modal backdrop DOM element
4229
+ * @param {Object} taco - The original TACO object
4230
+ */
4231
+ constructor(element, taco) {
4232
+ this.element = element;
4233
+ this._taco = taco;
4234
+ this._escHandler = null;
4235
+ }
4236
+
4237
+ /** Show the modal */
4238
+ show() {
4239
+ this.element.classList.add('bw-modal-show');
4240
+ document.body.style.overflow = 'hidden';
4241
+ return this;
4242
+ }
4243
+
4244
+ /** Hide the modal */
4245
+ hide() {
4246
+ this.element.classList.remove('bw-modal-show');
4247
+ document.body.style.overflow = '';
4248
+ return this;
4249
+ }
4250
+
4251
+ /** Toggle modal visibility */
4252
+ toggle() {
4253
+ if (this.element.classList.contains('bw-modal-show')) {
4254
+ this.hide();
4255
+ } else {
4256
+ this.show();
4257
+ }
4258
+ return this;
4259
+ }
4260
+
4261
+ /** Remove the modal from DOM and clean up */
4262
+ destroy() {
4263
+ this.hide();
4264
+ if (this._escHandler) {
4265
+ document.removeEventListener('keydown', this._escHandler);
4266
+ }
4267
+ if (this.element.parentNode) {
4268
+ this.element.parentNode.removeChild(this.element);
4269
+ }
4270
+ }
4271
+ }
4272
+
4273
+ /**
4274
+ * Create a modal dialog overlay
4275
+ *
4276
+ * @param {Object} [props] - Modal configuration
4277
+ * @param {string} [props.title] - Modal title in header
4278
+ * @param {string|Object|Array} [props.content] - Modal body content
4279
+ * @param {string|Object|Array} [props.footer] - Modal footer content
4280
+ * @param {string} [props.size] - Modal size ("sm", "lg", "xl")
4281
+ * @param {boolean} [props.closeButton=true] - Show X close button in header
4282
+ * @param {Function} [props.onClose] - Callback when modal is closed
4283
+ * @param {string} [props.className] - Additional CSS classes
4284
+ * @returns {Object} TACO object representing a modal
4285
+ * @category Component Builders
4286
+ * @example
4287
+ * const modal = makeModal({
4288
+ * title: "Confirm",
4289
+ * content: "Are you sure?",
4290
+ * footer: makeButton({ text: "OK", variant: "primary" })
4291
+ * });
4292
+ */
4293
+ function makeModal(props = {}) {
4294
+ const {
4295
+ title,
4296
+ content,
4297
+ footer,
4298
+ size,
4299
+ closeButton = true,
4300
+ onClose,
4301
+ className = ''
4302
+ } = props;
4303
+
4304
+ function closeModal(el) {
4305
+ var backdrop = el.closest('.bw-modal');
4306
+ if (backdrop) {
4307
+ backdrop.classList.remove('bw-modal-show');
4308
+ document.body.style.overflow = '';
4309
+ }
4310
+ if (onClose) onClose();
4311
+ }
4312
+
4313
+ return {
4314
+ t: 'div',
4315
+ a: { class: `bw-modal ${className}`.trim() },
4316
+ c: {
4317
+ t: 'div',
4318
+ a: { class: `bw-modal-dialog ${size ? 'bw-modal-' + size : ''}`.trim() },
4319
+ c: {
4320
+ t: 'div',
4321
+ a: { class: 'bw-modal-content' },
4322
+ c: [
4323
+ (title || closeButton) && {
4324
+ t: 'div',
4325
+ a: { class: 'bw-modal-header' },
4326
+ c: [
4327
+ title && { t: 'h5', a: { class: 'bw-modal-title' }, c: title },
4328
+ closeButton && {
4329
+ t: 'button',
4330
+ a: {
4331
+ type: 'button',
4332
+ class: 'bw-close',
4333
+ 'aria-label': 'Close',
4334
+ onclick: function(e) { closeModal(e.target); }
4335
+ },
4336
+ c: '\u00D7'
4337
+ }
4338
+ ].filter(Boolean)
4339
+ },
4340
+ content && {
4341
+ t: 'div',
4342
+ a: { class: 'bw-modal-body' },
4343
+ c: content
4344
+ },
4345
+ footer && {
4346
+ t: 'div',
4347
+ a: { class: 'bw-modal-footer' },
4348
+ c: footer
4349
+ }
4350
+ ].filter(Boolean)
4351
+ }
4352
+ },
4353
+ o: {
4354
+ type: 'modal',
4355
+ mounted: function(el) {
4356
+ // Click backdrop to close
4357
+ el.addEventListener('click', function(e) {
4358
+ if (e.target === el) closeModal(el);
4359
+ });
4360
+ // Escape key to close
4361
+ var escHandler = function(e) {
4362
+ if (e.key === 'Escape' && el.classList.contains('bw-modal-show')) {
4363
+ closeModal(el);
4364
+ }
4365
+ };
4366
+ document.addEventListener('keydown', escHandler);
4367
+ el._bw_escHandler = escHandler;
4368
+ },
4369
+ unmount: function(el) {
4370
+ if (el._bw_escHandler) {
4371
+ document.removeEventListener('keydown', el._bw_escHandler);
4372
+ }
4373
+ document.body.style.overflow = '';
4374
+ }
4375
+ }
4376
+ };
4377
+ }
4378
+
4379
+ /**
4380
+ * Create a toast notification popup
4381
+ *
4382
+ * @param {Object} [props] - Toast configuration
4383
+ * @param {string} [props.title] - Toast title
4384
+ * @param {string|Object|Array} [props.content] - Toast body content
4385
+ * @param {string} [props.variant="info"] - Color variant ("primary", "success", "danger", "warning", "info")
4386
+ * @param {boolean} [props.autoDismiss=true] - Auto-dismiss after delay
4387
+ * @param {number} [props.delay=5000] - Auto-dismiss delay in ms
4388
+ * @param {string} [props.position="top-right"] - Container position
4389
+ * @param {string} [props.className] - Additional CSS classes
4390
+ * @returns {Object} TACO object representing a toast
4391
+ * @category Component Builders
4392
+ * @example
4393
+ * const toast = makeToast({
4394
+ * title: "Success",
4395
+ * content: "File saved!",
4396
+ * variant: "success"
4397
+ * });
4398
+ */
4399
+ function makeToast(props = {}) {
4400
+ const {
4401
+ title,
4402
+ content,
4403
+ variant = 'info',
4404
+ autoDismiss = true,
4405
+ delay = 5000,
4406
+ position = 'top-right',
4407
+ className = ''
4408
+ } = props;
4409
+
4410
+ return {
4411
+ t: 'div',
4412
+ a: {
4413
+ class: `bw-toast bw-toast-${variant} ${className}`.trim(),
4414
+ role: 'alert',
4415
+ 'data-position': position
4416
+ },
4417
+ c: [
4418
+ (title) && {
4419
+ t: 'div',
4420
+ a: { class: 'bw-toast-header' },
4421
+ c: [
4422
+ { t: 'strong', c: title },
4423
+ {
4424
+ t: 'button',
4425
+ a: {
4426
+ type: 'button',
4427
+ class: 'bw-close',
4428
+ 'aria-label': 'Close',
4429
+ onclick: function(e) {
4430
+ var toast = e.target.closest('.bw-toast');
4431
+ if (toast) {
4432
+ toast.classList.add('bw-toast-hiding');
4433
+ setTimeout(function() { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 300);
4434
+ }
4435
+ }
4436
+ },
4437
+ c: '\u00D7'
4438
+ }
4439
+ ]
4440
+ },
4441
+ content && {
4442
+ t: 'div',
4443
+ a: { class: 'bw-toast-body' },
4444
+ c: content
4445
+ }
4446
+ ].filter(Boolean),
4447
+ o: {
4448
+ type: 'toast',
4449
+ mounted: function(el) {
4450
+ // Trigger show animation
4451
+ requestAnimationFrame(function() {
4452
+ el.classList.add('bw-toast-show');
4453
+ });
4454
+ // Auto-dismiss
4455
+ if (autoDismiss) {
4456
+ setTimeout(function() {
4457
+ el.classList.add('bw-toast-hiding');
4458
+ setTimeout(function() { if (el.parentNode) el.parentNode.removeChild(el); }, 300);
4459
+ }, delay);
4460
+ }
4461
+ }
4462
+ }
4463
+ };
4464
+ }
4465
+
4466
+ // =========================================================================
4467
+ // Phase 3: Essential Modern
4468
+ // =========================================================================
4469
+
4470
+ /**
4471
+ * Create a dropdown menu triggered by a button
4472
+ *
4473
+ * @param {Object} [props] - Dropdown configuration
4474
+ * @param {string|Object} [props.trigger] - Button text or TACO for the trigger
4475
+ * @param {Array<Object>} [props.items=[]] - Menu items
4476
+ * @param {string} [props.items[].text] - Item display text
4477
+ * @param {string} [props.items[].href] - Item link URL
4478
+ * @param {Function} [props.items[].onclick] - Item click handler
4479
+ * @param {boolean} [props.items[].divider] - Render as a divider line
4480
+ * @param {boolean} [props.items[].disabled] - Whether the item is disabled
4481
+ * @param {string} [props.align="start"] - Menu alignment ("start" or "end")
4482
+ * @param {string} [props.variant="primary"] - Trigger button variant
4483
+ * @param {string} [props.className] - Additional CSS classes
4484
+ * @returns {Object} TACO object representing a dropdown
4485
+ * @category Component Builders
4486
+ * @example
4487
+ * const dropdown = makeDropdown({
4488
+ * trigger: "Actions",
4489
+ * items: [
4490
+ * { text: "Edit", onclick: () => edit() },
4491
+ * { divider: true },
4492
+ * { text: "Delete", onclick: () => del() }
4493
+ * ]
4494
+ * });
4495
+ */
4496
+ function makeDropdown(props = {}) {
4497
+ const {
4498
+ trigger,
4499
+ items = [],
4500
+ align = 'start',
4501
+ variant = 'primary',
4502
+ className = ''
4503
+ } = props;
4504
+
4505
+ var triggerTaco;
4506
+ if (typeof trigger === 'string' || trigger === undefined) {
4507
+ triggerTaco = {
4508
+ t: 'button',
4509
+ a: {
4510
+ class: `bw-btn bw-btn-${variant} bw-dropdown-toggle`,
4511
+ type: 'button',
4512
+ onclick: function(e) {
4513
+ var dropdown = e.target.closest('.bw-dropdown');
4514
+ var menu = dropdown.querySelector('.bw-dropdown-menu');
4515
+ menu.classList.toggle('bw-dropdown-show');
4516
+ }
4517
+ },
4518
+ c: trigger || 'Dropdown'
4519
+ };
4520
+ } else {
4521
+ triggerTaco = trigger;
4522
+ }
4523
+
4524
+ return {
4525
+ t: 'div',
4526
+ a: { class: `bw-dropdown ${className}`.trim() },
4527
+ c: [
4528
+ triggerTaco,
4529
+ {
4530
+ t: 'div',
4531
+ a: { class: `bw-dropdown-menu ${align === 'end' ? 'bw-dropdown-menu-end' : ''}`.trim() },
4532
+ c: items.map(function(item) {
4533
+ if (item.divider) {
4534
+ return { t: 'hr', a: { class: 'bw-dropdown-divider' } };
4535
+ }
4536
+ return {
4537
+ t: 'a',
4538
+ a: {
4539
+ class: `bw-dropdown-item ${item.disabled ? 'disabled' : ''}`.trim(),
4540
+ href: item.href || '#',
4541
+ onclick: item.disabled ? undefined : function(e) {
4542
+ if (!item.href) e.preventDefault();
4543
+ var dropdown = e.target.closest('.bw-dropdown');
4544
+ var menu = dropdown.querySelector('.bw-dropdown-menu');
4545
+ menu.classList.remove('bw-dropdown-show');
4546
+ if (item.onclick) item.onclick(e);
4547
+ }
4548
+ },
4549
+ c: item.text
4550
+ };
4551
+ })
4552
+ }
4553
+ ],
4554
+ o: {
4555
+ type: 'dropdown',
4556
+ mounted: function(el) {
4557
+ // Click outside to close
4558
+ var outsideHandler = function(e) {
4559
+ if (!el.contains(e.target)) {
4560
+ var menu = el.querySelector('.bw-dropdown-menu');
4561
+ if (menu) menu.classList.remove('bw-dropdown-show');
4562
+ }
4563
+ };
4564
+ document.addEventListener('click', outsideHandler);
4565
+ el._bw_outsideHandler = outsideHandler;
4566
+ },
4567
+ unmount: function(el) {
4568
+ if (el._bw_outsideHandler) {
4569
+ document.removeEventListener('click', el._bw_outsideHandler);
4570
+ }
4571
+ }
4572
+ }
4573
+ };
4574
+ }
4575
+
4576
+ /**
4577
+ * Create a toggle switch (styled checkbox)
4578
+ *
4579
+ * @param {Object} [props] - Switch configuration
4580
+ * @param {string} [props.label] - Switch label text
4581
+ * @param {boolean} [props.checked=false] - Whether the switch is on
4582
+ * @param {string} [props.id] - Element ID (links label to switch)
4583
+ * @param {string} [props.name] - Input name attribute
4584
+ * @param {boolean} [props.disabled=false] - Whether the switch is disabled
4585
+ * @param {string} [props.className] - Additional CSS classes
4586
+ * @returns {Object} TACO object representing a toggle switch
4587
+ * @category Component Builders
4588
+ * @example
4589
+ * const toggle = makeSwitch({
4590
+ * label: "Dark mode",
4591
+ * checked: false,
4592
+ * onchange: (e) => toggleDark(e.target.checked)
4593
+ * });
4594
+ */
4595
+ function makeSwitch(props = {}) {
4596
+ const {
4597
+ label,
4598
+ checked = false,
4599
+ id,
4600
+ name,
4601
+ disabled = false,
4602
+ className = '',
4603
+ ...eventHandlers
4604
+ } = props;
4605
+
4606
+ return {
4607
+ t: 'div',
4608
+ a: { class: `bw-form-check bw-form-switch ${className}`.trim() },
4609
+ c: [
4610
+ {
4611
+ t: 'input',
4612
+ a: {
4613
+ type: 'checkbox',
4614
+ class: 'bw-form-check-input bw-switch-input',
4615
+ role: 'switch',
4616
+ checked,
4617
+ id,
4618
+ name,
4619
+ disabled,
4620
+ ...eventHandlers
4621
+ }
4622
+ },
4623
+ label && {
4624
+ t: 'label',
4625
+ a: { class: 'bw-form-check-label', for: id },
4626
+ c: label
4627
+ }
4628
+ ].filter(Boolean)
4629
+ };
4630
+ }
4631
+
4632
+ /**
4633
+ * Create a skeleton loading placeholder
3410
4634
  *
3411
- * @category Component Handles
4635
+ * @param {Object} [props] - Skeleton configuration
4636
+ * @param {string} [props.variant="text"] - Shape variant ("text", "circle", "rect")
4637
+ * @param {string} [props.width] - Custom width (e.g. "200px", "100%")
4638
+ * @param {string} [props.height] - Custom height (e.g. "20px")
4639
+ * @param {number} [props.count=1] - Number of skeleton lines (for text variant)
4640
+ * @param {string} [props.className] - Additional CSS classes
4641
+ * @returns {Object} TACO object representing a skeleton placeholder
4642
+ * @category Component Builders
4643
+ * @example
4644
+ * const skeleton = makeSkeleton({ variant: "text", count: 3, width: "100%" });
3412
4645
  */
3413
- class TabsHandle {
3414
- /**
3415
- * @param {Element} element - The tabs container DOM element
3416
- * @param {Object} taco - The original TACO object used to create the tabs
3417
- */
3418
- constructor(element, taco) {
3419
- this.element = element;
3420
- this._taco = taco;
3421
- this.state = taco.o?.state || {};
4646
+ function makeSkeleton(props = {}) {
4647
+ const {
4648
+ variant = 'text',
4649
+ width,
4650
+ height,
4651
+ count = 1,
4652
+ className = ''
4653
+ } = props;
3422
4654
 
3423
- this.children = {
3424
- navItems: element.querySelectorAll('.bw-nav-link'),
3425
- tabPanes: element.querySelectorAll('.bw-tab-pane')
4655
+ if (variant === 'circle') {
4656
+ var circleSize = width || height || '3rem';
4657
+ return {
4658
+ t: 'div',
4659
+ a: {
4660
+ class: `bw-skeleton bw-skeleton-circle ${className}`.trim(),
4661
+ style: { width: circleSize, height: circleSize }
4662
+ }
3426
4663
  };
3427
-
3428
- this._setupTabs();
3429
4664
  }
3430
4665
 
3431
- /**
3432
- * Attach click handlers to tab navigation buttons
3433
- * @private
3434
- */
3435
- _setupTabs() {
3436
- this.children.navItems.forEach((navItem, index) => {
3437
- navItem.onclick = (e) => {
3438
- e.preventDefault();
3439
- this.switchTo(index);
3440
- };
3441
- });
4666
+ if (variant === 'rect') {
4667
+ return {
4668
+ t: 'div',
4669
+ a: {
4670
+ class: `bw-skeleton bw-skeleton-rect ${className}`.trim(),
4671
+ style: {
4672
+ width: width || '100%',
4673
+ height: height || '120px'
4674
+ }
4675
+ }
4676
+ };
3442
4677
  }
3443
4678
 
3444
- /**
3445
- * Programmatically switch to a tab by index
3446
- *
3447
- * @param {number} index - Zero-based tab index to activate
3448
- * @returns {TabsHandle} this (for chaining)
3449
- */
3450
- switchTo(index) {
3451
- this.children.navItems.forEach((item, i) => {
3452
- if (i === index) {
3453
- item.classList.add('active');
3454
- } else {
3455
- item.classList.remove('active');
4679
+ // Text variant — multiple lines
4680
+ if (count === 1) {
4681
+ return {
4682
+ t: 'div',
4683
+ a: {
4684
+ class: `bw-skeleton bw-skeleton-text ${className}`.trim(),
4685
+ style: {
4686
+ width: width || '100%',
4687
+ height: height || '1em'
4688
+ }
3456
4689
  }
3457
- });
4690
+ };
4691
+ }
3458
4692
 
3459
- this.children.tabPanes.forEach((pane, i) => {
3460
- if (i === index) {
3461
- pane.classList.add('active');
3462
- } else {
3463
- pane.classList.remove('active');
4693
+ var lines = [];
4694
+ for (var i = 0; i < count; i++) {
4695
+ lines.push({
4696
+ t: 'div',
4697
+ a: {
4698
+ class: 'bw-skeleton bw-skeleton-text',
4699
+ style: {
4700
+ width: i === count - 1 ? '75%' : (width || '100%'),
4701
+ height: height || '1em'
4702
+ }
3464
4703
  }
3465
4704
  });
3466
-
3467
- this.state.activeIndex = index;
3468
- return this;
3469
4705
  }
4706
+
4707
+ return {
4708
+ t: 'div',
4709
+ a: { class: `bw-skeleton-group ${className}`.trim() },
4710
+ c: lines
4711
+ };
3470
4712
  }
3471
4713
 
3472
4714
  /**
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
4715
+ * Create a user avatar with image or initials fallback
4716
+ *
4717
+ * @param {Object} [props] - Avatar configuration
4718
+ * @param {string} [props.src] - Image source URL
4719
+ * @param {string} [props.alt] - Image alt text
4720
+ * @param {string} [props.initials] - Fallback initials (e.g. "JD")
4721
+ * @param {string} [props.size="md"] - Size ("sm", "md", "lg", "xl")
4722
+ * @param {string} [props.variant="primary"] - Background color variant for initials
4723
+ * @param {string} [props.className] - Additional CSS classes
4724
+ * @returns {Object} TACO object representing an avatar
3485
4725
  * @category Component Builders
3486
4726
  * @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" })
3492
- * });
4727
+ * const avatar = makeAvatar({ src: "/photo.jpg", alt: "Jane Doe", size: "lg" });
4728
+ * const avatarInitials = makeAvatar({ initials: "JD", variant: "success" });
3493
4729
  */
3494
- function makeCodeDemo(props = {}) {
4730
+ function makeAvatar(props = {}) {
3495
4731
  const {
3496
- title,
3497
- description,
3498
- code,
3499
- result,
3500
- language = 'javascript'
4732
+ src,
4733
+ alt = '',
4734
+ initials,
4735
+ size = 'md',
4736
+ variant = 'primary',
4737
+ className = ''
3501
4738
  } = props;
3502
4739
 
3503
- // Generate unique ID for this demo
3504
- `demo-${Math.random().toString(36).substr(2, 9)}`;
3505
-
3506
- const tabs = [
3507
- {
3508
- label: 'Result',
3509
- active: true,
3510
- content: result
3511
- }
3512
- ];
3513
-
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
3556
- }
3557
- }
3558
- ]
4740
+ if (src) {
4741
+ return {
4742
+ t: 'img',
4743
+ a: {
4744
+ class: `bw-avatar bw-avatar-${size} ${className}`.trim(),
4745
+ src: src,
4746
+ alt: alt
3559
4747
  }
3560
- });
4748
+ };
3561
4749
  }
3562
4750
 
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
4751
  return {
3574
4752
  t: 'div',
3575
- a: { class: 'bw-code-demo' },
3576
- c: content
4753
+ a: {
4754
+ class: `bw-avatar bw-avatar-${size} bw-avatar-${variant} ${className}`.trim()
4755
+ },
4756
+ c: initials || ''
3577
4757
  };
3578
4758
  }
3579
4759
 
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
4760
  const componentHandles = {
3589
4761
  card: CardHandle,
3590
4762
  table: TableHandle,
3591
4763
  navbar: NavbarHandle,
3592
- tabs: TabsHandle
4764
+ tabs: TabsHandle,
4765
+ modal: ModalHandle
3593
4766
  };
3594
4767
 
3595
4768
  var components = /*#__PURE__*/Object.freeze({
3596
4769
  __proto__: null,
3597
4770
  CardHandle: CardHandle,
4771
+ ModalHandle: ModalHandle,
3598
4772
  NavbarHandle: NavbarHandle,
3599
4773
  TableHandle: TableHandle,
3600
4774
  TabsHandle: TabsHandle,
3601
4775
  componentHandles: componentHandles,
4776
+ makeAccordion: makeAccordion,
3602
4777
  makeAlert: makeAlert,
4778
+ makeAvatar: makeAvatar,
3603
4779
  makeBadge: makeBadge,
3604
4780
  makeBreadcrumb: makeBreadcrumb,
3605
4781
  makeButton: makeButton,
4782
+ makeButtonGroup: makeButtonGroup,
3606
4783
  makeCTA: makeCTA,
3607
4784
  makeCard: makeCard,
3608
4785
  makeCheckbox: makeCheckbox,
3609
4786
  makeCodeDemo: makeCodeDemo,
3610
4787
  makeCol: makeCol,
3611
4788
  makeContainer: makeContainer,
4789
+ makeDropdown: makeDropdown,
3612
4790
  makeFeatureGrid: makeFeatureGrid,
3613
4791
  makeForm: makeForm,
3614
4792
  makeFormGroup: makeFormGroup,
3615
4793
  makeHero: makeHero,
3616
4794
  makeInput: makeInput,
3617
4795
  makeListGroup: makeListGroup,
4796
+ makeModal: makeModal,
3618
4797
  makeNav: makeNav,
3619
4798
  makeNavbar: makeNavbar,
4799
+ makePagination: makePagination,
3620
4800
  makeProgress: makeProgress,
4801
+ makeRadio: makeRadio,
3621
4802
  makeRow: makeRow,
3622
4803
  makeSection: makeSection,
3623
4804
  makeSelect: makeSelect,
4805
+ makeSkeleton: makeSkeleton,
3624
4806
  makeSpinner: makeSpinner,
3625
4807
  makeStack: makeStack,
4808
+ makeSwitch: makeSwitch,
3626
4809
  makeTabs: makeTabs,
3627
- makeTextarea: makeTextarea
4810
+ makeTextarea: makeTextarea,
4811
+ makeToast: makeToast
3628
4812
  });
3629
4813
 
3630
4814
  /**
@@ -4766,6 +5950,16 @@ bw.patch = function(id, content, attr) {
4766
5950
  if (attr) {
4767
5951
  // Patch an attribute
4768
5952
  el.setAttribute(attr, String(content));
5953
+ } else if (Array.isArray(content)) {
5954
+ // Patch with array of children (strings and/or TACOs)
5955
+ el.innerHTML = '';
5956
+ content.forEach(function(item) {
5957
+ if (typeof item === 'string' || typeof item === 'number') {
5958
+ el.appendChild(document.createTextNode(String(item)));
5959
+ } else if (item && item.t) {
5960
+ el.appendChild(bw.createDOM(item));
5961
+ }
5962
+ });
4769
5963
  } else if (typeof content === 'object' && content !== null && content.t) {
4770
5964
  // Patch with a TACO — replace children
4771
5965
  el.innerHTML = '';
@@ -5962,6 +7156,7 @@ bw.getURLParam = function(key, defaultValue) {
5962
7156
  * @see bw.makeTable
5963
7157
  */
5964
7158
  bw.htmlTable = function(data, opts = {}) {
7159
+ console.warn('bw.htmlTable() is deprecated. Use bw.makeTableFromArray() for TACO output or bw.makeTable() for object-array data.');
5965
7160
  if (bw.typeOf(data) !== "array" || data.length < 1) return "";
5966
7161
 
5967
7162
  const dopts = {
@@ -6061,6 +7256,7 @@ bw._attrsToStr = function(attrs) {
6061
7256
  * @see bw.makeTabs
6062
7257
  */
6063
7258
  bw.htmlTabs = function(tabData, opts = {}) {
7259
+ console.warn('bw.htmlTabs() is deprecated. Use bw.makeTabs() instead.');
6064
7260
  if (bw.typeOf(tabData) !== "array" || tabData.length < 1) return "";
6065
7261
 
6066
7262
  const dopts = {
@@ -6107,6 +7303,7 @@ bw.htmlTabs = function(tabData, opts = {}) {
6107
7303
  * @category Legacy (v1)
6108
7304
  */
6109
7305
  bw.selectTabContent = function(tabElement) {
7306
+ console.warn('bw.selectTabContent() is deprecated. Use bw.makeTabs() instead.');
6110
7307
  if (!bw._isBrowser || !tabElement) return;
6111
7308
 
6112
7309
  const container = tabElement.closest(".bw-tab-container");
@@ -6635,12 +7832,21 @@ bw.makeTable = function(config) {
6635
7832
  const {
6636
7833
  data = [],
6637
7834
  columns,
6638
- className = "table",
7835
+ className = '',
7836
+ striped = false,
7837
+ hover = false,
6639
7838
  sortable = true,
6640
7839
  onSort,
6641
7840
  sortColumn,
6642
7841
  sortDirection = 'asc'
6643
7842
  } = config;
7843
+
7844
+ // Build class list: always include bw-table, add striped/hover, append user className
7845
+ let cls = 'bw-table';
7846
+ if (striped) cls += ' bw-table-striped';
7847
+ if (hover) cls += ' bw-table-hover';
7848
+ if (className) cls += ' ' + className;
7849
+ cls = cls.trim();
6644
7850
 
6645
7851
  // Auto-detect columns if not provided
6646
7852
  const cols = columns || (data.length > 0
@@ -6728,11 +7934,176 @@ bw.makeTable = function(config) {
6728
7934
 
6729
7935
  return {
6730
7936
  t: 'table',
6731
- a: { class: className },
7937
+ a: { class: cls },
6732
7938
  c: [thead, tbody]
6733
7939
  };
6734
7940
  };
6735
7941
 
7942
+ /**
7943
+ * Create a table from a 2D array.
7944
+ *
7945
+ * Converts a 2D array into the object-array format that `bw.makeTable()`
7946
+ * expects, then delegates. By default, the first row is used as column
7947
+ * headers. All standard `makeTable` props (striped, hover, sortable,
7948
+ * columns, onSort, etc.) are passed through.
7949
+ *
7950
+ * @param {Object} config - Configuration object
7951
+ * @param {Array<Array>} config.data - 2D array of values
7952
+ * @param {boolean} [config.headerRow=true] - Treat first row as column headers
7953
+ * @param {boolean} [config.striped=false] - Striped rows
7954
+ * @param {boolean} [config.hover=false] - Hover highlight
7955
+ * @param {boolean} [config.sortable=true] - Enable sort
7956
+ * @param {Array<Object>} [config.columns] - Override auto-generated column defs
7957
+ * @param {string} [config.className=''] - Additional CSS classes
7958
+ * @param {Function} [config.onSort] - Sort callback
7959
+ * @param {string} [config.sortColumn] - Currently sorted column key
7960
+ * @param {string} [config.sortDirection='asc'] - Sort direction
7961
+ * @returns {Object} TACO object for table
7962
+ * @category Component Builders
7963
+ * @see bw.makeTable
7964
+ * @example
7965
+ * bw.makeTableFromArray({
7966
+ * data: [
7967
+ * ['Name', 'Role', 'Status'],
7968
+ * ['Alice', 'Engineer', 'Active'],
7969
+ * ['Bob', 'Designer', 'Away']
7970
+ * ],
7971
+ * striped: true,
7972
+ * hover: true
7973
+ * });
7974
+ */
7975
+ bw.makeTableFromArray = function(config) {
7976
+ const { data = [], headerRow = true, columns, ...rest } = config;
7977
+
7978
+ if (!Array.isArray(data) || data.length === 0) {
7979
+ return bw.makeTable({ data: [], columns: columns || [], ...rest });
7980
+ }
7981
+
7982
+ // Determine headers
7983
+ let headers;
7984
+ let rows;
7985
+ if (headerRow && data.length > 0) {
7986
+ headers = data[0].map(function(h) { return String(h); });
7987
+ rows = data.slice(1);
7988
+ } else {
7989
+ // Generate col0, col1, ... headers
7990
+ const width = data[0].length;
7991
+ headers = [];
7992
+ for (let i = 0; i < width; i++) {
7993
+ headers.push('col' + i);
7994
+ }
7995
+ rows = data;
7996
+ }
7997
+
7998
+ // Convert rows to object arrays
7999
+ const objData = rows.map(function(row) {
8000
+ const obj = {};
8001
+ headers.forEach(function(key, i) {
8002
+ obj[key] = row[i] !== undefined ? row[i] : '';
8003
+ });
8004
+ return obj;
8005
+ });
8006
+
8007
+ // Auto-generate column defs if not provided
8008
+ const cols = columns || headers.map(function(key) {
8009
+ return { key: key, label: key };
8010
+ });
8011
+
8012
+ return bw.makeTable({ data: objData, columns: cols, ...rest });
8013
+ };
8014
+
8015
+ /**
8016
+ * Create a vertical bar chart from data.
8017
+ *
8018
+ * Renders a pure-CSS bar chart using flexbox and percentage heights.
8019
+ * No canvas, SVG, or external charting library required.
8020
+ *
8021
+ * @param {Object} config - Chart configuration
8022
+ * @param {Array<Object>} config.data - Array of data objects
8023
+ * @param {string} [config.labelKey='label'] - Key for bar labels
8024
+ * @param {string} [config.valueKey='value'] - Key for bar values
8025
+ * @param {string} [config.title] - Chart title
8026
+ * @param {string} [config.color='#006666'] - Bar color (hex or CSS color)
8027
+ * @param {string} [config.height='200px'] - Height of the chart area
8028
+ * @param {Function} [config.formatValue] - Value label formatter: (value) => string
8029
+ * @param {boolean} [config.showValues=true] - Show value labels above bars
8030
+ * @param {boolean} [config.showLabels=true] - Show labels below bars
8031
+ * @param {string} [config.className=''] - Additional CSS classes
8032
+ * @returns {Object} TACO object
8033
+ * @category Component Builders
8034
+ * @example
8035
+ * bw.makeBarChart({
8036
+ * data: [
8037
+ * { label: 'Jan', value: 12400 },
8038
+ * { label: 'Feb', value: 15800 },
8039
+ * { label: 'Mar', value: 9200 }
8040
+ * ],
8041
+ * title: 'Monthly Revenue',
8042
+ * color: '#0077b6',
8043
+ * formatValue: (v) => '$' + (v / 1000).toFixed(1) + 'k'
8044
+ * });
8045
+ */
8046
+ bw.makeBarChart = function(config) {
8047
+ const {
8048
+ data = [],
8049
+ labelKey = 'label',
8050
+ valueKey = 'value',
8051
+ title,
8052
+ color = '#006666',
8053
+ height = '200px',
8054
+ formatValue,
8055
+ showValues = true,
8056
+ showLabels = true,
8057
+ className = ''
8058
+ } = config;
8059
+
8060
+ if (!Array.isArray(data) || data.length === 0) {
8061
+ return { t: 'div', a: { class: ('bw-bar-chart-container ' + className).trim() }, c: '' };
8062
+ }
8063
+
8064
+ const values = data.map(function(d) { return Number(d[valueKey]) || 0; });
8065
+ const maxVal = Math.max.apply(null, values);
8066
+
8067
+ const bars = data.map(function(d, i) {
8068
+ const val = values[i];
8069
+ const pct = maxVal > 0 ? (val / maxVal * 100) : 0;
8070
+ const formatted = formatValue ? formatValue(val) : String(val);
8071
+
8072
+ const children = [];
8073
+ if (showValues) {
8074
+ children.push({ t: 'div', a: { class: 'bw-bar-value' }, c: formatted });
8075
+ }
8076
+ children.push({
8077
+ t: 'div',
8078
+ a: {
8079
+ class: 'bw-bar',
8080
+ style: 'height:' + pct + '%;background:' + color + ';'
8081
+ }
8082
+ });
8083
+ if (showLabels) {
8084
+ children.push({ t: 'div', a: { class: 'bw-bar-label' }, c: String(d[labelKey] || '') });
8085
+ }
8086
+
8087
+ return { t: 'div', a: { class: 'bw-bar-group' }, c: children };
8088
+ });
8089
+
8090
+ const chartChildren = [];
8091
+ if (title) {
8092
+ chartChildren.push({ t: 'h3', a: { class: 'bw-bar-chart-title' }, c: title });
8093
+ }
8094
+ chartChildren.push({
8095
+ t: 'div',
8096
+ a: { class: 'bw-bar-chart', style: 'height:' + height + ';' },
8097
+ c: bars
8098
+ });
8099
+
8100
+ return {
8101
+ t: 'div',
8102
+ a: { class: ('bw-bar-chart-container ' + className).trim() },
8103
+ c: chartChildren
8104
+ };
8105
+ };
8106
+
6736
8107
  /**
6737
8108
  * Create a responsive data table with title and optional wrapper
6738
8109
  *
@@ -6743,7 +8114,9 @@ bw.makeTable = function(config) {
6743
8114
  * @param {string} [config.title] - Table title heading
6744
8115
  * @param {Array<Object>} config.data - Array of row objects
6745
8116
  * @param {Array<Object>} [config.columns] - Column definitions
6746
- * @param {string} [config.className="table table-striped table-hover"] - Table CSS class
8117
+ * @param {string} [config.className=''] - Additional CSS classes for the table
8118
+ * @param {boolean} [config.striped=true] - Add striped row styling
8119
+ * @param {boolean} [config.hover=true] - Add hover row highlighting
6747
8120
  * @param {boolean} [config.responsive=true] - Wrap table in responsive overflow div
6748
8121
  * @returns {Object} TACO object for table with wrapper
6749
8122
  * @example
@@ -6758,7 +8131,9 @@ bw.makeDataTable = function(config) {
6758
8131
  title,
6759
8132
  data,
6760
8133
  columns,
6761
- className = "table table-striped table-hover",
8134
+ className = '',
8135
+ striped = true,
8136
+ hover = true,
6762
8137
  responsive = true,
6763
8138
  ...tableConfig
6764
8139
  } = config;
@@ -6767,6 +8142,8 @@ bw.makeDataTable = function(config) {
6767
8142
  data,
6768
8143
  columns,
6769
8144
  className,
8145
+ striped,
8146
+ hover,
6770
8147
  ...tableConfig
6771
8148
  });
6772
8149