From 14e28957d44c04f66706e66f5536418359bac326 Mon Sep 17 00:00:00 2001 From: Kiro Agent <244629292+kiro-agent@users.noreply.github.com> Date: Tue, 18 Aug 2026 11:58:23 +0000 Subject: [PATCH 1/2] fix(bricks): resolve swatches for all remaining picker color tokens A diff of the variable-picker color tokens (parsed from the optimal/full bundles) against the resolver's hex-map keys surfaced 26 `--sf-color-*` entries that rendered without a swatch because the resolver never produced a key for them: - per-family raw source tokens `-source-light` / `-source-dark` (10 families), - literal `white` / `black`, - `caret` (framework aliases it to action), - the alt-selection pair `selection-bg--alt` / `selection-text--alt`, - `text--subtle`. Added Slashed_Color_Resolver::add_picker_only_tokens(), called from both resolve() and resolve_dark() with the same light/dark source sets so the two maps keep an identical key set. Source tokens are emitted as absolute (mode-independent) values; text--subtle is derived from the mode-appropriate neutral source to mirror core/tokens.css; the alt selection reuses the same-scheme selection swatch (both are an action-tinted highlight). Verified against both bundles: 0 picker color tokens now lack a swatch (was 26). Added regression tests. PHP suite: 232 pass (1031 assertions). --- .../includes/class-color-resolver.php | 80 +++++++++++++++++++ tests-php/ColorResolverTest.php | 53 ++++++++++++ 2 files changed, 133 insertions(+) diff --git a/SLASHED-for-WP/includes/class-color-resolver.php b/SLASHED-for-WP/includes/class-color-resolver.php index 5be49cf..67590c3 100644 --- a/SLASHED-for-WP/includes/class-color-resolver.php +++ b/SLASHED-for-WP/includes/class-color-resolver.php @@ -172,6 +172,10 @@ public static function resolve( $color_values ) { // Semantic tokens with reasonable light-mode defaults. $hex_map = self::resolve_semantic_tokens( $hex_map, $sources ); + // Remaining picker tokens (per-family sources, white/black, caret, …). + $dark_sources = self::derive_dark_sources( $sources, $color_values ); + $hex_map = self::add_picker_only_tokens( $hex_map, $sources, $dark_sources, 'light' ); + return $hex_map; } @@ -205,6 +209,82 @@ public static function resolve_dark( $color_values ) { $hex_map = self::build_family_scales( $dark_sources, Slashed_Color_Math::hex_to_rgb( $base_dark_hex ) ); $hex_map = self::resolve_semantic_tokens_dark( $hex_map, $dark_sources, $light_sources ); + // Remaining picker tokens (per-family sources, white/black, caret, …). + $hex_map = self::add_picker_only_tokens( $hex_map, $light_sources, $dark_sources, 'dark' ); + + return $hex_map; + } + + /** + * Emit the colour tokens the framework ships in its variable picker that the + * per-family scale and semantic passes don't already produce, so every + * `--sf-color-*` entry in the Bricks dropdown gets a swatch. + * + * Covered here: + * - the raw per-family SOURCE tokens (`-source-light` / `-source-dark`) — + * absolute values, so both maps carry the same hex regardless of the + * page mode being previewed; + * - the literal `white` / `black`; + * - `caret` (the framework aliases it to `--sf-color-action`); + * - the alt-selection pair (`selection-bg--alt` / `selection-text--alt`), + * approximated by the same-scheme selection swatch — both are an + * action-tinted highlight, adequate for a preview square; + * - `text--subtle`, derived from the mode-appropriate neutral source to + * mirror core/tokens.css. + * + * Called by both resolvers with the same light/dark source sets so the two + * maps expose an identical key set (see the light/dark parity test). + * + * @param array $hex_map Map built so far. + * @param array $light_sources Family => [L, C, H] (light). + * @param array $dark_sources Family => [L, C, H] (dark). + * @param string $mode 'light' or 'dark' — selects the neutral used for text--subtle. + * @return array + */ + private static function add_picker_only_tokens( $hex_map, $light_sources, $dark_sources, $mode ) { + // Per-family source tokens. Absolute (mode-independent) values, so the + // light and dark maps carry the same hex for each. + foreach ( $light_sources as $family => $lch ) { + $hex_map[ '--sf-color-' . $family . '-source-light' ] = Slashed_Color_Math::oklch_to_hex( $lch[0], $lch[1], $lch[2] ); + } + foreach ( $dark_sources as $family => $lch ) { + $hex_map[ '--sf-color-' . $family . '-source-dark' ] = Slashed_Color_Math::oklch_to_hex( $lch[0], $lch[1], $lch[2] ); + } + + // Literal white / black (oklch(100% 0 0) / oklch(0% 0 0)). + $hex_map['--sf-color-white'] = '#ffffff'; + $hex_map['--sf-color-black'] = '#000000'; + + // caret aliases action. + if ( isset( $hex_map['--sf-color-action'] ) ) { + $hex_map['--sf-color-caret'] = $hex_map['--sf-color-action']; + } + + // Alt selection — opposite-scheme treatment; approximate with the + // same-scheme selection swatch (both are an action-tinted highlight). + if ( isset( $hex_map['--sf-color-selection-bg'] ) ) { + $hex_map['--sf-color-selection-bg--alt'] = $hex_map['--sf-color-selection-bg']; + } + if ( isset( $hex_map['--sf-color-selection-text'] ) ) { + $hex_map['--sf-color-selection-text--alt'] = $hex_map['--sf-color-selection-text']; + } + + // text--subtle — derived from the mode-appropriate neutral source, + // mirroring the clamp() formulas in core/tokens.css. (contrast-bias is 0 + // by default, matching the rest of this resolver.) + $neutral = ( 'dark' === $mode ) + ? ( isset( $dark_sources['neutral'] ) ? $dark_sources['neutral'] : null ) + : ( isset( $light_sources['neutral'] ) ? $light_sources['neutral'] : null ); + if ( null !== $neutral ) { + list( $nl, $nc, $nh ) = $neutral; + $l = ( 'dark' === $mode ) + ? max( 0.55, min( $nl + 0.1, 0.90 ) ) + : max( 0.15, min( $nl - 0.25, 0.45 ) ); + $hex_map['--sf-color-text--subtle'] = Slashed_Color_Math::oklch_to_hex( $l, $nc, $nh ); + } else { + $hex_map['--sf-color-text--subtle'] = ( 'dark' === $mode ) ? '#9a9aae' : '#3a3a4d'; + } + return $hex_map; } diff --git a/tests-php/ColorResolverTest.php b/tests-php/ColorResolverTest.php index beed1c1..5fb0e6d 100644 --- a/tests-php/ColorResolverTest.php +++ b/tests-php/ColorResolverTest.php @@ -125,6 +125,59 @@ public function test_state_modifier_aliases_use_the_double_dash_bem_naming() { } } + public function test_picker_only_tokens_are_resolved_in_both_modes() { + // Regression guard: these tokens ship in the framework's variable picker + // but are not produced by the per-family scale or the semantic passes. + // Without them the Bricks colour dropdown rendered those rows swatch-less. + $maps = array( + 'light' => Slashed_Color_Resolver::resolve( array() ), + 'dark' => Slashed_Color_Resolver::resolve_dark( array() ), + ); + $expected = array( + // Per-family raw source tokens. + '--sf-color-primary-source-light', + '--sf-color-primary-source-dark', + '--sf-color-action-source-light', + '--sf-color-action-source-dark', + '--sf-color-base-source-light', + '--sf-color-base-source-dark', + // Literals, caret alias, alt selection, subtle text. + '--sf-color-white', + '--sf-color-black', + '--sf-color-caret', + '--sf-color-selection-bg--alt', + '--sf-color-selection-text--alt', + '--sf-color-text--subtle', + ); + foreach ( $maps as $mode => $map ) { + foreach ( $expected as $key ) { + $this->assertArrayHasKey( $key, $map, "$mode map must resolve $key" ); + } + } + } + + public function test_source_tokens_are_mode_independent_and_match_the_family_base() { + // A -source-light / -source-dark token is an absolute input value, so it + // reads the same in the light and dark maps, and -source-light equals the + // family base (which is the light source). + $light = Slashed_Color_Resolver::resolve( array() ); + $dark = Slashed_Color_Resolver::resolve_dark( array() ); + foreach ( array( 'primary', 'action', 'neutral', 'success' ) as $family ) { + $sl = '--sf-color-' . $family . '-source-light'; + $sd = '--sf-color-' . $family . '-source-dark'; + $this->assertSame( $light[ $sl ], $dark[ $sl ], "$family -source-light must be mode-independent" ); + $this->assertSame( $light[ $sd ], $dark[ $sd ], "$family -source-dark must be mode-independent" ); + $this->assertSame( $light[ '--sf-color-' . $family ], $light[ $sl ], "$family -source-light must equal the light family base" ); + } + } + + public function test_caret_and_literals_resolve_as_expected() { + $light = Slashed_Color_Resolver::resolve( array() ); + $this->assertSame( '#ffffff', $light['--sf-color-white'] ); + $this->assertSame( '#000000', $light['--sf-color-black'] ); + $this->assertSame( $light['--sf-color-action'], $light['--sf-color-caret'], 'caret aliases action' ); + } + public function test_light_and_dark_resolve_the_same_variable_set() { $light = array_keys( Slashed_Color_Resolver::resolve( array() ) ); $dark = array_keys( Slashed_Color_Resolver::resolve_dark( array() ) ); From 45f1658c535c5ce7018f95aa2201bf2f43a55b9c Mon Sep 17 00:00:00 2001 From: Kiro Agent <244629292+kiro-agent@users.noreply.github.com> Date: Tue, 18 Aug 2026 13:24:10 +0000 Subject: [PATCH 2/2] fix(bricks): use opposite-mode formula for alt selection swatch; widen tests Address review feedback on #242: - selection-bg--alt now applies the OPPOSITE scheme's formula (per core/tokens.css) instead of copying selection-bg: in light mode the dark formula (action dark-source clamped, over the light page), in dark mode the light formula (action light-source over the dark surface). selection-text--alt stays as the current text colour (framework value is `inherit`). - ColorResolverTest now derives the family list from the resolved map so every emitted -source-light/-source-dark token is covered, asserts the dark family base equals its -source-dark token, and pins the exact light alt-selection value as a regression guard. PHP suite: 233 pass (1079 assertions). --- .../includes/class-color-resolver.php | 55 +++++++++++++++++-- tests-php/ColorResolverTest.php | 45 ++++++++++++++- 2 files changed, 93 insertions(+), 7 deletions(-) diff --git a/SLASHED-for-WP/includes/class-color-resolver.php b/SLASHED-for-WP/includes/class-color-resolver.php index 67590c3..b287a31 100644 --- a/SLASHED-for-WP/includes/class-color-resolver.php +++ b/SLASHED-for-WP/includes/class-color-resolver.php @@ -260,11 +260,17 @@ private static function add_picker_only_tokens( $hex_map, $light_sources, $dark_ $hex_map['--sf-color-caret'] = $hex_map['--sf-color-action']; } - // Alt selection — opposite-scheme treatment; approximate with the - // same-scheme selection swatch (both are an action-tinted highlight). - if ( isset( $hex_map['--sf-color-selection-bg'] ) ) { - $hex_map['--sf-color-selection-bg--alt'] = $hex_map['--sf-color-selection-bg']; + // Alt selection background — the OPPOSITE scheme's treatment (see + // core/tokens.css, where the light/dark branches are swapped relative to + // --sf-color-selection-bg): in light mode it uses the dark selection + // formula composited over the light page; in dark mode the light formula + // over the dark surface. Approximated to an opaque swatch. + $alt_bg = self::resolve_alt_selection_bg( $hex_map, $light_sources, $dark_sources, $mode ); + if ( null !== $alt_bg ) { + $hex_map['--sf-color-selection-bg--alt'] = $alt_bg; } + // selection-text--alt is `inherit` in the framework → the current text + // colour, which the resolver models as --sf-color-selection-text. if ( isset( $hex_map['--sf-color-selection-text'] ) ) { $hex_map['--sf-color-selection-text--alt'] = $hex_map['--sf-color-selection-text']; } @@ -288,6 +294,47 @@ private static function add_picker_only_tokens( $hex_map, $light_sources, $dark_ return $hex_map; } + /** + * Resolve the alternate selection background swatch for one mode. + * + * Mirrors core/tokens.css, where `--sf-color-selection-bg--alt` deliberately + * inverts the scheme of `--sf-color-selection-bg`: + * - light page → the DARK formula: action dark-source at clamp(0.62, + * 0.93 - l*0.4, 0.78) lightness, 55% over the (white) page; + * - dark page → the LIGHT formula: action light-source at 28% over the + * dark surface. + * The translucent CSS value is approximated as an opaque composite, matching + * how the rest of this resolver previews alpha tokens. + * + * @param array $hex_map Map built so far (used for the dark surface). + * @param array $light_sources Family => [L, C, H] (light). + * @param array $dark_sources Family => [L, C, H] (dark). + * @param string $mode 'light' or 'dark'. + * @return string|null Hex string, or null when the action source is missing. + */ + private static function resolve_alt_selection_bg( $hex_map, $light_sources, $dark_sources, $mode ) { + if ( 'light' === $mode ) { + if ( ! isset( $dark_sources['action'] ) ) { + return null; + } + list( $dl, $dc, $dh ) = $dark_sources['action']; + $l = max( 0.62, min( 0.93 - $dl * 0.4, 0.78 ) ); + $rgb = Slashed_Color_Math::hex_to_rgb( Slashed_Color_Math::oklch_to_hex( $l, $dc, $dh ) ); + return Slashed_Color_Math::rgb_to_hex( Slashed_Color_Math::mix_rgb( $rgb, array( 255, 255, 255 ), 0.55 ) ); + } + + if ( ! isset( $light_sources['action'] ) ) { + return null; + } + list( $ll, $lc, $lh ) = $light_sources['action']; + $rgb = Slashed_Color_Math::hex_to_rgb( Slashed_Color_Math::oklch_to_hex( $ll, $lc, $lh ) ); + $surface_hex = isset( $hex_map['--sf-color-surface'] ) + ? $hex_map['--sf-color-surface'] + : ( isset( $hex_map['--sf-color-base'] ) ? $hex_map['--sf-color-base'] : '#1a1b1e' ); + $surface_rgb = Slashed_Color_Math::hex_to_rgb( $surface_hex ); + return Slashed_Color_Math::rgb_to_hex( Slashed_Color_Math::mix_rgb( $rgb, $surface_rgb, 0.28 ) ); + } + /** * Build the per-family scale/alpha/alias hex map from resolved sources. * diff --git a/tests-php/ColorResolverTest.php b/tests-php/ColorResolverTest.php index 5fb0e6d..6baf6d1 100644 --- a/tests-php/ColorResolverTest.php +++ b/tests-php/ColorResolverTest.php @@ -158,16 +158,31 @@ public function test_picker_only_tokens_are_resolved_in_both_modes() { public function test_source_tokens_are_mode_independent_and_match_the_family_base() { // A -source-light / -source-dark token is an absolute input value, so it - // reads the same in the light and dark maps, and -source-light equals the - // family base (which is the light source). + // reads the same in the light and dark maps; -source-light equals the + // LIGHT family base and -source-dark equals the DARK family base. $light = Slashed_Color_Resolver::resolve( array() ); $dark = Slashed_Color_Resolver::resolve_dark( array() ); - foreach ( array( 'primary', 'action', 'neutral', 'success' ) as $family ) { + + // Cover EVERY family the resolver actually emits a source token for + // (derived from the map so a newly-added family is covered automatically). + $families = array(); + foreach ( array_keys( $light ) as $key ) { + if ( preg_match( '/^--sf-color-(.+)-source-light$/', $key, $m ) ) { + $families[] = $m[1]; + } + } + $this->assertNotEmpty( $families, 'resolver must emit per-family source tokens' ); + + foreach ( $families as $family ) { $sl = '--sf-color-' . $family . '-source-light'; $sd = '--sf-color-' . $family . '-source-dark'; + $this->assertArrayHasKey( $sd, $light, "$family must also expose -source-dark" ); + $this->assertSame( $light[ $sl ], $dark[ $sl ], "$family -source-light must be mode-independent" ); $this->assertSame( $light[ $sd ], $dark[ $sd ], "$family -source-dark must be mode-independent" ); + $this->assertSame( $light[ '--sf-color-' . $family ], $light[ $sl ], "$family -source-light must equal the light family base" ); + $this->assertSame( $dark[ '--sf-color-' . $family ], $dark[ $sd ], "$family -source-dark must equal the dark family base" ); } } @@ -178,6 +193,30 @@ public function test_caret_and_literals_resolve_as_expected() { $this->assertSame( $light['--sf-color-action'], $light['--sf-color-caret'], 'caret aliases action' ); } + public function test_alt_selection_bg_uses_the_opposite_mode_formula() { + // --sf-color-selection-bg--alt inverts the scheme of --sf-color-selection-bg + // (see core/tokens.css), so it must NOT simply copy the same-mode value. + $light = Slashed_Color_Resolver::resolve( array() ); + $dark = Slashed_Color_Resolver::resolve_dark( array() ); + + $this->assertArrayHasKey( '--sf-color-selection-bg--alt', $light ); + $this->assertArrayHasKey( '--sf-color-selection-bg--alt', $dark ); + $this->assertNotSame( + $light['--sf-color-selection-bg'], + $light['--sf-color-selection-bg--alt'], + 'alt selection must differ from the same-mode selection background' + ); + + // Exact-value guard: recompute the light-mode alt (the dark formula — + // action dark-source clamped, 55% over white) from the public primitives. + $dark_action = Slashed_Color_Math::parse_oklch( 'oklch(0.70 0.198 235)' ); // action -source-dark default + list( $dl, $dc, $dh ) = $dark_action; + $l = max( 0.62, min( 0.93 - $dl * 0.4, 0.78 ) ); + $rgb = Slashed_Color_Math::hex_to_rgb( Slashed_Color_Math::oklch_to_hex( $l, $dc, $dh ) ); + $expected = Slashed_Color_Math::rgb_to_hex( Slashed_Color_Math::mix_rgb( $rgb, array( 255, 255, 255 ), 0.55 ) ); + $this->assertSame( $expected, $light['--sf-color-selection-bg--alt'], 'light alt selection uses the dark formula over white' ); + } + public function test_light_and_dark_resolve_the_same_variable_set() { $light = array_keys( Slashed_Color_Resolver::resolve( array() ) ); $dark = array_keys( Slashed_Color_Resolver::resolve_dark( array() ) );