bitwrench 1.2.15 → 2.0.7

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 (119) hide show
  1. package/README.md +160 -158
  2. package/bin/bitwrench.js +3 -0
  3. package/dist/bitwrench-code-edit.cjs.js +639 -0
  4. package/dist/bitwrench-code-edit.es5.js +875 -0
  5. package/dist/bitwrench-code-edit.es5.min.js +15 -0
  6. package/dist/bitwrench-code-edit.esm.js +628 -0
  7. package/dist/bitwrench-code-edit.esm.min.js +15 -0
  8. package/dist/bitwrench-code-edit.umd.js +645 -0
  9. package/dist/bitwrench-code-edit.umd.min.js +15 -0
  10. package/dist/bitwrench.cjs.js +6983 -0
  11. package/dist/bitwrench.cjs.min.js +62 -0
  12. package/dist/bitwrench.css +5100 -0
  13. package/dist/bitwrench.es5.js +8446 -0
  14. package/dist/bitwrench.es5.min.js +31 -0
  15. package/dist/bitwrench.esm.js +6981 -0
  16. package/dist/bitwrench.esm.min.js +62 -0
  17. package/dist/bitwrench.umd.js +6989 -0
  18. package/dist/bitwrench.umd.min.js +62 -0
  19. package/dist/builds.json +127 -0
  20. package/dist/sri.json +18 -0
  21. package/package.json +86 -24
  22. package/readme.html +288 -0
  23. package/src/bitwrench-code-edit.js +627 -0
  24. package/src/bitwrench-color-utils.js +311 -0
  25. package/src/bitwrench-component-base.js +736 -0
  26. package/src/bitwrench-components-inline.js +374 -0
  27. package/src/bitwrench-components-v2.js +1879 -0
  28. package/src/bitwrench-components.js +610 -0
  29. package/src/bitwrench-styles.js +3240 -0
  30. package/src/bitwrench.js +3367 -0
  31. package/src/cli/convert.js +205 -0
  32. package/src/cli/index.js +122 -0
  33. package/src/cli/inject.js +55 -0
  34. package/src/cli/layout-default.js +142 -0
  35. package/src/generate-css.js +381 -0
  36. package/src/vendor/quikdown.js +654 -0
  37. package/src/version.js +16 -0
  38. package/.eslintrc.json +0 -27
  39. package/.github/workflows/codeql-analysis.yml +0 -72
  40. package/.travis.yml +0 -34
  41. package/bitwrench.css +0 -92
  42. package/bitwrench.js +0 -3348
  43. package/bitwrench.js_sri.txt +0 -1
  44. package/bitwrench.min.js +0 -1
  45. package/bitwrench.min.js_sri.txt +0 -1
  46. package/bitwrench_ESM.js +0 -3207
  47. package/dev/bitwrench-todo.md +0 -215
  48. package/dev/css-arrows.md +0 -23
  49. package/dev/docStringDev.js +0 -124
  50. package/dev/docStringParseDev.js +0 -171
  51. package/dev/figures.html +0 -37
  52. package/dev/html_gen.js +0 -349
  53. package/dev/htmld.md +0 -250
  54. package/dev/htmldev.html +0 -45
  55. package/dev/index-old.html +0 -87
  56. package/dev/misc-notes.md +0 -21
  57. package/dev/notes.md +0 -2
  58. package/dev/sizes.html +0 -49
  59. package/dev/universal-js-module.js +0 -37
  60. package/examples/example1.html +0 -78
  61. package/examples/example10.html +0 -84
  62. package/examples/example2.html +0 -44
  63. package/examples/example3.html +0 -50
  64. package/examples/example4.html +0 -22
  65. package/examples/example5.html +0 -82
  66. package/examples/example6.html +0 -128
  67. package/examples/example7.html +0 -91
  68. package/examples/example8.html +0 -27
  69. package/examples/example9.html +0 -102
  70. package/icon/bitwrench-dark-tall.png +0 -0
  71. package/icon/bitwrench-dark.png +0 -0
  72. package/icon/bitwrench-icon-lt-grey.png +0 -0
  73. package/icon/bitwrench-icon.vsd +0 -0
  74. package/icon/bitwrench-logo-dark.png +0 -0
  75. package/icon/bitwrench-logo-full.png +0 -0
  76. package/icon/bitwrench-logo-green.png +0 -0
  77. package/icon/bitwrench-logo-grey.png +0 -0
  78. package/icon/bitwrench-logo-white.png +0 -0
  79. package/icon/bitwrench-logos-colors.png +0 -0
  80. package/icon/bitwrench-thick-logo.png +0 -0
  81. package/icon/bitwrench-thick-teal/android-chrome-192x192.png +0 -0
  82. package/icon/bitwrench-thick-teal/android-chrome-512x512.png +0 -0
  83. package/icon/bitwrench-thick-teal/apple-touch-icon.png +0 -0
  84. package/icon/bitwrench-thick-teal/browserconfig.xml +0 -9
  85. package/icon/bitwrench-thick-teal/favicon-16x16.png +0 -0
  86. package/icon/bitwrench-thick-teal/favicon-32x32.png +0 -0
  87. package/icon/bitwrench-thick-teal/favicon.ico +0 -0
  88. package/icon/bitwrench-thick-teal/mstile-144x144.png +0 -0
  89. package/icon/bitwrench-thick-teal/mstile-150x150.png +0 -0
  90. package/icon/bitwrench-thick-teal/mstile-310x150.png +0 -0
  91. package/icon/bitwrench-thick-teal/mstile-310x310.png +0 -0
  92. package/icon/bitwrench-thick-teal/mstile-70x70.png +0 -0
  93. package/icon/bitwrench-thick-teal/site.webmanifest +0 -19
  94. package/icon/bitwrench-thick-teal.ico +0 -0
  95. package/icon/bitwrench-thick-teal.svg +0 -44
  96. package/icon/bitwrench-thick-teal.zip +0 -0
  97. package/icon/favicon-test.html +0 -20
  98. package/icon/logos-test.PNG +0 -0
  99. package/images/bitwrench-512x512.png +0 -0
  100. package/images/bitwrench-logo-med.png +0 -0
  101. package/images/bitwrench-thick-logo.png +0 -0
  102. package/images/bitwrench-thick-logo.svg +0 -64
  103. package/images/bitwrench-thick-teal.ico +0 -0
  104. package/images/favicon.ico +0 -0
  105. package/index.html +0 -256
  106. package/instr_tmp/bitwrench.js +0 -1350
  107. package/karma.conf.js +0 -140
  108. package/makefile +0 -21
  109. package/quick-docs.html +0 -206
  110. package/test/bitwrench_test.js +0 -1255
  111. package/test/karma-test.js +0 -1081
  112. package/tools/bw_deprecatedNames.js +0 -19
  113. package/tools/bwconsole.js +0 -20
  114. package/tools/createSimpleHTMLPage.js +0 -41
  115. package/tools/emitreadme.sh +0 -4
  116. package/tools/export-bw-default-css.js +0 -41
  117. package/tools/umd2ModuleHack.js +0 -32
  118. package/tools/update-bw-package.js +0 -36
  119. package/tools/updatereadme.js +0 -34
