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