bitwrench 2.0.15 → 2.0.17

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.
Files changed (53) hide show
  1. package/README.md +57 -21
  2. package/dist/bitwrench-bccl.cjs.js +3750 -0
  3. package/dist/bitwrench-bccl.cjs.min.js +40 -0
  4. package/dist/bitwrench-bccl.esm.js +3745 -0
  5. package/dist/bitwrench-bccl.esm.min.js +40 -0
  6. package/dist/bitwrench-bccl.umd.js +3756 -0
  7. package/dist/bitwrench-bccl.umd.min.js +40 -0
  8. package/dist/bitwrench-code-edit.cjs.js +57 -7
  9. package/dist/bitwrench-code-edit.cjs.min.js +9 -2
  10. package/dist/bitwrench-code-edit.es5.js +74 -11
  11. package/dist/bitwrench-code-edit.es5.min.js +9 -2
  12. package/dist/bitwrench-code-edit.esm.js +57 -7
  13. package/dist/bitwrench-code-edit.esm.min.js +9 -2
  14. package/dist/bitwrench-code-edit.umd.js +57 -7
  15. package/dist/bitwrench-code-edit.umd.min.js +9 -2
  16. package/dist/bitwrench-lean.cjs.js +905 -157
  17. package/dist/bitwrench-lean.cjs.min.js +7 -7
  18. package/dist/bitwrench-lean.es5.js +931 -157
  19. package/dist/bitwrench-lean.es5.min.js +5 -5
  20. package/dist/bitwrench-lean.esm.js +904 -157
  21. package/dist/bitwrench-lean.esm.min.js +7 -7
  22. package/dist/bitwrench-lean.umd.js +905 -157
  23. package/dist/bitwrench-lean.umd.min.js +7 -7
  24. package/dist/bitwrench.cjs.js +910 -158
  25. package/dist/bitwrench.cjs.min.js +8 -8
  26. package/dist/bitwrench.css +60 -17
  27. package/dist/bitwrench.es5.js +939 -158
  28. package/dist/bitwrench.es5.min.js +6 -6
  29. package/dist/bitwrench.esm.js +909 -158
  30. package/dist/bitwrench.esm.min.js +8 -8
  31. package/dist/bitwrench.min.css +1 -1
  32. package/dist/bitwrench.umd.js +910 -158
  33. package/dist/bitwrench.umd.min.js +8 -8
  34. package/dist/builds.json +168 -80
  35. package/dist/bwserve.cjs.js +660 -0
  36. package/dist/bwserve.esm.js +652 -0
  37. package/dist/sri.json +36 -28
  38. package/package.json +20 -3
  39. package/readme.html +62 -23
  40. package/src/bitwrench-bccl-entry.js +72 -0
  41. package/src/bitwrench-bccl.js +5 -1
  42. package/src/bitwrench-code-edit.js +56 -6
  43. package/src/bitwrench-color-utils.js +5 -6
  44. package/src/bitwrench-styles.js +20 -8
  45. package/src/bitwrench.js +876 -140
  46. package/src/bwserve/client.js +182 -0
  47. package/src/bwserve/index.js +363 -0
  48. package/src/bwserve/shell.js +106 -0
  49. package/src/cli/index.js +36 -15
  50. package/src/cli/layout-default.js +47 -32
  51. package/src/cli/serve.js +325 -0
  52. package/src/version.js +3 -3
  53. /package/bin/{bitwrench.js → bwcli.js} +0 -0