@@ -0,0 +1,1879 @@
1
+ /**
2
+ * Bitwrench v2 Components
3
+ *
4
+ * TACO-based UI component library providing Bootstrap-inspired components
5
+ * as pure JavaScript objects. Every make* function returns a TACO object
6
+ * ({t, a, c, o}) that can be rendered with bw.html() or bw.DOM().
7
+ *
8
+ * Components included: Card, Button, Container, Row, Col, Nav, Navbar,
9
+ * Tabs, Alert, Badge, Progress, ListGroup, Breadcrumb, Form controls,
10
+ * Stack, Spinner, Hero, FeatureGrid, CardV2, CTA, Section, CodeDemo.
11
+ *
12
+ * Handle classes (CardHandle, TableHandle, NavbarHandle, TabsHandle)
13
+ * provide imperative DOM manipulation for rendered components.
14
+ *
15
+ * @module bitwrench-components-v2
16
+ * @license BSD-2-Clause
17
+ * @author M A Chatterjee <deftio [at] deftio [dot] com>
18
+ */
19
+
20
+ /**
21
+ * Create a card component with optional header, body, footer, and image support
22
+ *
23
+ * Supports images (top, bottom, left, right), shadow levels, subtitle,
24
+ * hover animation, and custom section class overrides. For horizontal
25
+ * image layouts (left/right), content is wrapped in a row grid.
26
+ *
27
+ * @param {Object} [props] - Card configuration
28
+ * @param {string} [props.title] - Card title displayed in the body
29
+ * @param {string} [props.subtitle] - Card subtitle (muted text below title)
30
+ * @param {string|Object|Array} [props.content] - Card body content (string, TACO, or array)
31
+ * @param {string|Object} [props.footer] - Card footer content
32
+ * @param {string|Object} [props.header] - Card header content
33
+ * @param {Object} [props.image] - Card image configuration
34
+ * @param {string} props.image.src - Image source URL
35
+ * @param {string} [props.image.alt] - Image alt text
36
+ * @param {string} [props.imagePosition="top"] - Image position ("top", "bottom", "left", "right")
37
+ * @param {string} [props.variant] - Color variant (e.g. "primary", "danger")
38
+ * @param {boolean} [props.bordered=true] - Show card border
39
+ * @param {string} [props.shadow] - Shadow level ("none", "sm", "md", "lg")
40
+ * @param {boolean} [props.hoverable=false] - Enable hover lift animation
41
+ * @param {string} [props.className] - Additional CSS classes
42
+ * @param {Object} [props.style] - Inline style object
43
+ * @param {string} [props.headerClass] - Additional header CSS classes
44
+ * @param {string} [props.bodyClass] - Additional body CSS classes
45
+ * @param {string} [props.footerClass] - Additional footer CSS classes
46
+ * @param {Object} [props.state] - Component state object
47
+ * @returns {Object} TACO object representing a card component
48
+ * @category Component Builders
49
+ * @example
50
+ * const card = makeCard({
51
+ * title: "Status",
52
+ * content: "All systems operational",
53
+ * variant: "success"
54
+ * });
55
+ * bw.DOM("#app", card);
56
+ */
57
+ export function makeCard(props = {}) {
58
+ const {
59
+ title,
60
+ subtitle,
61
+ content,
62
+ footer,
63
+ header,
64
+ image,
65
+ imagePosition = 'top',
66
+ variant,
67
+ bordered = true,
68
+ shadow,
69
+ hoverable = false,
70
+ className = '',
71
+ style,
72
+ headerClass = '',
73
+ bodyClass = '',
74
+ footerClass = ''
75
+ } = props;
76
+
77
+ const shadowClasses = {
78
+ none: '',
79
+ sm: 'bw-shadow-sm',
80
+ md: 'bw-shadow',
81
+ lg: 'bw-shadow-lg'
82
+ };
83
+
84
+ const cardClasses = [
85
+ 'bw-card',
86
+ variant ? `bw-card-${variant}` : '',
87
+ shadow ? (shadowClasses[shadow] || '') : '',
88
+ !bordered ? 'bw-border-0' : '',
89
+ hoverable ? 'bw-card-hoverable' : '',
90
+ className
91
+ ].filter(Boolean).join(' ').trim();
92
+
93
+ const cardContent = [
94
+ header && {
95
+ t: 'div',
96
+ a: { class: `bw-card-header ${headerClass}`.trim() },
97
+ c: header
98
+ },
99
+ image && (imagePosition === 'top' || imagePosition === 'left') && {
100
+ t: 'img',
101
+ a: {
102
+ class: `bw-card-img-${imagePosition}`,
103
+ src: image.src,
104
+ alt: image.alt || ''
105
+ }
106
+ },
107
+ {
108
+ t: 'div',
109
+ a: { class: `bw-card-body ${bodyClass}`.trim() },
110
+ c: [
111
+ title && { t: 'h5', a: { class: 'bw-card-title' }, c: title },
112
+ subtitle && { t: 'h6', a: { class: 'bw-card-subtitle bw-mb-2 bw-text-muted' }, c: subtitle },
113
+ content && (Array.isArray(content) ? content : [content])
114
+ ].flat().filter(Boolean)
115
+ },
116
+ image && (imagePosition === 'bottom' || imagePosition === 'right') && {
117
+ t: 'img',
118
+ a: {
119
+ class: `bw-card-img-${imagePosition}`,
120
+ src: image.src,
121
+ alt: image.alt || ''
122
+ }
123
+ },
124
+ footer && {
125
+ t: 'div',
126
+ a: { class: `bw-card-footer ${footerClass}`.trim() },
127
+ c: footer
128
+ }
129
+ ].filter(Boolean);
130
+
131
+ // Handle horizontal layout for left/right images
132
+ if (image && (imagePosition === 'left' || imagePosition === 'right')) {
133
+ return {
134
+ t: 'div',
135
+ a: { class: cardClasses, style },
136
+ c: {
137
+ t: 'div',
138
+ a: { class: 'bw-row bw-g-0' },
139
+ c: cardContent
140
+ },
141
+ o: {
142
+ type: 'card',
143
+ state: props.state || {}
144
+ }
145
+ };
146
+ }
147
+
148
+ return {
149
+ t: 'div',
150
+ a: { class: cardClasses, style },
151
+ c: cardContent,
152
+ o: {
153
+ type: 'card',
154
+ state: props.state || {}
155
+ }
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Create a button component
161
+ *
162
+ * @param {Object} [props] - Button configuration
163
+ * @param {string} [props.text] - Button label text
164
+ * @param {string} [props.variant="primary"] - Color variant (e.g. "primary", "secondary", "danger")
165
+ * @param {string} [props.size] - Size variant ("sm" or "lg")
166
+ * @param {boolean} [props.disabled=false] - Whether the button is disabled
167
+ * @param {Function} [props.onclick] - Click event handler
168
+ * @param {string} [props.type="button"] - HTML button type ("button", "submit", "reset")
169
+ * @param {string} [props.className] - Additional CSS classes
170
+ * @param {Object} [props.style] - Inline style object
171
+ * @returns {Object} TACO object representing a button element
172
+ * @category Component Builders
173
+ * @example
174
+ * const btn = makeButton({
175
+ * text: "Save",
176
+ * variant: "success",
177
+ * onclick: () => console.log("saved")
178
+ * });
179
+ */
180
+ export function makeButton(props = {}) {
181
+ const {
182
+ text,
183
+ variant = 'primary',
184
+ size,
185
+ disabled = false,
186
+ onclick,
187
+ type = 'button',
188
+ className = '',
189
+ style
190
+ } = props;
191
+
192
+ return {
193
+ t: 'button',
194
+ a: {
195
+ type,
196
+ class: [
197
+ 'bw-btn',
198
+ `bw-btn-${variant}`,
199
+ size && `bw-btn-${size}`,
200
+ className
201
+ ].filter(Boolean).join(' '),
202
+ disabled,
203
+ onclick,
204
+ style
205
+ },
206
+ c: text,
207
+ o: {
208
+ type: 'button'
209
+ }
210
+ };
211
+ }
212
+
213
+ /**
214
+ * Create a container component for centering and constraining content width
215
+ *
216
+ * @param {Object} [props] - Container configuration
217
+ * @param {boolean} [props.fluid=false] - Use full-width fluid container
218
+ * @param {Array|Object|string} [props.children] - Child content
219
+ * @param {string} [props.className] - Additional CSS classes
220
+ * @returns {Object} TACO object representing a container div
221
+ * @category Component Builders
222
+ * @example
223
+ * const container = makeContainer({
224
+ * fluid: true,
225
+ * children: [makeRow({ children: [...] })]
226
+ * });
227
+ */
228
+ export function makeContainer(props = {}) {
229
+ const { fluid = false, children, className = '' } = props;
230
+
231
+ return {
232
+ t: 'div',
233
+ a: { class: `bw-container${fluid ? '-fluid' : ''} ${className}`.trim() },
234
+ c: children
235
+ };
236
+ }
237
+
238
+ /**
239
+ * Create a flexbox row for the grid system
240
+ *
241
+ * @param {Object} [props] - Row configuration
242
+ * @param {Array|Object|string} [props.children] - Child columns
243
+ * @param {string} [props.className] - Additional CSS classes
244
+ * @param {number} [props.gap] - Gap size (1-5) applied via bw-g-{gap} class
245
+ * @returns {Object} TACO object representing a grid row
246
+ * @category Component Builders
247
+ * @example
248
+ * const row = makeRow({
249
+ * gap: 4,
250
+ * children: [makeCol({ size: 6, content: "Left" }), makeCol({ size: 6, content: "Right" })]
251
+ * });
252
+ */
253
+ export function makeRow(props = {}) {
254
+ const { children, className = '', gap } = props;
255
+
256
+ return {
257
+ t: 'div',
258
+ a: {
259
+ class: `bw-row ${gap ? `bw-g-${gap}` : ''} ${className}`.trim()
260
+ },
261
+ c: children
262
+ };
263
+ }
264
+
265
+ /**
266
+ * Create a grid column with responsive sizing
267
+ *
268
+ * Supports both fixed and responsive column sizes. Pass an object for
269
+ * responsive breakpoints (e.g. {xs: 12, md: 6, lg: 4}).
270
+ *
271
+ * @param {Object} [props] - Column configuration
272
+ * @param {number|Object} [props.size] - Column size (1-12) or responsive object {xs, sm, md, lg, xl}
273
+ * @param {number} [props.offset] - Column offset (1-12)
274
+ * @param {number} [props.push] - Column push (1-12)
275
+ * @param {number} [props.pull] - Column pull (1-12)
276
+ * @param {Array|Object|string} [props.content] - Column content (alias for children)
277
+ * @param {Array|Object|string} [props.children] - Column content
278
+ * @param {string} [props.className] - Additional CSS classes
279
+ * @returns {Object} TACO object representing a grid column
280
+ * @category Component Builders
281
+ * @example
282
+ * const col = makeCol({ size: { xs: 12, md: 6 }, content: "Responsive column" });
283
+ */
284
+ export function makeCol(props = {}) {
285
+ const { size, offset, push, pull, content, children, className = '' } = props;
286
+
287
+ const classes = [];
288
+
289
+ if (typeof size === 'object') {
290
+ // Responsive sizes
291
+ Object.entries(size).forEach(([breakpoint, value]) => {
292
+ if (breakpoint === 'xs') {
293
+ classes.push(`bw-col-${value}`);
294
+ } else {
295
+ classes.push(`bw-col-${breakpoint}-${value}`);
296
+ }
297
+ });
298
+ } else if (size) {
299
+ classes.push(`bw-col-${size}`);
300
+ } else {
301
+ classes.push('bw-col');
302
+ }
303
+
304
+ if (offset) classes.push(`bw-offset-${offset}`);
305
+ if (push) classes.push(`bw-push-${push}`);
306
+ if (pull) classes.push(`bw-pull-${pull}`);
307
+
308
+ return {
309
+ t: 'div',
310
+ a: { class: `${classes.join(' ')} ${className}`.trim() },
311
+ c: content || children
312
+ };
313
+ }
314
+
315
+ /**
316
+ * Create a navigation component with tabs or pills styling
317
+ *
318
+ * @param {Object} [props] - Nav configuration
319
+ * @param {Array<Object>} [props.items=[]] - Navigation items
320
+ * @param {string} props.items[].text - Item display text
321
+ * @param {string} [props.items[].href="#"] - Item link URL
322
+ * @param {boolean} [props.items[].active] - Whether this item is active
323
+ * @param {boolean} [props.items[].disabled] - Whether this item is disabled
324
+ * @param {boolean} [props.pills=false] - Use pill styling instead of tabs
325
+ * @param {boolean} [props.vertical=false] - Stack items vertically
326
+ * @param {string} [props.className] - Additional CSS classes
327
+ * @returns {Object} TACO object representing a nav element
328
+ * @category Component Builders
329
+ * @example
330
+ * const nav = makeNav({
331
+ * pills: true,
332
+ * items: [
333
+ * { text: "Home", href: "/", active: true },
334
+ * { text: "About", href: "/about" }
335
+ * ]
336
+ * });
337
+ */
338
+ export function makeNav(props = {}) {
339
+ const {
340
+ items = [],
341
+ pills = false,
342
+ vertical = false,
343
+ className = ''
344
+ } = props;
345
+
346
+ return {
347
+ t: 'ul',
348
+ a: {
349
+ class: `bw-nav ${pills ? 'bw-nav-pills' : 'bw-nav-tabs'} ${vertical ? 'bw-nav-vertical' : ''} ${className}`.trim()
350
+ },
351
+ c: items.map(item => ({
352
+ t: 'li',
353
+ a: { class: 'bw-nav-item' },
354
+ c: {
355
+ t: 'a',
356
+ a: {
357
+ href: item.href || '#',
358
+ class: `bw-nav-link ${item.active ? 'active' : ''} ${item.disabled ? 'disabled' : ''}`.trim()
359
+ },
360
+ c: item.text
361
+ }
362
+ }))
363
+ };
364
+ }
365
+
366
+ /**
367
+ * Create a navbar component with brand and navigation links
368
+ *
369
+ * @param {Object} [props] - Navbar configuration
370
+ * @param {string} [props.brand] - Brand name or logo text
371
+ * @param {string} [props.brandHref="#"] - Brand link URL
372
+ * @param {Array<Object>} [props.items=[]] - Navigation items
373
+ * @param {string} props.items[].text - Item display text
374
+ * @param {string} [props.items[].href="#"] - Item link URL
375
+ * @param {boolean} [props.items[].active] - Whether this item is active
376
+ * @param {boolean} [props.dark=true] - Use dark theme styling
377
+ * @param {string} [props.className] - Additional CSS classes
378
+ * @returns {Object} TACO object representing a navbar element
379
+ * @category Component Builders
380
+ * @example
381
+ * const navbar = makeNavbar({
382
+ * brand: "MyApp",
383
+ * dark: true,
384
+ * items: [
385
+ * { text: "Home", href: "/", active: true },
386
+ * { text: "Docs", href: "/docs" }
387
+ * ]
388
+ * });
389
+ */
390
+ export function makeNavbar(props = {}) {
391
+ const {
392
+ brand,
393
+ brandHref = '#',
394
+ items = [],
395
+ dark = true,
396
+ className = ''
397
+ } = props;
398
+
399
+ return {
400
+ t: 'nav',
401
+ a: {
402
+ class: `bw-navbar ${dark ? 'bw-navbar-dark' : 'bw-navbar-light'} ${className}`.trim()
403
+ },
404
+ c: {
405
+ t: 'div',
406
+ a: { class: 'bw-container' },
407
+ c: [
408
+ brand && {
409
+ t: 'a',
410
+ a: { href: brandHref, class: 'bw-navbar-brand' },
411
+ c: brand
412
+ },
413
+ items.length > 0 && {
414
+ t: 'div',
415
+ a: { class: 'bw-navbar-nav' },
416
+ c: items.map(item => ({
417
+ t: 'a',
418
+ a: {
419
+ href: item.href || '#',
420
+ class: `bw-nav-link ${item.active ? 'active' : ''}`
421
+ },
422
+ c: item.text
423
+ }))
424
+ }
425
+ ].filter(Boolean)
426
+ },
427
+ o: {
428
+ type: 'navbar',
429
+ state: { activeItem: items.findIndex(i => i.active) }
430
+ }
431
+ };
432
+ }
433
+
434
+ /**
435
+ * Create a tabbed interface with accessible tab navigation
436
+ *
437
+ * Each tab is rendered as a button with ARIA attributes for accessibility.
438
+ * Clicking a tab shows its content pane and hides others. The active tab
439
+ * can be set via activeIndex or by setting active:true on a tab item.
440
+ *
441
+ * @param {Object} [props] - Tabs configuration
442
+ * @param {Array<Object>} [props.tabs=[]] - Tab definitions
443
+ * @param {string} props.tabs[].label - Tab button label
444
+ * @param {string|Object|Array} props.tabs[].content - Tab pane content
445
+ * @param {boolean} [props.tabs[].active] - Whether this tab is initially active
446
+ * @param {number} [props.activeIndex=0] - Default active tab index (overridden by tab.active)
447
+ * @returns {Object} TACO object representing a tabbed interface
448
+ * @category Component Builders
449
+ * @example
450
+ * const tabs = makeTabs({
451
+ * tabs: [
452
+ * { label: "Overview", content: "Tab 1 content", active: true },
453
+ * { label: "Details", content: "Tab 2 content" }
454
+ * ]
455
+ * });
456
+ * bw.DOM("#app", tabs);
457
+ */
458
+ export function makeTabs(props = {}) {
459
+ const { tabs = [], activeIndex = 0 } = props;
460
+
461
+ // Find the active tab index based on the active property or use activeIndex
462
+ let actualActiveIndex = activeIndex;
463
+ tabs.forEach((tab, index) => {
464
+ if (tab.active) {
465
+ actualActiveIndex = index;
466
+ }
467
+ });
468
+
469
+ return {
470
+ t: 'div',
471
+ a: { class: 'bw-tabs' },
472
+ c: [
473
+ {
474
+ t: 'ul',
475
+ a: { class: 'bw-nav bw-nav-tabs', role: 'tablist' },
476
+ c: tabs.map((tab, index) => ({
477
+ t: 'li',
478
+ a: { class: 'bw-nav-item', role: 'presentation' },
479
+ c: {
480
+ t: 'button',
481
+ a: {
482
+ class: `bw-nav-link ${index === actualActiveIndex ? 'active' : ''}`,
483
+ type: 'button',
484
+ role: 'tab',
485
+ 'aria-selected': index === actualActiveIndex ? 'true' : 'false',
486
+ 'data-tab-index': index,
487
+ onclick: (e) => {
488
+ const tabsContainer = e.target.closest('.bw-tabs');
489
+ const allTabs = tabsContainer.querySelectorAll('.bw-nav-link');
490
+ const allPanes = tabsContainer.querySelectorAll('.bw-tab-pane');
491
+
492
+ allTabs.forEach(t => {
493
+ t.classList.remove('active');
494
+ t.setAttribute('aria-selected', 'false');
495
+ });
496
+ allPanes.forEach(p => p.classList.remove('active'));
497
+
498
+ e.target.classList.add('active');
499
+ e.target.setAttribute('aria-selected', 'true');
500
+ const targetIndex = parseInt(e.target.getAttribute('data-tab-index'));
501
+ allPanes[targetIndex].classList.add('active');
502
+ }
503
+ },
504
+ c: tab.label
505
+ }
506
+ }))
507
+ },
508
+ {
509
+ t: 'div',
510
+ a: { class: 'bw-tab-content' },
511
+ c: tabs.map((tab, index) => ({
512
+ t: 'div',
513
+ a: {
514
+ class: `bw-tab-pane ${index === actualActiveIndex ? 'active' : ''}`,
515
+ role: 'tabpanel'
516
+ },
517
+ c: tab.content
518
+ }))
519
+ }
520
+ ],
521
+ o: {
522
+ type: 'tabs',
523
+ state: { activeIndex: actualActiveIndex }
524
+ }
525
+ };
526
+ }
527
+
528
+ /**
529
+ * Create an alert/notification component
530
+ *
531
+ * @param {Object} [props] - Alert configuration
532
+ * @param {string|Object|Array} [props.content] - Alert message content
533
+ * @param {string} [props.variant="info"] - Color variant ("primary", "secondary", "success", "danger", "warning", "info", "light", "dark")
534
+ * @param {boolean} [props.dismissible=false] - Show a close button to dismiss the alert
535
+ * @param {string} [props.className] - Additional CSS classes
536
+ * @returns {Object} TACO object representing an alert element
537
+ * @category Component Builders
538
+ * @example
539
+ * const alert = makeAlert({
540
+ * content: "Operation completed successfully!",
541
+ * variant: "success",
542
+ * dismissible: true
543
+ * });
544
+ */
545
+ export function makeAlert(props = {}) {
546
+ const {
547
+ content,
548
+ variant = 'info',
549
+ dismissible = false,
550
+ className = ''
551
+ } = props;
552
+
553
+ return {
554
+ t: 'div',
555
+ a: {
556
+ class: `bw-alert bw-alert-${variant} ${dismissible ? 'bw-alert-dismissible' : ''} ${className}`.trim(),
557
+ role: 'alert'
558
+ },
559
+ c: [
560
+ content,
561
+ dismissible && {
562
+ t: 'button',
563
+ a: {
564
+ type: 'button',
565
+ class: 'bw-close',
566
+ 'aria-label': 'Close'
567
+ },
568
+ c: '×'
569
+ }
570
+ ].filter(Boolean)
571
+ };
572
+ }
573
+
574
+ /**
575
+ * Create an inline badge/label component
576
+ *
577
+ * @param {Object} [props] - Badge configuration
578
+ * @param {string} [props.text] - Badge display text
579
+ * @param {string} [props.variant="primary"] - Color variant
580
+ * @param {boolean} [props.pill=false] - Use pill (rounded) shape
581
+ * @param {string} [props.className] - Additional CSS classes
582
+ * @returns {Object} TACO object representing a badge span
583
+ * @category Component Builders
584
+ * @example
585
+ * const badge = makeBadge({ text: "New", variant: "danger", pill: true });
586
+ */
587
+ export function makeBadge(props = {}) {
588
+ const {
589
+ text,
590
+ variant = 'primary',
591
+ pill = false,
592
+ className = ''
593
+ } = props;
594
+
595
+ return {
596
+ t: 'span',
597
+ a: {
598
+ class: `bw-badge bw-badge-${variant} ${pill ? 'bw-badge-pill' : ''} ${className}`.trim()
599
+ },
600
+ c: text
601
+ };
602
+ }
603
+
604
+ /**
605
+ * Create a progress bar component with ARIA accessibility
606
+ *
607
+ * @param {Object} [props] - Progress bar configuration
608
+ * @param {number} [props.value=0] - Current progress value
609
+ * @param {number} [props.max=100] - Maximum value
610
+ * @param {string} [props.variant="primary"] - Color variant
611
+ * @param {boolean} [props.striped=false] - Use striped pattern
612
+ * @param {boolean} [props.animated=false] - Animate the stripes
613
+ * @param {string} [props.label] - Custom label text (defaults to percentage)
614
+ * @param {number} [props.height] - Custom height in pixels
615
+ * @returns {Object} TACO object representing a progress bar
616
+ * @category Component Builders
617
+ * @example
618
+ * const progress = makeProgress({
619
+ * value: 75,
620
+ * variant: "success",
621
+ * striped: true,
622
+ * animated: true
623
+ * });
624
+ */
625
+ export function makeProgress(props = {}) {
626
+ const {
627
+ value = 0,
628
+ max = 100,
629
+ variant = 'primary',
630
+ striped = false,
631
+ animated = false,
632
+ label,
633
+ height
634
+ } = props;
635
+
636
+ const percentage = Math.round((value / max) * 100);
637
+
638
+ return {
639
+ t: 'div',
640
+ a: {
641
+ class: 'bw-progress',
642
+ style: height ? { height: `${height}px` } : undefined
643
+ },
644
+ c: {
645
+ t: 'div',
646
+ a: {
647
+ class: [
648
+ 'bw-progress-bar',
649
+ `bw-progress-bar-${variant}`,
650
+ striped && 'bw-progress-bar-striped',
651
+ animated && 'bw-progress-bar-animated'
652
+ ].filter(Boolean).join(' '),
653
+ role: 'progressbar',
654
+ style: { width: `${percentage}%` },
655
+ 'aria-valuenow': value,
656
+ 'aria-valuemin': 0,
657
+ 'aria-valuemax': max
658
+ },
659
+ c: label || `${percentage}%`
660
+ }
661
+ };
662
+ }
663
+
664
+ /**
665
+ * Create a list group component for displaying lists of items
666
+ *
667
+ * Items can be simple strings or objects with text, active, disabled,
668
+ * href, and onclick properties. When interactive is true or items have
669
+ * href/onclick, items render as anchor tags.
670
+ *
671
+ * @param {Object} [props] - List group configuration
672
+ * @param {Array<string|Object>} [props.items=[]] - List items (strings or objects)
673
+ * @param {string} props.items[].text - Item display text
674
+ * @param {boolean} [props.items[].active] - Whether this item is active
675
+ * @param {boolean} [props.items[].disabled] - Whether this item is disabled
676
+ * @param {string} [props.items[].href] - Item link URL
677
+ * @param {Function} [props.items[].onclick] - Item click handler
678
+ * @param {boolean} [props.flush=false] - Remove borders for use inside cards
679
+ * @param {boolean} [props.interactive=false] - Make all items interactive (anchor tags)
680
+ * @returns {Object} TACO object representing a list group
681
+ * @category Component Builders
682
+ * @example
683
+ * const list = makeListGroup({
684
+ * interactive: true,
685
+ * items: [
686
+ * { text: "Active item", active: true },
687
+ * { text: "Regular item" },
688
+ * { text: "Disabled item", disabled: true }
689
+ * ]
690
+ * });
691
+ */
692
+ export function makeListGroup(props = {}) {
693
+ const { items = [], flush = false, interactive = false } = props;
694
+
695
+ return {
696
+ t: 'div',
697
+ a: { class: `bw-list-group ${flush ? 'bw-list-group-flush' : ''}`.trim() },
698
+ c: items.map(item => {
699
+ const isObject = typeof item === 'object';
700
+ const text = isObject ? item.text : item;
701
+ const active = isObject ? item.active : false;
702
+ const disabled = isObject ? item.disabled : false;
703
+ const href = isObject ? item.href : null;
704
+ const onclick = isObject ? item.onclick : null;
705
+
706
+ // For interactive items or items with href/onclick, use anchor tag
707
+ if (interactive || href || onclick) {
708
+ return {
709
+ t: 'a',
710
+ a: {
711
+ class: [
712
+ 'bw-list-group-item',
713
+ active && 'active',
714
+ disabled && 'disabled'
715
+ ].filter(Boolean).join(' '),
716
+ href: href || '#',
717
+ onclick: onclick || ((e) => {
718
+ if (!href) e.preventDefault();
719
+ }),
720
+ style: disabled ? 'pointer-events: none; opacity: 0.65;' : ''
721
+ },
722
+ c: text
723
+ };
724
+ }
725
+
726
+ // For non-interactive items, use div
727
+ return {
728
+ t: 'div',
729
+ a: {
730
+ class: [
731
+ 'bw-list-group-item',
732
+ active && 'active',
733
+ disabled && 'disabled'
734
+ ].filter(Boolean).join(' ')
735
+ },
736
+ c: text
737
+ };
738
+ })
739
+ };
740
+ }
741
+
742
+ /**
743
+ * Create a breadcrumb navigation component
744
+ *
745
+ * The last item with active:true is rendered as plain text (no link).
746
+ * All other items render as anchor tags.
747
+ *
748
+ * @param {Object} [props] - Breadcrumb configuration
749
+ * @param {Array<Object>} [props.items=[]] - Breadcrumb items
750
+ * @param {string} props.items[].text - Item display text
751
+ * @param {string} [props.items[].href="#"] - Item link URL
752
+ * @param {boolean} [props.items[].active] - Whether this is the current page
753
+ * @returns {Object} TACO object representing a breadcrumb nav
754
+ * @category Component Builders
755
+ * @example
756
+ * const crumbs = makeBreadcrumb({
757
+ * items: [
758
+ * { text: "Home", href: "/" },
759
+ * { text: "Products", href: "/products" },
760
+ * { text: "Widget", active: true }
761
+ * ]
762
+ * });
763
+ */
764
+ export function makeBreadcrumb(props = {}) {
765
+ const { items = [] } = props;
766
+
767
+ return {
768
+ t: 'nav',
769
+ a: { 'aria-label': 'breadcrumb' },
770
+ c: {
771
+ t: 'ol',
772
+ a: { class: 'bw-breadcrumb' },
773
+ c: items.map((item, index) => ({
774
+ t: 'li',
775
+ a: {
776
+ class: `bw-breadcrumb-item ${item.active ? 'active' : ''}`,
777
+ 'aria-current': item.active ? 'page' : undefined
778
+ },
779
+ c: item.active ? item.text : {
780
+ t: 'a',
781
+ a: { href: item.href || '#' },
782
+ c: item.text
783
+ }
784
+ }))
785
+ }
786
+ };
787
+ }
788
+
789
+ /**
790
+ * Create a form wrapper with default submit prevention
791
+ *
792
+ * @param {Object} [props] - Form configuration
793
+ * @param {Array|Object|string} [props.children] - Form contents (form groups, inputs, buttons)
794
+ * @param {Function} [props.onsubmit] - Submit handler (defaults to preventDefault)
795
+ * @param {string} [props.className] - Additional CSS classes
796
+ * @returns {Object} TACO object representing a form element
797
+ * @category Component Builders
798
+ * @example
799
+ * const form = makeForm({
800
+ * onsubmit: (e) => { e.preventDefault(); handleSubmit(); },
801
+ * children: [
802
+ * makeFormGroup({ label: "Name", input: makeInput({ placeholder: "Enter name" }) }),
803
+ * makeButton({ text: "Submit", type: "submit" })
804
+ * ]
805
+ * });
806
+ */
807
+ export function makeForm(props = {}) {
808
+ const { children, onsubmit, className = '' } = props;
809
+
810
+ return {
811
+ t: 'form',
812
+ a: {
813
+ class: className,
814
+ onsubmit: onsubmit || ((e) => e.preventDefault())
815
+ },
816
+ c: children
817
+ };
818
+ }
819
+
820
+ /**
821
+ * Create a form group with label, input, and optional help text
822
+ *
823
+ * @param {Object} [props] - Form group configuration
824
+ * @param {string} [props.label] - Label text
825
+ * @param {Object} [props.input] - Input TACO object (from makeInput, makeSelect, etc.)
826
+ * @param {string} [props.help] - Help text displayed below the input
827
+ * @param {string} [props.id] - Input ID (links label to input via for/id)
828
+ * @returns {Object} TACO object representing a form group
829
+ * @category Component Builders
830
+ * @example
831
+ * const group = makeFormGroup({
832
+ * label: "Email",
833
+ * id: "email",
834
+ * input: makeInput({ type: "email", id: "email", placeholder: "you@example.com" }),
835
+ * help: "We'll never share your email."
836
+ * });
837
+ */
838
+ export function makeFormGroup(props = {}) {
839
+ const { label, input, help, id } = props;
840
+
841
+ return {
842
+ t: 'div',
843
+ a: { class: 'bw-form-group' },
844
+ c: [
845
+ label && {
846
+ t: 'label',
847
+ a: { for: id, class: 'bw-form-label' },
848
+ c: label
849
+ },
850
+ input,
851
+ help && {
852
+ t: 'small',
853
+ a: { class: 'bw-form-text bw-text-muted' },
854
+ c: help
855
+ }
856
+ ].filter(Boolean)
857
+ };
858
+ }
859
+
860
+ /**
861
+ * Create an input element with form control styling
862
+ *
863
+ * Additional event handlers (oninput, onchange, etc.) can be passed
864
+ * as extra properties and are spread onto the element attributes.
865
+ *
866
+ * @param {Object} [props] - Input configuration
867
+ * @param {string} [props.type="text"] - Input type ("text", "email", "password", "number", etc.)
868
+ * @param {string} [props.placeholder] - Placeholder text
869
+ * @param {string} [props.value] - Input value
870
+ * @param {string} [props.id] - Element ID
871
+ * @param {string} [props.name] - Input name attribute
872
+ * @param {boolean} [props.disabled=false] - Whether the input is disabled
873
+ * @param {boolean} [props.readonly=false] - Whether the input is read-only
874
+ * @param {boolean} [props.required=false] - Whether the input is required
875
+ * @param {string} [props.className] - Additional CSS classes
876
+ * @param {Object} [props.style] - Inline style object
877
+ * @returns {Object} TACO object representing an input element
878
+ * @category Component Builders
879
+ * @example
880
+ * const input = makeInput({
881
+ * type: "email",
882
+ * placeholder: "you@example.com",
883
+ * required: true,
884
+ * oninput: (e) => validate(e.target.value)
885
+ * });
886
+ */
887
+ export function makeInput(props = {}) {
888
+ const {
889
+ type = 'text',
890
+ placeholder,
891
+ value,
892
+ id,
893
+ name,
894
+ disabled = false,
895
+ readonly = false,
896
+ required = false,
897
+ className = '',
898
+ style,
899
+ ...eventHandlers
900
+ } = props;
901
+
902
+ return {
903
+ t: 'input',
904
+ a: {
905
+ type,
906
+ class: `bw-form-control ${className}`.trim(),
907
+ placeholder,
908
+ value,
909
+ id,
910
+ name,
911
+ style,
912
+ disabled,
913
+ readonly,
914
+ required,
915
+ ...eventHandlers
916
+ }
917
+ };
918
+ }
919
+
920
+ /**
921
+ * Create a textarea element with form control styling
922
+ *
923
+ * @param {Object} [props] - Textarea configuration
924
+ * @param {string} [props.placeholder] - Placeholder text
925
+ * @param {string} [props.value] - Textarea content
926
+ * @param {number} [props.rows=3] - Number of visible text rows
927
+ * @param {string} [props.id] - Element ID
928
+ * @param {string} [props.name] - Textarea name attribute
929
+ * @param {boolean} [props.disabled=false] - Whether the textarea is disabled
930
+ * @param {boolean} [props.readonly=false] - Whether the textarea is read-only
931
+ * @param {boolean} [props.required=false] - Whether the textarea is required
932
+ * @param {string} [props.className] - Additional CSS classes
933
+ * @returns {Object} TACO object representing a textarea element
934
+ * @category Component Builders
935
+ * @example
936
+ * const textarea = makeTextarea({
937
+ * rows: 5,
938
+ * placeholder: "Enter your message...",
939
+ * required: true
940
+ * });
941
+ */
942
+ export function makeTextarea(props = {}) {
943
+ const {
944
+ placeholder,
945
+ value,
946
+ rows = 3,
947
+ id,
948
+ name,
949
+ disabled = false,
950
+ readonly = false,
951
+ required = false,
952
+ className = '',
953
+ ...eventHandlers
954
+ } = props;
955
+
956
+ return {
957
+ t: 'textarea',
958
+ a: {
959
+ class: `bw-form-control ${className}`.trim(),
960
+ placeholder,
961
+ rows,
962
+ id,
963
+ name,
964
+ disabled,
965
+ readonly,
966
+ required,
967
+ ...eventHandlers
968
+ },
969
+ c: value
970
+ };
971
+ }
972
+
973
+ /**
974
+ * Create a select dropdown with options
975
+ *
976
+ * @param {Object} [props] - Select configuration
977
+ * @param {Array<Object>} [props.options=[]] - Dropdown options
978
+ * @param {string} props.options[].value - Option value
979
+ * @param {string} [props.options[].text] - Option display text (defaults to value)
980
+ * @param {string} [props.value] - Currently selected value
981
+ * @param {string} [props.id] - Element ID
982
+ * @param {string} [props.name] - Select name attribute
983
+ * @param {boolean} [props.disabled=false] - Whether the select is disabled
984
+ * @param {boolean} [props.required=false] - Whether the select is required
985
+ * @param {string} [props.className] - Additional CSS classes
986
+ * @returns {Object} TACO object representing a select element
987
+ * @category Component Builders
988
+ * @example
989
+ * const select = makeSelect({
990
+ * value: "b",
991
+ * options: [
992
+ * { value: "a", text: "Option A" },
993
+ * { value: "b", text: "Option B" },
994
+ * { value: "c", text: "Option C" }
995
+ * ]
996
+ * });
997
+ */
998
+ export function makeSelect(props = {}) {
999
+ const {
1000
+ options = [],
1001
+ value,
1002
+ id,
1003
+ name,
1004
+ disabled = false,
1005
+ required = false,
1006
+ className = '',
1007
+ ...eventHandlers
1008
+ } = props;
1009
+
1010
+ return {
1011
+ t: 'select',
1012
+ a: {
1013
+ class: `bw-form-control ${className}`.trim(),
1014
+ id,
1015
+ name,
1016
+ disabled,
1017
+ required,
1018
+ ...eventHandlers
1019
+ },
1020
+ c: options.map(opt => ({
1021
+ t: 'option',
1022
+ a: {
1023
+ value: opt.value,
1024
+ selected: opt.value === value
1025
+ },
1026
+ c: opt.text || opt.value
1027
+ }))
1028
+ };
1029
+ }
1030
+
1031
+ /**
1032
+ * Create a checkbox input with label
1033
+ *
1034
+ * @param {Object} [props] - Checkbox configuration
1035
+ * @param {string} [props.label] - Checkbox label text
1036
+ * @param {boolean} [props.checked=false] - Whether the checkbox is checked
1037
+ * @param {string} [props.id] - Element ID (links label to checkbox)
1038
+ * @param {string} [props.name] - Input name attribute
1039
+ * @param {boolean} [props.disabled=false] - Whether the checkbox is disabled
1040
+ * @param {string} [props.value] - Checkbox value attribute
1041
+ * @returns {Object} TACO object representing a checkbox form group
1042
+ * @category Component Builders
1043
+ * @example
1044
+ * const checkbox = makeCheckbox({
1045
+ * label: "I agree to the terms",
1046
+ * id: "agree",
1047
+ * checked: false
1048
+ * });
1049
+ */
1050
+ export function makeCheckbox(props = {}) {
1051
+ const {
1052
+ label,
1053
+ checked = false,
1054
+ id,
1055
+ name,
1056
+ disabled = false,
1057
+ value
1058
+ } = props;
1059
+
1060
+ return {
1061
+ t: 'div',
1062
+ a: { class: 'bw-form-check' },
1063
+ c: [
1064
+ {
1065
+ t: 'input',
1066
+ a: {
1067
+ type: 'checkbox',
1068
+ class: 'bw-form-check-input',
1069
+ checked,
1070
+ id,
1071
+ name,
1072
+ disabled,
1073
+ value
1074
+ }
1075
+ },
1076
+ label && {
1077
+ t: 'label',
1078
+ a: { class: 'bw-form-check-label', for: id },
1079
+ c: label
1080
+ }
1081
+ ].filter(Boolean)
1082
+ };
1083
+ }
1084
+
1085
+ /**
1086
+ * Create a flexbox stack layout (vertical or horizontal)
1087
+ *
1088
+ * @param {Object} [props] - Stack configuration
1089
+ * @param {Array|Object|string} [props.children] - Stack children
1090
+ * @param {string} [props.direction="vertical"] - Stack direction ("vertical" or "horizontal")
1091
+ * @param {number} [props.gap=3] - Gap size (0-5)
1092
+ * @param {string} [props.className] - Additional CSS classes
1093
+ * @returns {Object} TACO object representing a stack layout
1094
+ * @category Component Builders
1095
+ * @example
1096
+ * const stack = makeStack({
1097
+ * direction: "horizontal",
1098
+ * gap: 2,
1099
+ * children: [
1100
+ * makeButton({ text: "Cancel", variant: "secondary" }),
1101
+ * makeButton({ text: "Save", variant: "primary" })
1102
+ * ]
1103
+ * });
1104
+ */
1105
+ export function makeStack(props = {}) {
1106
+ const {
1107
+ children,
1108
+ direction = 'vertical',
1109
+ gap = 3,
1110
+ className = ''
1111
+ } = props;
1112
+
1113
+ return {
1114
+ t: 'div',
1115
+ a: {
1116
+ class: `bw-${direction === 'vertical' ? 'vstack' : 'hstack'} bw-gap-${gap} ${className}`.trim()
1117
+ },
1118
+ c: children
1119
+ };
1120
+ }
1121
+
1122
+ /**
1123
+ * Create a loading spinner indicator
1124
+ *
1125
+ * @param {Object} [props] - Spinner configuration
1126
+ * @param {string} [props.variant="primary"] - Color variant
1127
+ * @param {string} [props.size="md"] - Spinner size ("sm", "md", "lg")
1128
+ * @param {string} [props.type="border"] - Spinner type ("border" or "grow")
1129
+ * @returns {Object} TACO object representing a spinner with screen-reader text
1130
+ * @category Component Builders
1131
+ * @example
1132
+ * const spinner = makeSpinner({ variant: "info", size: "sm" });
1133
+ */
1134
+ export function makeSpinner(props = {}) {
1135
+ const {
1136
+ variant = 'primary',
1137
+ size = 'md',
1138
+ type = 'border'
1139
+ } = props;
1140
+
1141
+ return {
1142
+ t: 'div',
1143
+ a: {
1144
+ class: `bw-spinner-${type} bw-spinner-${type}-${size} bw-text-${variant}`,
1145
+ role: 'status'
1146
+ },
1147
+ c: {
1148
+ t: 'span',
1149
+ a: { class: 'bw-visually-hidden' },
1150
+ c: 'Loading...'
1151
+ }
1152
+ };
1153
+ }
1154
+
1155
+ /**
1156
+ * Create a hero section for landing pages and headers
1157
+ *
1158
+ * Supports gradient backgrounds, background images with overlays,
1159
+ * and action buttons. Commonly used as the first visible section.
1160
+ *
1161
+ * @param {Object} [props] - Hero configuration
1162
+ * @param {string} [props.title] - Main headline text
1163
+ * @param {string} [props.subtitle] - Supporting description text
1164
+ * @param {string|Object|Array} [props.content] - Additional body content
1165
+ * @param {string} [props.variant="primary"] - Background variant ("primary", "secondary", "light", "dark")
1166
+ * @param {string} [props.size="lg"] - Vertical padding size ("sm", "md", "lg", "xl")
1167
+ * @param {boolean} [props.centered=true] - Center-align text
1168
+ * @param {boolean} [props.overlay=false] - Add dark overlay (for background images)
1169
+ * @param {string} [props.backgroundImage] - Background image URL
1170
+ * @param {Array|Object} [props.actions] - Call-to-action buttons
1171
+ * @param {string} [props.className] - Additional CSS classes
1172
+ * @returns {Object} TACO object representing a hero section
1173
+ * @category Component Builders
1174
+ * @example
1175
+ * const hero = makeHero({
1176
+ * title: "Welcome to Bitwrench",
1177
+ * subtitle: "Build UIs with pure JavaScript",
1178
+ * variant: "dark",
1179
+ * actions: [
1180
+ * makeButton({ text: "Get Started", variant: "primary", size: "lg" }),
1181
+ * makeButton({ text: "Learn More", variant: "outline-light", size: "lg" })
1182
+ * ]
1183
+ * });
1184
+ */
1185
+ export function makeHero(props = {}) {
1186
+ const {
1187
+ title,
1188
+ subtitle,
1189
+ content,
1190
+ variant = 'primary',
1191
+ size = 'lg',
1192
+ centered = true,
1193
+ overlay = false,
1194
+ backgroundImage,
1195
+ actions,
1196
+ className = ''
1197
+ } = props;
1198
+
1199
+ const sizeClasses = {
1200
+ sm: 'bw-py-3',
1201
+ md: 'bw-py-4',
1202
+ lg: 'bw-py-5',
1203
+ xl: 'bw-py-6'
1204
+ };
1205
+
1206
+ return {
1207
+ t: 'section',
1208
+ a: {
1209
+ class: `bw-hero bw-hero-${variant} ${sizeClasses[size] || sizeClasses.lg} ${centered ? 'bw-text-center' : ''} ${className}`.trim(),
1210
+ style: backgroundImage ? `background-image: url('${backgroundImage}'); background-size: cover; background-position: center;` : undefined
1211
+ },
1212
+ c: [
1213
+ overlay && {
1214
+ t: 'div',
1215
+ a: { class: 'bw-hero-overlay' }
1216
+ },
1217
+ {
1218
+ t: 'div',
1219
+ a: { class: 'bw-container' },
1220
+ c: {
1221
+ t: 'div',
1222
+ a: { class: 'bw-hero-content' },
1223
+ c: [
1224
+ title && {
1225
+ t: 'h1',
1226
+ a: { class: 'bw-hero-title bw-display-4 bw-mb-3' },
1227
+ c: title
1228
+ },
1229
+ subtitle && {
1230
+ t: 'p',
1231
+ a: { class: 'bw-hero-subtitle bw-lead bw-mb-4' },
1232
+ c: subtitle
1233
+ },
1234
+ content,
1235
+ actions && {
1236
+ t: 'div',
1237
+ a: { class: 'bw-hero-actions bw-mt-4' },
1238
+ c: actions
1239
+ }
1240
+ ].filter(Boolean)
1241
+ }
1242
+ }
1243
+ ].filter(Boolean)
1244
+ };
1245
+ }
1246
+
1247
+ /**
1248
+ * Create a responsive feature grid for showcasing capabilities
1249
+ *
1250
+ * Renders features in an equal-width column grid with optional icons,
1251
+ * titles, and descriptions.
1252
+ *
1253
+ * @param {Object} [props] - Feature grid configuration
1254
+ * @param {Array<Object>} [props.features=[]] - Feature items
1255
+ * @param {string} [props.features[].icon] - Icon content (emoji, HTML entity, or text)
1256
+ * @param {string} [props.features[].title] - Feature title
1257
+ * @param {string} [props.features[].description] - Feature description text
1258
+ * @param {number} [props.columns=3] - Number of columns (divides 12-col grid)
1259
+ * @param {boolean} [props.centered=true] - Center-align feature text
1260
+ * @param {string} [props.iconSize="3rem"] - Icon font size
1261
+ * @param {string} [props.className] - Additional CSS classes
1262
+ * @returns {Object} TACO object representing a feature grid
1263
+ * @category Component Builders
1264
+ * @example
1265
+ * const features = makeFeatureGrid({
1266
+ * columns: 3,
1267
+ * features: [
1268
+ * { icon: "⚡", title: "Fast", description: "Zero build step" },
1269
+ * { icon: "📦", title: "Small", description: "Under 45KB gzipped" },
1270
+ * { icon: "🔧", title: "Flexible", description: "Pure JS objects" }
1271
+ * ]
1272
+ * });
1273
+ */
1274
+ export function makeFeatureGrid(props = {}) {
1275
+ const {
1276
+ features = [],
1277
+ columns = 3,
1278
+ centered = true,
1279
+ iconSize = '3rem',
1280
+ className = ''
1281
+ } = props;
1282
+
1283
+ const colClass = `bw-col-md-${12/columns}`;
1284
+
1285
+ return {
1286
+ t: 'div',
1287
+ a: { class: `bw-feature-grid ${className}`.trim() },
1288
+ c: {
1289
+ t: 'div',
1290
+ a: { class: 'bw-row bw-g-4' },
1291
+ c: features.map(feature => ({
1292
+ t: 'div',
1293
+ a: { class: colClass },
1294
+ c: {
1295
+ t: 'div',
1296
+ a: { class: `bw-feature ${centered ? 'bw-text-center' : ''}` },
1297
+ c: [
1298
+ feature.icon && {
1299
+ t: 'div',
1300
+ a: {
1301
+ class: 'bw-feature-icon bw-mb-3',
1302
+ style: `font-size: ${iconSize}; color: var(--bw-primary);`
1303
+ },
1304
+ c: feature.icon
1305
+ },
1306
+ feature.title && {
1307
+ t: 'h3',
1308
+ a: { class: 'bw-feature-title bw-h5 bw-mb-2' },
1309
+ c: feature.title
1310
+ },
1311
+ feature.description && {
1312
+ t: 'p',
1313
+ a: { class: 'bw-feature-description bw-text-muted' },
1314
+ c: feature.description
1315
+ }
1316
+ ].filter(Boolean)
1317
+ }
1318
+ }))
1319
+ }
1320
+ };
1321
+ }
1322
+
1323
+
1324
+ /**
1325
+ * Create a call-to-action section with title, description, and action buttons
1326
+ *
1327
+ * @param {Object} [props] - CTA configuration
1328
+ * @param {string} [props.title] - CTA headline
1329
+ * @param {string} [props.description] - CTA description text
1330
+ * @param {Array|Object} [props.actions] - CTA buttons or content
1331
+ * @param {string} [props.variant="light"] - Background variant
1332
+ * @param {boolean} [props.centered=true] - Center-align content
1333
+ * @param {string} [props.className] - Additional CSS classes
1334
+ * @returns {Object} TACO object representing a CTA section
1335
+ * @category Component Builders
1336
+ * @example
1337
+ * const cta = makeCTA({
1338
+ * title: "Ready to get started?",
1339
+ * description: "Join thousands of developers using Bitwrench.",
1340
+ * actions: [
1341
+ * makeButton({ text: "Sign Up Free", variant: "primary", size: "lg" })
1342
+ * ]
1343
+ * });
1344
+ */
1345
+ export function makeCTA(props = {}) {
1346
+ const {
1347
+ title,
1348
+ description,
1349
+ actions,
1350
+ variant = 'light',
1351
+ centered = true,
1352
+ className = ''
1353
+ } = props;
1354
+
1355
+ return {
1356
+ t: 'section',
1357
+ a: { class: `bw-cta bw-bg-${variant} bw-py-5 ${className}`.trim() },
1358
+ c: {
1359
+ t: 'div',
1360
+ a: { class: 'bw-container' },
1361
+ c: {
1362
+ t: 'div',
1363
+ a: { class: `bw-cta-content ${centered ? 'bw-text-center' : ''}` },
1364
+ c: [
1365
+ title && { t: 'h2', a: { class: 'bw-cta-title bw-mb-3' }, c: title },
1366
+ description && { t: 'p', a: { class: 'bw-cta-description bw-lead bw-mb-4' }, c: description },
1367
+ actions && {
1368
+ t: 'div',
1369
+ a: { class: 'bw-cta-actions' },
1370
+ c: actions
1371
+ }
1372
+ ].filter(Boolean)
1373
+ }
1374
+ }
1375
+ };
1376
+ }
1377
+
1378
+ /**
1379
+ * Create a page section with optional centered header and background
1380
+ *
1381
+ * @param {Object} [props] - Section configuration
1382
+ * @param {string} [props.title] - Section title
1383
+ * @param {string} [props.subtitle] - Section subtitle (muted)
1384
+ * @param {string|Object|Array} [props.content] - Section body content
1385
+ * @param {string} [props.variant="default"] - Background variant ("default" for none, or a color name)
1386
+ * @param {string} [props.spacing="md"] - Vertical padding ("sm", "md", "lg", "xl")
1387
+ * @param {string} [props.className] - Additional CSS classes
1388
+ * @returns {Object} TACO object representing a content section
1389
+ * @category Component Builders
1390
+ * @example
1391
+ * const section = makeSection({
1392
+ * title: "Features",
1393
+ * subtitle: "Everything you need to build great UIs",
1394
+ * spacing: "lg",
1395
+ * content: makeFeatureGrid({ features: [...] })
1396
+ * });
1397
+ */
1398
+ export function makeSection(props = {}) {
1399
+ const {
1400
+ title,
1401
+ subtitle,
1402
+ content,
1403
+ variant = 'default',
1404
+ spacing = 'md',
1405
+ className = ''
1406
+ } = props;
1407
+
1408
+ const spacingClasses = {
1409
+ sm: 'bw-py-3',
1410
+ md: 'bw-py-4',
1411
+ lg: 'bw-py-5',
1412
+ xl: 'bw-py-6'
1413
+ };
1414
+
1415
+ return {
1416
+ t: 'section',
1417
+ a: {
1418
+ class: `bw-section ${spacingClasses[spacing] || spacingClasses.md} ${variant !== 'default' ? `bw-bg-${variant}` : ''} ${className}`.trim()
1419
+ },
1420
+ c: {
1421
+ t: 'div',
1422
+ a: { class: 'bw-container' },
1423
+ c: [
1424
+ (title || subtitle) && {
1425
+ t: 'div',
1426
+ a: { class: 'bw-section-header bw-text-center bw-mb-5' },
1427
+ c: [
1428
+ title && { t: 'h2', a: { class: 'bw-section-title' }, c: title },
1429
+ subtitle && { t: 'p', a: { class: 'bw-section-subtitle bw-text-muted' }, c: subtitle }
1430
+ ].filter(Boolean)
1431
+ },
1432
+ content
1433
+ ].filter(Boolean)
1434
+ }
1435
+ };
1436
+ }
1437
+
1438
+ // =========================================================================
1439
+ // Component Handle Classes
1440
+ //
1441
+ // Handle classes provide imperative DOM manipulation for rendered components.
1442
+ // They cache child element references for efficient updates without
1443
+ // full re-renders. Used by bw.createCard(), bw.createTable(), etc.
1444
+ // =========================================================================
1445
+
1446
+ /**
1447
+ * Imperative handle for a rendered card component
1448
+ *
1449
+ * Provides methods to update card title, content, and CSS classes
1450
+ * without re-rendering the entire component. Created automatically
1451
+ * when using bw.createCard().
1452
+ *
1453
+ * @category Component Handles
1454
+ */
1455
+ export class CardHandle {
1456
+ /**
1457
+ * @param {Element} element - The card's root DOM element
1458
+ * @param {Object} taco - The original TACO object used to create the card
1459
+ */
1460
+ constructor(element, taco) {
1461
+ this.element = element;
1462
+ this._taco = taco;
1463
+ this.state = taco.o?.state || {};
1464
+
1465
+ // Cache child elements
1466
+ this.children = {
1467
+ header: element.querySelector('.bw-card-header'),
1468
+ title: element.querySelector('.bw-card-title'),
1469
+ body: element.querySelector('.bw-card-body'),
1470
+ footer: element.querySelector('.bw-card-footer')
1471
+ };
1472
+ }
1473
+
1474
+ /**
1475
+ * Update the card title text
1476
+ *
1477
+ * @param {string} title - New title text
1478
+ * @returns {CardHandle} this (for chaining)
1479
+ */
1480
+ setTitle(title) {
1481
+ if (this.children.title) {
1482
+ this.children.title.textContent = title;
1483
+ }
1484
+ return this;
1485
+ }
1486
+
1487
+ /**
1488
+ * Replace the card body content
1489
+ *
1490
+ * @param {string|Object} content - New content (string or TACO object)
1491
+ * @returns {CardHandle} this (for chaining)
1492
+ */
1493
+ setContent(content) {
1494
+ if (this.children.body) {
1495
+ if (typeof content === 'string') {
1496
+ this.children.body.textContent = content;
1497
+ } else {
1498
+ // Re-render content
1499
+ this.children.body.innerHTML = '';
1500
+ const newContent = window.bw.taco.toDOM(content);
1501
+ this.children.body.appendChild(newContent);
1502
+ }
1503
+ }
1504
+ return this;
1505
+ }
1506
+
1507
+ /**
1508
+ * Add a CSS class to the card root element
1509
+ *
1510
+ * @param {string} className - Class to add
1511
+ * @returns {CardHandle} this (for chaining)
1512
+ */
1513
+ addClass(className) {
1514
+ this.element.classList.add(className);
1515
+ return this;
1516
+ }
1517
+
1518
+ /**
1519
+ * Remove a CSS class from the card root element
1520
+ *
1521
+ * @param {string} className - Class to remove
1522
+ * @returns {CardHandle} this (for chaining)
1523
+ */
1524
+ removeClass(className) {
1525
+ this.element.classList.remove(className);
1526
+ return this;
1527
+ }
1528
+
1529
+ /**
1530
+ * Query a child element within the card
1531
+ *
1532
+ * @param {string} selector - CSS selector
1533
+ * @returns {Element|null} Matching element or null
1534
+ */
1535
+ select(selector) {
1536
+ return this.element.querySelector(selector);
1537
+ }
1538
+ }
1539
+
1540
+ /**
1541
+ * Imperative handle for a rendered table component
1542
+ *
1543
+ * Provides methods for data updates and column sorting. Caches
1544
+ * thead/tbody/header references for efficient DOM updates.
1545
+ * Created automatically when using bw.createTable().
1546
+ *
1547
+ * @category Component Handles
1548
+ */
1549
+ export class TableHandle {
1550
+ /**
1551
+ * @param {Element} element - The table's root DOM element
1552
+ * @param {Object} taco - The original TACO object used to create the table
1553
+ */
1554
+ constructor(element, taco) {
1555
+ this.element = element;
1556
+ this._taco = taco;
1557
+ this.state = taco.o?.state || {};
1558
+ this._data = this.state.data || [];
1559
+ this._sortColumn = null;
1560
+ this._sortDirection = 'asc';
1561
+
1562
+ // Cache elements
1563
+ this.children = {
1564
+ thead: element.querySelector('thead'),
1565
+ tbody: element.querySelector('tbody'),
1566
+ headers: element.querySelectorAll('th')
1567
+ };
1568
+
1569
+ // Set up sorting if enabled
1570
+ if (this.state.sortable) {
1571
+ this._setupSorting();
1572
+ }
1573
+ }
1574
+
1575
+ /**
1576
+ * Attach click-to-sort handlers on all column headers
1577
+ * @private
1578
+ */
1579
+ _setupSorting() {
1580
+ this.children.headers.forEach((th, index) => {
1581
+ th.style.cursor = 'pointer';
1582
+ th.onclick = () => this.sortBy(th.textContent);
1583
+ });
1584
+ }
1585
+
1586
+ /**
1587
+ * Replace the table data and re-render the body
1588
+ *
1589
+ * @param {Array<Object>} data - Array of row objects
1590
+ * @returns {TableHandle} this (for chaining)
1591
+ */
1592
+ setData(data) {
1593
+ this._data = data;
1594
+ this._renderBody();
1595
+ return this;
1596
+ }
1597
+
1598
+ /**
1599
+ * Sort the table by a column name
1600
+ *
1601
+ * Toggles direction if the same column is sorted again.
1602
+ *
1603
+ * @param {string} column - Column header text to sort by
1604
+ * @param {string} [direction] - Sort direction ("asc" or "desc"); toggles if omitted
1605
+ * @returns {TableHandle} this (for chaining)
1606
+ */
1607
+ sortBy(column, direction) {
1608
+ if (column === this._sortColumn && !direction) {
1609
+ this._sortDirection = this._sortDirection === 'asc' ? 'desc' : 'asc';
1610
+ } else {
1611
+ this._sortColumn = column;
1612
+ this._sortDirection = direction || 'asc';
1613
+ }
1614
+
1615
+ const columnKey = Object.keys(this._data[0])[
1616
+ Array.from(this.children.headers).findIndex(th => th.textContent === column)
1617
+ ];
1618
+
1619
+ this._data.sort((a, b) => {
1620
+ const aVal = a[columnKey];
1621
+ const bVal = b[columnKey];
1622
+ const result = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
1623
+ return this._sortDirection === 'asc' ? result : -result;
1624
+ });
1625
+
1626
+ this._renderBody();
1627
+ return this;
1628
+ }
1629
+
1630
+ /**
1631
+ * Re-render the tbody from current _data
1632
+ * @private
1633
+ */
1634
+ _renderBody() {
1635
+ this.children.tbody.innerHTML = '';
1636
+ this._data.forEach(row => {
1637
+ const tr = document.createElement('tr');
1638
+ Object.values(row).forEach(value => {
1639
+ const td = document.createElement('td');
1640
+ td.textContent = value;
1641
+ tr.appendChild(td);
1642
+ });
1643
+ this.children.tbody.appendChild(tr);
1644
+ });
1645
+ }
1646
+ }
1647
+
1648
+ /**
1649
+ * Imperative handle for a rendered navbar component
1650
+ *
1651
+ * Provides methods to update the active navigation link.
1652
+ * Created automatically when using bw.createNavbar().
1653
+ *
1654
+ * @category Component Handles
1655
+ */
1656
+ export class NavbarHandle {
1657
+ /**
1658
+ * @param {Element} element - The navbar's root DOM element
1659
+ * @param {Object} taco - The original TACO object used to create the navbar
1660
+ */
1661
+ constructor(element, taco) {
1662
+ this.element = element;
1663
+ this._taco = taco;
1664
+ this.state = taco.o?.state || {};
1665
+
1666
+ this.children = {
1667
+ brand: element.querySelector('.bw-navbar-brand'),
1668
+ links: element.querySelectorAll('.bw-nav-link')
1669
+ };
1670
+ }
1671
+
1672
+ /**
1673
+ * Set the active navigation link by href
1674
+ *
1675
+ * @param {string} href - The href value of the link to activate
1676
+ * @returns {NavbarHandle} this (for chaining)
1677
+ */
1678
+ setActive(href) {
1679
+ this.children.links.forEach(link => {
1680
+ if (link.getAttribute('href') === href) {
1681
+ link.classList.add('active');
1682
+ } else {
1683
+ link.classList.remove('active');
1684
+ }
1685
+ });
1686
+ return this;
1687
+ }
1688
+ }
1689
+
1690
+ /**
1691
+ * Imperative handle for a rendered tabs component
1692
+ *
1693
+ * Provides programmatic tab switching. Sets up click handlers
1694
+ * on tab buttons and manages active states on both buttons and panes.
1695
+ * Created automatically when using bw.createTabs().
1696
+ *
1697
+ * @category Component Handles
1698
+ */
1699
+ export class TabsHandle {
1700
+ /**
1701
+ * @param {Element} element - The tabs container DOM element
1702
+ * @param {Object} taco - The original TACO object used to create the tabs
1703
+ */
1704
+ constructor(element, taco) {
1705
+ this.element = element;
1706
+ this._taco = taco;
1707
+ this.state = taco.o?.state || {};
1708
+
1709
+ this.children = {
1710
+ navItems: element.querySelectorAll('.bw-nav-link'),
1711
+ tabPanes: element.querySelectorAll('.bw-tab-pane')
1712
+ };
1713
+
1714
+ this._setupTabs();
1715
+ }
1716
+
1717
+ /**
1718
+ * Attach click handlers to tab navigation buttons
1719
+ * @private
1720
+ */
1721
+ _setupTabs() {
1722
+ this.children.navItems.forEach((navItem, index) => {
1723
+ navItem.onclick = (e) => {
1724
+ e.preventDefault();
1725
+ this.switchTo(index);
1726
+ };
1727
+ });
1728
+ }
1729
+
1730
+ /**
1731
+ * Programmatically switch to a tab by index
1732
+ *
1733
+ * @param {number} index - Zero-based tab index to activate
1734
+ * @returns {TabsHandle} this (for chaining)
1735
+ */
1736
+ switchTo(index) {
1737
+ this.children.navItems.forEach((item, i) => {
1738
+ if (i === index) {
1739
+ item.classList.add('active');
1740
+ } else {
1741
+ item.classList.remove('active');
1742
+ }
1743
+ });
1744
+
1745
+ this.children.tabPanes.forEach((pane, i) => {
1746
+ if (i === index) {
1747
+ pane.classList.add('active');
1748
+ } else {
1749
+ pane.classList.remove('active');
1750
+ }
1751
+ });
1752
+
1753
+ this.state.activeIndex = index;
1754
+ return this;
1755
+ }
1756
+ }
1757
+
1758
+ /**
1759
+ * Create a code demo component for documentation pages
1760
+ *
1761
+ * Displays a live result alongside source code in a tabbed interface.
1762
+ * Includes a copy-to-clipboard button on the code tab.
1763
+ *
1764
+ * @param {Object} [props] - Code demo configuration
1765
+ * @param {string} [props.title] - Demo title heading
1766
+ * @param {string} [props.description] - Demo description text
1767
+ * @param {string} [props.code] - Source code to display (adds a "Code" tab when present)
1768
+ * @param {string|Object|Array} [props.result] - Live result content for the "Result" tab
1769
+ * @param {string} [props.language="javascript"] - Code language for syntax class
1770
+ * @returns {Object} TACO object representing a code demo with tabbed Result/Code views
1771
+ * @category Component Builders
1772
+ * @example
1773
+ * const demo = makeCodeDemo({
1774
+ * title: "Button Example",
1775
+ * description: "A simple primary button",
1776
+ * code: 'makeButton({ text: "Click me" })',
1777
+ * result: makeButton({ text: "Click me" })
1778
+ * });
1779
+ */
1780
+ export function makeCodeDemo(props = {}) {
1781
+ const {
1782
+ title,
1783
+ description,
1784
+ code,
1785
+ result,
1786
+ language = 'javascript'
1787
+ } = props;
1788
+
1789
+ // Generate unique ID for this demo
1790
+ const demoId = `demo-${Math.random().toString(36).substr(2, 9)}`;
1791
+
1792
+ const tabs = [
1793
+ {
1794
+ label: 'Result',
1795
+ active: true,
1796
+ content: result
1797
+ }
1798
+ ];
1799
+
1800
+ // Only add Code tab if code is provided
1801
+ if (code) {
1802
+ tabs.push({
1803
+ label: 'Code',
1804
+ content: {
1805
+ t: 'div',
1806
+ a: { style: 'position: relative;' },
1807
+ c: [
1808
+ {
1809
+ t: 'button',
1810
+ a: {
1811
+ class: 'bw-copy-btn',
1812
+ style: 'position: absolute; top: 0.5rem; right: 0.5rem; padding: 0.25rem 0.625rem; font-size: 0.6875rem; background: rgba(255,255,255,0.12); color: #aaa; border: 1px solid rgba(255,255,255,0.15); border-radius: 4px; cursor: pointer; font-family: inherit; transition: all 0.15s;',
1813
+ onclick: (e) => {
1814
+ navigator.clipboard.writeText(code).then(() => {
1815
+ const btn = e.target;
1816
+ const originalText = btn.textContent;
1817
+ btn.textContent = 'Copied!';
1818
+ btn.style.background = '#006666';
1819
+ btn.style.color = '#fff';
1820
+ setTimeout(() => {
1821
+ btn.textContent = originalText;
1822
+ btn.style.background = 'rgba(255,255,255,0.12)';
1823
+ btn.style.color = '#aaa';
1824
+ }, 2000);
1825
+ });
1826
+ }
1827
+ },
1828
+ c: 'Copy'
1829
+ },
1830
+ {
1831
+ t: 'pre',
1832
+ a: {
1833
+ style: 'margin: 0; background: #1e293b; border: none; border-radius: 6px; overflow-x: auto;'
1834
+ },
1835
+ c: {
1836
+ t: 'code',
1837
+ a: {
1838
+ class: `language-${language}`,
1839
+ style: 'display: block; padding: 1.25rem; font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace; font-size: 0.8125rem; line-height: 1.6; color: #e2e8f0;'
1840
+ },
1841
+ c: code
1842
+ }
1843
+ }
1844
+ ]
1845
+ }
1846
+ });
1847
+ }
1848
+
1849
+ const content = [
1850
+ title && { t: 'h3', c: title },
1851
+ description && {
1852
+ t: 'p',
1853
+ a: { style: 'color: #6c757d; margin-bottom: 1rem;' },
1854
+ c: description
1855
+ },
1856
+ makeTabs({ tabs, id: demoId })
1857
+ ].filter(Boolean);
1858
+
1859
+ return {
1860
+ t: 'div',
1861
+ a: { class: 'bw-code-demo' },
1862
+ c: content
1863
+ };
1864
+ }
1865
+
1866
+ /**
1867
+ * Registry mapping component type names to their handle classes
1868
+ *
1869
+ * Used by bw.createCard(), bw.createTable(), etc. to wrap rendered
1870
+ * DOM elements in the appropriate imperative handle.
1871
+ *
1872
+ * @type {Object.<string, Function>}
1873
+ */
1874
+ export const componentHandles = {
1875
+ card: CardHandle,
1876
+ table: TableHandle,
1877
+ navbar: NavbarHandle,
1878
+ tabs: TabsHandle
1879
+ };