diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst index c5613c920e..4d63a21fa8 100644 --- a/CONTRIBUTORS.rst +++ b/CONTRIBUTORS.rst @@ -492,6 +492,7 @@ Contributors * Bohreromir * Fernando Cordeiro * Matthias Rohmer +* Joshua Marantz Translators =========== diff --git a/client/scss/_tools.scss b/client/scss/_tools.scss index 3fa1ccd6d8..3bd20aa5ca 100644 --- a/client/scss/_tools.scss +++ b/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'; diff --git a/client/scss/components/_listing.scss b/client/scss/components/_listing.scss index 254b07fbb3..e7196c6748 100644 --- a/client/scss/components/_listing.scss +++ b/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; diff --git a/client/scss/components/_tabs.scss b/client/scss/components/_tabs.scss index 64d035ed27..6302492705 100644 --- a/client/scss/components/_tabs.scss +++ b/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); } } diff --git a/client/scss/elements/_root.scss b/client/scss/elements/_root.scss new file mode 100644 index 0000000000..7c6bf55f30 --- /dev/null +++ b/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%)); +} diff --git a/client/scss/settings/_variables.scss b/client/scss/settings/_variables.scss index f78ac1ea6d..30fd84a412 100644 --- a/client/scss/settings/_variables.scss +++ b/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; diff --git a/client/scss/styles.scss b/client/scss/styles.scss index ddf4c8e1a0..1a23584d13 100644 --- a/client/scss/styles.scss +++ b/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 diff --git a/client/scss/tools/_various.colors.scss b/client/scss/tools/_various.colors.scss new file mode 100644 index 0000000000..37183e4486 --- /dev/null +++ b/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); +} diff --git a/client/src/components/StreamField/StreamField.scss b/client/src/components/StreamField/StreamField.scss index f52eee75b1..fa2e88684b 100644 --- a/client/src/components/StreamField/StreamField.scss +++ b/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 { diff --git a/docs/advanced_topics/customisation/admin_templates.rst b/docs/advanced_topics/customisation/admin_templates.rst index 17ad37a685..a793477962 100644 --- a/docs/advanced_topics/customisation/admin_templates.rst +++ b/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 `_. + +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 `_ 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 ========================================= diff --git a/docs/releases/2.12.rst b/docs/releases/2.12.rst index 4166148111..5ddda869d7 100644 --- a/docs/releases/2.12.rst +++ b/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 ~~~~~~~~~~~~~~ diff --git a/gulpfile.js/tasks/styles.js b/gulpfile.js/tasks/styles.js index ee322b263f..5f86619088 100644 --- a/gulpfile.js/tasks/styles.js +++ b/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()) diff --git a/package-lock.json b/package-lock.json index ef0ddfafdc..bd154222bf 100644 --- a/package-lock.json +++ b/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", diff --git a/package.json b/package.json index d84b80ab07..307abf8e44 100644 --- a/package.json +++ b/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", diff --git a/wagtail/admin/static_src/wagtailadmin/scss/panels/streamfield.scss b/wagtail/admin/static_src/wagtailadmin/scss/panels/streamfield.scss index b2283153f9..d0a4fd2aa9 100644 --- a/wagtail/admin/static_src/wagtailadmin/scss/panels/streamfield.scss +++ b/wagtail/admin/static_src/wagtailadmin/scss/panels/streamfield.scss @@ -1 +1,2 @@ +@import 'wagtailadmin/scss/helpers'; @import '../../../../../../client/src/components/StreamField/StreamField';