From 7af392cfbf0bf5792b525f6074c883bb1ab94133 Mon Sep 17 00:00:00 2001 From: Bill Date: Tue, 17 Feb 2026 08:55:32 -0700 Subject: [PATCH] Step 515: add semantic theme token system v2 --- editor/CMakeLists.txt | 9 +++ editor/src/ThemeTokenSystemV2.h | 107 ++++++++++++++++++++++++++++ editor/tests/step515_test.cpp | 121 ++++++++++++++++++++++++++++++++ progress.md | 36 ++++++++++ 4 files changed, 273 insertions(+) create mode 100644 editor/src/ThemeTokenSystemV2.h create mode 100644 editor/tests/step515_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index a7946d2..8a1306a 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3514,4 +3514,13 @@ target_link_libraries(step514_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step515_test tests/step515_test.cpp) +target_include_directories(step515_test PRIVATE src) +target_link_libraries(step515_test PRIVATE + nlohmann_json::nlohmann_json + unofficial::tree-sitter::tree-sitter + tree_sitter_python tree_sitter_cpp tree_sitter_elisp + tree_sitter_javascript tree_sitter_typescript + tree_sitter_java tree_sitter_rust tree_sitter_go) + # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) diff --git a/editor/src/ThemeTokenSystemV2.h b/editor/src/ThemeTokenSystemV2.h new file mode 100644 index 0000000..81b4871 --- /dev/null +++ b/editor/src/ThemeTokenSystemV2.h @@ -0,0 +1,107 @@ +#pragma once +// Step 515: Theme Token System v2 +// Semantic token set with derivation support and contrast checks. + +#include +#include +#include +#include +#include + +struct ThemeColor { + float r = 0.0f; + float g = 0.0f; + float b = 0.0f; + float a = 1.0f; +}; + +struct ThemeTokensV2 { + ThemeColor bgBase; + ThemeColor bgLayer1; + ThemeColor bgLayer2; + ThemeColor textPrimary; + ThemeColor textSecondary; + ThemeColor accentPrimary; + ThemeColor accentDanger; + float radiusSmall = 2.0f; + float radiusMedium = 6.0f; + float borderThin = 1.0f; + float borderThick = 2.0f; + float elevationLow = 2.0f; + float elevationHigh = 8.0f; +}; + +class ThemeTokenSystemV2 { +public: + static ThemeTokensV2 baseBlackStone() { + return { + {0.05f, 0.05f, 0.05f, 1.0f}, + {0.10f, 0.10f, 0.10f, 1.0f}, + {0.16f, 0.16f, 0.16f, 1.0f}, + {0.92f, 0.92f, 0.92f, 1.0f}, + {0.70f, 0.70f, 0.70f, 1.0f}, + {0.15f, 0.56f, 0.95f, 1.0f}, + {0.86f, 0.30f, 0.30f, 1.0f}, + 2.0f, 6.0f, 1.0f, 2.0f, 2.0f, 8.0f + }; + } + + static ThemeTokensV2 deriveContrastVariant(const ThemeTokensV2& in, + float textBoost, + float layerLift) { + ThemeTokensV2 out = in; + out.textPrimary = lighten(out.textPrimary, textBoost); + out.textSecondary = lighten(out.textSecondary, textBoost * 0.7f); + out.bgLayer1 = lighten(out.bgLayer1, layerLift); + out.bgLayer2 = lighten(out.bgLayer2, layerLift); + return out; + } + + static std::map spacingScale(float base = 4.0f) { + return { + {"xs", base}, + {"sm", base * 2.0f}, + {"md", base * 3.0f}, + {"lg", base * 4.0f}, + {"xl", base * 6.0f} + }; + } + + static bool hasHardcodedPurple(const ThemeTokensV2& t) { + // Reject old default purple bias in primary accents. + return (t.accentPrimary.r > 0.40f && t.accentPrimary.b > 0.70f); + } + + static float contrastTextVsBg(const ThemeColor& text, const ThemeColor& bg) { + float l1 = luminance(text); + float l2 = luminance(bg); + if (l1 < l2) std::swap(l1, l2); + return (l1 + 0.05f) / (l2 + 0.05f); + } + + static bool validateAA(const ThemeTokensV2& t) { + return contrastTextVsBg(t.textPrimary, t.bgBase) >= 4.5f && + contrastTextVsBg(t.textSecondary, t.bgLayer1) >= 3.0f; + } + +private: + static ThemeColor lighten(ThemeColor c, float delta) { + c.r = clamp01(c.r + delta); + c.g = clamp01(c.g + delta); + c.b = clamp01(c.b + delta); + return c; + } + + static float clamp01(float v) { + return std::max(0.0f, std::min(1.0f, v)); + } + + static float linear(float c) { + if (c <= 0.03928f) return c / 12.92f; + return std::pow((c + 0.055f) / 1.055f, 2.4f); + } + + static float luminance(const ThemeColor& c) { + return 0.2126f * linear(c.r) + 0.7152f * linear(c.g) + 0.0722f * linear(c.b); + } +}; diff --git a/editor/tests/step515_test.cpp b/editor/tests/step515_test.cpp new file mode 100644 index 0000000..999e7e5 --- /dev/null +++ b/editor/tests/step515_test.cpp @@ -0,0 +1,121 @@ +// Step 515: Theme Token System v2 (12 tests) + +#include "ThemeTokenSystemV2.h" + +#include + +static int passed = 0, failed = 0; +#define TEST(name) { std::cout << " " << #name << "... "; } +#define PASS() { std::cout << "PASS\n"; ++passed; } +#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; } +#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {} + +void test_base_theme_uses_black_stone_layers() { + TEST(base_theme_uses_black_stone_layers); + auto t = ThemeTokenSystemV2::baseBlackStone(); + CHECK(t.bgBase.r < 0.1f && t.bgLayer2.r > t.bgBase.r, "expected layered dark theme"); + PASS(); +} + +void test_spacing_scale_has_expected_keys() { + TEST(spacing_scale_has_expected_keys); + auto s = ThemeTokenSystemV2::spacingScale(); + CHECK(s.count("xs") && s.count("md") && s.count("xl"), "missing spacing keys"); + PASS(); +} + +void test_spacing_scale_is_monotonic() { + TEST(spacing_scale_is_monotonic); + auto s = ThemeTokenSystemV2::spacingScale(); + CHECK(s["xs"] < s["sm"] && s["sm"] < s["md"] && s["md"] < s["lg"] && s["lg"] < s["xl"], + "spacing scale should be monotonic"); + PASS(); +} + +void test_derived_variant_increases_text_brightness() { + TEST(derived_variant_increases_text_brightness); + auto base = ThemeTokenSystemV2::baseBlackStone(); + auto v = ThemeTokenSystemV2::deriveContrastVariant(base, 0.05f, 0.02f); + CHECK(v.textPrimary.r >= base.textPrimary.r, "primary text should brighten"); + PASS(); +} + +void test_derived_variant_lifts_layers() { + TEST(derived_variant_lifts_layers); + auto base = ThemeTokenSystemV2::baseBlackStone(); + auto v = ThemeTokenSystemV2::deriveContrastVariant(base, 0.05f, 0.02f); + CHECK(v.bgLayer1.r >= base.bgLayer1.r && v.bgLayer2.r >= base.bgLayer2.r, + "layers should lift"); + PASS(); +} + +void test_base_theme_passes_aa_validation() { + TEST(base_theme_passes_aa_validation); + auto t = ThemeTokenSystemV2::baseBlackStone(); + CHECK(ThemeTokenSystemV2::validateAA(t), "base theme should pass AA"); + PASS(); +} + +void test_contrast_ratio_primary_text_high_enough() { + TEST(contrast_ratio_primary_text_high_enough); + auto t = ThemeTokenSystemV2::baseBlackStone(); + auto ratio = ThemeTokenSystemV2::contrastTextVsBg(t.textPrimary, t.bgBase); + CHECK(ratio >= 4.5f, "primary contrast below AA"); + PASS(); +} + +void test_no_default_purple_bias() { + TEST(no_default_purple_bias); + auto t = ThemeTokenSystemV2::baseBlackStone(); + CHECK(!ThemeTokenSystemV2::hasHardcodedPurple(t), "unexpected purple bias"); + PASS(); +} + +void test_detects_purple_bias_when_present() { + TEST(detects_purple_bias_when_present); + auto t = ThemeTokenSystemV2::baseBlackStone(); + t.accentPrimary = {0.55f, 0.25f, 0.85f, 1.0f}; + CHECK(ThemeTokenSystemV2::hasHardcodedPurple(t), "should detect purple bias"); + PASS(); +} + +void test_radius_scale_is_ordered() { + TEST(radius_scale_is_ordered); + auto t = ThemeTokenSystemV2::baseBlackStone(); + CHECK(t.radiusSmall < t.radiusMedium, "radius scale invalid"); + PASS(); +} + +void test_border_scale_is_ordered() { + TEST(border_scale_is_ordered); + auto t = ThemeTokenSystemV2::baseBlackStone(); + CHECK(t.borderThin < t.borderThick, "border scale invalid"); + PASS(); +} + +void test_elevation_scale_is_ordered() { + TEST(elevation_scale_is_ordered); + auto t = ThemeTokenSystemV2::baseBlackStone(); + CHECK(t.elevationLow < t.elevationHigh, "elevation scale invalid"); + PASS(); +} + +int main() { + std::cout << "Step 515: Theme Token System v2\n"; + + test_base_theme_uses_black_stone_layers(); // 1 + test_spacing_scale_has_expected_keys(); // 2 + test_spacing_scale_is_monotonic(); // 3 + test_derived_variant_increases_text_brightness(); // 4 + test_derived_variant_lifts_layers(); // 5 + test_base_theme_passes_aa_validation(); // 6 + test_contrast_ratio_primary_text_high_enough(); // 7 + test_no_default_purple_bias(); // 8 + test_detects_purple_bias_when_present(); // 9 + test_radius_scale_is_ordered(); // 10 + test_border_scale_is_ordered(); // 11 + test_elevation_scale_is_ordered(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 0d0c561..225b74a 100644 --- a/progress.md +++ b/progress.md @@ -8211,3 +8211,39 @@ accessibility baseline compliance with an explicit editor-flow-safe signal. - `editor/src/Phase26aIntegration.h` within header-size limit (`100` <= `600`) - `editor/tests/step514_test.cpp` within test-file size guidance (`91` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 515: Theme Token System v2 +**Status:** PASS (12/12 tests) + +Implements a semantic token system for Sprint 26 visual language with +black/stone layered surfaces, accent channels, spacing/radius/border/elevation +scales, derivation support, and AA contrast validation. + +**Files added:** +- `editor/src/ThemeTokenSystemV2.h` - token system module: + - semantic color token set (base/layers/text/accent/error) + - contrast-variant derivation for adaptive readability + - spacing scale generation (`xs`..`xl`) + - contrast ratio + AA validation helpers + - explicit purple-bias detection guard for default accent policy +- `editor/tests/step515_test.cpp` - 12 tests covering: + - layered black/stone baseline semantics + - spacing monotonicity and required keys + - variant derivation effects on text/layer brightness + - AA contrast checks for baseline tokens + - purple-bias detection behavior + - radius/border/elevation scale ordering + +**Files modified:** +- `editor/CMakeLists.txt` - `step515_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step515_test` - PASS +- `./editor/build-native/step515_test` - PASS (12/12) +- `./editor/build-native/step514_test` - PASS (8/8) regression coverage + +**Architecture gate check:** +- `editor/src/ThemeTokenSystemV2.h` within header-size limit (`107` <= `600`) +- `editor/tests/step515_test.cpp` within test-file size guidance (`121` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`