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