feat: add CSS variables for status border and text

The variables like `--color-error` now are based on our secondary
theming, thus they are less "aggressive" colors. But there are two
usecases for primary based status colors:
- borders
- error text messages (e.g. validation errors in forms)

To simplify app changes due to the secondary color theme change this
introduces 3 new variables:
- `--color-text-error` this shall be used if text should have error
  status theming and is displayed on normal background (while
  `--color-error-text` is only for text shown on `--color-error` similar
  as primary and secondary colors)
- `--color-border-error` and `--color-border-success` those should be
  used for element borders if there is one of those statuses to be
  reported (we use this for validation errors as well as for indicating a
  value was saved)

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
pull/54492/head
Ferdinand Thiessen 2025-08-18 18:11:34 +07:00
parent d779255c5f
commit d79e18ab18
No known key found for this signature in database
GPG Key ID: 45FAE7268762B400
6 changed files with 102 additions and 79 deletions

@ -18,28 +18,33 @@
--color-text-maxcontrast: #6b6b6b; --color-text-maxcontrast: #6b6b6b;
--color-text-maxcontrast-default: #6b6b6b; --color-text-maxcontrast-default: #6b6b6b;
--color-text-maxcontrast-background-blur: #595959; --color-text-maxcontrast-background-blur: #595959;
--color-text-error: #c90000;
/** @deprecated use ` --color-main-text` instead */ /** @deprecated use ` --color-main-text` instead */
--color-text-light: var(--color-main-text); --color-text-light: var(--color-main-text);
/** @deprecated use `--color-text-maxcontrast` instead */ /** @deprecated use `--color-text-maxcontrast` instead */
--color-text-lighter: var(--color-text-maxcontrast); --color-text-lighter: var(--color-text-maxcontrast);
--color-scrollbar: var(--color-border-maxcontrast) transparent; --color-scrollbar: var(--color-border-maxcontrast) transparent;
--color-error: #FFE7E7; --color-error: #FFE7E7;
--color-error-rgb: 255,231,231;
--color-error-hover: #ffc3c3; --color-error-hover: #ffc3c3;
--color-error-text: #8A0000; --color-error-text: #8A0000;
--color-warning: #FFEEC5; --color-warning: #FFEEC5;
--color-warning-rgb: 255,238,197;
--color-warning-hover: #ffe4a1; --color-warning-hover: #ffe4a1;
--color-warning-text: #664700; --color-warning-text: #664700;
--color-success: #D8F3DA; --color-success: #D8F3DA;
--color-success-rgb: 216,243,218;
--color-success-hover: #bdebc0; --color-success-hover: #bdebc0;
--color-success-text: #005416; --color-success-text: #005416;
--color-info: #D5F1FA; --color-info: #D5F1FA;
--color-info-rgb: 213,241,250;
--color-info-hover: #b5e6f6; --color-info-hover: #b5e6f6;
--color-info-text: #0066AC; --color-info-text: #0066AC;
--color-favorite: #A37200; --color-favorite: #A37200;
/** @deprecated use css color functions */
--color-error-rgb: 255,231,231;
/** @deprecated use css color functions */
--color-warning-rgb: 255,238,197;
/** @deprecated use css color functions */
--color-success-rgb: 216,243,218;
/** @deprecated use css color functions */
--color-info-rgb: 213,241,250;
--color-loading-light: #cccccc; --color-loading-light: #cccccc;
--color-loading-dark: #444444; --color-loading-dark: #444444;
--color-box-shadow-rgb: 77,77,77; --color-box-shadow-rgb: 77,77,77;
@ -47,6 +52,8 @@
--color-border: #ededed; --color-border: #ededed;
--color-border-dark: #dbdbdb; --color-border-dark: #dbdbdb;
--color-border-maxcontrast: #7d7d7d; --color-border-maxcontrast: #7d7d7d;
--color-border-error: #c90000;
--color-border-success: #099f05;
--font-face: system-ui, -apple-system, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, Ubuntu, 'Helvetica Neue', 'Noto Sans', 'Liberation Sans', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; --font-face: system-ui, -apple-system, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, Ubuntu, 'Helvetica Neue', 'Noto Sans', 'Liberation Sans', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--default-font-size: 15px; --default-font-size: 15px;
--font-size-small: 13px; --font-size-small: 13px;

