bitwrench 2.0.13 → 2.0.15

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 (46) hide show
  1. package/README.md +4 -4
  2. package/dist/bitwrench-code-edit.cjs.js +46 -46
  3. package/dist/bitwrench-code-edit.cjs.min.js +16 -0
  4. package/dist/bitwrench-code-edit.es5.js +8 -8
  5. package/dist/bitwrench-code-edit.es5.min.js +2 -2
  6. package/dist/bitwrench-code-edit.esm.js +46 -46
  7. package/dist/bitwrench-code-edit.esm.min.js +2 -2
  8. package/dist/bitwrench-code-edit.umd.js +46 -46
  9. package/dist/bitwrench-code-edit.umd.min.js +2 -2
  10. package/dist/bitwrench-lean.cjs.js +5011 -3419
  11. package/dist/bitwrench-lean.cjs.min.js +35 -6
  12. package/dist/bitwrench-lean.es5.js +6218 -4272
  13. package/dist/bitwrench-lean.es5.min.js +32 -3
  14. package/dist/bitwrench-lean.esm.js +5011 -3419
  15. package/dist/bitwrench-lean.esm.min.js +35 -6
  16. package/dist/bitwrench-lean.umd.js +5011 -3419
  17. package/dist/bitwrench-lean.umd.min.js +35 -6
  18. package/dist/bitwrench.cjs.js +6966 -4662
  19. package/dist/bitwrench.cjs.min.js +38 -8
  20. package/dist/bitwrench.css +2453 -4784
  21. package/dist/bitwrench.es5.js +9592 -6813
  22. package/dist/bitwrench.es5.min.js +34 -5
  23. package/dist/bitwrench.esm.js +6966 -4662
  24. package/dist/bitwrench.esm.min.js +38 -8
  25. package/dist/bitwrench.min.css +1 -0
  26. package/dist/bitwrench.umd.js +6966 -4662
  27. package/dist/bitwrench.umd.min.js +38 -8
  28. package/dist/builds.json +89 -67
  29. package/dist/sri.json +28 -26
  30. package/package.json +7 -5
  31. package/readme.html +14 -14
  32. package/src/{bitwrench-components-v2.js → bitwrench-bccl.js} +1311 -600
  33. package/src/bitwrench-code-edit.js +45 -45
  34. package/src/bitwrench-color-utils.js +154 -27
  35. package/src/bitwrench-components-stub.js +4 -1
  36. package/src/bitwrench-file-ops.js +180 -0
  37. package/src/bitwrench-lean.js +2 -2
  38. package/src/bitwrench-styles.js +1468 -3494
  39. package/src/bitwrench-utils.js +458 -0
  40. package/src/bitwrench.js +1795 -1349
  41. package/src/cli/layout-default.js +18 -18
  42. package/src/generate-css.js +73 -53
  43. package/src/version.js +3 -3
  44. package/src/bitwrench-component-base.js +0 -736
  45. package/src/bitwrench-components-inline.js +0 -374
  46. package/src/bitwrench-components.js +0 -610
@@ -12,11 +12,43 @@
12
12
  * Handle classes (CardHandle, TableHandle, NavbarHandle, TabsHandle)
13
13
  * provide imperative DOM manipulation for rendered components.
14
14
  *
15
- * @module bitwrench-components-v2
15
+ * @module bitwrench-bccl
16
16
  * @license BSD-2-Clause
17
17
  * @author M A Chatterjee <deftio [at] deftio [dot] com>
18
18
  */
19
19
 
20
+ // =========================================================================
21
+ // Variant → Utility Class Mapping
22
+ //
23
+ // Components compose these shared utility classes instead of owning
24
+ // their own variant selectors. The CSS is generated once by
25
+ // generatePaletteUtilities() + generateInteractionRules().
26
+ // =========================================================================
27
+
28
+ /**
29
+ * Maps component type to a function that returns utility classes for a variant.
30
+ * Each function takes a variant name (e.g. 'primary') and returns a class string.
31
+ * @type {Object.<string, function(string): string>}
32
+ */
33
+ /**
34
+ * Convert a variant name to a single palette class.
35
+ * All BCCL components use this: variant='primary' → class includes 'bw_primary'.
36
+ * The CSS palette class (.bw-primary) sets bg/color/border; component-specific
37
+ * overrides in generatePaletteClasses() adjust per component type.
38
+ *
39
+ * @param {string} v - Variant name (e.g. 'primary', 'danger', 'outline_primary')
40
+ * @returns {string} CSS class string
41
+ */
42
+ export function variantClass(v) {
43
+ if (!v) return '';
44
+ // Handle outline variants: 'outline_primary' or 'outline-primary'
45
+ if (v.indexOf('outline') === 0) {
46
+ var base = v.replace(/^outline[_-]/, '');
47
+ return 'bw_btn_outline bw_' + base;
48
+ }
49
+ return 'bw_' + v;
50
+ }
51
+
20
52
  /**
21
53
  * Create a card component with optional header, body, footer, and image support
22
54
  *
@@ -76,54 +108,54 @@ export function makeCard(props = {}) {
76
108
 
77
109
  const shadowClasses = {
78
110
  none: '',
79
- sm: 'bw-shadow-sm',
80
- md: 'bw-shadow',
81
- lg: 'bw-shadow-lg'
111
+ sm: 'bw_shadow_sm',
112
+ md: 'bw_shadow',
113
+ lg: 'bw_shadow_lg'
82
114
  };
83
115
 
84
116
  const cardClasses = [
85
- 'bw-card',
86
- variant ? `bw-card-${variant}` : '',
117
+ 'bw_card',
118
+ variantClass(variant),
87
119
  shadow ? (shadowClasses[shadow] || '') : '',
88
- !bordered ? 'bw-border-0' : '',
89
- hoverable ? 'bw-card-hoverable' : '',
120
+ !bordered ? 'bw_border_0' : '',
121
+ hoverable ? 'bw_card_hoverable' : '',
90
122
  className
91
123
  ].filter(Boolean).join(' ').trim();
92
124
 
93
125
  const cardContent = [
94
126
  header && {
95
127
  t: 'div',
96
- a: { class: `bw-card-header ${headerClass}`.trim() },
128
+ a: { class: `bw_card_header ${headerClass}`.trim() },
97
129
  c: header
98
130
  },
99
131
  image && (imagePosition === 'top' || imagePosition === 'left') && {
100
132
  t: 'img',
101
133
  a: {
102
- class: `bw-card-img-${imagePosition}`,
134
+ class: `bw_card_img_${imagePosition}`,
103
135
  src: image.src,
104
136
  alt: image.alt || ''
105
137
  }
106
138
  },
107
139
  {
108
140
  t: 'div',
109
- a: { class: `bw-card-body ${bodyClass}`.trim() },
141
+ a: { class: `bw_card_body ${bodyClass}`.trim() },
110
142
  c: [
111
- title && { t: 'h5', a: { class: 'bw-card-title' }, c: title },
112
- subtitle && { t: 'h6', a: { class: 'bw-card-subtitle bw-mb-2 bw-text-muted' }, c: subtitle },
143
+ title && { t: 'h5', a: { class: 'bw_card_title' }, c: title },
144
+ subtitle && { t: 'h6', a: { class: 'bw_card_subtitle bw_mb_2 bw_text_muted' }, c: subtitle },
113
145
  content && (Array.isArray(content) ? content : [content])
114
146
  ].flat().filter(Boolean)
115
147
  },
116
148
  image && (imagePosition === 'bottom' || imagePosition === 'right') && {
117
149
  t: 'img',
118
150
  a: {
119
- class: `bw-card-img-${imagePosition}`,
151
+ class: `bw_card_img_${imagePosition}`,
120
152
  src: image.src,
121
153
  alt: image.alt || ''
122
154
  }
123
155
  },
124
156
  footer && {
125
157
  t: 'div',
126
- a: { class: `bw-card-footer ${footerClass}`.trim() },
158
+ a: { class: `bw_card_footer ${footerClass}`.trim() },
127
159
  c: footer
128
160
  }
129
161
  ].filter(Boolean);
@@ -135,7 +167,7 @@ export function makeCard(props = {}) {
135
167
  a: { class: cardClasses, style },
136
168
  c: {
137
169
  t: 'div',
138
- a: { class: 'bw-row bw-g-0' },
170
+ a: { class: 'bw_row bw_g_0' },
139
171
  c: cardContent
140
172
  },
141
173
  o: {
@@ -176,8 +208,11 @@ export function makeCard(props = {}) {
176
208
  * variant: "success",
177
209
  * onclick: () => console.log("saved")
178
210
  * });
211
+ * // String shorthand:
212
+ * const ok = makeButton("OK");
179
213
  */
