Step 515: add semantic theme token system v2
This commit is contained in:
@@ -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)
|
||||
|
||||
107
editor/src/ThemeTokenSystemV2.h
Normal file
107
editor/src/ThemeTokenSystemV2.h
Normal file
@@ -0,0 +1,107 @@
|
||||
#pragma once
|
||||
// Step 515: Theme Token System v2
|
||||
// Semantic token set with derivation support and contrast checks.
|
||||
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
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<std::string, float> 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);
|
||||
}
|
||||
};
|
||||
121
editor/tests/step515_test.cpp
Normal file
121
editor/tests/step515_test.cpp
Normal file
@@ -0,0 +1,121 @@
|
||||
// Step 515: Theme Token System v2 (12 tests)
|
||||
|
||||
#include "ThemeTokenSystemV2.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
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;
|
||||
}
|
||||
36
progress.md
36
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`
|
||||
|
||||
Reference in New Issue
Block a user