@@ -0,0 +1,3756 @@
1
+ /*! bitwrench-bccl v2.0.17 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
2
+ (function (global, factory) {
3
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
4
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
5
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.bwBCCL = {}));
6
+ })(this, (function (exports) { 'use strict';
7
+
8
+ /**
9
+ * Bitwrench v2 Components
10
+ *
11
+ * TACO-based UI component library providing Bootstrap-inspired components
12
+ * as pure JavaScript objects. Every make* function returns a TACO object
13
+ * ({t, a, c, o}) that can be rendered with bw.html() or bw.DOM().
14
+ *
15
+ * Components included: Card, Button, Container, Row, Col, Nav, Navbar,
16
+ * Tabs, Alert, Badge, Progress, ListGroup, Breadcrumb, Form controls,
17
+ * Stack, Spinner, Hero, FeatureGrid, CardV2, CTA, Section, CodeDemo.
18
+ *
19
+ * Handle classes (CardHandle, TableHandle, NavbarHandle, TabsHandle)
20
+ * provide imperative DOM manipulation for rendered components.
21
+ *
22
+ * @module bitwrench-bccl
23
+ * @license BSD-2-Clause
24
+ * @author M A Chatterjee <deftio [at] deftio [dot] com>
25
+ */
26
+
27
+ // =========================================================================
28
+ // Variant → Utility Class Mapping
29
+ //
30
+ // Components compose these shared utility classes instead of owning
31
+ // their own variant selectors. The CSS is generated once by
32
+ // generatePaletteUtilities() + generateInteractionRules().
33
+ // =========================================================================
34
+
35
+ /**
36
+ * Maps component type to a function that returns utility classes for a variant.
37
+ * Each function takes a variant name (e.g. 'primary') and returns a class string.
38
+ * @type {Object.<string, function(string): string>}
39
+ */
40
+ /**
41
+ * Convert a variant name to a single palette class.
42
+ * All BCCL components use this: variant='primary' → class includes 'bw_primary'.
43
+ * The CSS palette class (.bw-primary) sets bg/color/border; component-specific
44
+ * overrides in generatePaletteClasses() adjust per component type.
45
+ *
46
+ * @param {string} v - Variant name (e.g. 'primary', 'danger', 'outline_primary')
47
+ * @returns {string} CSS class string
48
+ */
49
+ function variantClass(v) {
50
+ if (!v) return '';
51
+ // Handle outline variants: 'outline_primary' or 'outline-primary'
52
+ if (v.indexOf('outline') === 0) {
53
+ var base = v.replace(/^outline[_-]/, '');
54
+ return 'bw_btn_outline bw_' + base;
55
+ }
56
+ return 'bw_' + v;
57
+ }
58
+
59
+ /**
60
+ * Create a card component with optional header, body, footer, and image support
61
+ *
62
+ * Supports images (top, bottom, left, right), shadow levels, subtitle,
63
+ * hover animation, and custom section class overrides. For horizontal
64
+ * image layouts (left/right), content is wrapped in a row grid.
65
+ *
66
+ * @param {Object} [props] - Card configuration
67
+ * @param {string} [props.title] - Card title displayed in the body
68
+ * @param {string} [props.subtitle] - Card subtitle (muted text below title)
69
+ * @param {string|Object|Array} [props.content] - Card body content (string, TACO, or array)
70
+ * @param {string|Object} [props.footer] - Card footer content
71
+ * @param {string|Object} [props.header] - Card header content
72
+ * @param {Object} [props.image] - Card image configuration
73
+ * @param {string} props.image.src - Image source URL
74
+ * @param {string} [props.image.alt] - Image alt text
75
+ * @param {string} [props.imagePosition="top"] - Image position ("top", "bottom", "left", "right")
76
+ * @param {string} [props.variant] - Color variant (e.g. "primary", "danger")
77
+ * @param {boolean} [props.bordered=true] - Show card border
78
+ * @param {string} [props.shadow] - Shadow level ("none", "sm", "md", "lg")
79
+ * @param {boolean} [props.hoverable=false] - Enable hover lift animation
80
+ * @param {string} [props.className] - Additional CSS classes
81
+ * @param {Object} [props.style] - Inline style object
82
+ * @param {string} [props.headerClass] - Additional header CSS classes
83
+ * @param {string} [props.bodyClass] - Additional body CSS classes
84
+ * @param {string} [props.footerClass] - Additional footer CSS classes
85
+ * @param {Object} [props.state] - Component state object
86
+ * @returns {Object} TACO object representing a card component
87
+ * @category Component Builders
88
+ * @example
89
+ * const card = makeCard({
90
+ * title: "Status",
91
+ * content: "All systems operational",
92
+ * variant: "success"
93
+ * });
94
+ * bw.DOM("#app", card);
95
+ */
96
+ function makeCard(props = {}) {
97
+ const {
98
+ title,
99
+ subtitle,
100
+ content,
101
+ footer,
102
+ header,
103
+ image,
104
+ imagePosition = 'top',
105
+ variant,
106
+ bordered = true,
107
+ shadow,
108
+ hoverable = false,
109
+ className = '',
110
+ style,
111
+ headerClass = '',
112
+ bodyClass = '',
113
+ footerClass = ''
114
+ } = props;
115
+
116
+ const shadowClasses = {
117
+ none: '',
118
+ sm: 'bw_shadow_sm',
119
+ md: 'bw_shadow',
120
+ lg: 'bw_shadow_lg'
121
+ };
122
+
123
+ const cardClasses = [
124
+ 'bw_card',
125
+ variantClass(variant),
126
+ shadow ? (shadowClasses[shadow] || '') : '',
127
+ !bordered ? 'bw_border_0' : '',
128
+ hoverable ? 'bw_card_hoverable' : '',
129
+ className
130
+ ].filter(Boolean).join(' ').trim();
131
+
132
+ const cardContent = [
133
+ header && {
134
+ t: 'div',
135
+ a: { class: `bw_card_header ${headerClass}`.trim() },
136
+ c: header
137
+ },
138
+ image && (imagePosition === 'top' || imagePosition === 'left') && {
139
+ t: 'img',
140
+ a: {
141
+ class: `bw_card_img_${imagePosition}`,
142
+ src: image.src,
143
+ alt: image.alt || ''
144
+ }
145
+ },
146
+ {
147
+ t: 'div',
148
+ a: { class: `bw_card_body ${bodyClass}`.trim() },
149
+ c: [
150
+ title && { t: 'h5', a: { class: 'bw_card_title' }, c: title },
151
+ subtitle && { t: 'h6', a: { class: 'bw_card_subtitle bw_mb_2 bw_text_muted' }, c: subtitle },
152
+ content && (Array.isArray(content) ? content : [content])
153
+ ].flat().filter(Boolean)
154
+ },
155
+ image && (imagePosition === 'bottom' || imagePosition === 'right') && {
156
+ t: 'img',
157
+ a: {
158
+ class: `bw_card_img_${imagePosition}`,
159
+ src: image.src,
160
+ alt: image.alt || ''
161
+ }
162
+ },
163
+ footer && {
164
+ t: 'div',
165
+ a: { class: `bw_card_footer ${footerClass}`.trim() },
166
+ c: footer
167
+ }
168
+ ].filter(Boolean);
169
+
170
+ // Handle horizontal layout for left/right images
171
+ if (image && (imagePosition === 'left' || imagePosition === 'right')) {
172
+ return {
173
+ t: 'div',
174
+ a: { class: cardClasses, style },
175
+ c: {
176
+ t: 'div',
177
+ a: { class: 'bw_row bw_g_0' },
178
+ c: cardContent
179
+ },
180
+ o: {
181
+ type: 'card',
182
+ state: props.state || {}
183
+ }
184
+ };
185
+ }
186
+
187
+ return {
188
+ t: 'div',
189
+ a: { class: cardClasses, style },
190
+ c: cardContent,
191
+ o: {
192
+ type: 'card',
193
+ state: props.state || {}
194
+ }
195
+ };
196
+ }
197
+
198
+ /**
199
+ * Create a button component
200
+ *
201
+ * @param {Object} [props] - Button configuration
202
+ * @param {string} [props.text] - Button label text
203
+ * @param {string} [props.variant="primary"] - Color variant (e.g. "primary", "secondary", "danger")
204
+ * @param {string} [props.size] - Size variant ("sm" or "lg")
205
+ * @param {boolean} [props.disabled=false] - Whether the button is disabled
206
+ * @param {Function} [props.onclick] - Click event handler
207
+ * @param {string} [props.type="button"] - HTML button type ("button", "submit", "reset")
208
+ * @param {string} [props.className] - Additional CSS classes
209
+ * @param {Object} [props.style] - Inline style object
210
+ * @returns {Object} TACO object representing a button element
211
+ * @category Component Builders
212
+ * @example
213
+ * const btn = makeButton({
214
+ * text: "Save",
215
+ * variant: "success",
216
+ * onclick: () => console.log("saved")
217
+ * });
218
+ * // String shorthand:
219
+ * const ok = makeButton("OK");
220
+ */
221
+ function makeButton(props = {}) {
222
+ if (typeof props === 'string') props = { text: props };
223
+ const {
224
+ text,
225
+ variant = 'primary',
226
+ size,
227
+ disabled = false,
228
+ onclick,
229
+ type = 'button',
230
+ className = '',
231
+ style
232
+ } = props;
233
+
234
+ return {
235
+ t: 'button',
236
+ a: {
237
+ type,
238
+ class: [
239
+ 'bw_btn',
240
+ variantClass(variant),
241
+ size && `bw_btn_${size}`,
242
+ className
243
+ ].filter(Boolean).join(' '),
244
+ disabled,
245
+ onclick,
246
+ style
247
+ },
248
+ c: text,
249
+ o: {
250
+ type: 'button'
251
+ }
252
+ };
253
+ }
254
+
255
+ /**
256
+ * Create a container component for centering and constraining content width
257
+ *
258
+ * @param {Object} [props] - Container configuration
259
+ * @param {boolean} [props.fluid=false] - Use full-width fluid container
260
+ * @param {Array|Object|string} [props.children] - Child content
261
+ * @param {string} [props.className] - Additional CSS classes
262
+ * @returns {Object} TACO object representing a container div
263
+ * @category Component Builders
264
+ * @example
265
+ * const container = makeContainer({
266
+ * fluid: true,
267
+ * children: [makeRow({ children: [...] })]
268
+ * });
269
+ */
270
+ function makeContainer(props = {}) {
271
+ const { fluid = false, children, className = '' } = props;
272
+
273
+ return {
274
+ t: 'div',
275
+ a: { class: `bw_container${fluid ? '-fluid' : ''} ${className}`.trim() },
276
+ c: children
277
+ };
278
+ }
279
+
280
+ /**
281
+ * Create a flexbox row for the grid system
282
+ *
283
+ * @param {Object} [props] - Row configuration
284
+ * @param {Array|Object|string} [props.children] - Child columns
285
+ * @param {string} [props.className] - Additional CSS classes
286
+ * @param {number} [props.gap] - Gap size (1-5) applied via bw_g_{gap} class
287
+ * @returns {Object} TACO object representing a grid row
288
+ * @category Component Builders
289
+ * @example
290
+ * const row = makeRow({
291
+ * gap: 4,
292
+ * children: [makeCol({ size: 6, content: "Left" }), makeCol({ size: 6, content: "Right" })]
293
+ * });
294
+ */
295
+ function makeRow(props = {}) {
296
+ const { children, className = '', gap } = props;
297
+
298
+ return {
299
+ t: 'div',
300
+ a: {
301
+ class: `bw_row ${gap ? `bw_g_${gap}` : ''} ${className}`.trim()
302
+ },
303
+ c: children
304
+ };
305
+ }
306
+
307
+ /**
308
+ * Create a grid column with responsive sizing
309
+ *
310
+ * Supports both fixed and responsive column sizes. Pass an object for
311
+ * responsive breakpoints (e.g. {xs: 12, md: 6, lg: 4}).
312
+ *
313
+ * @param {Object} [props] - Column configuration
314
+ * @param {number|Object} [props.size] - Column size (1-12) or responsive object {xs, sm, md, lg, xl}
315
+ * @param {number} [props.offset] - Column offset (1-12)
316
+ * @param {number} [props.push] - Column push (1-12)
317
+ * @param {number} [props.pull] - Column pull (1-12)
318
+ * @param {Array|Object|string} [props.content] - Column content (alias for children)
319
+ * @param {Array|Object|string} [props.children] - Column content
320
+ * @param {string} [props.className] - Additional CSS classes
321
+ * @returns {Object} TACO object representing a grid column
322
+ * @category Component Builders
323
+ * @example
324
+ * const col = makeCol({ size: { xs: 12, md: 6 }, content: "Responsive column" });
325
+ */
326
+ function makeCol(props = {}) {
327
+ const { size, offset, push, pull, content, children, className = '' } = props;
328
+
329
+ const classes = [];
330
+
331
+ if (typeof size === 'object') {
332
+ // Responsive sizes
333
+ Object.entries(size).forEach(([breakpoint, value]) => {
334
+ if (breakpoint === 'xs') {
335
+ classes.push(`bw_col_${value}`);
336
+ } else {
337
+ classes.push(`bw_col_${breakpoint}-${value}`);
338
+ }
339
+ });
340
+ } else if (size) {
341
+ classes.push(`bw_col_${size}`);
342
+ } else {
343
+ classes.push('bw_col');
344
+ }
345
+
346
+ if (offset) classes.push(`bw_offset_${offset}`);
347
+ if (push) classes.push(`bw_push_${push}`);
348
+ if (pull) classes.push(`bw_pull_${pull}`);
349
+
350
+ return {
351
+ t: 'div',
352
+ a: { class: `${classes.join(' ')} ${className}`.trim() },
353
+ c: content || children
354
+ };
355
+ }
356
+
357
+ /**
358
+ * Create a navigation component with tabs or pills styling
359
+ *
360
+ * @param {Object} [props] - Nav configuration
361
+ * @param {Array<Object>} [props.items=[]] - Navigation items
362
+ * @param {string} props.items[].text - Item display text
363
+ * @param {string} [props.items[].href="#"] - Item link URL
364
+ * @param {boolean} [props.items[].active] - Whether this item is active
365
+ * @param {boolean} [props.items[].disabled] - Whether this item is disabled
366
+ * @param {boolean} [props.pills=false] - Use pill styling instead of tabs
367
+ * @param {boolean} [props.vertical=false] - Stack items vertically
368
+ * @param {string} [props.className] - Additional CSS classes
369
+ * @returns {Object} TACO object representing a nav element
370
+ * @category Component Builders
371
+ * @example
372
+ * const nav = makeNav({
373
+ * pills: true,
374
+ * items: [
375
+ * { text: "Home", href: "/", active: true },
376
+ * { text: "About", href: "/about" }
377
+ * ]
378
+ * });
379
+ */
380
+ function makeNav(props = {}) {
381
+ const {
382
+ items = [],
383
+ pills = false,
384
+ vertical = false,
385
+ className = ''
386
+ } = props;
387
+
388
+ return {
389
+ t: 'ul',
390
+ a: {
391
+ class: `bw_nav ${pills ? 'bw_nav_pills' : 'bw_nav_tabs'} ${vertical ? 'bw_nav_vertical' : ''} ${className}`.trim()
392
+ },
393
+ c: items.map(item => ({
394
+ t: 'li',
395
+ a: { class: 'bw_nav_item' },
396
+ c: {
397
+ t: 'a',
398
+ a: {
399
+ href: item.href || '#',
400
+ class: `bw_nav_link ${item.active ? 'active' : ''} ${item.disabled ? 'disabled' : ''}`.trim()
401
+ },
402
+ c: item.text
403
+ }
404
+ }))
405
+ };
406
+ }
407
+
408
+ /**
409
+ * Create a navbar component with brand and navigation links
410
+ *
411
+ * @param {Object} [props] - Navbar configuration
412
+ * @param {string} [props.brand] - Brand name or logo text
413
+ * @param {string} [props.brandHref="#"] - Brand link URL
414
+ * @param {Array<Object>} [props.items=[]] - Navigation items
415
+ * @param {string} props.items[].text - Item display text
416
+ * @param {string} [props.items[].href="#"] - Item link URL
417
+ * @param {boolean} [props.items[].active] - Whether this item is active
418
+ * @param {boolean} [props.dark=true] - Use dark theme styling
419
+ * @param {string} [props.className] - Additional CSS classes
420
+ * @returns {Object} TACO object representing a navbar element
421
+ * @category Component Builders
422
+ * @example
423
+ * const navbar = makeNavbar({
424
+ * brand: "MyApp",
425
+ * dark: true,
426
+ * items: [
427
+ * { text: "Home", href: "/", active: true },
428
+ * { text: "Docs", href: "/docs" }
429
+ * ]
430
+ * });
431
+ */
432
+ function makeNavbar(props = {}) {
433
+ const {
434
+ brand,
435
+ brandHref = '#',
436
+ items = [],
437
+ dark = true,
438
+ className = ''
439
+ } = props;
440
+
441
+ return {
442
+ t: 'nav',
443
+ a: {
444
+ class: `bw_navbar ${dark ? 'bw_navbar_dark' : 'bw_navbar_light'} ${className}`.trim()
445
+ },
446
+ c: {
447
+ t: 'div',
448
+ a: { class: 'bw_container' },
449
+ c: [
450
+ brand && {
451
+ t: 'a',
452
+ a: { href: brandHref, class: 'bw_navbar_brand' },
453
+ c: brand
454
+ },
455
+ items.length > 0 && {
456
+ t: 'div',
457
+ a: { class: 'bw_navbar_nav' },
458
+ c: items.map(item => ({
459
+ t: 'a',
460
+ a: {
461
+ href: item.href || '#',
462
+ class: `bw_nav_link ${item.active ? 'active' : ''}`
463
+ },
464
+ c: item.text
465
+ }))
466
+ }
467
+ ].filter(Boolean)
468
+ },
469
+ o: {
470
+ type: 'navbar',
471
+ state: { activeItem: items.findIndex(i => i.active) }
472
+ }
473
+ };
474
+ }
475
+
476
+ /**
477
+ * Create a tabbed interface with accessible tab navigation
478
+ *
479
+ * Each tab is rendered as a button with ARIA attributes for accessibility.
480
+ * Clicking a tab shows its content pane and hides others. The active tab
481
+ * can be set via activeIndex or by setting active:true on a tab item.
482
+ *
483
+ * @param {Object} [props] - Tabs configuration
484
+ * @param {Array<Object>} [props.tabs=[]] - Tab definitions
485
+ * @param {string} props.tabs[].label - Tab button label
486
+ * @param {string|Object|Array} props.tabs[].content - Tab pane content
487
+ * @param {boolean} [props.tabs[].active] - Whether this tab is initially active
488
+ * @param {number} [props.activeIndex=0] - Default active tab index (overridden by tab.active)
489
+ * @returns {Object} TACO object representing a tabbed interface
490
+ * @category Component Builders
491
+ * @example
492
+ * const tabs = makeTabs({
493
+ * tabs: [
494
+ * { label: "Overview", content: "Tab 1 content", active: true },
495
+ * { label: "Details", content: "Tab 2 content" }
496
+ * ]
497
+ * });
498
+ * bw.DOM("#app", tabs);
499
+ */
500
+ function makeTabs(props = {}) {
501
+ const { tabs = [], activeIndex = 0 } = props;
502
+
503
+ // Find the active tab index based on the active property or use activeIndex
504
+ let actualActiveIndex = activeIndex;
505
+ tabs.forEach((tab, index) => {
506
+ if (tab.active) {
507
+ actualActiveIndex = index;
508
+ }
509
+ });
510
+
511
+ return {
512
+ t: 'div',
513
+ a: { class: 'bw_tabs' },
514
+ c: [
515
+ {
516
+ t: 'ul',
517
+ a: { class: 'bw_nav bw_nav_tabs', role: 'tablist' },
518
+ c: tabs.map((tab, index) => ({
519
+ t: 'li',
520
+ a: { class: 'bw_nav_item', role: 'presentation' },
521
+ c: {
522
+ t: 'button',
523
+ a: {
524
+ class: `bw_nav_link ${index === actualActiveIndex ? 'active' : ''}`,
525
+ type: 'button',
526
+ role: 'tab',
527
+ tabindex: index === actualActiveIndex ? '0' : '-1',
528
+ 'aria-selected': index === actualActiveIndex ? 'true' : 'false',
529
+ 'data-tab-index': index,
530
+ onclick: (e) => {
531
+ const tabsContainer = e.target.closest('.bw_tabs');
532
+ const allTabs = tabsContainer.querySelectorAll('.bw_nav_link');
533
+ const allPanes = tabsContainer.querySelectorAll('.bw_tab_pane');
534
+
535
+ allTabs.forEach(t => {
536
+ t.classList.remove('active');
537
+ t.setAttribute('aria-selected', 'false');
538
+ t.setAttribute('tabindex', '-1');
539
+ });
540
+ allPanes.forEach(p => p.classList.remove('active'));
541
+
542
+ e.target.classList.add('active');
543
+ e.target.setAttribute('aria-selected', 'true');
544
+ e.target.setAttribute('tabindex', '0');
545
+ const targetIndex = parseInt(e.target.getAttribute('data-tab-index'));
546
+ allPanes[targetIndex].classList.add('active');
547
+ }
548
+ },
549
+ c: tab.label
550
+ }
551
+ }))
552
+ },
553
+ {
554
+ t: 'div',
555
+ a: { class: 'bw_tab_content' },
556
+ c: tabs.map((tab, index) => ({
557
+ t: 'div',
558
+ a: {
559
+ class: `bw_tab_pane ${index === actualActiveIndex ? 'active' : ''}`,
560
+ role: 'tabpanel'
561
+ },
562
+ c: tab.content
563
+ }))
564
+ }
565
+ ],
566
+ o: {
567
+ type: 'tabs',
568
+ state: { activeIndex: actualActiveIndex },
569
+ mounted: function(el) {
570
+ var tablist = el.querySelector('[role="tablist"]');
571
+ if (!tablist) return;
572
+ tablist.addEventListener('keydown', function(e) {
573
+ var tabButtons = tablist.querySelectorAll('[role="tab"]');
574
+ var currentIndex = -1;
575
+ for (var i = 0; i < tabButtons.length; i++) {
576
+ if (tabButtons[i] === e.target) { currentIndex = i; break; }
577
+ }
578
+ if (currentIndex === -1) return;
579
+
580
+ var newIndex = -1;
581
+ if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
582
+ e.preventDefault();
583
+ newIndex = currentIndex > 0 ? currentIndex - 1 : tabButtons.length - 1;
584
+ } else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
585
+ e.preventDefault();
586
+ newIndex = currentIndex < tabButtons.length - 1 ? currentIndex + 1 : 0;
587
+ } else if (e.key === 'Home') {
588
+ e.preventDefault();
589
+ newIndex = 0;
590
+ } else if (e.key === 'End') {
591
+ e.preventDefault();
592
+ newIndex = tabButtons.length - 1;
593
+ }
594
+
595
+ if (newIndex >= 0) {
596
+ tabButtons[newIndex].focus();
597
+ tabButtons[newIndex].click();
598
+ }
599
+ });
600
+ }
601
+ }
602
+ };
603
+ }
604
+
605
+ /**
606
+ * Create an alert/notification component
607
+ *
608
+ * @param {Object} [props] - Alert configuration
609
+ * @param {string|Object|Array} [props.content] - Alert message content
610
+ * @param {string} [props.variant="info"] - Color variant ("primary", "secondary", "success", "danger", "warning", "info", "light", "dark")
611
+ * @param {boolean} [props.dismissible=false] - Show a close button to dismiss the alert
612
+ * @param {string} [props.className] - Additional CSS classes
613
+ * @returns {Object} TACO object representing an alert element
614
+ * @category Component Builders
615
+ * @example
616
+ * const alert = makeAlert({
617
+ * content: "Operation completed successfully!",
618
+ * variant: "success",
619
+ * dismissible: true
620
+ * });
621
+ * // String shorthand:
622
+ * const msg = makeAlert("Something happened");
623
+ */
624
+ function makeAlert(props = {}) {
625
+ if (typeof props === 'string') props = { content: props };
626
+ const {
627
+ content,
628
+ variant = 'info',
629
+ dismissible = false,
630
+ className = ''
631
+ } = props;
632
+
633
+ return {
634
+ t: 'div',
635
+ a: {
636
+ class: `bw_alert ${variantClass(variant)} ${dismissible ? 'bw_alert_dismissible' : ''} ${className}`.trim(),
637
+ role: 'alert'
638
+ },
639
+ c: [
640
+ content,
641
+ dismissible && {
642
+ t: 'button',
643
+ a: {
644
+ type: 'button',
645
+ class: 'bw_close',
646
+ 'aria-label': 'Close',
647
+ onclick: function(e) {
648
+ var alert = e.target.closest('.bw_alert');
649
+ if (alert) { alert.remove(); }
650
+ }
651
+ },
652
+ c: '×'
653
+ }
654
+ ].filter(Boolean)
655
+ };
656
+ }
657
+
658
+ /**
659
+ * Create an inline badge/label component
660
+ *
661
+ * @param {Object} [props] - Badge configuration
662
+ * @param {string} [props.text] - Badge display text
663
+ * @param {string} [props.variant="primary"] - Color variant
664
+ * @param {string} [props.size] - Size variant: 'sm' or 'lg' (default is medium)
665
+ * @param {boolean} [props.pill=false] - Use pill (rounded) shape
666
+ * @param {string} [props.className] - Additional CSS classes
667
+ * @returns {Object} TACO object representing a badge span
668
+ * @category Component Builders
669
+ * @example
670
+ * const badge = makeBadge({ text: "New", variant: "danger", pill: true });
671
+ * const small = makeBadge({ text: "3", variant: "info", size: "sm" });
672
+ * // String shorthand:
673
+ * const tag = makeBadge("New");
674
+ */
675
+ function makeBadge(props = {}) {
676
+ if (typeof props === 'string') props = { text: props };
677
+ const {
678
+ text,
679
+ variant = 'primary',
680
+ size,
681
+ pill = false,
682
+ className = ''
683
+ } = props;
684
+
685
+ const sizeClass = size === 'sm' ? ' bw_badge_sm' : size === 'lg' ? ' bw_badge_lg' : '';
686
+
687
+ return {
688
+ t: 'span',
689
+ a: {
690
+ class: `bw_badge ${variantClass(variant)}${sizeClass} ${pill ? 'bw_badge_pill' : ''} ${className}`.trim()
691
+ },
692
+ c: text
693
+ };
694
+ }
695
+
696
+ /**
697
+ * Create a progress bar component with ARIA accessibility
698
+ *
699
+ * @param {Object} [props] - Progress bar configuration
700
+ * @param {number} [props.value=0] - Current progress value
701
+ * @param {number} [props.max=100] - Maximum value
702
+ * @param {string} [props.variant="primary"] - Color variant
703
+ * @param {boolean} [props.striped=false] - Use striped pattern
704
+ * @param {boolean} [props.animated=false] - Animate the stripes
705
+ * @param {string} [props.label] - Custom label text (defaults to percentage)
706
+ * @param {number} [props.height] - Custom height in pixels
707
+ * @returns {Object} TACO object representing a progress bar
708
+ * @category Component Builders
709
+ * @example
710
+ * const progress = makeProgress({
711
+ * value: 75,
712
+ * variant: "success",
713
+ * striped: true,
714
+ * animated: true
715
+ * });
716
+ */
717
+ function makeProgress(props = {}) {
718
+ const {
719
+ value = 0,
720
+ max = 100,
721
+ variant = 'primary',
722
+ striped = false,
723
+ animated = false,
724
+ label,
725
+ height
726
+ } = props;
727
+
728
+ const percentage = Math.round((value / max) * 100);
729
+
730
+ return {
731
+ t: 'div',
732
+ a: {
733
+ class: 'bw_progress',
734
+ style: height ? { height: `${height}px` } : undefined
735
+ },
736
+ c: {
737
+ t: 'div',
738
+ a: {
739
+ class: [
740
+ 'bw_progress_bar',
741
+ variantClass(variant),
742
+ striped && 'bw_progress_bar_striped',
743
+ animated && 'bw_progress_bar_animated'
744
+ ].filter(Boolean).join(' '),
745
+ role: 'progressbar',
746
+ style: { width: `${percentage}%` },
747
+ 'aria-valuenow': value,
748
+ 'aria-valuemin': 0,
749
+ 'aria-valuemax': max
750
+ },
751
+ c: label || `${percentage}%`
752
+ }
753
+ };
754
+ }
755
+
756
+ /**
757
+ * Create a list group component for displaying lists of items
758
+ *
759
+ * Items can be simple strings or objects with text, active, disabled,
760
+ * href, and onclick properties. When interactive is true or items have
761
+ * href/onclick, items render as anchor tags.
762
+ *
763
+ * @param {Object} [props] - List group configuration
764
+ * @param {Array<string|Object>} [props.items=[]] - List items (strings or objects)
765
+ * @param {string} props.items[].text - Item display text
766
+ * @param {boolean} [props.items[].active] - Whether this item is active
767
+ * @param {boolean} [props.items[].disabled] - Whether this item is disabled
768
+ * @param {string} [props.items[].href] - Item link URL
769
+ * @param {Function} [props.items[].onclick] - Item click handler
770
+ * @param {boolean} [props.flush=false] - Remove borders for use inside cards
771
+ * @param {boolean} [props.interactive=false] - Make all items interactive (anchor tags)
772
+ * @returns {Object} TACO object representing a list group
773
+ * @category Component Builders
774
+ * @example
775
+ * const list = makeListGroup({
776
+ * interactive: true,
777
+ * items: [
778
+ * { text: "Active item", active: true },
779
+ * { text: "Regular item" },
780
+ * { text: "Disabled item", disabled: true }
781
+ * ]
782
+ * });
783
+ */
784
+ function makeListGroup(props = {}) {
785
+ const { items = [], flush = false, interactive = false } = props;
786
+
787
+ return {
788
+ t: 'div',
789
+ a: { class: `bw_list_group ${flush ? 'bw_list_group_flush' : ''}`.trim() },
790
+ c: items.map(item => {
791
+ const isObject = typeof item === 'object';
792
+ const text = isObject ? item.text : item;
793
+ const active = isObject ? item.active : false;
794
+ const disabled = isObject ? item.disabled : false;
795
+ const href = isObject ? item.href : null;
796
+ const onclick = isObject ? item.onclick : null;
797
+
798
+ // For interactive items or items with href/onclick, use anchor tag
799
+ if (interactive || href || onclick) {
800
+ return {
801
+ t: 'a',
802
+ a: {
803
+ class: [
804
+ 'bw_list_group_item',
805
+ active && 'active',
806
+ disabled && 'disabled'
807
+ ].filter(Boolean).join(' '),
808
+ href: href || '#',
809
+ onclick: onclick || ((e) => {
810
+ if (!href) e.preventDefault();
811
+ }),
812
+ style: disabled ? 'pointer-events: none; opacity: 0.65;' : ''
813
+ },
814
+ c: text
815
+ };
816
+ }
817
+
818
+ // For non-interactive items, use div
819
+ return {
820
+ t: 'div',
821
+ a: {
822
+ class: [
823
+ 'bw_list_group_item',
824
+ active && 'active',
825
+ disabled && 'disabled'
826
+ ].filter(Boolean).join(' ')
827
+ },
828
+ c: text
829
+ };
830
+ })
831
+ };
832
+ }
833
+
834
+ /**
835
+ * Create a breadcrumb navigation component
836
+ *
837
+ * The last item with active:true is rendered as plain text (no link).
838
+ * All other items render as anchor tags.
839
+ *
840
+ * @param {Object} [props] - Breadcrumb configuration
841
+ * @param {Array<Object>} [props.items=[]] - Breadcrumb items
842
+ * @param {string} props.items[].text - Item display text
843
+ * @param {string} [props.items[].href="#"] - Item link URL
844
+ * @param {boolean} [props.items[].active] - Whether this is the current page
845
+ * @returns {Object} TACO object representing a breadcrumb nav
846
+ * @category Component Builders
847
+ * @example
848
+ * const crumbs = makeBreadcrumb({
849
+ * items: [
850
+ * { text: "Home", href: "/" },
851
+ * { text: "Products", href: "/products" },
852
+ * { text: "Widget", active: true }
853
+ * ]
854
+ * });
855
+ */
856
+ function makeBreadcrumb(props = {}) {
857
+ const { items = [] } = props;
858
+
859
+ return {
860
+ t: 'nav',
861
+ a: { 'aria-label': 'breadcrumb' },
862
+ c: {
863
+ t: 'ol',
864
+ a: { class: 'bw_breadcrumb' },
865
+ c: items.map((item, index) => ({
866
+ t: 'li',
867
+ a: {
868
+ class: `bw_breadcrumb_item ${item.active ? 'active' : ''}`,
869
+ 'aria-current': item.active ? 'page' : undefined
870
+ },
871
+ c: item.active ? item.text : {
872
+ t: 'a',
873
+ a: { href: item.href || '#' },
874
+ c: item.text
875
+ }
876
+ }))
877
+ }
878
+ };
879
+ }
880
+
881
+ /**
882
+ * Create a form wrapper with default submit prevention
883
+ *
884
+ * @param {Object} [props] - Form configuration
885
+ * @param {Array|Object|string} [props.children] - Form contents (form groups, inputs, buttons)
886
+ * @param {Function} [props.onsubmit] - Submit handler (defaults to preventDefault)
887
+ * @param {string} [props.className] - Additional CSS classes
888
+ * @returns {Object} TACO object representing a form element
889
+ * @category Component Builders
890
+ * @example
891
+ * const form = makeForm({
892
+ * onsubmit: (e) => { e.preventDefault(); handleSubmit(); },
893
+ * children: [
894
+ * makeFormGroup({ label: "Name", input: makeInput({ placeholder: "Enter name" }) }),
895
+ * makeButton({ text: "Submit", type: "submit" })
896
+ * ]
897
+ * });
898
+ */
899
+ function makeForm(props = {}) {
900
+ const { children, onsubmit, className = '' } = props;
901
+
902
+ return {
903
+ t: 'form',
904
+ a: {
905
+ class: className,
906
+ onsubmit: onsubmit || ((e) => e.preventDefault())
907
+ },
908
+ c: children
909
+ };
910
+ }
911
+
912
+ /**
913
+ * Create a form group with label, input, optional help text and validation feedback
914
+ *
915
+ * @param {Object} [props] - Form group configuration
916
+ * @param {string} [props.label] - Label text
917
+ * @param {Object} [props.input] - Input TACO object (from makeInput, makeSelect, etc.)
918
+ * @param {string} [props.help] - Help text displayed below the input
919
+ * @param {string} [props.id] - Input ID (links label to input via for/id)
920
+ * @param {string} [props.validation] - Validation state ("valid" or "invalid")
921
+ * @param {string} [props.feedback] - Validation feedback text shown below input
922
+ * @param {boolean} [props.required=false] - Show required indicator (*) on label
923
+ * @returns {Object} TACO object representing a form group
924
+ * @category Component Builders
925
+ * @example
926
+ * const group = makeFormGroup({
927
+ * label: "Email",
928
+ * id: "email",
929
+ * input: makeInput({ type: "email", id: "email", placeholder: "you@example.com" }),
930
+ * validation: "invalid",
931
+ * feedback: "Please enter a valid email address."
932
+ * });
933
+ */
934
+ function makeFormGroup(props = {}) {
935
+ var { label, input, help, id, validation, feedback, required } = props;
936
+
937
+ // Shallow-clone input TACO to add validation class without mutating original
938
+ var styledInput = input;
939
+ if (validation && input && input.a) {
940
+ styledInput = { t: input.t, a: Object.assign({}, input.a), c: input.c, o: input.o };
941
+ var validClass = validation === 'valid' ? 'bw_is_valid' : validation === 'invalid' ? 'bw_is_invalid' : '';
942
+ if (validClass) {
943
+ styledInput.a.class = ((styledInput.a.class || '') + ' ' + validClass).trim();
944
+ }
945
+ }
946
+
947
+ return {
948
+ t: 'div',
949
+ a: { class: 'bw_form_group' },
950
+ c: [
951
+ label && {
952
+ t: 'label',
953
+ a: { for: id, class: 'bw_form_label' },
954
+ c: required ? [label, { t: 'span', a: { class: 'bw_text_danger bw_ms_1' }, c: '*' }] : label
955
+ },
956
+ styledInput,
957
+ feedback && validation && {
958
+ t: 'div',
959
+ a: { class: validation === 'valid' ? 'bw_valid_feedback' : 'bw_invalid_feedback' },
960
+ c: feedback
961
+ },
962
+ help && {
963
+ t: 'small',
964
+ a: { class: 'bw_form_text bw_text_muted' },
965
+ c: help
966
+ }
967
+ ].filter(Boolean)
968
+ };
969
+ }
970
+
971
+ /**
972
+ * Create an input element with form control styling
973
+ *
974
+ * Additional event handlers (oninput, onchange, etc.) can be passed
975
+ * as extra properties and are spread onto the element attributes.
976
+ *
977
+ * @param {Object} [props] - Input configuration
978
+ * @param {string} [props.type="text"] - Input type ("text", "email", "password", "number", etc.)
979
+ * @param {string} [props.placeholder] - Placeholder text
980
+ * @param {string} [props.value] - Input value
981
+ * @param {string} [props.id] - Element ID
982
+ * @param {string} [props.name] - Input name attribute
983
+ * @param {boolean} [props.disabled=false] - Whether the input is disabled
984
+ * @param {boolean} [props.readonly=false] - Whether the input is read-only
985
+ * @param {boolean} [props.required=false] - Whether the input is required
986
+ * @param {string} [props.className] - Additional CSS classes
987
+ * @param {Object} [props.style] - Inline style object
988
+ * @returns {Object} TACO object representing an input element
989
+ * @category Component Builders
990
+ * @example
991
+ * const input = makeInput({
992
+ * type: "email",
993
+ * placeholder: "you@example.com",
994
+ * required: true,
995
+ * oninput: (e) => validate(e.target.value)
996
+ * });
997
+ */
998
+ function makeInput(props = {}) {
999
+ const {
1000
+ type = 'text',
1001
+ placeholder,
1002
+ value,
1003
+ id,
1004
+ name,
1005
+ disabled = false,
1006
+ readonly = false,
1007
+ required = false,
1008
+ className = '',
1009
+ style,
1010
+ ...eventHandlers
1011
+ } = props;
1012
+
1013
+ return {
1014
+ t: 'input',
1015
+ a: {
1016
+ type,
1017
+ class: `bw_form_control ${className}`.trim(),
1018
+ placeholder,
1019
+ value,
1020
+ id,
1021
+ name,
1022
+ style,
1023
+ disabled,
1024
+ readonly,
1025
+ required,
1026
+ ...eventHandlers
1027
+ }
1028
+ };
1029
+ }
1030
+
1031
+ /**
1032
+ * Create a textarea element with form control styling
1033
+ *
1034
+ * @param {Object} [props] - Textarea configuration
1035
+ * @param {string} [props.placeholder] - Placeholder text
1036
+ * @param {string} [props.value] - Textarea content
1037
+ * @param {number} [props.rows=3] - Number of visible text rows
1038
+ * @param {string} [props.id] - Element ID
1039
+ * @param {string} [props.name] - Textarea name attribute
1040
+ * @param {boolean} [props.disabled=false] - Whether the textarea is disabled
1041
+ * @param {boolean} [props.readonly=false] - Whether the textarea is read-only
1042
+ * @param {boolean} [props.required=false] - Whether the textarea is required
1043
+ * @param {string} [props.className] - Additional CSS classes
1044
+ * @returns {Object} TACO object representing a textarea element
1045
+ * @category Component Builders
1046
+ * @example
1047
+ * const textarea = makeTextarea({
1048
+ * rows: 5,
1049
+ * placeholder: "Enter your message...",
1050
+ * required: true
1051
+ * });
1052
+ */
1053
+ function makeTextarea(props = {}) {
1054
+ const {
1055
+ placeholder,
1056
+ value,
1057
+ rows = 3,
1058
+ id,
1059
+ name,
1060
+ disabled = false,
1061
+ readonly = false,
1062
+ required = false,
1063
+ className = '',
1064
+ ...eventHandlers
1065
+ } = props;
1066
+
1067
+ return {
1068
+ t: 'textarea',
1069
+ a: {
1070
+ class: `bw_form_control ${className}`.trim(),
1071
+ placeholder,
1072
+ rows,
1073
+ id,
1074
+ name,
1075
+ disabled,
1076
+ readonly,
1077
+ required,
1078
+ ...eventHandlers
1079
+ },
1080
+ c: value
1081
+ };
1082
+ }
1083
+
1084
+ /**
1085
+ * Create a select dropdown with options
1086
+ *
1087
+ * @param {Object} [props] - Select configuration
1088
+ * @param {Array<Object>} [props.options=[]] - Dropdown options
1089
+ * @param {string} props.options[].value - Option value
1090
+ * @param {string} [props.options[].text] - Option display text (defaults to value)
1091
+ * @param {string} [props.value] - Currently selected value
1092
+ * @param {string} [props.id] - Element ID
1093
+ * @param {string} [props.name] - Select name attribute
1094
+ * @param {boolean} [props.disabled=false] - Whether the select is disabled
1095
+ * @param {boolean} [props.required=false] - Whether the select is required
1096
+ * @param {string} [props.className] - Additional CSS classes
1097
+ * @returns {Object} TACO object representing a select element
1098
+ * @category Component Builders
1099
+ * @example
1100
+ * const select = makeSelect({
1101
+ * value: "b",
1102
+ * options: [
1103
+ * { value: "a", text: "Option A" },
1104
+ * { value: "b", text: "Option B" },
1105
+ * { value: "c", text: "Option C" }
1106
+ * ]
1107
+ * });
1108
+ */
1109
+ function makeSelect(props = {}) {
1110
+ const {
1111
+ options = [],
1112
+ value,
1113
+ id,
1114
+ name,
1115
+ disabled = false,
1116
+ required = false,
1117
+ className = '',
1118
+ ...eventHandlers
1119
+ } = props;
1120
+
1121
+ return {
1122
+ t: 'select',
1123
+ a: {
1124
+ class: `bw_form_control ${className}`.trim(),
1125
+ id,
1126
+ name,
1127
+ disabled,
1128
+ required,
1129
+ ...eventHandlers
1130
+ },
1131
+ c: options.map(opt => ({
1132
+ t: 'option',
1133
+ a: {
1134
+ value: opt.value,
1135
+ selected: opt.value === value
1136
+ },
1137
+ c: opt.text || opt.value
1138
+ }))
1139
+ };
1140
+ }
1141
+
1142
+ /**
1143
+ * Create a checkbox input with label
1144
+ *
1145
+ * @param {Object} [props] - Checkbox configuration
1146
+ * @param {string} [props.label] - Checkbox label text
1147
+ * @param {boolean} [props.checked=false] - Whether the checkbox is checked
1148
+ * @param {string} [props.id] - Element ID (links label to checkbox)
1149
+ * @param {string} [props.name] - Input name attribute
1150
+ * @param {boolean} [props.disabled=false] - Whether the checkbox is disabled
1151
+ * @param {string} [props.value] - Checkbox value attribute
1152
+ * @returns {Object} TACO object representing a checkbox form group
1153
+ * @category Component Builders
1154
+ * @example
1155
+ * const checkbox = makeCheckbox({
1156
+ * label: "I agree to the terms",
1157
+ * id: "agree",
1158
+ * checked: false
1159
+ * });
1160
+ */
1161
+ function makeCheckbox(props = {}) {
1162
+ const {
1163
+ label,
1164
+ checked = false,
1165
+ id,
1166
+ name,
1167
+ disabled = false,
1168
+ value,
1169
+ className = '',
1170
+ ...eventHandlers
1171
+ } = props;
1172
+
1173
+ return {
1174
+ t: 'div',
1175
+ a: { class: `bw_form_check ${className}`.trim() },
1176
+ c: [
1177
+ {
1178
+ t: 'input',
1179
+ a: {
1180
+ type: 'checkbox',
1181
+ class: 'bw_form_check_input',
1182
+ checked,
1183
+ id,
1184
+ name,
1185
+ disabled,
1186
+ value,
1187
+ ...eventHandlers
1188
+ }
1189
+ },
1190
+ label && {
1191
+ t: 'label',
1192
+ a: { class: 'bw_form_check_label', for: id },
1193
+ c: label
1194
+ }
1195
+ ].filter(Boolean)
1196
+ };
1197
+ }
1198
+
1199
+ /**
1200
+ * Create a flexbox stack layout (vertical or horizontal)
1201
+ *
1202
+ * @param {Object} [props] - Stack configuration
1203
+ * @param {Array|Object|string} [props.children] - Stack children
1204
+ * @param {string} [props.direction="vertical"] - Stack direction ("vertical" or "horizontal")
1205
+ * @param {number} [props.gap=3] - Gap size (0-5)
1206
+ * @param {string} [props.className] - Additional CSS classes
1207
+ * @returns {Object} TACO object representing a stack layout
1208
+ * @category Component Builders
1209
+ * @example
1210
+ * const stack = makeStack({
1211
+ * direction: "horizontal",
1212
+ * gap: 2,
1213
+ * children: [
1214
+ * makeButton({ text: "Cancel", variant: "secondary" }),
1215
+ * makeButton({ text: "Save", variant: "primary" })
1216
+ * ]
1217
+ * });
1218
+ */
1219
+ function makeStack(props = {}) {
1220
+ const {
1221
+ children,
1222
+ direction = 'vertical',
1223
+ gap = 3,
1224
+ className = ''
1225
+ } = props;
1226
+
1227
+ return {
1228
+ t: 'div',
1229
+ a: {
1230
+ class: `bw_${direction === 'vertical' ? 'vstack' : 'hstack'} bw_gap_${gap} ${className}`.trim()
1231
+ },
1232
+ c: children
1233
+ };
1234
+ }
1235
+
1236
+ /**
1237
+ * Create a loading spinner indicator
1238
+ *
1239
+ * @param {Object} [props] - Spinner configuration
1240
+ * @param {string} [props.variant="primary"] - Color variant
1241
+ * @param {string} [props.size="md"] - Spinner size ("sm", "md", "lg")
1242
+ * @param {string} [props.type="border"] - Spinner type ("border" or "grow")
1243
+ * @returns {Object} TACO object representing a spinner with screen-reader text
1244
+ * @category Component Builders
1245
+ * @example
1246
+ * const spinner = makeSpinner({ variant: "info", size: "sm" });
1247
+ */
1248
+ function makeSpinner(props = {}) {
1249
+ const {
1250
+ variant = 'primary',
1251
+ size = 'md',
1252
+ type = 'border'
1253
+ } = props;
1254
+
1255
+ return {
1256
+ t: 'div',
1257
+ a: {
1258
+ class: `bw_spinner_${type} bw_spinner_${type}-${size} ${variantClass(variant)}`,
1259
+ role: 'status'
1260
+ },
1261
+ c: {
1262
+ t: 'span',
1263
+ a: { class: 'bw_visually_hidden' },
1264
+ c: 'Loading...'
1265
+ }
1266
+ };
1267
+ }
1268
+
1269
+ /**
1270
+ * Create a hero section for landing pages and headers
1271
+ *
1272
+ * Supports gradient backgrounds, background images with overlays,
1273
+ * and action buttons. Commonly used as the first visible section.
1274
+ *
1275
+ * @param {Object} [props] - Hero configuration
1276
+ * @param {string} [props.title] - Main headline text
1277
+ * @param {string} [props.subtitle] - Supporting description text
1278
+ * @param {string|Object|Array} [props.content] - Additional body content
1279
+ * @param {string} [props.variant="primary"] - Background variant ("primary", "secondary", "light", "dark")
1280
+ * @param {string} [props.size="lg"] - Vertical padding size ("sm", "md", "lg", "xl")
1281
+ * @param {boolean} [props.centered=true] - Center-align text
1282
+ * @param {boolean} [props.overlay=false] - Add dark overlay (for background images)
1283
+ * @param {string} [props.backgroundImage] - Background image URL
1284
+ * @param {Array|Object} [props.actions] - Call-to-action buttons
1285
+ * @param {string} [props.className] - Additional CSS classes
1286
+ * @returns {Object} TACO object representing a hero section
1287
+ * @category Component Builders
1288
+ * @example
1289
+ * const hero = makeHero({
1290
+ * title: "Welcome to Bitwrench",
1291
+ * subtitle: "Build UIs with pure JavaScript",
1292
+ * variant: "dark",
1293
+ * actions: [
1294
+ * makeButton({ text: "Get Started", variant: "primary", size: "lg" }),
1295
+ * makeButton({ text: "Learn More", variant: "outline-light", size: "lg" })
1296
+ * ]
1297
+ * });
1298
+ */
1299
+ function makeHero(props = {}) {
1300
+ const {
1301
+ title,
1302
+ subtitle,
1303
+ content,
1304
+ variant = 'primary',
1305
+ size = 'lg',
1306
+ centered = true,
1307
+ overlay = false,
1308
+ backgroundImage,
1309
+ actions,
1310
+ className = ''
1311
+ } = props;
1312
+
1313
+ const sizeClasses = {
1314
+ sm: 'bw_py_3',
1315
+ md: 'bw_py_4',
1316
+ lg: 'bw_py_5',
1317
+ xl: 'bw_py_6'
1318
+ };
1319
+
1320
+ return {
1321
+ t: 'section',
1322
+ a: {
1323
+ class: `bw_hero ${variantClass(variant)} ${sizeClasses[size] || sizeClasses.lg} ${centered ? 'bw_text_center' : ''} ${className}`.trim(),
1324
+ style: backgroundImage ? `background-image: url('${backgroundImage}'); background-size: cover; background-position: center;` : undefined
1325
+ },
1326
+ c: [
1327
+ overlay && {
1328
+ t: 'div',
1329
+ a: { class: 'bw_hero_overlay' }
1330
+ },
1331
+ {
1332
+ t: 'div',
1333
+ a: { class: 'bw_container' },
1334
+ c: {
1335
+ t: 'div',
1336
+ a: { class: 'bw_hero_content' },
1337
+ c: [
1338
+ title && {
1339
+ t: 'h1',
1340
+ a: { class: 'bw_hero_title bw_display_4 bw_mb_3' },
1341
+ c: title
1342
+ },
1343
+ subtitle && {
1344
+ t: 'p',
1345
+ a: { class: 'bw_hero_subtitle bw_lead bw_mb_4' },
1346
+ c: subtitle
1347
+ },
1348
+ content,
1349
+ actions && {
1350
+ t: 'div',
1351
+ a: { class: 'bw_hero_actions bw_mt_4' },
1352
+ c: actions
1353
+ }
1354
+ ].filter(Boolean)
1355
+ }
1356
+ }
1357
+ ].filter(Boolean)
1358
+ };
1359
+ }
1360
+
1361
+ /**
1362
+ * Create a responsive feature grid for showcasing capabilities
1363
+ *
1364
+ * Renders features in an equal-width column grid with optional icons,
1365
+ * titles, and descriptions.
1366
+ *
1367
+ * @param {Object} [props] - Feature grid configuration
1368
+ * @param {Array<Object>} [props.features=[]] - Feature items
1369
+ * @param {string} [props.features[].icon] - Icon content (emoji, HTML entity, or text)
1370
+ * @param {string} [props.features[].title] - Feature title
1371
+ * @param {string} [props.features[].description] - Feature description text
1372
+ * @param {number} [props.columns=3] - Number of columns (divides 12-col grid)
1373
+ * @param {boolean} [props.centered=true] - Center-align feature text
1374
+ * @param {string} [props.iconSize="3rem"] - Icon font size
1375
+ * @param {string} [props.className] - Additional CSS classes
1376
+ * @returns {Object} TACO object representing a feature grid
1377
+ * @category Component Builders
1378
+ * @example
1379
+ * const features = makeFeatureGrid({
1380
+ * columns: 3,
1381
+ * features: [
1382
+ * { icon: "⚡", title: "Fast", description: "Zero build step" },
1383
+ * { icon: "📦", title: "Small", description: "Under 45KB gzipped" },
1384
+ * { icon: "🔧", title: "Flexible", description: "Pure JS objects" }
1385
+ * ]
1386
+ * });
1387
+ */
1388
+ function makeFeatureGrid(props = {}) {
1389
+ const {
1390
+ features = [],
1391
+ columns = 3,
1392
+ centered = true,
1393
+ iconSize = '3rem',
1394
+ className = ''
1395
+ } = props;
1396
+
1397
+ const colClass = `bw_col_md_${12/columns}`;
1398
+
1399
+ return {
1400
+ t: 'div',
1401
+ a: { class: `bw_feature_grid ${className}`.trim() },
1402
+ c: {
1403
+ t: 'div',
1404
+ a: { class: 'bw_row bw_g_4' },
1405
+ c: features.map(feature => ({
1406
+ t: 'div',
1407
+ a: { class: colClass },
1408
+ c: {
1409
+ t: 'div',
1410
+ a: { class: `bw_feature ${centered ? 'bw_text_center' : ''}` },
1411
+ c: [
1412
+ feature.icon && {
1413
+ t: 'div',
1414
+ a: {
1415
+ class: 'bw_feature_icon bw_mb_3 bw_text_primary',
1416
+ style: `font-size: ${iconSize};`
1417
+ },
1418
+ c: feature.icon
1419
+ },
1420
+ feature.title && {
1421
+ t: 'h3',
1422
+ a: { class: 'bw_feature_title bw_h5 bw_mb_2' },
1423
+ c: feature.title
1424
+ },
1425
+ feature.description && {
1426
+ t: 'p',
1427
+ a: { class: 'bw_feature_description bw_text_muted' },
1428
+ c: feature.description
1429
+ }
1430
+ ].filter(Boolean)
1431
+ }
1432
+ }))
1433
+ }
1434
+ };
1435
+ }
1436
+
1437
+
1438
+ /**
1439
+ * Create a call-to-action section with title, description, and action buttons
1440
+ *
1441
+ * @param {Object} [props] - CTA configuration
1442
+ * @param {string} [props.title] - CTA headline
1443
+ * @param {string} [props.description] - CTA description text
1444
+ * @param {Array|Object} [props.actions] - CTA buttons or content
1445
+ * @param {string} [props.variant="light"] - Background variant
1446
+ * @param {boolean} [props.centered=true] - Center-align content
1447
+ * @param {string} [props.className] - Additional CSS classes
1448
+ * @returns {Object} TACO object representing a CTA section
1449
+ * @category Component Builders
1450
+ * @example
1451
+ * const cta = makeCTA({
1452
+ * title: "Ready to get started?",
1453
+ * description: "Join thousands of developers using Bitwrench.",
1454
+ * actions: [
1455
+ * makeButton({ text: "Sign Up Free", variant: "primary", size: "lg" })
1456
+ * ]
1457
+ * });
1458
+ */
1459
+ function makeCTA(props = {}) {
1460
+ const {
1461
+ title,
1462
+ description,
1463
+ actions,
1464
+ variant = 'light',
1465
+ centered = true,
1466
+ className = ''
1467
+ } = props;
1468
+
1469
+ return {
1470
+ t: 'section',
1471
+ a: { class: `bw_cta bw_bg_${variant} bw_py_5 ${className}`.trim() },
1472
+ c: {
1473
+ t: 'div',
1474
+ a: { class: 'bw_container' },
1475
+ c: {
1476
+ t: 'div',
1477
+ a: { class: `bw_cta_content ${centered ? 'bw_text_center' : ''}` },
1478
+ c: [
1479
+ title && { t: 'h2', a: { class: 'bw_cta_title bw_mb_3' }, c: title },
1480
+ description && { t: 'p', a: { class: 'bw_cta_description bw_lead bw_mb_4' }, c: description },
1481
+ actions && {
1482
+ t: 'div',
1483
+ a: { class: 'bw_cta_actions' },
1484
+ c: actions
1485
+ }
1486
+ ].filter(Boolean)
1487
+ }
1488
+ }
1489
+ };
1490
+ }
1491
+
1492
+ /**
1493
+ * Create a page section with optional centered header and background
1494
+ *
1495
+ * @param {Object} [props] - Section configuration
1496
+ * @param {string} [props.title] - Section title
1497
+ * @param {string} [props.subtitle] - Section subtitle (muted)
1498
+ * @param {string|Object|Array} [props.content] - Section body content
1499
+ * @param {string} [props.variant="default"] - Background variant ("default" for none, or a color name)
1500
+ * @param {string} [props.spacing="md"] - Vertical padding ("sm", "md", "lg", "xl")
1501
+ * @param {string} [props.className] - Additional CSS classes
1502
+ * @returns {Object} TACO object representing a content section
1503
+ * @category Component Builders
1504
+ * @example
1505
+ * const section = makeSection({
1506
+ * title: "Features",
1507
+ * subtitle: "Everything you need to build great UIs",
1508
+ * spacing: "lg",
1509
+ * content: makeFeatureGrid({ features: [...] })
1510
+ * });
1511
+ */
1512
+ function makeSection(props = {}) {
1513
+ const {
1514
+ title,
1515
+ subtitle,
1516
+ content,
1517
+ variant = 'default',
1518
+ spacing = 'md',
1519
+ className = ''
1520
+ } = props;
1521
+
1522
+ const spacingClasses = {
1523
+ sm: 'bw_py_3',
1524
+ md: 'bw_py_4',
1525
+ lg: 'bw_py_5',
1526
+ xl: 'bw_py_6'
1527
+ };
1528
+
1529
+ return {
1530
+ t: 'section',
1531
+ a: {
1532
+ class: `bw_section ${spacingClasses[spacing] || spacingClasses.md} ${variant !== 'default' ? `bw_bg_${variant}` : ''} ${className}`.trim()
1533
+ },
1534
+ c: {
1535
+ t: 'div',
1536
+ a: { class: 'bw_container' },
1537
+ c: [
1538
+ (title || subtitle) && {
1539
+ t: 'div',
1540
+ a: { class: 'bw_section_header bw_text_center bw_mb_5' },
1541
+ c: [
1542
+ title && { t: 'h2', a: { class: 'bw_section_title' }, c: title },
1543
+ subtitle && { t: 'p', a: { class: 'bw_section_subtitle bw_text_muted' }, c: subtitle }
1544
+ ].filter(Boolean)
1545
+ },
1546
+ content
1547
+ ].filter(Boolean)
1548
+ }
1549
+ };
1550
+ }
1551
+
1552
+ // =========================================================================
1553
+ // Component Handle Classes
1554
+ //
1555
+ // Handle classes provide imperative DOM manipulation for rendered components.
1556
+ // They cache child element references for efficient updates without
1557
+ // full re-renders. Used by bw.createCard(), bw.createTable(), etc.
1558
+ // =========================================================================
1559
+
1560
+ // Handle classes (CardHandle, TableHandle, NavbarHandle, TabsHandle)
1561
+ // removed in v2.0.15 — superseded by ComponentHandle.
1562
+ // See dev/dead-code-elimination-v2.0.15.md for recovery.
1563
+
1564
+ /**
1565
+ * Create a code demo component for documentation pages
1566
+ *
1567
+ * Displays a live result alongside source code in a tabbed interface.
1568
+ * Includes a copy-to-clipboard button on the code tab.
1569
+ *
1570
+ * @param {Object} [props] - Code demo configuration
1571
+ * @param {string} [props.title] - Demo title heading
1572
+ * @param {string} [props.description] - Demo description text
1573
+ * @param {string} [props.code] - Source code to display (adds a "Code" tab when present)
1574
+ * @param {string|Object|Array} [props.result] - Live result content for the "Result" tab
1575
+ * @param {string} [props.language="javascript"] - Code language for syntax class
1576
+ * @returns {Object} TACO object representing a code demo with tabbed Result/Code views
1577
+ * @category Component Builders
1578
+ * @example
1579
+ * const demo = makeCodeDemo({
1580
+ * title: "Button Example",
1581
+ * description: "A simple primary button",
1582
+ * code: 'makeButton({ text: "Click me" })',
1583
+ * result: makeButton({ text: "Click me" })
1584
+ * });
1585
+ */
1586
+ function makeCodeDemo(props = {}) {
1587
+ const {
1588
+ title,
1589
+ description,
1590
+ code,
1591
+ result,
1592
+ language = 'javascript'
1593
+ } = props;
1594
+
1595
+ // Generate unique ID for this demo
1596
+ `demo-${Math.random().toString(36).substr(2, 9)}`;
1597
+
1598
+ const tabs = [
1599
+ {
1600
+ label: 'Result',
1601
+ active: true,
1602
+ content: result
1603
+ }
1604
+ ];
1605
+
1606
+ // Only add Code tab if code is provided
1607
+ if (code) {
1608
+ tabs.push({
1609
+ label: 'Code',
1610
+ content: {
1611
+ t: 'div',
1612
+ a: { style: 'position: relative;' },
1613
+ c: [
1614
+ {
1615
+ t: 'button',
1616
+ a: {
1617
+ class: 'bw_copy_btn bw_code_copy_btn',
1618
+ onclick: function(e) {
1619
+ navigator.clipboard.writeText(code).then(function() {
1620
+ var btn = e.target;
1621
+ var originalText = btn.textContent;
1622
+ btn.textContent = 'Copied!';
1623
+ btn.classList.add('bw_code_copy_btn_copied');
1624
+ setTimeout(function() {
1625
+ btn.textContent = originalText;
1626
+ btn.classList.remove('bw_code_copy_btn_copied');
1627
+ }, 2000);
1628
+ });
1629
+ }
1630
+ },
1631
+ c: 'Copy'
1632
+ },
1633
+ (typeof globalThis !== 'undefined' && typeof globalThis.bw !== 'undefined' && typeof globalThis.bw.codeEditor === 'function')
1634
+ ? globalThis.bw.codeEditor({ code: code, lang: language === 'javascript' ? 'js' : language, readOnly: true, height: 'auto' })
1635
+ : {
1636
+ t: 'pre',
1637
+ a: { class: 'bw_code_pre' },
1638
+ c: {
1639
+ t: 'code',
1640
+ a: { class: `bw_code_block language-${language}` },
1641
+ c: code
1642
+ }
1643
+ }
1644
+ ]
1645
+ }
1646
+ });
1647
+ }
1648
+
1649
+ const content = [
1650
+ title && { t: 'h3', c: title },
1651
+ description && {
1652
+ t: 'p',
1653
+ a: { class: 'bw_text_muted bw_mb_3' },
1654
+ c: description
1655
+ },
1656
+ makeTabs({ tabs})
1657
+ ].filter(Boolean);
1658
+
1659
+ return {
1660
+ t: 'div',
1661
+ a: { class: 'bw_code_demo' },
1662
+ c: content
1663
+ };
1664
+ }
1665
+
1666
+ /**
1667
+ * Registry mapping component type names to their handle classes
1668
+ *
1669
+ * Used by bw.createCard(), bw.createTable(), etc. to wrap rendered
1670
+ * DOM elements in the appropriate imperative handle.
1671
+ *
1672
+ * @type {Object.<string, Function>}
1673
+ */
1674
+ // =========================================================================
1675
+ // Phase 1: Quick Wins
1676
+ // =========================================================================
1677
+
1678
+ /**
1679
+ * Create a pagination navigation component
1680
+ *
1681
+ * @param {Object} [props] - Pagination configuration
1682
+ * @param {number} [props.pages=1] - Total number of pages
1683
+ * @param {number} [props.currentPage=1] - Currently active page (1-based)
1684
+ * @param {Function} [props.onPageChange] - Callback when page changes, receives page number
1685
+ * @param {string} [props.size] - Size variant ("sm" or "lg")
1686
+ * @param {string} [props.className] - Additional CSS classes
1687
+ * @returns {Object} TACO object representing a pagination nav
1688
+ * @category Component Builders
1689
+ * @example
1690
+ * const pager = makePagination({
1691
+ * pages: 10,
1692
+ * currentPage: 3,
1693
+ * onPageChange: (page) => loadPage(page)
1694
+ * });
1695
+ */
1696
+ function makePagination(props = {}) {
1697
+ const {
1698
+ pages = 1,
1699
+ currentPage = 1,
1700
+ onPageChange,
1701
+ size,
1702
+ className = ''
1703
+ } = props;
1704
+
1705
+ function handleClick(page) {
1706
+ return function(e) {
1707
+ e.preventDefault();
1708
+ if (page < 1 || page > pages || page === currentPage) return;
1709
+ if (onPageChange) onPageChange(page);
1710
+ };
1711
+ }
1712
+
1713
+ const items = [];
1714
+
1715
+ // Previous arrow
1716
+ items.push({
1717
+ t: 'li',
1718
+ a: { class: `bw_page_item ${currentPage <= 1 ? 'bw_disabled' : ''}`.trim() },
1719
+ c: {
1720
+ t: 'a',
1721
+ a: { class: 'bw_page_link', href: '#', onclick: handleClick(currentPage - 1), 'aria-label': 'Previous' },
1722
+ c: '\u2039'
1723
+ }
1724
+ });
1725
+
1726
+ // Page numbers
1727
+ for (var i = 1; i <= pages; i++) {
1728
+ (function(pageNum) {
1729
+ items.push({
1730
+ t: 'li',
1731
+ a: { class: `bw_page_item ${pageNum === currentPage ? 'bw_active' : ''}`.trim() },
1732
+ c: {
1733
+ t: 'a',
1734
+ a: { class: 'bw_page_link', href: '#', onclick: handleClick(pageNum) },
1735
+ c: '' + pageNum
1736
+ }
1737
+ });
1738
+ })(i);
1739
+ }
1740
+
1741
+ // Next arrow
1742
+ items.push({
1743
+ t: 'li',
1744
+ a: { class: `bw_page_item ${currentPage >= pages ? 'bw_disabled' : ''}`.trim() },
1745
+ c: {
1746
+ t: 'a',
1747
+ a: { class: 'bw_page_link', href: '#', onclick: handleClick(currentPage + 1), 'aria-label': 'Next' },
1748
+ c: '\u203A'
1749
+ }
1750
+ });
1751
+
1752
+ return {
1753
+ t: 'nav',
1754
+ a: { 'aria-label': 'Pagination' },
1755
+ c: {
1756
+ t: 'ul',
1757
+ a: {
1758
+ class: `bw_pagination ${size ? 'bw_pagination_' + size : ''} ${className}`.trim()
1759
+ },
1760
+ c: items
1761
+ }
1762
+ };
1763
+ }
1764
+
1765
+ /**
1766
+ * Create a radio button input with label
1767
+ *
1768
+ * @param {Object} [props] - Radio configuration
1769
+ * @param {string} [props.label] - Radio label text
1770
+ * @param {string} [props.name] - Radio group name
1771
+ * @param {string} [props.value] - Radio value attribute
1772
+ * @param {boolean} [props.checked=false] - Whether the radio is selected
1773
+ * @param {string} [props.id] - Element ID (links label to radio)
1774
+ * @param {boolean} [props.disabled=false] - Whether the radio is disabled
1775
+ * @param {string} [props.className] - Additional CSS classes
1776
+ * @returns {Object} TACO object representing a radio form group
1777
+ * @category Component Builders
1778
+ * @example
1779
+ * const radio = makeRadio({
1780
+ * label: "Option A",
1781
+ * name: "choice",
1782
+ * value: "a",
1783
+ * checked: true
1784
+ * });
1785
+ */
1786
+ function makeRadio(props = {}) {
1787
+ const {
1788
+ label,
1789
+ name,
1790
+ value,
1791
+ checked = false,
1792
+ id,
1793
+ disabled = false,
1794
+ className = '',
1795
+ ...eventHandlers
1796
+ } = props;
1797
+
1798
+ return {
1799
+ t: 'div',
1800
+ a: { class: `bw_form_check ${className}`.trim() },
1801
+ c: [
1802
+ {
1803
+ t: 'input',
1804
+ a: {
1805
+ type: 'radio',
1806
+ class: 'bw_form_check_input',
1807
+ name,
1808
+ value,
1809
+ checked,
1810
+ id,
1811
+ disabled,
1812
+ ...eventHandlers
1813
+ }
1814
+ },
1815
+ label && {
1816
+ t: 'label',
1817
+ a: { class: 'bw_form_check_label', for: id },
1818
+ c: label
1819
+ }
1820
+ ].filter(Boolean)
1821
+ };
1822
+ }
1823
+
1824
+ /**
1825
+ * Create a button group wrapper
1826
+ *
1827
+ * @param {Object} [props] - Button group configuration
1828
+ * @param {Array} [props.children] - Button TACO objects to group
1829
+ * @param {string} [props.size] - Size variant ("sm" or "lg")
1830
+ * @param {boolean} [props.vertical=false] - Stack buttons vertically
1831
+ * @param {string} [props.className] - Additional CSS classes
1832
+ * @returns {Object} TACO object representing a button group
1833
+ * @category Component Builders
1834
+ * @example
1835
+ * const group = makeButtonGroup({
1836
+ * children: [
1837
+ * makeButton({ text: "Left", variant: "primary" }),
1838
+ * makeButton({ text: "Middle", variant: "primary" }),
1839
+ * makeButton({ text: "Right", variant: "primary" })
1840
+ * ]
1841
+ * });
1842
+ */
1843
+ function makeButtonGroup(props = {}) {
1844
+ const {
1845
+ children,
1846
+ size,
1847
+ vertical = false,
1848
+ className = ''
1849
+ } = props;
1850
+
1851
+ return {
1852
+ t: 'div',
1853
+ a: {
1854
+ class: `${vertical ? 'bw_btn_group_vertical' : 'bw_btn_group'} ${size ? 'bw_btn_group_' + size : ''} ${className}`.trim(),
1855
+ role: 'group'
1856
+ },
1857
+ c: children
1858
+ };
1859
+ }
1860
+
1861
+ // =========================================================================
1862
+ // Phase 2: Core Interactive
1863
+ // =========================================================================
1864
+
1865
+ /**
1866
+ * Create an accordion component with collapsible items
1867
+ *
1868
+ * @param {Object} [props] - Accordion configuration
1869
+ * @param {Array<Object>} [props.items=[]] - Accordion items
1870
+ * @param {string} props.items[].title - Header text for the accordion item
1871
+ * @param {string|Object|Array} props.items[].content - Collapsible content
1872
+ * @param {boolean} [props.items[].open=false] - Whether the item is initially open
1873
+ * @param {boolean} [props.multiOpen=false] - Allow multiple items open simultaneously
1874
+ * @param {string} [props.className] - Additional CSS classes
1875
+ * @returns {Object} TACO object representing an accordion
1876
+ * @category Component Builders
1877
+ * @example
1878
+ * const accordion = makeAccordion({
1879
+ * items: [
1880
+ * { title: "Section 1", content: "Content 1", open: true },
1881
+ * { title: "Section 2", content: "Content 2" }
1882
+ * ]
1883
+ * });
1884
+ */
1885
+ function makeAccordion(props = {}) {
1886
+ const {
1887
+ items = [],
1888
+ multiOpen = false,
1889
+ className = ''
1890
+ } = props;
1891
+
1892
+ return {
1893
+ t: 'div',
1894
+ a: { class: `bw_accordion ${className}`.trim() },
1895
+ c: items.map(function(item, index) {
1896
+ return {
1897
+ t: 'div',
1898
+ a: { class: 'bw_accordion_item' },
1899
+ c: [
1900
+ {
1901
+ t: 'h2',
1902
+ a: { class: 'bw_accordion_header' },
1903
+ c: {
1904
+ t: 'button',
1905
+ a: {
1906
+ class: `bw_accordion_button ${item.open ? '' : 'bw_collapsed'}`.trim(),
1907
+ type: 'button',
1908
+ 'aria-expanded': item.open ? 'true' : 'false',
1909
+ 'data-accordion-index': index,
1910
+ onclick: function(e) {
1911
+ var btn = e.target.closest('.bw_accordion_button');
1912
+ var accordionEl = btn.closest('.bw_accordion');
1913
+ var accordionItem = btn.closest('.bw_accordion_item');
1914
+ var collapse = accordionItem.querySelector('.bw_accordion_collapse');
1915
+ var isOpen = collapse.classList.contains('bw_collapse_show');
1916
+
1917
+ if (!multiOpen) {
1918
+ // Animate-close all other open siblings
1919
+ var allItems = accordionEl.querySelectorAll('.bw_accordion_item');
1920
+ for (var j = 0; j < allItems.length; j++) {
1921
+ if (allItems[j] === accordionItem) continue;
1922
+ var sibCollapse = allItems[j].querySelector('.bw_accordion_collapse');
1923
+ var sibBtn = allItems[j].querySelector('.bw_accordion_button');
1924
+ if (sibCollapse.classList.contains('bw_collapse_show')) {
1925
+ sibCollapse.style.maxHeight = sibCollapse.scrollHeight + 'px';
1926
+ sibCollapse.offsetHeight; // force reflow
1927
+ sibCollapse.style.maxHeight = '0px';
1928
+ sibCollapse.classList.remove('bw_collapse_show');
1929
+ sibBtn.classList.add('bw_collapsed');
1930
+ sibBtn.setAttribute('aria-expanded', 'false');
1931
+ }
1932
+ }
1933
+ }
1934
+
1935
+ if (isOpen) {
1936
+ // Animate close
1937
+ collapse.style.maxHeight = collapse.scrollHeight + 'px';
1938
+ collapse.offsetHeight; // force reflow
1939
+ collapse.style.maxHeight = '0px';
1940
+ collapse.classList.remove('bw_collapse_show');
1941
+ btn.classList.add('bw_collapsed');
1942
+ btn.setAttribute('aria-expanded', 'false');
1943
+ } else {
1944
+ // Animate open
1945
+ collapse.classList.add('bw_collapse_show');
1946
+ collapse.style.maxHeight = '0px';
1947
+ collapse.offsetHeight; // force reflow
1948
+ collapse.style.maxHeight = collapse.scrollHeight + 'px';
1949
+ btn.classList.remove('bw_collapsed');
1950
+ btn.setAttribute('aria-expanded', 'true');
1951
+ // After transition, allow dynamic content sizing
1952
+ var onEnd = function(ev) {
1953
+ if (ev.propertyName === 'max-height' && collapse.classList.contains('bw_collapse_show')) {
1954
+ collapse.style.maxHeight = 'none';
1955
+ }
1956
+ collapse.removeEventListener('transitionend', onEnd);
1957
+ };
1958
+ collapse.addEventListener('transitionend', onEnd);
1959
+ }
1960
+ }
1961
+ },
1962
+ c: item.title
1963
+ }
1964
+ },
1965
+ {
1966
+ t: 'div',
1967
+ a: { class: `bw_accordion_collapse ${item.open ? 'bw_collapse_show' : ''}`.trim() },
1968
+ c: {
1969
+ t: 'div',
1970
+ a: { class: 'bw_accordion_body' },
1971
+ c: item.content
1972
+ },
1973
+ o: item.open ? {
1974
+ mounted: function(el) {
1975
+ el.style.maxHeight = 'none';
1976
+ }
1977
+ } : undefined
1978
+ }
1979
+ ]
1980
+ };
1981
+ }),
1982
+ o: {
1983
+ type: 'accordion',
1984
+ state: { multiOpen: multiOpen }
1985
+ }
1986
+ };
1987
+ }
1988
+
1989
+ // ModalHandle removed in v2.0.15 — superseded by ComponentHandle.
1990
+ // See dev/dead-code-elimination-v2.0.15.md for recovery.
1991
+
1992
+ /**
1993
+ * Create a modal dialog overlay
1994
+ *
1995
+ * @param {Object} [props] - Modal configuration
1996
+ * @param {string} [props.title] - Modal title in header
1997
+ * @param {string|Object|Array} [props.content] - Modal body content
1998
+ * @param {string|Object|Array} [props.footer] - Modal footer content
1999
+ * @param {string} [props.size] - Modal size ("sm", "lg", "xl")
2000
+ * @param {boolean} [props.closeButton=true] - Show X close button in header
2001
+ * @param {Function} [props.onClose] - Callback when modal is closed
2002
+ * @param {string} [props.className] - Additional CSS classes
2003
+ * @returns {Object} TACO object representing a modal
2004
+ * @category Component Builders
2005
+ * @example
2006
+ * const modal = makeModal({
2007
+ * title: "Confirm",
2008
+ * content: "Are you sure?",
2009
+ * footer: makeButton({ text: "OK", variant: "primary" })
2010
+ * });
2011
+ */
2012
+ function makeModal(props = {}) {
2013
+ const {
2014
+ title,
2015
+ content,
2016
+ footer,
2017
+ size,
2018
+ closeButton = true,
2019
+ onClose,
2020
+ className = ''
2021
+ } = props;
2022
+
2023
+ function closeModal(el) {
2024
+ var backdrop = el.closest('.bw_modal');
2025
+ if (backdrop) {
2026
+ backdrop.classList.remove('bw_modal_show');
2027
+ document.body.style.overflow = '';
2028
+ }
2029
+ if (onClose) onClose();
2030
+ }
2031
+
2032
+ return {
2033
+ t: 'div',
2034
+ a: { class: `bw_modal ${className}`.trim() },
2035
+ c: {
2036
+ t: 'div',
2037
+ a: { class: `bw_modal_dialog ${size ? 'bw_modal_' + size : ''}`.trim() },
2038
+ c: {
2039
+ t: 'div',
2040
+ a: { class: 'bw_modal_content' },
2041
+ c: [
2042
+ (title || closeButton) && {
2043
+ t: 'div',
2044
+ a: { class: 'bw_modal_header' },
2045
+ c: [
2046
+ title && { t: 'h5', a: { class: 'bw_modal_title' }, c: title },
2047
+ closeButton && {
2048
+ t: 'button',
2049
+ a: {
2050
+ type: 'button',
2051
+ class: 'bw_close',
2052
+ 'aria-label': 'Close',
2053
+ onclick: function(e) { closeModal(e.target); }
2054
+ },
2055
+ c: '\u00D7'
2056
+ }
2057
+ ].filter(Boolean)
2058
+ },
2059
+ content && {
2060
+ t: 'div',
2061
+ a: { class: 'bw_modal_body' },
2062
+ c: content
2063
+ },
2064
+ footer && {
2065
+ t: 'div',
2066
+ a: { class: 'bw_modal_footer' },
2067
+ c: footer
2068
+ }
2069
+ ].filter(Boolean)
2070
+ }
2071
+ },
2072
+ o: {
2073
+ type: 'modal',
2074
+ mounted: function(el) {
2075
+ // Click backdrop to close
2076
+ el.addEventListener('click', function(e) {
2077
+ if (e.target === el) closeModal(el);
2078
+ });
2079
+ // Escape key to close
2080
+ var escHandler = function(e) {
2081
+ if (e.key === 'Escape' && el.classList.contains('bw_modal_show')) {
2082
+ closeModal(el);
2083
+ }
2084
+ };
2085
+ document.addEventListener('keydown', escHandler);
2086
+ el._bw_escHandler = escHandler;
2087
+ },
2088
+ unmount: function(el) {
2089
+ if (el._bw_escHandler) {
2090
+ document.removeEventListener('keydown', el._bw_escHandler);
2091
+ }
2092
+ document.body.style.overflow = '';
2093
+ }
2094
+ }
2095
+ };
2096
+ }
2097
+
2098
+ /**
2099
+ * Create a toast notification popup
2100
+ *
2101
+ * @param {Object} [props] - Toast configuration
2102
+ * @param {string} [props.title] - Toast title
2103
+ * @param {string|Object|Array} [props.content] - Toast body content
2104
+ * @param {string} [props.variant="info"] - Color variant ("primary", "success", "danger", "warning", "info")
2105
+ * @param {boolean} [props.autoDismiss=true] - Auto-dismiss after delay
2106
+ * @param {number} [props.delay=5000] - Auto-dismiss delay in ms
2107
+ * @param {string} [props.position="top-right"] - Container position
2108
+ * @param {string} [props.className] - Additional CSS classes
2109
+ * @returns {Object} TACO object representing a toast
2110
+ * @category Component Builders
2111
+ * @example
2112
+ * const toast = makeToast({
2113
+ * title: "Success",
2114
+ * content: "File saved!",
2115
+ * variant: "success"
2116
+ * });
2117
+ */
2118
+ function makeToast(props = {}) {
2119
+ const {
2120
+ title,
2121
+ content,
2122
+ variant = 'info',
2123
+ autoDismiss = true,
2124
+ delay = 5000,
2125
+ position = 'top-right',
2126
+ className = ''
2127
+ } = props;
2128
+
2129
+ return {
2130
+ t: 'div',
2131
+ a: {
2132
+ class: `bw_toast ${variantClass(variant)} ${className}`.trim(),
2133
+ role: 'alert',
2134
+ 'data-position': position
2135
+ },
2136
+ c: [
2137
+ (title) && {
2138
+ t: 'div',
2139
+ a: { class: 'bw_toast_header' },
2140
+ c: [
2141
+ { t: 'strong', c: title },
2142
+ {
2143
+ t: 'button',
2144
+ a: {
2145
+ type: 'button',
2146
+ class: 'bw_close',
2147
+ 'aria-label': 'Close',
2148
+ onclick: function(e) {
2149
+ var toast = e.target.closest('.bw_toast');
2150
+ if (toast) {
2151
+ toast.classList.add('bw_toast_hiding');
2152
+ setTimeout(function() { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 300);
2153
+ }
2154
+ }
2155
+ },
2156
+ c: '\u00D7'
2157
+ }
2158
+ ]
2159
+ },
2160
+ content && {
2161
+ t: 'div',
2162
+ a: { class: 'bw_toast_body' },
2163
+ c: content
2164
+ }
2165
+ ].filter(Boolean),
2166
+ o: {
2167
+ type: 'toast',
2168
+ mounted: function(el) {
2169
+ // Trigger show animation
2170
+ requestAnimationFrame(function() {
2171
+ el.classList.add('bw_toast_show');
2172
+ });
2173
+ // Auto-dismiss
2174
+ if (autoDismiss) {
2175
+ setTimeout(function() {
2176
+ el.classList.add('bw_toast_hiding');
2177
+ setTimeout(function() { if (el.parentNode) el.parentNode.removeChild(el); }, 300);
2178
+ }, delay);
2179
+ }
2180
+ }
2181
+ }
2182
+ };
2183
+ }
2184
+
2185
+ // =========================================================================
2186
+ // Phase 3: Essential Modern
2187
+ // =========================================================================
2188
+
2189
+ /**
2190
+ * Create a dropdown menu triggered by a button
2191
+ *
2192
+ * @param {Object} [props] - Dropdown configuration
2193
+ * @param {string|Object} [props.trigger] - Button text or TACO for the trigger
2194
+ * @param {Array<Object>} [props.items=[]] - Menu items
2195
+ * @param {string} [props.items[].text] - Item display text
2196
+ * @param {string} [props.items[].href] - Item link URL
2197
+ * @param {Function} [props.items[].onclick] - Item click handler
2198
+ * @param {boolean} [props.items[].divider] - Render as a divider line
2199
+ * @param {boolean} [props.items[].disabled] - Whether the item is disabled
2200
+ * @param {string} [props.align="start"] - Menu alignment ("start" or "end")
2201
+ * @param {string} [props.variant="primary"] - Trigger button variant
2202
+ * @param {string} [props.className] - Additional CSS classes
2203
+ * @returns {Object} TACO object representing a dropdown
2204
+ * @category Component Builders
2205
+ * @example
2206
+ * const dropdown = makeDropdown({
2207
+ * trigger: "Actions",
2208
+ * items: [
2209
+ * { text: "Edit", onclick: () => edit() },
2210
+ * { divider: true },
2211
+ * { text: "Delete", onclick: () => del() }
2212
+ * ]
2213
+ * });
2214
+ */
2215
+ function makeDropdown(props = {}) {
2216
+ const {
2217
+ trigger,
2218
+ items = [],
2219
+ align = 'start',
2220
+ variant = 'primary',
2221
+ className = ''
2222
+ } = props;
2223
+
2224
+ var triggerTaco;
2225
+ if (typeof trigger === 'string' || trigger === undefined) {
2226
+ triggerTaco = {
2227
+ t: 'button',
2228
+ a: {
2229
+ class: `bw_btn ${variantClass(variant)} bw_dropdown_toggle`,
2230
+ type: 'button',
2231
+ onclick: function(e) {
2232
+ var dropdown = e.target.closest('.bw_dropdown');
2233
+ var menu = dropdown.querySelector('.bw_dropdown_menu');
2234
+ menu.classList.toggle('bw_dropdown_show');
2235
+ }
2236
+ },
2237
+ c: trigger || 'Dropdown'
2238
+ };
2239
+ } else {
2240
+ triggerTaco = trigger;
2241
+ }
2242
+
2243
+ return {
2244
+ t: 'div',
2245
+ a: { class: `bw_dropdown ${className}`.trim() },
2246
+ c: [
2247
+ triggerTaco,
2248
+ {
2249
+ t: 'div',
2250
+ a: { class: `bw_dropdown_menu ${align === 'end' ? 'bw_dropdown_menu_end' : ''}`.trim() },
2251
+ c: items.map(function(item) {
2252
+ if (item.divider) {
2253
+ return { t: 'hr', a: { class: 'bw_dropdown_divider' } };
2254
+ }
2255
+ return {
2256
+ t: 'a',
2257
+ a: {
2258
+ class: `bw_dropdown_item ${item.disabled ? 'disabled' : ''}`.trim(),
2259
+ href: item.href || '#',
2260
+ onclick: item.disabled ? undefined : function(e) {
2261
+ if (!item.href) e.preventDefault();
2262
+ var dropdown = e.target.closest('.bw_dropdown');
2263
+ var menu = dropdown.querySelector('.bw_dropdown_menu');
2264
+ menu.classList.remove('bw_dropdown_show');
2265
+ if (item.onclick) item.onclick(e);
2266
+ }
2267
+ },
2268
+ c: item.text
2269
+ };
2270
+ })
2271
+ }
2272
+ ],
2273
+ o: {
2274
+ type: 'dropdown',
2275
+ mounted: function(el) {
2276
+ // Click outside to close
2277
+ var outsideHandler = function(e) {
2278
+ if (!el.contains(e.target)) {
2279
+ var menu = el.querySelector('.bw_dropdown_menu');
2280
+ if (menu) menu.classList.remove('bw_dropdown_show');
2281
+ }
2282
+ };
2283
+ document.addEventListener('click', outsideHandler);
2284
+ el._bw_outsideHandler = outsideHandler;
2285
+ },
2286
+ unmount: function(el) {
2287
+ if (el._bw_outsideHandler) {
2288
+ document.removeEventListener('click', el._bw_outsideHandler);
2289
+ }
2290
+ }
2291
+ }
2292
+ };
2293
+ }
2294
+
2295
+ /**
2296
+ * Create a toggle switch (styled checkbox)
2297
+ *
2298
+ * @param {Object} [props] - Switch configuration
2299
+ * @param {string} [props.label] - Switch label text
2300
+ * @param {boolean} [props.checked=false] - Whether the switch is on
2301
+ * @param {string} [props.id] - Element ID (links label to switch)
2302
+ * @param {string} [props.name] - Input name attribute
2303
+ * @param {boolean} [props.disabled=false] - Whether the switch is disabled
2304
+ * @param {string} [props.className] - Additional CSS classes
2305
+ * @returns {Object} TACO object representing a toggle switch
2306
+ * @category Component Builders
2307
+ * @example
2308
+ * const toggle = makeSwitch({
2309
+ * label: "Dark mode",
2310
+ * checked: false,
2311
+ * onchange: (e) => toggleDark(e.target.checked)
2312
+ * });
2313
+ */
2314
+ function makeSwitch(props = {}) {
2315
+ const {
2316
+ label,
2317
+ checked = false,
2318
+ id,
2319
+ name,
2320
+ disabled = false,
2321
+ className = '',
2322
+ ...eventHandlers
2323
+ } = props;
2324
+
2325
+ return {
2326
+ t: 'div',
2327
+ a: { class: `bw_form_check bw_form_switch ${className}`.trim() },
2328
+ c: [
2329
+ {
2330
+ t: 'input',
2331
+ a: {
2332
+ type: 'checkbox',
2333
+ class: 'bw_form_check_input bw_switch_input',
2334
+ role: 'switch',
2335
+ checked,
2336
+ id,
2337
+ name,
2338
+ disabled,
2339
+ ...eventHandlers
2340
+ }
2341
+ },
2342
+ label && {
2343
+ t: 'label',
2344
+ a: { class: 'bw_form_check_label', for: id },
2345
+ c: label
2346
+ }
2347
+ ].filter(Boolean)
2348
+ };
2349
+ }
2350
+
2351
+ /**
2352
+ * Create a skeleton loading placeholder
2353
+ *
2354
+ * @param {Object} [props] - Skeleton configuration
2355
+ * @param {string} [props.variant="text"] - Shape variant ("text", "circle", "rect")
2356
+ * @param {string} [props.width] - Custom width (e.g. "200px", "100%")
2357
+ * @param {string} [props.height] - Custom height (e.g. "20px")
2358
+ * @param {number} [props.count=1] - Number of skeleton lines (for text variant)
2359
+ * @param {string} [props.className] - Additional CSS classes
2360
+ * @returns {Object} TACO object representing a skeleton placeholder
2361
+ * @category Component Builders
2362
+ * @example
2363
+ * const skeleton = makeSkeleton({ variant: "text", count: 3, width: "100%" });
2364
+ */
2365
+ function makeSkeleton(props = {}) {
2366
+ const {
2367
+ variant = 'text',
2368
+ width,
2369
+ height,
2370
+ count = 1,
2371
+ className = ''
2372
+ } = props;
2373
+
2374
+ if (variant === 'circle') {
2375
+ var circleSize = width || height || '3rem';
2376
+ return {
2377
+ t: 'div',
2378
+ a: {
2379
+ class: `bw_skeleton bw_skeleton_circle ${className}`.trim(),
2380
+ style: { width: circleSize, height: circleSize }
2381
+ }
2382
+ };
2383
+ }
2384
+
2385
+ if (variant === 'rect') {
2386
+ return {
2387
+ t: 'div',
2388
+ a: {
2389
+ class: `bw_skeleton bw_skeleton_rect ${className}`.trim(),
2390
+ style: {
2391
+ width: width || '100%',
2392
+ height: height || '120px'
2393
+ }
2394
+ }
2395
+ };
2396
+ }
2397
+
2398
+ // Text variant — multiple lines
2399
+ if (count === 1) {
2400
+ return {
2401
+ t: 'div',
2402
+ a: {
2403
+ class: `bw_skeleton bw_skeleton_text ${className}`.trim(),
2404
+ style: {
2405
+ width: width || '100%',
2406
+ height: height || '1em'
2407
+ }
2408
+ }
2409
+ };
2410
+ }
2411
+
2412
+ var lines = [];
2413
+ for (var i = 0; i < count; i++) {
2414
+ lines.push({
2415
+ t: 'div',
2416
+ a: {
2417
+ class: 'bw_skeleton bw_skeleton_text',
2418
+ style: {
2419
+ width: i === count - 1 ? '75%' : (width || '100%'),
2420
+ height: height || '1em'
2421
+ }
2422
+ }
2423
+ });
2424
+ }
2425
+
2426
+ return {
2427
+ t: 'div',
2428
+ a: { class: `bw_skeleton_group ${className}`.trim() },
2429
+ c: lines
2430
+ };
2431
+ }
2432
+
2433
+ /**
2434
+ * Create a user avatar with image or initials fallback
2435
+ *
2436
+ * @param {Object} [props] - Avatar configuration
2437
+ * @param {string} [props.src] - Image source URL
2438
+ * @param {string} [props.alt] - Image alt text
2439
+ * @param {string} [props.initials] - Fallback initials (e.g. "JD")
2440
+ * @param {string} [props.size="md"] - Size ("sm", "md", "lg", "xl")
2441
+ * @param {string} [props.variant="primary"] - Background color variant for initials
2442
+ * @param {string} [props.className] - Additional CSS classes
2443
+ * @returns {Object} TACO object representing an avatar
2444
+ * @category Component Builders
2445
+ * @example
2446
+ * const avatar = makeAvatar({ src: "/photo.jpg", alt: "Jane Doe", size: "lg" });
2447
+ * const avatarInitials = makeAvatar({ initials: "JD", variant: "success" });
2448
+ */
2449
+ function makeAvatar(props = {}) {
2450
+ const {
2451
+ src,
2452
+ alt = '',
2453
+ initials,
2454
+ size = 'md',
2455
+ variant = 'primary',
2456
+ className = ''
2457
+ } = props;
2458
+
2459
+ if (src) {
2460
+ return {
2461
+ t: 'img',
2462
+ a: {
2463
+ class: `bw_avatar bw_avatar_${size} ${className}`.trim(),
2464
+ src: src,
2465
+ alt: alt
2466
+ }
2467
+ };
2468
+ }
2469
+
2470
+ return {
2471
+ t: 'div',
2472
+ a: {
2473
+ class: `bw_avatar bw_avatar_${size} ${variantClass(variant)} ${className}`.trim()
2474
+ },
2475
+ c: initials || ''
2476
+ };
2477
+ }
2478
+
2479
+ /**
2480
+ * Create a carousel/slideshow component with slide transitions
2481
+ *
2482
+ * Supports image slides, TACO content slides, captions, prev/next controls,
2483
+ * dot indicators, and optional auto-play. Uses CSS translateX transitions.
2484
+ *
2485
+ * @param {Object} [props] - Carousel configuration
2486
+ * @param {Array<Object>} [props.items=[]] - Slide items
2487
+ * @param {string|Object} props.items[].content - Slide content (TACO, string, or img element)
2488
+ * @param {string} [props.items[].caption] - Caption text shown at bottom of slide
2489
+ * @param {boolean} [props.showControls=true] - Show prev/next arrow buttons
2490
+ * @param {boolean} [props.showIndicators=true] - Show dot navigation
2491
+ * @param {boolean} [props.autoPlay=false] - Auto-advance slides
2492
+ * @param {number} [props.interval=5000] - Auto-advance interval in ms
2493
+ * @param {string} [props.height='300px'] - Carousel height
2494
+ * @param {number} [props.startIndex=0] - Initial slide index
2495
+ * @param {string} [props.className] - Additional CSS classes
2496
+ * @returns {Object} TACO object representing a carousel
2497
+ * @category Component Builders
2498
+ * @example
2499
+ * const carousel = makeCarousel({
2500
+ * items: [
2501
+ * { content: { t: 'img', a: { src: 'photo.jpg' } }, caption: 'Photo 1' },
2502
+ * { content: { t: 'div', c: 'Text slide' } }
2503
+ * ],
2504
+ * autoPlay: true,
2505
+ * interval: 3000
2506
+ * });
2507
+ */
2508
+ function makeCarousel(props = {}) {
2509
+ const {
2510
+ items = [],
2511
+ showControls = true,
2512
+ showIndicators = true,
2513
+ autoPlay = false,
2514
+ interval = 5000,
2515
+ height = '300px',
2516
+ startIndex = 0,
2517
+ className = ''
2518
+ } = props;
2519
+
2520
+ // Shared navigation logic
2521
+ function goToSlide(carouselEl, index) {
2522
+ var total = carouselEl.querySelectorAll('.bw_carousel_slide').length;
2523
+ if (index < 0) index = total - 1;
2524
+ if (index >= total) index = 0;
2525
+ carouselEl.setAttribute('data-carousel-index', index);
2526
+ var track = carouselEl.querySelector('.bw_carousel_track');
2527
+ track.style.transform = 'translateX(-' + (index * 100) + '%)';
2528
+ // Update indicators
2529
+ var indicators = carouselEl.querySelectorAll('.bw_carousel_indicator');
2530
+ for (var i = 0; i < indicators.length; i++) {
2531
+ if (i === index) {
2532
+ indicators[i].classList.add('active');
2533
+ } else {
2534
+ indicators[i].classList.remove('active');
2535
+ }
2536
+ }
2537
+ }
2538
+
2539
+ // Arrow SVGs (inline data URIs, same pattern as accordion chevrons)
2540
+ 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";
2541
+ 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";
2542
+
2543
+ var slides = items.map(function(item) {
2544
+ var slideContent = [
2545
+ item.content,
2546
+ item.caption && {
2547
+ t: 'div',
2548
+ a: { class: 'bw_carousel_caption' },
2549
+ c: item.caption
2550
+ }
2551
+ ].filter(Boolean);
2552
+
2553
+ return {
2554
+ t: 'div',
2555
+ a: { class: 'bw_carousel_slide' },
2556
+ c: slideContent.length === 1 ? slideContent[0] : slideContent
2557
+ };
2558
+ });
2559
+
2560
+ var children = [
2561
+ // Track
2562
+ {
2563
+ t: 'div',
2564
+ a: {
2565
+ class: 'bw_carousel_track',
2566
+ style: 'transform: translateX(-' + (startIndex * 100) + '%)'
2567
+ },
2568
+ c: slides
2569
+ }
2570
+ ];
2571
+
2572
+ // Prev/Next controls
2573
+ if (showControls && items.length > 1) {
2574
+ children.push({
2575
+ t: 'button',
2576
+ a: {
2577
+ class: 'bw_carousel_control bw_carousel_control_prev',
2578
+ type: 'button',
2579
+ 'aria-label': 'Previous slide',
2580
+ onclick: function(e) {
2581
+ var carousel = e.target.closest('.bw_carousel');
2582
+ var idx = parseInt(carousel.getAttribute('data-carousel-index') || '0');
2583
+ goToSlide(carousel, idx - 1);
2584
+ }
2585
+ },
2586
+ c: { t: 'img', a: { src: prevArrow, alt: '', role: 'presentation' } }
2587
+ });
2588
+ children.push({
2589
+ t: 'button',
2590
+ a: {
2591
+ class: 'bw_carousel_control bw_carousel_control_next',
2592
+ type: 'button',
2593
+ 'aria-label': 'Next slide',
2594
+ onclick: function(e) {
2595
+ var carousel = e.target.closest('.bw_carousel');
2596
+ var idx = parseInt(carousel.getAttribute('data-carousel-index') || '0');
2597
+ goToSlide(carousel, idx + 1);
2598
+ }
2599
+ },
2600
+ c: { t: 'img', a: { src: nextArrow, alt: '', role: 'presentation' } }
2601
+ });
2602
+ }
2603
+
2604
+ // Indicators
2605
+ if (showIndicators && items.length > 1) {
2606
+ children.push({
2607
+ t: 'div',
2608
+ a: { class: 'bw_carousel_indicators' },
2609
+ c: items.map(function(_, i) {
2610
+ return {
2611
+ t: 'button',
2612
+ a: {
2613
+ class: 'bw_carousel_indicator' + (i === startIndex ? ' active' : ''),
2614
+ type: 'button',
2615
+ 'aria-label': 'Go to slide ' + (i + 1),
2616
+ 'data-slide-index': i,
2617
+ onclick: function(e) {
2618
+ var carousel = e.target.closest('.bw_carousel');
2619
+ var idx = parseInt(e.target.getAttribute('data-slide-index'));
2620
+ goToSlide(carousel, idx);
2621
+ }
2622
+ }
2623
+ };
2624
+ })
2625
+ });
2626
+ }
2627
+
2628
+ return {
2629
+ t: 'div',
2630
+ a: {
2631
+ class: ('bw_carousel ' + className).trim(),
2632
+ style: 'height: ' + height,
2633
+ tabindex: '0',
2634
+ 'aria-roledescription': 'carousel',
2635
+ 'data-carousel-index': startIndex
2636
+ },
2637
+ c: children,
2638
+ o: {
2639
+ type: 'carousel',
2640
+ state: { activeIndex: startIndex, autoPlay: autoPlay, interval: interval },
2641
+ mounted: function(el) {
2642
+ // Keyboard navigation
2643
+ el.addEventListener('keydown', function(e) {
2644
+ var idx = parseInt(el.getAttribute('data-carousel-index') || '0');
2645
+ if (e.key === 'ArrowLeft') {
2646
+ e.preventDefault();
2647
+ goToSlide(el, idx - 1);
2648
+ } else if (e.key === 'ArrowRight') {
2649
+ e.preventDefault();
2650
+ goToSlide(el, idx + 1);
2651
+ }
2652
+ });
2653
+ // Auto-play
2654
+ if (autoPlay) {
2655
+ var intervalId = setInterval(function() {
2656
+ var idx = parseInt(el.getAttribute('data-carousel-index') || '0');
2657
+ goToSlide(el, idx + 1);
2658
+ }, interval);
2659
+ el._bw_carouselInterval = intervalId;
2660
+ // Pause on hover/focus for usability
2661
+ el.addEventListener('mouseenter', function() {
2662
+ if (el._bw_carouselInterval) clearInterval(el._bw_carouselInterval);
2663
+ });
2664
+ el.addEventListener('mouseleave', function() {
2665
+ el._bw_carouselInterval = setInterval(function() {
2666
+ var idx = parseInt(el.getAttribute('data-carousel-index') || '0');
2667
+ goToSlide(el, idx + 1);
2668
+ }, interval);
2669
+ });
2670
+ }
2671
+ },
2672
+ unmount: function(el) {
2673
+ if (el._bw_carouselInterval) {
2674
+ clearInterval(el._bw_carouselInterval);
2675
+ }
2676
+ }
2677
+ }
2678
+ };
2679
+ }
2680
+
2681
+ // =========================================================================
2682
+ // Phase 4: Dashboard & Data Display
2683
+ // =========================================================================
2684
+
2685
+ /**
2686
+ * Create a stat card for dashboard metrics display
2687
+ *
2688
+ * Shows a large value with a label and optional change indicator.
2689
+ * Designed for dashboard grid layouts with left-border accent.
2690
+ *
2691
+ * @param {Object|string} [props] - Stat card configuration (string shorthand sets label)
2692
+ * @param {string|number} [props.value=0] - The main stat value to display
2693
+ * @param {string} [props.label] - Descriptive label below the value
2694
+ * @param {number} [props.change] - Percentage change indicator (positive = green arrow, negative = red)
2695
+ * @param {string} [props.format] - Value format ("number", "currency", "percent")
2696
+ * @param {string} [props.prefix] - Custom prefix (e.g. "$")
2697
+ * @param {string} [props.suffix] - Custom suffix (e.g. "%")
2698
+ * @param {string} [props.icon] - Icon content (emoji or text) shown above value
2699
+ * @param {string} [props.variant] - Left-border color variant ("primary", "success", "danger", etc.)
2700
+ * @param {string} [props.className] - Additional CSS classes
2701
+ * @param {Object} [props.style] - Inline style object
2702
+ * @returns {Object} TACO object representing a stat card
2703
+ * @category Component Builders
2704
+ * @example
2705
+ * const stat = makeStatCard({
2706
+ * value: 2345,
2707
+ * label: 'Active Users',
2708
+ * change: 5.3,
2709
+ * format: 'number',
2710
+ * variant: 'primary'
2711
+ * });
2712
+ */
2713
+ function makeStatCard(props = {}) {
2714
+ if (typeof props === 'string') props = { label: props };
2715
+ var {
2716
+ value = 0,
2717
+ label,
2718
+ change,
2719
+ format,
2720
+ prefix,
2721
+ suffix,
2722
+ icon,
2723
+ variant,
2724
+ className = '',
2725
+ style
2726
+ } = props;
2727
+
2728
+ function formatValue(val, fmt) {
2729
+ if (prefix || suffix) return (prefix || '') + val + (suffix || '');
2730
+ switch (fmt) {
2731
+ case 'currency': return '$' + Number(val).toLocaleString();
2732
+ case 'percent': return val + '%';
2733
+ case 'number': return Number(val).toLocaleString();
2734
+ default: return '' + val;
2735
+ }
2736
+ }
2737
+
2738
+ var classes = [
2739
+ 'bw_stat_card',
2740
+ variantClass(variant),
2741
+ className
2742
+ ].filter(Boolean).join(' ').trim();
2743
+
2744
+ var children = [];
2745
+
2746
+ if (icon) {
2747
+ children.push({
2748
+ t: 'div',
2749
+ a: { class: 'bw_stat_icon' },
2750
+ c: icon
2751
+ });
2752
+ }
2753
+
2754
+ children.push({
2755
+ t: 'div',
2756
+ a: { class: 'bw_stat_value' },
2757
+ c: formatValue(value, format)
2758
+ });
2759
+
2760
+ if (label) {
2761
+ children.push({
2762
+ t: 'div',
2763
+ a: { class: 'bw_stat_label' },
2764
+ c: label
2765
+ });
2766
+ }
2767
+
2768
+ if (change !== undefined && change !== null) {
2769
+ children.push({
2770
+ t: 'div',
2771
+ a: {
2772
+ class: 'bw_stat_change ' + (change >= 0 ? 'bw_stat_change_up' : 'bw_stat_change_down')
2773
+ },
2774
+ c: (change >= 0 ? '\u2191 +' : '\u2193 ') + change + '%'
2775
+ });
2776
+ }
2777
+
2778
+ return {
2779
+ t: 'div',
2780
+ a: { class: classes, style: style },
2781
+ c: children,
2782
+ o: { type: 'stat-card' }
2783
+ };
2784
+ }
2785
+
2786
+ // =========================================================================
2787
+ // Phase 5: Overlays & Popovers
2788
+ // =========================================================================
2789
+
2790
+ /**
2791
+ * Create a tooltip wrapper around trigger content
2792
+ *
2793
+ * Wraps the trigger element in a container that shows tooltip text
2794
+ * on hover and focus. Pure CSS-driven show/hide with JS lifecycle
2795
+ * for event binding.
2796
+ *
2797
+ * @param {Object} [props] - Tooltip configuration
2798
+ * @param {string|Object|Array} [props.content] - Trigger content (what the user hovers/focuses)
2799
+ * @param {string} [props.text=""] - Tooltip text to display
2800
+ * @param {string} [props.placement="top"] - Tooltip placement ("top", "bottom", "left", "right")
2801
+ * @param {string} [props.className] - Additional CSS classes
2802
+ * @returns {Object} TACO object representing a tooltip wrapper
2803
+ * @category Component Builders
2804
+ * @example
2805
+ * const tip = makeTooltip({
2806
+ * content: makeButton({ text: 'Hover me' }),
2807
+ * text: 'This is a tooltip!',
2808
+ * placement: 'top'
2809
+ * });
2810
+ */
2811
+ function makeTooltip(props = {}) {
2812
+ var {
2813
+ content,
2814
+ text = '',
2815
+ placement = 'top',
2816
+ className = ''
2817
+ } = props;
2818
+
2819
+ return {
2820
+ t: 'span',
2821
+ a: { class: ('bw_tooltip_wrapper ' + className).trim() },
2822
+ c: [
2823
+ content,
2824
+ {
2825
+ t: 'span',
2826
+ a: {
2827
+ class: 'bw_tooltip bw_tooltip_' + placement,
2828
+ role: 'tooltip'
2829
+ },
2830
+ c: text
2831
+ }
2832
+ ],
2833
+ o: {
2834
+ type: 'tooltip',
2835
+ mounted: function(el) {
2836
+ var tip = el.querySelector('.bw_tooltip');
2837
+ el.addEventListener('mouseenter', function() {
2838
+ tip.classList.add('bw_tooltip_show');
2839
+ });
2840
+ el.addEventListener('mouseleave', function() {
2841
+ tip.classList.remove('bw_tooltip_show');
2842
+ });
2843
+ el.addEventListener('focusin', function() {
2844
+ tip.classList.add('bw_tooltip_show');
2845
+ });
2846
+ el.addEventListener('focusout', function() {
2847
+ tip.classList.remove('bw_tooltip_show');
2848
+ });
2849
+ }
2850
+ }
2851
+ };
2852
+ }
2853
+
2854
+ /**
2855
+ * Create a popover wrapper around trigger content
2856
+ *
2857
+ * Like a tooltip but richer — supports title + body content and is
2858
+ * triggered by click rather than hover. Dismisses on click outside.
2859
+ *
2860
+ * @param {Object} [props] - Popover configuration
2861
+ * @param {string|Object|Array} [props.trigger] - Trigger content (what the user clicks)
2862
+ * @param {string} [props.title] - Popover header title
2863
+ * @param {string|Object|Array} [props.content] - Popover body content
2864
+ * @param {string} [props.placement="top"] - Placement ("top", "bottom", "left", "right")
2865
+ * @param {string} [props.className] - Additional CSS classes
2866
+ * @returns {Object} TACO object representing a popover wrapper
2867
+ * @category Component Builders
2868
+ * @example
2869
+ * const pop = makePopover({
2870
+ * trigger: makeButton({ text: 'Click me' }),
2871
+ * title: 'Popover Title',
2872
+ * content: 'Some helpful information here.',
2873
+ * placement: 'bottom'
2874
+ * });
2875
+ */
2876
+ function makePopover(props = {}) {
2877
+ var {
2878
+ trigger,
2879
+ title,
2880
+ content,
2881
+ placement = 'top',
2882
+ className = ''
2883
+ } = props;
2884
+
2885
+ var popoverContent = [
2886
+ title && {
2887
+ t: 'div',
2888
+ a: { class: 'bw_popover_header' },
2889
+ c: title
2890
+ },
2891
+ content && {
2892
+ t: 'div',
2893
+ a: { class: 'bw_popover_body' },
2894
+ c: content
2895
+ }
2896
+ ].filter(Boolean);
2897
+
2898
+ return {
2899
+ t: 'span',
2900
+ a: { class: ('bw_popover_wrapper ' + className).trim() },
2901
+ c: [
2902
+ {
2903
+ t: 'span',
2904
+ a: {
2905
+ class: 'bw_popover_trigger',
2906
+ onclick: function(e) {
2907
+ var wrapper = e.target.closest('.bw_popover_wrapper');
2908
+ var pop = wrapper.querySelector('.bw_popover');
2909
+ pop.classList.toggle('bw_popover_show');
2910
+ }
2911
+ },
2912
+ c: trigger
2913
+ },
2914
+ {
2915
+ t: 'div',
2916
+ a: {
2917
+ class: 'bw_popover bw_popover_' + placement
2918
+ },
2919
+ c: popoverContent
2920
+ }
2921
+ ],
2922
+ o: {
2923
+ type: 'popover',
2924
+ mounted: function(el) {
2925
+ // Click outside to close
2926
+ var outsideHandler = function(e) {
2927
+ if (!el.contains(e.target)) {
2928
+ var pop = el.querySelector('.bw_popover');
2929
+ if (pop) pop.classList.remove('bw_popover_show');
2930
+ }
2931
+ };
2932
+ document.addEventListener('click', outsideHandler);
2933
+ el._bw_outsideHandler = outsideHandler;
2934
+ },
2935
+ unmount: function(el) {
2936
+ if (el._bw_outsideHandler) {
2937
+ document.removeEventListener('click', el._bw_outsideHandler);
2938
+ }
2939
+ }
2940
+ }
2941
+ };
2942
+ }
2943
+
2944
+ // =========================================================================
2945
+ // Phase 6: Form Enhancements & Layout
2946
+ // =========================================================================
2947
+
2948
+ /**
2949
+ * Create a search input with clear button
2950
+ *
2951
+ * Wraps a text input with a clear (×) button that appears when
2952
+ * the field has content. Calls onSearch on Enter key.
2953
+ *
2954
+ * @param {Object} [props] - Search input configuration
2955
+ * @param {string} [props.placeholder="Search..."] - Placeholder text
2956
+ * @param {string} [props.value] - Initial value
2957
+ * @param {Function} [props.onSearch] - Callback when Enter is pressed, receives value
2958
+ * @param {Function} [props.onInput] - Callback on each keystroke, receives value
2959
+ * @param {string} [props.id] - Element ID
2960
+ * @param {string} [props.name] - Input name attribute
2961
+ * @param {string} [props.className] - Additional CSS classes
2962
+ * @returns {Object} TACO object representing a search input
2963
+ * @category Component Builders
2964
+ * @example
2965
+ * const search = makeSearchInput({
2966
+ * placeholder: 'Search users...',
2967
+ * onSearch: (val) => filterUsers(val)
2968
+ * });
2969
+ */
2970
+ function makeSearchInput(props = {}) {
2971
+ if (typeof props === 'string') props = { placeholder: props };
2972
+ var {
2973
+ placeholder = 'Search...',
2974
+ value,
2975
+ onSearch,
2976
+ onInput,
2977
+ id,
2978
+ name,
2979
+ className = ''
2980
+ } = props;
2981
+
2982
+ return {
2983
+ t: 'div',
2984
+ a: { class: ('bw_search_input ' + className).trim() },
2985
+ c: [
2986
+ {
2987
+ t: 'input',
2988
+ a: {
2989
+ type: 'search',
2990
+ class: 'bw_form_control bw_search_field',
2991
+ placeholder: placeholder,
2992
+ value: value,
2993
+ id: id,
2994
+ name: name,
2995
+ onkeydown: function(e) {
2996
+ if (e.key === 'Enter' && onSearch) {
2997
+ e.preventDefault();
2998
+ onSearch(e.target.value);
2999
+ }
3000
+ },
3001
+ oninput: function(e) {
3002
+ var wrapper = e.target.closest('.bw_search_input');
3003
+ var clearBtn = wrapper.querySelector('.bw_search_clear');
3004
+ if (clearBtn) {
3005
+ clearBtn.style.display = e.target.value ? 'flex' : 'none';
3006
+ }
3007
+ if (onInput) onInput(e.target.value);
3008
+ }
3009
+ }
3010
+ },
3011
+ {
3012
+ t: 'button',
3013
+ a: {
3014
+ type: 'button',
3015
+ class: 'bw_search_clear',
3016
+ 'aria-label': 'Clear search',
3017
+ style: value ? undefined : 'display: none',
3018
+ onclick: function(e) {
3019
+ var wrapper = e.target.closest('.bw_search_input');
3020
+ var input = wrapper.querySelector('.bw_search_field');
3021
+ input.value = '';
3022
+ e.target.style.display = 'none';
3023
+ input.focus();
3024
+ if (onInput) onInput('');
3025
+ if (onSearch) onSearch('');
3026
+ }
3027
+ },
3028
+ c: '\u00D7'
3029
+ }
3030
+ ],
3031
+ o: { type: 'search-input' }
3032
+ };
3033
+ }
3034
+
3035
+ /**
3036
+ * Create a styled range slider input
3037
+ *
3038
+ * @param {Object} [props] - Range configuration
3039
+ * @param {number} [props.min=0] - Minimum value
3040
+ * @param {number} [props.max=100] - Maximum value
3041
+ * @param {number} [props.step=1] - Step increment
3042
+ * @param {number} [props.value=50] - Current value
3043
+ * @param {string} [props.label] - Label text
3044
+ * @param {boolean} [props.showValue=false] - Show current value display
3045
+ * @param {string} [props.id] - Element ID
3046
+ * @param {string} [props.name] - Input name attribute
3047
+ * @param {boolean} [props.disabled=false] - Whether the slider is disabled
3048
+ * @param {string} [props.className] - Additional CSS classes
3049
+ * @returns {Object} TACO object representing a range input
3050
+ * @category Component Builders
3051
+ * @example
3052
+ * const slider = makeRange({
3053
+ * min: 0, max: 100, value: 50,
3054
+ * label: 'Volume',
3055
+ * showValue: true,
3056
+ * oninput: (e) => setVolume(e.target.value)
3057
+ * });
3058
+ */
3059
+ function makeRange(props = {}) {
3060
+ var {
3061
+ min = 0,
3062
+ max = 100,
3063
+ step = 1,
3064
+ value = 50,
3065
+ label,
3066
+ showValue = false,
3067
+ id,
3068
+ name,
3069
+ disabled = false,
3070
+ className = '',
3071
+ ...eventHandlers
3072
+ } = props;
3073
+
3074
+ var children = [];
3075
+
3076
+ if (label || showValue) {
3077
+ var labelContent = [];
3078
+ if (label) {
3079
+ labelContent.push({
3080
+ t: 'span',
3081
+ c: label
3082
+ });
3083
+ }
3084
+ if (showValue) {
3085
+ labelContent.push({
3086
+ t: 'span',
3087
+ a: { class: 'bw_range_value' },
3088
+ c: '' + value
3089
+ });
3090
+ }
3091
+ children.push({
3092
+ t: 'div',
3093
+ a: { class: 'bw_range_label' },
3094
+ c: labelContent
3095
+ });
3096
+ }
3097
+
3098
+ // Wrap oninput to update value display
3099
+ var userOnInput = eventHandlers.oninput;
3100
+ if (showValue) {
3101
+ eventHandlers.oninput = function(e) {
3102
+ var wrapper = e.target.closest('.bw_range_wrapper');
3103
+ var valDisplay = wrapper.querySelector('.bw_range_value');
3104
+ if (valDisplay) valDisplay.textContent = e.target.value;
3105
+ if (userOnInput) userOnInput(e);
3106
+ };
3107
+ }
3108
+
3109
+ children.push({
3110
+ t: 'input',
3111
+ a: {
3112
+ type: 'range',
3113
+ class: 'bw_range',
3114
+ min: min,
3115
+ max: max,
3116
+ step: step,
3117
+ value: value,
3118
+ id: id,
3119
+ name: name,
3120
+ disabled: disabled,
3121
+ ...eventHandlers
3122
+ }
3123
+ });
3124
+
3125
+ return {
3126
+ t: 'div',
3127
+ a: { class: ('bw_range_wrapper ' + className).trim() },
3128
+ c: children,
3129
+ o: { type: 'range' }
3130
+ };
3131
+ }
3132
+
3133
+ /**
3134
+ * Create a media object layout (image + text side-by-side)
3135
+ *
3136
+ * Classic media object pattern: image/icon on one side, text content
3137
+ * on the other, using flexbox. Supports reversed layout.
3138
+ *
3139
+ * @param {Object} [props] - Media object configuration
3140
+ * @param {string} [props.src] - Image source URL
3141
+ * @param {string} [props.alt=""] - Image alt text
3142
+ * @param {string} [props.title] - Title text
3143
+ * @param {string|Object|Array} [props.content] - Body content
3144
+ * @param {boolean} [props.reverse=false] - Put image on the right
3145
+ * @param {string} [props.imageSize="3rem"] - Image width/height
3146
+ * @param {string} [props.className] - Additional CSS classes
3147
+ * @returns {Object} TACO object representing a media object
3148
+ * @category Component Builders
3149
+ * @example
3150
+ * const media = makeMediaObject({
3151
+ * src: '/avatar.jpg',
3152
+ * title: 'Jane Doe',
3153
+ * content: 'Posted a comment 5 minutes ago.'
3154
+ * });
3155
+ */
3156
+ function makeMediaObject(props = {}) {
3157
+ var {
3158
+ src,
3159
+ alt = '',
3160
+ title,
3161
+ content,
3162
+ reverse = false,
3163
+ imageSize = '3rem',
3164
+ className = ''
3165
+ } = props;
3166
+
3167
+ var imgEl = src ? {
3168
+ t: 'img',
3169
+ a: {
3170
+ class: 'bw_media_img',
3171
+ src: src,
3172
+ alt: alt,
3173
+ style: 'width:' + imageSize + ';height:' + imageSize
3174
+ }
3175
+ } : null;
3176
+
3177
+ var bodyEl = {
3178
+ t: 'div',
3179
+ a: { class: 'bw_media_body' },
3180
+ c: [
3181
+ title && { t: 'h5', a: { class: 'bw_media_title' }, c: title },
3182
+ content
3183
+ ].filter(Boolean)
3184
+ };
3185
+
3186
+ return {
3187
+ t: 'div',
3188
+ a: { class: ('bw_media ' + (reverse ? 'bw_media_reverse ' : '') + className).trim() },
3189
+ c: reverse
3190
+ ? [bodyEl, imgEl].filter(Boolean)
3191
+ : [imgEl, bodyEl].filter(Boolean),
3192
+ o: { type: 'media-object' }
3193
+ };
3194
+ }
3195
+
3196
+ /**
3197
+ * Create a file upload zone with drag-and-drop support
3198
+ *
3199
+ * Styled drop zone with file input. Supports drag-and-drop visuals
3200
+ * and multiple file selection.
3201
+ *
3202
+ * @param {Object} [props] - File upload configuration
3203
+ * @param {string} [props.accept] - Accepted file types (e.g. "image/*", ".pdf,.doc")
3204
+ * @param {boolean} [props.multiple=false] - Allow multiple file selection
3205
+ * @param {Function} [props.onFiles] - Callback when files are selected, receives FileList
3206
+ * @param {string} [props.text="Drop files here or click to browse"] - Zone label text
3207
+ * @param {string} [props.id] - Element ID
3208
+ * @param {string} [props.className] - Additional CSS classes
3209
+ * @returns {Object} TACO object representing a file upload zone
3210
+ * @category Component Builders
3211
+ * @example
3212
+ * const upload = makeFileUpload({
3213
+ * accept: 'image/*',
3214
+ * multiple: true,
3215
+ * onFiles: (files) => uploadFiles(files)
3216
+ * });
3217
+ */
3218
+ function makeFileUpload(props = {}) {
3219
+ var {
3220
+ accept,
3221
+ multiple = false,
3222
+ onFiles,
3223
+ text = 'Drop files here or click to browse',
3224
+ id,
3225
+ className = ''
3226
+ } = props;
3227
+
3228
+ return {
3229
+ t: 'div',
3230
+ a: {
3231
+ class: ('bw_file_upload ' + className).trim(),
3232
+ tabindex: '0',
3233
+ role: 'button',
3234
+ 'aria-label': text
3235
+ },
3236
+ c: [
3237
+ { t: 'div', a: { class: 'bw_file_upload_icon' }, c: '\uD83D\uDCC1' },
3238
+ { t: 'div', a: { class: 'bw_file_upload_text' }, c: text },
3239
+ {
3240
+ t: 'input',
3241
+ a: {
3242
+ type: 'file',
3243
+ class: 'bw_file_upload_input',
3244
+ accept: accept,
3245
+ multiple: multiple,
3246
+ id: id,
3247
+ onchange: function(e) {
3248
+ if (onFiles && e.target.files.length) onFiles(e.target.files);
3249
+ }
3250
+ }
3251
+ }
3252
+ ],
3253
+ o: {
3254
+ type: 'file-upload',
3255
+ mounted: function(el) {
3256
+ var input = el.querySelector('.bw_file_upload_input');
3257
+
3258
+ // Click zone to trigger file input
3259
+ el.addEventListener('click', function(e) {
3260
+ if (e.target !== input) input.click();
3261
+ });
3262
+
3263
+ // Keyboard activation
3264
+ el.addEventListener('keydown', function(e) {
3265
+ if (e.key === 'Enter' || e.key === ' ') {
3266
+ e.preventDefault();
3267
+ input.click();
3268
+ }
3269
+ });
3270
+
3271
+ // Drag-and-drop visuals
3272
+ el.addEventListener('dragover', function(e) {
3273
+ e.preventDefault();
3274
+ el.classList.add('bw_file_upload_active');
3275
+ });
3276
+ el.addEventListener('dragleave', function() {
3277
+ el.classList.remove('bw_file_upload_active');
3278
+ });
3279
+ el.addEventListener('drop', function(e) {
3280
+ e.preventDefault();
3281
+ el.classList.remove('bw_file_upload_active');
3282
+ if (onFiles && e.dataTransfer.files.length) onFiles(e.dataTransfer.files);
3283
+ });
3284
+ }
3285
+ }
3286
+ };
3287
+ }
3288
+
3289
+ // =========================================================================
3290
+ // Phase 7: Data Display & Workflow
3291
+ // =========================================================================
3292
+
3293
+ /**
3294
+ * Create a vertical timeline for chronological event display
3295
+ *
3296
+ * Renders events as a vertical line with markers and content cards.
3297
+ * Each item can have a colored variant marker.
3298
+ *
3299
+ * @param {Object} [props] - Timeline configuration
3300
+ * @param {Array<Object>} [props.items=[]] - Timeline events
3301
+ * @param {string} [props.items[].title] - Event title
3302
+ * @param {string|Object|Array} [props.items[].content] - Event description content
3303
+ * @param {string} [props.items[].date] - Date or time label
3304
+ * @param {string} [props.items[].variant="primary"] - Marker color variant
3305
+ * @param {string} [props.className] - Additional CSS classes
3306
+ * @returns {Object} TACO object representing a timeline
3307
+ * @category Component Builders
3308
+ * @example
3309
+ * const timeline = makeTimeline({
3310
+ * items: [
3311
+ * { title: 'Project Started', date: 'Jan 2026', variant: 'primary' },
3312
+ * { title: 'Beta Release', date: 'Mar 2026', content: 'v2.0 beta shipped' },
3313
+ * { title: 'Stable Release', date: 'Jun 2026', variant: 'success' }
3314
+ * ]
3315
+ * });
3316
+ */
3317
+ function makeTimeline(props = {}) {
3318
+ var {
3319
+ items = [],
3320
+ className = ''
3321
+ } = props;
3322
+
3323
+ return {
3324
+ t: 'div',
3325
+ a: { class: ('bw_timeline ' + className).trim() },
3326
+ c: items.map(function(item) {
3327
+ return {
3328
+ t: 'div',
3329
+ a: { class: 'bw_timeline_item' },
3330
+ c: [
3331
+ {
3332
+ t: 'div',
3333
+ a: { class: 'bw_timeline_marker ' + variantClass(item.variant || 'primary') }
3334
+ },
3335
+ {
3336
+ t: 'div',
3337
+ a: { class: 'bw_timeline_content' },
3338
+ c: [
3339
+ item.date && {
3340
+ t: 'div',
3341
+ a: { class: 'bw_timeline_date' },
3342
+ c: item.date
3343
+ },
3344
+ item.title && {
3345
+ t: 'h5',
3346
+ a: { class: 'bw_timeline_title' },
3347
+ c: item.title
3348
+ },
3349
+ item.content && (typeof item.content === 'string'
3350
+ ? { t: 'p', a: { class: 'bw_timeline_text' }, c: item.content }
3351
+ : item.content)
3352
+ ].filter(Boolean)
3353
+ }
3354
+ ]
3355
+ };
3356
+ }),
3357
+ o: { type: 'timeline' }
3358
+ };
3359
+ }
3360
+
3361
+ /**
3362
+ * Create a multi-step wizard/progress indicator
3363
+ *
3364
+ * Displays numbered steps with active and completed states.
3365
+ * Steps before currentStep are marked completed, the currentStep
3366
+ * is active, and subsequent steps are pending.
3367
+ *
3368
+ * @param {Object} [props] - Stepper configuration
3369
+ * @param {Array<Object>} [props.steps=[]] - Step definitions
3370
+ * @param {string} [props.steps[].label] - Step label text
3371
+ * @param {string} [props.steps[].description] - Optional step description
3372
+ * @param {number} [props.currentStep=0] - Zero-based index of the active step
3373
+ * @param {string} [props.className] - Additional CSS classes
3374
+ * @returns {Object} TACO object representing a stepper
3375
+ * @category Component Builders
3376
+ * @example
3377
+ * const stepper = makeStepper({
3378
+ * currentStep: 1,
3379
+ * steps: [
3380
+ * { label: 'Account', description: 'Create account' },
3381
+ * { label: 'Profile', description: 'Set up profile' },
3382
+ * { label: 'Confirm', description: 'Review & submit' }
3383
+ * ]
3384
+ * });
3385
+ */
3386
+ function makeStepper(props = {}) {
3387
+ var {
3388
+ steps = [],
3389
+ currentStep = 0,
3390
+ className = ''
3391
+ } = props;
3392
+
3393
+ return {
3394
+ t: 'div',
3395
+ a: { class: ('bw_stepper ' + className).trim(), role: 'list' },
3396
+ c: steps.map(function(step, index) {
3397
+ var state = index < currentStep ? 'completed' : index === currentStep ? 'active' : 'pending';
3398
+ return {
3399
+ t: 'div',
3400
+ a: {
3401
+ class: 'bw_step bw_step_' + state,
3402
+ role: 'listitem',
3403
+ 'aria-current': state === 'active' ? 'step' : undefined
3404
+ },
3405
+ c: [
3406
+ {
3407
+ t: 'div',
3408
+ a: { class: 'bw_step_indicator' },
3409
+ c: state === 'completed' ? '\u2713' : '' + (index + 1)
3410
+ },
3411
+ {
3412
+ t: 'div',
3413
+ a: { class: 'bw_step_body' },
3414
+ c: [
3415
+ { t: 'div', a: { class: 'bw_step_label' }, c: step.label },
3416
+ step.description && { t: 'div', a: { class: 'bw_step_description' }, c: step.description }
3417
+ ].filter(Boolean)
3418
+ }
3419
+ ]
3420
+ };
3421
+ }),
3422
+ o: { type: 'stepper' }
3423
+ };
3424
+ }
3425
+
3426
+ /**
3427
+ * Create a chip/tag input for managing a list of items
3428
+ *
3429
+ * Displays existing chips with remove buttons and an input field
3430
+ * for adding new ones. Chips are added on Enter and removed on
3431
+ * clicking the × button.
3432
+ *
3433
+ * @param {Object} [props] - Chip input configuration
3434
+ * @param {Array<string>} [props.chips=[]] - Initial chip values
3435
+ * @param {string} [props.placeholder="Add..."] - Input placeholder text
3436
+ * @param {Function} [props.onAdd] - Callback when a chip is added, receives value
3437
+ * @param {Function} [props.onRemove] - Callback when a chip is removed, receives value
3438
+ * @param {string} [props.className] - Additional CSS classes
3439
+ * @returns {Object} TACO object representing a chip input
3440
+ * @category Component Builders
3441
+ * @example
3442
+ * const tags = makeChipInput({
3443
+ * chips: ['JavaScript', 'CSS'],
3444
+ * placeholder: 'Add tag...',
3445
+ * onAdd: (val) => addTag(val),
3446
+ * onRemove: (val) => removeTag(val)
3447
+ * });
3448
+ */
3449
+ function makeChipInput(props = {}) {
3450
+ var {
3451
+ chips = [],
3452
+ placeholder = 'Add...',
3453
+ onAdd,
3454
+ onRemove,
3455
+ className = ''
3456
+ } = props;
3457
+
3458
+ function makeChipEl(text) {
3459
+ return {
3460
+ t: 'span',
3461
+ a: { class: 'bw_chip', 'data-chip-value': text },
3462
+ c: [
3463
+ text,
3464
+ {
3465
+ t: 'button',
3466
+ a: {
3467
+ type: 'button',
3468
+ class: 'bw_chip_remove',
3469
+ 'aria-label': 'Remove ' + text,
3470
+ onclick: function(e) {
3471
+ var chip = e.target.closest('.bw_chip');
3472
+ var val = chip.getAttribute('data-chip-value');
3473
+ chip.parentNode.removeChild(chip);
3474
+ if (onRemove) onRemove(val);
3475
+ }
3476
+ },
3477
+ c: '\u00D7'
3478
+ }
3479
+ ]
3480
+ };
3481
+ }
3482
+
3483
+ return {
3484
+ t: 'div',
3485
+ a: { class: ('bw_chip_input ' + className).trim() },
3486
+ c: [
3487
+ ...chips.map(makeChipEl),
3488
+ {
3489
+ t: 'input',
3490
+ a: {
3491
+ type: 'text',
3492
+ class: 'bw_chip_field',
3493
+ placeholder: placeholder,
3494
+ onkeydown: function(e) {
3495
+ if (e.key === 'Enter' && e.target.value.trim()) {
3496
+ e.preventDefault();
3497
+ var val = e.target.value.trim();
3498
+ var wrapper = e.target.closest('.bw_chip_input');
3499
+ // Insert chip before the input
3500
+ var chipEl = document.createElement('span');
3501
+ chipEl.className = 'bw_chip';
3502
+ chipEl.setAttribute('data-chip-value', val);
3503
+ chipEl.innerHTML = '';
3504
+ chipEl.textContent = val;
3505
+ var removeBtn = document.createElement('button');
3506
+ removeBtn.type = 'button';
3507
+ removeBtn.className = 'bw_chip_remove';
3508
+ removeBtn.setAttribute('aria-label', 'Remove ' + val);
3509
+ removeBtn.textContent = '\u00D7';
3510
+ removeBtn.onclick = function() {
3511
+ chipEl.parentNode.removeChild(chipEl);
3512
+ if (onRemove) onRemove(val);
3513
+ };
3514
+ chipEl.appendChild(removeBtn);
3515
+ wrapper.insertBefore(chipEl, e.target);
3516
+ e.target.value = '';
3517
+ if (onAdd) onAdd(val);
3518
+ }
3519
+ // Backspace on empty input removes last chip
3520
+ if (e.key === 'Backspace' && !e.target.value) {
3521
+ var wrapper = e.target.closest('.bw_chip_input');
3522
+ var chipEls = wrapper.querySelectorAll('.bw_chip');
3523
+ if (chipEls.length) {
3524
+ var last = chipEls[chipEls.length - 1];
3525
+ var removedVal = last.getAttribute('data-chip-value');
3526
+ last.parentNode.removeChild(last);
3527
+ if (onRemove) onRemove(removedVal);
3528
+ }
3529
+ }
3530
+ }
3531
+ }
3532
+ }
3533
+ ],
3534
+ o: { type: 'chip-input' }
3535
+ };
3536
+ }
3537
+
3538
+ // componentHandles registry removed in v2.0.15.
3539
+ // See dev/dead-code-elimination-v2.0.15.md for recovery.
3540
+
3541
+ // =========================================================================
3542
+ // BCCL Component Registry
3543
+ //
3544
+ // Single registry mapping type names to their factory functions.
3545
+ // Enables bw.make('card', props) dispatch and introspection via
3546
+ // Object.keys(BCCL).
3547
+ // =========================================================================
3548
+
3549
+ /**
3550
+ * BCCL component registry — maps component type names to factory functions.
3551
+ * Each entry's `make` function is the corresponding exported makeXxx().
3552
+ *
3553
+ * @type {Object.<string, {make: Function}>}
3554
+ */
3555
+ var BCCL = {
3556
+ card: { make: makeCard },
3557
+ button: { make: makeButton },
3558
+ container: { make: makeContainer },
3559
+ row: { make: makeRow },
3560
+ col: { make: makeCol },
3561
+ nav: { make: makeNav },
3562
+ navbar: { make: makeNavbar },
3563
+ tabs: { make: makeTabs },
3564
+ alert: { make: makeAlert },
3565
+ badge: { make: makeBadge },
3566
+ progress: { make: makeProgress },
3567
+ listGroup: { make: makeListGroup },
3568
+ breadcrumb: { make: makeBreadcrumb },
3569
+ form: { make: makeForm },
3570
+ formGroup: { make: makeFormGroup },
3571
+ input: { make: makeInput },
3572
+ textarea: { make: makeTextarea },
3573
+ select: { make: makeSelect },
3574
+ checkbox: { make: makeCheckbox },
3575
+ stack: { make: makeStack },
3576
+ spinner: { make: makeSpinner },
3577
+ hero: { make: makeHero },
3578
+ featureGrid: { make: makeFeatureGrid },
3579
+ cta: { make: makeCTA },
3580
+ section: { make: makeSection },
3581
+ codeDemo: { make: makeCodeDemo },
3582
+ pagination: { make: makePagination },
3583
+ radio: { make: makeRadio },
3584
+ buttonGroup: { make: makeButtonGroup },
3585
+ accordion: { make: makeAccordion },
3586
+ modal: { make: makeModal },
3587
+ toast: { make: makeToast },
3588
+ dropdown: { make: makeDropdown },
3589
+ switch: { make: makeSwitch },
3590
+ skeleton: { make: makeSkeleton },
3591
+ avatar: { make: makeAvatar },
3592
+ carousel: { make: makeCarousel },
3593
+ statCard: { make: makeStatCard },
3594
+ tooltip: { make: makeTooltip },
3595
+ popover: { make: makePopover },
3596
+ searchInput: { make: makeSearchInput },
3597
+ range: { make: makeRange },
3598
+ mediaObject: { make: makeMediaObject },
3599
+ fileUpload: { make: makeFileUpload },
3600
+ timeline: { make: makeTimeline },
3601
+ stepper: { make: makeStepper },
3602
+ chipInput: { make: makeChipInput }
3603
+ };
3604
+
3605
+ /**
3606
+ * Factory function — create any BCCL component by type name.
3607
+ *
3608
+ * @param {string} type - Component type (e.g. 'card', 'button', 'alert')
3609
+ * @param {Object} [props] - Component properties
3610
+ * @returns {Object} TACO object
3611
+ * @throws {Error} If type is not found in the registry
3612
+ * @example
3613
+ * var card = make('card', { title: 'Hello', variant: 'primary' });
3614
+ * var btn = make('button', { text: 'Click', variant: 'success' });
3615
+ * var types = Object.keys(BCCL); // list all available types
3616
+ */
3617
+ function make(type, props) {
3618
+ var def = BCCL[type];
3619
+ if (!def) throw new Error('bw.make: unknown component type "' + type + '". Available: ' + Object.keys(BCCL).join(', '));
3620
+ var taco = def.make(props || {});
3621
+ if (taco && typeof taco === 'object') {
3622
+ taco._bwFactory = { type: type, props: props || {} };
3623
+ }
3624
+ return taco;
3625
+ }
3626
+
3627
+ var components = /*#__PURE__*/Object.freeze({
3628
+ __proto__: null,
3629
+ BCCL: BCCL,
3630
+ make: make,
3631
+ makeAccordion: makeAccordion,
3632
+ makeAlert: makeAlert,
3633
+ makeAvatar: makeAvatar,
3634
+ makeBadge: makeBadge,
3635
+ makeBreadcrumb: makeBreadcrumb,
3636
+ makeButton: makeButton,
3637
+ makeButtonGroup: makeButtonGroup,
3638
+ makeCTA: makeCTA,
3639
+ makeCard: makeCard,
3640
+ makeCarousel: makeCarousel,
3641
+ makeCheckbox: makeCheckbox,
3642
+ makeChipInput: makeChipInput,
3643
+ makeCodeDemo: makeCodeDemo,
3644
+ makeCol: makeCol,
3645
+ makeContainer: makeContainer,
3646
+ makeDropdown: makeDropdown,
3647
+ makeFeatureGrid: makeFeatureGrid,
3648
+ makeFileUpload: makeFileUpload,
3649
+ makeForm: makeForm,
3650
+ makeFormGroup: makeFormGroup,
3651
+ makeHero: makeHero,
3652
+ makeInput: makeInput,
3653
+ makeListGroup: makeListGroup,
3654
+ makeMediaObject: makeMediaObject,
3655
+ makeModal: makeModal,
3656
+ makeNav: makeNav,
3657
+ makeNavbar: makeNavbar,
3658
+ makePagination: makePagination,
3659
+ makePopover: makePopover,
3660
+ makeProgress: makeProgress,
3661
+ makeRadio: makeRadio,
3662
+ makeRange: makeRange,
3663
+ makeRow: makeRow,
3664
+ makeSearchInput: makeSearchInput,
3665
+ makeSection: makeSection,
3666
+ makeSelect: makeSelect,
3667
+ makeSkeleton: makeSkeleton,
3668
+ makeSpinner: makeSpinner,
3669
+ makeStack: makeStack,
3670
+ makeStatCard: makeStatCard,
3671
+ makeStepper: makeStepper,
3672
+ makeSwitch: makeSwitch,
3673
+ makeTabs: makeTabs,
3674
+ makeTextarea: makeTextarea,
3675
+ makeTimeline: makeTimeline,
3676
+ makeToast: makeToast,
3677
+ makeTooltip: makeTooltip,
3678
+ variantClass: variantClass
3679
+ });
3680
+
3681
+ /**
3682
+ * bitwrench-bccl-entry.js — Standalone entry point for BCCL component library.
3683
+ *
3684
+ * Use this alongside bitwrench-lean when you want the core library and
3685
+ * BCCL components as separate files. The UMD build auto-registers all
3686
+ * make*() functions onto the global `bw` object if present.
3687
+ *
3688
+ * Usage (browser):
3689
+ * <script src="bitwrench-lean.umd.min.js"></script>
3690
+ * <script src="bitwrench-bccl.umd.min.js"></script>
3691
+ *
3692
+ * Usage (ESM):
3693
+ * import bw from 'bitwrench/lean';
3694
+ * import { registerBCCL } from 'bitwrench/bccl';
3695
+ * registerBCCL(bw);
3696
+ *
3697
+ * @module bitwrench-bccl
3698
+ * @license BSD-2-Clause
3699
+ */
3700
+
3701
+
3702
+ /**
3703
+ * Register all BCCL components onto a bitwrench instance.
3704
+ * Called automatically in UMD builds when `bw` is a global.
3705
+ *
3706
+ * @param {Object} bw - The bitwrench instance to register on
3707
+ */
3708
+ function registerBCCL(bw) {
3709
+ if (!bw) return;
3710
+
3711
+ // Register all make* functions
3712
+ Object.entries(components).forEach(function(entry) {
3713
+ var name = entry[0], fn = entry[1];
3714
+ if (name.indexOf('make') === 0) {
3715
+ bw[name] = fn;
3716
+ }
3717
+ });
3718
+
3719
+ // Factory dispatch: bw.make('card', props) → bw.makeCard(props)
3720
+ bw.make = make;
3721
+
3722
+ // Component registry
3723
+ bw.BCCL = BCCL;
3724
+
3725
+ // Variant class helper
3726
+ bw.variantClass = variantClass;
3727
+
3728
+ // Create functions that return handles
3729
+ if (typeof bw.renderComponent === 'function') {
3730
+ Object.entries(components).forEach(function(entry) {
3731
+ var name = entry[0], fn = entry[1];
3732
+ if (name.indexOf('make') === 0) {
3733
+ var createName = 'create' + name.substring(4);
3734
+ bw[createName] = function(props) {
3735
+ var taco = fn(props);
3736
+ return bw.renderComponent(taco);
3737
+ };
3738
+ }
3739
+ });
3740
+ }
3741
+ }
3742
+
3743
+ // UMD auto-registration: if `bw` exists as a global, register automatically
3744
+ if (typeof window !== 'undefined' && typeof window.bw !== 'undefined') {
3745
+ registerBCCL(window.bw);
3746
+ } else if (typeof globalThis !== 'undefined' && typeof globalThis.bw !== 'undefined') {
3747
+ registerBCCL(globalThis.bw);
3748
+ }
3749
+
3750
+ exports.BCCL = BCCL;
3751
+ exports.make = make;
3752
+ exports.registerBCCL = registerBCCL;
3753
+ exports.variantClass = variantClass;
3754
+
3755
+ }));
3756
+ //# sourceMappingURL=bitwrench-bccl.umd.js.map