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