sc-theme-builder/lib/deepOverlay.js
2026-07-01 20:07:28 -05:00

89 lines
3.9 KiB
JavaScript

// The "deep overlay": emit the per-component --bs-btn-* variables that Bootstrap
// 5.3 bakes at Sass compile time, recomputed from the theme's colors -- so solid
// and outline BUTTONS actually recolor under the CSS-variable overlay (a plain
// :root{--bs-primary} override does NOT recolor .btn-primary, whose colors are
// build-time literals). Mirrors scss/_buttons.scss + mixins/_buttons.scss.
const {
parseHexColor, toHex, toRgbString, mix, shade, tint, colorContrast,
} = require("./bootstrapColor");
// The themeable button variants, in Bootstrap order.
const BTN_VARIANTS = ["primary", "secondary", "success", "info", "warning", "danger", "light", "dark"];
// button-variant hover/active shade & tint amounts (scss/_variables.scss).
const A = {
hoverBgShade: 15, hoverBgTint: 15,
hoverBorderShade: 20, hoverBorderTint: 10,
activeBgShade: 20, activeBgTint: 20,
activeBorderShade: 25, activeBorderTint: 10,
};
// One solid .btn-<name> rule. `name` selects the light/dark force-overrides:
// .btn-light -> always SHADE; .btn-dark -> always TINT; else by text color.
function solidButtonRule(name, color) {
const c = parseHexColor(color);
if (!c) {
return "";
}
const textColor = colorContrast(c); // "#ffffff" | "#000000"
const shadeMode = name === "light" ? true : name === "dark" ? false : textColor === "#ffffff";
const hoverBg = shadeMode ? shade(c, A.hoverBgShade) : tint(c, A.hoverBgTint);
const hoverBorder = shadeMode ? shade(c, A.hoverBorderShade) : tint(c, A.hoverBorderTint);
const activeBg = shadeMode ? shade(c, A.activeBgShade) : tint(c, A.activeBgTint);
const activeBorder = shadeMode ? shade(c, A.activeBorderShade) : tint(c, A.activeBorderTint);
const focusRgb = toRgbString(mix(parseHexColor(textColor), c, 0.15));
const bg = toHex(c);
return `.btn-${name}{`
+ `--bs-btn-color:${textColor};--bs-btn-bg:${bg};--bs-btn-border-color:${bg};`
+ `--bs-btn-hover-color:${colorContrast(hoverBg)};--bs-btn-hover-bg:${toHex(hoverBg)};--bs-btn-hover-border-color:${toHex(hoverBorder)};`
+ `--bs-btn-focus-shadow-rgb:${focusRgb};`
+ `--bs-btn-active-color:${colorContrast(activeBg)};--bs-btn-active-bg:${toHex(activeBg)};--bs-btn-active-border-color:${toHex(activeBorder)};`
+ `--bs-btn-disabled-color:${textColor};--bs-btn-disabled-bg:${bg};--bs-btn-disabled-border-color:${bg};`
+ `}`;
}
// One .btn-outline-<name> rule (button-outline-variant): transparent fill that
// inverts to the variant color on hover/active.
function outlineButtonRule(name, color) {
const c = parseHexColor(color);
if (!c) {
return "";
}
const hex = toHex(c);
const onColor = colorContrast(c);
return `.btn-outline-${name}{`
+ `--bs-btn-color:${hex};--bs-btn-border-color:${hex};`
+ `--bs-btn-hover-color:${onColor};--bs-btn-hover-bg:${hex};--bs-btn-hover-border-color:${hex};`
+ `--bs-btn-focus-shadow-rgb:${toRgbString(c)};`
+ `--bs-btn-active-color:${onColor};--bs-btn-active-bg:${hex};--bs-btn-active-border-color:${hex};`
+ `--bs-btn-disabled-color:${hex};--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:${hex};`
+ `}`;
}
// Emit the full deep-overlay CSS for the given color map { primary, secondary,
// ... }. Variants whose value is not a hex color are skipped (the :root overlay
// still sets --bs-<name> for them).
function emitButtonRules(colors) {
if (!colors || typeof colors !== "object") {
return "";
}
const out = [];
for (const name of BTN_VARIANTS) {
const color = colors[name];
if (!color || !parseHexColor(color)) {
continue;
}
out.push(solidButtonRule(name, color));
out.push(outlineButtonRule(name, color));
}
return out.filter(Boolean).join("\n");
}
module.exports = { emitButtonRules, solidButtonRule, outlineButtonRule, BTN_VARIANTS };