bitwrench 2.0.12 → 2.0.14
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 +4 -4
- package/dist/bitwrench-code-edit.cjs.js +1 -1
- package/dist/bitwrench-code-edit.es5.js +1 -1
- package/dist/bitwrench-code-edit.es5.min.js +1 -1
- package/dist/bitwrench-code-edit.esm.js +1 -1
- package/dist/bitwrench-code-edit.esm.min.js +1 -1
- package/dist/bitwrench-code-edit.umd.js +1 -1
- package/dist/bitwrench-code-edit.umd.min.js +1 -1
- package/dist/bitwrench-lean.cjs.js +659 -346
- package/dist/bitwrench-lean.cjs.min.js +5 -5
- package/dist/bitwrench-lean.es5.js +960 -347
- package/dist/bitwrench-lean.es5.min.js +3 -3
- package/dist/bitwrench-lean.esm.js +659 -346
- package/dist/bitwrench-lean.esm.min.js +5 -5
- package/dist/bitwrench-lean.umd.js +659 -346
- package/dist/bitwrench-lean.umd.min.js +5 -5
- package/dist/bitwrench.cjs.js +1737 -452
- package/dist/bitwrench.cjs.min.js +6 -6
- package/dist/bitwrench.css +1625 -168
- package/dist/bitwrench.es5.js +4016 -2341
- package/dist/bitwrench.es5.min.js +4 -4
- package/dist/bitwrench.esm.js +1737 -452
- package/dist/bitwrench.esm.min.js +6 -6
- package/dist/bitwrench.umd.js +1737 -452
- package/dist/bitwrench.umd.min.js +6 -6
- package/dist/builds.json +61 -61
- package/dist/sri.json +26 -26
- package/package.json +1 -1
- package/readme.html +5 -5
- package/src/bitwrench-color-utils.js +137 -17
- package/src/bitwrench-components-v2.js +997 -35
- package/src/bitwrench-styles.js +1098 -370
- package/src/bitwrench.js +128 -75
- package/src/version.js +3 -3
package/dist/bitwrench.umd.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! bitwrench v2.0.
|
|
1
|
+
/*! bitwrench v2.0.14 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
|
|
2
2
|
(function (global, factory) {
|
|
3
3
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
|
4
4
|
typeof define === 'function' && define.amd ? define(factory) :
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
const VERSION_INFO = {
|
|
14
|
-
version: '2.0.
|
|
14
|
+
version: '2.0.14',
|
|
15
15
|
name: 'bitwrench',
|
|
16
16
|
description: 'A library for javascript UI functions.',
|
|
17
17
|
license: 'BSD-2-Clause',
|
|
18
18
|
homepage: 'https://deftio.github.com/bitwrench/pages',
|
|
19
19
|
repository: 'git+https://github.com/deftio/bitwrench.git',
|
|
20
20
|
author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
|
|
21
|
-
buildDate: '2026-03-
|
|
21
|
+
buildDate: '2026-03-08T08:04:06.572Z'
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -276,6 +276,29 @@
|
|
|
276
276
|
return relativeLuminance(hex) > 0.179 ? '#000' : '#fff';
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Shift a color's hue toward a target hue by a given amount.
|
|
281
|
+
* Uses shortest-arc interpolation on the hue wheel.
|
|
282
|
+
* @param {string} sourceHex - Color to shift
|
|
283
|
+
* @param {string} targetHex - Color whose hue to shift toward
|
|
284
|
+
* @param {number} [amount=0.20] - 0 = no shift, 1 = full shift to target hue
|
|
285
|
+
* @returns {string} Harmonized hex color
|
|
286
|
+
*/
|
|
287
|
+
function harmonize(sourceHex, targetHex, amount) {
|
|
288
|
+
if (amount === undefined) amount = 0.20;
|
|
289
|
+
if (amount === 0) return sourceHex;
|
|
290
|
+
var srcHsl = hexToHsl(sourceHex);
|
|
291
|
+
var tgtHsl = hexToHsl(targetHex);
|
|
292
|
+
|
|
293
|
+
// Shortest-arc hue interpolation
|
|
294
|
+
var diff = tgtHsl[0] - srcHsl[0];
|
|
295
|
+
if (diff > 180) diff -= 360;
|
|
296
|
+
if (diff < -180) diff += 360;
|
|
297
|
+
|
|
298
|
+
var newHue = (srcHsl[0] + diff * amount + 360) % 360;
|
|
299
|
+
return hslToHex([newHue, srcHsl[1], srcHsl[2]]);
|
|
300
|
+
}
|
|
301
|
+
|
|
279
302
|
/**
|
|
280
303
|
* Derive a full shade palette for a single semantic color.
|
|
281
304
|
* @param {string} hex - Base color hex
|
|
@@ -295,31 +318,128 @@
|
|
|
295
318
|
};
|
|
296
319
|
}
|
|
297
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Derive the alternate (luminance-inverted) version of a single seed color.
|
|
323
|
+
* Preserves hue, mirrors lightness, adjusts saturation for readability.
|
|
324
|
+
* @param {string} hex - Seed hex color
|
|
325
|
+
* @returns {string} Alternate hex color
|
|
326
|
+
*/
|
|
327
|
+
function deriveAlternateSeed(hex) {
|
|
328
|
+
var hsl = hexToHsl(hex);
|
|
329
|
+
var h = hsl[0], s = hsl[1], l = hsl[2];
|
|
330
|
+
var altL, altS;
|
|
331
|
+
|
|
332
|
+
if (l > 50) {
|
|
333
|
+
// Light color → make dark. Map 50-100 → 30-10 range
|
|
334
|
+
altL = clip(100 - l - 10, 8, 40);
|
|
335
|
+
// Reduce saturation slightly — vivid colors at low lightness look garish
|
|
336
|
+
altS = clip(s * 0.85, 0, 100);
|
|
337
|
+
} else {
|
|
338
|
+
// Dark color → make light. Map 0-50 → 65-92 range
|
|
339
|
+
altL = clip(100 - l + 10, 60, 92);
|
|
340
|
+
// Slightly increase saturation for light variant
|
|
341
|
+
altS = clip(s * 1.1, 0, 100);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return hslToHex([h, altS, altL]);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Determine whether a palette config is "light-flavored" based on
|
|
349
|
+
* the average luminance of its seed colors.
|
|
350
|
+
* @param {Object} config - Theme config with primary, secondary hex colors
|
|
351
|
+
* @returns {boolean} true if the seeds are predominantly light
|
|
352
|
+
*/
|
|
353
|
+
function isLightPalette(config) {
|
|
354
|
+
var lum = relativeLuminance(config.primary);
|
|
355
|
+
if (config.secondary) lum = (lum + relativeLuminance(config.secondary)) / 2;
|
|
356
|
+
if (config.tertiary) lum = (lum * 2 + relativeLuminance(config.tertiary)) / 3;
|
|
357
|
+
return lum > 0.179;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Derive a complete alternate config from a primary theme config.
|
|
362
|
+
* Each seed color is luminance-inverted; semantic colors are adjusted for
|
|
363
|
+
* the new luminance context.
|
|
364
|
+
* @param {Object} config - Primary theme config
|
|
365
|
+
* @returns {Object} Alternate theme config (same shape, inverted lightness)
|
|
366
|
+
*/
|
|
367
|
+
function deriveAlternateConfig(config) {
|
|
368
|
+
var alt = {};
|
|
369
|
+
// Invert the user's seed colors
|
|
370
|
+
alt.primary = deriveAlternateSeed(config.primary);
|
|
371
|
+
alt.secondary = deriveAlternateSeed(config.secondary);
|
|
372
|
+
alt.tertiary = config.tertiary ? deriveAlternateSeed(config.tertiary) : alt.primary;
|
|
373
|
+
|
|
374
|
+
// Derive alternate surface colors from primary hue
|
|
375
|
+
var priHsl = hexToHsl(config.primary);
|
|
376
|
+
var h = priHsl[0];
|
|
377
|
+
var isLight = isLightPalette(config);
|
|
378
|
+
|
|
379
|
+
if (isLight) {
|
|
380
|
+
// Primary is light → alternate needs dark surfaces
|
|
381
|
+
alt.light = hslToHex([h, Math.min(priHsl[1], 15), 15]);
|
|
382
|
+
alt.dark = hslToHex([h, 5, 88]);
|
|
383
|
+
} else {
|
|
384
|
+
// Primary is dark → alternate needs light surfaces
|
|
385
|
+
alt.light = hslToHex([h, Math.min(priHsl[1], 10), 96]);
|
|
386
|
+
alt.dark = hslToHex([h, 10, 18]);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Semantic colors: harmonize toward primary, then invert for alternate
|
|
390
|
+
var amt = config.harmonize !== undefined ? config.harmonize : 0.20;
|
|
391
|
+
var semanticDefaults = {
|
|
392
|
+
success: '#198754', danger: '#dc3545',
|
|
393
|
+
warning: '#f0ad4e', info: '#17a2b8'
|
|
394
|
+
};
|
|
395
|
+
var semantics = ['success', 'danger', 'warning', 'info'];
|
|
396
|
+
for (var i = 0; i < semantics.length; i++) {
|
|
397
|
+
var key = semantics[i];
|
|
398
|
+
var seed = config[key] || semanticDefaults[key];
|
|
399
|
+
var harmonized = harmonize(seed, config.primary, amt);
|
|
400
|
+
alt[key] = deriveAlternateSeed(harmonized);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Semantic colors are already harmonized+inverted — don't re-harmonize in derivePalette
|
|
404
|
+
alt.harmonize = 0;
|
|
405
|
+
|
|
406
|
+
return alt;
|
|
407
|
+
}
|
|
408
|
+
|
|
298
409
|
/**
|
|
299
410
|
* Derive complete palette from a theme config object.
|
|
411
|
+
* Semantic colors are harmonized toward the primary hue (configurable).
|
|
412
|
+
* Light/dark surface colors are tinted with the primary hue.
|
|
300
413
|
* @param {Object} config - Theme config with primary, secondary, tertiary, etc.
|
|
301
|
-
* @
|
|
414
|
+
* @param {number} [config.harmonize=0.20] - Hue shift amount for semantic colors (0-1)
|
|
415
|
+
* @returns {Object} Full palette with shades for all 9 semantic colors
|
|
302
416
|
*/
|
|
303
417
|
function derivePalette(config) {
|
|
304
|
-
var
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
418
|
+
var amt = config.harmonize !== undefined ? config.harmonize : 0.20;
|
|
419
|
+
var pri = config.primary;
|
|
420
|
+
var priHsl = hexToHsl(pri);
|
|
421
|
+
var h = priHsl[0];
|
|
422
|
+
|
|
423
|
+
// Semantic defaults — harmonized toward primary hue
|
|
424
|
+
var successBase = harmonize(config.success || '#198754', pri, amt);
|
|
425
|
+
var dangerBase = harmonize(config.danger || '#dc3545', pri, amt);
|
|
426
|
+
var warningBase = harmonize(config.warning || '#f0ad4e', pri, amt);
|
|
427
|
+
var infoBase = harmonize(config.info || '#17a2b8', pri, amt);
|
|
428
|
+
|
|
429
|
+
// Light/dark: derive from primary hue with low saturation (if not user-supplied)
|
|
430
|
+
var lightBase = config.light || hslToHex([h, 8, 97]);
|
|
431
|
+
var darkBase = config.dark || hslToHex([h, 10, 13]);
|
|
312
432
|
|
|
313
433
|
var palette = {
|
|
314
|
-
primary:
|
|
434
|
+
primary: deriveShades(config.primary),
|
|
315
435
|
secondary: deriveShades(config.secondary),
|
|
316
|
-
tertiary:
|
|
317
|
-
success:
|
|
318
|
-
danger:
|
|
319
|
-
warning:
|
|
320
|
-
info:
|
|
321
|
-
light:
|
|
322
|
-
dark:
|
|
436
|
+
tertiary: deriveShades(config.tertiary),
|
|
437
|
+
success: deriveShades(successBase),
|
|
438
|
+
danger: deriveShades(dangerBase),
|
|
439
|
+
warning: deriveShades(warningBase),
|
|
440
|
+
info: deriveShades(infoBase),
|
|
441
|
+
light: deriveShades(lightBase),
|
|
442
|
+
dark: deriveShades(darkBase)
|
|
323
443
|
};
|
|
324
444
|
|
|
325
445
|
return palette;
|
|
@@ -368,6 +488,88 @@
|
|
|
368
488
|
pill: { btn: '50rem', card: '1rem', badge: '50rem', alert: '1rem', input: '50rem' }
|
|
369
489
|
};
|
|
370
490
|
|
|
491
|
+
// ---- Typography scale presets ----
|
|
492
|
+
|
|
493
|
+
var TYPE_RATIO_PRESETS = {
|
|
494
|
+
tight: 1.125,
|
|
495
|
+
normal: 1.200,
|
|
496
|
+
relaxed: 1.250,
|
|
497
|
+
dramatic: 1.333
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Generate a modular type scale from a base size and ratio.
|
|
502
|
+
* @param {number} base - Base font size in px (default 16)
|
|
503
|
+
* @param {number} ratio - Scale ratio (default 1.200)
|
|
504
|
+
* @returns {Object} { xs, sm, base, lg, xl, '2xl', '3xl', '4xl' } in px
|
|
505
|
+
*/
|
|
506
|
+
function generateTypeScale(base, ratio) {
|
|
507
|
+
if (!base) base = 16;
|
|
508
|
+
if (!ratio) ratio = 1.200;
|
|
509
|
+
return {
|
|
510
|
+
xs: Math.round(base / (ratio * ratio)),
|
|
511
|
+
sm: Math.round(base / ratio),
|
|
512
|
+
base: base,
|
|
513
|
+
lg: Math.round(base * ratio),
|
|
514
|
+
xl: Math.round(base * ratio * ratio),
|
|
515
|
+
'2xl': Math.round(base * Math.pow(ratio, 3)),
|
|
516
|
+
'3xl': Math.round(base * Math.pow(ratio, 4)),
|
|
517
|
+
'4xl': Math.round(base * Math.pow(ratio, 5))
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// ---- Elevation (shadow depth) presets ----
|
|
522
|
+
|
|
523
|
+
var ELEVATION_PRESETS = {
|
|
524
|
+
flat: {
|
|
525
|
+
sm: 'none',
|
|
526
|
+
md: 'none',
|
|
527
|
+
lg: 'none',
|
|
528
|
+
xl: 'none'
|
|
529
|
+
},
|
|
530
|
+
sm: {
|
|
531
|
+
sm: '0 1px 2px rgba(0,0,0,0.05)',
|
|
532
|
+
md: '0 1px 3px rgba(0,0,0,0.08)',
|
|
533
|
+
lg: '0 2px 6px rgba(0,0,0,0.10)',
|
|
534
|
+
xl: '0 4px 12px rgba(0,0,0,0.12)'
|
|
535
|
+
},
|
|
536
|
+
md: {
|
|
537
|
+
sm: '0 1px 3px rgba(0,0,0,0.08)',
|
|
538
|
+
md: '0 2px 6px rgba(0,0,0,0.12)',
|
|
539
|
+
lg: '0 4px 12px rgba(0,0,0,0.16)',
|
|
540
|
+
xl: '0 8px 24px rgba(0,0,0,0.20)'
|
|
541
|
+
},
|
|
542
|
+
lg: {
|
|
543
|
+
sm: '0 2px 4px rgba(0,0,0,0.10)',
|
|
544
|
+
md: '0 4px 12px rgba(0,0,0,0.16)',
|
|
545
|
+
lg: '0 8px 24px rgba(0,0,0,0.22)',
|
|
546
|
+
xl: '0 16px 48px rgba(0,0,0,0.28)'
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
// ---- Motion (transition) presets ----
|
|
551
|
+
|
|
552
|
+
var MOTION_PRESETS = {
|
|
553
|
+
reduced: {
|
|
554
|
+
fast: '0ms',
|
|
555
|
+
normal: '0ms',
|
|
556
|
+
slow: '0ms',
|
|
557
|
+
easing: 'linear'
|
|
558
|
+
},
|
|
559
|
+
standard: {
|
|
560
|
+
fast: '100ms',
|
|
561
|
+
normal: '200ms',
|
|
562
|
+
slow: '300ms',
|
|
563
|
+
easing: 'ease-out'
|
|
564
|
+
},
|
|
565
|
+
expressive: {
|
|
566
|
+
fast: '150ms',
|
|
567
|
+
normal: '300ms',
|
|
568
|
+
slow: '500ms',
|
|
569
|
+
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)'
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
|
|
371
573
|
/**
|
|
372
574
|
* Default palette config — matches existing hardcoded colors
|
|
373
575
|
*/
|
|
@@ -377,8 +579,8 @@
|
|
|
377
579
|
tertiary: '#006666',
|
|
378
580
|
success: '#198754',
|
|
379
581
|
danger: '#dc3545',
|
|
380
|
-
warning: '#
|
|
381
|
-
info: '#
|
|
582
|
+
warning: '#b38600',
|
|
583
|
+
info: '#0891b2',
|
|
382
584
|
light: '#f8f9fa',
|
|
383
585
|
dark: '#212529'
|
|
384
586
|
};
|
|
@@ -403,18 +605,32 @@
|
|
|
403
605
|
};
|
|
404
606
|
|
|
405
607
|
/**
|
|
406
|
-
* Resolve layout config to spacing
|
|
407
|
-
* @param {Object} config - { spacing, radius, fontSize }
|
|
408
|
-
* @returns {Object} { spacing, radius, fontSize }
|
|
608
|
+
* Resolve layout config to spacing, radius, typeScale, elevation, and motion objects.
|
|
609
|
+
* @param {Object} config - { spacing, radius, fontSize, typeRatio, elevation, motion }
|
|
610
|
+
* @returns {Object} { spacing, radius, fontSize, typeScale, elevation, motion }
|
|
409
611
|
*/
|
|
410
612
|
function resolveLayout(config) {
|
|
411
613
|
var sp = (config && config.spacing) || 'normal';
|
|
412
614
|
var rd = (config && config.radius) || 'md';
|
|
413
615
|
var fs = (config && config.fontSize) || 1.0;
|
|
616
|
+
|
|
617
|
+
// typeRatio: accept preset name or number
|
|
618
|
+
var tr = (config && config.typeRatio) || 'normal';
|
|
619
|
+
var ratioNum = typeof tr === 'string' ? (TYPE_RATIO_PRESETS[tr] || TYPE_RATIO_PRESETS.normal) : tr;
|
|
620
|
+
|
|
621
|
+
// elevation: accept preset name or object
|
|
622
|
+
var el = (config && config.elevation) || 'md';
|
|
623
|
+
|
|
624
|
+
// motion: accept preset name or object
|
|
625
|
+
var mo = (config && config.motion) || 'standard';
|
|
626
|
+
|
|
414
627
|
return {
|
|
415
628
|
spacing: typeof sp === 'string' ? (SPACING_PRESETS[sp] || SPACING_PRESETS.normal) : sp,
|
|
416
629
|
radius: typeof rd === 'string' ? (RADIUS_PRESETS[rd] || RADIUS_PRESETS.md) : rd,
|
|
417
|
-
fontSize: fs
|
|
630
|
+
fontSize: fs,
|
|
631
|
+
typeScale: generateTypeScale(16, ratioNum),
|
|
632
|
+
elevation: typeof el === 'string' ? (ELEVATION_PRESETS[el] || ELEVATION_PRESETS.md) : el,
|
|
633
|
+
motion: typeof mo === 'string' ? (MOTION_PRESETS[mo] || MOTION_PRESETS.standard) : mo
|
|
418
634
|
};
|
|
419
635
|
}
|
|
420
636
|
|
|
@@ -438,12 +654,13 @@
|
|
|
438
654
|
// Themed CSS generators
|
|
439
655
|
// =========================================================================
|
|
440
656
|
|
|
441
|
-
function generateTypographyThemed(scope, palette) {
|
|
657
|
+
function generateTypographyThemed(scope, palette, layout) {
|
|
658
|
+
var mot = layout.motion;
|
|
442
659
|
var rules = {};
|
|
443
660
|
rules[scopeSelector(scope, 'a')] = {
|
|
444
661
|
'color': palette.primary.base,
|
|
445
662
|
'text-decoration': 'none',
|
|
446
|
-
'transition': 'color
|
|
663
|
+
'transition': 'color ' + mot.fast + ' ' + mot.easing
|
|
447
664
|
};
|
|
448
665
|
rules[scopeSelector(scope, 'a:hover')] = {
|
|
449
666
|
'color': palette.primary.hover,
|
|
@@ -463,7 +680,8 @@
|
|
|
463
680
|
'border-radius': rd.btn
|
|
464
681
|
};
|
|
465
682
|
rules[scopeSelector(scope, '.bw-btn:focus-visible')] = {
|
|
466
|
-
'outline': '
|
|
683
|
+
'outline': '2px solid currentColor',
|
|
684
|
+
'outline-offset': '2px',
|
|
467
685
|
'box-shadow': '0 0 0 3px ' + palette.primary.focus
|
|
468
686
|
};
|
|
469
687
|
|
|
@@ -553,14 +771,15 @@
|
|
|
553
771
|
var sp = layout.spacing;
|
|
554
772
|
var rd = layout.radius;
|
|
555
773
|
|
|
774
|
+
var elev = layout.elevation;
|
|
556
775
|
rules[scopeSelector(scope, '.bw-card')] = {
|
|
557
776
|
'background-color': '#fff',
|
|
558
777
|
'border': '1px solid ' + palette.light.border,
|
|
559
778
|
'border-radius': rd.card,
|
|
560
|
-
'box-shadow':
|
|
779
|
+
'box-shadow': elev.sm
|
|
561
780
|
};
|
|
562
781
|
rules[scopeSelector(scope, '.bw-card:hover')] = {
|
|
563
|
-
'box-shadow':
|
|
782
|
+
'box-shadow': elev.md
|
|
564
783
|
};
|
|
565
784
|
rules[scopeSelector(scope, '.bw-card-body')] = {
|
|
566
785
|
'padding': sp.card
|
|
@@ -607,6 +826,8 @@
|
|
|
607
826
|
};
|
|
608
827
|
rules[scopeSelector(scope, '.bw-form-control:focus')] = {
|
|
609
828
|
'border-color': palette.primary.border,
|
|
829
|
+
'outline': '2px solid ' + palette.primary.base,
|
|
830
|
+
'outline-offset': '-1px',
|
|
610
831
|
'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
|
|
611
832
|
};
|
|
612
833
|
rules[scopeSelector(scope, '.bw-form-control::placeholder')] = {
|
|
@@ -764,7 +985,8 @@
|
|
|
764
985
|
'border-color': palette.light.border
|
|
765
986
|
};
|
|
766
987
|
rules[scopeSelector(scope, '.bw-page-link:focus')] = {
|
|
767
|
-
'
|
|
988
|
+
'outline': '2px solid ' + palette.primary.base,
|
|
989
|
+
'outline-offset': '-2px'
|
|
768
990
|
};
|
|
769
991
|
rules[scopeSelector(scope, '.bw-page-item.bw-active .bw-page-link')] = {
|
|
770
992
|
'color': palette.primary.textOn,
|
|
@@ -923,12 +1145,12 @@
|
|
|
923
1145
|
return rules;
|
|
924
1146
|
}
|
|
925
1147
|
|
|
926
|
-
function generateModalThemed(scope, palette) {
|
|
1148
|
+
function generateModalThemed(scope, palette, layout) {
|
|
927
1149
|
var rules = {};
|
|
928
1150
|
rules[scopeSelector(scope, '.bw-modal-content')] = {
|
|
929
1151
|
'background-color': '#fff',
|
|
930
1152
|
'border-color': palette.light.border,
|
|
931
|
-
'box-shadow':
|
|
1153
|
+
'box-shadow': layout.elevation.lg
|
|
932
1154
|
};
|
|
933
1155
|
rules[scopeSelector(scope, '.bw-modal-header')] = {
|
|
934
1156
|
'border-bottom-color': palette.light.border
|
|
@@ -942,12 +1164,12 @@
|
|
|
942
1164
|
return rules;
|
|
943
1165
|
}
|
|
944
1166
|
|
|
945
|
-
function generateToastThemed(scope, palette) {
|
|
1167
|
+
function generateToastThemed(scope, palette, layout) {
|
|
946
1168
|
var rules = {};
|
|
947
1169
|
rules[scopeSelector(scope, '.bw-toast')] = {
|
|
948
1170
|
'background-color': '#fff',
|
|
949
1171
|
'border-color': 'rgba(0,0,0,0.1)',
|
|
950
|
-
'box-shadow':
|
|
1172
|
+
'box-shadow': layout.elevation.lg
|
|
951
1173
|
};
|
|
952
1174
|
rules[scopeSelector(scope, '.bw-toast-header')] = {
|
|
953
1175
|
'border-bottom-color': 'rgba(0,0,0,0.05)'
|
|
@@ -961,12 +1183,12 @@
|
|
|
961
1183
|
return rules;
|
|
962
1184
|
}
|
|
963
1185
|
|
|
964
|
-
function generateDropdownThemed(scope, palette) {
|
|
1186
|
+
function generateDropdownThemed(scope, palette, layout) {
|
|
965
1187
|
var rules = {};
|
|
966
1188
|
rules[scopeSelector(scope, '.bw-dropdown-menu')] = {
|
|
967
1189
|
'background-color': '#fff',
|
|
968
1190
|
'border-color': palette.light.border,
|
|
969
|
-
'box-shadow':
|
|
1191
|
+
'box-shadow': layout.elevation.md
|
|
970
1192
|
};
|
|
971
1193
|
rules[scopeSelector(scope, '.bw-dropdown-item')] = {
|
|
972
1194
|
'color': palette.dark.base
|
|
@@ -1032,7 +1254,7 @@
|
|
|
1032
1254
|
function generateThemedCSS(scopeName, palette, layout) {
|
|
1033
1255
|
return Object.assign({},
|
|
1034
1256
|
generateResetThemed(scopeName, palette),
|
|
1035
|
-
generateTypographyThemed(scopeName, palette),
|
|
1257
|
+
generateTypographyThemed(scopeName, palette, layout),
|
|
1036
1258
|
generateButtons(scopeName, palette, layout),
|
|
1037
1259
|
generateAlerts(scopeName, palette, layout),
|
|
1038
1260
|
generateBadges(scopeName, palette),
|
|
@@ -1051,9 +1273,9 @@
|
|
|
1051
1273
|
generateSectionsThemed(scopeName, palette),
|
|
1052
1274
|
generateAccordionThemed(scopeName, palette),
|
|
1053
1275
|
generateCarouselThemed(scopeName, palette),
|
|
1054
|
-
generateModalThemed(scopeName, palette),
|
|
1055
|
-
generateToastThemed(scopeName, palette),
|
|
1056
|
-
generateDropdownThemed(scopeName, palette),
|
|
1276
|
+
generateModalThemed(scopeName, palette, layout),
|
|
1277
|
+
generateToastThemed(scopeName, palette, layout),
|
|
1278
|
+
generateDropdownThemed(scopeName, palette, layout),
|
|
1057
1279
|
generateSwitchThemed(scopeName, palette),
|
|
1058
1280
|
generateSkeletonThemed(scopeName, palette),
|
|
1059
1281
|
generateAvatarThemed(scopeName, palette),
|
|
@@ -1206,11 +1428,23 @@
|
|
|
1206
1428
|
// =========================================================================
|
|
1207
1429
|
|
|
1208
1430
|
/**
|
|
1209
|
-
* Structural styles
|
|
1210
|
-
* properties. No colors, backgrounds, shadows, or border-colors.
|
|
1211
|
-
* These never change with themes.
|
|
1431
|
+
* Structural styles — layout, sizing, spacing, positioning, and behavior.
|
|
1212
1432
|
*
|
|
1213
|
-
*
|
|
1433
|
+
* POLICY: No colors, backgrounds, shadows, or border-colors in this function.
|
|
1434
|
+
* All cosmetic values belong in `defaultStyles.*` sections (unthemed defaults)
|
|
1435
|
+
* or in `generateThemedCSS()` (theme-driven colors).
|
|
1436
|
+
*
|
|
1437
|
+
* Exception: `.bw-progress-bar-striped` uses rgba(255,255,255,.15) for the
|
|
1438
|
+
* stripe pattern overlay. This is theme-neutral — a semi-transparent white
|
|
1439
|
+
* gradient that creates visible stripes on any background color.
|
|
1440
|
+
*
|
|
1441
|
+
* Architecture:
|
|
1442
|
+
* getStructuralStyles() → layout-only rules (never change with themes)
|
|
1443
|
+
* defaultStyles.* → cosmetic defaults (colors, shadows, borders)
|
|
1444
|
+
* generateThemedCSS() → palette-driven cosmetics from seed colors
|
|
1445
|
+
* generateAlternateCSS() → alternate palette (luminance-inverted)
|
|
1446
|
+
*
|
|
1447
|
+
* @returns {Object} CSS rules object (layout-only, theme-independent)
|
|
1214
1448
|
*/
|
|
1215
1449
|
function getStructuralStyles() {
|
|
1216
1450
|
var rules = {};
|
|
@@ -1261,12 +1495,12 @@
|
|
|
1261
1495
|
'text-decoration': 'none', 'vertical-align': 'middle', 'cursor': 'pointer',
|
|
1262
1496
|
'user-select': 'none', 'border': '1px solid transparent',
|
|
1263
1497
|
'padding': '0.5rem 1.125rem', 'font-size': '0.875rem', 'font-family': 'inherit',
|
|
1264
|
-
'border-radius': '6px', 'transition': 'all 0.15s
|
|
1498
|
+
'border-radius': '6px', 'transition': 'all 0.15s ease-out',
|
|
1265
1499
|
'gap': '0.5rem'
|
|
1266
1500
|
};
|
|
1267
1501
|
rules['.bw-btn:hover'] = { 'text-decoration': 'none', 'transform': 'translateY(-1px)' };
|
|
1268
1502
|
rules['.bw-btn:active'] = { 'transform': 'translateY(0)' };
|
|
1269
|
-
rules['.bw-btn:focus-visible'] = { 'outline': '
|
|
1503
|
+
rules['.bw-btn:focus-visible'] = { 'outline': '2px solid currentColor', 'outline-offset': '2px' };
|
|
1270
1504
|
rules['.bw-btn:disabled'] = { 'opacity': '0.5', 'cursor': 'not-allowed', 'pointer-events': 'none' };
|
|
1271
1505
|
rules['.bw-btn-lg'] = { 'padding': '0.625rem 1.5rem', 'font-size': '1rem', 'border-radius': '8px' };
|
|
1272
1506
|
rules['.bw-btn-sm'] = { 'padding': '0.25rem 0.75rem', 'font-size': '0.8125rem', 'border-radius': '5px' };
|
|
@@ -1276,7 +1510,7 @@
|
|
|
1276
1510
|
'position': 'relative', 'display': 'flex', 'flex-direction': 'column',
|
|
1277
1511
|
'min-width': '0', 'height': '100%', 'word-wrap': 'break-word',
|
|
1278
1512
|
'background-clip': 'border-box', 'border': '1px solid transparent',
|
|
1279
|
-
'border-radius': '8px', 'transition': 'box-shadow 0.2s
|
|
1513
|
+
'border-radius': '8px', 'transition': 'box-shadow 0.2s ease-out, transform 0.2s ease-out',
|
|
1280
1514
|
'margin-bottom': '1.5rem', 'overflow': 'hidden'
|
|
1281
1515
|
};
|
|
1282
1516
|
rules['.bw-card-body'] = { 'flex': '1 1 auto', 'padding': '1.25rem 1.5rem' };
|
|
@@ -1285,7 +1519,7 @@
|
|
|
1285
1519
|
rules['.bw-card-text'] = { 'margin-bottom': '0', 'font-size': '0.9375rem', 'line-height': '1.6' };
|
|
1286
1520
|
rules['.bw-card-header'] = { 'padding': '0.875rem 1.5rem', 'margin-bottom': '0', 'font-weight': '600', 'font-size': '0.875rem' };
|
|
1287
1521
|
rules['.bw-card-footer'] = { 'padding': '0.75rem 1.5rem', 'font-size': '0.875rem' };
|
|
1288
|
-
rules['.bw-card-hoverable'] = { 'transition': 'all 0.3s
|
|
1522
|
+
rules['.bw-card-hoverable'] = { 'transition': 'all 0.3s ease-out' };
|
|
1289
1523
|
rules['.bw-card-img-top'] = { 'width': '100%', 'border-top-left-radius': '7px', 'border-top-right-radius': '7px' };
|
|
1290
1524
|
rules['.bw-card-img-bottom'] = { 'width': '100%', 'border-bottom-left-radius': '7px', 'border-bottom-right-radius': '7px' };
|
|
1291
1525
|
rules['.bw-card-img-left'] = { 'width': '40%', 'object-fit': 'cover' };
|
|
@@ -1298,10 +1532,10 @@
|
|
|
1298
1532
|
'font-size': '0.9375rem', 'font-weight': '400', 'line-height': '1.5',
|
|
1299
1533
|
'background-clip': 'padding-box', 'appearance': 'none',
|
|
1300
1534
|
'border': '1px solid transparent', 'border-radius': '6px',
|
|
1301
|
-
'transition': 'border-color 0.15s ease-
|
|
1535
|
+
'transition': 'border-color 0.15s ease-out, box-shadow 0.15s ease-out',
|
|
1302
1536
|
'font-family': 'inherit'
|
|
1303
1537
|
};
|
|
1304
|
-
rules['.bw-form-control:focus'] = { 'outline': '
|
|
1538
|
+
rules['.bw-form-control:focus'] = { 'outline': '2px solid currentColor', 'outline-offset': '-1px' };
|
|
1305
1539
|
rules['.bw-form-control::placeholder'] = { 'opacity': '1' };
|
|
1306
1540
|
rules['.bw-form-label'] = { 'display': 'block', 'margin-bottom': '0.375rem', 'font-size': '0.875rem', 'font-weight': '600' };
|
|
1307
1541
|
rules['.bw-form-group'] = { 'margin-bottom': '1.25rem' };
|
|
@@ -1313,6 +1547,10 @@
|
|
|
1313
1547
|
};
|
|
1314
1548
|
rules['textarea.bw-form-control'] = { 'min-height': '5rem', 'resize': 'vertical' };
|
|
1315
1549
|
|
|
1550
|
+
// Form validation (structural)
|
|
1551
|
+
rules['.bw-valid-feedback'] = { 'display': 'block', 'font-size': '0.875rem', 'margin-top': '0.25rem' };
|
|
1552
|
+
rules['.bw-invalid-feedback'] = { 'display': 'block', 'font-size': '0.875rem', 'margin-top': '0.25rem' };
|
|
1553
|
+
|
|
1316
1554
|
// Form checks (structural)
|
|
1317
1555
|
Object.assign(rules, {
|
|
1318
1556
|
'.bw-form-check': { 'display': 'flex', 'align-items': 'center', 'gap': '0.5rem', 'min-height': '1.5rem', 'margin-bottom': '0.25rem' },
|
|
@@ -1371,13 +1609,13 @@
|
|
|
1371
1609
|
|
|
1372
1610
|
// Badges (structural)
|
|
1373
1611
|
rules['.bw-badge'] = {
|
|
1374
|
-
'display': 'inline-block', 'padding': '.
|
|
1612
|
+
'display': 'inline-block', 'padding': '0.375rem 0.625rem', 'font-size': '0.875rem',
|
|
1375
1613
|
'font-weight': '600', 'line-height': '1.3', 'text-align': 'center',
|
|
1376
1614
|
'white-space': 'nowrap', 'vertical-align': 'baseline', 'border-radius': '.375rem'
|
|
1377
1615
|
};
|
|
1378
1616
|
rules['.bw-badge:empty'] = { 'display': 'none' };
|
|
1379
|
-
rules['.bw-badge-sm'] = { 'font-size': '.
|
|
1380
|
-
rules['.bw-badge-lg'] = { 'font-size': '
|
|
1617
|
+
rules['.bw-badge-sm'] = { 'font-size': '0.75rem', 'padding': '0.25rem 0.5rem' };
|
|
1618
|
+
rules['.bw-badge-lg'] = { 'font-size': '1rem', 'padding': '0.5rem 0.875rem' };
|
|
1381
1619
|
rules['.bw-badge-pill'] = { 'border-radius': '50rem' };
|
|
1382
1620
|
|
|
1383
1621
|
// Progress (structural)
|
|
@@ -1385,7 +1623,7 @@
|
|
|
1385
1623
|
rules['.bw-progress-bar'] = {
|
|
1386
1624
|
'display': 'flex', 'flex-direction': 'column', 'justify-content': 'center',
|
|
1387
1625
|
'overflow': 'hidden', 'text-align': 'center', 'white-space': 'nowrap',
|
|
1388
|
-
'transition': 'width .
|
|
1626
|
+
'transition': 'width 0.3s ease-out', 'font-weight': '600'
|
|
1389
1627
|
};
|
|
1390
1628
|
rules['.bw-progress-bar-striped'] = {
|
|
1391
1629
|
'background-image': 'linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)',
|
|
@@ -1402,7 +1640,7 @@
|
|
|
1402
1640
|
'display': 'block', 'padding': '0.625rem 1rem', 'font-size': '0.875rem',
|
|
1403
1641
|
'font-weight': '500', 'text-decoration': 'none', 'cursor': 'pointer',
|
|
1404
1642
|
'border': 'none', 'background': 'transparent',
|
|
1405
|
-
'transition': 'color 0.15s, border-color 0.15s', 'font-family': 'inherit'
|
|
1643
|
+
'transition': 'color 0.15s ease-out, background-color 0.15s ease-out, border-color 0.15s ease-out', 'font-family': 'inherit'
|
|
1406
1644
|
};
|
|
1407
1645
|
rules['.bw-nav-tabs .bw-nav-link'] = { 'border': 'none', 'border-bottom': '2px solid transparent', 'border-radius': '0', 'background-color': 'transparent' };
|
|
1408
1646
|
rules['.bw-nav-pills .bw-nav-link'] = { 'border-radius': '6px' };
|
|
@@ -1420,7 +1658,8 @@
|
|
|
1420
1658
|
rules['.bw-list-group-item:last-child'] = { 'border-bottom-right-radius': 'inherit', 'border-bottom-left-radius': 'inherit' };
|
|
1421
1659
|
rules['.bw-list-group-item + .bw-list-group-item'] = { 'border-top-width': '0' };
|
|
1422
1660
|
rules['.bw-list-group-item.disabled'] = { 'pointer-events': 'none' };
|
|
1423
|
-
rules['a.bw-list-group-item'] = { 'cursor': 'pointer' };
|
|
1661
|
+
rules['a.bw-list-group-item'] = { 'cursor': 'pointer', 'transition': 'background-color 0.15s ease-out, color 0.15s ease-out' };
|
|
1662
|
+
rules['a.bw-list-group-item:focus-visible, .bw-list-group-item:focus-visible'] = { 'z-index': '2', 'outline': '2px solid currentColor', 'outline-offset': '-2px' };
|
|
1424
1663
|
rules['.bw-list-group-flush'] = { 'border-radius': '0' };
|
|
1425
1664
|
rules['.bw-list-group-flush > .bw-list-group-item'] = { 'border-width': '0 0 1px', 'border-radius': '0' };
|
|
1426
1665
|
rules['.bw-list-group-flush > .bw-list-group-item:last-child'] = { 'border-bottom-width': '0' };
|
|
@@ -1431,16 +1670,19 @@
|
|
|
1431
1670
|
rules['.bw-page-link'] = {
|
|
1432
1671
|
'position': 'relative', 'display': 'block', 'padding': '0.375rem 0.75rem',
|
|
1433
1672
|
'margin-left': '-1px', 'line-height': '1.25', 'text-decoration': 'none',
|
|
1434
|
-
'transition': 'color 0.15s ease-
|
|
1673
|
+
'transition': 'color 0.15s ease-out, background-color 0.15s ease-out, border-color 0.15s ease-out'
|
|
1435
1674
|
};
|
|
1436
1675
|
rules['.bw-page-item:first-child .bw-page-link'] = { 'margin-left': '0', 'border-top-left-radius': '0.375rem', 'border-bottom-left-radius': '0.375rem' };
|
|
1437
1676
|
rules['.bw-page-item:last-child .bw-page-link'] = { 'border-top-right-radius': '0.375rem', 'border-bottom-right-radius': '0.375rem' };
|
|
1677
|
+
rules['.bw-page-link:focus-visible'] = { 'z-index': '3', 'outline': '2px solid currentColor', 'outline-offset': '-2px' };
|
|
1438
1678
|
|
|
1439
1679
|
// Breadcrumb (structural)
|
|
1440
1680
|
rules['.bw-breadcrumb'] = { 'display': 'flex', 'flex-wrap': 'wrap', 'padding': '0 0', 'margin-bottom': '1rem', 'list-style': 'none' };
|
|
1441
1681
|
rules['.bw-breadcrumb-item'] = { 'display': 'flex' };
|
|
1442
1682
|
rules['.bw-breadcrumb-item + .bw-breadcrumb-item'] = { 'padding-left': '0.5rem' };
|
|
1443
1683
|
rules['.bw-breadcrumb-item + .bw-breadcrumb-item::before'] = { 'float': 'left', 'padding-right': '0.5rem', 'content': '"/"' };
|
|
1684
|
+
rules['.bw-breadcrumb-item a'] = { 'text-decoration': 'none', 'transition': 'color 0.15s ease-out' };
|
|
1685
|
+
rules['.bw-breadcrumb-item.active'] = { 'font-weight': '500' };
|
|
1444
1686
|
|
|
1445
1687
|
// Hero (structural)
|
|
1446
1688
|
rules['.bw-hero'] = { 'position': 'relative', 'overflow': 'hidden' };
|
|
@@ -1543,12 +1785,12 @@
|
|
|
1543
1785
|
'position': 'relative', 'display': 'flex', 'align-items': 'center', 'width': '100%',
|
|
1544
1786
|
'padding': '1rem 1.25rem', 'font-size': '1rem', 'font-weight': '500', 'text-align': 'left',
|
|
1545
1787
|
'background-color': 'transparent', 'border': '0', 'overflow-anchor': 'none', 'cursor': 'pointer',
|
|
1546
|
-
'font-family': 'inherit', 'transition': 'color 0.15s ease-
|
|
1788
|
+
'font-family': 'inherit', 'transition': 'color 0.15s ease-out, background-color 0.15s ease-out'
|
|
1547
1789
|
};
|
|
1548
1790
|
rules['.bw-accordion-button::after'] = {
|
|
1549
1791
|
'flex-shrink': '0', 'width': '1.25rem', 'height': '1.25rem', 'margin-left': 'auto',
|
|
1550
1792
|
'content': '""', 'background-repeat': 'no-repeat', 'background-size': '1.25rem',
|
|
1551
|
-
'transition': 'transform 0.2s ease-
|
|
1793
|
+
'transition': 'transform 0.2s ease-out'
|
|
1552
1794
|
};
|
|
1553
1795
|
rules['.bw-accordion-button:not(.bw-collapsed)::after'] = { 'transform': 'rotate(-180deg)' };
|
|
1554
1796
|
rules['.bw-accordion-collapse'] = { 'max-height': '0', 'overflow': 'hidden', 'transition': 'max-height 0.3s ease' };
|
|
@@ -1557,10 +1799,13 @@
|
|
|
1557
1799
|
|
|
1558
1800
|
// Modal (structural)
|
|
1559
1801
|
rules['.bw-modal'] = {
|
|
1560
|
-
'display': '
|
|
1561
|
-
'
|
|
1802
|
+
'display': 'flex', 'align-items': 'center', 'justify-content': 'center',
|
|
1803
|
+
'position': 'fixed', 'top': '0', 'left': '0', 'width': '100%', 'height': '100%',
|
|
1804
|
+
'z-index': '1050', 'overflow-x': 'hidden', 'overflow-y': 'auto',
|
|
1805
|
+
'opacity': '0', 'visibility': 'hidden', 'pointer-events': 'none',
|
|
1806
|
+
'transition': 'opacity 0.2s ease, visibility 0.2s ease'
|
|
1562
1807
|
};
|
|
1563
|
-
rules['.bw-modal.bw-modal-show'] = { '
|
|
1808
|
+
rules['.bw-modal.bw-modal-show'] = { 'opacity': '1', 'visibility': 'visible', 'pointer-events': 'auto' };
|
|
1564
1809
|
rules['.bw-modal-dialog'] = {
|
|
1565
1810
|
'position': 'relative', 'width': '100%', 'max-width': '500px', 'margin': '1.75rem auto',
|
|
1566
1811
|
'pointer-events': 'none', 'transform': 'translateY(-20px)', 'transition': 'transform 0.2s ease-out'
|
|
@@ -1580,7 +1825,7 @@
|
|
|
1580
1825
|
|
|
1581
1826
|
// Carousel (structural)
|
|
1582
1827
|
rules['.bw-carousel'] = { 'position': 'relative', 'overflow': 'hidden', 'border-radius': '8px' };
|
|
1583
|
-
rules['.bw-carousel-track'] = { 'display': 'flex', 'transition': 'transform 0.
|
|
1828
|
+
rules['.bw-carousel-track'] = { 'display': 'flex', 'transition': 'transform 0.3s ease-out', 'height': '100%' };
|
|
1584
1829
|
rules['.bw-carousel-slide'] = { 'min-width': '100%', 'flex-shrink': '0', 'overflow': 'hidden', 'position': 'relative', 'display': 'flex', 'align-items': 'center', 'justify-content': 'center' };
|
|
1585
1830
|
rules['.bw-carousel-slide img'] = { 'width': '100%', 'height': '100%', 'object-fit': 'cover' };
|
|
1586
1831
|
rules['.bw-carousel-caption'] = { 'position': 'absolute', 'bottom': '0', 'left': '0', 'right': '0', 'padding': '0.75rem 1rem' };
|
|
@@ -1624,11 +1869,14 @@
|
|
|
1624
1869
|
'border-bottom': '0', 'border-left': '0.3em solid transparent'
|
|
1625
1870
|
};
|
|
1626
1871
|
rules['.bw-dropdown-menu'] = {
|
|
1627
|
-
'position': 'absolute', 'top': '100%', 'left': '0', 'z-index': '1000', 'display': '
|
|
1872
|
+
'position': 'absolute', 'top': '100%', 'left': '0', 'z-index': '1000', 'display': 'block',
|
|
1628
1873
|
'min-width': '10rem', 'padding': '0.5rem 0', 'margin': '0.125rem 0 0',
|
|
1629
|
-
'background-clip': 'padding-box', 'border-radius': '6px'
|
|
1874
|
+
'background-clip': 'padding-box', 'border-radius': '6px',
|
|
1875
|
+
'opacity': '0', 'visibility': 'hidden', 'transform': 'translateY(-4px)',
|
|
1876
|
+
'pointer-events': 'none',
|
|
1877
|
+
'transition': 'opacity 0.15s ease, transform 0.15s ease, visibility 0.15s ease'
|
|
1630
1878
|
};
|
|
1631
|
-
rules['.bw-dropdown-menu.bw-dropdown-show'] = { '
|
|
1879
|
+
rules['.bw-dropdown-menu.bw-dropdown-show'] = { 'opacity': '1', 'visibility': 'visible', 'transform': 'translateY(0)', 'pointer-events': 'auto' };
|
|
1632
1880
|
rules['.bw-dropdown-menu-end'] = { 'left': 'auto', 'right': '0' };
|
|
1633
1881
|
rules['.bw-dropdown-item'] = {
|
|
1634
1882
|
'display': 'block', 'width': '100%', 'padding': '0.375rem 1rem', 'clear': 'both',
|
|
@@ -1636,6 +1884,7 @@
|
|
|
1636
1884
|
'background-color': 'transparent', 'border': '0', 'font-size': '0.9375rem',
|
|
1637
1885
|
'transition': 'background-color 0.15s, color 0.15s'
|
|
1638
1886
|
};
|
|
1887
|
+
rules['.bw-dropdown-item:focus-visible'] = { 'outline': '2px solid currentColor', 'outline-offset': '-2px' };
|
|
1639
1888
|
rules['.bw-dropdown-divider'] = { 'height': '0', 'margin': '0.5rem 0', 'overflow': 'hidden', 'opacity': '1' };
|
|
1640
1889
|
|
|
1641
1890
|
// Switch (structural)
|
|
@@ -1643,7 +1892,7 @@
|
|
|
1643
1892
|
rules['.bw-form-switch .bw-switch-input'] = {
|
|
1644
1893
|
'width': '2em', 'height': '1.125em', 'margin-left': '-2.5em', 'border-radius': '2em',
|
|
1645
1894
|
'appearance': 'none', 'background-position': 'left center', 'background-repeat': 'no-repeat',
|
|
1646
|
-
'background-size': 'contain', 'transition': 'background-position 0.15s ease-
|
|
1895
|
+
'background-size': 'contain', 'transition': 'background-position 0.15s ease-out, background-color 0.15s ease-out',
|
|
1647
1896
|
'cursor': 'pointer'
|
|
1648
1897
|
};
|
|
1649
1898
|
rules['.bw-form-switch .bw-switch-input:checked'] = { 'background-position': 'right center' };
|
|
@@ -1668,6 +1917,123 @@
|
|
|
1668
1917
|
rules['.bw-avatar-lg'] = { 'width': '4rem', 'height': '4rem', 'font-size': '1.25rem' };
|
|
1669
1918
|
rules['.bw-avatar-xl'] = { 'width': '5rem', 'height': '5rem', 'font-size': '1.5rem' };
|
|
1670
1919
|
|
|
1920
|
+
// Stat card (structural)
|
|
1921
|
+
rules['.bw-stat-card'] = {
|
|
1922
|
+
'border-radius': '8px', 'padding': '1.25rem',
|
|
1923
|
+
'border-left': '4px solid transparent',
|
|
1924
|
+
'transition': 'box-shadow 0.15s ease-out, transform 0.15s ease-out'
|
|
1925
|
+
};
|
|
1926
|
+
rules['.bw-stat-card:hover'] = { 'transform': 'translateY(-1px)' };
|
|
1927
|
+
rules['.bw-stat-icon'] = { 'font-size': '1.5rem', 'margin-bottom': '0.5rem' };
|
|
1928
|
+
rules['.bw-stat-value'] = { 'font-size': '2rem', 'font-weight': '700', 'line-height': '1.2' };
|
|
1929
|
+
rules['.bw-stat-label'] = { 'font-size': '0.875rem', 'margin-top': '0.25rem' };
|
|
1930
|
+
rules['.bw-stat-change'] = { 'font-size': '0.875rem', 'font-weight': '500', 'margin-top': '0.5rem' };
|
|
1931
|
+
|
|
1932
|
+
// Tooltip (structural)
|
|
1933
|
+
rules['.bw-tooltip-wrapper'] = { 'position': 'relative', 'display': 'inline-block' };
|
|
1934
|
+
rules['.bw-tooltip'] = {
|
|
1935
|
+
'position': 'absolute', 'z-index': '999',
|
|
1936
|
+
'padding': '0.375rem 0.75rem', 'border-radius': '4px', 'font-size': '0.875rem',
|
|
1937
|
+
'white-space': 'nowrap', 'pointer-events': 'none',
|
|
1938
|
+
'opacity': '0', 'visibility': 'hidden',
|
|
1939
|
+
'transition': 'opacity 0.15s ease, visibility 0.15s ease, transform 0.15s ease'
|
|
1940
|
+
};
|
|
1941
|
+
rules['.bw-tooltip.bw-tooltip-show'] = { 'opacity': '1', 'visibility': 'visible' };
|
|
1942
|
+
rules['.bw-tooltip-top'] = { 'bottom': '100%', 'left': '50%', 'transform': 'translateX(-50%) translateY(-4px)', 'margin-bottom': '4px' };
|
|
1943
|
+
rules['.bw-tooltip-top.bw-tooltip-show'] = { 'transform': 'translateX(-50%) translateY(0)' };
|
|
1944
|
+
rules['.bw-tooltip-bottom'] = { 'top': '100%', 'left': '50%', 'transform': 'translateX(-50%) translateY(4px)', 'margin-top': '4px' };
|
|
1945
|
+
rules['.bw-tooltip-bottom.bw-tooltip-show'] = { 'transform': 'translateX(-50%) translateY(0)' };
|
|
1946
|
+
rules['.bw-tooltip-left'] = { 'right': '100%', 'top': '50%', 'transform': 'translateY(-50%) translateX(-4px)', 'margin-right': '4px' };
|
|
1947
|
+
rules['.bw-tooltip-left.bw-tooltip-show'] = { 'transform': 'translateY(-50%) translateX(0)' };
|
|
1948
|
+
rules['.bw-tooltip-right'] = { 'left': '100%', 'top': '50%', 'transform': 'translateY(-50%) translateX(4px)', 'margin-left': '4px' };
|
|
1949
|
+
rules['.bw-tooltip-right.bw-tooltip-show'] = { 'transform': 'translateY(-50%) translateX(0)' };
|
|
1950
|
+
|
|
1951
|
+
// Search input (structural)
|
|
1952
|
+
rules['.bw-search-input'] = { 'position': 'relative', 'display': 'flex', 'align-items': 'center' };
|
|
1953
|
+
rules['.bw-search-input .bw-search-field'] = { 'padding-right': '2.5rem' };
|
|
1954
|
+
rules['.bw-search-clear'] = {
|
|
1955
|
+
'position': 'absolute', 'right': '0.5rem',
|
|
1956
|
+
'display': 'flex', 'align-items': 'center', 'justify-content': 'center',
|
|
1957
|
+
'width': '1.5rem', 'height': '1.5rem',
|
|
1958
|
+
'border': 'none', 'background': 'none',
|
|
1959
|
+
'font-size': '1.25rem', 'cursor': 'pointer', 'padding': '0',
|
|
1960
|
+
'border-radius': '50%', 'transition': 'color 0.15s ease-out'
|
|
1961
|
+
};
|
|
1962
|
+
|
|
1963
|
+
// Range slider (structural)
|
|
1964
|
+
rules['.bw-range-wrapper'] = { 'margin-bottom': '1rem' };
|
|
1965
|
+
rules['.bw-range-label'] = { 'display': 'flex', 'justify-content': 'space-between', 'align-items': 'center', 'margin-bottom': '0.5rem', 'font-size': '0.875rem', 'font-weight': '500' };
|
|
1966
|
+
rules['.bw-range-value'] = { 'font-weight': '600' };
|
|
1967
|
+
rules['.bw-range'] = { 'width': '100%', 'height': '0.5rem', 'padding': '0', 'appearance': 'none', 'border': 'none', 'border-radius': '0.25rem', 'cursor': 'pointer', 'outline': 'none' };
|
|
1968
|
+
rules['.bw-range:disabled'] = { 'opacity': '0.5', 'cursor': 'not-allowed' };
|
|
1969
|
+
|
|
1970
|
+
// Media object (structural)
|
|
1971
|
+
rules['.bw-media'] = { 'display': 'flex', 'align-items': 'flex-start', 'gap': '1rem' };
|
|
1972
|
+
rules['.bw-media-reverse'] = { 'flex-direction': 'row-reverse' };
|
|
1973
|
+
rules['.bw-media-img'] = { 'border-radius': '50%', 'object-fit': 'cover', 'flex-shrink': '0' };
|
|
1974
|
+
rules['.bw-media-body'] = { 'flex': '1', 'min-width': '0' };
|
|
1975
|
+
rules['.bw-media-title'] = { 'margin': '0 0 0.25rem 0', 'font-size': '1rem', 'font-weight': '600', 'line-height': '1.3' };
|
|
1976
|
+
|
|
1977
|
+
// File upload (structural)
|
|
1978
|
+
rules['.bw-file-upload'] = {
|
|
1979
|
+
'display': 'flex', 'flex-direction': 'column', 'align-items': 'center', 'justify-content': 'center',
|
|
1980
|
+
'padding': '2rem', 'border': '2px dashed transparent', 'border-radius': '8px',
|
|
1981
|
+
'cursor': 'pointer', 'text-align': 'center', 'position': 'relative',
|
|
1982
|
+
'transition': 'border-color 0.15s ease-out, background-color 0.15s ease-out'
|
|
1983
|
+
};
|
|
1984
|
+
rules['.bw-file-upload-icon'] = { 'font-size': '2rem', 'margin-bottom': '0.5rem' };
|
|
1985
|
+
rules['.bw-file-upload-text'] = { 'font-size': '0.875rem' };
|
|
1986
|
+
rules['.bw-file-upload-input'] = {
|
|
1987
|
+
'position': 'absolute', 'width': '1px', 'height': '1px', 'padding': '0',
|
|
1988
|
+
'margin': '-1px', 'overflow': 'hidden', 'clip': 'rect(0,0,0,0)', 'border': '0'
|
|
1989
|
+
};
|
|
1990
|
+
|
|
1991
|
+
// Timeline (structural)
|
|
1992
|
+
rules['.bw-timeline'] = { 'position': 'relative', 'padding-left': '2rem' };
|
|
1993
|
+
rules['.bw-timeline-item'] = { 'position': 'relative', 'padding-bottom': '1.5rem' };
|
|
1994
|
+
rules['.bw-timeline-item:last-child'] = { 'padding-bottom': '0' };
|
|
1995
|
+
rules['.bw-timeline-marker'] = { 'position': 'absolute', 'left': '-1.75rem', 'top': '0.25rem', 'width': '0.75rem', 'height': '0.75rem', 'border-radius': '50%' };
|
|
1996
|
+
rules['.bw-timeline-content'] = { 'padding-left': '0.5rem' };
|
|
1997
|
+
rules['.bw-timeline-date'] = { 'font-size': '0.75rem', 'margin-bottom': '0.25rem', 'font-weight': '500' };
|
|
1998
|
+
rules['.bw-timeline-title'] = { 'font-size': '1rem', 'font-weight': '600', 'margin': '0 0 0.25rem 0', 'line-height': '1.3' };
|
|
1999
|
+
rules['.bw-timeline-text'] = { 'font-size': '0.875rem', 'margin': '0', 'line-height': '1.5' };
|
|
2000
|
+
|
|
2001
|
+
// Stepper (structural)
|
|
2002
|
+
rules['.bw-stepper'] = { 'display': 'flex', 'gap': '0' };
|
|
2003
|
+
rules['.bw-step'] = { 'flex': '1', 'display': 'flex', 'flex-direction': 'column', 'align-items': 'center', 'text-align': 'center', 'position': 'relative' };
|
|
2004
|
+
rules['.bw-step-indicator'] = { 'width': '2rem', 'height': '2rem', 'border-radius': '50%', 'display': 'flex', 'align-items': 'center', 'justify-content': 'center', 'font-size': '0.875rem', 'font-weight': '600', 'position': 'relative', 'z-index': '1', 'transition': 'background-color 0.2s ease-out, color 0.2s ease-out' };
|
|
2005
|
+
rules['.bw-step-body'] = { 'margin-top': '0.5rem' };
|
|
2006
|
+
rules['.bw-step-label'] = { 'font-size': '0.875rem', 'font-weight': '500' };
|
|
2007
|
+
rules['.bw-step-description'] = { 'font-size': '0.75rem', 'margin-top': '0.125rem' };
|
|
2008
|
+
|
|
2009
|
+
// Chip input (structural)
|
|
2010
|
+
rules['.bw-chip-input'] = { 'display': 'flex', 'flex-wrap': 'wrap', 'align-items': 'center', 'gap': '0.375rem', 'padding': '0.375rem 0.5rem', 'border-radius': '6px', 'min-height': '2.5rem', 'cursor': 'text', 'transition': 'border-color 0.15s ease-out, box-shadow 0.15s ease-out' };
|
|
2011
|
+
rules['.bw-chip'] = { 'display': 'inline-flex', 'align-items': 'center', 'gap': '0.25rem', 'padding': '0.125rem 0.5rem', 'border-radius': '1rem', 'font-size': '0.8125rem', 'line-height': '1.5', 'white-space': 'nowrap' };
|
|
2012
|
+
rules['.bw-chip-remove'] = { 'display': 'inline-flex', 'align-items': 'center', 'justify-content': 'center', 'width': '1rem', 'height': '1rem', 'border': 'none', 'background': 'none', 'font-size': '0.875rem', 'cursor': 'pointer', 'padding': '0', 'border-radius': '50%', 'transition': 'color 0.15s ease-out, background-color 0.15s ease-out' };
|
|
2013
|
+
rules['.bw-chip-field'] = { 'flex': '1', 'min-width': '80px', 'border': 'none', 'outline': 'none', 'font-size': '0.875rem', 'padding': '0.125rem 0', 'background': 'transparent' };
|
|
2014
|
+
|
|
2015
|
+
// Popover (structural)
|
|
2016
|
+
rules['.bw-popover-wrapper'] = { 'position': 'relative', 'display': 'inline-block' };
|
|
2017
|
+
rules['.bw-popover-trigger'] = { 'cursor': 'pointer' };
|
|
2018
|
+
rules['.bw-popover'] = {
|
|
2019
|
+
'position': 'absolute', 'z-index': '1000',
|
|
2020
|
+
'min-width': '200px', 'max-width': '320px',
|
|
2021
|
+
'border-radius': '8px',
|
|
2022
|
+
'pointer-events': 'none', 'opacity': '0', 'visibility': 'hidden',
|
|
2023
|
+
'transition': 'opacity 0.15s ease, visibility 0.15s ease, transform 0.15s ease'
|
|
2024
|
+
};
|
|
2025
|
+
rules['.bw-popover.bw-popover-show'] = { 'opacity': '1', 'visibility': 'visible', 'pointer-events': 'auto' };
|
|
2026
|
+
rules['.bw-popover-header'] = { 'padding': '0.625rem 0.875rem', 'font-weight': '600', 'font-size': '0.9375rem' };
|
|
2027
|
+
rules['.bw-popover-body'] = { 'padding': '0.75rem 0.875rem', 'font-size': '0.875rem', 'line-height': '1.5' };
|
|
2028
|
+
rules['.bw-popover-top'] = { 'bottom': '100%', 'left': '50%', 'transform': 'translateX(-50%) translateY(-8px)', 'margin-bottom': '8px' };
|
|
2029
|
+
rules['.bw-popover-top.bw-popover-show'] = { 'transform': 'translateX(-50%) translateY(0)' };
|
|
2030
|
+
rules['.bw-popover-bottom'] = { 'top': '100%', 'left': '50%', 'transform': 'translateX(-50%) translateY(8px)', 'margin-top': '8px' };
|
|
2031
|
+
rules['.bw-popover-bottom.bw-popover-show'] = { 'transform': 'translateX(-50%) translateY(0)' };
|
|
2032
|
+
rules['.bw-popover-left'] = { 'right': '100%', 'top': '50%', 'transform': 'translateY(-50%) translateX(-8px)', 'margin-right': '8px' };
|
|
2033
|
+
rules['.bw-popover-left.bw-popover-show'] = { 'transform': 'translateY(-50%) translateX(0)' };
|
|
2034
|
+
rules['.bw-popover-right'] = { 'left': '100%', 'top': '50%', 'transform': 'translateY(-50%) translateX(8px)', 'margin-left': '8px' };
|
|
2035
|
+
rules['.bw-popover-right.bw-popover-show'] = { 'transform': 'translateY(-50%) translateX(0)' };
|
|
2036
|
+
|
|
1671
2037
|
// Bar chart (structural)
|
|
1672
2038
|
rules['.bw-bar-chart-container'] = {
|
|
1673
2039
|
'padding': '1rem', 'border': '1px solid transparent', 'border-radius': '8px'
|
|
@@ -1681,7 +2047,7 @@
|
|
|
1681
2047
|
};
|
|
1682
2048
|
rules['.bw-bar'] = {
|
|
1683
2049
|
'width': '100%', 'border-radius': '3px 3px 0 0',
|
|
1684
|
-
'transition': 'height 0.
|
|
2050
|
+
'transition': 'height 0.3s ease-out', 'min-height': '4px'
|
|
1685
2051
|
};
|
|
1686
2052
|
rules['.bw-bar:hover'] = { 'opacity': '0.85' };
|
|
1687
2053
|
rules['.bw-bar-value'] = {
|
|
@@ -1810,6 +2176,16 @@
|
|
|
1810
2176
|
// Responsive grid
|
|
1811
2177
|
Object.assign(rules, defaultStyles.responsive);
|
|
1812
2178
|
|
|
2179
|
+
// Accessibility: reduce motion for users who prefer it
|
|
2180
|
+
rules['@media (prefers-reduced-motion: reduce)'] = {
|
|
2181
|
+
'*, *::before, *::after': {
|
|
2182
|
+
'animation-duration': '0.01ms !important',
|
|
2183
|
+
'animation-iteration-count': '1 !important',
|
|
2184
|
+
'transition-duration': '0.01ms !important',
|
|
2185
|
+
'scroll-behavior': 'auto !important'
|
|
2186
|
+
}
|
|
2187
|
+
};
|
|
2188
|
+
|
|
1813
2189
|
return addUnderscoreAliases(rules);
|
|
1814
2190
|
}
|
|
1815
2191
|
|
|
@@ -1818,9 +2194,25 @@
|
|
|
1818
2194
|
// =========================================================================
|
|
1819
2195
|
|
|
1820
2196
|
/**
|
|
1821
|
-
* Add underscore aliases for all bw
|
|
2197
|
+
* Add underscore aliases for all `.bw-` selectors.
|
|
2198
|
+
*
|
|
2199
|
+
* CSS CLASS NAMING CONVENTION:
|
|
2200
|
+
*
|
|
2201
|
+
* Canonical form: `.bw-btn`, `.bw-card`, `.bw-table-hover` (hyphens)
|
|
2202
|
+
* Underscore alias: `.bw_btn`, `.bw_card`, `.bw_table_hover` (underscores)
|
|
2203
|
+
*
|
|
2204
|
+
* Both forms are valid in HTML and produce identical results. The hyphen
|
|
2205
|
+
* form is canonical (used in docs, generated CSS, component output).
|
|
2206
|
+
* Underscore aliases exist because:
|
|
2207
|
+
* 1. TACO attribute keys use underscores (`bw_id`, `bw_meta`) — no
|
|
2208
|
+
* quoting needed in JS object literals
|
|
2209
|
+
* 2. Some users prefer underscores for consistency with JS identifiers
|
|
2210
|
+
*
|
|
2211
|
+
* Use `bw.normalizeClass()` to convert underscore classes to canonical
|
|
2212
|
+
* hyphen form at runtime if needed.
|
|
2213
|
+
*
|
|
1822
2214
|
* @param {Object} rules - CSS rules object
|
|
1823
|
-
* @returns {Object}
|
|
2215
|
+
* @returns {Object} Rules with underscore aliases added (both forms work)
|
|
1824
2216
|
*/
|
|
1825
2217
|
function addUnderscoreAliases(rules) {
|
|
1826
2218
|
const result = {};
|
|
@@ -1837,6 +2229,27 @@
|
|
|
1837
2229
|
// =========================================================================
|
|
1838
2230
|
// Theme tokens (backwards compatible)
|
|
1839
2231
|
// =========================================================================
|
|
2232
|
+
//
|
|
2233
|
+
// DESIGN NOTE — Why no CSS custom properties (CSS variables)?
|
|
2234
|
+
//
|
|
2235
|
+
// Bitwrench targets IE11 as Tier 1 (see dev/bw2x-compatibility.md).
|
|
2236
|
+
// CSS custom properties (var(--color-primary)) are not supported in IE11.
|
|
2237
|
+
//
|
|
2238
|
+
// Instead, bitwrench uses class-scoped CSS generation:
|
|
2239
|
+
// 1. `defaultStyles.*` provides hardcoded cosmetic defaults
|
|
2240
|
+
// 2. `generateTheme(name, config)` generates a complete set of
|
|
2241
|
+
// class-scoped CSS rules from 3 seed colors (primary, secondary,
|
|
2242
|
+
// tertiary) — all components are restyled with the new palette
|
|
2243
|
+
// 3. `generateAlternateCSS()` produces the alternate (dark/light)
|
|
2244
|
+
// variant scoped under `.bw-theme-alt`
|
|
2245
|
+
//
|
|
2246
|
+
// This achieves full theme customization without CSS variables:
|
|
2247
|
+
// bw.generateTheme('ocean', { primary: '#006666', secondary: '#cc6633' })
|
|
2248
|
+
// → generates .ocean .bw-btn-primary { background: #006666; } etc.
|
|
2249
|
+
//
|
|
2250
|
+
// When IE11 support is dropped, CSS custom properties can be added as
|
|
2251
|
+
// an optimization (one rule with var() instead of many scoped rules).
|
|
2252
|
+
// The generateTheme() API stays the same — only the output format changes.
|
|
1840
2253
|
|
|
1841
2254
|
let theme = {
|
|
1842
2255
|
colors: {
|
|
@@ -1844,8 +2257,8 @@
|
|
|
1844
2257
|
secondary: '#6c757d',
|
|
1845
2258
|
success: '#198754',
|
|
1846
2259
|
danger: '#dc3545',
|
|
1847
|
-
warning: '#
|
|
1848
|
-
info: '#
|
|
2260
|
+
warning: '#b38600',
|
|
2261
|
+
info: '#0891b2',
|
|
1849
2262
|
light: '#f8f9fa',
|
|
1850
2263
|
dark: '#212529',
|
|
1851
2264
|
white: '#fff',
|
|
@@ -1881,214 +2294,63 @@
|
|
|
1881
2294
|
'5xl': '3rem'
|
|
1882
2295
|
}
|
|
1883
2296
|
},
|
|
1884
|
-
darkMode: false
|
|
1885
2297
|
};
|
|
1886
2298
|
|
|
1887
2299
|
/**
|
|
1888
|
-
* Generate
|
|
1889
|
-
*
|
|
2300
|
+
* Generate alternate-palette CSS scoped under `.bw-theme-alt`.
|
|
2301
|
+
* Uses the same `generateThemedCSS()` pipeline as the primary palette —
|
|
2302
|
+
* both sides go through identical code paths.
|
|
1890
2303
|
*
|
|
1891
|
-
* @param {
|
|
1892
|
-
* @
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
//
|
|
1898
|
-
var
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
var
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
}
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
'
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
},
|
|
1930
|
-
'.bw-dark .bw-navbar': {
|
|
1931
|
-
'background-color': surfaceBg,
|
|
1932
|
-
'border-bottom-color': borderColor
|
|
1933
|
-
},
|
|
1934
|
-
'.bw-dark .bw-navbar-brand': {
|
|
1935
|
-
'color': textColor
|
|
1936
|
-
},
|
|
1937
|
-
'.bw-dark .bw-navbar-nav .bw-nav-link': {
|
|
1938
|
-
'color': adjustLightness(textColor, -15)
|
|
1939
|
-
},
|
|
1940
|
-
'.bw-dark .bw-navbar-nav .bw-nav-link:hover': {
|
|
1941
|
-
'color': textColor
|
|
1942
|
-
},
|
|
1943
|
-
'.bw-dark .bw-form-control': {
|
|
1944
|
-
'background-color': surfaceBg,
|
|
1945
|
-
'border-color': borderColor,
|
|
1946
|
-
'color': textColor
|
|
1947
|
-
},
|
|
1948
|
-
'.bw-dark .bw-form-label': {
|
|
1949
|
-
'color': textColor
|
|
1950
|
-
},
|
|
1951
|
-
'.bw-dark .bw-form-text': {
|
|
1952
|
-
'color': adjustLightness(textColor, -20)
|
|
1953
|
-
},
|
|
1954
|
-
'.bw-dark .bw-table': {
|
|
1955
|
-
'color': textColor
|
|
1956
|
-
},
|
|
1957
|
-
'.bw-dark .bw-table > :not(caption) > * > *': {
|
|
1958
|
-
'border-bottom-color': borderColor
|
|
1959
|
-
},
|
|
1960
|
-
'.bw-dark .bw-table > thead > tr > *': {
|
|
1961
|
-
'background-color': bodyBg,
|
|
1962
|
-
'color': adjustLightness(textColor, -10),
|
|
1963
|
-
'border-bottom-color': borderColor
|
|
1964
|
-
},
|
|
1965
|
-
'.bw-dark .bw-table-striped > tbody > tr:nth-of-type(odd) > *': {
|
|
1966
|
-
'background-color': 'rgba(255, 255, 255, 0.05)'
|
|
1967
|
-
},
|
|
1968
|
-
'.bw-dark .bw-alert': {
|
|
1969
|
-
'border-color': borderColor
|
|
1970
|
-
},
|
|
1971
|
-
'.bw-dark .bw-list-group-item': {
|
|
1972
|
-
'background-color': surfaceBg,
|
|
1973
|
-
'border-color': borderColor,
|
|
1974
|
-
'color': textColor
|
|
1975
|
-
},
|
|
1976
|
-
'.bw-dark .bw-badge': {
|
|
1977
|
-
'color': textColor
|
|
1978
|
-
},
|
|
1979
|
-
'.bw-dark .bw-nav-tabs': {
|
|
1980
|
-
'border-bottom-color': borderColor
|
|
1981
|
-
},
|
|
1982
|
-
'.bw-dark .bw-nav-link': {
|
|
1983
|
-
'color': adjustLightness(textColor, -15)
|
|
1984
|
-
},
|
|
1985
|
-
'.bw-dark .bw-nav-tabs .bw-nav-link:hover': {
|
|
1986
|
-
'color': textColor,
|
|
1987
|
-
'border-bottom-color': borderColor
|
|
1988
|
-
},
|
|
1989
|
-
'.bw-dark .bw-pagination .bw-page-link': {
|
|
1990
|
-
'background-color': surfaceBg,
|
|
1991
|
-
'border-color': borderColor,
|
|
1992
|
-
'color': textColor
|
|
1993
|
-
},
|
|
1994
|
-
'.bw-dark .bw-breadcrumb-item + .bw-breadcrumb-item::before': {
|
|
1995
|
-
'color': adjustLightness(textColor, -20)
|
|
1996
|
-
},
|
|
1997
|
-
'.bw-dark .bw-breadcrumb-item.active': {
|
|
1998
|
-
'color': adjustLightness(textColor, -10)
|
|
1999
|
-
},
|
|
2000
|
-
'.bw-dark .bw-hero-light': {
|
|
2001
|
-
'background': surfaceBg,
|
|
2002
|
-
'color': textColor
|
|
2003
|
-
},
|
|
2004
|
-
'.bw-dark .bw-progress': {
|
|
2005
|
-
'background-color': surfaceBg
|
|
2006
|
-
},
|
|
2007
|
-
'.bw-dark .bw-section-subtitle': {
|
|
2008
|
-
'color': adjustLightness(textColor, -15)
|
|
2009
|
-
},
|
|
2010
|
-
'.bw-dark .bw-close': {
|
|
2011
|
-
'color': textColor
|
|
2012
|
-
},
|
|
2013
|
-
'.bw-dark .bw-accordion-item': {
|
|
2014
|
-
'background-color': surfaceBg,
|
|
2015
|
-
'border-color': borderColor
|
|
2016
|
-
},
|
|
2017
|
-
'.bw-dark .bw-accordion-button': {
|
|
2018
|
-
'color': textColor
|
|
2019
|
-
},
|
|
2020
|
-
'.bw-dark .bw-accordion-button:not(.bw-collapsed)': {
|
|
2021
|
-
'color': '#7dd3e0',
|
|
2022
|
-
'background-color': 'rgba(125, 211, 224, 0.1)'
|
|
2023
|
-
},
|
|
2024
|
-
'.bw-dark .bw-accordion-button:hover': {
|
|
2025
|
-
'background-color': bodyBg
|
|
2026
|
-
},
|
|
2027
|
-
'.bw-dark .bw-accordion-button:not(.bw-collapsed):hover': {
|
|
2028
|
-
'background-color': 'rgba(125, 211, 224, 0.15)'
|
|
2029
|
-
},
|
|
2030
|
-
'.bw-dark .bw-accordion-button:focus-visible': {
|
|
2031
|
-
'box-shadow': '0 0 0 0.2rem rgba(125, 211, 224, 0.3)'
|
|
2032
|
-
},
|
|
2033
|
-
'.bw-dark .bw-accordion-body': {
|
|
2034
|
-
'border-top-color': borderColor
|
|
2035
|
-
},
|
|
2036
|
-
'.bw-dark .bw-carousel': {
|
|
2037
|
-
'background-color': bodyBg
|
|
2038
|
-
},
|
|
2039
|
-
'.bw-dark .bw-carousel-control': {
|
|
2040
|
-
'background-color': 'rgba(255,255,255,0.15)'
|
|
2041
|
-
},
|
|
2042
|
-
'.bw-dark .bw-carousel-control:hover': {
|
|
2043
|
-
'background-color': 'rgba(255,255,255,0.25)'
|
|
2044
|
-
},
|
|
2045
|
-
'.bw-dark .bw-modal-content': {
|
|
2046
|
-
'background-color': surfaceBg,
|
|
2047
|
-
'border-color': borderColor
|
|
2048
|
-
},
|
|
2049
|
-
'.bw-dark .bw-modal-header': {
|
|
2050
|
-
'border-bottom-color': borderColor
|
|
2051
|
-
},
|
|
2052
|
-
'.bw-dark .bw-modal-footer': {
|
|
2053
|
-
'border-top-color': borderColor
|
|
2054
|
-
},
|
|
2055
|
-
'.bw-dark .bw-modal-title': {
|
|
2056
|
-
'color': textColor
|
|
2057
|
-
},
|
|
2058
|
-
'.bw-dark .bw-toast': {
|
|
2059
|
-
'background-color': surfaceBg,
|
|
2060
|
-
'border-color': borderColor
|
|
2061
|
-
},
|
|
2062
|
-
'.bw-dark .bw-toast-header': {
|
|
2063
|
-
'border-bottom-color': borderColor,
|
|
2064
|
-
'color': textColor
|
|
2065
|
-
},
|
|
2066
|
-
'.bw-dark .bw-dropdown-menu': {
|
|
2067
|
-
'background-color': surfaceBg,
|
|
2068
|
-
'border-color': borderColor
|
|
2069
|
-
},
|
|
2070
|
-
'.bw-dark .bw-dropdown-item': {
|
|
2071
|
-
'color': textColor
|
|
2072
|
-
},
|
|
2073
|
-
'.bw-dark .bw-dropdown-item:hover': {
|
|
2074
|
-
'background-color': bodyBg
|
|
2075
|
-
},
|
|
2076
|
-
'.bw-dark .bw-dropdown-divider': {
|
|
2077
|
-
'border-top-color': borderColor
|
|
2078
|
-
},
|
|
2079
|
-
'.bw-dark .bw-skeleton': {
|
|
2080
|
-
'background': 'linear-gradient(90deg, ' + borderColor + ' 25%, ' + surfaceBg + ' 37%, ' + borderColor + ' 63%)'
|
|
2081
|
-
},
|
|
2082
|
-
'.bw-dark h1, .bw-dark h2, .bw-dark h3, .bw-dark h4, .bw-dark h5, .bw-dark h6': {
|
|
2083
|
-
'color': textColor
|
|
2084
|
-
},
|
|
2085
|
-
'@media (prefers-color-scheme: dark)': {
|
|
2086
|
-
':root.bw-auto-dark body': {
|
|
2087
|
-
'color': textColor,
|
|
2088
|
-
'background-color': bodyBg
|
|
2304
|
+
* @param {string} name - Theme scope name (e.g. 'ocean'). '' for global.
|
|
2305
|
+
* @param {Object} altPalette - From derivePalette(deriveAlternateConfig(...))
|
|
2306
|
+
* @param {Object} layout - From resolveLayout()
|
|
2307
|
+
* @returns {Object} CSS rules object scoped under .bw-theme-alt (+ optional .name)
|
|
2308
|
+
*/
|
|
2309
|
+
function generateAlternateCSS(name, altPalette, layout) {
|
|
2310
|
+
// Generate themed CSS using the same pipeline as primary
|
|
2311
|
+
var rawRules = generateThemedCSS('', altPalette, layout);
|
|
2312
|
+
|
|
2313
|
+
// Re-scope every selector under .bw-theme-alt (+ optional theme name)
|
|
2314
|
+
var altPrefix = name ? '.' + name + '.bw-theme-alt' : '.bw-theme-alt';
|
|
2315
|
+
var altRules = {};
|
|
2316
|
+
|
|
2317
|
+
for (var sel in rawRules) {
|
|
2318
|
+
if (!rawRules.hasOwnProperty(sel)) continue;
|
|
2319
|
+
|
|
2320
|
+
if (sel.charAt(0) === '@') {
|
|
2321
|
+
// @media / @keyframes — recurse into the block
|
|
2322
|
+
var innerBlock = rawRules[sel];
|
|
2323
|
+
var altInner = {};
|
|
2324
|
+
for (var innerSel in innerBlock) {
|
|
2325
|
+
if (!innerBlock.hasOwnProperty(innerSel)) continue;
|
|
2326
|
+
altInner[altPrefix + ' ' + innerSel] = innerBlock[innerSel];
|
|
2327
|
+
}
|
|
2328
|
+
altRules[sel] = altInner;
|
|
2329
|
+
} else {
|
|
2330
|
+
// Regular selector — prefix with alt scope
|
|
2331
|
+
// Handle comma-separated selectors
|
|
2332
|
+
var parts = sel.split(',');
|
|
2333
|
+
var scopedParts = [];
|
|
2334
|
+
for (var i = 0; i < parts.length; i++) {
|
|
2335
|
+
var s = parts[i].trim();
|
|
2336
|
+
// 'body' selector gets special treatment: .bw-theme-alt body
|
|
2337
|
+
if (s === 'body' || s.indexOf('body') === 0) {
|
|
2338
|
+
scopedParts.push(altPrefix + ' ' + s);
|
|
2339
|
+
} else {
|
|
2340
|
+
scopedParts.push(altPrefix + ' ' + s);
|
|
2341
|
+
}
|
|
2089
2342
|
}
|
|
2343
|
+
altRules[scopedParts.join(', ')] = rawRules[sel];
|
|
2090
2344
|
}
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
// Add body-level overrides for the alternate surface
|
|
2348
|
+
altRules[altPrefix + ' body, :root' + altPrefix + ' body'] = {
|
|
2349
|
+
'color': altPalette.dark.base,
|
|
2350
|
+
'background-color': altPalette.light.base
|
|
2091
2351
|
};
|
|
2352
|
+
|
|
2353
|
+
return altRules;
|
|
2092
2354
|
}
|
|
2093
2355
|
|
|
2094
2356
|
function deepMerge(target, source) {
|
|
@@ -2285,8 +2547,11 @@
|
|
|
2285
2547
|
* variant: "success",
|
|
2286
2548
|
* onclick: () => console.log("saved")
|
|
2287
2549
|
* });
|
|
2550
|
+
* // String shorthand:
|
|
2551
|
+
* const ok = makeButton("OK");
|
|
2288
2552
|
*/
|
|
2289
2553
|
function makeButton(props = {}) {
|
|
2554
|
+
if (typeof props === 'string') props = { text: props };
|
|
2290
2555
|
const {
|
|
2291
2556
|
text,
|
|
2292
2557
|
variant = 'primary',
|
|
@@ -2591,6 +2856,7 @@
|
|
|
2591
2856
|
class: `bw-nav-link ${index === actualActiveIndex ? 'active' : ''}`,
|
|
2592
2857
|
type: 'button',
|
|
2593
2858
|
role: 'tab',
|
|
2859
|
+
tabindex: index === actualActiveIndex ? '0' : '-1',
|
|
2594
2860
|
'aria-selected': index === actualActiveIndex ? 'true' : 'false',
|
|
2595
2861
|
'data-tab-index': index,
|
|
2596
2862
|
onclick: (e) => {
|
|
@@ -2601,11 +2867,13 @@
|
|
|
2601
2867
|
allTabs.forEach(t => {
|
|
2602
2868
|
t.classList.remove('active');
|
|
2603
2869
|
t.setAttribute('aria-selected', 'false');
|
|
2870
|
+
t.setAttribute('tabindex', '-1');
|
|
2604
2871
|
});
|
|
2605
2872
|
allPanes.forEach(p => p.classList.remove('active'));
|
|
2606
2873
|
|
|
2607
2874
|
e.target.classList.add('active');
|
|
2608
2875
|
e.target.setAttribute('aria-selected', 'true');
|
|
2876
|
+
e.target.setAttribute('tabindex', '0');
|
|
2609
2877
|
const targetIndex = parseInt(e.target.getAttribute('data-tab-index'));
|
|
2610
2878
|
allPanes[targetIndex].classList.add('active');
|
|
2611
2879
|
}
|
|
@@ -2629,7 +2897,39 @@
|
|
|
2629
2897
|
],
|
|
2630
2898
|
o: {
|
|
2631
2899
|
type: 'tabs',
|
|
2632
|
-
state: { activeIndex: actualActiveIndex }
|
|
2900
|
+
state: { activeIndex: actualActiveIndex },
|
|
2901
|
+
mounted: function(el) {
|
|
2902
|
+
var tablist = el.querySelector('[role="tablist"]');
|
|
2903
|
+
if (!tablist) return;
|
|
2904
|
+
tablist.addEventListener('keydown', function(e) {
|
|
2905
|
+
var tabButtons = tablist.querySelectorAll('[role="tab"]');
|
|
2906
|
+
var currentIndex = -1;
|
|
2907
|
+
for (var i = 0; i < tabButtons.length; i++) {
|
|
2908
|
+
if (tabButtons[i] === e.target) { currentIndex = i; break; }
|
|
2909
|
+
}
|
|
2910
|
+
if (currentIndex === -1) return;
|
|
2911
|
+
|
|
2912
|
+
var newIndex = -1;
|
|
2913
|
+
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
|
2914
|
+
e.preventDefault();
|
|
2915
|
+
newIndex = currentIndex > 0 ? currentIndex - 1 : tabButtons.length - 1;
|
|
2916
|
+
} else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
|
2917
|
+
e.preventDefault();
|
|
2918
|
+
newIndex = currentIndex < tabButtons.length - 1 ? currentIndex + 1 : 0;
|
|
2919
|
+
} else if (e.key === 'Home') {
|
|
2920
|
+
e.preventDefault();
|
|
2921
|
+
newIndex = 0;
|
|
2922
|
+
} else if (e.key === 'End') {
|
|
2923
|
+
e.preventDefault();
|
|
2924
|
+
newIndex = tabButtons.length - 1;
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
if (newIndex >= 0) {
|
|
2928
|
+
tabButtons[newIndex].focus();
|
|
2929
|
+
tabButtons[newIndex].click();
|
|
2930
|
+
}
|
|
2931
|
+
});
|
|
2932
|
+
}
|
|
2633
2933
|
}
|
|
2634
2934
|
};
|
|
2635
2935
|
}
|
|
@@ -2650,8 +2950,11 @@
|
|
|
2650
2950
|
* variant: "success",
|
|
2651
2951
|
* dismissible: true
|
|
2652
2952
|
* });
|
|
2953
|
+
* // String shorthand:
|
|
2954
|
+
* const msg = makeAlert("Something happened");
|
|
2653
2955
|
*/
|
|
2654
2956
|
function makeAlert(props = {}) {
|
|
2957
|
+
if (typeof props === 'string') props = { content: props };
|
|
2655
2958
|
const {
|
|
2656
2959
|
content,
|
|
2657
2960
|
variant = 'info',
|
|
@@ -2698,8 +3001,11 @@
|
|
|
2698
3001
|
* @example
|
|
2699
3002
|
* const badge = makeBadge({ text: "New", variant: "danger", pill: true });
|
|
2700
3003
|
* const small = makeBadge({ text: "3", variant: "info", size: "sm" });
|
|
3004
|
+
* // String shorthand:
|
|
3005
|
+
* const tag = makeBadge("New");
|
|
2701
3006
|
*/
|
|
2702
3007
|
function makeBadge(props = {}) {
|
|
3008
|
+
if (typeof props === 'string') props = { text: props };
|
|
2703
3009
|
const {
|
|
2704
3010
|
text,
|
|
2705
3011
|
variant = 'primary',
|
|
@@ -2936,13 +3242,16 @@
|
|
|
2936
3242
|
}
|
|
2937
3243
|
|
|
2938
3244
|
/**
|
|
2939
|
-
* Create a form group with label, input,
|
|
3245
|
+
* Create a form group with label, input, optional help text and validation feedback
|
|
2940
3246
|
*
|
|
2941
3247
|
* @param {Object} [props] - Form group configuration
|
|
2942
3248
|
* @param {string} [props.label] - Label text
|
|
2943
3249
|
* @param {Object} [props.input] - Input TACO object (from makeInput, makeSelect, etc.)
|
|
2944
3250
|
* @param {string} [props.help] - Help text displayed below the input
|
|
2945
3251
|
* @param {string} [props.id] - Input ID (links label to input via for/id)
|
|
3252
|
+
* @param {string} [props.validation] - Validation state ("valid" or "invalid")
|
|
3253
|
+
* @param {string} [props.feedback] - Validation feedback text shown below input
|
|
3254
|
+
* @param {boolean} [props.required=false] - Show required indicator (*) on label
|
|
2946
3255
|
* @returns {Object} TACO object representing a form group
|
|
2947
3256
|
* @category Component Builders
|
|
2948
3257
|
* @example
|
|
@@ -2950,11 +3259,22 @@
|
|
|
2950
3259
|
* label: "Email",
|
|
2951
3260
|
* id: "email",
|
|
2952
3261
|
* input: makeInput({ type: "email", id: "email", placeholder: "you@example.com" }),
|
|
2953
|
-
*
|
|
3262
|
+
* validation: "invalid",
|
|
3263
|
+
* feedback: "Please enter a valid email address."
|
|
2954
3264
|
* });
|
|
2955
3265
|
*/
|
|
2956
3266
|
function makeFormGroup(props = {}) {
|
|
2957
|
-
|
|
3267
|
+
var { label, input, help, id, validation, feedback, required } = props;
|
|
3268
|
+
|
|
3269
|
+
// Shallow-clone input TACO to add validation class without mutating original
|
|
3270
|
+
var styledInput = input;
|
|
3271
|
+
if (validation && input && input.a) {
|
|
3272
|
+
styledInput = { t: input.t, a: Object.assign({}, input.a), c: input.c, o: input.o };
|
|
3273
|
+
var validClass = validation === 'valid' ? 'bw-is-valid' : validation === 'invalid' ? 'bw-is-invalid' : '';
|
|
3274
|
+
if (validClass) {
|
|
3275
|
+
styledInput.a.class = ((styledInput.a.class || '') + ' ' + validClass).trim();
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
2958
3278
|
|
|
2959
3279
|
return {
|
|
2960
3280
|
t: 'div',
|
|
@@ -2963,9 +3283,14 @@
|
|
|
2963
3283
|
label && {
|
|
2964
3284
|
t: 'label',
|
|
2965
3285
|
a: { for: id, class: 'bw-form-label' },
|
|
2966
|
-
c: label
|
|
3286
|
+
c: required ? [label, { t: 'span', a: { class: 'bw-text-danger', style: 'margin-left: 0.25rem' }, c: '*' }] : label
|
|
3287
|
+
},
|
|
3288
|
+
styledInput,
|
|
3289
|
+
feedback && validation && {
|
|
3290
|
+
t: 'div',
|
|
3291
|
+
a: { class: validation === 'valid' ? 'bw-valid-feedback' : 'bw-invalid-feedback' },
|
|
3292
|
+
c: feedback
|
|
2967
3293
|
},
|
|
2968
|
-
input,
|
|
2969
3294
|
help && {
|
|
2970
3295
|
t: 'small',
|
|
2971
3296
|
a: { class: 'bw-form-text bw-text-muted' },
|
|
@@ -3930,17 +4255,15 @@
|
|
|
3930
4255
|
t: 'button',
|
|
3931
4256
|
a: {
|
|
3932
4257
|
class: 'bw-copy-btn bw-code-copy-btn',
|
|
3933
|
-
onclick: (e)
|
|
3934
|
-
navigator.clipboard.writeText(code).then(()
|
|
3935
|
-
|
|
3936
|
-
|
|
4258
|
+
onclick: function(e) {
|
|
4259
|
+
navigator.clipboard.writeText(code).then(function() {
|
|
4260
|
+
var btn = e.target;
|
|
4261
|
+
var originalText = btn.textContent;
|
|
3937
4262
|
btn.textContent = 'Copied!';
|
|
3938
|
-
btn.
|
|
3939
|
-
|
|
3940
|
-
setTimeout(() => {
|
|
4263
|
+
btn.classList.add('bw-code-copy-btn-copied');
|
|
4264
|
+
setTimeout(function() {
|
|
3941
4265
|
btn.textContent = originalText;
|
|
3942
|
-
btn.
|
|
3943
|
-
btn.style.color = '#aaa';
|
|
4266
|
+
btn.classList.remove('bw-code-copy-btn-copied');
|
|
3944
4267
|
}, 2000);
|
|
3945
4268
|
});
|
|
3946
4269
|
}
|
|
@@ -4232,29 +4555,47 @@
|
|
|
4232
4555
|
var isOpen = collapse.classList.contains('bw-collapse-show');
|
|
4233
4556
|
|
|
4234
4557
|
if (!multiOpen) {
|
|
4235
|
-
//
|
|
4236
|
-
var
|
|
4237
|
-
var
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4558
|
+
// Animate-close all other open siblings
|
|
4559
|
+
var allItems = accordionEl.querySelectorAll('.bw-accordion-item');
|
|
4560
|
+
for (var j = 0; j < allItems.length; j++) {
|
|
4561
|
+
if (allItems[j] === accordionItem) continue;
|
|
4562
|
+
var sibCollapse = allItems[j].querySelector('.bw-accordion-collapse');
|
|
4563
|
+
var sibBtn = allItems[j].querySelector('.bw-accordion-button');
|
|
4564
|
+
if (sibCollapse.classList.contains('bw-collapse-show')) {
|
|
4565
|
+
sibCollapse.style.maxHeight = sibCollapse.scrollHeight + 'px';
|
|
4566
|
+
sibCollapse.offsetHeight; // force reflow
|
|
4567
|
+
sibCollapse.style.maxHeight = '0px';
|
|
4568
|
+
sibCollapse.classList.remove('bw-collapse-show');
|
|
4569
|
+
sibBtn.classList.add('bw-collapsed');
|
|
4570
|
+
sibBtn.setAttribute('aria-expanded', 'false');
|
|
4571
|
+
}
|
|
4245
4572
|
}
|
|
4246
4573
|
}
|
|
4247
4574
|
|
|
4248
4575
|
if (isOpen) {
|
|
4576
|
+
// Animate close
|
|
4577
|
+
collapse.style.maxHeight = collapse.scrollHeight + 'px';
|
|
4578
|
+
collapse.offsetHeight; // force reflow
|
|
4579
|
+
collapse.style.maxHeight = '0px';
|
|
4249
4580
|
collapse.classList.remove('bw-collapse-show');
|
|
4250
|
-
collapse.style.maxHeight = null;
|
|
4251
4581
|
btn.classList.add('bw-collapsed');
|
|
4252
4582
|
btn.setAttribute('aria-expanded', 'false');
|
|
4253
4583
|
} else {
|
|
4584
|
+
// Animate open
|
|
4254
4585
|
collapse.classList.add('bw-collapse-show');
|
|
4586
|
+
collapse.style.maxHeight = '0px';
|
|
4587
|
+
collapse.offsetHeight; // force reflow
|
|
4255
4588
|
collapse.style.maxHeight = collapse.scrollHeight + 'px';
|
|
4256
4589
|
btn.classList.remove('bw-collapsed');
|
|
4257
4590
|
btn.setAttribute('aria-expanded', 'true');
|
|
4591
|
+
// After transition, allow dynamic content sizing
|
|
4592
|
+
var onEnd = function(ev) {
|
|
4593
|
+
if (ev.propertyName === 'max-height' && collapse.classList.contains('bw-collapse-show')) {
|
|
4594
|
+
collapse.style.maxHeight = 'none';
|
|
4595
|
+
}
|
|
4596
|
+
collapse.removeEventListener('transitionend', onEnd);
|
|
4597
|
+
};
|
|
4598
|
+
collapse.addEventListener('transitionend', onEnd);
|
|
4258
4599
|
}
|
|
4259
4600
|
}
|
|
4260
4601
|
},
|
|
@@ -4271,7 +4612,7 @@
|
|
|
4271
4612
|
},
|
|
4272
4613
|
o: item.open ? {
|
|
4273
4614
|
mounted: function(el) {
|
|
4274
|
-
el.style.maxHeight =
|
|
4615
|
+
el.style.maxHeight = 'none';
|
|
4275
4616
|
}
|
|
4276
4617
|
} : undefined
|
|
4277
4618
|
}
|
|
@@ -4981,104 +5322,997 @@
|
|
|
4981
5322
|
a: {
|
|
4982
5323
|
class: ('bw-carousel ' + className).trim(),
|
|
4983
5324
|
style: 'height: ' + height,
|
|
5325
|
+
tabindex: '0',
|
|
5326
|
+
'aria-roledescription': 'carousel',
|
|
4984
5327
|
'data-carousel-index': startIndex
|
|
4985
5328
|
},
|
|
4986
5329
|
c: children,
|
|
4987
5330
|
o: {
|
|
4988
5331
|
type: 'carousel',
|
|
4989
5332
|
state: { activeIndex: startIndex, autoPlay: autoPlay, interval: interval },
|
|
4990
|
-
mounted:
|
|
4991
|
-
|
|
5333
|
+
mounted: function(el) {
|
|
5334
|
+
// Keyboard navigation
|
|
5335
|
+
el.addEventListener('keydown', function(e) {
|
|
4992
5336
|
var idx = parseInt(el.getAttribute('data-carousel-index') || '0');
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
5337
|
+
if (e.key === 'ArrowLeft') {
|
|
5338
|
+
e.preventDefault();
|
|
5339
|
+
goToSlide(el, idx - 1);
|
|
5340
|
+
} else if (e.key === 'ArrowRight') {
|
|
5341
|
+
e.preventDefault();
|
|
5342
|
+
goToSlide(el, idx + 1);
|
|
5343
|
+
}
|
|
5344
|
+
});
|
|
5345
|
+
// Auto-play
|
|
5346
|
+
if (autoPlay) {
|
|
5347
|
+
var intervalId = setInterval(function() {
|
|
5348
|
+
var idx = parseInt(el.getAttribute('data-carousel-index') || '0');
|
|
5349
|
+
goToSlide(el, idx + 1);
|
|
5350
|
+
}, interval);
|
|
5351
|
+
el._bw_carouselInterval = intervalId;
|
|
5352
|
+
// Pause on hover/focus for usability
|
|
5353
|
+
el.addEventListener('mouseenter', function() {
|
|
5354
|
+
if (el._bw_carouselInterval) clearInterval(el._bw_carouselInterval);
|
|
5355
|
+
});
|
|
5356
|
+
el.addEventListener('mouseleave', function() {
|
|
5357
|
+
el._bw_carouselInterval = setInterval(function() {
|
|
5358
|
+
var idx = parseInt(el.getAttribute('data-carousel-index') || '0');
|
|
5359
|
+
goToSlide(el, idx + 1);
|
|
5360
|
+
}, interval);
|
|
5361
|
+
});
|
|
5362
|
+
}
|
|
5363
|
+
},
|
|
5364
|
+
unmount: function(el) {
|
|
4998
5365
|
if (el._bw_carouselInterval) {
|
|
4999
5366
|
clearInterval(el._bw_carouselInterval);
|
|
5000
5367
|
}
|
|
5001
|
-
}
|
|
5368
|
+
}
|
|
5002
5369
|
}
|
|
5003
5370
|
};
|
|
5004
5371
|
}
|
|
5005
5372
|
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
navbar: NavbarHandle,
|
|
5010
|
-
tabs: TabsHandle,
|
|
5011
|
-
modal: ModalHandle
|
|
5012
|
-
};
|
|
5013
|
-
|
|
5014
|
-
var components = /*#__PURE__*/Object.freeze({
|
|
5015
|
-
__proto__: null,
|
|
5016
|
-
CardHandle: CardHandle,
|
|
5017
|
-
ModalHandle: ModalHandle,
|
|
5018
|
-
NavbarHandle: NavbarHandle,
|
|
5019
|
-
TableHandle: TableHandle,
|
|
5020
|
-
TabsHandle: TabsHandle,
|
|
5021
|
-
componentHandles: componentHandles,
|
|
5022
|
-
makeAccordion: makeAccordion,
|
|
5023
|
-
makeAlert: makeAlert,
|
|
5024
|
-
makeAvatar: makeAvatar,
|
|
5025
|
-
makeBadge: makeBadge,
|
|
5026
|
-
makeBreadcrumb: makeBreadcrumb,
|
|
5027
|
-
makeButton: makeButton,
|
|
5028
|
-
makeButtonGroup: makeButtonGroup,
|
|
5029
|
-
makeCTA: makeCTA,
|
|
5030
|
-
makeCard: makeCard,
|
|
5031
|
-
makeCarousel: makeCarousel,
|
|
5032
|
-
makeCheckbox: makeCheckbox,
|
|
5033
|
-
makeCodeDemo: makeCodeDemo,
|
|
5034
|
-
makeCol: makeCol,
|
|
5035
|
-
makeContainer: makeContainer,
|
|
5036
|
-
makeDropdown: makeDropdown,
|
|
5037
|
-
makeFeatureGrid: makeFeatureGrid,
|
|
5038
|
-
makeForm: makeForm,
|
|
5039
|
-
makeFormGroup: makeFormGroup,
|
|
5040
|
-
makeHero: makeHero,
|
|
5041
|
-
makeInput: makeInput,
|
|
5042
|
-
makeListGroup: makeListGroup,
|
|
5043
|
-
makeModal: makeModal,
|
|
5044
|
-
makeNav: makeNav,
|
|
5045
|
-
makeNavbar: makeNavbar,
|
|
5046
|
-
makePagination: makePagination,
|
|
5047
|
-
makeProgress: makeProgress,
|
|
5048
|
-
makeRadio: makeRadio,
|
|
5049
|
-
makeRow: makeRow,
|
|
5050
|
-
makeSection: makeSection,
|
|
5051
|
-
makeSelect: makeSelect,
|
|
5052
|
-
makeSkeleton: makeSkeleton,
|
|
5053
|
-
makeSpinner: makeSpinner,
|
|
5054
|
-
makeStack: makeStack,
|
|
5055
|
-
makeSwitch: makeSwitch,
|
|
5056
|
-
makeTabs: makeTabs,
|
|
5057
|
-
makeTextarea: makeTextarea,
|
|
5058
|
-
makeToast: makeToast
|
|
5059
|
-
});
|
|
5373
|
+
// =========================================================================
|
|
5374
|
+
// Phase 4: Dashboard & Data Display
|
|
5375
|
+
// =========================================================================
|
|
5060
5376
|
|
|
5061
5377
|
/**
|
|
5062
|
-
*
|
|
5063
|
-
*
|
|
5064
|
-
*
|
|
5065
|
-
*
|
|
5066
|
-
*
|
|
5067
|
-
* @
|
|
5378
|
+
* Create a stat card for dashboard metrics display
|
|
5379
|
+
*
|
|
5380
|
+
* Shows a large value with a label and optional change indicator.
|
|
5381
|
+
* Designed for dashboard grid layouts with left-border accent.
|
|
5382
|
+
*
|
|
5383
|
+
* @param {Object|string} [props] - Stat card configuration (string shorthand sets label)
|
|
5384
|
+
* @param {string|number} [props.value=0] - The main stat value to display
|
|
5385
|
+
* @param {string} [props.label] - Descriptive label below the value
|
|
5386
|
+
* @param {number} [props.change] - Percentage change indicator (positive = green arrow, negative = red)
|
|
5387
|
+
* @param {string} [props.format] - Value format ("number", "currency", "percent")
|
|
5388
|
+
* @param {string} [props.prefix] - Custom prefix (e.g. "$")
|
|
5389
|
+
* @param {string} [props.suffix] - Custom suffix (e.g. "%")
|
|
5390
|
+
* @param {string} [props.icon] - Icon content (emoji or text) shown above value
|
|
5391
|
+
* @param {string} [props.variant] - Left-border color variant ("primary", "success", "danger", etc.)
|
|
5392
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
5393
|
+
* @param {Object} [props.style] - Inline style object
|
|
5394
|
+
* @returns {Object} TACO object representing a stat card
|
|
5395
|
+
* @category Component Builders
|
|
5396
|
+
* @example
|
|
5397
|
+
* const stat = makeStatCard({
|
|
5398
|
+
* value: 2345,
|
|
5399
|
+
* label: 'Active Users',
|
|
5400
|
+
* change: 5.3,
|
|
5401
|
+
* format: 'number',
|
|
5402
|
+
* variant: 'primary'
|
|
5403
|
+
* });
|
|
5068
5404
|
*/
|
|
5405
|
+
function makeStatCard(props = {}) {
|
|
5406
|
+
if (typeof props === 'string') props = { label: props };
|
|
5407
|
+
var {
|
|
5408
|
+
value = 0,
|
|
5409
|
+
label,
|
|
5410
|
+
change,
|
|
5411
|
+
format,
|
|
5412
|
+
prefix,
|
|
5413
|
+
suffix,
|
|
5414
|
+
icon,
|
|
5415
|
+
variant,
|
|
5416
|
+
className = '',
|
|
5417
|
+
style
|
|
5418
|
+
} = props;
|
|
5069
5419
|
|
|
5420
|
+
function formatValue(val, fmt) {
|
|
5421
|
+
if (prefix || suffix) return (prefix || '') + val + (suffix || '');
|
|
5422
|
+
switch (fmt) {
|
|
5423
|
+
case 'currency': return '$' + Number(val).toLocaleString();
|
|
5424
|
+
case 'percent': return val + '%';
|
|
5425
|
+
case 'number': return Number(val).toLocaleString();
|
|
5426
|
+
default: return '' + val;
|
|
5427
|
+
}
|
|
5428
|
+
}
|
|
5070
5429
|
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5430
|
+
var classes = [
|
|
5431
|
+
'bw-stat-card',
|
|
5432
|
+
variant ? 'bw-stat-card-' + variant : '',
|
|
5433
|
+
className
|
|
5434
|
+
].filter(Boolean).join(' ').trim();
|
|
5074
5435
|
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5436
|
+
var children = [];
|
|
5437
|
+
|
|
5438
|
+
if (icon) {
|
|
5439
|
+
children.push({
|
|
5440
|
+
t: 'div',
|
|
5441
|
+
a: { class: 'bw-stat-icon' },
|
|
5442
|
+
c: icon
|
|
5443
|
+
});
|
|
5444
|
+
}
|
|
5445
|
+
|
|
5446
|
+
children.push({
|
|
5447
|
+
t: 'div',
|
|
5448
|
+
a: { class: 'bw-stat-value' },
|
|
5449
|
+
c: formatValue(value, format)
|
|
5450
|
+
});
|
|
5451
|
+
|
|
5452
|
+
if (label) {
|
|
5453
|
+
children.push({
|
|
5454
|
+
t: 'div',
|
|
5455
|
+
a: { class: 'bw-stat-label' },
|
|
5456
|
+
c: label
|
|
5457
|
+
});
|
|
5458
|
+
}
|
|
5459
|
+
|
|
5460
|
+
if (change !== undefined && change !== null) {
|
|
5461
|
+
children.push({
|
|
5462
|
+
t: 'div',
|
|
5463
|
+
a: {
|
|
5464
|
+
class: 'bw-stat-change ' + (change >= 0 ? 'bw-stat-change-up' : 'bw-stat-change-down')
|
|
5465
|
+
},
|
|
5466
|
+
c: (change >= 0 ? '\u2191 +' : '\u2193 ') + change + '%'
|
|
5467
|
+
});
|
|
5468
|
+
}
|
|
5469
|
+
|
|
5470
|
+
return {
|
|
5471
|
+
t: 'div',
|
|
5472
|
+
a: { class: classes, style: style },
|
|
5473
|
+
c: children,
|
|
5474
|
+
o: { type: 'stat-card' }
|
|
5475
|
+
};
|
|
5476
|
+
}
|
|
5477
|
+
|
|
5478
|
+
// =========================================================================
|
|
5479
|
+
// Phase 5: Overlays & Popovers
|
|
5480
|
+
// =========================================================================
|
|
5481
|
+
|
|
5482
|
+
/**
|
|
5483
|
+
* Create a tooltip wrapper around trigger content
|
|
5484
|
+
*
|
|
5485
|
+
* Wraps the trigger element in a container that shows tooltip text
|
|
5486
|
+
* on hover and focus. Pure CSS-driven show/hide with JS lifecycle
|
|
5487
|
+
* for event binding.
|
|
5488
|
+
*
|
|
5489
|
+
* @param {Object} [props] - Tooltip configuration
|
|
5490
|
+
* @param {string|Object|Array} [props.content] - Trigger content (what the user hovers/focuses)
|
|
5491
|
+
* @param {string} [props.text=""] - Tooltip text to display
|
|
5492
|
+
* @param {string} [props.placement="top"] - Tooltip placement ("top", "bottom", "left", "right")
|
|
5493
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
5494
|
+
* @returns {Object} TACO object representing a tooltip wrapper
|
|
5495
|
+
* @category Component Builders
|
|
5496
|
+
* @example
|
|
5497
|
+
* const tip = makeTooltip({
|
|
5498
|
+
* content: makeButton({ text: 'Hover me' }),
|
|
5499
|
+
* text: 'This is a tooltip!',
|
|
5500
|
+
* placement: 'top'
|
|
5501
|
+
* });
|
|
5502
|
+
*/
|
|
5503
|
+
function makeTooltip(props = {}) {
|
|
5504
|
+
var {
|
|
5505
|
+
content,
|
|
5506
|
+
text = '',
|
|
5507
|
+
placement = 'top',
|
|
5508
|
+
className = ''
|
|
5509
|
+
} = props;
|
|
5510
|
+
|
|
5511
|
+
return {
|
|
5512
|
+
t: 'span',
|
|
5513
|
+
a: { class: ('bw-tooltip-wrapper ' + className).trim() },
|
|
5514
|
+
c: [
|
|
5515
|
+
content,
|
|
5516
|
+
{
|
|
5517
|
+
t: 'span',
|
|
5518
|
+
a: {
|
|
5519
|
+
class: 'bw-tooltip bw-tooltip-' + placement,
|
|
5520
|
+
role: 'tooltip'
|
|
5521
|
+
},
|
|
5522
|
+
c: text
|
|
5523
|
+
}
|
|
5524
|
+
],
|
|
5525
|
+
o: {
|
|
5526
|
+
type: 'tooltip',
|
|
5527
|
+
mounted: function(el) {
|
|
5528
|
+
var tip = el.querySelector('.bw-tooltip');
|
|
5529
|
+
el.addEventListener('mouseenter', function() {
|
|
5530
|
+
tip.classList.add('bw-tooltip-show');
|
|
5531
|
+
});
|
|
5532
|
+
el.addEventListener('mouseleave', function() {
|
|
5533
|
+
tip.classList.remove('bw-tooltip-show');
|
|
5534
|
+
});
|
|
5535
|
+
el.addEventListener('focusin', function() {
|
|
5536
|
+
tip.classList.add('bw-tooltip-show');
|
|
5537
|
+
});
|
|
5538
|
+
el.addEventListener('focusout', function() {
|
|
5539
|
+
tip.classList.remove('bw-tooltip-show');
|
|
5540
|
+
});
|
|
5541
|
+
}
|
|
5542
|
+
}
|
|
5543
|
+
};
|
|
5544
|
+
}
|
|
5545
|
+
|
|
5546
|
+
/**
|
|
5547
|
+
* Create a popover wrapper around trigger content
|
|
5548
|
+
*
|
|
5549
|
+
* Like a tooltip but richer — supports title + body content and is
|
|
5550
|
+
* triggered by click rather than hover. Dismisses on click outside.
|
|
5551
|
+
*
|
|
5552
|
+
* @param {Object} [props] - Popover configuration
|
|
5553
|
+
* @param {string|Object|Array} [props.trigger] - Trigger content (what the user clicks)
|
|
5554
|
+
* @param {string} [props.title] - Popover header title
|
|
5555
|
+
* @param {string|Object|Array} [props.content] - Popover body content
|
|
5556
|
+
* @param {string} [props.placement="top"] - Placement ("top", "bottom", "left", "right")
|
|
5557
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
5558
|
+
* @returns {Object} TACO object representing a popover wrapper
|
|
5559
|
+
* @category Component Builders
|
|
5560
|
+
* @example
|
|
5561
|
+
* const pop = makePopover({
|
|
5562
|
+
* trigger: makeButton({ text: 'Click me' }),
|
|
5563
|
+
* title: 'Popover Title',
|
|
5564
|
+
* content: 'Some helpful information here.',
|
|
5565
|
+
* placement: 'bottom'
|
|
5566
|
+
* });
|
|
5567
|
+
*/
|
|
5568
|
+
function makePopover(props = {}) {
|
|
5569
|
+
var {
|
|
5570
|
+
trigger,
|
|
5571
|
+
title,
|
|
5572
|
+
content,
|
|
5573
|
+
placement = 'top',
|
|
5574
|
+
className = ''
|
|
5575
|
+
} = props;
|
|
5576
|
+
|
|
5577
|
+
var popoverContent = [
|
|
5578
|
+
title && {
|
|
5579
|
+
t: 'div',
|
|
5580
|
+
a: { class: 'bw-popover-header' },
|
|
5581
|
+
c: title
|
|
5582
|
+
},
|
|
5583
|
+
content && {
|
|
5584
|
+
t: 'div',
|
|
5585
|
+
a: { class: 'bw-popover-body' },
|
|
5586
|
+
c: content
|
|
5587
|
+
}
|
|
5588
|
+
].filter(Boolean);
|
|
5589
|
+
|
|
5590
|
+
return {
|
|
5591
|
+
t: 'span',
|
|
5592
|
+
a: { class: ('bw-popover-wrapper ' + className).trim() },
|
|
5593
|
+
c: [
|
|
5594
|
+
{
|
|
5595
|
+
t: 'span',
|
|
5596
|
+
a: {
|
|
5597
|
+
class: 'bw-popover-trigger',
|
|
5598
|
+
onclick: function(e) {
|
|
5599
|
+
var wrapper = e.target.closest('.bw-popover-wrapper');
|
|
5600
|
+
var pop = wrapper.querySelector('.bw-popover');
|
|
5601
|
+
pop.classList.toggle('bw-popover-show');
|
|
5602
|
+
}
|
|
5603
|
+
},
|
|
5604
|
+
c: trigger
|
|
5605
|
+
},
|
|
5606
|
+
{
|
|
5607
|
+
t: 'div',
|
|
5608
|
+
a: {
|
|
5609
|
+
class: 'bw-popover bw-popover-' + placement
|
|
5610
|
+
},
|
|
5611
|
+
c: popoverContent
|
|
5612
|
+
}
|
|
5613
|
+
],
|
|
5614
|
+
o: {
|
|
5615
|
+
type: 'popover',
|
|
5616
|
+
mounted: function(el) {
|
|
5617
|
+
// Click outside to close
|
|
5618
|
+
var outsideHandler = function(e) {
|
|
5619
|
+
if (!el.contains(e.target)) {
|
|
5620
|
+
var pop = el.querySelector('.bw-popover');
|
|
5621
|
+
if (pop) pop.classList.remove('bw-popover-show');
|
|
5622
|
+
}
|
|
5623
|
+
};
|
|
5624
|
+
document.addEventListener('click', outsideHandler);
|
|
5625
|
+
el._bw_outsideHandler = outsideHandler;
|
|
5626
|
+
},
|
|
5627
|
+
unmount: function(el) {
|
|
5628
|
+
if (el._bw_outsideHandler) {
|
|
5629
|
+
document.removeEventListener('click', el._bw_outsideHandler);
|
|
5630
|
+
}
|
|
5631
|
+
}
|
|
5632
|
+
}
|
|
5633
|
+
};
|
|
5634
|
+
}
|
|
5635
|
+
|
|
5636
|
+
// =========================================================================
|
|
5637
|
+
// Phase 6: Form Enhancements & Layout
|
|
5638
|
+
// =========================================================================
|
|
5639
|
+
|
|
5640
|
+
/**
|
|
5641
|
+
* Create a search input with clear button
|
|
5642
|
+
*
|
|
5643
|
+
* Wraps a text input with a clear (×) button that appears when
|
|
5644
|
+
* the field has content. Calls onSearch on Enter key.
|
|
5645
|
+
*
|
|
5646
|
+
* @param {Object} [props] - Search input configuration
|
|
5647
|
+
* @param {string} [props.placeholder="Search..."] - Placeholder text
|
|
5648
|
+
* @param {string} [props.value] - Initial value
|
|
5649
|
+
* @param {Function} [props.onSearch] - Callback when Enter is pressed, receives value
|
|
5650
|
+
* @param {Function} [props.onInput] - Callback on each keystroke, receives value
|
|
5651
|
+
* @param {string} [props.id] - Element ID
|
|
5652
|
+
* @param {string} [props.name] - Input name attribute
|
|
5653
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
5654
|
+
* @returns {Object} TACO object representing a search input
|
|
5655
|
+
* @category Component Builders
|
|
5656
|
+
* @example
|
|
5657
|
+
* const search = makeSearchInput({
|
|
5658
|
+
* placeholder: 'Search users...',
|
|
5659
|
+
* onSearch: (val) => filterUsers(val)
|
|
5660
|
+
* });
|
|
5661
|
+
*/
|
|
5662
|
+
function makeSearchInput(props = {}) {
|
|
5663
|
+
if (typeof props === 'string') props = { placeholder: props };
|
|
5664
|
+
var {
|
|
5665
|
+
placeholder = 'Search...',
|
|
5666
|
+
value,
|
|
5667
|
+
onSearch,
|
|
5668
|
+
onInput,
|
|
5669
|
+
id,
|
|
5670
|
+
name,
|
|
5671
|
+
className = ''
|
|
5672
|
+
} = props;
|
|
5673
|
+
|
|
5674
|
+
return {
|
|
5675
|
+
t: 'div',
|
|
5676
|
+
a: { class: ('bw-search-input ' + className).trim() },
|
|
5677
|
+
c: [
|
|
5678
|
+
{
|
|
5679
|
+
t: 'input',
|
|
5680
|
+
a: {
|
|
5681
|
+
type: 'search',
|
|
5682
|
+
class: 'bw-form-control bw-search-field',
|
|
5683
|
+
placeholder: placeholder,
|
|
5684
|
+
value: value,
|
|
5685
|
+
id: id,
|
|
5686
|
+
name: name,
|
|
5687
|
+
onkeydown: function(e) {
|
|
5688
|
+
if (e.key === 'Enter' && onSearch) {
|
|
5689
|
+
e.preventDefault();
|
|
5690
|
+
onSearch(e.target.value);
|
|
5691
|
+
}
|
|
5692
|
+
},
|
|
5693
|
+
oninput: function(e) {
|
|
5694
|
+
var wrapper = e.target.closest('.bw-search-input');
|
|
5695
|
+
var clearBtn = wrapper.querySelector('.bw-search-clear');
|
|
5696
|
+
if (clearBtn) {
|
|
5697
|
+
clearBtn.style.display = e.target.value ? 'flex' : 'none';
|
|
5698
|
+
}
|
|
5699
|
+
if (onInput) onInput(e.target.value);
|
|
5700
|
+
}
|
|
5701
|
+
}
|
|
5702
|
+
},
|
|
5703
|
+
{
|
|
5704
|
+
t: 'button',
|
|
5705
|
+
a: {
|
|
5706
|
+
type: 'button',
|
|
5707
|
+
class: 'bw-search-clear',
|
|
5708
|
+
'aria-label': 'Clear search',
|
|
5709
|
+
style: value ? undefined : 'display: none',
|
|
5710
|
+
onclick: function(e) {
|
|
5711
|
+
var wrapper = e.target.closest('.bw-search-input');
|
|
5712
|
+
var input = wrapper.querySelector('.bw-search-field');
|
|
5713
|
+
input.value = '';
|
|
5714
|
+
e.target.style.display = 'none';
|
|
5715
|
+
input.focus();
|
|
5716
|
+
if (onInput) onInput('');
|
|
5717
|
+
if (onSearch) onSearch('');
|
|
5718
|
+
}
|
|
5719
|
+
},
|
|
5720
|
+
c: '\u00D7'
|
|
5721
|
+
}
|
|
5722
|
+
],
|
|
5723
|
+
o: { type: 'search-input' }
|
|
5724
|
+
};
|
|
5725
|
+
}
|
|
5726
|
+
|
|
5727
|
+
/**
|
|
5728
|
+
* Create a styled range slider input
|
|
5729
|
+
*
|
|
5730
|
+
* @param {Object} [props] - Range configuration
|
|
5731
|
+
* @param {number} [props.min=0] - Minimum value
|
|
5732
|
+
* @param {number} [props.max=100] - Maximum value
|
|
5733
|
+
* @param {number} [props.step=1] - Step increment
|
|
5734
|
+
* @param {number} [props.value=50] - Current value
|
|
5735
|
+
* @param {string} [props.label] - Label text
|
|
5736
|
+
* @param {boolean} [props.showValue=false] - Show current value display
|
|
5737
|
+
* @param {string} [props.id] - Element ID
|
|
5738
|
+
* @param {string} [props.name] - Input name attribute
|
|
5739
|
+
* @param {boolean} [props.disabled=false] - Whether the slider is disabled
|
|
5740
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
5741
|
+
* @returns {Object} TACO object representing a range input
|
|
5742
|
+
* @category Component Builders
|
|
5743
|
+
* @example
|
|
5744
|
+
* const slider = makeRange({
|
|
5745
|
+
* min: 0, max: 100, value: 50,
|
|
5746
|
+
* label: 'Volume',
|
|
5747
|
+
* showValue: true,
|
|
5748
|
+
* oninput: (e) => setVolume(e.target.value)
|
|
5749
|
+
* });
|
|
5750
|
+
*/
|
|
5751
|
+
function makeRange(props = {}) {
|
|
5752
|
+
var {
|
|
5753
|
+
min = 0,
|
|
5754
|
+
max = 100,
|
|
5755
|
+
step = 1,
|
|
5756
|
+
value = 50,
|
|
5757
|
+
label,
|
|
5758
|
+
showValue = false,
|
|
5759
|
+
id,
|
|
5760
|
+
name,
|
|
5761
|
+
disabled = false,
|
|
5762
|
+
className = '',
|
|
5763
|
+
...eventHandlers
|
|
5764
|
+
} = props;
|
|
5765
|
+
|
|
5766
|
+
var children = [];
|
|
5767
|
+
|
|
5768
|
+
if (label || showValue) {
|
|
5769
|
+
var labelContent = [];
|
|
5770
|
+
if (label) {
|
|
5771
|
+
labelContent.push({
|
|
5772
|
+
t: 'span',
|
|
5773
|
+
c: label
|
|
5774
|
+
});
|
|
5775
|
+
}
|
|
5776
|
+
if (showValue) {
|
|
5777
|
+
labelContent.push({
|
|
5778
|
+
t: 'span',
|
|
5779
|
+
a: { class: 'bw-range-value' },
|
|
5780
|
+
c: '' + value
|
|
5781
|
+
});
|
|
5782
|
+
}
|
|
5783
|
+
children.push({
|
|
5784
|
+
t: 'div',
|
|
5785
|
+
a: { class: 'bw-range-label' },
|
|
5786
|
+
c: labelContent
|
|
5787
|
+
});
|
|
5788
|
+
}
|
|
5789
|
+
|
|
5790
|
+
// Wrap oninput to update value display
|
|
5791
|
+
var userOnInput = eventHandlers.oninput;
|
|
5792
|
+
if (showValue) {
|
|
5793
|
+
eventHandlers.oninput = function(e) {
|
|
5794
|
+
var wrapper = e.target.closest('.bw-range-wrapper');
|
|
5795
|
+
var valDisplay = wrapper.querySelector('.bw-range-value');
|
|
5796
|
+
if (valDisplay) valDisplay.textContent = e.target.value;
|
|
5797
|
+
if (userOnInput) userOnInput(e);
|
|
5798
|
+
};
|
|
5799
|
+
}
|
|
5800
|
+
|
|
5801
|
+
children.push({
|
|
5802
|
+
t: 'input',
|
|
5803
|
+
a: {
|
|
5804
|
+
type: 'range',
|
|
5805
|
+
class: 'bw-range',
|
|
5806
|
+
min: min,
|
|
5807
|
+
max: max,
|
|
5808
|
+
step: step,
|
|
5809
|
+
value: value,
|
|
5810
|
+
id: id,
|
|
5811
|
+
name: name,
|
|
5812
|
+
disabled: disabled,
|
|
5813
|
+
...eventHandlers
|
|
5814
|
+
}
|
|
5815
|
+
});
|
|
5816
|
+
|
|
5817
|
+
return {
|
|
5818
|
+
t: 'div',
|
|
5819
|
+
a: { class: ('bw-range-wrapper ' + className).trim() },
|
|
5820
|
+
c: children,
|
|
5821
|
+
o: { type: 'range' }
|
|
5822
|
+
};
|
|
5823
|
+
}
|
|
5824
|
+
|
|
5825
|
+
/**
|
|
5826
|
+
* Create a media object layout (image + text side-by-side)
|
|
5827
|
+
*
|
|
5828
|
+
* Classic media object pattern: image/icon on one side, text content
|
|
5829
|
+
* on the other, using flexbox. Supports reversed layout.
|
|
5830
|
+
*
|
|
5831
|
+
* @param {Object} [props] - Media object configuration
|
|
5832
|
+
* @param {string} [props.src] - Image source URL
|
|
5833
|
+
* @param {string} [props.alt=""] - Image alt text
|
|
5834
|
+
* @param {string} [props.title] - Title text
|
|
5835
|
+
* @param {string|Object|Array} [props.content] - Body content
|
|
5836
|
+
* @param {boolean} [props.reverse=false] - Put image on the right
|
|
5837
|
+
* @param {string} [props.imageSize="3rem"] - Image width/height
|
|
5838
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
5839
|
+
* @returns {Object} TACO object representing a media object
|
|
5840
|
+
* @category Component Builders
|
|
5841
|
+
* @example
|
|
5842
|
+
* const media = makeMediaObject({
|
|
5843
|
+
* src: '/avatar.jpg',
|
|
5844
|
+
* title: 'Jane Doe',
|
|
5845
|
+
* content: 'Posted a comment 5 minutes ago.'
|
|
5846
|
+
* });
|
|
5847
|
+
*/
|
|
5848
|
+
function makeMediaObject(props = {}) {
|
|
5849
|
+
var {
|
|
5850
|
+
src,
|
|
5851
|
+
alt = '',
|
|
5852
|
+
title,
|
|
5853
|
+
content,
|
|
5854
|
+
reverse = false,
|
|
5855
|
+
imageSize = '3rem',
|
|
5856
|
+
className = ''
|
|
5857
|
+
} = props;
|
|
5858
|
+
|
|
5859
|
+
var imgEl = src ? {
|
|
5860
|
+
t: 'img',
|
|
5861
|
+
a: {
|
|
5862
|
+
class: 'bw-media-img',
|
|
5863
|
+
src: src,
|
|
5864
|
+
alt: alt,
|
|
5865
|
+
style: 'width:' + imageSize + ';height:' + imageSize
|
|
5866
|
+
}
|
|
5867
|
+
} : null;
|
|
5868
|
+
|
|
5869
|
+
var bodyEl = {
|
|
5870
|
+
t: 'div',
|
|
5871
|
+
a: { class: 'bw-media-body' },
|
|
5872
|
+
c: [
|
|
5873
|
+
title && { t: 'h5', a: { class: 'bw-media-title' }, c: title },
|
|
5874
|
+
content
|
|
5875
|
+
].filter(Boolean)
|
|
5876
|
+
};
|
|
5877
|
+
|
|
5878
|
+
return {
|
|
5879
|
+
t: 'div',
|
|
5880
|
+
a: { class: ('bw-media ' + (reverse ? 'bw-media-reverse ' : '') + className).trim() },
|
|
5881
|
+
c: reverse
|
|
5882
|
+
? [bodyEl, imgEl].filter(Boolean)
|
|
5883
|
+
: [imgEl, bodyEl].filter(Boolean),
|
|
5884
|
+
o: { type: 'media-object' }
|
|
5885
|
+
};
|
|
5886
|
+
}
|
|
5887
|
+
|
|
5888
|
+
/**
|
|
5889
|
+
* Create a file upload zone with drag-and-drop support
|
|
5890
|
+
*
|
|
5891
|
+
* Styled drop zone with file input. Supports drag-and-drop visuals
|
|
5892
|
+
* and multiple file selection.
|
|
5893
|
+
*
|
|
5894
|
+
* @param {Object} [props] - File upload configuration
|
|
5895
|
+
* @param {string} [props.accept] - Accepted file types (e.g. "image/*", ".pdf,.doc")
|
|
5896
|
+
* @param {boolean} [props.multiple=false] - Allow multiple file selection
|
|
5897
|
+
* @param {Function} [props.onFiles] - Callback when files are selected, receives FileList
|
|
5898
|
+
* @param {string} [props.text="Drop files here or click to browse"] - Zone label text
|
|
5899
|
+
* @param {string} [props.id] - Element ID
|
|
5900
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
5901
|
+
* @returns {Object} TACO object representing a file upload zone
|
|
5902
|
+
* @category Component Builders
|
|
5903
|
+
* @example
|
|
5904
|
+
* const upload = makeFileUpload({
|
|
5905
|
+
* accept: 'image/*',
|
|
5906
|
+
* multiple: true,
|
|
5907
|
+
* onFiles: (files) => uploadFiles(files)
|
|
5908
|
+
* });
|
|
5909
|
+
*/
|
|
5910
|
+
function makeFileUpload(props = {}) {
|
|
5911
|
+
var {
|
|
5912
|
+
accept,
|
|
5913
|
+
multiple = false,
|
|
5914
|
+
onFiles,
|
|
5915
|
+
text = 'Drop files here or click to browse',
|
|
5916
|
+
id,
|
|
5917
|
+
className = ''
|
|
5918
|
+
} = props;
|
|
5919
|
+
|
|
5920
|
+
return {
|
|
5921
|
+
t: 'div',
|
|
5922
|
+
a: {
|
|
5923
|
+
class: ('bw-file-upload ' + className).trim(),
|
|
5924
|
+
tabindex: '0',
|
|
5925
|
+
role: 'button',
|
|
5926
|
+
'aria-label': text
|
|
5927
|
+
},
|
|
5928
|
+
c: [
|
|
5929
|
+
{ t: 'div', a: { class: 'bw-file-upload-icon' }, c: '\uD83D\uDCC1' },
|
|
5930
|
+
{ t: 'div', a: { class: 'bw-file-upload-text' }, c: text },
|
|
5931
|
+
{
|
|
5932
|
+
t: 'input',
|
|
5933
|
+
a: {
|
|
5934
|
+
type: 'file',
|
|
5935
|
+
class: 'bw-file-upload-input',
|
|
5936
|
+
accept: accept,
|
|
5937
|
+
multiple: multiple,
|
|
5938
|
+
id: id,
|
|
5939
|
+
onchange: function(e) {
|
|
5940
|
+
if (onFiles && e.target.files.length) onFiles(e.target.files);
|
|
5941
|
+
}
|
|
5942
|
+
}
|
|
5943
|
+
}
|
|
5944
|
+
],
|
|
5945
|
+
o: {
|
|
5946
|
+
type: 'file-upload',
|
|
5947
|
+
mounted: function(el) {
|
|
5948
|
+
var input = el.querySelector('.bw-file-upload-input');
|
|
5949
|
+
|
|
5950
|
+
// Click zone to trigger file input
|
|
5951
|
+
el.addEventListener('click', function(e) {
|
|
5952
|
+
if (e.target !== input) input.click();
|
|
5953
|
+
});
|
|
5954
|
+
|
|
5955
|
+
// Keyboard activation
|
|
5956
|
+
el.addEventListener('keydown', function(e) {
|
|
5957
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
5958
|
+
e.preventDefault();
|
|
5959
|
+
input.click();
|
|
5960
|
+
}
|
|
5961
|
+
});
|
|
5962
|
+
|
|
5963
|
+
// Drag-and-drop visuals
|
|
5964
|
+
el.addEventListener('dragover', function(e) {
|
|
5965
|
+
e.preventDefault();
|
|
5966
|
+
el.classList.add('bw-file-upload-active');
|
|
5967
|
+
});
|
|
5968
|
+
el.addEventListener('dragleave', function() {
|
|
5969
|
+
el.classList.remove('bw-file-upload-active');
|
|
5970
|
+
});
|
|
5971
|
+
el.addEventListener('drop', function(e) {
|
|
5972
|
+
e.preventDefault();
|
|
5973
|
+
el.classList.remove('bw-file-upload-active');
|
|
5974
|
+
if (onFiles && e.dataTransfer.files.length) onFiles(e.dataTransfer.files);
|
|
5975
|
+
});
|
|
5976
|
+
}
|
|
5977
|
+
}
|
|
5978
|
+
};
|
|
5979
|
+
}
|
|
5980
|
+
|
|
5981
|
+
// =========================================================================
|
|
5982
|
+
// Phase 7: Data Display & Workflow
|
|
5983
|
+
// =========================================================================
|
|
5984
|
+
|
|
5985
|
+
/**
|
|
5986
|
+
* Create a vertical timeline for chronological event display
|
|
5987
|
+
*
|
|
5988
|
+
* Renders events as a vertical line with markers and content cards.
|
|
5989
|
+
* Each item can have a colored variant marker.
|
|
5990
|
+
*
|
|
5991
|
+
* @param {Object} [props] - Timeline configuration
|
|
5992
|
+
* @param {Array<Object>} [props.items=[]] - Timeline events
|
|
5993
|
+
* @param {string} [props.items[].title] - Event title
|
|
5994
|
+
* @param {string|Object|Array} [props.items[].content] - Event description content
|
|
5995
|
+
* @param {string} [props.items[].date] - Date or time label
|
|
5996
|
+
* @param {string} [props.items[].variant="primary"] - Marker color variant
|
|
5997
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
5998
|
+
* @returns {Object} TACO object representing a timeline
|
|
5999
|
+
* @category Component Builders
|
|
6000
|
+
* @example
|
|
6001
|
+
* const timeline = makeTimeline({
|
|
6002
|
+
* items: [
|
|
6003
|
+
* { title: 'Project Started', date: 'Jan 2026', variant: 'primary' },
|
|
6004
|
+
* { title: 'Beta Release', date: 'Mar 2026', content: 'v2.0 beta shipped' },
|
|
6005
|
+
* { title: 'Stable Release', date: 'Jun 2026', variant: 'success' }
|
|
6006
|
+
* ]
|
|
6007
|
+
* });
|
|
6008
|
+
*/
|
|
6009
|
+
function makeTimeline(props = {}) {
|
|
6010
|
+
var {
|
|
6011
|
+
items = [],
|
|
6012
|
+
className = ''
|
|
6013
|
+
} = props;
|
|
6014
|
+
|
|
6015
|
+
return {
|
|
6016
|
+
t: 'div',
|
|
6017
|
+
a: { class: ('bw-timeline ' + className).trim() },
|
|
6018
|
+
c: items.map(function(item) {
|
|
6019
|
+
return {
|
|
6020
|
+
t: 'div',
|
|
6021
|
+
a: { class: 'bw-timeline-item' },
|
|
6022
|
+
c: [
|
|
6023
|
+
{
|
|
6024
|
+
t: 'div',
|
|
6025
|
+
a: { class: 'bw-timeline-marker bw-timeline-marker-' + (item.variant || 'primary') }
|
|
6026
|
+
},
|
|
6027
|
+
{
|
|
6028
|
+
t: 'div',
|
|
6029
|
+
a: { class: 'bw-timeline-content' },
|
|
6030
|
+
c: [
|
|
6031
|
+
item.date && {
|
|
6032
|
+
t: 'div',
|
|
6033
|
+
a: { class: 'bw-timeline-date' },
|
|
6034
|
+
c: item.date
|
|
6035
|
+
},
|
|
6036
|
+
item.title && {
|
|
6037
|
+
t: 'h5',
|
|
6038
|
+
a: { class: 'bw-timeline-title' },
|
|
6039
|
+
c: item.title
|
|
6040
|
+
},
|
|
6041
|
+
item.content && (typeof item.content === 'string'
|
|
6042
|
+
? { t: 'p', a: { class: 'bw-timeline-text' }, c: item.content }
|
|
6043
|
+
: item.content)
|
|
6044
|
+
].filter(Boolean)
|
|
6045
|
+
}
|
|
6046
|
+
]
|
|
6047
|
+
};
|
|
6048
|
+
}),
|
|
6049
|
+
o: { type: 'timeline' }
|
|
6050
|
+
};
|
|
6051
|
+
}
|
|
6052
|
+
|
|
6053
|
+
/**
|
|
6054
|
+
* Create a multi-step wizard/progress indicator
|
|
6055
|
+
*
|
|
6056
|
+
* Displays numbered steps with active and completed states.
|
|
6057
|
+
* Steps before currentStep are marked completed, the currentStep
|
|
6058
|
+
* is active, and subsequent steps are pending.
|
|
6059
|
+
*
|
|
6060
|
+
* @param {Object} [props] - Stepper configuration
|
|
6061
|
+
* @param {Array<Object>} [props.steps=[]] - Step definitions
|
|
6062
|
+
* @param {string} [props.steps[].label] - Step label text
|
|
6063
|
+
* @param {string} [props.steps[].description] - Optional step description
|
|
6064
|
+
* @param {number} [props.currentStep=0] - Zero-based index of the active step
|
|
6065
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
6066
|
+
* @returns {Object} TACO object representing a stepper
|
|
6067
|
+
* @category Component Builders
|
|
6068
|
+
* @example
|
|
6069
|
+
* const stepper = makeStepper({
|
|
6070
|
+
* currentStep: 1,
|
|
6071
|
+
* steps: [
|
|
6072
|
+
* { label: 'Account', description: 'Create account' },
|
|
6073
|
+
* { label: 'Profile', description: 'Set up profile' },
|
|
6074
|
+
* { label: 'Confirm', description: 'Review & submit' }
|
|
6075
|
+
* ]
|
|
6076
|
+
* });
|
|
6077
|
+
*/
|
|
6078
|
+
function makeStepper(props = {}) {
|
|
6079
|
+
var {
|
|
6080
|
+
steps = [],
|
|
6081
|
+
currentStep = 0,
|
|
6082
|
+
className = ''
|
|
6083
|
+
} = props;
|
|
6084
|
+
|
|
6085
|
+
return {
|
|
6086
|
+
t: 'div',
|
|
6087
|
+
a: { class: ('bw-stepper ' + className).trim(), role: 'list' },
|
|
6088
|
+
c: steps.map(function(step, index) {
|
|
6089
|
+
var state = index < currentStep ? 'completed' : index === currentStep ? 'active' : 'pending';
|
|
6090
|
+
return {
|
|
6091
|
+
t: 'div',
|
|
6092
|
+
a: {
|
|
6093
|
+
class: 'bw-step bw-step-' + state,
|
|
6094
|
+
role: 'listitem',
|
|
6095
|
+
'aria-current': state === 'active' ? 'step' : undefined
|
|
6096
|
+
},
|
|
6097
|
+
c: [
|
|
6098
|
+
{
|
|
6099
|
+
t: 'div',
|
|
6100
|
+
a: { class: 'bw-step-indicator' },
|
|
6101
|
+
c: state === 'completed' ? '\u2713' : '' + (index + 1)
|
|
6102
|
+
},
|
|
6103
|
+
{
|
|
6104
|
+
t: 'div',
|
|
6105
|
+
a: { class: 'bw-step-body' },
|
|
6106
|
+
c: [
|
|
6107
|
+
{ t: 'div', a: { class: 'bw-step-label' }, c: step.label },
|
|
6108
|
+
step.description && { t: 'div', a: { class: 'bw-step-description' }, c: step.description }
|
|
6109
|
+
].filter(Boolean)
|
|
6110
|
+
}
|
|
6111
|
+
]
|
|
6112
|
+
};
|
|
6113
|
+
}),
|
|
6114
|
+
o: { type: 'stepper' }
|
|
6115
|
+
};
|
|
6116
|
+
}
|
|
6117
|
+
|
|
6118
|
+
/**
|
|
6119
|
+
* Create a chip/tag input for managing a list of items
|
|
6120
|
+
*
|
|
6121
|
+
* Displays existing chips with remove buttons and an input field
|
|
6122
|
+
* for adding new ones. Chips are added on Enter and removed on
|
|
6123
|
+
* clicking the × button.
|
|
6124
|
+
*
|
|
6125
|
+
* @param {Object} [props] - Chip input configuration
|
|
6126
|
+
* @param {Array<string>} [props.chips=[]] - Initial chip values
|
|
6127
|
+
* @param {string} [props.placeholder="Add..."] - Input placeholder text
|
|
6128
|
+
* @param {Function} [props.onAdd] - Callback when a chip is added, receives value
|
|
6129
|
+
* @param {Function} [props.onRemove] - Callback when a chip is removed, receives value
|
|
6130
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
6131
|
+
* @returns {Object} TACO object representing a chip input
|
|
6132
|
+
* @category Component Builders
|
|
6133
|
+
* @example
|
|
6134
|
+
* const tags = makeChipInput({
|
|
6135
|
+
* chips: ['JavaScript', 'CSS'],
|
|
6136
|
+
* placeholder: 'Add tag...',
|
|
6137
|
+
* onAdd: (val) => addTag(val),
|
|
6138
|
+
* onRemove: (val) => removeTag(val)
|
|
6139
|
+
* });
|
|
6140
|
+
*/
|
|
6141
|
+
function makeChipInput(props = {}) {
|
|
6142
|
+
var {
|
|
6143
|
+
chips = [],
|
|
6144
|
+
placeholder = 'Add...',
|
|
6145
|
+
onAdd,
|
|
6146
|
+
onRemove,
|
|
6147
|
+
className = ''
|
|
6148
|
+
} = props;
|
|
6149
|
+
|
|
6150
|
+
function makeChipEl(text) {
|
|
6151
|
+
return {
|
|
6152
|
+
t: 'span',
|
|
6153
|
+
a: { class: 'bw-chip', 'data-chip-value': text },
|
|
6154
|
+
c: [
|
|
6155
|
+
text,
|
|
6156
|
+
{
|
|
6157
|
+
t: 'button',
|
|
6158
|
+
a: {
|
|
6159
|
+
type: 'button',
|
|
6160
|
+
class: 'bw-chip-remove',
|
|
6161
|
+
'aria-label': 'Remove ' + text,
|
|
6162
|
+
onclick: function(e) {
|
|
6163
|
+
var chip = e.target.closest('.bw-chip');
|
|
6164
|
+
var val = chip.getAttribute('data-chip-value');
|
|
6165
|
+
chip.parentNode.removeChild(chip);
|
|
6166
|
+
if (onRemove) onRemove(val);
|
|
6167
|
+
}
|
|
6168
|
+
},
|
|
6169
|
+
c: '\u00D7'
|
|
6170
|
+
}
|
|
6171
|
+
]
|
|
6172
|
+
};
|
|
6173
|
+
}
|
|
6174
|
+
|
|
6175
|
+
return {
|
|
6176
|
+
t: 'div',
|
|
6177
|
+
a: { class: ('bw-chip-input ' + className).trim() },
|
|
6178
|
+
c: [
|
|
6179
|
+
...chips.map(makeChipEl),
|
|
6180
|
+
{
|
|
6181
|
+
t: 'input',
|
|
6182
|
+
a: {
|
|
6183
|
+
type: 'text',
|
|
6184
|
+
class: 'bw-chip-field',
|
|
6185
|
+
placeholder: placeholder,
|
|
6186
|
+
onkeydown: function(e) {
|
|
6187
|
+
if (e.key === 'Enter' && e.target.value.trim()) {
|
|
6188
|
+
e.preventDefault();
|
|
6189
|
+
var val = e.target.value.trim();
|
|
6190
|
+
var wrapper = e.target.closest('.bw-chip-input');
|
|
6191
|
+
// Insert chip before the input
|
|
6192
|
+
var chipEl = document.createElement('span');
|
|
6193
|
+
chipEl.className = 'bw-chip';
|
|
6194
|
+
chipEl.setAttribute('data-chip-value', val);
|
|
6195
|
+
chipEl.innerHTML = '';
|
|
6196
|
+
chipEl.textContent = val;
|
|
6197
|
+
var removeBtn = document.createElement('button');
|
|
6198
|
+
removeBtn.type = 'button';
|
|
6199
|
+
removeBtn.className = 'bw-chip-remove';
|
|
6200
|
+
removeBtn.setAttribute('aria-label', 'Remove ' + val);
|
|
6201
|
+
removeBtn.textContent = '\u00D7';
|
|
6202
|
+
removeBtn.onclick = function() {
|
|
6203
|
+
chipEl.parentNode.removeChild(chipEl);
|
|
6204
|
+
if (onRemove) onRemove(val);
|
|
6205
|
+
};
|
|
6206
|
+
chipEl.appendChild(removeBtn);
|
|
6207
|
+
wrapper.insertBefore(chipEl, e.target);
|
|
6208
|
+
e.target.value = '';
|
|
6209
|
+
if (onAdd) onAdd(val);
|
|
6210
|
+
}
|
|
6211
|
+
// Backspace on empty input removes last chip
|
|
6212
|
+
if (e.key === 'Backspace' && !e.target.value) {
|
|
6213
|
+
var wrapper = e.target.closest('.bw-chip-input');
|
|
6214
|
+
var chipEls = wrapper.querySelectorAll('.bw-chip');
|
|
6215
|
+
if (chipEls.length) {
|
|
6216
|
+
var last = chipEls[chipEls.length - 1];
|
|
6217
|
+
var removedVal = last.getAttribute('data-chip-value');
|
|
6218
|
+
last.parentNode.removeChild(last);
|
|
6219
|
+
if (onRemove) onRemove(removedVal);
|
|
6220
|
+
}
|
|
6221
|
+
}
|
|
6222
|
+
}
|
|
6223
|
+
}
|
|
6224
|
+
}
|
|
6225
|
+
],
|
|
6226
|
+
o: { type: 'chip-input' }
|
|
6227
|
+
};
|
|
6228
|
+
}
|
|
6229
|
+
|
|
6230
|
+
const componentHandles = {
|
|
6231
|
+
card: CardHandle,
|
|
6232
|
+
table: TableHandle,
|
|
6233
|
+
navbar: NavbarHandle,
|
|
6234
|
+
tabs: TabsHandle,
|
|
6235
|
+
modal: ModalHandle
|
|
6236
|
+
};
|
|
6237
|
+
|
|
6238
|
+
var components = /*#__PURE__*/Object.freeze({
|
|
6239
|
+
__proto__: null,
|
|
6240
|
+
CardHandle: CardHandle,
|
|
6241
|
+
ModalHandle: ModalHandle,
|
|
6242
|
+
NavbarHandle: NavbarHandle,
|
|
6243
|
+
TableHandle: TableHandle,
|
|
6244
|
+
TabsHandle: TabsHandle,
|
|
6245
|
+
componentHandles: componentHandles,
|
|
6246
|
+
makeAccordion: makeAccordion,
|
|
6247
|
+
makeAlert: makeAlert,
|
|
6248
|
+
makeAvatar: makeAvatar,
|
|
6249
|
+
makeBadge: makeBadge,
|
|
6250
|
+
makeBreadcrumb: makeBreadcrumb,
|
|
6251
|
+
makeButton: makeButton,
|
|
6252
|
+
makeButtonGroup: makeButtonGroup,
|
|
6253
|
+
makeCTA: makeCTA,
|
|
6254
|
+
makeCard: makeCard,
|
|
6255
|
+
makeCarousel: makeCarousel,
|
|
6256
|
+
makeCheckbox: makeCheckbox,
|
|
6257
|
+
makeChipInput: makeChipInput,
|
|
6258
|
+
makeCodeDemo: makeCodeDemo,
|
|
6259
|
+
makeCol: makeCol,
|
|
6260
|
+
makeContainer: makeContainer,
|
|
6261
|
+
makeDropdown: makeDropdown,
|
|
6262
|
+
makeFeatureGrid: makeFeatureGrid,
|
|
6263
|
+
makeFileUpload: makeFileUpload,
|
|
6264
|
+
makeForm: makeForm,
|
|
6265
|
+
makeFormGroup: makeFormGroup,
|
|
6266
|
+
makeHero: makeHero,
|
|
6267
|
+
makeInput: makeInput,
|
|
6268
|
+
makeListGroup: makeListGroup,
|
|
6269
|
+
makeMediaObject: makeMediaObject,
|
|
6270
|
+
makeModal: makeModal,
|
|
6271
|
+
makeNav: makeNav,
|
|
6272
|
+
makeNavbar: makeNavbar,
|
|
6273
|
+
makePagination: makePagination,
|
|
6274
|
+
makePopover: makePopover,
|
|
6275
|
+
makeProgress: makeProgress,
|
|
6276
|
+
makeRadio: makeRadio,
|
|
6277
|
+
makeRange: makeRange,
|
|
6278
|
+
makeRow: makeRow,
|
|
6279
|
+
makeSearchInput: makeSearchInput,
|
|
6280
|
+
makeSection: makeSection,
|
|
6281
|
+
makeSelect: makeSelect,
|
|
6282
|
+
makeSkeleton: makeSkeleton,
|
|
6283
|
+
makeSpinner: makeSpinner,
|
|
6284
|
+
makeStack: makeStack,
|
|
6285
|
+
makeStatCard: makeStatCard,
|
|
6286
|
+
makeStepper: makeStepper,
|
|
6287
|
+
makeSwitch: makeSwitch,
|
|
6288
|
+
makeTabs: makeTabs,
|
|
6289
|
+
makeTextarea: makeTextarea,
|
|
6290
|
+
makeTimeline: makeTimeline,
|
|
6291
|
+
makeToast: makeToast,
|
|
6292
|
+
makeTooltip: makeTooltip
|
|
6293
|
+
});
|
|
6294
|
+
|
|
6295
|
+
/**
|
|
6296
|
+
* Bitwrench v2 Core
|
|
6297
|
+
* Zero-dependency UI library using JavaScript objects
|
|
6298
|
+
* Works in browsers (IE11+) and Node.js
|
|
6299
|
+
*
|
|
6300
|
+
* @license BSD-2-Clause
|
|
6301
|
+
* @author M A Chatterjee <deftio [at] deftio [dot] com>
|
|
6302
|
+
*/
|
|
6303
|
+
|
|
6304
|
+
|
|
6305
|
+
// Environment-aware module loader for optional Node.js built-ins (fs).
|
|
6306
|
+
// Strategy: try require() first (CJS/UMD), fall back to import() (ESM).
|
|
6307
|
+
// import() is wrapped in Function() to avoid parse errors in ES5/IE11 environments.
|
|
6308
|
+
|
|
6309
|
+
// Core bitwrench namespace
|
|
6310
|
+
const bw = {
|
|
6311
|
+
// Version info from generated file
|
|
6312
|
+
version: VERSION_INFO.version,
|
|
6313
|
+
versionInfo: VERSION_INFO,
|
|
6314
|
+
|
|
6315
|
+
/**
|
|
5082
6316
|
* Get version metadata object (v1-compatible callable API).
|
|
5083
6317
|
*
|
|
5084
6318
|
* Returns a copy of the build-time version info including version string,
|
|
@@ -6676,8 +7910,10 @@
|
|
|
6676
7910
|
/**
|
|
6677
7911
|
* Generate responsive CSS with media query breakpoints.
|
|
6678
7912
|
*
|
|
6679
|
-
* Produces a CSS string with `@media` rules for
|
|
6680
|
-
*
|
|
7913
|
+
* Produces a CSS string with `@media (min-width)` rules for standard
|
|
7914
|
+
* breakpoints. These match the grid system and theme.breakpoints:
|
|
7915
|
+
* sm: 576px, md: 768px, lg: 992px, xl: 1200px
|
|
7916
|
+
* Pass the result to `bw.injectCSS()`.
|
|
6681
7917
|
*
|
|
6682
7918
|
* @param {string} selector - CSS selector
|
|
6683
7919
|
* @param {Object} breakpoints - Object with keys: base, sm, md, lg, xl
|
|
@@ -6694,7 +7930,7 @@
|
|
|
6694
7930
|
* bw.injectCSS(css);
|
|
6695
7931
|
*/
|
|
6696
7932
|
bw.responsive = function(selector, breakpoints) {
|
|
6697
|
-
var sizes = { sm: '
|
|
7933
|
+
var sizes = { sm: '576px', md: '768px', lg: '992px', xl: '1200px' };
|
|
6698
7934
|
var parts = [];
|
|
6699
7935
|
Object.keys(breakpoints).forEach(function(key) {
|
|
6700
7936
|
var rules = {};
|
|
@@ -6828,7 +8064,8 @@
|
|
|
6828
8064
|
* @returns {Element|null} Style element if in browser, null in Node.js
|
|
6829
8065
|
* @category CSS & Styling
|
|
6830
8066
|
* @see bw.setTheme
|
|
6831
|
-
* @see bw.
|
|
8067
|
+
* @see bw.applyTheme
|
|
8068
|
+
* @see bw.toggleTheme
|
|
6832
8069
|
* @example
|
|
6833
8070
|
* bw.loadDefaultStyles(); // inject all default CSS
|
|
6834
8071
|
*/
|
|
@@ -6895,53 +8132,6 @@
|
|
|
6895
8132
|
return bw.getTheme();
|
|
6896
8133
|
};
|
|
6897
8134
|
|
|
6898
|
-
/**
|
|
6899
|
-
* Toggle dark mode on/off.
|
|
6900
|
-
*
|
|
6901
|
-
* Adds/removes the `bw-dark` class on `<html>` and injects dark mode CSS
|
|
6902
|
-
* overrides. Pass `true`/`false` to force a mode, or omit to toggle.
|
|
6903
|
-
*
|
|
6904
|
-
* @param {boolean} [force] - Force dark (true) or light (false). Omit to toggle.
|
|
6905
|
-
* @returns {boolean} Whether dark mode is now active
|
|
6906
|
-
* @category CSS & Styling
|
|
6907
|
-
* @see bw.setTheme
|
|
6908
|
-
* @example
|
|
6909
|
-
* bw.toggleDarkMode(); // toggle
|
|
6910
|
-
* bw.toggleDarkMode(true); // force dark
|
|
6911
|
-
* bw.toggleDarkMode(false); // force light
|
|
6912
|
-
*/
|
|
6913
|
-
bw.toggleDarkMode = function(force) {
|
|
6914
|
-
const isDark = force !== undefined ? force : !theme.darkMode;
|
|
6915
|
-
theme.darkMode = isDark;
|
|
6916
|
-
|
|
6917
|
-
if (bw._isBrowser) {
|
|
6918
|
-
const root = document.documentElement;
|
|
6919
|
-
if (isDark) {
|
|
6920
|
-
root.classList.add('bw-dark');
|
|
6921
|
-
// Generate palette-aware dark mode CSS, or fall back to default
|
|
6922
|
-
var palette = bw._activePalette || derivePalette(DEFAULT_PALETTE_CONFIG);
|
|
6923
|
-
var darkRules = generateDarkModeCSS(palette);
|
|
6924
|
-
var darkCSS = bw.css(darkRules);
|
|
6925
|
-
|
|
6926
|
-
// Remove existing dark styles to allow regeneration
|
|
6927
|
-
var existing = document.getElementById('bw-dark-styles');
|
|
6928
|
-
if (existing) existing.remove();
|
|
6929
|
-
|
|
6930
|
-
var styleEl = document.createElement('style');
|
|
6931
|
-
styleEl.id = 'bw-dark-styles';
|
|
6932
|
-
styleEl.textContent = darkCSS;
|
|
6933
|
-
document.head.appendChild(styleEl);
|
|
6934
|
-
} else {
|
|
6935
|
-
root.classList.remove('bw-dark');
|
|
6936
|
-
// Remove dark mode styles when switching to light
|
|
6937
|
-
var darkEl = document.getElementById('bw-dark-styles');
|
|
6938
|
-
if (darkEl) darkEl.remove();
|
|
6939
|
-
}
|
|
6940
|
-
}
|
|
6941
|
-
|
|
6942
|
-
return isDark;
|
|
6943
|
-
};
|
|
6944
|
-
|
|
6945
8135
|
/**
|
|
6946
8136
|
* Generate a complete, scoped theme from seed colors.
|
|
6947
8137
|
*
|
|
@@ -6964,13 +8154,19 @@
|
|
|
6964
8154
|
* @param {string} [config.spacing='normal'] - 'compact' | 'normal' | 'spacious'
|
|
6965
8155
|
* @param {string} [config.radius='md'] - 'none' | 'sm' | 'md' | 'lg' | 'pill'
|
|
6966
8156
|
* @param {number} [config.fontSize=1.0] - Base font size scale factor
|
|
8157
|
+
* @param {string|number} [config.typeRatio='normal'] - 'tight' | 'normal' | 'relaxed' | 'dramatic' or a number
|
|
8158
|
+
* @param {string} [config.elevation='md'] - 'flat' | 'sm' | 'md' | 'lg'
|
|
8159
|
+
* @param {string} [config.motion='standard'] - 'reduced' | 'standard' | 'expressive'
|
|
8160
|
+
* @param {number} [config.harmonize=0.20] - 0-1, semantic color hue shift toward primary
|
|
6967
8161
|
* @param {boolean} [config.inject=true] - Inject into DOM (browser only)
|
|
6968
|
-
* @returns {Object} { css, palette, name }
|
|
8162
|
+
* @returns {Object} { css, palette, name, isLightPrimary, alternate: { css, palette } }
|
|
6969
8163
|
* @category CSS & Styling
|
|
8164
|
+
* @see bw.applyTheme
|
|
8165
|
+
* @see bw.toggleTheme
|
|
6970
8166
|
* @see bw.loadDefaultStyles
|
|
6971
8167
|
* @example
|
|
6972
|
-
* // Generate and inject an ocean theme
|
|
6973
|
-
* bw.generateTheme('ocean', {
|
|
8168
|
+
* // Generate and inject an ocean theme (primary + alternate)
|
|
8169
|
+
* var theme = bw.generateTheme('ocean', {
|
|
6974
8170
|
* primary: '#0077b6',
|
|
6975
8171
|
* secondary: '#90e0ef',
|
|
6976
8172
|
* tertiary: '#00b4d8'
|
|
@@ -6979,14 +8175,16 @@
|
|
|
6979
8175
|
* // Apply to a container
|
|
6980
8176
|
* document.getElementById('app').classList.add('ocean');
|
|
6981
8177
|
*
|
|
8178
|
+
* // Toggle to alternate palette
|
|
8179
|
+
* bw.toggleTheme();
|
|
8180
|
+
*
|
|
6982
8181
|
* // Generate CSS for static export (Node.js)
|
|
6983
8182
|
* var result = bw.generateTheme('sunset', {
|
|
6984
8183
|
* primary: '#e76f51',
|
|
6985
8184
|
* secondary: '#264653',
|
|
6986
|
-
* tertiary: '#e9c46a',
|
|
6987
8185
|
* inject: false
|
|
6988
8186
|
* });
|
|
6989
|
-
* fs.writeFileSync('sunset.css', result.css);
|
|
8187
|
+
* fs.writeFileSync('sunset.css', result.css + result.alternate.css);
|
|
6990
8188
|
*/
|
|
6991
8189
|
bw.generateTheme = function(name, config) {
|
|
6992
8190
|
if (!config || !config.primary || !config.secondary) {
|
|
@@ -6997,29 +8195,37 @@
|
|
|
6997
8195
|
var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config);
|
|
6998
8196
|
if (!config.tertiary) fullConfig.tertiary = fullConfig.primary;
|
|
6999
8197
|
|
|
7000
|
-
// Derive palette
|
|
8198
|
+
// Derive primary palette
|
|
7001
8199
|
var palette = derivePalette(fullConfig);
|
|
7002
8200
|
|
|
7003
|
-
// Store active palette for dark mode
|
|
7004
|
-
bw._activePalette = palette;
|
|
7005
|
-
|
|
7006
8201
|
// Resolve layout
|
|
7007
8202
|
var layout = resolveLayout(fullConfig);
|
|
7008
8203
|
|
|
7009
|
-
// Generate themed CSS rules
|
|
8204
|
+
// Generate primary themed CSS rules
|
|
7010
8205
|
var themedRules = generateThemedCSS(name, palette, layout);
|
|
7011
|
-
|
|
7012
|
-
// Add underscore aliases
|
|
7013
8206
|
var aliasedRules = addUnderscoreAliases(themedRules);
|
|
7014
|
-
|
|
7015
|
-
// Convert to CSS string
|
|
7016
8207
|
var cssStr = bw.css(aliasedRules);
|
|
7017
8208
|
|
|
7018
|
-
//
|
|
8209
|
+
// Derive alternate palette (luminance-inverted)
|
|
8210
|
+
var altConfig = deriveAlternateConfig(fullConfig);
|
|
8211
|
+
var altPalette = derivePalette(altConfig);
|
|
8212
|
+
|
|
8213
|
+
// Generate alternate CSS scoped under .bw-theme-alt
|
|
8214
|
+
var altRules = generateAlternateCSS(name, altPalette, layout);
|
|
8215
|
+
var aliasedAltRules = addUnderscoreAliases(altRules);
|
|
8216
|
+
var altCssStr = bw.css(aliasedAltRules);
|
|
8217
|
+
|
|
8218
|
+
// Determine if primary is light-flavored
|
|
8219
|
+
var lightPrimary = isLightPalette(fullConfig);
|
|
8220
|
+
|
|
8221
|
+
// Inject both CSS sets into DOM if requested
|
|
7019
8222
|
var shouldInject = config.inject !== false;
|
|
7020
8223
|
if (shouldInject && bw._isBrowser) {
|
|
7021
8224
|
var styleId = name ? 'bw-theme-' + name : 'bw-theme-default';
|
|
7022
8225
|
bw.injectCSS(cssStr, { id: styleId, append: false });
|
|
8226
|
+
|
|
8227
|
+
var altStyleId = name ? 'bw-theme-' + name + '-alt' : 'bw-theme-default-alt';
|
|
8228
|
+
bw.injectCSS(altCssStr, { id: altStyleId, append: false });
|
|
7023
8229
|
}
|
|
7024
8230
|
|
|
7025
8231
|
// Update bw.u color entries to reflect the palette
|
|
@@ -7030,7 +8236,72 @@
|
|
|
7030
8236
|
bw.u.textWhite = { color: '#ffffff' };
|
|
7031
8237
|
}
|
|
7032
8238
|
|
|
7033
|
-
|
|
8239
|
+
// Store active theme state
|
|
8240
|
+
var result = {
|
|
8241
|
+
css: cssStr,
|
|
8242
|
+
palette: palette,
|
|
8243
|
+
name: name,
|
|
8244
|
+
isLightPrimary: lightPrimary,
|
|
8245
|
+
alternate: {
|
|
8246
|
+
css: altCssStr,
|
|
8247
|
+
palette: altPalette
|
|
8248
|
+
}
|
|
8249
|
+
};
|
|
8250
|
+
bw._activeTheme = result;
|
|
8251
|
+
bw._activeThemeMode = 'primary';
|
|
8252
|
+
|
|
8253
|
+
return result;
|
|
8254
|
+
};
|
|
8255
|
+
|
|
8256
|
+
/**
|
|
8257
|
+
* Apply a theme mode. Switches between primary and alternate palettes
|
|
8258
|
+
* by adding/removing the `bw-theme-alt` class on `<html>`.
|
|
8259
|
+
*
|
|
8260
|
+
* @param {string} mode - 'primary' | 'alternate' | 'light' | 'dark'
|
|
8261
|
+
* @returns {string} Active mode: 'primary' or 'alternate'
|
|
8262
|
+
* @category CSS & Styling
|
|
8263
|
+
* @see bw.generateTheme
|
|
8264
|
+
* @see bw.toggleTheme
|
|
8265
|
+
* @example
|
|
8266
|
+
* bw.applyTheme('alternate'); // switch to alternate palette
|
|
8267
|
+
* bw.applyTheme('dark'); // switch to whichever palette is darker
|
|
8268
|
+
* bw.applyTheme('primary'); // switch back to primary palette
|
|
8269
|
+
*/
|
|
8270
|
+
bw.applyTheme = function(mode) {
|
|
8271
|
+
if (!bw._isBrowser) return mode || 'primary';
|
|
8272
|
+
var root = document.documentElement;
|
|
8273
|
+
var isLight = bw._activeTheme ? bw._activeTheme.isLightPrimary : true;
|
|
8274
|
+
|
|
8275
|
+
var wantAlt;
|
|
8276
|
+
if (mode === 'primary') wantAlt = false;
|
|
8277
|
+
else if (mode === 'alternate') wantAlt = true;
|
|
8278
|
+
else if (mode === 'light') wantAlt = !isLight;
|
|
8279
|
+
else if (mode === 'dark') wantAlt = isLight;
|
|
8280
|
+
else wantAlt = false;
|
|
8281
|
+
|
|
8282
|
+
if (wantAlt) {
|
|
8283
|
+
root.classList.add('bw-theme-alt');
|
|
8284
|
+
} else {
|
|
8285
|
+
root.classList.remove('bw-theme-alt');
|
|
8286
|
+
}
|
|
8287
|
+
|
|
8288
|
+
bw._activeThemeMode = wantAlt ? 'alternate' : 'primary';
|
|
8289
|
+
return bw._activeThemeMode;
|
|
8290
|
+
};
|
|
8291
|
+
|
|
8292
|
+
/**
|
|
8293
|
+
* Toggle between primary and alternate theme palettes.
|
|
8294
|
+
*
|
|
8295
|
+
* @returns {string} Active mode after toggle: 'primary' or 'alternate'
|
|
8296
|
+
* @category CSS & Styling
|
|
8297
|
+
* @see bw.applyTheme
|
|
8298
|
+
* @see bw.generateTheme
|
|
8299
|
+
* @example
|
|
8300
|
+
* bw.toggleTheme(); // flip between primary and alternate
|
|
8301
|
+
*/
|
|
8302
|
+
bw.toggleTheme = function() {
|
|
8303
|
+
var current = bw._activeThemeMode || 'primary';
|
|
8304
|
+
return bw.applyTheme(current === 'primary' ? 'alternate' : 'primary');
|
|
7034
8305
|
};
|
|
7035
8306
|
|
|
7036
8307
|
// Expose color utility functions on bw namespace
|
|
@@ -7042,10 +8313,18 @@
|
|
|
7042
8313
|
bw.textOnColor = textOnColor;
|
|
7043
8314
|
bw.deriveShades = deriveShades;
|
|
7044
8315
|
bw.derivePalette = derivePalette;
|
|
8316
|
+
bw.harmonize = harmonize;
|
|
8317
|
+
bw.deriveAlternateSeed = deriveAlternateSeed;
|
|
8318
|
+
bw.deriveAlternateConfig = deriveAlternateConfig;
|
|
8319
|
+
bw.isLightPalette = isLightPalette;
|
|
7045
8320
|
|
|
7046
8321
|
// Expose layout and theme presets
|
|
7047
8322
|
bw.SPACING_PRESETS = SPACING_PRESETS;
|
|
7048
8323
|
bw.RADIUS_PRESETS = RADIUS_PRESETS;
|
|
8324
|
+
bw.TYPE_RATIO_PRESETS = TYPE_RATIO_PRESETS;
|
|
8325
|
+
bw.ELEVATION_PRESETS = ELEVATION_PRESETS;
|
|
8326
|
+
bw.MOTION_PRESETS = MOTION_PRESETS;
|
|
8327
|
+
bw.generateTypeScale = generateTypeScale;
|
|
7049
8328
|
bw.DEFAULT_PALETTE_CONFIG = DEFAULT_PALETTE_CONFIG;
|
|
7050
8329
|
bw.THEME_PRESETS = THEME_PRESETS;
|
|
7051
8330
|
|
|
@@ -8087,9 +9366,13 @@
|
|
|
8087
9366
|
/**
|
|
8088
9367
|
* Create a sortable TACO table from an array of row objects.
|
|
8089
9368
|
*
|
|
9369
|
+
* Returns a bare `<table>` TACO — no wrapper, title, or responsive scroll.
|
|
9370
|
+
* Use this when you need full control over table placement, or when embedding
|
|
9371
|
+
* the table inside your own layout. For a ready-to-use table with title,
|
|
9372
|
+
* responsive wrapper, and defaults (striped + hover), use `bw.makeDataTable()`.
|
|
9373
|
+
*
|
|
8090
9374
|
* Auto-detects columns from data keys if not specified. Supports click-to-sort
|
|
8091
|
-
* headers with ascending/descending indicators.
|
|
8092
|
-
* render with `bw.DOM()` or `bw.html()`.
|
|
9375
|
+
* headers with ascending/descending indicators.
|
|
8093
9376
|
*
|
|
8094
9377
|
* @param {Object} config - Table configuration
|
|
8095
9378
|
* @param {Array<Object>} config.data - Array of row objects to display
|
|
@@ -8389,10 +9672,12 @@
|
|
|
8389
9672
|
};
|
|
8390
9673
|
|
|
8391
9674
|
/**
|
|
8392
|
-
* Create a
|
|
9675
|
+
* Create a ready-to-use data table with title and responsive wrapper.
|
|
8393
9676
|
*
|
|
8394
|
-
*
|
|
8395
|
-
*
|
|
9677
|
+
* Convenience wrapper around `bw.makeTable()` that adds a title heading,
|
|
9678
|
+
* responsive horizontal scroll container, and defaults to striped + hover.
|
|
9679
|
+
* Use this for the common case; use `bw.makeTable()` when you need a bare
|
|
9680
|
+
* table element with no wrapper.
|
|
8396
9681
|
*
|
|
8397
9682
|
* @param {Object} config - Table configuration
|
|
8398
9683
|
* @param {string} [config.title] - Table title heading
|