Browse Source

Implement CSS variables for admin color theming (#6409)

Co-authored-by: JNaftali <jmarantz@thelabnyc.com>
Co-authored-by: Thibaud Colas <thibaudcolas@gmail.com>
Joshua Marantz 4 years ago
parent
commit
8e79c61564

+ 1 - 0
CONTRIBUTORS.rst

@@ -492,6 +492,7 @@ Contributors
 * Bohreromir
 * Fernando Cordeiro
 * Matthias Rohmer
+* Joshua Marantz
 
 Translators
 ===========

+ 1 - 0
client/scss/_tools.scss

@@ -8,3 +8,4 @@ No CSS should be produced by these files.
 @import 'tools/mixins.fonts';
 @import 'tools/mixins.general';
 @import 'tools/mixins.grid';
+@import 'tools/various.colors';

+ 0 - 1
client/scss/components/_listing.scss

@@ -414,7 +414,6 @@ ul.listing {
 
             &.bicolor {
                 background: $color-teal-darker;
-                border: solid 1px darken($color-teal-darker, 10%);
 
                 &:active {
                     color: $color-white;

+ 1 - 1
client/scss/components/_tabs.scss

@@ -37,7 +37,7 @@
 
         &:hover {
             color: $color-white;
-            border-top-color: darken($color-teal-darker, 8);
+            border-top-color: rgba(0, 0, 0, 0.35);
         }
     }
 

+ 8 - 0
client/scss/elements/_root.scss

@@ -0,0 +1,8 @@
+:root {
+    @include define-color('color-primary', #007d7e);
+    @include define-color('color-primary-darker', css-darken(css-adjust-hue(get-color('color-primary'), 1), 4%));
+    @include define-color('color-primary-dark', css-darken(css-adjust-hue(get-color('color-primary'), 1), 7%));
+
+    @include define-color('color-input-focus', css-lighten(css-desaturate(get-color('color-primary'), 40%), 72%));
+    @include define-color('color-input-focus-border', css-lighten(css-saturate(get-color('color-primary'), 12%), 10%));
+}

+ 5 - 5
client/scss/settings/_variables.scss

@@ -28,9 +28,9 @@ $breakpoints: (
 );
 
 // colours
-$color-teal: #007d7e;
-$color-teal-darker: darken(adjust-hue($color-teal, 1), 4);
-$color-teal-dark: darken(adjust-hue($color-teal, 1), 7);
+$color-teal: var(--color-primary);
+$color-teal-darker: var(--color-primary-darker);
+$color-teal-dark: var(--color-primary-dark);
 
 $color-blue: #71b2d4;
 $color-red: #cd3238;
@@ -59,8 +59,8 @@ $color-header-bg: $color-teal;
 
 $color-fieldset-hover: $color-grey-5;
 $color-input-border: $color-grey-4;
-$color-input-focus: lighten(desaturate($color-teal, 40), 72);
-$color-input-focus-border: lighten(saturate($color-teal, 12), 10);
+$color-input-focus: var(--color-input-focus);
+$color-input-focus-border: var(--color-input-focus-border);
 $color-input-error-bg: lighten(saturate($color-red, 28), 45);
 
 $color-button: $color-teal;

+ 1 - 0
client/scss/styles.scss

@@ -74,6 +74,7 @@ These are base styles for bare HTML elements.
 @import 'elements/elements';
 @import 'elements/typography';
 @import 'elements/forms';
+@import 'elements/root';
 
 
 /* OBJECTS

+ 62 - 0
client/scss/tools/_various.colors.scss

@@ -0,0 +1,62 @@
+// $color is either a color or an hsl tuple
+@mixin define-color($name, $color) {
+    $h: null;
+    $s: null;
+    $l: null;
+
+    @if type-of($color) == color {
+        $h: hue($color) / 1deg; // Cast to unitless
+        $s: saturation($color);
+        $l: lightness($color);
+    } @else {
+        $h: nth($color, 1);
+        $s: nth($color, 2);
+        $l: nth($color, 3);
+    }
+
+    --#{$name}-hue: #{$h};
+    --#{$name}-saturation: #{$s};
+    --#{$name}-lightness: #{$l};
+    --#{$name}: hsl(#{ var(--#{$name}-hue), var(--#{$name}-saturation), var(--#{$name}-lightness) });
+}
+
+@function get-color($name) {
+    @return (var(--#{$name}-hue), var(--#{$name}-saturation), var(--#{$name}-lightness));
+}
+
+@function css-darken($hsl-tuple, $darken-by) {
+    $h: nth($hsl-tuple, 1);
+    $s: nth($hsl-tuple, 2);
+    $l: nth($hsl-tuple, 3);
+    @return ($h, $s, calc(#{$l} - #{$darken-by + 0%}));
+}
+@function css-lighten($hsl-tuple, $lighten-by) {
+    $h: nth($hsl-tuple, 1);
+    $s: nth($hsl-tuple, 2);
+    $l: nth($hsl-tuple, 3);
+    @return ($h, $s, calc(#{$l} + #{$lighten-by + 0%}));
+}
+@function css-saturate($hsl-tuple, $saturate-by) {
+    $h: nth($hsl-tuple, 1);
+    $s: nth($hsl-tuple, 2);
+    $l: nth($hsl-tuple, 3);
+    @return ($h, calc(#{$s} + #{$saturate-by + 0%}), $l);
+}
+@function css-desaturate($hsl-tuple, $desaturate-by) {
+    $h: nth($hsl-tuple, 1);
+    $s: nth($hsl-tuple, 2);
+    $l: nth($hsl-tuple, 3);
+    @return ($h, calc(#{$s} - #{$desaturate-by + 0%}), $l);
+}
+@function css-adjust-hue($hsl-tuple, $adjust-by) {
+    $h: nth($hsl-tuple, 1);
+    $s: nth($hsl-tuple, 2);
+    $l: nth($hsl-tuple, 3);
+    @return (calc(#{$h} + #{$adjust-by}), $s, $l);
+}
+@function css-transparentize($hsl-tuple, $alpha) {
+    $h: nth($hsl-tuple, 1);
+    $s: nth($hsl-tuple, 2);
+    $l: nth($hsl-tuple, 3);
+    @return ($h, $s, $l, $alpha);
+}

+ 4 - 1
client/src/components/StreamField/StreamField.scss

@@ -2,7 +2,10 @@ $header-padding-vertical: 6px;
 $action-font-size: 18px;
 
 
-@import '../../../../node_modules/react-streamfield/src/scss/index';
+@use '../../../../node_modules/react-streamfield/src/scss/index' with (
+    $teal: $color-teal,
+    $error-color: $color-red,
+);
 
 
 .c-sf-container {

+ 47 - 0
docs/advanced_topics/customisation/admin_templates.rst

@@ -91,6 +91,53 @@ To replace the welcome message on the dashboard, create a template file ``dashbo
 
     {% block branding_welcome %}Welcome to Frank's Site{% endblock %}
 
+.. _custom_user_interface_colors:
+
+Custom user interface colors
+============================
+
+
+.. warning::
+    CSS variables are not supported in Internet Explorer, so the admin will appear with the default colors when viewed in that browser.
+
+    The default Wagtail colors conform to the WCAG2.1 AA level color contrast requirements. When customizing the admin colors you should test the contrast using tools like `Axe <https://www.deque.com/axe/browser-extensions/>`_.
+
+To customize the primary color used in the admin user interface, inject a CSS file using the hook :ref:`insert_global_admin_css` and override the variables within the ``:root`` selector:
+
+.. code-block:: text
+
+    :root {
+        --color-primary-hue: 25;
+    }
+
+``color-primary`` is an `hsl color <https://en.wikipedia.org/wiki/HSL_and_HSV>`_ composed of 3 CSS variables - ``--color-primary-hue`` (0-360 with no unit), ``--color-primary-saturation`` (a percentage), and ``--color-primary-lightness`` (also a percentage). Separating the color into 3 allows us to calculate variations on the color to use alongside the primary color. If needed, you can also control those variations manually by setting ``hue``, ``saturation``, and ``lightness`` variables for the following colors: ``color-primary-darker``, ``color-primary-dark``, ``color-input-focus``, and ``color-input-focus-border``:
+
+.. code-block:: text
+
+    :root {
+        --color-primary-hue: 25;
+        --color-primary-saturation: 100%;
+        --color-primary-lightness: 25%;
+        --color-primary-darker-hue: 24;
+        --color-primary-darker-saturation: 100%;
+        --color-primary-darker-lightness: 20%;
+        --color-primary-dark-hue: 23;
+        --color-primary-dark-saturation: 100%;
+        --color-primary-dark-lightness: 15%;
+    }
+
+If instead you intend to set all available colors, you can use any valid css colors:
+
+.. code-block:: text
+
+    :root {
+        --color-primary: mediumaquamarine;
+        --color-primary-darker: rebeccapurple;
+        --color-primary-dark: hsl(330, 100%, 70%);
+        --color-input-focus: rgb(204, 0, 102);
+        --color-input-focus-border: #4d0026;
+    }
+
 Specifying a site or page in the branding
 =========================================
 

+ 4 - 0
docs/releases/2.12.rst

@@ -21,6 +21,10 @@ In-place StreamField updating
 
 StreamField values now formally support being updated in-place from Python code, allowing blocks to be inserted, modified and deleted rather than having to assign a new list of blocks to the field. For further details, see :ref:`modifying_streamfield_data`. This feature was developed by Matt Westcott.
 
+Admin color themes
+~~~~~~~~~~~~~~~~~~
+
+Wagtail’s admin now uses CSS custom properties for its primary teal color. Applying brand colors for the whole user interface only takes a few lines of CSS, and third-party extensions can reuse Wagtail’s CSS variables to support the same degree of customization. Read on :ref:`custom_user_interface_colors`. This feature was developed by Joshua Marantz.
 
 Other features
 ~~~~~~~~~~~~~~

+ 4 - 0
gulpfile.js/tasks/styles.js

@@ -4,6 +4,8 @@ var sass = require('gulp-dart-sass');
 var postcss = require('gulp-postcss');
 var autoprefixer = require('autoprefixer');
 var cssnano = require('cssnano');
+var postcssCustomProperties = require('postcss-custom-properties');
+var postcssCalc = require('postcss-calc');
 var sourcemaps = require('gulp-sourcemaps');
 var size = require('gulp-size');
 var config = require('../config');
@@ -69,6 +71,8 @@ gulp.task('styles:sass', function () {
         .pipe(postcss([
           cssnano(cssnanoConfig),
           autoprefixer(autoprefixerConfig),
+          postcssCustomProperties(),
+          postcssCalc(),
         ]))
         .pipe(size({ title: 'Wagtail CSS' }))
         .pipe(config.isProduction ? gutil.noop() : sourcemaps.write())

+ 43 - 17
package-lock.json

@@ -3499,8 +3499,7 @@
     "cssesc": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
-      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
-      "dev": true
+      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="
     },
     "cssnano": {
       "version": "4.1.10",
@@ -7308,8 +7307,7 @@
     "indexes-of": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz",
-      "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=",
-      "dev": true
+      "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc="
     },
     "inflight": {
       "version": "1.0.6",
@@ -7654,6 +7652,12 @@
         "unc-path-regex": "^0.1.2"
       }
     },
+    "is-url-superb": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/is-url-superb/-/is-url-superb-4.0.0.tgz",
+      "integrity": "sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA==",
+      "dev": true
+    },
     "is-utf8": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
@@ -11778,7 +11782,6 @@
       "version": "7.0.35",
       "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz",
       "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==",
-      "dev": true,
       "requires": {
         "chalk": "^2.4.2",
         "source-map": "^0.6.1",
@@ -11788,14 +11791,12 @@
         "source-map": {
           "version": "0.6.1",
           "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
-          "dev": true
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
         },
         "supports-color": {
           "version": "6.1.0",
           "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
           "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
-          "dev": true,
           "requires": {
             "has-flag": "^3.0.0"
           }
@@ -11803,10 +11804,9 @@
       }
     },
     "postcss-calc": {
-      "version": "7.0.2",
-      "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.2.tgz",
-      "integrity": "sha512-rofZFHUg6ZIrvRwPeFktv06GdbDYLcGqh9EwiMutZg+a0oePCCw1zHOEiji6LCpyRcjTREtPASuUqeAvYlEVvQ==",
-      "dev": true,
+      "version": "7.0.5",
+      "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.5.tgz",
+      "integrity": "sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg==",
       "requires": {
         "postcss": "^7.0.27",
         "postcss-selector-parser": "^6.0.2",
@@ -11852,6 +11852,16 @@
         }
       }
     },
+    "postcss-custom-properties": {
+      "version": "10.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-10.0.0.tgz",
+      "integrity": "sha512-55BPj5FudpCiPZzBaO+MOeqmwMDa+nV9/0QBJBfhZjYg6D9hE+rW9lpMBLTJoF4OTXnS5Po4yM1nMlgkPbCxFg==",
+      "dev": true,
+      "requires": {
+        "postcss": "^7.0.17",
+        "postcss-values-parser": "^4.0.0"
+      }
+    },
     "postcss-discard-comments": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz",
@@ -12355,7 +12365,6 @@
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz",
       "integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==",
-      "dev": true,
       "requires": {
         "cssesc": "^3.0.0",
         "indexes-of": "^1.0.1",
@@ -12402,8 +12411,26 @@
     "postcss-value-parser": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz",
-      "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==",
-      "dev": true
+      "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ=="
+    },
+    "postcss-values-parser": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-4.0.0.tgz",
+      "integrity": "sha512-R9x2D87FcbhwXUmoCXJR85M1BLII5suXRuXibGYyBJ7lVDEpRIdKZh4+8q5S+/+A4m0IoG1U5tFw39asyhX/Hw==",
+      "dev": true,
+      "requires": {
+        "color-name": "^1.1.4",
+        "is-url-superb": "^4.0.0",
+        "postcss": "^7.0.5"
+      },
+      "dependencies": {
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true
+        }
+      }
     },
     "prelude-ls": {
       "version": "1.2.1",
@@ -15389,8 +15416,7 @@
     "uniq": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
-      "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=",
-      "dev": true
+      "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8="
     },
     "uniqs": {
       "version": "2.0.0",

+ 2 - 0
package.json

@@ -73,6 +73,7 @@
     "gulp-util": "~3.0.8",
     "jest": "^26.6.0",
     "npm-run-all": "^4.1.5",
+    "postcss-custom-properties": "^10.0.0",
     "react-axe": "^3.1.0",
     "react-test-renderer": "^16.13.1",
     "redux-mock-store": "^1.3.0",
@@ -89,6 +90,7 @@
     "draftail": "^1.2.1",
     "element-closest": "^2.0.2",
     "focus-trap-react": "^3.1.0",
+    "postcss-calc": "^7.0.5",
     "prop-types": "^15.6.2",
     "react": "^16.4.0",
     "react-dom": "^16.4.0",

+ 1 - 0
wagtail/admin/static_src/wagtailadmin/scss/panels/streamfield.scss

@@ -1 +1,2 @@
+@import 'wagtailadmin/scss/helpers';
 @import '../../../../../../client/src/components/StreamField/StreamField';