bitwrench 2.0.14 → 2.0.16
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.
- package/README.md +57 -21
- package/dist/bitwrench-bccl.cjs.js +3746 -0
- package/dist/bitwrench-bccl.cjs.min.js +40 -0
- package/dist/bitwrench-bccl.esm.js +3741 -0
- package/dist/bitwrench-bccl.esm.min.js +40 -0
- package/dist/bitwrench-bccl.umd.js +3752 -0
- package/dist/bitwrench-bccl.umd.min.js +40 -0
- package/dist/bitwrench-code-edit.cjs.js +99 -49
- package/dist/bitwrench-code-edit.cjs.min.js +23 -0
- package/dist/bitwrench-code-edit.es5.js +79 -16
- package/dist/bitwrench-code-edit.es5.min.js +9 -2
- package/dist/bitwrench-code-edit.esm.js +99 -49
- package/dist/bitwrench-code-edit.esm.min.js +9 -2
- package/dist/bitwrench-code-edit.umd.js +99 -49
- package/dist/bitwrench-code-edit.umd.min.js +9 -2
- package/dist/bitwrench-lean.cjs.js +4923 -3248
- package/dist/bitwrench-lean.cjs.min.js +35 -6
- package/dist/bitwrench-lean.es5.js +6325 -4580
- package/dist/bitwrench-lean.es5.min.js +32 -3
- package/dist/bitwrench-lean.esm.js +4923 -3248
- package/dist/bitwrench-lean.esm.min.js +35 -6
- package/dist/bitwrench-lean.umd.js +4923 -3248
- package/dist/bitwrench-lean.umd.min.js +35 -6
- package/dist/bitwrench.cjs.js +5082 -3667
- package/dist/bitwrench.cjs.min.js +38 -8
- package/dist/bitwrench.css +2289 -6034
- package/dist/bitwrench.es5.js +6862 -5346
- package/dist/bitwrench.es5.min.js +34 -5
- package/dist/bitwrench.esm.js +5082 -3667
- package/dist/bitwrench.esm.min.js +38 -8
- package/dist/bitwrench.min.css +1 -0
- package/dist/bitwrench.umd.js +5082 -3667
- package/dist/bitwrench.umd.min.js +38 -8
- package/dist/builds.json +184 -74
- package/dist/bwserve.cjs.js +646 -0
- package/dist/bwserve.esm.js +638 -0
- package/dist/sri.json +36 -26
- package/package.json +23 -6
- package/readme.html +71 -32
- package/src/bitwrench-bccl-entry.js +72 -0
- package/src/{bitwrench-components-v2.js → bitwrench-bccl.js} +396 -647
- package/src/bitwrench-code-edit.js +98 -48
- package/src/bitwrench-color-utils.js +24 -18
- package/src/bitwrench-components-stub.js +4 -1
- package/src/bitwrench-file-ops.js +180 -0
- package/src/bitwrench-lean.js +2 -2
- package/src/bitwrench-styles.js +1287 -4029
- package/src/bitwrench-utils.js +458 -0
- package/src/bitwrench.js +2070 -1292
- package/src/bwserve/client.js +182 -0
- package/src/bwserve/index.js +352 -0
- package/src/bwserve/shell.js +103 -0
- package/src/cli/index.js +36 -15
- package/src/cli/layout-default.js +18 -18
- package/src/cli/serve.js +325 -0
- package/src/generate-css.js +73 -53
- package/src/version.js +3 -3
- package/src/bitwrench-component-base.js +0 -736
- package/src/bitwrench-components-inline.js +0 -374
- package/src/bitwrench-components.js +0 -610
- /package/bin/{bitwrench.js → bwcli.js} +0 -0
|
@@ -0,0 +1,3746 @@
|
|
|
1
|
+
/*! bitwrench-bccl v2.0.16 | 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
|
+
return def.make(props || {});
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3619
|
+
var components = /*#__PURE__*/Object.freeze({
|
|
3620
|
+
__proto__: null,
|
|
3621
|
+
BCCL: BCCL,
|
|
3622
|
+
make: make,
|
|
3623
|
+
makeAccordion: makeAccordion,
|
|
3624
|
+
makeAlert: makeAlert,
|
|
3625
|
+
makeAvatar: makeAvatar,
|
|
3626
|
+
makeBadge: makeBadge,
|
|
3627
|
+
makeBreadcrumb: makeBreadcrumb,
|
|
3628
|
+
makeButton: makeButton,
|
|
3629
|
+
makeButtonGroup: makeButtonGroup,
|
|
3630
|
+
makeCTA: makeCTA,
|
|
3631
|
+
makeCard: makeCard,
|
|
3632
|
+
makeCarousel: makeCarousel,
|
|
3633
|
+
makeCheckbox: makeCheckbox,
|
|
3634
|
+
makeChipInput: makeChipInput,
|
|
3635
|
+
makeCodeDemo: makeCodeDemo,
|
|
3636
|
+
makeCol: makeCol,
|
|
3637
|
+
makeContainer: makeContainer,
|
|
3638
|
+
makeDropdown: makeDropdown,
|
|
3639
|
+
makeFeatureGrid: makeFeatureGrid,
|
|
3640
|
+
makeFileUpload: makeFileUpload,
|
|
3641
|
+
makeForm: makeForm,
|
|
3642
|
+
makeFormGroup: makeFormGroup,
|
|
3643
|
+
makeHero: makeHero,
|
|
3644
|
+
makeInput: makeInput,
|
|
3645
|
+
makeListGroup: makeListGroup,
|
|
3646
|
+
makeMediaObject: makeMediaObject,
|
|
3647
|
+
makeModal: makeModal,
|
|
3648
|
+
makeNav: makeNav,
|
|
3649
|
+
makeNavbar: makeNavbar,
|
|
3650
|
+
makePagination: makePagination,
|
|
3651
|
+
makePopover: makePopover,
|
|
3652
|
+
makeProgress: makeProgress,
|
|
3653
|
+
makeRadio: makeRadio,
|
|
3654
|
+
makeRange: makeRange,
|
|
3655
|
+
makeRow: makeRow,
|
|
3656
|
+
makeSearchInput: makeSearchInput,
|
|
3657
|
+
makeSection: makeSection,
|
|
3658
|
+
makeSelect: makeSelect,
|
|
3659
|
+
makeSkeleton: makeSkeleton,
|
|
3660
|
+
makeSpinner: makeSpinner,
|
|
3661
|
+
makeStack: makeStack,
|
|
3662
|
+
makeStatCard: makeStatCard,
|
|
3663
|
+
makeStepper: makeStepper,
|
|
3664
|
+
makeSwitch: makeSwitch,
|
|
3665
|
+
makeTabs: makeTabs,
|
|
3666
|
+
makeTextarea: makeTextarea,
|
|
3667
|
+
makeTimeline: makeTimeline,
|
|
3668
|
+
makeToast: makeToast,
|
|
3669
|
+
makeTooltip: makeTooltip,
|
|
3670
|
+
variantClass: variantClass
|
|
3671
|
+
});
|
|
3672
|
+
|
|
3673
|
+
/**
|
|
3674
|
+
* bitwrench-bccl-entry.js — Standalone entry point for BCCL component library.
|
|
3675
|
+
*
|
|
3676
|
+
* Use this alongside bitwrench-lean when you want the core library and
|
|
3677
|
+
* BCCL components as separate files. The UMD build auto-registers all
|
|
3678
|
+
* make*() functions onto the global `bw` object if present.
|
|
3679
|
+
*
|
|
3680
|
+
* Usage (browser):
|
|
3681
|
+
* <script src="bitwrench-lean.umd.min.js"></script>
|
|
3682
|
+
* <script src="bitwrench-bccl.umd.min.js"></script>
|
|
3683
|
+
*
|
|
3684
|
+
* Usage (ESM):
|
|
3685
|
+
* import bw from 'bitwrench/lean';
|
|
3686
|
+
* import { registerBCCL } from 'bitwrench/bccl';
|
|
3687
|
+
* registerBCCL(bw);
|
|
3688
|
+
*
|
|
3689
|
+
* @module bitwrench-bccl
|
|
3690
|
+
* @license BSD-2-Clause
|
|
3691
|
+
*/
|
|
3692
|
+
|
|
3693
|
+
|
|
3694
|
+
/**
|
|
3695
|
+
* Register all BCCL components onto a bitwrench instance.
|
|
3696
|
+
* Called automatically in UMD builds when `bw` is a global.
|
|
3697
|
+
*
|
|
3698
|
+
* @param {Object} bw - The bitwrench instance to register on
|
|
3699
|
+
*/
|
|
3700
|
+
function registerBCCL(bw) {
|
|
3701
|
+
if (!bw) return;
|
|
3702
|
+
|
|
3703
|
+
// Register all make* functions
|
|
3704
|
+
Object.entries(components).forEach(function(entry) {
|
|
3705
|
+
var name = entry[0], fn = entry[1];
|
|
3706
|
+
if (name.indexOf('make') === 0) {
|
|
3707
|
+
bw[name] = fn;
|
|
3708
|
+
}
|
|
3709
|
+
});
|
|
3710
|
+
|
|
3711
|
+
// Factory dispatch: bw.make('card', props) → bw.makeCard(props)
|
|
3712
|
+
bw.make = make;
|
|
3713
|
+
|
|
3714
|
+
// Component registry
|
|
3715
|
+
bw.BCCL = BCCL;
|
|
3716
|
+
|
|
3717
|
+
// Variant class helper
|
|
3718
|
+
bw.variantClass = variantClass;
|
|
3719
|
+
|
|
3720
|
+
// Create functions that return handles
|
|
3721
|
+
if (typeof bw.renderComponent === 'function') {
|
|
3722
|
+
Object.entries(components).forEach(function(entry) {
|
|
3723
|
+
var name = entry[0], fn = entry[1];
|
|
3724
|
+
if (name.indexOf('make') === 0) {
|
|
3725
|
+
var createName = 'create' + name.substring(4);
|
|
3726
|
+
bw[createName] = function(props) {
|
|
3727
|
+
var taco = fn(props);
|
|
3728
|
+
return bw.renderComponent(taco);
|
|
3729
|
+
};
|
|
3730
|
+
}
|
|
3731
|
+
});
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
|
|
3735
|
+
// UMD auto-registration: if `bw` exists as a global, register automatically
|
|
3736
|
+
if (typeof window !== 'undefined' && typeof window.bw !== 'undefined') {
|
|
3737
|
+
registerBCCL(window.bw);
|
|
3738
|
+
} else if (typeof globalThis !== 'undefined' && typeof globalThis.bw !== 'undefined') {
|
|
3739
|
+
registerBCCL(globalThis.bw);
|
|
3740
|
+
}
|
|
3741
|
+
|
|
3742
|
+
exports.BCCL = BCCL;
|
|
3743
|
+
exports.make = make;
|
|
3744
|
+
exports.registerBCCL = registerBCCL;
|
|
3745
|
+
exports.variantClass = variantClass;
|
|
3746
|
+
//# sourceMappingURL=bitwrench-bccl.cjs.js.map
|