bitwrench 2.0.10 → 2.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/bitwrench-code-edit.cjs.js +1 -1
- package/dist/bitwrench-code-edit.es5.js +1 -1
- package/dist/bitwrench-code-edit.es5.min.js +1 -1
- package/dist/bitwrench-code-edit.esm.js +1 -1
- package/dist/bitwrench-code-edit.esm.min.js +1 -1
- package/dist/bitwrench-code-edit.umd.js +1 -1
- package/dist/bitwrench-code-edit.umd.min.js +1 -1
- package/dist/bitwrench-lean.cjs.js +5880 -0
- package/dist/bitwrench-lean.cjs.min.js +44 -0
- package/dist/bitwrench-lean.es5.js +7547 -0
- package/dist/bitwrench-lean.es5.min.js +13 -0
- package/dist/bitwrench-lean.esm.js +5878 -0
- package/dist/bitwrench-lean.esm.min.js +44 -0
- package/dist/bitwrench-lean.umd.js +5886 -0
- package/dist/bitwrench-lean.umd.min.js +44 -0
- package/dist/bitwrench.cjs.js +1782 -125
- package/dist/bitwrench.cjs.min.js +6 -6
- package/dist/bitwrench.css +1714 -106
- package/dist/bitwrench.es5.js +4222 -2066
- package/dist/bitwrench.es5.min.js +4 -4
- package/dist/bitwrench.esm.js +1782 -125
- package/dist/bitwrench.esm.min.js +6 -6
- package/dist/bitwrench.umd.js +1782 -125
- package/dist/bitwrench.umd.min.js +6 -6
- package/dist/builds.json +137 -49
- package/dist/sri.json +25 -17
- package/package.json +6 -4
- package/readme.html +4 -3
- package/src/bitwrench-components-stub.js +5 -0
- package/src/bitwrench-components-v2.js +1049 -25
- package/src/bitwrench-lean.js +14 -0
- package/src/bitwrench-styles.js +1037 -15
- package/src/bitwrench.js +236 -6
- package/src/generate-css.js +20 -3
- package/src/version.js +4 -4
|
@@ -563,7 +563,11 @@ export function makeAlert(props = {}) {
|
|
|
563
563
|
a: {
|
|
564
564
|
type: 'button',
|
|
565
565
|
class: 'bw-close',
|
|
566
|
-
'aria-label': 'Close'
|
|
566
|
+
'aria-label': 'Close',
|
|
567
|
+
onclick: function(e) {
|
|
568
|
+
var alert = e.target.closest('.bw-alert');
|
|
569
|
+
if (alert) { alert.remove(); }
|
|
570
|
+
}
|
|
567
571
|
},
|
|
568
572
|
c: '×'
|
|
569
573
|
}
|
|
@@ -577,25 +581,30 @@ export function makeAlert(props = {}) {
|
|
|
577
581
|
* @param {Object} [props] - Badge configuration
|
|
578
582
|
* @param {string} [props.text] - Badge display text
|
|
579
583
|
* @param {string} [props.variant="primary"] - Color variant
|
|
584
|
+
* @param {string} [props.size] - Size variant: 'sm' or 'lg' (default is medium)
|
|
580
585
|
* @param {boolean} [props.pill=false] - Use pill (rounded) shape
|
|
581
586
|
* @param {string} [props.className] - Additional CSS classes
|
|
582
587
|
* @returns {Object} TACO object representing a badge span
|
|
583
588
|
* @category Component Builders
|
|
584
589
|
* @example
|
|
585
590
|
* const badge = makeBadge({ text: "New", variant: "danger", pill: true });
|
|
591
|
+
* const small = makeBadge({ text: "3", variant: "info", size: "sm" });
|
|
586
592
|
*/
|
|
587
593
|
export function makeBadge(props = {}) {
|
|
588
594
|
const {
|
|
589
595
|
text,
|
|
590
596
|
variant = 'primary',
|
|
597
|
+
size,
|
|
591
598
|
pill = false,
|
|
592
599
|
className = ''
|
|
593
600
|
} = props;
|
|
594
601
|
|
|
602
|
+
const sizeClass = size === 'sm' ? ' bw-badge-sm' : size === 'lg' ? ' bw-badge-lg' : '';
|
|
603
|
+
|
|
595
604
|
return {
|
|
596
605
|
t: 'span',
|
|
597
606
|
a: {
|
|
598
|
-
class: `bw-badge bw-badge-${variant} ${pill ? 'bw-badge-pill' : ''} ${className}`.trim()
|
|
607
|
+
class: `bw-badge bw-badge-${variant}${sizeClass} ${pill ? 'bw-badge-pill' : ''} ${className}`.trim()
|
|
599
608
|
},
|
|
600
609
|
c: text
|
|
601
610
|
};
|
|
@@ -1054,12 +1063,14 @@ export function makeCheckbox(props = {}) {
|
|
|
1054
1063
|
id,
|
|
1055
1064
|
name,
|
|
1056
1065
|
disabled = false,
|
|
1057
|
-
value
|
|
1066
|
+
value,
|
|
1067
|
+
className = '',
|
|
1068
|
+
...eventHandlers
|
|
1058
1069
|
} = props;
|
|
1059
1070
|
|
|
1060
1071
|
return {
|
|
1061
1072
|
t: 'div',
|
|
1062
|
-
a: { class:
|
|
1073
|
+
a: { class: `bw-form-check ${className}`.trim() },
|
|
1063
1074
|
c: [
|
|
1064
1075
|
{
|
|
1065
1076
|
t: 'input',
|
|
@@ -1070,7 +1081,8 @@ export function makeCheckbox(props = {}) {
|
|
|
1070
1081
|
id,
|
|
1071
1082
|
name,
|
|
1072
1083
|
disabled,
|
|
1073
|
-
value
|
|
1084
|
+
value,
|
|
1085
|
+
...eventHandlers
|
|
1074
1086
|
}
|
|
1075
1087
|
},
|
|
1076
1088
|
label && {
|
|
@@ -1298,8 +1310,8 @@ export function makeFeatureGrid(props = {}) {
|
|
|
1298
1310
|
feature.icon && {
|
|
1299
1311
|
t: 'div',
|
|
1300
1312
|
a: {
|
|
1301
|
-
class: 'bw-feature-icon bw-mb-3',
|
|
1302
|
-
style: `font-size: ${iconSize}
|
|
1313
|
+
class: 'bw-feature-icon bw-mb-3 bw-text-primary',
|
|
1314
|
+
style: `font-size: ${iconSize};`
|
|
1303
1315
|
},
|
|
1304
1316
|
c: feature.icon
|
|
1305
1317
|
},
|
|
@@ -1808,8 +1820,7 @@ export function makeCodeDemo(props = {}) {
|
|
|
1808
1820
|
{
|
|
1809
1821
|
t: 'button',
|
|
1810
1822
|
a: {
|
|
1811
|
-
class: 'bw-copy-btn',
|
|
1812
|
-
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;',
|
|
1823
|
+
class: 'bw-copy-btn bw-code-copy-btn',
|
|
1813
1824
|
onclick: (e) => {
|
|
1814
1825
|
navigator.clipboard.writeText(code).then(() => {
|
|
1815
1826
|
const btn = e.target;
|
|
@@ -1827,20 +1838,17 @@ export function makeCodeDemo(props = {}) {
|
|
|
1827
1838
|
},
|
|
1828
1839
|
c: 'Copy'
|
|
1829
1840
|
},
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
}
|
|
1841
|
-
c: code
|
|
1842
|
-
}
|
|
1843
|
-
}
|
|
1841
|
+
(typeof globalThis !== 'undefined' && typeof globalThis.bw !== 'undefined' && typeof globalThis.bw.codeEditor === 'function')
|
|
1842
|
+
? globalThis.bw.codeEditor({ code: code, lang: language === 'javascript' ? 'js' : language, readOnly: true, height: 'auto' })
|
|
1843
|
+
: {
|
|
1844
|
+
t: 'pre',
|
|
1845
|
+
a: { class: 'bw-code-pre' },
|
|
1846
|
+
c: {
|
|
1847
|
+
t: 'code',
|
|
1848
|
+
a: { class: `bw-code-block language-${language}` },
|
|
1849
|
+
c: code
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1844
1852
|
]
|
|
1845
1853
|
}
|
|
1846
1854
|
});
|
|
@@ -1850,7 +1858,7 @@ export function makeCodeDemo(props = {}) {
|
|
|
1850
1858
|
title && { t: 'h3', c: title },
|
|
1851
1859
|
description && {
|
|
1852
1860
|
t: 'p',
|
|
1853
|
-
a: {
|
|
1861
|
+
a: { class: 'bw-text-muted', style: 'margin-bottom: 1rem;' },
|
|
1854
1862
|
c: description
|
|
1855
1863
|
},
|
|
1856
1864
|
makeTabs({ tabs, id: demoId })
|
|
@@ -1871,9 +1879,1025 @@ export function makeCodeDemo(props = {}) {
|
|
|
1871
1879
|
*
|
|
1872
1880
|
* @type {Object.<string, Function>}
|
|
1873
1881
|
*/
|
|
1882
|
+
// =========================================================================
|
|
1883
|
+
// Phase 1: Quick Wins
|
|
1884
|
+
// =========================================================================
|
|
1885
|
+
|
|
1886
|
+
/**
|
|
1887
|
+
* Create a pagination navigation component
|
|
1888
|
+
*
|
|
1889
|
+
* @param {Object} [props] - Pagination configuration
|
|
1890
|
+
* @param {number} [props.pages=1] - Total number of pages
|
|
1891
|
+
* @param {number} [props.currentPage=1] - Currently active page (1-based)
|
|
1892
|
+
* @param {Function} [props.onPageChange] - Callback when page changes, receives page number
|
|
1893
|
+
* @param {string} [props.size] - Size variant ("sm" or "lg")
|
|
1894
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
1895
|
+
* @returns {Object} TACO object representing a pagination nav
|
|
1896
|
+
* @category Component Builders
|
|
1897
|
+
* @example
|
|
1898
|
+
* const pager = makePagination({
|
|
1899
|
+
* pages: 10,
|
|
1900
|
+
* currentPage: 3,
|
|
1901
|
+
* onPageChange: (page) => loadPage(page)
|
|
1902
|
+
* });
|
|
1903
|
+
*/
|
|
1904
|
+
export function makePagination(props = {}) {
|
|
1905
|
+
const {
|
|
1906
|
+
pages = 1,
|
|
1907
|
+
currentPage = 1,
|
|
1908
|
+
onPageChange,
|
|
1909
|
+
size,
|
|
1910
|
+
className = ''
|
|
1911
|
+
} = props;
|
|
1912
|
+
|
|
1913
|
+
function handleClick(page) {
|
|
1914
|
+
return function(e) {
|
|
1915
|
+
e.preventDefault();
|
|
1916
|
+
if (page < 1 || page > pages || page === currentPage) return;
|
|
1917
|
+
if (onPageChange) onPageChange(page);
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
const items = [];
|
|
1922
|
+
|
|
1923
|
+
// Previous arrow
|
|
1924
|
+
items.push({
|
|
1925
|
+
t: 'li',
|
|
1926
|
+
a: { class: `bw-page-item ${currentPage <= 1 ? 'bw-disabled' : ''}`.trim() },
|
|
1927
|
+
c: {
|
|
1928
|
+
t: 'a',
|
|
1929
|
+
a: { class: 'bw-page-link', href: '#', onclick: handleClick(currentPage - 1), 'aria-label': 'Previous' },
|
|
1930
|
+
c: '\u2039'
|
|
1931
|
+
}
|
|
1932
|
+
});
|
|
1933
|
+
|
|
1934
|
+
// Page numbers
|
|
1935
|
+
for (var i = 1; i <= pages; i++) {
|
|
1936
|
+
(function(pageNum) {
|
|
1937
|
+
items.push({
|
|
1938
|
+
t: 'li',
|
|
1939
|
+
a: { class: `bw-page-item ${pageNum === currentPage ? 'bw-active' : ''}`.trim() },
|
|
1940
|
+
c: {
|
|
1941
|
+
t: 'a',
|
|
1942
|
+
a: { class: 'bw-page-link', href: '#', onclick: handleClick(pageNum) },
|
|
1943
|
+
c: '' + pageNum
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1946
|
+
})(i);
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
// Next arrow
|
|
1950
|
+
items.push({
|
|
1951
|
+
t: 'li',
|
|
1952
|
+
a: { class: `bw-page-item ${currentPage >= pages ? 'bw-disabled' : ''}`.trim() },
|
|
1953
|
+
c: {
|
|
1954
|
+
t: 'a',
|
|
1955
|
+
a: { class: 'bw-page-link', href: '#', onclick: handleClick(currentPage + 1), 'aria-label': 'Next' },
|
|
1956
|
+
c: '\u203A'
|
|
1957
|
+
}
|
|
1958
|
+
});
|
|
1959
|
+
|
|
1960
|
+
return {
|
|
1961
|
+
t: 'nav',
|
|
1962
|
+
a: { 'aria-label': 'Pagination' },
|
|
1963
|
+
c: {
|
|
1964
|
+
t: 'ul',
|
|
1965
|
+
a: {
|
|
1966
|
+
class: `bw-pagination ${size ? 'bw-pagination-' + size : ''} ${className}`.trim()
|
|
1967
|
+
},
|
|
1968
|
+
c: items
|
|
1969
|
+
}
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
/**
|
|
1974
|
+
* Create a radio button input with label
|
|
1975
|
+
*
|
|
1976
|
+
* @param {Object} [props] - Radio configuration
|
|
1977
|
+
* @param {string} [props.label] - Radio label text
|
|
1978
|
+
* @param {string} [props.name] - Radio group name
|
|
1979
|
+
* @param {string} [props.value] - Radio value attribute
|
|
1980
|
+
* @param {boolean} [props.checked=false] - Whether the radio is selected
|
|
1981
|
+
* @param {string} [props.id] - Element ID (links label to radio)
|
|
1982
|
+
* @param {boolean} [props.disabled=false] - Whether the radio is disabled
|
|
1983
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
1984
|
+
* @returns {Object} TACO object representing a radio form group
|
|
1985
|
+
* @category Component Builders
|
|
1986
|
+
* @example
|
|
1987
|
+
* const radio = makeRadio({
|
|
1988
|
+
* label: "Option A",
|
|
1989
|
+
* name: "choice",
|
|
1990
|
+
* value: "a",
|
|
1991
|
+
* checked: true
|
|
1992
|
+
* });
|
|
1993
|
+
*/
|
|
1994
|
+
export function makeRadio(props = {}) {
|
|
1995
|
+
const {
|
|
1996
|
+
label,
|
|
1997
|
+
name,
|
|
1998
|
+
value,
|
|
1999
|
+
checked = false,
|
|
2000
|
+
id,
|
|
2001
|
+
disabled = false,
|
|
2002
|
+
className = '',
|
|
2003
|
+
...eventHandlers
|
|
2004
|
+
} = props;
|
|
2005
|
+
|
|
2006
|
+
return {
|
|
2007
|
+
t: 'div',
|
|
2008
|
+
a: { class: `bw-form-check ${className}`.trim() },
|
|
2009
|
+
c: [
|
|
2010
|
+
{
|
|
2011
|
+
t: 'input',
|
|
2012
|
+
a: {
|
|
2013
|
+
type: 'radio',
|
|
2014
|
+
class: 'bw-form-check-input',
|
|
2015
|
+
name,
|
|
2016
|
+
value,
|
|
2017
|
+
checked,
|
|
2018
|
+
id,
|
|
2019
|
+
disabled,
|
|
2020
|
+
...eventHandlers
|
|
2021
|
+
}
|
|
2022
|
+
},
|
|
2023
|
+
label && {
|
|
2024
|
+
t: 'label',
|
|
2025
|
+
a: { class: 'bw-form-check-label', for: id },
|
|
2026
|
+
c: label
|
|
2027
|
+
}
|
|
2028
|
+
].filter(Boolean)
|
|
2029
|
+
};
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
/**
|
|
2033
|
+
* Create a button group wrapper
|
|
2034
|
+
*
|
|
2035
|
+
* @param {Object} [props] - Button group configuration
|
|
2036
|
+
* @param {Array} [props.children] - Button TACO objects to group
|
|
2037
|
+
* @param {string} [props.size] - Size variant ("sm" or "lg")
|
|
2038
|
+
* @param {boolean} [props.vertical=false] - Stack buttons vertically
|
|
2039
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
2040
|
+
* @returns {Object} TACO object representing a button group
|
|
2041
|
+
* @category Component Builders
|
|
2042
|
+
* @example
|
|
2043
|
+
* const group = makeButtonGroup({
|
|
2044
|
+
* children: [
|
|
2045
|
+
* makeButton({ text: "Left", variant: "primary" }),
|
|
2046
|
+
* makeButton({ text: "Middle", variant: "primary" }),
|
|
2047
|
+
* makeButton({ text: "Right", variant: "primary" })
|
|
2048
|
+
* ]
|
|
2049
|
+
* });
|
|
2050
|
+
*/
|
|
2051
|
+
export function makeButtonGroup(props = {}) {
|
|
2052
|
+
const {
|
|
2053
|
+
children,
|
|
2054
|
+
size,
|
|
2055
|
+
vertical = false,
|
|
2056
|
+
className = ''
|
|
2057
|
+
} = props;
|
|
2058
|
+
|
|
2059
|
+
return {
|
|
2060
|
+
t: 'div',
|
|
2061
|
+
a: {
|
|
2062
|
+
class: `${vertical ? 'bw-btn-group-vertical' : 'bw-btn-group'} ${size ? 'bw-btn-group-' + size : ''} ${className}`.trim(),
|
|
2063
|
+
role: 'group'
|
|
2064
|
+
},
|
|
2065
|
+
c: children
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
// =========================================================================
|
|
2070
|
+
// Phase 2: Core Interactive
|
|
2071
|
+
// =========================================================================
|
|
2072
|
+
|
|
2073
|
+
/**
|
|
2074
|
+
* Create an accordion component with collapsible items
|
|
2075
|
+
*
|
|
2076
|
+
* @param {Object} [props] - Accordion configuration
|
|
2077
|
+
* @param {Array<Object>} [props.items=[]] - Accordion items
|
|
2078
|
+
* @param {string} props.items[].title - Header text for the accordion item
|
|
2079
|
+
* @param {string|Object|Array} props.items[].content - Collapsible content
|
|
2080
|
+
* @param {boolean} [props.items[].open=false] - Whether the item is initially open
|
|
2081
|
+
* @param {boolean} [props.multiOpen=false] - Allow multiple items open simultaneously
|
|
2082
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
2083
|
+
* @returns {Object} TACO object representing an accordion
|
|
2084
|
+
* @category Component Builders
|
|
2085
|
+
* @example
|
|
2086
|
+
* const accordion = makeAccordion({
|
|
2087
|
+
* items: [
|
|
2088
|
+
* { title: "Section 1", content: "Content 1", open: true },
|
|
2089
|
+
* { title: "Section 2", content: "Content 2" }
|
|
2090
|
+
* ]
|
|
2091
|
+
* });
|
|
2092
|
+
*/
|
|
2093
|
+
export function makeAccordion(props = {}) {
|
|
2094
|
+
const {
|
|
2095
|
+
items = [],
|
|
2096
|
+
multiOpen = false,
|
|
2097
|
+
className = ''
|
|
2098
|
+
} = props;
|
|
2099
|
+
|
|
2100
|
+
return {
|
|
2101
|
+
t: 'div',
|
|
2102
|
+
a: { class: `bw-accordion ${className}`.trim() },
|
|
2103
|
+
c: items.map(function(item, index) {
|
|
2104
|
+
return {
|
|
2105
|
+
t: 'div',
|
|
2106
|
+
a: { class: 'bw-accordion-item' },
|
|
2107
|
+
c: [
|
|
2108
|
+
{
|
|
2109
|
+
t: 'h2',
|
|
2110
|
+
a: { class: 'bw-accordion-header' },
|
|
2111
|
+
c: {
|
|
2112
|
+
t: 'button',
|
|
2113
|
+
a: {
|
|
2114
|
+
class: `bw-accordion-button ${item.open ? '' : 'bw-collapsed'}`.trim(),
|
|
2115
|
+
type: 'button',
|
|
2116
|
+
'aria-expanded': item.open ? 'true' : 'false',
|
|
2117
|
+
'data-accordion-index': index,
|
|
2118
|
+
onclick: function(e) {
|
|
2119
|
+
var btn = e.target.closest('.bw-accordion-button');
|
|
2120
|
+
var accordionEl = btn.closest('.bw-accordion');
|
|
2121
|
+
var accordionItem = btn.closest('.bw-accordion-item');
|
|
2122
|
+
var collapse = accordionItem.querySelector('.bw-accordion-collapse');
|
|
2123
|
+
var isOpen = collapse.classList.contains('bw-collapse-show');
|
|
2124
|
+
|
|
2125
|
+
if (!multiOpen) {
|
|
2126
|
+
// Close all siblings
|
|
2127
|
+
var allCollapses = accordionEl.querySelectorAll('.bw-accordion-collapse');
|
|
2128
|
+
var allButtons = accordionEl.querySelectorAll('.bw-accordion-button');
|
|
2129
|
+
for (var j = 0; j < allCollapses.length; j++) {
|
|
2130
|
+
allCollapses[j].classList.remove('bw-collapse-show');
|
|
2131
|
+
allCollapses[j].style.maxHeight = null;
|
|
2132
|
+
}
|
|
2133
|
+
for (var k = 0; k < allButtons.length; k++) {
|
|
2134
|
+
allButtons[k].classList.add('bw-collapsed');
|
|
2135
|
+
allButtons[k].setAttribute('aria-expanded', 'false');
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
if (isOpen) {
|
|
2140
|
+
collapse.classList.remove('bw-collapse-show');
|
|
2141
|
+
collapse.style.maxHeight = null;
|
|
2142
|
+
btn.classList.add('bw-collapsed');
|
|
2143
|
+
btn.setAttribute('aria-expanded', 'false');
|
|
2144
|
+
} else {
|
|
2145
|
+
collapse.classList.add('bw-collapse-show');
|
|
2146
|
+
collapse.style.maxHeight = collapse.scrollHeight + 'px';
|
|
2147
|
+
btn.classList.remove('bw-collapsed');
|
|
2148
|
+
btn.setAttribute('aria-expanded', 'true');
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
},
|
|
2152
|
+
c: item.title
|
|
2153
|
+
}
|
|
2154
|
+
},
|
|
2155
|
+
{
|
|
2156
|
+
t: 'div',
|
|
2157
|
+
a: { class: `bw-accordion-collapse ${item.open ? 'bw-collapse-show' : ''}`.trim() },
|
|
2158
|
+
c: {
|
|
2159
|
+
t: 'div',
|
|
2160
|
+
a: { class: 'bw-accordion-body' },
|
|
2161
|
+
c: item.content
|
|
2162
|
+
},
|
|
2163
|
+
o: item.open ? {
|
|
2164
|
+
mounted: function(el) {
|
|
2165
|
+
el.style.maxHeight = el.scrollHeight + 'px';
|
|
2166
|
+
}
|
|
2167
|
+
} : undefined
|
|
2168
|
+
}
|
|
2169
|
+
]
|
|
2170
|
+
};
|
|
2171
|
+
}),
|
|
2172
|
+
o: {
|
|
2173
|
+
type: 'accordion',
|
|
2174
|
+
state: { multiOpen: multiOpen }
|
|
2175
|
+
}
|
|
2176
|
+
};
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
/**
|
|
2180
|
+
* Imperative handle for a rendered modal component
|
|
2181
|
+
*
|
|
2182
|
+
* Provides `.show()`, `.hide()`, `.toggle()`, and `.destroy()` methods
|
|
2183
|
+
* for controlling the modal programmatically.
|
|
2184
|
+
*
|
|
2185
|
+
* @category Component Handles
|
|
2186
|
+
*/
|
|
2187
|
+
export class ModalHandle {
|
|
2188
|
+
/**
|
|
2189
|
+
* @param {Element} element - The modal backdrop DOM element
|
|
2190
|
+
* @param {Object} taco - The original TACO object
|
|
2191
|
+
*/
|
|
2192
|
+
constructor(element, taco) {
|
|
2193
|
+
this.element = element;
|
|
2194
|
+
this._taco = taco;
|
|
2195
|
+
this._escHandler = null;
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
/** Show the modal */
|
|
2199
|
+
show() {
|
|
2200
|
+
this.element.classList.add('bw-modal-show');
|
|
2201
|
+
document.body.style.overflow = 'hidden';
|
|
2202
|
+
return this;
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
/** Hide the modal */
|
|
2206
|
+
hide() {
|
|
2207
|
+
this.element.classList.remove('bw-modal-show');
|
|
2208
|
+
document.body.style.overflow = '';
|
|
2209
|
+
return this;
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
/** Toggle modal visibility */
|
|
2213
|
+
toggle() {
|
|
2214
|
+
if (this.element.classList.contains('bw-modal-show')) {
|
|
2215
|
+
this.hide();
|
|
2216
|
+
} else {
|
|
2217
|
+
this.show();
|
|
2218
|
+
}
|
|
2219
|
+
return this;
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
/** Remove the modal from DOM and clean up */
|
|
2223
|
+
destroy() {
|
|
2224
|
+
this.hide();
|
|
2225
|
+
if (this._escHandler) {
|
|
2226
|
+
document.removeEventListener('keydown', this._escHandler);
|
|
2227
|
+
}
|
|
2228
|
+
if (this.element.parentNode) {
|
|
2229
|
+
this.element.parentNode.removeChild(this.element);
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
/**
|
|
2235
|
+
* Create a modal dialog overlay
|
|
2236
|
+
*
|
|
2237
|
+
* @param {Object} [props] - Modal configuration
|
|
2238
|
+
* @param {string} [props.title] - Modal title in header
|
|
2239
|
+
* @param {string|Object|Array} [props.content] - Modal body content
|
|
2240
|
+
* @param {string|Object|Array} [props.footer] - Modal footer content
|
|
2241
|
+
* @param {string} [props.size] - Modal size ("sm", "lg", "xl")
|
|
2242
|
+
* @param {boolean} [props.closeButton=true] - Show X close button in header
|
|
2243
|
+
* @param {Function} [props.onClose] - Callback when modal is closed
|
|
2244
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
2245
|
+
* @returns {Object} TACO object representing a modal
|
|
2246
|
+
* @category Component Builders
|
|
2247
|
+
* @example
|
|
2248
|
+
* const modal = makeModal({
|
|
2249
|
+
* title: "Confirm",
|
|
2250
|
+
* content: "Are you sure?",
|
|
2251
|
+
* footer: makeButton({ text: "OK", variant: "primary" })
|
|
2252
|
+
* });
|
|
2253
|
+
*/
|
|
2254
|
+
export function makeModal(props = {}) {
|
|
2255
|
+
const {
|
|
2256
|
+
title,
|
|
2257
|
+
content,
|
|
2258
|
+
footer,
|
|
2259
|
+
size,
|
|
2260
|
+
closeButton = true,
|
|
2261
|
+
onClose,
|
|
2262
|
+
className = ''
|
|
2263
|
+
} = props;
|
|
2264
|
+
|
|
2265
|
+
function closeModal(el) {
|
|
2266
|
+
var backdrop = el.closest('.bw-modal');
|
|
2267
|
+
if (backdrop) {
|
|
2268
|
+
backdrop.classList.remove('bw-modal-show');
|
|
2269
|
+
document.body.style.overflow = '';
|
|
2270
|
+
}
|
|
2271
|
+
if (onClose) onClose();
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
return {
|
|
2275
|
+
t: 'div',
|
|
2276
|
+
a: { class: `bw-modal ${className}`.trim() },
|
|
2277
|
+
c: {
|
|
2278
|
+
t: 'div',
|
|
2279
|
+
a: { class: `bw-modal-dialog ${size ? 'bw-modal-' + size : ''}`.trim() },
|
|
2280
|
+
c: {
|
|
2281
|
+
t: 'div',
|
|
2282
|
+
a: { class: 'bw-modal-content' },
|
|
2283
|
+
c: [
|
|
2284
|
+
(title || closeButton) && {
|
|
2285
|
+
t: 'div',
|
|
2286
|
+
a: { class: 'bw-modal-header' },
|
|
2287
|
+
c: [
|
|
2288
|
+
title && { t: 'h5', a: { class: 'bw-modal-title' }, c: title },
|
|
2289
|
+
closeButton && {
|
|
2290
|
+
t: 'button',
|
|
2291
|
+
a: {
|
|
2292
|
+
type: 'button',
|
|
2293
|
+
class: 'bw-close',
|
|
2294
|
+
'aria-label': 'Close',
|
|
2295
|
+
onclick: function(e) { closeModal(e.target); }
|
|
2296
|
+
},
|
|
2297
|
+
c: '\u00D7'
|
|
2298
|
+
}
|
|
2299
|
+
].filter(Boolean)
|
|
2300
|
+
},
|
|
2301
|
+
content && {
|
|
2302
|
+
t: 'div',
|
|
2303
|
+
a: { class: 'bw-modal-body' },
|
|
2304
|
+
c: content
|
|
2305
|
+
},
|
|
2306
|
+
footer && {
|
|
2307
|
+
t: 'div',
|
|
2308
|
+
a: { class: 'bw-modal-footer' },
|
|
2309
|
+
c: footer
|
|
2310
|
+
}
|
|
2311
|
+
].filter(Boolean)
|
|
2312
|
+
}
|
|
2313
|
+
},
|
|
2314
|
+
o: {
|
|
2315
|
+
type: 'modal',
|
|
2316
|
+
mounted: function(el) {
|
|
2317
|
+
// Click backdrop to close
|
|
2318
|
+
el.addEventListener('click', function(e) {
|
|
2319
|
+
if (e.target === el) closeModal(el);
|
|
2320
|
+
});
|
|
2321
|
+
// Escape key to close
|
|
2322
|
+
var escHandler = function(e) {
|
|
2323
|
+
if (e.key === 'Escape' && el.classList.contains('bw-modal-show')) {
|
|
2324
|
+
closeModal(el);
|
|
2325
|
+
}
|
|
2326
|
+
};
|
|
2327
|
+
document.addEventListener('keydown', escHandler);
|
|
2328
|
+
el._bw_escHandler = escHandler;
|
|
2329
|
+
},
|
|
2330
|
+
unmount: function(el) {
|
|
2331
|
+
if (el._bw_escHandler) {
|
|
2332
|
+
document.removeEventListener('keydown', el._bw_escHandler);
|
|
2333
|
+
}
|
|
2334
|
+
document.body.style.overflow = '';
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
/**
|
|
2341
|
+
* Create a toast notification popup
|
|
2342
|
+
*
|
|
2343
|
+
* @param {Object} [props] - Toast configuration
|
|
2344
|
+
* @param {string} [props.title] - Toast title
|
|
2345
|
+
* @param {string|Object|Array} [props.content] - Toast body content
|
|
2346
|
+
* @param {string} [props.variant="info"] - Color variant ("primary", "success", "danger", "warning", "info")
|
|
2347
|
+
* @param {boolean} [props.autoDismiss=true] - Auto-dismiss after delay
|
|
2348
|
+
* @param {number} [props.delay=5000] - Auto-dismiss delay in ms
|
|
2349
|
+
* @param {string} [props.position="top-right"] - Container position
|
|
2350
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
2351
|
+
* @returns {Object} TACO object representing a toast
|
|
2352
|
+
* @category Component Builders
|
|
2353
|
+
* @example
|
|
2354
|
+
* const toast = makeToast({
|
|
2355
|
+
* title: "Success",
|
|
2356
|
+
* content: "File saved!",
|
|
2357
|
+
* variant: "success"
|
|
2358
|
+
* });
|
|
2359
|
+
*/
|
|
2360
|
+
export function makeToast(props = {}) {
|
|
2361
|
+
const {
|
|
2362
|
+
title,
|
|
2363
|
+
content,
|
|
2364
|
+
variant = 'info',
|
|
2365
|
+
autoDismiss = true,
|
|
2366
|
+
delay = 5000,
|
|
2367
|
+
position = 'top-right',
|
|
2368
|
+
className = ''
|
|
2369
|
+
} = props;
|
|
2370
|
+
|
|
2371
|
+
return {
|
|
2372
|
+
t: 'div',
|
|
2373
|
+
a: {
|
|
2374
|
+
class: `bw-toast bw-toast-${variant} ${className}`.trim(),
|
|
2375
|
+
role: 'alert',
|
|
2376
|
+
'data-position': position
|
|
2377
|
+
},
|
|
2378
|
+
c: [
|
|
2379
|
+
(title) && {
|
|
2380
|
+
t: 'div',
|
|
2381
|
+
a: { class: 'bw-toast-header' },
|
|
2382
|
+
c: [
|
|
2383
|
+
{ t: 'strong', c: title },
|
|
2384
|
+
{
|
|
2385
|
+
t: 'button',
|
|
2386
|
+
a: {
|
|
2387
|
+
type: 'button',
|
|
2388
|
+
class: 'bw-close',
|
|
2389
|
+
'aria-label': 'Close',
|
|
2390
|
+
onclick: function(e) {
|
|
2391
|
+
var toast = e.target.closest('.bw-toast');
|
|
2392
|
+
if (toast) {
|
|
2393
|
+
toast.classList.add('bw-toast-hiding');
|
|
2394
|
+
setTimeout(function() { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 300);
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
},
|
|
2398
|
+
c: '\u00D7'
|
|
2399
|
+
}
|
|
2400
|
+
]
|
|
2401
|
+
},
|
|
2402
|
+
content && {
|
|
2403
|
+
t: 'div',
|
|
2404
|
+
a: { class: 'bw-toast-body' },
|
|
2405
|
+
c: content
|
|
2406
|
+
}
|
|
2407
|
+
].filter(Boolean),
|
|
2408
|
+
o: {
|
|
2409
|
+
type: 'toast',
|
|
2410
|
+
mounted: function(el) {
|
|
2411
|
+
// Trigger show animation
|
|
2412
|
+
requestAnimationFrame(function() {
|
|
2413
|
+
el.classList.add('bw-toast-show');
|
|
2414
|
+
});
|
|
2415
|
+
// Auto-dismiss
|
|
2416
|
+
if (autoDismiss) {
|
|
2417
|
+
setTimeout(function() {
|
|
2418
|
+
el.classList.add('bw-toast-hiding');
|
|
2419
|
+
setTimeout(function() { if (el.parentNode) el.parentNode.removeChild(el); }, 300);
|
|
2420
|
+
}, delay);
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
// =========================================================================
|
|
2428
|
+
// Phase 3: Essential Modern
|
|
2429
|
+
// =========================================================================
|
|
2430
|
+
|
|
2431
|
+
/**
|
|
2432
|
+
* Create a dropdown menu triggered by a button
|
|
2433
|
+
*
|
|
2434
|
+
* @param {Object} [props] - Dropdown configuration
|
|
2435
|
+
* @param {string|Object} [props.trigger] - Button text or TACO for the trigger
|
|
2436
|
+
* @param {Array<Object>} [props.items=[]] - Menu items
|
|
2437
|
+
* @param {string} [props.items[].text] - Item display text
|
|
2438
|
+
* @param {string} [props.items[].href] - Item link URL
|
|
2439
|
+
* @param {Function} [props.items[].onclick] - Item click handler
|
|
2440
|
+
* @param {boolean} [props.items[].divider] - Render as a divider line
|
|
2441
|
+
* @param {boolean} [props.items[].disabled] - Whether the item is disabled
|
|
2442
|
+
* @param {string} [props.align="start"] - Menu alignment ("start" or "end")
|
|
2443
|
+
* @param {string} [props.variant="primary"] - Trigger button variant
|
|
2444
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
2445
|
+
* @returns {Object} TACO object representing a dropdown
|
|
2446
|
+
* @category Component Builders
|
|
2447
|
+
* @example
|
|
2448
|
+
* const dropdown = makeDropdown({
|
|
2449
|
+
* trigger: "Actions",
|
|
2450
|
+
* items: [
|
|
2451
|
+
* { text: "Edit", onclick: () => edit() },
|
|
2452
|
+
* { divider: true },
|
|
2453
|
+
* { text: "Delete", onclick: () => del() }
|
|
2454
|
+
* ]
|
|
2455
|
+
* });
|
|
2456
|
+
*/
|
|
2457
|
+
export function makeDropdown(props = {}) {
|
|
2458
|
+
const {
|
|
2459
|
+
trigger,
|
|
2460
|
+
items = [],
|
|
2461
|
+
align = 'start',
|
|
2462
|
+
variant = 'primary',
|
|
2463
|
+
className = ''
|
|
2464
|
+
} = props;
|
|
2465
|
+
|
|
2466
|
+
var triggerTaco;
|
|
2467
|
+
if (typeof trigger === 'string' || trigger === undefined) {
|
|
2468
|
+
triggerTaco = {
|
|
2469
|
+
t: 'button',
|
|
2470
|
+
a: {
|
|
2471
|
+
class: `bw-btn bw-btn-${variant} bw-dropdown-toggle`,
|
|
2472
|
+
type: 'button',
|
|
2473
|
+
onclick: function(e) {
|
|
2474
|
+
var dropdown = e.target.closest('.bw-dropdown');
|
|
2475
|
+
var menu = dropdown.querySelector('.bw-dropdown-menu');
|
|
2476
|
+
menu.classList.toggle('bw-dropdown-show');
|
|
2477
|
+
}
|
|
2478
|
+
},
|
|
2479
|
+
c: trigger || 'Dropdown'
|
|
2480
|
+
};
|
|
2481
|
+
} else {
|
|
2482
|
+
triggerTaco = trigger;
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
return {
|
|
2486
|
+
t: 'div',
|
|
2487
|
+
a: { class: `bw-dropdown ${className}`.trim() },
|
|
2488
|
+
c: [
|
|
2489
|
+
triggerTaco,
|
|
2490
|
+
{
|
|
2491
|
+
t: 'div',
|
|
2492
|
+
a: { class: `bw-dropdown-menu ${align === 'end' ? 'bw-dropdown-menu-end' : ''}`.trim() },
|
|
2493
|
+
c: items.map(function(item) {
|
|
2494
|
+
if (item.divider) {
|
|
2495
|
+
return { t: 'hr', a: { class: 'bw-dropdown-divider' } };
|
|
2496
|
+
}
|
|
2497
|
+
return {
|
|
2498
|
+
t: 'a',
|
|
2499
|
+
a: {
|
|
2500
|
+
class: `bw-dropdown-item ${item.disabled ? 'disabled' : ''}`.trim(),
|
|
2501
|
+
href: item.href || '#',
|
|
2502
|
+
onclick: item.disabled ? undefined : function(e) {
|
|
2503
|
+
if (!item.href) e.preventDefault();
|
|
2504
|
+
var dropdown = e.target.closest('.bw-dropdown');
|
|
2505
|
+
var menu = dropdown.querySelector('.bw-dropdown-menu');
|
|
2506
|
+
menu.classList.remove('bw-dropdown-show');
|
|
2507
|
+
if (item.onclick) item.onclick(e);
|
|
2508
|
+
}
|
|
2509
|
+
},
|
|
2510
|
+
c: item.text
|
|
2511
|
+
};
|
|
2512
|
+
})
|
|
2513
|
+
}
|
|
2514
|
+
],
|
|
2515
|
+
o: {
|
|
2516
|
+
type: 'dropdown',
|
|
2517
|
+
mounted: function(el) {
|
|
2518
|
+
// Click outside to close
|
|
2519
|
+
var outsideHandler = function(e) {
|
|
2520
|
+
if (!el.contains(e.target)) {
|
|
2521
|
+
var menu = el.querySelector('.bw-dropdown-menu');
|
|
2522
|
+
if (menu) menu.classList.remove('bw-dropdown-show');
|
|
2523
|
+
}
|
|
2524
|
+
};
|
|
2525
|
+
document.addEventListener('click', outsideHandler);
|
|
2526
|
+
el._bw_outsideHandler = outsideHandler;
|
|
2527
|
+
},
|
|
2528
|
+
unmount: function(el) {
|
|
2529
|
+
if (el._bw_outsideHandler) {
|
|
2530
|
+
document.removeEventListener('click', el._bw_outsideHandler);
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
};
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
/**
|
|
2538
|
+
* Create a toggle switch (styled checkbox)
|
|
2539
|
+
*
|
|
2540
|
+
* @param {Object} [props] - Switch configuration
|
|
2541
|
+
* @param {string} [props.label] - Switch label text
|
|
2542
|
+
* @param {boolean} [props.checked=false] - Whether the switch is on
|
|
2543
|
+
* @param {string} [props.id] - Element ID (links label to switch)
|
|
2544
|
+
* @param {string} [props.name] - Input name attribute
|
|
2545
|
+
* @param {boolean} [props.disabled=false] - Whether the switch is disabled
|
|
2546
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
2547
|
+
* @returns {Object} TACO object representing a toggle switch
|
|
2548
|
+
* @category Component Builders
|
|
2549
|
+
* @example
|
|
2550
|
+
* const toggle = makeSwitch({
|
|
2551
|
+
* label: "Dark mode",
|
|
2552
|
+
* checked: false,
|
|
2553
|
+
* onchange: (e) => toggleDark(e.target.checked)
|
|
2554
|
+
* });
|
|
2555
|
+
*/
|
|
2556
|
+
export function makeSwitch(props = {}) {
|
|
2557
|
+
const {
|
|
2558
|
+
label,
|
|
2559
|
+
checked = false,
|
|
2560
|
+
id,
|
|
2561
|
+
name,
|
|
2562
|
+
disabled = false,
|
|
2563
|
+
className = '',
|
|
2564
|
+
...eventHandlers
|
|
2565
|
+
} = props;
|
|
2566
|
+
|
|
2567
|
+
return {
|
|
2568
|
+
t: 'div',
|
|
2569
|
+
a: { class: `bw-form-check bw-form-switch ${className}`.trim() },
|
|
2570
|
+
c: [
|
|
2571
|
+
{
|
|
2572
|
+
t: 'input',
|
|
2573
|
+
a: {
|
|
2574
|
+
type: 'checkbox',
|
|
2575
|
+
class: 'bw-form-check-input bw-switch-input',
|
|
2576
|
+
role: 'switch',
|
|
2577
|
+
checked,
|
|
2578
|
+
id,
|
|
2579
|
+
name,
|
|
2580
|
+
disabled,
|
|
2581
|
+
...eventHandlers
|
|
2582
|
+
}
|
|
2583
|
+
},
|
|
2584
|
+
label && {
|
|
2585
|
+
t: 'label',
|
|
2586
|
+
a: { class: 'bw-form-check-label', for: id },
|
|
2587
|
+
c: label
|
|
2588
|
+
}
|
|
2589
|
+
].filter(Boolean)
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
/**
|
|
2594
|
+
* Create a skeleton loading placeholder
|
|
2595
|
+
*
|
|
2596
|
+
* @param {Object} [props] - Skeleton configuration
|
|
2597
|
+
* @param {string} [props.variant="text"] - Shape variant ("text", "circle", "rect")
|
|
2598
|
+
* @param {string} [props.width] - Custom width (e.g. "200px", "100%")
|
|
2599
|
+
* @param {string} [props.height] - Custom height (e.g. "20px")
|
|
2600
|
+
* @param {number} [props.count=1] - Number of skeleton lines (for text variant)
|
|
2601
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
2602
|
+
* @returns {Object} TACO object representing a skeleton placeholder
|
|
2603
|
+
* @category Component Builders
|
|
2604
|
+
* @example
|
|
2605
|
+
* const skeleton = makeSkeleton({ variant: "text", count: 3, width: "100%" });
|
|
2606
|
+
*/
|
|
2607
|
+
export function makeSkeleton(props = {}) {
|
|
2608
|
+
const {
|
|
2609
|
+
variant = 'text',
|
|
2610
|
+
width,
|
|
2611
|
+
height,
|
|
2612
|
+
count = 1,
|
|
2613
|
+
className = ''
|
|
2614
|
+
} = props;
|
|
2615
|
+
|
|
2616
|
+
if (variant === 'circle') {
|
|
2617
|
+
var circleSize = width || height || '3rem';
|
|
2618
|
+
return {
|
|
2619
|
+
t: 'div',
|
|
2620
|
+
a: {
|
|
2621
|
+
class: `bw-skeleton bw-skeleton-circle ${className}`.trim(),
|
|
2622
|
+
style: { width: circleSize, height: circleSize }
|
|
2623
|
+
}
|
|
2624
|
+
};
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
if (variant === 'rect') {
|
|
2628
|
+
return {
|
|
2629
|
+
t: 'div',
|
|
2630
|
+
a: {
|
|
2631
|
+
class: `bw-skeleton bw-skeleton-rect ${className}`.trim(),
|
|
2632
|
+
style: {
|
|
2633
|
+
width: width || '100%',
|
|
2634
|
+
height: height || '120px'
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
};
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
// Text variant — multiple lines
|
|
2641
|
+
if (count === 1) {
|
|
2642
|
+
return {
|
|
2643
|
+
t: 'div',
|
|
2644
|
+
a: {
|
|
2645
|
+
class: `bw-skeleton bw-skeleton-text ${className}`.trim(),
|
|
2646
|
+
style: {
|
|
2647
|
+
width: width || '100%',
|
|
2648
|
+
height: height || '1em'
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
};
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
var lines = [];
|
|
2655
|
+
for (var i = 0; i < count; i++) {
|
|
2656
|
+
lines.push({
|
|
2657
|
+
t: 'div',
|
|
2658
|
+
a: {
|
|
2659
|
+
class: 'bw-skeleton bw-skeleton-text',
|
|
2660
|
+
style: {
|
|
2661
|
+
width: i === count - 1 ? '75%' : (width || '100%'),
|
|
2662
|
+
height: height || '1em'
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
});
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
return {
|
|
2669
|
+
t: 'div',
|
|
2670
|
+
a: { class: `bw-skeleton-group ${className}`.trim() },
|
|
2671
|
+
c: lines
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
/**
|
|
2676
|
+
* Create a user avatar with image or initials fallback
|
|
2677
|
+
*
|
|
2678
|
+
* @param {Object} [props] - Avatar configuration
|
|
2679
|
+
* @param {string} [props.src] - Image source URL
|
|
2680
|
+
* @param {string} [props.alt] - Image alt text
|
|
2681
|
+
* @param {string} [props.initials] - Fallback initials (e.g. "JD")
|
|
2682
|
+
* @param {string} [props.size="md"] - Size ("sm", "md", "lg", "xl")
|
|
2683
|
+
* @param {string} [props.variant="primary"] - Background color variant for initials
|
|
2684
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
2685
|
+
* @returns {Object} TACO object representing an avatar
|
|
2686
|
+
* @category Component Builders
|
|
2687
|
+
* @example
|
|
2688
|
+
* const avatar = makeAvatar({ src: "/photo.jpg", alt: "Jane Doe", size: "lg" });
|
|
2689
|
+
* const avatarInitials = makeAvatar({ initials: "JD", variant: "success" });
|
|
2690
|
+
*/
|
|
2691
|
+
export function makeAvatar(props = {}) {
|
|
2692
|
+
const {
|
|
2693
|
+
src,
|
|
2694
|
+
alt = '',
|
|
2695
|
+
initials,
|
|
2696
|
+
size = 'md',
|
|
2697
|
+
variant = 'primary',
|
|
2698
|
+
className = ''
|
|
2699
|
+
} = props;
|
|
2700
|
+
|
|
2701
|
+
if (src) {
|
|
2702
|
+
return {
|
|
2703
|
+
t: 'img',
|
|
2704
|
+
a: {
|
|
2705
|
+
class: `bw-avatar bw-avatar-${size} ${className}`.trim(),
|
|
2706
|
+
src: src,
|
|
2707
|
+
alt: alt
|
|
2708
|
+
}
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
return {
|
|
2713
|
+
t: 'div',
|
|
2714
|
+
a: {
|
|
2715
|
+
class: `bw-avatar bw-avatar-${size} bw-avatar-${variant} ${className}`.trim()
|
|
2716
|
+
},
|
|
2717
|
+
c: initials || ''
|
|
2718
|
+
};
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
/**
|
|
2722
|
+
* Create a carousel/slideshow component with slide transitions
|
|
2723
|
+
*
|
|
2724
|
+
* Supports image slides, TACO content slides, captions, prev/next controls,
|
|
2725
|
+
* dot indicators, and optional auto-play. Uses CSS translateX transitions.
|
|
2726
|
+
*
|
|
2727
|
+
* @param {Object} [props] - Carousel configuration
|
|
2728
|
+
* @param {Array<Object>} [props.items=[]] - Slide items
|
|
2729
|
+
* @param {string|Object} props.items[].content - Slide content (TACO, string, or img element)
|
|
2730
|
+
* @param {string} [props.items[].caption] - Caption text shown at bottom of slide
|
|
2731
|
+
* @param {boolean} [props.showControls=true] - Show prev/next arrow buttons
|
|
2732
|
+
* @param {boolean} [props.showIndicators=true] - Show dot navigation
|
|
2733
|
+
* @param {boolean} [props.autoPlay=false] - Auto-advance slides
|
|
2734
|
+
* @param {number} [props.interval=5000] - Auto-advance interval in ms
|
|
2735
|
+
* @param {string} [props.height='300px'] - Carousel height
|
|
2736
|
+
* @param {number} [props.startIndex=0] - Initial slide index
|
|
2737
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
2738
|
+
* @returns {Object} TACO object representing a carousel
|
|
2739
|
+
* @category Component Builders
|
|
2740
|
+
* @example
|
|
2741
|
+
* const carousel = makeCarousel({
|
|
2742
|
+
* items: [
|
|
2743
|
+
* { content: { t: 'img', a: { src: 'photo.jpg' } }, caption: 'Photo 1' },
|
|
2744
|
+
* { content: { t: 'div', c: 'Text slide' } }
|
|
2745
|
+
* ],
|
|
2746
|
+
* autoPlay: true,
|
|
2747
|
+
* interval: 3000
|
|
2748
|
+
* });
|
|
2749
|
+
*/
|
|
2750
|
+
export function makeCarousel(props = {}) {
|
|
2751
|
+
const {
|
|
2752
|
+
items = [],
|
|
2753
|
+
showControls = true,
|
|
2754
|
+
showIndicators = true,
|
|
2755
|
+
autoPlay = false,
|
|
2756
|
+
interval = 5000,
|
|
2757
|
+
height = '300px',
|
|
2758
|
+
startIndex = 0,
|
|
2759
|
+
className = ''
|
|
2760
|
+
} = props;
|
|
2761
|
+
|
|
2762
|
+
// Shared navigation logic
|
|
2763
|
+
function goToSlide(carouselEl, index) {
|
|
2764
|
+
var total = carouselEl.querySelectorAll('.bw-carousel-slide').length;
|
|
2765
|
+
if (index < 0) index = total - 1;
|
|
2766
|
+
if (index >= total) index = 0;
|
|
2767
|
+
carouselEl.setAttribute('data-carousel-index', index);
|
|
2768
|
+
var track = carouselEl.querySelector('.bw-carousel-track');
|
|
2769
|
+
track.style.transform = 'translateX(-' + (index * 100) + '%)';
|
|
2770
|
+
// Update indicators
|
|
2771
|
+
var indicators = carouselEl.querySelectorAll('.bw-carousel-indicator');
|
|
2772
|
+
for (var i = 0; i < indicators.length; i++) {
|
|
2773
|
+
if (i === index) {
|
|
2774
|
+
indicators[i].classList.add('active');
|
|
2775
|
+
} else {
|
|
2776
|
+
indicators[i].classList.remove('active');
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
// Arrow SVGs (inline data URIs, same pattern as accordion chevrons)
|
|
2782
|
+
var prevArrow = "data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e";
|
|
2783
|
+
var nextArrow = "data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e";
|
|
2784
|
+
|
|
2785
|
+
var slides = items.map(function(item) {
|
|
2786
|
+
var slideContent = [
|
|
2787
|
+
item.content,
|
|
2788
|
+
item.caption && {
|
|
2789
|
+
t: 'div',
|
|
2790
|
+
a: { class: 'bw-carousel-caption' },
|
|
2791
|
+
c: item.caption
|
|
2792
|
+
}
|
|
2793
|
+
].filter(Boolean);
|
|
2794
|
+
|
|
2795
|
+
return {
|
|
2796
|
+
t: 'div',
|
|
2797
|
+
a: { class: 'bw-carousel-slide' },
|
|
2798
|
+
c: slideContent.length === 1 ? slideContent[0] : slideContent
|
|
2799
|
+
};
|
|
2800
|
+
});
|
|
2801
|
+
|
|
2802
|
+
var children = [
|
|
2803
|
+
// Track
|
|
2804
|
+
{
|
|
2805
|
+
t: 'div',
|
|
2806
|
+
a: {
|
|
2807
|
+
class: 'bw-carousel-track',
|
|
2808
|
+
style: 'transform: translateX(-' + (startIndex * 100) + '%)'
|
|
2809
|
+
},
|
|
2810
|
+
c: slides
|
|
2811
|
+
}
|
|
2812
|
+
];
|
|
2813
|
+
|
|
2814
|
+
// Prev/Next controls
|
|
2815
|
+
if (showControls && items.length > 1) {
|
|
2816
|
+
children.push({
|
|
2817
|
+
t: 'button',
|
|
2818
|
+
a: {
|
|
2819
|
+
class: 'bw-carousel-control bw-carousel-control-prev',
|
|
2820
|
+
type: 'button',
|
|
2821
|
+
'aria-label': 'Previous slide',
|
|
2822
|
+
onclick: function(e) {
|
|
2823
|
+
var carousel = e.target.closest('.bw-carousel');
|
|
2824
|
+
var idx = parseInt(carousel.getAttribute('data-carousel-index') || '0');
|
|
2825
|
+
goToSlide(carousel, idx - 1);
|
|
2826
|
+
}
|
|
2827
|
+
},
|
|
2828
|
+
c: { t: 'img', a: { src: prevArrow, alt: '', role: 'presentation' } }
|
|
2829
|
+
});
|
|
2830
|
+
children.push({
|
|
2831
|
+
t: 'button',
|
|
2832
|
+
a: {
|
|
2833
|
+
class: 'bw-carousel-control bw-carousel-control-next',
|
|
2834
|
+
type: 'button',
|
|
2835
|
+
'aria-label': 'Next slide',
|
|
2836
|
+
onclick: function(e) {
|
|
2837
|
+
var carousel = e.target.closest('.bw-carousel');
|
|
2838
|
+
var idx = parseInt(carousel.getAttribute('data-carousel-index') || '0');
|
|
2839
|
+
goToSlide(carousel, idx + 1);
|
|
2840
|
+
}
|
|
2841
|
+
},
|
|
2842
|
+
c: { t: 'img', a: { src: nextArrow, alt: '', role: 'presentation' } }
|
|
2843
|
+
});
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
// Indicators
|
|
2847
|
+
if (showIndicators && items.length > 1) {
|
|
2848
|
+
children.push({
|
|
2849
|
+
t: 'div',
|
|
2850
|
+
a: { class: 'bw-carousel-indicators' },
|
|
2851
|
+
c: items.map(function(_, i) {
|
|
2852
|
+
return {
|
|
2853
|
+
t: 'button',
|
|
2854
|
+
a: {
|
|
2855
|
+
class: 'bw-carousel-indicator' + (i === startIndex ? ' active' : ''),
|
|
2856
|
+
type: 'button',
|
|
2857
|
+
'aria-label': 'Go to slide ' + (i + 1),
|
|
2858
|
+
'data-slide-index': i,
|
|
2859
|
+
onclick: function(e) {
|
|
2860
|
+
var carousel = e.target.closest('.bw-carousel');
|
|
2861
|
+
var idx = parseInt(e.target.getAttribute('data-slide-index'));
|
|
2862
|
+
goToSlide(carousel, idx);
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
};
|
|
2866
|
+
})
|
|
2867
|
+
});
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
return {
|
|
2871
|
+
t: 'div',
|
|
2872
|
+
a: {
|
|
2873
|
+
class: ('bw-carousel ' + className).trim(),
|
|
2874
|
+
style: 'height: ' + height,
|
|
2875
|
+
'data-carousel-index': startIndex
|
|
2876
|
+
},
|
|
2877
|
+
c: children,
|
|
2878
|
+
o: {
|
|
2879
|
+
type: 'carousel',
|
|
2880
|
+
state: { activeIndex: startIndex, autoPlay: autoPlay, interval: interval },
|
|
2881
|
+
mounted: autoPlay ? function(el) {
|
|
2882
|
+
var intervalId = setInterval(function() {
|
|
2883
|
+
var idx = parseInt(el.getAttribute('data-carousel-index') || '0');
|
|
2884
|
+
goToSlide(el, idx + 1);
|
|
2885
|
+
}, interval);
|
|
2886
|
+
el._bw_carouselInterval = intervalId;
|
|
2887
|
+
} : undefined,
|
|
2888
|
+
unmount: autoPlay ? function(el) {
|
|
2889
|
+
if (el._bw_carouselInterval) {
|
|
2890
|
+
clearInterval(el._bw_carouselInterval);
|
|
2891
|
+
}
|
|
2892
|
+
} : undefined
|
|
2893
|
+
}
|
|
2894
|
+
};
|
|
2895
|
+
}
|
|
2896
|
+
|
|
1874
2897
|
export const componentHandles = {
|
|
1875
2898
|
card: CardHandle,
|
|
1876
2899
|
table: TableHandle,
|
|
1877
2900
|
navbar: NavbarHandle,
|
|
1878
|
-
tabs: TabsHandle
|
|
2901
|
+
tabs: TabsHandle,
|
|
2902
|
+
modal: ModalHandle
|
|
1879
2903
|
};
|