From 975cba327eee075d051b538b6aa19c2ab2ce0527 Mon Sep 17 00:00:00 2001 From: notbucki <103531753+notbucki@users.noreply.github.com> Date: Tue, 15 Sep 2026 10:54:29 +0200 Subject: [PATCH] fix(brand): apply #497 to the source copy so check:assets passes again #497 patched cli/assets/skills/brand/scripts/sync-brand-to-tokens.cjs and its test, but sync-assets.mjs mirrors .claude/skills/ into cli/assets/, so the source copy still carried the old adjustBrightness and check:assets reported two stale asset files on main. That check is the first step of both CI workflows, so every PR since then stopped there. Mirror the same two files into the source copy; the diff is byte-identical to #497's. Co-Authored-By: Claude Fable 5.1 --- .../brand/scripts/sync-brand-to-tokens.cjs | 29 +++++++++++++--- .../tests/test_sync_brand_to_tokens.py | 34 +++++++++++++++++++ 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/.claude/skills/brand/scripts/sync-brand-to-tokens.cjs b/.claude/skills/brand/scripts/sync-brand-to-tokens.cjs index 1e3b8ce..75e8430 100644 --- a/.claude/skills/brand/scripts/sync-brand-to-tokens.cjs +++ b/.claude/skills/brand/scripts/sync-brand-to-tokens.cjs @@ -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()}`; } /** diff --git a/.claude/skills/brand/scripts/tests/test_sync_brand_to_tokens.py b/.claude/skills/brand/scripts/tests/test_sync_brand_to_tokens.py index 53c5437..aed7946 100644 --- a/.claude/skills/brand/scripts/tests/test_sync_brand_to_tokens.py +++ b/.claude/skills/brand/scripts/tests/test_sync_brand_to_tokens.py @@ -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.