Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions SLASHED-for-WP/includes/class-color-resolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -205,9 +209,132 @@ 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<string, string>
*/
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 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'];
}

// 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;
}

/**
* 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.
*
Expand Down
92 changes: 92 additions & 0 deletions tests-php/ColorResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,98 @@ 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; -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() );

// 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" );
Comment thread
coderabbitai[bot] marked this conversation as resolved.
$this->assertSame( $dark[ '--sf-color-' . $family ], $dark[ $sd ], "$family -source-dark must equal the dark 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_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() ) );
Expand Down