@ -66,6 +66,7 @@ class DarkHighContrastTheme extends DarkTheme implements ITheme {
'--color-text-maxcontrast' => $colorMainText, '--color-text-maxcontrast' => $colorMainText,
'--color-text-maxcontrast-background-blur' => $colorMainText, '--color-text-maxcontrast-background-blur' => $colorMainText,
'--color-text-error' => $this->util->lighten($colorError, 65),
'--color-text-light' => $colorMainText, '--color-text-light' => $colorMainText,
'--color-text-lighter' => $colorMainText, '--color-text-lighter' => $colorMainText,
@ -101,6 +102,8 @@ class DarkHighContrastTheme extends DarkTheme implements ITheme {
'--color-border' => $this->util->lighten($colorMainBackground, 50), '--color-border' => $this->util->lighten($colorMainBackground, 50),
'--color-border-dark' => $this->util->lighten($colorMainBackground, 50), '--color-border-dark' => $this->util->lighten($colorMainBackground, 50),
'--color-border-maxcontrast' => $this->util->lighten($colorMainBackground, 55), '--color-border-maxcontrast' => $this->util->lighten($colorMainBackground, 55),
'--color-border-error' => $this->util->lighten($colorError, 30),
'--color-border-success' => $this->util->lighten($colorSuccess, 30),
] ]
); );
} }

