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