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