mirror of
https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git
synced 2026-09-16 17:16:16 +00:00
fix(brand): stop adjustBrightness collapsing dark color shades to black (#497)
adjustBrightness() added/subtracted a flat 255*percent to every RGB channel. For a dark base color (channels already close to 0), darkening by -0.3/-0.45/-0.6 clamped every channel to 0, so shades 700, 800, and 900 all came back as the identical, useless #000000 instead of a graded dark scale -- reproducible with any sufficiently dark brand primary (e.g. #4A3228), not just an edge case. Blend each channel proportionally toward white (percent > 0) or black (percent < 0) instead, so the shift scales with how much headroom the channel actually has. Adds a regression test that syncs a dark brand color and asserts the 700/800/900 shades stay distinct and non-black. Claude-Session: https://claude.ai/code/session_01MmtTPnoqMrjjhKjod9VTzr Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
7f69fed6a2
commit
d8ce090f82
@ -99,15 +99,34 @@ function generateColorScale(baseHex, darkHex, lightHex) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust hex color brightness
|
||||
* Adjust hex color brightness.
|
||||
*
|
||||
* Blends each channel proportionally toward white (percent > 0) or toward
|
||||
* black (percent < 0) instead of adding/subtracting a flat 255*percent to
|
||||
* every channel. The flat-shift approach clamped all three channels to 0
|
||||
* (or 255) whenever the base color's channels were already low (or high)
|
||||
* relative to the shift — e.g. darkening a dark brand color like #4A3228
|
||||
* by -0.3/-0.45/-0.6 produced #000000 for all three, collapsing shades
|
||||
* 700/800/900 into an identical, useless black.
|
||||
*/
|
||||
function adjustBrightness(hex, percent) {
|
||||
if (typeof hex !== 'string') return '#000000';
|
||||
const num = parseInt(hex.replace('#', ''), 16);
|
||||
const r = Math.min(255, Math.max(0, (num >> 16) + Math.round(255 * percent)));
|
||||
const g = Math.min(255, Math.max(0, ((num >> 8) & 0x00FF) + Math.round(255 * percent)));
|
||||
const b = Math.min(255, Math.max(0, (num & 0x0000FF) + Math.round(255 * percent)));
|
||||
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0').toUpperCase()}`;
|
||||
const r = (num >> 16) & 0xFF;
|
||||
const g = (num >> 8) & 0xFF;
|
||||
const b = num & 0xFF;
|
||||
|
||||
const adjustChannel = (channel) => {
|
||||
const adjusted = percent >= 0
|
||||
? channel + (255 - channel) * percent
|
||||
: channel * (1 + percent);
|
||||
return Math.min(255, Math.max(0, Math.round(adjusted)));
|
||||
};
|
||||
|
||||
const newR = adjustChannel(r);
|
||||
const newG = adjustChannel(g);
|
||||
const newB = adjustChannel(b);
|
||||
return `#${((newR << 16) | (newG << 8) | newB).toString(16).padStart(6, '0').toUpperCase()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -71,6 +71,40 @@ def test_sync_parses_bundled_starter_template(tmp_path):
|
||||
assert css.exists() and css.stat().st_size > 0
|
||||
|
||||
|
||||
def test_dark_base_color_does_not_collapse_shades_to_black(tmp_path):
|
||||
"""adjustBrightness() used to add/subtract a flat 255*percent per channel.
|
||||
|
||||
For a dark base color (channels already close to 0), darkening by
|
||||
-0.3/-0.45/-0.6 clamped every channel to 0, so shades 700, 800, and 900
|
||||
all came back as the identical, useless #000000 instead of a graded dark
|
||||
scale. This runs the sync against a dark, coffee-roastery-style brand
|
||||
color and asserts the three shades stay distinct and non-black.
|
||||
"""
|
||||
(tmp_path / "docs").mkdir()
|
||||
(tmp_path / "assets").mkdir()
|
||||
shutil.copy(TOKENS_STARTER, tmp_path / "assets" / "design-tokens.json")
|
||||
(tmp_path / "docs" / "brand-guidelines.md").write_text(
|
||||
"## Quick Reference\n\n"
|
||||
"| Element | Value |\n"
|
||||
"|---------|-------|\n"
|
||||
"| Primary Color | #4A3228 |\n"
|
||||
"| Secondary Color | #C08A3E |\n"
|
||||
"| Accent Color | #6B8F71 |\n"
|
||||
)
|
||||
|
||||
result = _run(tmp_path)
|
||||
assert result.returncode == 0, result.stderr + result.stdout
|
||||
|
||||
tokens = json.loads((tmp_path / "assets" / "design-tokens.json").read_text())
|
||||
primary = tokens["primitive"]["color"]["primary"]
|
||||
dark_shades = [primary[shade]["$value"] for shade in ("700", "800", "900")]
|
||||
|
||||
assert len(set(dark_shades)) == 3, (
|
||||
f"expected three distinct dark shades, got {dark_shades}"
|
||||
)
|
||||
assert "#000000" not in dark_shades, dark_shades
|
||||
|
||||
|
||||
def test_reports_missing_guidelines_without_breaking_the_harness(tmp_path):
|
||||
"""The missing-guidelines path is the one that breaks a locale-decoded pipe.
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user