180
214
  export function makeButton(props = {}) {
215
+ if (typeof props === 'string') props = { text: props };
181
216
  const {
182
217
  text,
183
218
  variant = 'primary',
@@ -194,9 +229,9 @@ export function makeButton(props = {}) {
194
229
  a: {
195
230
  type,
196
231
  class: [
197
- 'bw-btn',
198
- `bw-btn-${variant}`,
199
- size && `bw-btn-${size}`,
232
+ 'bw_btn',
233
+ variantClass(variant),
234
+ size && `bw_btn_${size}`,
200
235
  className
201
236
  ].filter(Boolean).join(' '),
202
237
  disabled,
@@ -230,7 +265,7 @@ export function makeContainer(props = {}) {
230
265
 
231
266
  return {
232
267
  t: 'div',
233
- a: { class: `bw-container${fluid ? '-fluid' : ''} ${className}`.trim() },
268
+ a: { class: `bw_container${fluid ? '-fluid' : ''} ${className}`.trim() },
234
269
  c: children
235
270
  };
236
271
  }
@@ -241,7 +276,7 @@ export function makeContainer(props = {}) {
241
276
  * @param {Object} [props] - Row configuration
242
277
  * @param {Array|Object|string} [props.children] - Child columns
243
278
  * @param {string} [props.className] - Additional CSS classes
244
- * @param {number} [props.gap] - Gap size (1-5) applied via bw-g-{gap} class
279
+ * @param {number} [props.gap] - Gap size (1-5) applied via bw_g_{gap} class
245
280
  * @returns {Object} TACO object representing a grid row
246
281
  * @category Component Builders
247
282
  * @example
@@ -256,7 +291,7 @@ export function makeRow(props = {}) {
256
291
  return {
257
292
  t: 'div',
258
293
  a: {
259
- class: `bw-row ${gap ? `bw-g-${gap}` : ''} ${className}`.trim()
294
+ class: `bw_row ${gap ? `bw_g_${gap}` : ''} ${className}`.trim()
260
295
  },
261
296
  c: children
262
297
  };
@@ -290,20 +325,20 @@ export function makeCol(props = {}) {
290
325
  // Responsive sizes
291
326
  Object.entries(size).forEach(([breakpoint, value]) => {
292
327
  if (breakpoint === 'xs') {
293
- classes.push(`bw-col-${value}`);
328
+ classes.push(`bw_col_${value}`);
294
329
  } else {
295
- classes.push(`bw-col-${breakpoint}-${value}`);
330
+ classes.push(`bw_col_${breakpoint}-${value}`);
296
331
  }
297
332
  });
298
333
  } else if (size) {
299
- classes.push(`bw-col-${size}`);
334
+ classes.push(`bw_col_${size}`);
300
335
  } else {
301
- classes.push('bw-col');
336
+ classes.push('bw_col');
302
337
  }
303
338
 
304
- if (offset) classes.push(`bw-offset-${offset}`);
305
- if (push) classes.push(`bw-push-${push}`);
306
- if (pull) classes.push(`bw-pull-${pull}`);
339
+ if (offset) classes.push(`bw_offset_${offset}`);
340
+ if (push) classes.push(`bw_push_${push}`);
341
+ if (pull) classes.push(`bw_pull_${pull}`);
307
342
 
308
343
  return {
309
344
  t: 'div',
@@ -346,16 +381,16 @@ export function makeNav(props = {}) {
346
381
  return {
347
382
  t: 'ul',
348
383
  a: {
349
- class: `bw-nav ${pills ? 'bw-nav-pills' : 'bw-nav-tabs'} ${vertical ? 'bw-nav-vertical' : ''} ${className}`.trim()
384
+ class: `bw_nav ${pills ? 'bw_nav_pills' : 'bw_nav_tabs'} ${vertical ? 'bw_nav_vertical' : ''} ${className}`.trim()
350
385
  },
351
386
  c: items.map(item => ({
352
387
  t: 'li',
353
- a: { class: 'bw-nav-item' },
388
+ a: { class: 'bw_nav_item' },
354
389
  c: {
355
390
  t: 'a',
356
391
  a: {
357
392
  href: item.href || '#',
358
- class: `bw-nav-link ${item.active ? 'active' : ''} ${item.disabled ? 'disabled' : ''}`.trim()
393
+ class: `bw_nav_link ${item.active ? 'active' : ''} ${item.disabled ? 'disabled' : ''}`.trim()
359
394
  },
360
395
  c: item.text
361
396
  }
@@ -399,25 +434,25 @@ export function makeNavbar(props = {}) {
399
434
  return {
400
435
  t: 'nav',
401
436
  a: {
402
- class: `bw-navbar ${dark ? 'bw-navbar-dark' : 'bw-navbar-light'} ${className}`.trim()
437
+ class: `bw_navbar ${dark ? 'bw_navbar_dark' : 'bw_navbar_light'} ${className}`.trim()
403
438
  },
404
439
  c: {
405
440
  t: 'div',
406
- a: { class: 'bw-container' },
441
+ a: { class: 'bw_container' },
407
442
  c: [
408
443
  brand && {
409
444
  t: 'a',
410
- a: { href: brandHref, class: 'bw-navbar-brand' },
445
+ a: { href: brandHref, class: 'bw_navbar_brand' },
411
446
  c: brand
412
447
  },
413
448
  items.length > 0 && {
414
449
  t: 'div',
415
- a: { class: 'bw-navbar-nav' },
450
+ a: { class: 'bw_navbar_nav' },
416
451
  c: items.map(item => ({
417
452
  t: 'a',
418
453
  a: {
419
454
  href: item.href || '#',
420
- class: `bw-nav-link ${item.active ? 'active' : ''}`
455
+ class: `bw_nav_link ${item.active ? 'active' : ''}`
421
456
  },
422
457
  c: item.text
423
458
  }))
@@ -468,35 +503,38 @@ export function makeTabs(props = {}) {
468
503
 
469
504
  return {
470
505
  t: 'div',
471
- a: { class: 'bw-tabs' },
506
+ a: { class: 'bw_tabs' },
472
507
  c: [
473
508
  {
474
509
  t: 'ul',
475
- a: { class: 'bw-nav bw-nav-tabs', role: 'tablist' },
510
+ a: { class: 'bw_nav bw_nav_tabs', role: 'tablist' },
476
511
  c: tabs.map((tab, index) => ({
477
512
  t: 'li',
478
- a: { class: 'bw-nav-item', role: 'presentation' },
513
+ a: { class: 'bw_nav_item', role: 'presentation' },
479
514
  c: {
480
515
  t: 'button',
481
516
  a: {
482
- class: `bw-nav-link ${index === actualActiveIndex ? 'active' : ''}`,
517
+ class: `bw_nav_link ${index === actualActiveIndex ? 'active' : ''}`,
483
518
  type: 'button',
484
519
  role: 'tab',
520
+ tabindex: index === actualActiveIndex ? '0' : '-1',
485
521
  'aria-selected': index === actualActiveIndex ? 'true' : 'false',
486
522
  'data-tab-index': index,
487
523
  onclick: (e) => {
488
- const tabsContainer = e.target.closest('.bw-tabs');
489
- const allTabs = tabsContainer.querySelectorAll('.bw-nav-link');
490
- const allPanes = tabsContainer.querySelectorAll('.bw-tab-pane');
524
+ const tabsContainer = e.target.closest('.bw_tabs');
525
+ const allTabs = tabsContainer.querySelectorAll('.bw_nav_link');
526
+ const allPanes = tabsContainer.querySelectorAll('.bw_tab_pane');
491
527
 
492
528
  allTabs.forEach(t => {
493
529
  t.classList.remove('active');
494
530
  t.setAttribute('aria-selected', 'false');
531
+ t.setAttribute('tabindex', '-1');
495
532
  });
496
533
  allPanes.forEach(p => p.classList.remove('active'));
497
534
 
498
535
  e.target.classList.add('active');
499
536
  e.target.setAttribute('aria-selected', 'true');
537
+ e.target.setAttribute('tabindex', '0');
500
538
  const targetIndex = parseInt(e.target.getAttribute('data-tab-index'));
501
539
  allPanes[targetIndex].classList.add('active');
502
540
  }
@@ -507,11 +545,11 @@ export function makeTabs(props = {}) {
507
545
  },
508
546
  {
509
547
  t: 'div',
510
- a: { class: 'bw-tab-content' },
548
+ a: { class: 'bw_tab_content' },
511
549
  c: tabs.map((tab, index) => ({
512
550
  t: 'div',
513
551
  a: {
514
- class: `bw-tab-pane ${index === actualActiveIndex ? 'active' : ''}`,
552
+ class: `bw_tab_pane ${index === actualActiveIndex ? 'active' : ''}`,
515
553
  role: 'tabpanel'
516
554
  },
517
555
  c: tab.content
@@ -520,7 +558,39 @@ export function makeTabs(props = {}) {
520
558
  ],
521
559
  o: {
522
560
  type: 'tabs',
523
- state: { activeIndex: actualActiveIndex }
561
+ state: { activeIndex: actualActiveIndex },
562
+ mounted: function(el) {
563
+ var tablist = el.querySelector('[role="tablist"]');
564
+ if (!tablist) return;
565
+ tablist.addEventListener('keydown', function(e) {
566
+ var tabButtons = tablist.querySelectorAll('[role="tab"]');
567
+ var currentIndex = -1;
568
+ for (var i = 0; i < tabButtons.length; i++) {
569
+ if (tabButtons[i] === e.target) { currentIndex = i; break; }
570
+ }
571
+ if (currentIndex === -1) return;
572
+
573
+ var newIndex = -1;
574
+ if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
575
+ e.preventDefault();
576
+ newIndex = currentIndex > 0 ? currentIndex - 1 : tabButtons.length - 1;
577
+ } else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
578
+ e.preventDefault();
579
+ newIndex = currentIndex < tabButtons.length - 1 ? currentIndex + 1 : 0;
580
+ } else if (e.key === 'Home') {
581
+ e.preventDefault();
582
+ newIndex = 0;
583
+ } else if (e.key === 'End') {
584
+ e.preventDefault();
585
+ newIndex = tabButtons.length - 1;
586
+ }
587
+
588
+ if (newIndex >= 0) {
589
+ tabButtons[newIndex].focus();
590
+ tabButtons[newIndex].click();
591
+ }
592
+ });
593
+ }
524
594
  }
525
595
  };
526
596
  }
@@ -541,8 +611,11 @@ export function makeTabs(props = {}) {
541
611
  * variant: "success",
542
612
  * dismissible: true
543
613
  * });
614
+ * // String shorthand:
615
+ * const msg = makeAlert("Something happened");
544
616
  */
545
617
  export function makeAlert(props = {}) {
618
+ if (typeof props === 'string') props = { content: props };
546
619
  const {
547
620
  content,
548
621
  variant = 'info',
@@ -553,7 +626,7 @@ export function makeAlert(props = {}) {
553
626
  return {
554
627
  t: 'div',
555
628
  a: {
556
- class: `bw-alert bw-alert-${variant} ${dismissible ? 'bw-alert-dismissible' : ''} ${className}`.trim(),
629
+ class: `bw_alert ${variantClass(variant)} ${dismissible ? 'bw_alert_dismissible' : ''} ${className}`.trim(),
557
630
  role: 'alert'
558
631
  },
559
632
  c: [
@@ -562,10 +635,10 @@ export function makeAlert(props = {}) {
562
635
  t: 'button',
563
636
  a: {
564
637
  type: 'button',
565
- class: 'bw-close',
638
+ class: 'bw_close',
566
639
  'aria-label': 'Close',
567
640
  onclick: function(e) {
568
- var alert = e.target.closest('.bw-alert');
641
+ var alert = e.target.closest('.bw_alert');
569
642
  if (alert) { alert.remove(); }
570
643
  }
571
644
  },
@@ -589,8 +662,11 @@ export function makeAlert(props = {}) {
589
662
  * @example
590
663
  * const badge = makeBadge({ text: "New", variant: "danger", pill: true });
591
664
  * const small = makeBadge({ text: "3", variant: "info", size: "sm" });
665
+ * // String shorthand:
666
+ * const tag = makeBadge("New");
592
667
  */
593
668
  export function makeBadge(props = {}) {
669
+ if (typeof props === 'string') props = { text: props };
594
670
  const {
595
671
  text,
596
672
  variant = 'primary',
@@ -599,12 +675,12 @@ export function makeBadge(props = {}) {
599
675
  className = ''
600
676
  } = props;
601
677
 
602
- const sizeClass = size === 'sm' ? ' bw-badge-sm' : size === 'lg' ? ' bw-badge-lg' : '';
678
+ const sizeClass = size === 'sm' ? ' bw_badge_sm' : size === 'lg' ? ' bw_badge_lg' : '';
603
679
 
604
680
  return {
605
681
  t: 'span',
606
682
  a: {
607
- class: `bw-badge bw-badge-${variant}${sizeClass} ${pill ? 'bw-badge-pill' : ''} ${className}`.trim()
683
+ class: `bw_badge ${variantClass(variant)}${sizeClass} ${pill ? 'bw_badge_pill' : ''} ${className}`.trim()
608
684
  },
609
685
  c: text
610
686
  };
@@ -647,17 +723,17 @@ export function makeProgress(props = {}) {
647
723
  return {
648
724
  t: 'div',
649
725
  a: {
650
- class: 'bw-progress',
726
+ class: 'bw_progress',
651
727
  style: height ? { height: `${height}px` } : undefined
652
728
  },
653
729
  c: {
654
730
  t: 'div',
655
731
  a: {
656
732
  class: [
657
- 'bw-progress-bar',
658
- `bw-progress-bar-${variant}`,
659
- striped && 'bw-progress-bar-striped',
660
- animated && 'bw-progress-bar-animated'
733
+ 'bw_progress_bar',
734
+ variantClass(variant),
735
+ striped && 'bw_progress_bar_striped',
736
+ animated && 'bw_progress_bar_animated'
661
737
  ].filter(Boolean).join(' '),
662
738
  role: 'progressbar',
663
739
  style: { width: `${percentage}%` },
@@ -703,7 +779,7 @@ export function makeListGroup(props = {}) {
703
779
 
704
780
  return {
705
781
  t: 'div',
706
- a: { class: `bw-list-group ${flush ? 'bw-list-group-flush' : ''}`.trim() },
782
+ a: { class: `bw_list_group ${flush ? 'bw_list_group_flush' : ''}`.trim() },
707
783
  c: items.map(item => {
708
784
  const isObject = typeof item === 'object';
709
785
  const text = isObject ? item.text : item;
@@ -718,7 +794,7 @@ export function makeListGroup(props = {}) {
718
794
  t: 'a',
719
795
  a: {
720
796
  class: [
721
- 'bw-list-group-item',
797
+ 'bw_list_group_item',
722
798
  active && 'active',
723
799
  disabled && 'disabled'
724
800
  ].filter(Boolean).join(' '),
@@ -737,7 +813,7 @@ export function makeListGroup(props = {}) {
737
813
  t: 'div',
738
814
  a: {
739
815
  class: [
740
- 'bw-list-group-item',
816
+ 'bw_list_group_item',
741
817
  active && 'active',
742
818
  disabled && 'disabled'
743
819
  ].filter(Boolean).join(' ')
@@ -778,11 +854,11 @@ export function makeBreadcrumb(props = {}) {
778
854
  a: { 'aria-label': 'breadcrumb' },
779
855
  c: {
780
856
  t: 'ol',
781
- a: { class: 'bw-breadcrumb' },
857
+ a: { class: 'bw_breadcrumb' },
782
858
  c: items.map((item, index) => ({
783
859
  t: 'li',
784
860
  a: {
785
- class: `bw-breadcrumb-item ${item.active ? 'active' : ''}`,
861
+ class: `bw_breadcrumb_item ${item.active ? 'active' : ''}`,
786
862
  'aria-current': item.active ? 'page' : undefined
787
863
  },
788
864
  c: item.active ? item.text : {
@@ -827,13 +903,16 @@ export function makeForm(props = {}) {
827
903
  }
828
904
 
829
905
  /**
830
- * Create a form group with label, input, and optional help text
906
+ * Create a form group with label, input, optional help text and validation feedback
831
907
  *
832
908
  * @param {Object} [props] - Form group configuration
833
909
  * @param {string} [props.label] - Label text
834
910
  * @param {Object} [props.input] - Input TACO object (from makeInput, makeSelect, etc.)
835
911
  * @param {string} [props.help] - Help text displayed below the input
836
912
  * @param {string} [props.id] - Input ID (links label to input via for/id)
913
+ * @param {string} [props.validation] - Validation state ("valid" or "invalid")
914
+ * @param {string} [props.feedback] - Validation feedback text shown below input
915
+ * @param {boolean} [props.required=false] - Show required indicator (*) on label
837
916
  * @returns {Object} TACO object representing a form group
838
917
  * @category Component Builders
839
918
  * @example
@@ -841,25 +920,41 @@ export function makeForm(props = {}) {
841
920
  * label: "Email",
842
921
  * id: "email",
843
922
  * input: makeInput({ type: "email", id: "email", placeholder: "you@example.com" }),
844
- * help: "We'll never share your email."
923
+ * validation: "invalid",
924
+ * feedback: "Please enter a valid email address."
845
925
  * });
846
926
  */
847
927
  export function makeFormGroup(props = {}) {
848
- const { label, input, help, id } = props;
928
+ var { label, input, help, id, validation, feedback, required } = props;
929
+
930
+ // Shallow-clone input TACO to add validation class without mutating original
931
+ var styledInput = input;
932
+ if (validation && input && input.a) {
933
+ styledInput = { t: input.t, a: Object.assign({}, input.a), c: input.c, o: input.o };
934
+ var validClass = validation === 'valid' ? 'bw_is_valid' : validation === 'invalid' ? 'bw_is_invalid' : '';
935
+ if (validClass) {
936
+ styledInput.a.class = ((styledInput.a.class || '') + ' ' + validClass).trim();
937
+ }
938
+ }
849
939
 
850
940
  return {
851
941
  t: 'div',
852
- a: { class: 'bw-form-group' },
942
+ a: { class: 'bw_form_group' },
853
943
  c: [
854
944
  label && {
855
945
  t: 'label',
856
- a: { for: id, class: 'bw-form-label' },
857
- c: label
946
+ a: { for: id, class: 'bw_form_label' },
947
+ c: required ? [label, { t: 'span', a: { class: 'bw_text_danger bw_ms_1' }, c: '*' }] : label
948
+ },
949
+ styledInput,
950
+ feedback && validation && {
951
+ t: 'div',
952
+ a: { class: validation === 'valid' ? 'bw_valid_feedback' : 'bw_invalid_feedback' },
953
+ c: feedback
858
954
  },
859
- input,
860
955
  help && {
861
956
  t: 'small',
862
- a: { class: 'bw-form-text bw-text-muted' },
957
+ a: { class: 'bw_form_text bw_text_muted' },
863
958
  c: help
864
959
  }
865
960
  ].filter(Boolean)
@@ -912,7 +1007,7 @@ export function makeInput(props = {}) {
912
1007
  t: 'input',
913
1008
  a: {
914
1009
  type,
915
- class: `bw-form-control ${className}`.trim(),
1010
+ class: `bw_form_control ${className}`.trim(),
916
1011
  placeholder,
917
1012
  value,
918
1013
  id,
@@ -965,7 +1060,7 @@ export function makeTextarea(props = {}) {
965
1060
  return {
966
1061
  t: 'textarea',
967
1062
  a: {
968
- class: `bw-form-control ${className}`.trim(),
1063
+ class: `bw_form_control ${className}`.trim(),
969
1064
  placeholder,
970
1065
  rows,
971
1066
  id,
@@ -1019,7 +1114,7 @@ export function makeSelect(props = {}) {
1019
1114
  return {
1020
1115
  t: 'select',
1021
1116
  a: {
1022
- class: `bw-form-control ${className}`.trim(),
1117
+ class: `bw_form_control ${className}`.trim(),
1023
1118
  id,
1024
1119
  name,
1025
1120
  disabled,
@@ -1070,13 +1165,13 @@ export function makeCheckbox(props = {}) {
1070
1165
 
1071
1166
  return {
1072
1167
  t: 'div',
1073
- a: { class: `bw-form-check ${className}`.trim() },
1168
+ a: { class: `bw_form_check ${className}`.trim() },
1074
1169
  c: [
1075
1170
  {
1076
1171
  t: 'input',
1077
1172
  a: {
1078
1173
  type: 'checkbox',
1079
- class: 'bw-form-check-input',
1174
+ class: 'bw_form_check_input',
1080
1175
  checked,
1081
1176
  id,
1082
1177
  name,
@@ -1087,7 +1182,7 @@ export function makeCheckbox(props = {}) {
1087
1182
  },
1088
1183
  label && {
1089
1184
  t: 'label',
1090
- a: { class: 'bw-form-check-label', for: id },
1185
+ a: { class: 'bw_form_check_label', for: id },
1091
1186
  c: label
1092
1187
  }
1093
1188
  ].filter(Boolean)
@@ -1125,7 +1220,7 @@ export function makeStack(props = {}) {
1125
1220
  return {
1126
1221
  t: 'div',
1127
1222
  a: {
1128
- class: `bw-${direction === 'vertical' ? 'vstack' : 'hstack'} bw-gap-${gap} ${className}`.trim()
1223
+ class: `bw_${direction === 'vertical' ? 'vstack' : 'hstack'} bw_gap_${gap} ${className}`.trim()
1129
1224
  },
1130
1225
  c: children
1131
1226
  };
@@ -1153,12 +1248,12 @@ export function makeSpinner(props = {}) {
1153
1248
  return {
1154
1249
  t: 'div',
1155
1250
  a: {
1156
- class: `bw-spinner-${type} bw-spinner-${type}-${size} bw-text-${variant}`,
1251
+ class: `bw_spinner_${type} bw_spinner_${type}-${size} ${variantClass(variant)}`,
1157
1252
  role: 'status'
1158
1253
  },
1159
1254
  c: {
1160
1255
  t: 'span',
1161
- a: { class: 'bw-visually-hidden' },
1256
+ a: { class: 'bw_visually_hidden' },
1162
1257
  c: 'Loading...'
1163
1258
  }
1164
1259
  };
@@ -1209,44 +1304,44 @@ export function makeHero(props = {}) {
1209
1304
  } = props;
1210
1305
 
1211
1306
  const sizeClasses = {
1212
- sm: 'bw-py-3',
1213
- md: 'bw-py-4',
1214
- lg: 'bw-py-5',
1215
- xl: 'bw-py-6'
1307
+ sm: 'bw_py_3',
1308
+ md: 'bw_py_4',
1309
+ lg: 'bw_py_5',
1310
+ xl: 'bw_py_6'
1216
1311
  };
1217
1312
 
1218
1313
  return {
1219
1314
  t: 'section',
1220
1315
  a: {
1221
- class: `bw-hero bw-hero-${variant} ${sizeClasses[size] || sizeClasses.lg} ${centered ? 'bw-text-center' : ''} ${className}`.trim(),
1316
+ class: `bw_hero ${variantClass(variant)} ${sizeClasses[size] || sizeClasses.lg} ${centered ? 'bw_text_center' : ''} ${className}`.trim(),
1222
1317
  style: backgroundImage ? `background-image: url('${backgroundImage}'); background-size: cover; background-position: center;` : undefined
1223
1318
  },
1224
1319
  c: [
1225
1320
  overlay && {
1226
1321
  t: 'div',
1227
- a: { class: 'bw-hero-overlay' }
1322
+ a: { class: 'bw_hero_overlay' }
1228
1323
  },
1229
1324
  {
1230
1325
  t: 'div',
1231
- a: { class: 'bw-container' },
1326
+ a: { class: 'bw_container' },
1232
1327
  c: {
1233
1328
  t: 'div',
1234
- a: { class: 'bw-hero-content' },
1329
+ a: { class: 'bw_hero_content' },
1235
1330
  c: [
1236
1331
  title && {
1237
1332
  t: 'h1',
1238
- a: { class: 'bw-hero-title bw-display-4 bw-mb-3' },
1333
+ a: { class: 'bw_hero_title bw_display_4 bw_mb_3' },
1239
1334
  c: title
1240
1335
  },
1241
1336
  subtitle && {
1242
1337
  t: 'p',
1243
- a: { class: 'bw-hero-subtitle bw-lead bw-mb-4' },
1338
+ a: { class: 'bw_hero_subtitle bw_lead bw_mb_4' },
1244
1339
  c: subtitle
1245
1340
  },
1246
1341
  content,
1247
1342
  actions && {
1248
1343
  t: 'div',
1249
- a: { class: 'bw-hero-actions bw-mt-4' },
1344
+ a: { class: 'bw_hero_actions bw_mt_4' },
1250
1345
  c: actions
1251
1346
  }
1252
1347
  ].filter(Boolean)
@@ -1292,37 +1387,37 @@ export function makeFeatureGrid(props = {}) {
1292
1387
  className = ''
1293
1388
  } = props;
1294
1389
 
1295
- const colClass = `bw-col-md-${12/columns}`;
1390
+ const colClass = `bw_col_md_${12/columns}`;
1296
1391
 
1297
1392
  return {
1298
1393
  t: 'div',
1299
- a: { class: `bw-feature-grid ${className}`.trim() },
1394
+ a: { class: `bw_feature_grid ${className}`.trim() },
1300
1395
  c: {
1301
1396
  t: 'div',
1302
- a: { class: 'bw-row bw-g-4' },
1397
+ a: { class: 'bw_row bw_g_4' },
1303
1398
  c: features.map(feature => ({
1304
1399
  t: 'div',
1305
1400
  a: { class: colClass },
1306
1401
  c: {
1307
1402
  t: 'div',
1308
- a: { class: `bw-feature ${centered ? 'bw-text-center' : ''}` },
1403
+ a: { class: `bw_feature ${centered ? 'bw_text_center' : ''}` },
1309
1404
  c: [
1310
1405
  feature.icon && {
1311
1406
  t: 'div',
1312
1407
  a: {
1313
- class: 'bw-feature-icon bw-mb-3 bw-text-primary',
1408
+ class: 'bw_feature_icon bw_mb_3 bw_text_primary',
1314
1409
  style: `font-size: ${iconSize};`
1315
1410
  },
1316
1411
  c: feature.icon
1317
1412
  },
1318
1413
  feature.title && {
1319
1414
  t: 'h3',
1320
- a: { class: 'bw-feature-title bw-h5 bw-mb-2' },
1415
+ a: { class: 'bw_feature_title bw_h5 bw_mb_2' },
1321
1416
  c: feature.title
1322
1417
  },
1323
1418
  feature.description && {
1324
1419
  t: 'p',
1325
- a: { class: 'bw-feature-description bw-text-muted' },
1420
+ a: { class: 'bw_feature_description bw_text_muted' },
1326
1421
  c: feature.description
1327
1422
  }
1328
1423
  ].filter(Boolean)
@@ -1366,19 +1461,19 @@ export function makeCTA(props = {}) {
1366
1461
 
1367
1462
  return {
1368
1463
  t: 'section',
1369
- a: { class: `bw-cta bw-bg-${variant} bw-py-5 ${className}`.trim() },
1464
+ a: { class: `bw_cta bw_bg_${variant} bw_py_5 ${className}`.trim() },
1370
1465
  c: {
1371
1466
  t: 'div',
1372
- a: { class: 'bw-container' },
1467
+ a: { class: 'bw_container' },
1373
1468
  c: {
1374
1469
  t: 'div',
1375
- a: { class: `bw-cta-content ${centered ? 'bw-text-center' : ''}` },
1470
+ a: { class: `bw_cta_content ${centered ? 'bw_text_center' : ''}` },
1376
1471
  c: [
1377
- title && { t: 'h2', a: { class: 'bw-cta-title bw-mb-3' }, c: title },
1378
- description && { t: 'p', a: { class: 'bw-cta-description bw-lead bw-mb-4' }, c: description },
1472
+ title && { t: 'h2', a: { class: 'bw_cta_title bw_mb_3' }, c: title },
1473
+ description && { t: 'p', a: { class: 'bw_cta_description bw_lead bw_mb_4' }, c: description },
1379
1474
  actions && {
1380
1475
  t: 'div',
1381
- a: { class: 'bw-cta-actions' },
1476
+ a: { class: 'bw_cta_actions' },
1382
1477
  c: actions
1383
1478
  }
1384
1479
  ].filter(Boolean)
@@ -1418,27 +1513,27 @@ export function makeSection(props = {}) {
1418
1513
  } = props;
1419
1514
 
1420
1515
  const spacingClasses = {
1421
- sm: 'bw-py-3',
1422
- md: 'bw-py-4',
1423
- lg: 'bw-py-5',
1424
- xl: 'bw-py-6'
1516
+ sm: 'bw_py_3',
1517
+ md: 'bw_py_4',
1518
+ lg: 'bw_py_5',
1519
+ xl: 'bw_py_6'
1425
1520
  };
1426
1521
 
1427
1522
  return {
1428
1523
  t: 'section',
1429
1524
  a: {
1430
- class: `bw-section ${spacingClasses[spacing] || spacingClasses.md} ${variant !== 'default' ? `bw-bg-${variant}` : ''} ${className}`.trim()
1525
+ class: `bw_section ${spacingClasses[spacing] || spacingClasses.md} ${variant !== 'default' ? `bw_bg_${variant}` : ''} ${className}`.trim()
1431
1526
  },
1432
1527
  c: {
1433
1528
  t: 'div',
1434
- a: { class: 'bw-container' },
1529
+ a: { class: 'bw_container' },
1435
1530
  c: [
1436
1531
  (title || subtitle) && {
1437
1532
  t: 'div',
1438
- a: { class: 'bw-section-header bw-text-center bw-mb-5' },
1533
+ a: { class: 'bw_section_header bw_text_center bw_mb_5' },
1439
1534
  c: [
1440
- title && { t: 'h2', a: { class: 'bw-section-title' }, c: title },
1441
- subtitle && { t: 'p', a: { class: 'bw-section-subtitle bw-text-muted' }, c: subtitle }
1535
+ title && { t: 'h2', a: { class: 'bw_section_title' }, c: title },
1536
+ subtitle && { t: 'p', a: { class: 'bw_section_subtitle bw_text_muted' }, c: subtitle }
1442
1537
  ].filter(Boolean)
1443
1538
  },
1444
1539
  content
@@ -1455,317 +1550,9 @@ export function makeSection(props = {}) {
1455
1550
  // full re-renders. Used by bw.createCard(), bw.createTable(), etc.
1456
1551
  // =========================================================================
1457
1552
 
1458
- /**
1459
- * Imperative handle for a rendered card component
1460
- *
1461
- * Provides methods to update card title, content, and CSS classes
1462
- * without re-rendering the entire component. Created automatically
1463
- * when using bw.createCard().
1464
- *
1465
- * @category Component Handles
1466
- */
1467
- export class CardHandle {
1468
- /**
1469
- * @param {Element} element - The card's root DOM element
1470
- * @param {Object} taco - The original TACO object used to create the card
1471
- */
1472
- constructor(element, taco) {
1473
- this.element = element;
1474
- this._taco = taco;
1475
- this.state = taco.o?.state || {};
1476
-
1477
- // Cache child elements
1478
- this.children = {
1479
- header: element.querySelector('.bw-card-header'),
1480
- title: element.querySelector('.bw-card-title'),
1481
- body: element.querySelector('.bw-card-body'),
1482
- footer: element.querySelector('.bw-card-footer')
1483
- };
1484
- }
1485
-
1486
- /**
1487
- * Update the card title text
1488
- *
1489
- * @param {string} title - New title text
1490
- * @returns {CardHandle} this (for chaining)
1491
- */
1492
- setTitle(title) {
1493
- if (this.children.title) {
1494
- this.children.title.textContent = title;
1495
- }
1496
- return this;
1497
- }
1498
-
1499
- /**
1500
- * Replace the card body content
1501
- *
1502
- * @param {string|Object} content - New content (string or TACO object)
1503
- * @returns {CardHandle} this (for chaining)
1504
- */
1505
- setContent(content) {
1506
- if (this.children.body) {
1507
- if (typeof content === 'string') {
1508
- this.children.body.textContent = content;
1509
- } else {
1510
- // Re-render content
1511
- this.children.body.innerHTML = '';
1512
- const newContent = window.bw.taco.toDOM(content);
1513
- this.children.body.appendChild(newContent);
1514
- }
1515
- }
1516
- return this;
1517
- }
1518
-
1519
- /**
1520
- * Add a CSS class to the card root element
1521
- *
1522
- * @param {string} className - Class to add
1523
- * @returns {CardHandle} this (for chaining)
1524
- */
1525
- addClass(className) {
1526
- this.element.classList.add(className);
1527
- return this;
1528
- }
1529
-
1530
- /**
1531
- * Remove a CSS class from the card root element
1532
- *
1533
- * @param {string} className - Class to remove
1534
- * @returns {CardHandle} this (for chaining)
1535
- */
1536
- removeClass(className) {
1537
- this.element.classList.remove(className);
1538
- return this;
1539
- }
1540
-
1541
- /**
1542
- * Query a child element within the card
1543
- *
1544
- * @param {string} selector - CSS selector
1545
- * @returns {Element|null} Matching element or null
1546
- */
1547
- select(selector) {
1548
- return this.element.querySelector(selector);
1549
- }
1550
- }
1551
-
1552
- /**
1553
- * Imperative handle for a rendered table component
1554
- *
1555
- * Provides methods for data updates and column sorting. Caches
1556
- * thead/tbody/header references for efficient DOM updates.
1557
- * Created automatically when using bw.createTable().
1558
- *
1559
- * @category Component Handles
1560
- */
1561
- export class TableHandle {
1562
- /**
1563
- * @param {Element} element - The table's root DOM element
1564
- * @param {Object} taco - The original TACO object used to create the table
1565
- */
1566
- constructor(element, taco) {
1567
- this.element = element;
1568
- this._taco = taco;
1569
- this.state = taco.o?.state || {};
1570
- this._data = this.state.data || [];
1571
- this._sortColumn = null;
1572
- this._sortDirection = 'asc';
1573
-
1574
- // Cache elements
1575
- this.children = {
1576
- thead: element.querySelector('thead'),
1577
- tbody: element.querySelector('tbody'),
1578
- headers: element.querySelectorAll('th')
1579
- };
1580
-
1581
- // Set up sorting if enabled
1582
- if (this.state.sortable) {
1583
- this._setupSorting();
1584
- }
1585
- }
1586
-
1587
- /**
1588
- * Attach click-to-sort handlers on all column headers
1589
- * @private
1590
- */
1591
- _setupSorting() {
1592
- this.children.headers.forEach((th, index) => {
1593
- th.style.cursor = 'pointer';
1594
- th.onclick = () => this.sortBy(th.textContent);
1595
- });
1596
- }
1597
-
1598
- /**
1599
- * Replace the table data and re-render the body
1600
- *
1601
- * @param {Array<Object>} data - Array of row objects
1602
- * @returns {TableHandle} this (for chaining)
1603
- */
1604
- setData(data) {
1605
- this._data = data;
1606
- this._renderBody();
1607
- return this;
1608
- }
1609
-
1610
- /**
1611
- * Sort the table by a column name
1612
- *
1613
- * Toggles direction if the same column is sorted again.
1614
- *
1615
- * @param {string} column - Column header text to sort by
1616
- * @param {string} [direction] - Sort direction ("asc" or "desc"); toggles if omitted
1617
- * @returns {TableHandle} this (for chaining)
1618
- */
1619
- sortBy(column, direction) {
1620
- if (column === this._sortColumn && !direction) {
1621
- this._sortDirection = this._sortDirection === 'asc' ? 'desc' : 'asc';
1622
- } else {
1623
- this._sortColumn = column;
1624
- this._sortDirection = direction || 'asc';
1625
- }
1626
-
1627
- const columnKey = Object.keys(this._data[0])[
1628
- Array.from(this.children.headers).findIndex(th => th.textContent === column)
1629
- ];
1630
-
1631
- this._data.sort((a, b) => {
1632
- const aVal = a[columnKey];
1633
- const bVal = b[columnKey];
1634
- const result = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
1635
- return this._sortDirection === 'asc' ? result : -result;
1636
- });
1637
-
1638
- this._renderBody();
1639
- return this;
1640
- }
1641
-
1642
- /**
1643
- * Re-render the tbody from current _data
1644
- * @private
1645
- */
1646
- _renderBody() {
1647
- this.children.tbody.innerHTML = '';
1648
- this._data.forEach(row => {
1649
- const tr = document.createElement('tr');
1650
- Object.values(row).forEach(value => {
1651
- const td = document.createElement('td');
1652
- td.textContent = value;
1653
- tr.appendChild(td);
1654
- });
1655
- this.children.tbody.appendChild(tr);
1656
- });
1657
- }
1658
- }
1659
-
1660
- /**
1661
- * Imperative handle for a rendered navbar component
1662
- *
1663
- * Provides methods to update the active navigation link.
1664
- * Created automatically when using bw.createNavbar().
1665
- *
1666
- * @category Component Handles
1667
- */
1668
- export class NavbarHandle {
1669
- /**
1670
- * @param {Element} element - The navbar's root DOM element
1671
- * @param {Object} taco - The original TACO object used to create the navbar
1672
- */
1673
- constructor(element, taco) {
1674
- this.element = element;
1675
- this._taco = taco;
1676
- this.state = taco.o?.state || {};
1677
-
1678
- this.children = {
1679
- brand: element.querySelector('.bw-navbar-brand'),
1680
- links: element.querySelectorAll('.bw-nav-link')
1681
- };
1682
- }
1683
-
1684
- /**
1685
- * Set the active navigation link by href
1686
- *
1687
- * @param {string} href - The href value of the link to activate
1688
- * @returns {NavbarHandle} this (for chaining)
1689
- */
1690
- setActive(href) {
1691
- this.children.links.forEach(link => {
1692
- if (link.getAttribute('href') === href) {
1693
- link.classList.add('active');
1694
- } else {
1695
- link.classList.remove('active');
1696
- }
1697
- });
1698
- return this;
1699
- }
1700
- }
1701
-
1702
- /**
1703
- * Imperative handle for a rendered tabs component
1704
- *
1705
- * Provides programmatic tab switching. Sets up click handlers
1706
- * on tab buttons and manages active states on both buttons and panes.
1707
- * Created automatically when using bw.createTabs().
1708
- *
1709
- * @category Component Handles
1710
- */
1711
- export class TabsHandle {
1712
- /**
1713
- * @param {Element} element - The tabs container DOM element
1714
- * @param {Object} taco - The original TACO object used to create the tabs
1715
- */
1716
- constructor(element, taco) {
1717
- this.element = element;
1718
- this._taco = taco;
1719
- this.state = taco.o?.state || {};
1720
-
1721
- this.children = {
1722
- navItems: element.querySelectorAll('.bw-nav-link'),
1723
- tabPanes: element.querySelectorAll('.bw-tab-pane')
1724
- };
1725
-
1726
- this._setupTabs();
1727
- }
1728
-
1729
- /**
1730
- * Attach click handlers to tab navigation buttons
1731
- * @private
1732
- */
1733
- _setupTabs() {
1734
- this.children.navItems.forEach((navItem, index) => {
1735
- navItem.onclick = (e) => {
1736
- e.preventDefault();
1737
- this.switchTo(index);
1738
- };
1739
- });
1740
- }
1741
-
1742
- /**
1743
- * Programmatically switch to a tab by index
1744
- *
1745
- * @param {number} index - Zero-based tab index to activate
1746
- * @returns {TabsHandle} this (for chaining)
1747
- */
1748
- switchTo(index) {
1749
- this.children.navItems.forEach((item, i) => {
1750
- if (i === index) {
1751
- item.classList.add('active');
1752
- } else {
1753
- item.classList.remove('active');
1754
- }
1755
- });
1756
-
1757
- this.children.tabPanes.forEach((pane, i) => {
1758
- if (i === index) {
1759
- pane.classList.add('active');
1760
- } else {
1761
- pane.classList.remove('active');
1762
- }
1763
- });
1764
-
1765
- this.state.activeIndex = index;
1766
- return this;
1767
- }
1768
- }
1553
+ // Handle classes (CardHandle, TableHandle, NavbarHandle, TabsHandle)
1554
+ // removed in v2.0.15 superseded by ComponentHandle.
1555
+ // See dev/dead-code-elimination-v2.0.15.md for recovery.
1769
1556
 
1770
1557
  /**
1771
1558
  * Create a code demo component for documentation pages
@@ -1820,18 +1607,16 @@ export function makeCodeDemo(props = {}) {
1820
1607
  {
1821
1608
  t: 'button',
1822
1609
  a: {
1823
- class: 'bw-copy-btn bw-code-copy-btn',
1824
- onclick: (e) => {
1825
- navigator.clipboard.writeText(code).then(() => {
1826
- const btn = e.target;
1827
- const originalText = btn.textContent;
1610
+ class: 'bw_copy_btn bw_code_copy_btn',
1611
+ onclick: function(e) {
1612
+ navigator.clipboard.writeText(code).then(function() {
1613
+ var btn = e.target;
1614
+ var originalText = btn.textContent;
1828
1615
  btn.textContent = 'Copied!';
1829
- btn.style.background = '#006666';
1830
- btn.style.color = '#fff';
1831
- setTimeout(() => {
1616
+ btn.classList.add('bw_code_copy_btn_copied');
1617
+ setTimeout(function() {
1832
1618
  btn.textContent = originalText;
1833
- btn.style.background = 'rgba(255,255,255,0.12)';
1834
- btn.style.color = '#aaa';
1619
+ btn.classList.remove('bw_code_copy_btn_copied');
1835
1620
  }, 2000);
1836
1621
  });
1837
1622
  }
@@ -1842,10 +1627,10 @@ export function makeCodeDemo(props = {}) {
1842
1627
  ? globalThis.bw.codeEditor({ code: code, lang: language === 'javascript' ? 'js' : language, readOnly: true, height: 'auto' })
1843
1628
  : {
1844
1629
  t: 'pre',
1845
- a: { class: 'bw-code-pre' },
1630
+ a: { class: 'bw_code_pre' },
1846
1631
  c: {
1847
1632
  t: 'code',
1848
- a: { class: `bw-code-block language-${language}` },
1633
+ a: { class: `bw_code_block language-${language}` },
1849
1634
  c: code
1850
1635
  }
1851
1636
  }
@@ -1858,7 +1643,7 @@ export function makeCodeDemo(props = {}) {
1858
1643
  title && { t: 'h3', c: title },
1859
1644
  description && {
1860
1645
  t: 'p',
1861
- a: { class: 'bw-text-muted', style: 'margin-bottom: 1rem;' },
1646
+ a: { class: 'bw_text_muted bw_mb_3' },
1862
1647
  c: description
1863
1648
  },
1864
1649
  makeTabs({ tabs, id: demoId })
@@ -1866,7 +1651,7 @@ export function makeCodeDemo(props = {}) {
1866
1651
 
1867
1652
  return {
1868
1653
  t: 'div',
1869
- a: { class: 'bw-code-demo' },
1654
+ a: { class: 'bw_code_demo' },
1870
1655
  c: content
1871
1656
  };
1872
1657
  }
@@ -1923,10 +1708,10 @@ export function makePagination(props = {}) {
1923
1708
  // Previous arrow
1924
1709
  items.push({
1925
1710
  t: 'li',
1926
- a: { class: `bw-page-item ${currentPage <= 1 ? 'bw-disabled' : ''}`.trim() },
1711
+ a: { class: `bw_page_item ${currentPage <= 1 ? 'bw_disabled' : ''}`.trim() },
1927
1712
  c: {
1928
1713
  t: 'a',
1929
- a: { class: 'bw-page-link', href: '#', onclick: handleClick(currentPage - 1), 'aria-label': 'Previous' },
1714
+ a: { class: 'bw_page_link', href: '#', onclick: handleClick(currentPage - 1), 'aria-label': 'Previous' },
1930
1715
  c: '\u2039'
1931
1716
  }
1932
1717
  });
@@ -1936,10 +1721,10 @@ export function makePagination(props = {}) {
1936
1721
  (function(pageNum) {
1937
1722
  items.push({
1938
1723
  t: 'li',
1939
- a: { class: `bw-page-item ${pageNum === currentPage ? 'bw-active' : ''}`.trim() },
1724
+ a: { class: `bw_page_item ${pageNum === currentPage ? 'bw_active' : ''}`.trim() },
1940
1725
  c: {
1941
1726
  t: 'a',
1942
- a: { class: 'bw-page-link', href: '#', onclick: handleClick(pageNum) },
1727
+ a: { class: 'bw_page_link', href: '#', onclick: handleClick(pageNum) },
1943
1728
  c: '' + pageNum
1944
1729
  }
1945
1730
  });
@@ -1949,10 +1734,10 @@ export function makePagination(props = {}) {
1949
1734
  // Next arrow
1950
1735
  items.push({
1951
1736
  t: 'li',
1952
- a: { class: `bw-page-item ${currentPage >= pages ? 'bw-disabled' : ''}`.trim() },
1737
+ a: { class: `bw_page_item ${currentPage >= pages ? 'bw_disabled' : ''}`.trim() },
1953
1738
  c: {
1954
1739
  t: 'a',
1955
- a: { class: 'bw-page-link', href: '#', onclick: handleClick(currentPage + 1), 'aria-label': 'Next' },
1740
+ a: { class: 'bw_page_link', href: '#', onclick: handleClick(currentPage + 1), 'aria-label': 'Next' },
1956
1741
  c: '\u203A'
1957
1742
  }
1958
1743
  });
@@ -1963,7 +1748,7 @@ export function makePagination(props = {}) {
1963
1748
  c: {
1964
1749
  t: 'ul',
1965
1750
  a: {
1966
- class: `bw-pagination ${size ? 'bw-pagination-' + size : ''} ${className}`.trim()
1751
+ class: `bw_pagination ${size ? 'bw_pagination_' + size : ''} ${className}`.trim()
1967
1752
  },
1968
1753
  c: items
1969
1754
  }
@@ -2005,13 +1790,13 @@ export function makeRadio(props = {}) {
2005
1790
 
2006
1791
  return {
2007
1792
  t: 'div',
2008
- a: { class: `bw-form-check ${className}`.trim() },
1793
+ a: { class: `bw_form_check ${className}`.trim() },
2009
1794
  c: [
2010
1795
  {
2011
1796
  t: 'input',
2012
1797
  a: {
2013
1798
  type: 'radio',
2014
- class: 'bw-form-check-input',
1799
+ class: 'bw_form_check_input',
2015
1800
  name,
2016
1801
  value,
2017
1802
  checked,
@@ -2022,7 +1807,7 @@ export function makeRadio(props = {}) {
2022
1807
  },
2023
1808
  label && {
2024
1809
  t: 'label',
2025
- a: { class: 'bw-form-check-label', for: id },
1810
+ a: { class: 'bw_form_check_label', for: id },
2026
1811
  c: label
2027
1812
  }
2028
1813
  ].filter(Boolean)
@@ -2059,7 +1844,7 @@ export function makeButtonGroup(props = {}) {
2059
1844
  return {
2060
1845
  t: 'div',
2061
1846
  a: {
2062
- class: `${vertical ? 'bw-btn-group-vertical' : 'bw-btn-group'} ${size ? 'bw-btn-group-' + size : ''} ${className}`.trim(),
1847
+ class: `${vertical ? 'bw_btn_group_vertical' : 'bw_btn_group'} ${size ? 'bw_btn_group_' + size : ''} ${className}`.trim(),
2063
1848
  role: 'group'
2064
1849
  },
2065
1850
  c: children
@@ -2099,53 +1884,71 @@ export function makeAccordion(props = {}) {
2099
1884
 
2100
1885
  return {
2101
1886
  t: 'div',
2102
- a: { class: `bw-accordion ${className}`.trim() },
1887
+ a: { class: `bw_accordion ${className}`.trim() },
2103
1888
  c: items.map(function(item, index) {
2104
1889
  return {
2105
1890
  t: 'div',
2106
- a: { class: 'bw-accordion-item' },
1891
+ a: { class: 'bw_accordion_item' },
2107
1892
  c: [
2108
1893
  {
2109
1894
  t: 'h2',
2110
- a: { class: 'bw-accordion-header' },
1895
+ a: { class: 'bw_accordion_header' },
2111
1896
  c: {
2112
1897
  t: 'button',
2113
1898
  a: {
2114
- class: `bw-accordion-button ${item.open ? '' : 'bw-collapsed'}`.trim(),
1899
+ class: `bw_accordion_button ${item.open ? '' : 'bw_collapsed'}`.trim(),
2115
1900
  type: 'button',
2116
1901
  'aria-expanded': item.open ? 'true' : 'false',
2117
1902
  'data-accordion-index': index,
2118
1903
  onclick: function(e) {
2119
- var btn = e.target.closest('.bw-accordion-button');
2120
- var accordionEl = btn.closest('.bw-accordion');
2121
- var accordionItem = btn.closest('.bw-accordion-item');
2122
- var collapse = accordionItem.querySelector('.bw-accordion-collapse');
2123
- var isOpen = collapse.classList.contains('bw-collapse-show');
1904
+ var btn = e.target.closest('.bw_accordion_button');
1905
+ var accordionEl = btn.closest('.bw_accordion');
1906
+ var accordionItem = btn.closest('.bw_accordion_item');
1907
+ var collapse = accordionItem.querySelector('.bw_accordion_collapse');
1908
+ var isOpen = collapse.classList.contains('bw_collapse_show');
2124
1909
 
2125
1910
  if (!multiOpen) {
2126
- // Close all siblings
2127
- var allCollapses = accordionEl.querySelectorAll('.bw-accordion-collapse');
2128
- var allButtons = accordionEl.querySelectorAll('.bw-accordion-button');
2129
- for (var j = 0; j < allCollapses.length; j++) {
2130
- allCollapses[j].classList.remove('bw-collapse-show');
2131
- allCollapses[j].style.maxHeight = null;
2132
- }
2133
- for (var k = 0; k < allButtons.length; k++) {
2134
- allButtons[k].classList.add('bw-collapsed');
2135
- allButtons[k].setAttribute('aria-expanded', 'false');
1911
+ // Animate-close all other open siblings
1912
+ var allItems = accordionEl.querySelectorAll('.bw_accordion_item');
1913
+ for (var j = 0; j < allItems.length; j++) {
1914
+ if (allItems[j] === accordionItem) continue;
1915
+ var sibCollapse = allItems[j].querySelector('.bw_accordion_collapse');
1916
+ var sibBtn = allItems[j].querySelector('.bw_accordion_button');
1917
+ if (sibCollapse.classList.contains('bw_collapse_show')) {
1918
+ sibCollapse.style.maxHeight = sibCollapse.scrollHeight + 'px';
1919
+ sibCollapse.offsetHeight; // force reflow
1920
+ sibCollapse.style.maxHeight = '0px';
1921
+ sibCollapse.classList.remove('bw_collapse_show');
1922
+ sibBtn.classList.add('bw_collapsed');
1923
+ sibBtn.setAttribute('aria-expanded', 'false');
1924
+ }
2136
1925
  }
2137
1926
  }
2138
1927
 
2139
1928
  if (isOpen) {
2140
- collapse.classList.remove('bw-collapse-show');
2141
- collapse.style.maxHeight = null;
2142
- btn.classList.add('bw-collapsed');
1929
+ // Animate close
1930
+ collapse.style.maxHeight = collapse.scrollHeight + 'px';
1931
+ collapse.offsetHeight; // force reflow
1932
+ collapse.style.maxHeight = '0px';
1933
+ collapse.classList.remove('bw_collapse_show');
1934
+ btn.classList.add('bw_collapsed');
2143
1935
  btn.setAttribute('aria-expanded', 'false');
2144
1936
  } else {
2145
- collapse.classList.add('bw-collapse-show');
1937
+ // Animate open
1938
+ collapse.classList.add('bw_collapse_show');
1939
+ collapse.style.maxHeight = '0px';
1940
+ collapse.offsetHeight; // force reflow
2146
1941
  collapse.style.maxHeight = collapse.scrollHeight + 'px';
2147
- btn.classList.remove('bw-collapsed');
1942
+ btn.classList.remove('bw_collapsed');
2148
1943
  btn.setAttribute('aria-expanded', 'true');
1944
+ // After transition, allow dynamic content sizing
1945
+ var onEnd = function(ev) {
1946
+ if (ev.propertyName === 'max-height' && collapse.classList.contains('bw_collapse_show')) {
1947
+ collapse.style.maxHeight = 'none';
1948
+ }
1949
+ collapse.removeEventListener('transitionend', onEnd);
1950
+ };
1951
+ collapse.addEventListener('transitionend', onEnd);
2149
1952
  }
2150
1953
  }
2151
1954
  },
@@ -2154,15 +1957,15 @@ export function makeAccordion(props = {}) {
2154
1957
  },
2155
1958
  {
2156
1959
  t: 'div',
2157
- a: { class: `bw-accordion-collapse ${item.open ? 'bw-collapse-show' : ''}`.trim() },
1960
+ a: { class: `bw_accordion_collapse ${item.open ? 'bw_collapse_show' : ''}`.trim() },
2158
1961
  c: {
2159
1962
  t: 'div',
2160
- a: { class: 'bw-accordion-body' },
1963
+ a: { class: 'bw_accordion_body' },
2161
1964
  c: item.content
2162
1965
  },
2163
1966
  o: item.open ? {
2164
1967
  mounted: function(el) {
2165
- el.style.maxHeight = el.scrollHeight + 'px';
1968
+ el.style.maxHeight = 'none';
2166
1969
  }
2167
1970
  } : undefined
2168
1971
  }
@@ -2176,60 +1979,8 @@ export function makeAccordion(props = {}) {
2176
1979
  };
2177
1980
  }
2178
1981
 
2179
- /**
2180
- * Imperative handle for a rendered modal component
2181
- *
2182
- * Provides `.show()`, `.hide()`, `.toggle()`, and `.destroy()` methods
2183
- * for controlling the modal programmatically.
2184
- *
2185
- * @category Component Handles
2186
- */
2187
- export class ModalHandle {
2188
- /**
2189
- * @param {Element} element - The modal backdrop DOM element
2190
- * @param {Object} taco - The original TACO object
2191
- */
2192
- constructor(element, taco) {
2193
- this.element = element;
2194
- this._taco = taco;
2195
- this._escHandler = null;
2196
- }
2197
-
2198
- /** Show the modal */
2199
- show() {
2200
- this.element.classList.add('bw-modal-show');
2201
- document.body.style.overflow = 'hidden';
2202
- return this;
2203
- }
2204
-
2205
- /** Hide the modal */
2206
- hide() {
2207
- this.element.classList.remove('bw-modal-show');
2208
- document.body.style.overflow = '';
2209
- return this;
2210
- }
2211
-
2212
- /** Toggle modal visibility */
2213
- toggle() {
2214
- if (this.element.classList.contains('bw-modal-show')) {
2215
- this.hide();
2216
- } else {
2217
- this.show();
2218
- }
2219
- return this;
2220
- }
2221
-
2222
- /** Remove the modal from DOM and clean up */
2223
- destroy() {
2224
- this.hide();
2225
- if (this._escHandler) {
2226
- document.removeEventListener('keydown', this._escHandler);
2227
- }
2228
- if (this.element.parentNode) {
2229
- this.element.parentNode.removeChild(this.element);
2230
- }
2231
- }
2232
- }
1982
+ // ModalHandle removed in v2.0.15 — superseded by ComponentHandle.
1983
+ // See dev/dead-code-elimination-v2.0.15.md for recovery.
2233
1984
 
2234
1985
  /**
2235
1986
  * Create a modal dialog overlay
@@ -2263,9 +2014,9 @@ export function makeModal(props = {}) {
2263
2014
  } = props;
2264
2015
 
2265
2016
  function closeModal(el) {
2266
- var backdrop = el.closest('.bw-modal');
2017
+ var backdrop = el.closest('.bw_modal');
2267
2018
  if (backdrop) {
2268
- backdrop.classList.remove('bw-modal-show');
2019
+ backdrop.classList.remove('bw_modal_show');
2269
2020
  document.body.style.overflow = '';
2270
2021
  }
2271
2022
  if (onClose) onClose();
@@ -2273,24 +2024,24 @@ export function makeModal(props = {}) {
2273
2024
 
2274
2025
  return {
2275
2026
  t: 'div',
2276
- a: { class: `bw-modal ${className}`.trim() },
2027
+ a: { class: `bw_modal ${className}`.trim() },
2277
2028
  c: {
2278
2029
  t: 'div',
2279
- a: { class: `bw-modal-dialog ${size ? 'bw-modal-' + size : ''}`.trim() },
2030
+ a: { class: `bw_modal_dialog ${size ? 'bw_modal_' + size : ''}`.trim() },
2280
2031
  c: {
2281
2032
  t: 'div',
2282
- a: { class: 'bw-modal-content' },
2033
+ a: { class: 'bw_modal_content' },
2283
2034
  c: [
2284
2035
  (title || closeButton) && {
2285
2036
  t: 'div',
2286
- a: { class: 'bw-modal-header' },
2037
+ a: { class: 'bw_modal_header' },
2287
2038
  c: [
2288
- title && { t: 'h5', a: { class: 'bw-modal-title' }, c: title },
2039
+ title && { t: 'h5', a: { class: 'bw_modal_title' }, c: title },
2289
2040
  closeButton && {
2290
2041
  t: 'button',
2291
2042
  a: {
2292
2043
  type: 'button',
2293
- class: 'bw-close',
2044
+ class: 'bw_close',
2294
2045
  'aria-label': 'Close',
2295
2046
  onclick: function(e) { closeModal(e.target); }
2296
2047
  },
@@ -2300,12 +2051,12 @@ export function makeModal(props = {}) {
2300
2051
  },
2301
2052
  content && {
2302
2053
  t: 'div',
2303
- a: { class: 'bw-modal-body' },
2054
+ a: { class: 'bw_modal_body' },
2304
2055
  c: content
2305
2056
  },
2306
2057
  footer && {
2307
2058
  t: 'div',
2308
- a: { class: 'bw-modal-footer' },
2059
+ a: { class: 'bw_modal_footer' },
2309
2060
  c: footer
2310
2061
  }
2311
2062
  ].filter(Boolean)
@@ -2320,7 +2071,7 @@ export function makeModal(props = {}) {
2320
2071
  });
2321
2072
  // Escape key to close
2322
2073
  var escHandler = function(e) {
2323
- if (e.key === 'Escape' && el.classList.contains('bw-modal-show')) {
2074
+ if (e.key === 'Escape' && el.classList.contains('bw_modal_show')) {
2324
2075
  closeModal(el);
2325
2076
  }
2326
2077
  };
@@ -2371,26 +2122,26 @@ export function makeToast(props = {}) {
2371
2122
  return {
2372
2123
  t: 'div',
2373
2124
  a: {
2374
- class: `bw-toast bw-toast-${variant} ${className}`.trim(),
2125
+ class: `bw_toast ${variantClass(variant)} ${className}`.trim(),
2375
2126
  role: 'alert',
2376
2127
  'data-position': position
2377
2128
  },
2378
2129
  c: [
2379
2130
  (title) && {
2380
2131
  t: 'div',
2381
- a: { class: 'bw-toast-header' },
2132
+ a: { class: 'bw_toast_header' },
2382
2133
  c: [
2383
2134
  { t: 'strong', c: title },
2384
2135
  {
2385
2136
  t: 'button',
2386
2137
  a: {
2387
2138
  type: 'button',
2388
- class: 'bw-close',
2139
+ class: 'bw_close',
2389
2140
  'aria-label': 'Close',
2390
2141
  onclick: function(e) {
2391
- var toast = e.target.closest('.bw-toast');
2142
+ var toast = e.target.closest('.bw_toast');
2392
2143
  if (toast) {
2393
- toast.classList.add('bw-toast-hiding');
2144
+ toast.classList.add('bw_toast_hiding');
2394
2145
  setTimeout(function() { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 300);
2395
2146
  }
2396
2147
  }
@@ -2401,7 +2152,7 @@ export function makeToast(props = {}) {
2401
2152
  },
2402
2153
  content && {
2403
2154
  t: 'div',
2404
- a: { class: 'bw-toast-body' },
2155
+ a: { class: 'bw_toast_body' },
2405
2156
  c: content
2406
2157
  }
2407
2158
  ].filter(Boolean),
@@ -2410,12 +2161,12 @@ export function makeToast(props = {}) {
2410
2161
  mounted: function(el) {
2411
2162
  // Trigger show animation
2412
2163
  requestAnimationFrame(function() {
2413
- el.classList.add('bw-toast-show');
2164
+ el.classList.add('bw_toast_show');
2414
2165
  });
2415
2166
  // Auto-dismiss
2416
2167
  if (autoDismiss) {
2417
2168
  setTimeout(function() {
2418
- el.classList.add('bw-toast-hiding');
2169
+ el.classList.add('bw_toast_hiding');
2419
2170
  setTimeout(function() { if (el.parentNode) el.parentNode.removeChild(el); }, 300);
2420
2171
  }, delay);
2421
2172
  }
@@ -2468,12 +2219,12 @@ export function makeDropdown(props = {}) {
2468
2219
  triggerTaco = {
2469
2220
  t: 'button',
2470
2221
  a: {
2471
- class: `bw-btn bw-btn-${variant} bw-dropdown-toggle`,
2222
+ class: `bw_btn ${variantClass(variant)} bw_dropdown_toggle`,
2472
2223
  type: 'button',
2473
2224
  onclick: function(e) {
2474
- var dropdown = e.target.closest('.bw-dropdown');
2475
- var menu = dropdown.querySelector('.bw-dropdown-menu');
2476
- menu.classList.toggle('bw-dropdown-show');
2225
+ var dropdown = e.target.closest('.bw_dropdown');
2226
+ var menu = dropdown.querySelector('.bw_dropdown_menu');
2227
+ menu.classList.toggle('bw_dropdown_show');
2477
2228
  }
2478
2229
  },
2479
2230
  c: trigger || 'Dropdown'
@@ -2484,26 +2235,26 @@ export function makeDropdown(props = {}) {
2484
2235
 
2485
2236
  return {
2486
2237
  t: 'div',
2487
- a: { class: `bw-dropdown ${className}`.trim() },
2238
+ a: { class: `bw_dropdown ${className}`.trim() },
2488
2239
  c: [
2489
2240
  triggerTaco,
2490
2241
  {
2491
2242
  t: 'div',
2492
- a: { class: `bw-dropdown-menu ${align === 'end' ? 'bw-dropdown-menu-end' : ''}`.trim() },
2243
+ a: { class: `bw_dropdown_menu ${align === 'end' ? 'bw_dropdown_menu_end' : ''}`.trim() },
2493
2244
  c: items.map(function(item) {
2494
2245
  if (item.divider) {
2495
- return { t: 'hr', a: { class: 'bw-dropdown-divider' } };
2246
+ return { t: 'hr', a: { class: 'bw_dropdown_divider' } };
2496
2247
  }
2497
2248
  return {
2498
2249
  t: 'a',
2499
2250
  a: {
2500
- class: `bw-dropdown-item ${item.disabled ? 'disabled' : ''}`.trim(),
2251
+ class: `bw_dropdown_item ${item.disabled ? 'disabled' : ''}`.trim(),
2501
2252
  href: item.href || '#',
2502
2253
  onclick: item.disabled ? undefined : function(e) {
2503
2254
  if (!item.href) e.preventDefault();
2504
- var dropdown = e.target.closest('.bw-dropdown');
2505
- var menu = dropdown.querySelector('.bw-dropdown-menu');
2506
- menu.classList.remove('bw-dropdown-show');
2255
+ var dropdown = e.target.closest('.bw_dropdown');
2256
+ var menu = dropdown.querySelector('.bw_dropdown_menu');
2257
+ menu.classList.remove('bw_dropdown_show');
2507
2258
  if (item.onclick) item.onclick(e);
2508
2259
  }
2509
2260
  },
@@ -2518,8 +2269,8 @@ export function makeDropdown(props = {}) {
2518
2269
  // Click outside to close
2519
2270
  var outsideHandler = function(e) {
2520
2271
  if (!el.contains(e.target)) {
2521
- var menu = el.querySelector('.bw-dropdown-menu');
2522
- if (menu) menu.classList.remove('bw-dropdown-show');
2272
+ var menu = el.querySelector('.bw_dropdown_menu');
2273
+ if (menu) menu.classList.remove('bw_dropdown_show');
2523
2274
  }
2524
2275
  };
2525
2276
  document.addEventListener('click', outsideHandler);
@@ -2566,13 +2317,13 @@ export function makeSwitch(props = {}) {
2566
2317
 
2567
2318
  return {
2568
2319
  t: 'div',
2569
- a: { class: `bw-form-check bw-form-switch ${className}`.trim() },
2320
+ a: { class: `bw_form_check bw_form_switch ${className}`.trim() },
2570
2321
  c: [
2571
2322
  {
2572
2323
  t: 'input',
2573
2324
  a: {
2574
2325
  type: 'checkbox',
2575
- class: 'bw-form-check-input bw-switch-input',
2326
+ class: 'bw_form_check_input bw_switch_input',
2576
2327
  role: 'switch',
2577
2328
  checked,
2578
2329
  id,
@@ -2583,7 +2334,7 @@ export function makeSwitch(props = {}) {
2583
2334
  },
2584
2335
  label && {
2585
2336
  t: 'label',
2586
- a: { class: 'bw-form-check-label', for: id },
2337
+ a: { class: 'bw_form_check_label', for: id },
2587
2338
  c: label
2588
2339
  }
2589
2340
  ].filter(Boolean)
@@ -2618,7 +2369,7 @@ export function makeSkeleton(props = {}) {
2618
2369
  return {
2619
2370
  t: 'div',
2620
2371
  a: {
2621
- class: `bw-skeleton bw-skeleton-circle ${className}`.trim(),
2372
+ class: `bw_skeleton bw_skeleton_circle ${className}`.trim(),
2622
2373
  style: { width: circleSize, height: circleSize }
2623
2374
  }
2624
2375
  };
@@ -2628,7 +2379,7 @@ export function makeSkeleton(props = {}) {
2628
2379
  return {
2629
2380
  t: 'div',
2630
2381
  a: {
2631
- class: `bw-skeleton bw-skeleton-rect ${className}`.trim(),
2382
+ class: `bw_skeleton bw_skeleton_rect ${className}`.trim(),
2632
2383
  style: {
2633
2384
  width: width || '100%',
2634
2385
  height: height || '120px'
@@ -2642,7 +2393,7 @@ export function makeSkeleton(props = {}) {
2642
2393
  return {
2643
2394
  t: 'div',
2644
2395
  a: {
2645
- class: `bw-skeleton bw-skeleton-text ${className}`.trim(),
2396
+ class: `bw_skeleton bw_skeleton_text ${className}`.trim(),
2646
2397
  style: {
2647
2398
  width: width || '100%',
2648
2399
  height: height || '1em'
@@ -2656,7 +2407,7 @@ export function makeSkeleton(props = {}) {
2656
2407
  lines.push({
2657
2408
  t: 'div',
2658
2409
  a: {
2659
- class: 'bw-skeleton bw-skeleton-text',
2410
+ class: 'bw_skeleton bw_skeleton_text',
2660
2411
  style: {
2661
2412
  width: i === count - 1 ? '75%' : (width || '100%'),
2662
2413
  height: height || '1em'
@@ -2667,7 +2418,7 @@ export function makeSkeleton(props = {}) {
2667
2418
 
2668
2419
  return {
2669
2420
  t: 'div',
2670
- a: { class: `bw-skeleton-group ${className}`.trim() },
2421
+ a: { class: `bw_skeleton_group ${className}`.trim() },
2671
2422
  c: lines
2672
2423
  };
2673
2424
  }
@@ -2702,7 +2453,7 @@ export function makeAvatar(props = {}) {
2702
2453
  return {
2703
2454
  t: 'img',
2704
2455
  a: {
2705
- class: `bw-avatar bw-avatar-${size} ${className}`.trim(),
2456
+ class: `bw_avatar bw_avatar_${size} ${className}`.trim(),
2706
2457
  src: src,
2707
2458
  alt: alt
2708
2459
  }
@@ -2712,7 +2463,7 @@ export function makeAvatar(props = {}) {
2712
2463
  return {
2713
2464
  t: 'div',
2714
2465
  a: {
2715
- class: `bw-avatar bw-avatar-${size} bw-avatar-${variant} ${className}`.trim()
2466
+ class: `bw_avatar bw_avatar_${size} ${variantClass(variant)} ${className}`.trim()
2716
2467
  },
2717
2468
  c: initials || ''
2718
2469
  };
@@ -2761,14 +2512,14 @@ export function makeCarousel(props = {}) {
2761
2512
 
2762
2513
  // Shared navigation logic
2763
2514
  function goToSlide(carouselEl, index) {
2764
- var total = carouselEl.querySelectorAll('.bw-carousel-slide').length;
2515
+ var total = carouselEl.querySelectorAll('.bw_carousel_slide').length;
2765
2516
  if (index < 0) index = total - 1;
2766
2517
  if (index >= total) index = 0;
2767
2518
  carouselEl.setAttribute('data-carousel-index', index);
2768
- var track = carouselEl.querySelector('.bw-carousel-track');
2519
+ var track = carouselEl.querySelector('.bw_carousel_track');
2769
2520
  track.style.transform = 'translateX(-' + (index * 100) + '%)';
2770
2521
  // Update indicators
2771
- var indicators = carouselEl.querySelectorAll('.bw-carousel-indicator');
2522
+ var indicators = carouselEl.querySelectorAll('.bw_carousel_indicator');
2772
2523
  for (var i = 0; i < indicators.length; i++) {
2773
2524
  if (i === index) {
2774
2525
  indicators[i].classList.add('active');
@@ -2787,14 +2538,14 @@ export function makeCarousel(props = {}) {
2787
2538
  item.content,
2788
2539
  item.caption && {
2789
2540
  t: 'div',
2790
- a: { class: 'bw-carousel-caption' },
2541
+ a: { class: 'bw_carousel_caption' },
2791
2542
  c: item.caption
2792
2543
  }
2793
2544
  ].filter(Boolean);
2794
2545
 
2795
2546
  return {
2796
2547
  t: 'div',
2797
- a: { class: 'bw-carousel-slide' },
2548
+ a: { class: 'bw_carousel_slide' },
2798
2549
  c: slideContent.length === 1 ? slideContent[0] : slideContent
2799
2550
  };
2800
2551
  });
@@ -2804,7 +2555,7 @@ export function makeCarousel(props = {}) {
2804
2555
  {
2805
2556
  t: 'div',
2806
2557
  a: {
2807
- class: 'bw-carousel-track',
2558
+ class: 'bw_carousel_track',
2808
2559
  style: 'transform: translateX(-' + (startIndex * 100) + '%)'
2809
2560
  },
2810
2561
  c: slides
@@ -2816,11 +2567,11 @@ export function makeCarousel(props = {}) {
2816
2567
  children.push({
2817
2568
  t: 'button',
2818
2569
  a: {
2819
- class: 'bw-carousel-control bw-carousel-control-prev',
2570
+ class: 'bw_carousel_control bw_carousel_control_prev',
2820
2571
  type: 'button',
2821
2572
  'aria-label': 'Previous slide',
2822
2573
  onclick: function(e) {
2823
- var carousel = e.target.closest('.bw-carousel');
2574
+ var carousel = e.target.closest('.bw_carousel');
2824
2575
  var idx = parseInt(carousel.getAttribute('data-carousel-index') || '0');
2825
2576
  goToSlide(carousel, idx - 1);
2826
2577
  }
@@ -2830,11 +2581,11 @@ export function makeCarousel(props = {}) {
2830
2581
  children.push({
2831
2582
  t: 'button',
2832
2583
  a: {
2833
- class: 'bw-carousel-control bw-carousel-control-next',
2584
+ class: 'bw_carousel_control bw_carousel_control_next',
2834
2585
  type: 'button',
2835
2586
  'aria-label': 'Next slide',
2836
2587
  onclick: function(e) {
2837
- var carousel = e.target.closest('.bw-carousel');
2588
+ var carousel = e.target.closest('.bw_carousel');
2838
2589
  var idx = parseInt(carousel.getAttribute('data-carousel-index') || '0');
2839
2590
  goToSlide(carousel, idx + 1);
2840
2591
  }
@@ -2847,17 +2598,17 @@ export function makeCarousel(props = {}) {
2847
2598
  if (showIndicators && items.length > 1) {
2848
2599
  children.push({
2849
2600
  t: 'div',
2850
- a: { class: 'bw-carousel-indicators' },
2601
+ a: { class: 'bw_carousel_indicators' },
2851
2602
  c: items.map(function(_, i) {
2852
2603
  return {
2853
2604
  t: 'button',
2854
2605
  a: {
2855
- class: 'bw-carousel-indicator' + (i === startIndex ? ' active' : ''),
2606
+ class: 'bw_carousel_indicator' + (i === startIndex ? ' active' : ''),
2856
2607
  type: 'button',
2857
2608
  'aria-label': 'Go to slide ' + (i + 1),
2858
2609
  'data-slide-index': i,
2859
2610
  onclick: function(e) {
2860
- var carousel = e.target.closest('.bw-carousel');
2611
+ var carousel = e.target.closest('.bw_carousel');
2861
2612
  var idx = parseInt(e.target.getAttribute('data-slide-index'));
2862
2613
  goToSlide(carousel, idx);
2863
2614
  }
@@ -2870,34 +2621,994 @@ export function makeCarousel(props = {}) {
2870
2621
  return {
2871
2622
  t: 'div',
2872
2623
  a: {
2873
- class: ('bw-carousel ' + className).trim(),
2624
+ class: ('bw_carousel ' + className).trim(),
2874
2625
  style: 'height: ' + height,
2626
+ tabindex: '0',
2627
+ 'aria-roledescription': 'carousel',
2875
2628
  'data-carousel-index': startIndex
2876
2629
  },
2877
2630
  c: children,
2878
2631
  o: {
2879
2632
  type: 'carousel',
2880
2633
  state: { activeIndex: startIndex, autoPlay: autoPlay, interval: interval },
2881
- mounted: autoPlay ? function(el) {
2882
- var intervalId = setInterval(function() {
2634
+ mounted: function(el) {
2635
+ // Keyboard navigation
2636
+ el.addEventListener('keydown', function(e) {
2883
2637
  var idx = parseInt(el.getAttribute('data-carousel-index') || '0');
2884
- goToSlide(el, idx + 1);
2885
- }, interval);
2886
- el._bw_carouselInterval = intervalId;
2887
- } : undefined,
2888
- unmount: autoPlay ? function(el) {
2638
+ if (e.key === 'ArrowLeft') {
2639
+ e.preventDefault();
2640
+ goToSlide(el, idx - 1);
2641
+ } else if (e.key === 'ArrowRight') {
2642
+ e.preventDefault();
2643
+ goToSlide(el, idx + 1);
2644
+ }
2645
+ });
2646
+ // Auto-play
2647
+ if (autoPlay) {
2648
+ var intervalId = setInterval(function() {
2649
+ var idx = parseInt(el.getAttribute('data-carousel-index') || '0');
2650
+ goToSlide(el, idx + 1);
2651
+ }, interval);
2652
+ el._bw_carouselInterval = intervalId;
2653
+ // Pause on hover/focus for usability
2654
+ el.addEventListener('mouseenter', function() {
2655
+ if (el._bw_carouselInterval) clearInterval(el._bw_carouselInterval);
2656
+ });
2657
+ el.addEventListener('mouseleave', function() {
2658
+ el._bw_carouselInterval = setInterval(function() {
2659
+ var idx = parseInt(el.getAttribute('data-carousel-index') || '0');
2660
+ goToSlide(el, idx + 1);
2661
+ }, interval);
2662
+ });
2663
+ }
2664
+ },
2665
+ unmount: function(el) {
2889
2666
  if (el._bw_carouselInterval) {
2890
2667
  clearInterval(el._bw_carouselInterval);
2891
2668
  }
2892
- } : undefined
2669
+ }
2893
2670
  }
2894
2671
  };
2895
2672
  }
2896
2673
 
2897
- export const componentHandles = {
2898
- card: CardHandle,
2899
- table: TableHandle,
2900
- navbar: NavbarHandle,
2901
- tabs: TabsHandle,
2902
- modal: ModalHandle
2903
- };
2674
+ // =========================================================================
2675
+ // Phase 4: Dashboard & Data Display
2676
+ // =========================================================================
2677
+
2678
+ /**
2679
+ * Create a stat card for dashboard metrics display
2680
+ *
2681
+ * Shows a large value with a label and optional change indicator.
2682
+ * Designed for dashboard grid layouts with left-border accent.
2683
+ *
2684
+ * @param {Object|string} [props] - Stat card configuration (string shorthand sets label)
2685
+ * @param {string|number} [props.value=0] - The main stat value to display
2686
+ * @param {string} [props.label] - Descriptive label below the value
2687
+ * @param {number} [props.change] - Percentage change indicator (positive = green arrow, negative = red)
2688
+ * @param {string} [props.format] - Value format ("number", "currency", "percent")
2689
+ * @param {string} [props.prefix] - Custom prefix (e.g. "$")
2690
+ * @param {string} [props.suffix] - Custom suffix (e.g. "%")
2691
+ * @param {string} [props.icon] - Icon content (emoji or text) shown above value
2692
+ * @param {string} [props.variant] - Left-border color variant ("primary", "success", "danger", etc.)
2693
+ * @param {string} [props.className] - Additional CSS classes
2694
+ * @param {Object} [props.style] - Inline style object
2695
+ * @returns {Object} TACO object representing a stat card
2696
+ * @category Component Builders
2697
+ * @example
2698
+ * const stat = makeStatCard({
2699
+ * value: 2345,
2700
+ * label: 'Active Users',
2701
+ * change: 5.3,
2702
+ * format: 'number',
2703
+ * variant: 'primary'
2704
+ * });
2705
+ */
2706
+ export function makeStatCard(props = {}) {
2707
+ if (typeof props === 'string') props = { label: props };
2708
+ var {
2709
+ value = 0,
2710
+ label,
2711
+ change,
2712
+ format,
2713
+ prefix,
2714
+ suffix,
2715
+ icon,
2716
+ variant,
2717
+ className = '',
2718
+ style
2719
+ } = props;
2720
+
2721
+ function formatValue(val, fmt) {
2722
+ if (prefix || suffix) return (prefix || '') + val + (suffix || '');
2723
+ switch (fmt) {
2724
+ case 'currency': return '$' + Number(val).toLocaleString();
2725
+ case 'percent': return val + '%';
2726
+ case 'number': return Number(val).toLocaleString();
2727
+ default: return '' + val;
2728
+ }
2729
+ }
2730
+
2731
+ var classes = [
2732
+ 'bw_stat_card',
2733
+ variantClass(variant),
2734
+ className
2735
+ ].filter(Boolean).join(' ').trim();
2736
+
2737
+ var children = [];
2738
+
2739
+ if (icon) {
2740
+ children.push({
2741
+ t: 'div',
2742
+ a: { class: 'bw_stat_icon' },
2743
+ c: icon
2744
+ });
2745
+ }
2746
+
2747
+ children.push({
2748
+ t: 'div',
2749
+ a: { class: 'bw_stat_value' },
2750
+ c: formatValue(value, format)
2751
+ });
2752
+
2753
+ if (label) {
2754
+ children.push({
2755
+ t: 'div',
2756
+ a: { class: 'bw_stat_label' },
2757
+ c: label
2758
+ });
2759
+ }
2760
+
2761
+ if (change !== undefined && change !== null) {
2762
+ children.push({
2763
+ t: 'div',
2764
+ a: {
2765
+ class: 'bw_stat_change ' + (change >= 0 ? 'bw_stat_change_up' : 'bw_stat_change_down')
2766
+ },
2767
+ c: (change >= 0 ? '\u2191 +' : '\u2193 ') + change + '%'
2768
+ });
2769
+ }
2770
+
2771
+ return {
2772
+ t: 'div',
2773
+ a: { class: classes, style: style },
2774
+ c: children,
2775
+ o: { type: 'stat-card' }
2776
+ };
2777
+ }
2778
+
2779
+ // =========================================================================
2780
+ // Phase 5: Overlays & Popovers
2781
+ // =========================================================================
2782
+
2783
+ /**
2784
+ * Create a tooltip wrapper around trigger content
2785
+ *
2786
+ * Wraps the trigger element in a container that shows tooltip text
2787
+ * on hover and focus. Pure CSS-driven show/hide with JS lifecycle
2788
+ * for event binding.
2789
+ *
2790
+ * @param {Object} [props] - Tooltip configuration
2791
+ * @param {string|Object|Array} [props.content] - Trigger content (what the user hovers/focuses)
2792
+ * @param {string} [props.text=""] - Tooltip text to display
2793
+ * @param {string} [props.placement="top"] - Tooltip placement ("top", "bottom", "left", "right")
2794
+ * @param {string} [props.className] - Additional CSS classes
2795
+ * @returns {Object} TACO object representing a tooltip wrapper
2796
+ * @category Component Builders
2797
+ * @example
2798
+ * const tip = makeTooltip({
2799
+ * content: makeButton({ text: 'Hover me' }),
2800
+ * text: 'This is a tooltip!',
2801
+ * placement: 'top'
2802
+ * });
2803
+ */
2804
+ export function makeTooltip(props = {}) {
2805
+ var {
2806
+ content,
2807
+ text = '',
2808
+ placement = 'top',
2809
+ className = ''
2810
+ } = props;
2811
+
2812
+ return {
2813
+ t: 'span',
2814
+ a: { class: ('bw_tooltip_wrapper ' + className).trim() },
2815
+ c: [
2816
+ content,
2817
+ {
2818
+ t: 'span',
2819
+ a: {
2820
+ class: 'bw_tooltip bw_tooltip_' + placement,
2821
+ role: 'tooltip'
2822
+ },
2823
+ c: text
2824
+ }
2825
+ ],
2826
+ o: {
2827
+ type: 'tooltip',
2828
+ mounted: function(el) {
2829
+ var tip = el.querySelector('.bw_tooltip');
2830
+ el.addEventListener('mouseenter', function() {
2831
+ tip.classList.add('bw_tooltip_show');
2832
+ });
2833
+ el.addEventListener('mouseleave', function() {
2834
+ tip.classList.remove('bw_tooltip_show');
2835
+ });
2836
+ el.addEventListener('focusin', function() {
2837
+ tip.classList.add('bw_tooltip_show');
2838
+ });
2839
+ el.addEventListener('focusout', function() {
2840
+ tip.classList.remove('bw_tooltip_show');
2841
+ });
2842
+ }
2843
+ }
2844
+ };
2845
+ }
2846
+
2847
+ /**
2848
+ * Create a popover wrapper around trigger content
2849
+ *
2850
+ * Like a tooltip but richer — supports title + body content and is
2851
+ * triggered by click rather than hover. Dismisses on click outside.
2852
+ *
2853
+ * @param {Object} [props] - Popover configuration
2854
+ * @param {string|Object|Array} [props.trigger] - Trigger content (what the user clicks)
2855
+ * @param {string} [props.title] - Popover header title
2856
+ * @param {string|Object|Array} [props.content] - Popover body content
2857
+ * @param {string} [props.placement="top"] - Placement ("top", "bottom", "left", "right")
2858
+ * @param {string} [props.className] - Additional CSS classes
2859
+ * @returns {Object} TACO object representing a popover wrapper
2860
+ * @category Component Builders
2861
+ * @example
2862
+ * const pop = makePopover({
2863
+ * trigger: makeButton({ text: 'Click me' }),
2864
+ * title: 'Popover Title',
2865
+ * content: 'Some helpful information here.',
2866
+ * placement: 'bottom'
2867
+ * });
2868
+ */
2869
+ export function makePopover(props = {}) {
2870
+ var {
2871
+ trigger,
2872
+ title,
2873
+ content,
2874
+ placement = 'top',
2875
+ className = ''
2876
+ } = props;
2877
+
2878
+ var popoverContent = [
2879
+ title && {
2880
+ t: 'div',
2881
+ a: { class: 'bw_popover_header' },
2882
+ c: title
2883
+ },
2884
+ content && {
2885
+ t: 'div',
2886
+ a: { class: 'bw_popover_body' },
2887
+ c: content
2888
+ }
2889
+ ].filter(Boolean);
2890
+
2891
+ return {
2892
+ t: 'span',
2893
+ a: { class: ('bw_popover_wrapper ' + className).trim() },
2894
+ c: [
2895
+ {
2896
+ t: 'span',
2897
+ a: {
2898
+ class: 'bw_popover_trigger',
2899
+ onclick: function(e) {
2900
+ var wrapper = e.target.closest('.bw_popover_wrapper');
2901
+ var pop = wrapper.querySelector('.bw_popover');
2902
+ pop.classList.toggle('bw_popover_show');
2903
+ }
2904
+ },
2905
+ c: trigger
2906
+ },
2907
+ {
2908
+ t: 'div',
2909
+ a: {
2910
+ class: 'bw_popover bw_popover_' + placement
2911
+ },
2912
+ c: popoverContent
2913
+ }
2914
+ ],
2915
+ o: {
2916
+ type: 'popover',
2917
+ mounted: function(el) {
2918
+ // Click outside to close
2919
+ var outsideHandler = function(e) {
2920
+ if (!el.contains(e.target)) {
2921
+ var pop = el.querySelector('.bw_popover');
2922
+ if (pop) pop.classList.remove('bw_popover_show');
2923
+ }
2924
+ };
2925
+ document.addEventListener('click', outsideHandler);
2926
+ el._bw_outsideHandler = outsideHandler;
2927
+ },
2928
+ unmount: function(el) {
2929
+ if (el._bw_outsideHandler) {
2930
+ document.removeEventListener('click', el._bw_outsideHandler);
2931
+ }
2932
+ }
2933
+ }
2934
+ };
2935
+ }
2936
+
2937
+ // =========================================================================
2938
+ // Phase 6: Form Enhancements & Layout
2939
+ // =========================================================================
2940
+
2941
+ /**
2942
+ * Create a search input with clear button
2943
+ *
2944
+ * Wraps a text input with a clear (×) button that appears when
2945
+ * the field has content. Calls onSearch on Enter key.
2946
+ *
2947
+ * @param {Object} [props] - Search input configuration
2948
+ * @param {string} [props.placeholder="Search..."] - Placeholder text
2949
+ * @param {string} [props.value] - Initial value
2950
+ * @param {Function} [props.onSearch] - Callback when Enter is pressed, receives value
2951
+ * @param {Function} [props.onInput] - Callback on each keystroke, receives value
2952
+ * @param {string} [props.id] - Element ID
2953
+ * @param {string} [props.name] - Input name attribute
2954
+ * @param {string} [props.className] - Additional CSS classes
2955
+ * @returns {Object} TACO object representing a search input
2956
+ * @category Component Builders
2957
+ * @example
2958
+ * const search = makeSearchInput({
2959
+ * placeholder: 'Search users...',
2960
+ * onSearch: (val) => filterUsers(val)
2961
+ * });
2962
+ */
2963
+ export function makeSearchInput(props = {}) {
2964
+ if (typeof props === 'string') props = { placeholder: props };
2965
+ var {
2966
+ placeholder = 'Search...',
2967
+ value,
2968
+ onSearch,
2969
+ onInput,
2970
+ id,
2971
+ name,
2972
+ className = ''
2973
+ } = props;
2974
+
2975
+ return {
2976
+ t: 'div',
2977
+ a: { class: ('bw_search_input ' + className).trim() },
2978
+ c: [
2979
+ {
2980
+ t: 'input',
2981
+ a: {
2982
+ type: 'search',
2983
+ class: 'bw_form_control bw_search_field',
2984
+ placeholder: placeholder,
2985
+ value: value,
2986
+ id: id,
2987
+ name: name,
2988
+ onkeydown: function(e) {
2989
+ if (e.key === 'Enter' && onSearch) {
2990
+ e.preventDefault();
2991
+ onSearch(e.target.value);
2992
+ }
2993
+ },
2994
+ oninput: function(e) {
2995
+ var wrapper = e.target.closest('.bw_search_input');
2996
+ var clearBtn = wrapper.querySelector('.bw_search_clear');
2997
+ if (clearBtn) {
2998
+ clearBtn.style.display = e.target.value ? 'flex' : 'none';
2999
+ }
3000
+ if (onInput) onInput(e.target.value);
3001
+ }
3002
+ }
3003
+ },
3004
+ {
3005
+ t: 'button',
3006
+ a: {
3007
+ type: 'button',
3008
+ class: 'bw_search_clear',
3009
+ 'aria-label': 'Clear search',
3010
+ style: value ? undefined : 'display: none',
3011
+ onclick: function(e) {
3012
+ var wrapper = e.target.closest('.bw_search_input');
3013
+ var input = wrapper.querySelector('.bw_search_field');
3014
+ input.value = '';
3015
+ e.target.style.display = 'none';
3016
+ input.focus();
3017
+ if (onInput) onInput('');
3018
+ if (onSearch) onSearch('');
3019
+ }
3020
+ },
3021
+ c: '\u00D7'
3022
+ }
3023
+ ],
3024
+ o: { type: 'search-input' }
3025
+ };
3026
+ }
3027
+
3028
+ /**
3029
+ * Create a styled range slider input
3030
+ *
3031
+ * @param {Object} [props] - Range configuration
3032
+ * @param {number} [props.min=0] - Minimum value
3033
+ * @param {number} [props.max=100] - Maximum value
3034
+ * @param {number} [props.step=1] - Step increment
3035
+ * @param {number} [props.value=50] - Current value
3036
+ * @param {string} [props.label] - Label text
3037
+ * @param {boolean} [props.showValue=false] - Show current value display
3038
+ * @param {string} [props.id] - Element ID
3039
+ * @param {string} [props.name] - Input name attribute
3040
+ * @param {boolean} [props.disabled=false] - Whether the slider is disabled
3041
+ * @param {string} [props.className] - Additional CSS classes
3042
+ * @returns {Object} TACO object representing a range input
3043
+ * @category Component Builders
3044
+ * @example
3045
+ * const slider = makeRange({
3046
+ * min: 0, max: 100, value: 50,
3047
+ * label: 'Volume',
3048
+ * showValue: true,
3049
+ * oninput: (e) => setVolume(e.target.value)
3050
+ * });
3051
+ */
3052
+ export function makeRange(props = {}) {
3053
+ var {
3054
+ min = 0,
3055
+ max = 100,
3056
+ step = 1,
3057
+ value = 50,
3058
+ label,
3059
+ showValue = false,
3060
+ id,
3061
+ name,
3062
+ disabled = false,
3063
+ className = '',
3064
+ ...eventHandlers
3065
+ } = props;
3066
+
3067
+ var children = [];
3068
+
3069
+ if (label || showValue) {
3070
+ var labelContent = [];
3071
+ if (label) {
3072
+ labelContent.push({
3073
+ t: 'span',
3074
+ c: label
3075
+ });
3076
+ }
3077
+ if (showValue) {
3078
+ labelContent.push({
3079
+ t: 'span',
3080
+ a: { class: 'bw_range_value' },
3081
+ c: '' + value
3082
+ });
3083
+ }
3084
+ children.push({
3085
+ t: 'div',
3086
+ a: { class: 'bw_range_label' },
3087
+ c: labelContent
3088
+ });
3089
+ }
3090
+
3091
+ // Wrap oninput to update value display
3092
+ var userOnInput = eventHandlers.oninput;
3093
+ if (showValue) {
3094
+ eventHandlers.oninput = function(e) {
3095
+ var wrapper = e.target.closest('.bw_range_wrapper');
3096
+ var valDisplay = wrapper.querySelector('.bw_range_value');
3097
+ if (valDisplay) valDisplay.textContent = e.target.value;
3098
+ if (userOnInput) userOnInput(e);
3099
+ };
3100
+ }
3101
+
3102
+ children.push({
3103
+ t: 'input',
3104
+ a: {
3105
+ type: 'range',
3106
+ class: 'bw_range',
3107
+ min: min,
3108
+ max: max,
3109
+ step: step,
3110
+ value: value,
3111
+ id: id,
3112
+ name: name,
3113
+ disabled: disabled,
3114
+ ...eventHandlers
3115
+ }
3116
+ });
3117
+
3118
+ return {
3119
+ t: 'div',
3120
+ a: { class: ('bw_range_wrapper ' + className).trim() },
3121
+ c: children,
3122
+ o: { type: 'range' }
3123
+ };
3124
+ }
3125
+
3126
+ /**
3127
+ * Create a media object layout (image + text side-by-side)
3128
+ *
3129
+ * Classic media object pattern: image/icon on one side, text content
3130
+ * on the other, using flexbox. Supports reversed layout.
3131
+ *
3132
+ * @param {Object} [props] - Media object configuration
3133
+ * @param {string} [props.src] - Image source URL
3134
+ * @param {string} [props.alt=""] - Image alt text
3135
+ * @param {string} [props.title] - Title text
3136
+ * @param {string|Object|Array} [props.content] - Body content
3137
+ * @param {boolean} [props.reverse=false] - Put image on the right
3138
+ * @param {string} [props.imageSize="3rem"] - Image width/height
3139
+ * @param {string} [props.className] - Additional CSS classes
3140
+ * @returns {Object} TACO object representing a media object
3141
+ * @category Component Builders
3142
+ * @example
3143
+ * const media = makeMediaObject({
3144
+ * src: '/avatar.jpg',
3145
+ * title: 'Jane Doe',
3146
+ * content: 'Posted a comment 5 minutes ago.'
3147
+ * });
3148
+ */
3149
+ export function makeMediaObject(props = {}) {
3150
+ var {
3151
+ src,
3152
+ alt = '',
3153
+ title,
3154
+ content,
3155
+ reverse = false,
3156
+ imageSize = '3rem',
3157
+ className = ''
3158
+ } = props;
3159
+
3160
+ var imgEl = src ? {
3161
+ t: 'img',
3162
+ a: {
3163
+ class: 'bw_media_img',
3164
+ src: src,
3165
+ alt: alt,
3166
+ style: 'width:' + imageSize + ';height:' + imageSize
3167
+ }
3168
+ } : null;
3169
+
3170
+ var bodyEl = {
3171
+ t: 'div',
3172
+ a: { class: 'bw_media_body' },
3173
+ c: [
3174
+ title && { t: 'h5', a: { class: 'bw_media_title' }, c: title },
3175
+ content
3176
+ ].filter(Boolean)
3177
+ };
3178
+
3179
+ return {
3180
+ t: 'div',
3181
+ a: { class: ('bw_media ' + (reverse ? 'bw_media_reverse ' : '') + className).trim() },
3182
+ c: reverse
3183
+ ? [bodyEl, imgEl].filter(Boolean)
3184
+ : [imgEl, bodyEl].filter(Boolean),
3185
+ o: { type: 'media-object' }
3186
+ };
3187
+ }
3188
+
3189
+ /**
3190
+ * Create a file upload zone with drag-and-drop support
3191
+ *
3192
+ * Styled drop zone with file input. Supports drag-and-drop visuals
3193
+ * and multiple file selection.
3194
+ *
3195
+ * @param {Object} [props] - File upload configuration
3196
+ * @param {string} [props.accept] - Accepted file types (e.g. "image/*", ".pdf,.doc")
3197
+ * @param {boolean} [props.multiple=false] - Allow multiple file selection
3198
+ * @param {Function} [props.onFiles] - Callback when files are selected, receives FileList
3199
+ * @param {string} [props.text="Drop files here or click to browse"] - Zone label text
3200
+ * @param {string} [props.id] - Element ID
3201
+ * @param {string} [props.className] - Additional CSS classes
3202
+ * @returns {Object} TACO object representing a file upload zone
3203
+ * @category Component Builders
3204
+ * @example
3205
+ * const upload = makeFileUpload({
3206
+ * accept: 'image/*',
3207
+ * multiple: true,
3208
+ * onFiles: (files) => uploadFiles(files)
3209
+ * });
3210
+ */
3211
+ export function makeFileUpload(props = {}) {
3212
+ var {
3213
+ accept,
3214
+ multiple = false,
3215
+ onFiles,
3216
+ text = 'Drop files here or click to browse',
3217
+ id,
3218
+ className = ''
3219
+ } = props;
3220
+
3221
+ return {
3222
+ t: 'div',
3223
+ a: {
3224
+ class: ('bw_file_upload ' + className).trim(),
3225
+ tabindex: '0',
3226
+ role: 'button',
3227
+ 'aria-label': text
3228
+ },
3229
+ c: [
3230
+ { t: 'div', a: { class: 'bw_file_upload_icon' }, c: '\uD83D\uDCC1' },
3231
+ { t: 'div', a: { class: 'bw_file_upload_text' }, c: text },
3232
+ {
3233
+ t: 'input',
3234
+ a: {
3235
+ type: 'file',
3236
+ class: 'bw_file_upload_input',
3237
+ accept: accept,
3238
+ multiple: multiple,
3239
+ id: id,
3240
+ onchange: function(e) {
3241
+ if (onFiles && e.target.files.length) onFiles(e.target.files);
3242
+ }
3243
+ }
3244
+ }
3245
+ ],
3246
+ o: {
3247
+ type: 'file-upload',
3248
+ mounted: function(el) {
3249
+ var input = el.querySelector('.bw_file_upload_input');
3250
+
3251
+ // Click zone to trigger file input
3252
+ el.addEventListener('click', function(e) {
3253
+ if (e.target !== input) input.click();
3254
+ });
3255
+
3256
+ // Keyboard activation
3257
+ el.addEventListener('keydown', function(e) {
3258
+ if (e.key === 'Enter' || e.key === ' ') {
3259
+ e.preventDefault();
3260
+ input.click();
3261
+ }
3262
+ });
3263
+
3264
+ // Drag-and-drop visuals
3265
+ el.addEventListener('dragover', function(e) {
3266
+ e.preventDefault();
3267
+ el.classList.add('bw_file_upload_active');
3268
+ });
3269
+ el.addEventListener('dragleave', function() {
3270
+ el.classList.remove('bw_file_upload_active');
3271
+ });
3272
+ el.addEventListener('drop', function(e) {
3273
+ e.preventDefault();
3274
+ el.classList.remove('bw_file_upload_active');
3275
+ if (onFiles && e.dataTransfer.files.length) onFiles(e.dataTransfer.files);
3276
+ });
3277
+ }
3278
+ }
3279
+ };
3280
+ }
3281
+
3282
+ // =========================================================================
3283
+ // Phase 7: Data Display & Workflow
3284
+ // =========================================================================
3285
+
3286
+ /**
3287
+ * Create a vertical timeline for chronological event display
3288
+ *
3289
+ * Renders events as a vertical line with markers and content cards.
3290
+ * Each item can have a colored variant marker.
3291
+ *
3292
+ * @param {Object} [props] - Timeline configuration
3293
+ * @param {Array<Object>} [props.items=[]] - Timeline events
3294
+ * @param {string} [props.items[].title] - Event title
3295
+ * @param {string|Object|Array} [props.items[].content] - Event description content
3296
+ * @param {string} [props.items[].date] - Date or time label
3297
+ * @param {string} [props.items[].variant="primary"] - Marker color variant
3298
+ * @param {string} [props.className] - Additional CSS classes
3299
+ * @returns {Object} TACO object representing a timeline
3300
+ * @category Component Builders
3301
+ * @example
3302
+ * const timeline = makeTimeline({
3303
+ * items: [
3304
+ * { title: 'Project Started', date: 'Jan 2026', variant: 'primary' },
3305
+ * { title: 'Beta Release', date: 'Mar 2026', content: 'v2.0 beta shipped' },
3306
+ * { title: 'Stable Release', date: 'Jun 2026', variant: 'success' }
3307
+ * ]
3308
+ * });
3309
+ */
3310
+ export function makeTimeline(props = {}) {
3311
+ var {
3312
+ items = [],
3313
+ className = ''
3314
+ } = props;
3315
+
3316
+ return {
3317
+ t: 'div',
3318
+ a: { class: ('bw_timeline ' + className).trim() },
3319
+ c: items.map(function(item) {
3320
+ return {
3321
+ t: 'div',
3322
+ a: { class: 'bw_timeline_item' },
3323
+ c: [
3324
+ {
3325
+ t: 'div',
3326
+ a: { class: 'bw_timeline_marker ' + variantClass(item.variant || 'primary') }
3327
+ },
3328
+ {
3329
+ t: 'div',
3330
+ a: { class: 'bw_timeline_content' },
3331
+ c: [
3332
+ item.date && {
3333
+ t: 'div',
3334
+ a: { class: 'bw_timeline_date' },
3335
+ c: item.date
3336
+ },
3337
+ item.title && {
3338
+ t: 'h5',
3339
+ a: { class: 'bw_timeline_title' },
3340
+ c: item.title
3341
+ },
3342
+ item.content && (typeof item.content === 'string'
3343
+ ? { t: 'p', a: { class: 'bw_timeline_text' }, c: item.content }
3344
+ : item.content)
3345
+ ].filter(Boolean)
3346
+ }
3347
+ ]
3348
+ };
3349
+ }),
3350
+ o: { type: 'timeline' }
3351
+ };
3352
+ }
3353
+
3354
+ /**
3355
+ * Create a multi-step wizard/progress indicator
3356
+ *
3357
+ * Displays numbered steps with active and completed states.
3358
+ * Steps before currentStep are marked completed, the currentStep
3359
+ * is active, and subsequent steps are pending.
3360
+ *
3361
+ * @param {Object} [props] - Stepper configuration
3362
+ * @param {Array<Object>} [props.steps=[]] - Step definitions
3363
+ * @param {string} [props.steps[].label] - Step label text
3364
+ * @param {string} [props.steps[].description] - Optional step description
3365
+ * @param {number} [props.currentStep=0] - Zero-based index of the active step
3366
+ * @param {string} [props.className] - Additional CSS classes
3367
+ * @returns {Object} TACO object representing a stepper
3368
+ * @category Component Builders
3369
+ * @example
3370
+ * const stepper = makeStepper({
3371
+ * currentStep: 1,
3372
+ * steps: [
3373
+ * { label: 'Account', description: 'Create account' },
3374
+ * { label: 'Profile', description: 'Set up profile' },
3375
+ * { label: 'Confirm', description: 'Review & submit' }
3376
+ * ]
3377
+ * });
3378
+ */
3379
+ export function makeStepper(props = {}) {
3380
+ var {
3381
+ steps = [],
3382
+ currentStep = 0,
3383
+ className = ''
3384
+ } = props;
3385
+
3386
+ return {
3387
+ t: 'div',
3388
+ a: { class: ('bw_stepper ' + className).trim(), role: 'list' },
3389
+ c: steps.map(function(step, index) {
3390
+ var state = index < currentStep ? 'completed' : index === currentStep ? 'active' : 'pending';
3391
+ return {
3392
+ t: 'div',
3393
+ a: {
3394
+ class: 'bw_step bw_step_' + state,
3395
+ role: 'listitem',
3396
+ 'aria-current': state === 'active' ? 'step' : undefined
3397
+ },
3398
+ c: [
3399
+ {
3400
+ t: 'div',
3401
+ a: { class: 'bw_step_indicator' },
3402
+ c: state === 'completed' ? '\u2713' : '' + (index + 1)
3403
+ },
3404
+ {
3405
+ t: 'div',
3406
+ a: { class: 'bw_step_body' },
3407
+ c: [
3408
+ { t: 'div', a: { class: 'bw_step_label' }, c: step.label },
3409
+ step.description && { t: 'div', a: { class: 'bw_step_description' }, c: step.description }
3410
+ ].filter(Boolean)
3411
+ }
3412
+ ]
3413
+ };
3414
+ }),
3415
+ o: { type: 'stepper' }
3416
+ };
3417
+ }
3418
+
3419
+ /**
3420
+ * Create a chip/tag input for managing a list of items
3421
+ *
3422
+ * Displays existing chips with remove buttons and an input field
3423
+ * for adding new ones. Chips are added on Enter and removed on
3424
+ * clicking the × button.
3425
+ *
3426
+ * @param {Object} [props] - Chip input configuration
3427
+ * @param {Array<string>} [props.chips=[]] - Initial chip values
3428
+ * @param {string} [props.placeholder="Add..."] - Input placeholder text
3429
+ * @param {Function} [props.onAdd] - Callback when a chip is added, receives value
3430
+ * @param {Function} [props.onRemove] - Callback when a chip is removed, receives value
3431
+ * @param {string} [props.className] - Additional CSS classes
3432
+ * @returns {Object} TACO object representing a chip input
3433
+ * @category Component Builders
3434
+ * @example
3435
+ * const tags = makeChipInput({
3436
+ * chips: ['JavaScript', 'CSS'],
3437
+ * placeholder: 'Add tag...',
3438
+ * onAdd: (val) => addTag(val),
3439
+ * onRemove: (val) => removeTag(val)
3440
+ * });
3441
+ */
3442
+ export function makeChipInput(props = {}) {
3443
+ var {
3444
+ chips = [],
3445
+ placeholder = 'Add...',
3446
+ onAdd,
3447
+ onRemove,
3448
+ className = ''
3449
+ } = props;
3450
+
3451
+ function makeChipEl(text) {
3452
+ return {
3453
+ t: 'span',
3454
+ a: { class: 'bw_chip', 'data-chip-value': text },
3455
+ c: [
3456
+ text,
3457
+ {
3458
+ t: 'button',
3459
+ a: {
3460
+ type: 'button',
3461
+ class: 'bw_chip_remove',
3462
+ 'aria-label': 'Remove ' + text,
3463
+ onclick: function(e) {
3464
+ var chip = e.target.closest('.bw_chip');
3465
+ var val = chip.getAttribute('data-chip-value');
3466
+ chip.parentNode.removeChild(chip);
3467
+ if (onRemove) onRemove(val);
3468
+ }
3469
+ },
3470
+ c: '\u00D7'
3471
+ }
3472
+ ]
3473
+ };
3474
+ }
3475
+
3476
+ return {
3477
+ t: 'div',
3478
+ a: { class: ('bw_chip_input ' + className).trim() },
3479
+ c: [
3480
+ ...chips.map(makeChipEl),
3481
+ {
3482
+ t: 'input',
3483
+ a: {
3484
+ type: 'text',
3485
+ class: 'bw_chip_field',
3486
+ placeholder: placeholder,
3487
+ onkeydown: function(e) {
3488
+ if (e.key === 'Enter' && e.target.value.trim()) {
3489
+ e.preventDefault();
3490
+ var val = e.target.value.trim();
3491
+ var wrapper = e.target.closest('.bw_chip_input');
3492
+ // Insert chip before the input
3493
+ var chipEl = document.createElement('span');
3494
+ chipEl.className = 'bw_chip';
3495
+ chipEl.setAttribute('data-chip-value', val);
3496
+ chipEl.innerHTML = '';
3497
+ chipEl.textContent = val;
3498
+ var removeBtn = document.createElement('button');
3499
+ removeBtn.type = 'button';
3500
+ removeBtn.className = 'bw_chip_remove';
3501
+ removeBtn.setAttribute('aria-label', 'Remove ' + val);
3502
+ removeBtn.textContent = '\u00D7';
3503
+ removeBtn.onclick = function() {
3504
+ chipEl.parentNode.removeChild(chipEl);
3505
+ if (onRemove) onRemove(val);
3506
+ };
3507
+ chipEl.appendChild(removeBtn);
3508
+ wrapper.insertBefore(chipEl, e.target);
3509
+ e.target.value = '';
3510
+ if (onAdd) onAdd(val);
3511
+ }
3512
+ // Backspace on empty input removes last chip
3513
+ if (e.key === 'Backspace' && !e.target.value) {
3514
+ var wrapper = e.target.closest('.bw_chip_input');
3515
+ var chipEls = wrapper.querySelectorAll('.bw_chip');
3516
+ if (chipEls.length) {
3517
+ var last = chipEls[chipEls.length - 1];
3518
+ var removedVal = last.getAttribute('data-chip-value');
3519
+ last.parentNode.removeChild(last);
3520
+ if (onRemove) onRemove(removedVal);
3521
+ }
3522
+ }
3523
+ }
3524
+ }
3525
+ }
3526
+ ],
3527
+ o: { type: 'chip-input' }
3528
+ };
3529
+ }
3530
+
3531
+ // componentHandles registry removed in v2.0.15.
3532
+ // See dev/dead-code-elimination-v2.0.15.md for recovery.
3533
+
3534
+ // =========================================================================
3535
+ // BCCL Component Registry
3536
+ //
3537
+ // Single registry mapping type names to their factory functions.
3538
+ // Enables bw.make('card', props) dispatch and introspection via
3539
+ // Object.keys(BCCL).
3540
+ // =========================================================================
3541
+
3542
+ /**
3543
+ * BCCL component registry — maps component type names to factory functions.
3544
+ * Each entry's `make` function is the corresponding exported makeXxx().
3545
+ *
3546
+ * @type {Object.<string, {make: Function}>}
3547
+ */
3548
+ export var BCCL = {
3549
+ card: { make: makeCard },
3550
+ button: { make: makeButton },
3551
+ container: { make: makeContainer },
3552
+ row: { make: makeRow },
3553
+ col: { make: makeCol },
3554
+ nav: { make: makeNav },
3555
+ navbar: { make: makeNavbar },
3556
+ tabs: { make: makeTabs },
3557
+ alert: { make: makeAlert },
3558
+ badge: { make: makeBadge },
3559
+ progress: { make: makeProgress },
3560
+ listGroup: { make: makeListGroup },
3561
+ breadcrumb: { make: makeBreadcrumb },
3562
+ form: { make: makeForm },
3563
+ formGroup: { make: makeFormGroup },
3564
+ input: { make: makeInput },
3565
+ textarea: { make: makeTextarea },
3566
+ select: { make: makeSelect },
3567
+ checkbox: { make: makeCheckbox },
3568
+ stack: { make: makeStack },
3569
+ spinner: { make: makeSpinner },
3570
+ hero: { make: makeHero },
3571
+ featureGrid: { make: makeFeatureGrid },
3572
+ cta: { make: makeCTA },
3573
+ section: { make: makeSection },
3574
+ codeDemo: { make: makeCodeDemo },
3575
+ pagination: { make: makePagination },
3576
+ radio: { make: makeRadio },
3577
+ buttonGroup: { make: makeButtonGroup },
3578
+ accordion: { make: makeAccordion },
3579
+ modal: { make: makeModal },
3580
+ toast: { make: makeToast },
3581
+ dropdown: { make: makeDropdown },
3582
+ switch: { make: makeSwitch },
3583
+ skeleton: { make: makeSkeleton },
3584
+ avatar: { make: makeAvatar },
3585
+ carousel: { make: makeCarousel },
3586
+ statCard: { make: makeStatCard },
3587
+ tooltip: { make: makeTooltip },
3588
+ popover: { make: makePopover },
3589
+ searchInput: { make: makeSearchInput },
3590
+ range: { make: makeRange },
3591
+ mediaObject: { make: makeMediaObject },
3592
+ fileUpload: { make: makeFileUpload },
3593
+ timeline: { make: makeTimeline },
3594
+ stepper: { make: makeStepper },
3595
+ chipInput: { make: makeChipInput }
3596
+ };
3597
+
3598
+ /**
3599
+ * Factory function — create any BCCL component by type name.
3600
+ *
3601
+ * @param {string} type - Component type (e.g. 'card', 'button', 'alert')
3602
+ * @param {Object} [props] - Component properties
3603
+ * @returns {Object} TACO object
3604
+ * @throws {Error} If type is not found in the registry
3605
+ * @example
3606
+ * var card = make('card', { title: 'Hello', variant: 'primary' });
3607
+ * var btn = make('button', { text: 'Click', variant: 'success' });
3608
+ * var types = Object.keys(BCCL); // list all available types
3609
+ */
3610
+ export function make(type, props) {
3611
+ var def = BCCL[type];
3612
+ if (!def) throw new Error('bw.make: unknown component type "' + type + '". Available: ' + Object.keys(BCCL).join(', '));
3613
+ return def.make(props || {});
3614
+ }