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.
@@ -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: 'bw-form-check' },
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}; color: var(--bw-primary);`
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
- t: 'pre',
1832
- a: {
1833
- style: 'margin: 0; background: #1e293b; border: none; border-radius: 6px; overflow-x: auto;'
1834
- },
1835
- c: {
1836
- t: 'code',
1837
- a: {
1838
- class: `language-${language}`,
1839
- 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;'
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: { style: 'color: #6c757d; margin-bottom: 1rem;' },
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
  };