@ -52,14 +52,16 @@ class DarkTheme extends DefaultTheme implements ITheme {
$colorBoxShadow = $this->util->darken($colorMainBackground, 70); $colorBoxShadow = $this->util->darken($colorMainBackground, 70);
$colorBoxShadowRGB = join(',', $this->util->hexToRGB($colorBoxShadow)); $colorBoxShadowRGB = join(',', $this->util->hexToRGB($colorBoxShadow));
$colorError = '#FFCCCC'; $colorError = '#552121';
$colorErrorElement = '#552121'; $colorErrorText = '#FFCCCC';
$colorWarning = '#FFEEC5'; $colorErrorElement = '#ff6c69';
$colorWarningElement = '#3D3010'; $colorWarning = '#3D3010';
$colorSuccess = '#D5F2DC'; $colorWarningText = '#FFEEC5';
$colorSuccessElement = '#11321A'; $colorSuccess = '#11321A';
$colorInfo = '#00AEFF'; $colorSuccessText = '#D5F2DC';
$colorInfoElement = '#003553'; $colorSuccessElement = '#3B973B';
$colorInfo = '#003553';
$colorInfoText = '#00AEFF';
return array_merge( return array_merge(
$defaultVariables, $defaultVariables,
@ -80,26 +82,28 @@ class DarkTheme extends DefaultTheme implements ITheme {
'--color-text-maxcontrast' => $colorTextMaxcontrast, '--color-text-maxcontrast' => $colorTextMaxcontrast,
'--color-text-maxcontrast-default' => $colorTextMaxcontrast, '--color-text-maxcontrast-default' => $colorTextMaxcontrast,
'--color-text-maxcontrast-background-blur' => $this->util->lighten($colorTextMaxcontrast, 6), '--color-text-maxcontrast-background-blur' => $this->util->lighten($colorTextMaxcontrast, 6),
'--color-text-error' => $colorErrorElement,
'--color-text-light' => 'var(--color-main-text)', // deprecated '--color-text-light' => 'var(--color-main-text)', // deprecated
'--color-text-lighter' => 'var(--color-text-maxcontrast)', // deprecated '--color-text-lighter' => 'var(--color-text-maxcontrast)', // deprecated
'--color-error' => $colorErrorElement, '--color-error' => $colorError,
'--color-error-rgb' => join(',', $this->util->hexToRGB($colorErrorElement)), '--color-error-hover' => $this->util->lighten($colorError, 10),
'--color-error-hover' => $this->util->lighten($colorErrorElement, 10), '--color-error-text' => $colorErrorText,
'--color-error-text' => $colorError, '--color-warning' => $colorWarning,
'--color-warning' => $colorWarningElement, '--color-warning-hover' => $this->util->lighten($colorWarning, 10),
'--color-warning-rgb' => join(',', $this->util->hexToRGB($colorWarningElement)), '--color-warning-text' => $colorWarningText,
'--color-warning-hover' => $this->util->lighten($colorWarningElement, 10), '--color-success' => $colorSuccess,
'--color-warning-text' => $colorWarning, '--color-success-hover' => $this->util->lighten($colorSuccess, 10),
'--color-success' => $colorSuccessElement, '--color-success-text' => $colorSuccessText,
'--color-success-rgb' => join(',', $this->util->hexToRGB($colorSuccessElement)), '--color-info' => $colorInfo,
'--color-success-hover' => $this->util->lighten($colorSuccessElement, 10), '--color-info-hover' => $this->util->lighten($colorInfo, 10),
'--color-success-text' => $colorSuccess, '--color-info-text' => $colorInfoText,
'--color-info' => $colorInfoElement,
'--color-info-rgb' => join(',', $this->util->hexToRGB($colorInfoElement)),
'--color-info-hover' => $this->util->lighten($colorInfoElement, 10),
'--color-info-text' => $colorInfo,
'--color-favorite' => '#ffde00', '--color-favorite' => '#ffde00',
// deprecated
'--color-error-rgb' => join(',', $this->util->hexToRGB($colorError)),
'--color-warning-rgb' => join(',', $this->util->hexToRGB($colorWarning)),
'--color-success-rgb' => join(',', $this->util->hexToRGB($colorSuccess)),
'--color-info-rgb' => join(',', $this->util->hexToRGB($colorInfo)),
// used for the icon loading animation // used for the icon loading animation
'--color-loading-light' => '#777', '--color-loading-light' => '#777',
@ -111,6 +115,8 @@ class DarkTheme extends DefaultTheme implements ITheme {
'--color-border' => $this->util->lighten($colorMainBackground, 7), '--color-border' => $this->util->lighten($colorMainBackground, 7),
'--color-border-dark' => $this->util->lighten($colorMainBackground, 14), '--color-border-dark' => $this->util->lighten($colorMainBackground, 14),
'--color-border-maxcontrast' => $this->util->lighten($colorMainBackground, 40), '--color-border-maxcontrast' => $this->util->lighten($colorMainBackground, 40),
'--color-border-error' => $colorErrorElement,
'--color-border-success' => $colorSuccessElement,
'--background-invert-if-dark' => 'invert(100%)', '--background-invert-if-dark' => 'invert(100%)',
'--background-invert-if-bright' => 'no', '--background-invert-if-bright' => 'no',

@ -77,14 +77,16 @@ class DefaultTheme implements ITheme {
$colorBoxShadow = $this->util->darken($colorMainBackground, 70); $colorBoxShadow = $this->util->darken($colorMainBackground, 70);
$colorBoxShadowRGB = join(',', $this->util->hexToRGB($colorBoxShadow)); $colorBoxShadowRGB = join(',', $this->util->hexToRGB($colorBoxShadow));
$colorError = '#8A0000'; $colorError = '#FFE7E7';
$colorErrorElement = '#FFE7E7'; $colorErrorText = '#8A0000';
$colorWarning = '#664700'; $colorErrorElement = '#c90000';
$colorWarningElement = '#FFEEC5'; $colorWarning = '#FFEEC5';
$colorSuccess = '#005416'; $colorWarningText = '#664700';
$colorSuccessElement = '#D8F3DA'; $colorSuccess = '#D8F3DA';
$colorInfo = '#0066AC'; $colorSuccessText = '#005416';
$colorInfoElement = '#D5F1FA'; $colorSuccessElement = '#099f05';
$colorInfo = '#D5F1FA';
$colorInfoText = '#0066AC';
$user = $this->userSession->getUser(); $user = $this->userSession->getUser();
// Chromium based browsers currently (2024) have huge performance issues with blur filters // Chromium based browsers currently (2024) have huge performance issues with blur filters
@ -126,29 +128,31 @@ class DefaultTheme implements ITheme {
'--color-text-maxcontrast' => $colorTextMaxcontrast, '--color-text-maxcontrast' => $colorTextMaxcontrast,
'--color-text-maxcontrast-default' => $colorTextMaxcontrast, '--color-text-maxcontrast-default' => $colorTextMaxcontrast,
'--color-text-maxcontrast-background-blur' => $this->util->darken($colorTextMaxcontrast, 7), '--color-text-maxcontrast-background-blur' => $this->util->darken($colorTextMaxcontrast, 7),
'--color-text-error' => $colorErrorElement,
'--color-text-light' => 'var(--color-main-text)', // deprecated '--color-text-light' => 'var(--color-main-text)', // deprecated
'--color-text-lighter' => 'var(--color-text-maxcontrast)', // deprecated '--color-text-lighter' => 'var(--color-text-maxcontrast)', // deprecated
'--color-scrollbar' => 'var(--color-border-maxcontrast) transparent', '--color-scrollbar' => 'var(--color-border-maxcontrast) transparent',
// error/warning/success/info feedback colours // error/warning/success/info feedback colors
'--color-error' => $colorErrorElement, '--color-error' => $colorError,
'--color-error-rgb' => join(',', $this->util->hexToRGB($colorErrorElement)), '--color-error-hover' => $this->util->darken($colorError, 7),
'--color-error-hover' => $this->util->darken($colorErrorElement, 7), '--color-error-text' => $colorErrorText,
'--color-error-text' => $colorError, '--color-warning' => $colorWarning,
'--color-warning' => $colorWarningElement, '--color-warning-hover' => $this->util->darken($colorWarning, 7),
'--color-warning-rgb' => join(',', $this->util->hexToRGB($colorWarningElement)), '--color-warning-text' => $colorWarningText,
'--color-warning-hover' => $this->util->darken($colorWarningElement, 7), '--color-success' => $colorSuccess,
'--color-warning-text' => $colorWarning, '--color-success-hover' => $this->util->darken($colorSuccess, 7),
'--color-success' => $colorSuccessElement, '--color-success-text' => $colorSuccessText,
'--color-success-rgb' => join(',', $this->util->hexToRGB($colorSuccessElement)), '--color-info' => $colorInfo,
'--color-success-hover' => $this->util->darken($colorSuccessElement, 7), '--color-info-hover' => $this->util->darken($colorInfo, 7),
'--color-success-text' => $colorSuccess, '--color-info-text' => $colorInfoText,
'--color-info' => $colorInfoElement,
'--color-info-rgb' => join(',', $this->util->hexToRGB($colorInfoElement)),
'--color-info-hover' => $this->util->darken($colorInfoElement, 7),
'--color-info-text' => $colorInfo,
'--color-favorite' => '#A37200', '--color-favorite' => '#A37200',
// deprecated
'--color-error-rgb' => join(',', $this->util->hexToRGB($colorError)),
'--color-warning-rgb' => join(',', $this->util->hexToRGB($colorWarning)),
'--color-success-rgb' => join(',', $this->util->hexToRGB($colorSuccess)),
'--color-info-rgb' => join(',', $this->util->hexToRGB($colorInfo)),
// used for the icon loading animation // used for the icon loading animation
'--color-loading-light' => '#cccccc', '--color-loading-light' => '#cccccc',
@ -160,6 +164,8 @@ class DefaultTheme implements ITheme {
'--color-border' => $this->util->darken($colorMainBackground, 7), '--color-border' => $this->util->darken($colorMainBackground, 7),
'--color-border-dark' => $this->util->darken($colorMainBackground, 14), '--color-border-dark' => $this->util->darken($colorMainBackground, 14),
'--color-border-maxcontrast' => $this->util->darken($colorMainBackground, 51), '--color-border-maxcontrast' => $this->util->darken($colorMainBackground, 51),
'--color-border-error' => $colorErrorElement,
'--color-border-success' => $colorSuccessElement,
'--font-face' => "system-ui, -apple-system, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, Ubuntu, 'Helvetica Neue', 'Noto Sans', 'Liberation Sans', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'", '--font-face' => "system-ui, -apple-system, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, Ubuntu, 'Helvetica Neue', 'Noto Sans', 'Liberation Sans', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'",
'--default-font-size' => '15px', '--default-font-size' => '15px',

@ -69,6 +69,7 @@ class HighContrastTheme extends DefaultTheme implements ITheme {
'--color-text-maxcontrast' => $colorMainText, '--color-text-maxcontrast' => $colorMainText,
'--color-text-maxcontrast-background-blur' => $colorMainText, '--color-text-maxcontrast-background-blur' => $colorMainText,
'--color-text-error' => $this->util->darken($colorError, 65),
'--color-text-light' => $colorMainText, '--color-text-light' => $colorMainText,
'--color-text-lighter' => $colorMainText, '--color-text-lighter' => $colorMainText,
@ -106,6 +107,8 @@ class HighContrastTheme extends DefaultTheme implements ITheme {
'--color-border' => $this->util->darken($colorMainBackground, 50), '--color-border' => $this->util->darken($colorMainBackground, 50),
'--color-border-dark' => $this->util->darken($colorMainBackground, 50), '--color-border-dark' => $this->util->darken($colorMainBackground, 50),
'--color-border-maxcontrast' => $this->util->darken($colorMainBackground, 56), '--color-border-maxcontrast' => $this->util->darken($colorMainBackground, 56),
'--color-border-error' => $this->util->darken($colorError, 42),
'--color-border-success' => $this->util->darken($colorSuccess, 55),
// remove the gradient from the app icons // remove the gradient from the app icons
'--header-menu-icon-mask' => 'none', '--header-menu-icon-mask' => 'none',

@ -64,17 +64,6 @@ class AccessibleThemeTestCase extends TestCase {
], ],
$elementContrast, $elementContrast,
], ],
// Those two colors are used for borders which will be `color-main-text` on focussed state, thus need 3:1 contrast to it
'success-error-border-colors' => [
[
'--color-error',
'--color-success',
],
[
'--color-main-text',
],
$elementContrast,
],
'primary-element-text' => [ 'primary-element-text' => [
[ [
'--color-primary-element-text', '--color-primary-element-text',
@ -121,21 +110,6 @@ class AccessibleThemeTestCase extends TestCase {
], ],
$textContrast, $textContrast,
], ],
'status-text-on-background' => [
[
'--color-error-text',
'--color-warning-text',
'--color-success-text',
'--color-info-text',
],
[
'--color-main-background',
'--color-background-hover',
'--color-background-dark',
'--color-main-background-blur',
],
$textContrast,
],
'text-on-status-background' => [ 'text-on-status-background' => [
[ [
'--color-main-text', '--color-main-text',
@ -161,6 +135,30 @@ class AccessibleThemeTestCase extends TestCase {
], ],
$textContrast, $textContrast,
], ],
'status-border-colors-on-background' => [
[
'--color-border-error',
'--color-border-success',
],
[
'--color-main-background',
'--color-background-hover',
'--color-background-dark',
],
$elementContrast,
],
'error-text-on-background' => [
[
'--color-text-error',
],
[
'--color-main-background',
'--color-background-hover',
'--color-background-dark',
'--color-main-background-blur',
],
$textContrast,
],
'error-text-on-error-background' => [ 'error-text-on-error-background' => [
['--color-error-text'], ['--color-error-text'],
[ [