diff --git a/libs/design/scss/theming/_configure-palette.scss b/libs/design/scss/theming/_configure-palette.scss index 0171fdb14b..a1a573585c 100644 --- a/libs/design/scss/theming/_configure-palette.scss +++ b/libs/design/scss/theming/_configure-palette.scss @@ -7,6 +7,9 @@ /// as the palette’s default color. The palette can then be used /// to configure themes by setting them as primary, secondary, or tertiary colors. /// +/// The hue is recorded alongside the color as `default-hue`, so that colors can +/// be selected relative to the default with `daff-color-shift`. +/// /// @group Theming /// /// @param {Map} $color-palette — A color palette map (e.g., `$daff-blue`, `$daff-purple`). @@ -21,7 +24,8 @@ @return map.merge( $color-palette, ( - 'default': get-color.daff-color($color-palette, $hue) + 'default': get-color.daff-color($color-palette, $hue), + 'default-hue': $hue ) ); } diff --git a/libs/design/scss/theming/_index.scss b/libs/design/scss/theming/_index.scss index 11003560c0..9b808cf00d 100644 --- a/libs/design/scss/theming/_index.scss +++ b/libs/design/scss/theming/_index.scss @@ -1,8 +1,9 @@ @forward 'color-palettes'; @forward 'get-color'; +@forward 'color-shift'; @forward 'contrast'; @forward 'configure-palette'; -@forward 'create-theme/create-theme'; +@forward 'create-theme'; @forward 'light-dark'; @forward 'get-palette'; @forward 'get-base-color'; diff --git a/libs/design/scss/theming/color-shift/_color-shift.scss b/libs/design/scss/theming/color-shift/_color-shift.scss new file mode 100644 index 0000000000..44bf6aafdf --- /dev/null +++ b/libs/design/scss/theming/color-shift/_color-shift.scss @@ -0,0 +1,56 @@ +// stylelint-disable @stylistic/max-line-length +@use 'sass:map'; +@use '../get-color'; +@use '../../core/error/error-to-string'; + +/// @access private +/// The distance between two adjacent hues in a color palette. +$hue-step: 10; + +/// Retrieves a color relative to a palette's default hue. +/// +/// Positive steps move toward the darker end of the palette, negative steps +/// toward the lighter end. Use this instead of a hardcoded hue when a color is +/// meant to sit a fixed distance from a palette's default, such as the hover +/// and active states of a component. +/// +/// The palette must be configured with `daff-configure-palette`, which records +/// the hue that the default was set from. +/// +/// @group Theming +/// +/// @param {Map} $palette — The palette to retrieve a color from. +/// @param {Number} $steps [0] — How many hue steps to shift from the default. +/// +/// @return {Color} The color `$steps` away from the palette's default hue. +/// +/// @throws Will throw an error if `$palette` was not configured with +/// `daff-configure-palette`, or if the shifted hue does not exist in `$palette`. +/// +/// @example scss +/// // $primary configured at hue 60 +/// .custom-content { +/// background-color: daff-color($primary); // hue 60 +/// border-color: daff-color-shift($primary, 1); // hue 70 +/// color: daff-color-shift($primary, -1); // hue 50 +/// } +/// +@function daff-color-shift($palette, $steps: 0) { + $default-hue: map.get($palette, 'default-hue'); + + @if ($default-hue == null) { + @return error-to-string.error-to-string( + 'Palette has no `default-hue`. Configure it with `daff-configure-palette`.' + ); + } + + $hue: $default-hue + ($steps * $hue-step); + + @if (not map.has-key($palette, $hue)) { + @return error-to-string.error-to-string( + 'Cannot shift `#{$steps}` step(s) from hue `#{$default-hue}`. `#{$hue}` does not exist in palette.' + ); + } + + @return get-color.daff-color($palette, $hue); +} diff --git a/libs/design/scss/theming/color-shift/_color-shift.spec.scss b/libs/design/scss/theming/color-shift/_color-shift.spec.scss new file mode 100644 index 0000000000..0f8453beb0 --- /dev/null +++ b/libs/design/scss/theming/color-shift/_color-shift.spec.scss @@ -0,0 +1,107 @@ +@use 'true' as *; +@use 'sass:map'; +@use '../color-shift' as *; +@use '../color-palettes' as palette; +@use '../configure-palette' as *; +@use '../get-color' as *; +@use '../../core/error/error-to-string'; + +@include describe('daff-color-shift') { + $default: daff-configure-palette(palette.$daff-blue); + $light: daff-configure-palette(palette.$daff-blue, 30); + $dark: daff-configure-palette(palette.$daff-blue, 50); + $lightest: daff-configure-palette(palette.$daff-blue, 10); + $darkest: daff-configure-palette(palette.$daff-blue, 100); + + @include it('returns the default color when there is no shift') { + @include assert-equal(daff-color-shift($default), daff-color($default)); + @include assert-equal(daff-color-shift($default, 0), daff-color($default, 60)); + } + + @include it('returns a darker color for positive steps') { + @include assert-equal(daff-color-shift($default, 1), daff-color($default, 70)); + @include assert-equal(daff-color-shift($default, 2), daff-color($default, 80)); + } + + @include it('returns a lighter color for negative steps') { + @include assert-equal(daff-color-shift($dark, -1), daff-color($dark, 40)); + @include assert-equal(daff-color-shift($dark, -2), daff-color($dark, 30)); + } + + @include it('shifts relative to the hue the palette was configured with') { + @include assert-equal(daff-color-shift($light, 1), daff-color($light, 40)); + @include assert-equal(daff-color-shift($light, 2), daff-color($light, 50)); + } + + @include it('shifts across the whole palette') { + @include assert-equal(daff-color-shift($lightest, 9), daff-color($lightest, 100)); + @include assert-equal(daff-color-shift($darkest, -9), daff-color($darkest, 10)); + } + + @include it('shifts relative to the configured hue, not a duplicate color earlier in the palette') { + $duplicate: daff-configure-palette((10: #ffffff, 20: #cccccc, 30: #cccccc, 40: #999999), 30); + + @include assert-equal(daff-color-shift($duplicate, 1), #999999); + } + + @include error-to-string.set-use-string(true); + + @include it('errors when the shift falls past the dark end of the palette') { + @include assert-equal( + daff-color-shift($darkest, 1), + 'Cannot shift `1` step(s) from hue `100`. `110` does not exist in palette.' + ); + } + + @include it('errors when the shift falls past the light end of the palette') { + @include assert-equal( + daff-color-shift($lightest, -1), + 'Cannot shift `-1` step(s) from hue `10`. `0` does not exist in palette.' + ); + } + + @include it('errors when the shift lands between hues') { + @include assert-equal( + daff-color-shift($default, 0.5), + 'Cannot shift `0.5` step(s) from hue `60`. `65` does not exist in palette.' + ); + } + + @include it('errors when the palette was not configured with daff-configure-palette') { + @include assert-equal( + daff-color-shift(palette.$daff-blue, 1), + 'Palette has no `default-hue`. Configure it with `daff-configure-palette`.' + ); + } + + @include it('errors when the palette has a default color but no default hue') { + $manual: map.merge(palette.$daff-blue, ('default': daff-color(palette.$daff-blue, 60))); + + @include assert-equal( + daff-color-shift($manual, 1), + 'Palette has no `default-hue`. Configure it with `daff-configure-palette`.' + ); + } + + @include error-to-string.set-use-string(false); +} + +@include describe('daff-configure-palette') { + @include it('records the hue the default was configured from') { + @include assert-equal( + map.get(daff-configure-palette(palette.$daff-blue), 'default-hue'), + 60 + ); + @include assert-equal( + map.get(daff-configure-palette(palette.$daff-blue, 30), 'default-hue'), + 30 + ); + } + + @include it('replaces the recorded hue when a palette is reconfigured') { + $reconfigured: daff-configure-palette(daff-configure-palette(palette.$daff-blue, 30), 80); + + @include assert-equal(map.get($reconfigured, 'default-hue'), 80); + @include assert-equal(map.get($reconfigured, 'default'), daff-color(palette.$daff-blue, 80)); + } +} diff --git a/libs/design/scss/theming/color-shift/_index.scss b/libs/design/scss/theming/color-shift/_index.scss new file mode 100644 index 0000000000..92e84fa7af --- /dev/null +++ b/libs/design/scss/theming/color-shift/_index.scss @@ -0,0 +1 @@ +@forward 'color-shift';