bitwrench 2.0.15 → 2.0.17

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