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