The theme consists in a set of properties (border, radius, shadow, colors) that are consistent throughout the application. Let's assume the customization of your application is outsourced to a client. If the client changes one of those properties, it will impact all the elements that rely upon it. For example, if in the theme, a shadow color is defined, the client can override it and impact all the components with shadows.
The theme properties can be split according to the following concerns:
For more information on the Amadeus palettes you can refer to Amadeus Color Guidelines.
You can find below the process to generate your own theme.
Create a new theme generator in your repository. It shall generate a map of properties that will be used directly in the component's stylesheets. The generator shall take a map of overridden properties in order to allow different variations of the theme.
The theme properties can be computed from private variables. Your private variables are not part of your theme and
should not be used within the components. There is no guarantee they will always be available in your theme.
If you find the theme properties lacking, please update the generator and do not rely upon $overridden-properties
.
Note: Your theme generator should always extend the basic generator in the otter library:
generate-theme-variables
. The sole purpose of this generator is to make sure all the mandatory theme properties are
available with a default value for each. It is up to the theme to override it with its own variables.
// Generate a map of theme variables for your application and override them with the customer's properties
// Add the variables that are specific to your application theme
// ---
// @access private
// ---
// @param {bool} $is-dark-theme [false]
// @param {map} $override [()] map with the list of the properties to override for one implementation
// ---
// @return map
@function _generate-app-variables($is-dark-theme: false, $overridden-properties: ()) {
// Overridable variables used to compute the style.
$private-variables-default: (
graphical-line: #e3e3e3,
line-style: solid,
);
$private: map_merge($private-variables-default, $overridden-properties);
// Properties that are specific to the application
$own-variables: (
border-style: get-mandatory($private, 'line-style'),
border-color: get-mandatory($private, 'graphical-line'),
separator-style: get-mandatory($private, 'line-style'),
separator-color: lighten(get-mandatory($private, 'graphical-line'), 10%),
corner-border-radius: 20px
);
@return generate-theme-variables($application-theme-variables, $overridden-properties);
}
// Results:
// $generated-theme-with-no-overridden-properties: (
// border-style: solid,
// border-color: #e3e3e3,
// separator-style: solid,
// separator-color: #fdfdfd,
// corner-border-radius: 20px,
//
// //Variables not overridden for this theme
//
// panel-background: #FFF,
// ...
// text: #000
// )
The application theme - and its implementation variations - can be inconsistent with the material theme as implemented by the material Angular team. To avoid any disparity between your application custom components and the classic Angular material ones, you will also need to override the theme generated by material.
There are two functions to generate a theme in material angular: mat-dark-theme
and mat-light-theme
. These two
functions returns a map with the following entries:
There is no direct way to override the values within the theme but to call map_merge
. Material has not provided a
way to create a consistent theme from a text color and a background color.
@use '@o3r/styling' as o3r;
@function _override-mat-theme($mat-theme, $application-variables) {
$mat-foreground: o3r.get-mandatory($mat-theme, 'foreground');
$mat-background: o3r.get-mandatory($mat-theme, 'background');
$foreground-override: (
divider: o3r.get-mandatory($application-variables, 'separator-color'),
dividers: o3r.get-mandatory($application-variables, 'separator-color'),
elevation: o3r.get-mandatory($application-variables, 'shadow-color'),
hint-text: o3r.get-mandatory($application-variables, 'text'),
secondary-text: o3r.get-mandatory($application-variables, 'text'),
icon: o3r.get-mandatory($application-variables, 'text'),
icons: o3r.get-mandatory($application-variables, 'text'),
text: o3r.get-mandatory($application-variables, 'text')
);
$background-override: (
background: o3r.get-mandatory($application-variables, 'panel-background'),
hover: o3r.get-mandatory($application-variables, 'panel-hover'),
card: o3r.get-mandatory($application-variables, 'panel-background'),
dialog: o3r.get-mandatory($application-variables, 'dialog-background')
);
@return map_merge(
$mat-theme,
( foreground: $foreground-override,
background: $background-override,
)
)
}
The overridden theme is then enhanced with the application theme computed and the other - optional - palettes.
Note To use the get-application-property
function provided in the otter library - see
Apply the theme - the theme variables shall be stored within an application node.
@function generate-app-theme($primary, $main, $highlight, $warn, $is-dark-theme: false, $override: ()) {
$theme: generate-theme(
$primary: $primary,
$highlight: $highlight,
$accent: $accent,
$warn: $warn,
$application: _generate-app-variables($is-dark-theme, $override),
$is-dark-theme: $is-dark-theme
);
@return _override-mat-theme($theme);
}
// Result
// app-theme: (
// primary: (
// 50: #ffebee,
// ...
// A700: #d50000,
// contrast: (
// 50: #61688f,
// ...
// A700: #fff,
// ),
// default: #f44336,
// lighter: #ffcdd2,
// darker: #b71c1c,
// default-contrast: #fff,
// lighter-contrast: #61688f,
// darker-contrast: #fff,
// ),
// highlight: (
// ...
// ),
// accent: (
// ...
// ),
// warn: (
// ...
// ),
// is-dark-theme: false,
// foreground: (
// base: black,
// divider: #e3e3e3,
// dividers: #e3e3e3,
// disabled: $dark-disabled-text,
// disabled-button: rgba(black, 0.26),
// disabled-text: $dark-disabled-text,
// elevation: #f3f3f3,
// hint-text: #61688f,
// secondary-text: #61688f,
// icon: #61688f,
// icons: #61688f,
// text: #61688f,
// slider-min: rgba(black, 0.87),
// slider-off: rgba(black, 0.26),
// slider-off-active: rgba(black, 0.38),
// ),
// background: (
// status-bar: black,
// app-bar: map_get($mat-grey, 900),
// background: #303030,
// hover: rgba(white, 0.04),
// card: map_get($mat-grey, 800),
// dialog: map_get($mat-grey, 800),
// disabled-button: rgba(white, 0.12),
// raised-button: map-get($mat-grey, 800),
// focused-button: $light-focused,
// selected-button: map_get($mat-grey, 900),
// selected-disabled-button: map_get($mat-grey, 800),
// disabled-button-toggle: black,
// unselected-chip: map_get($mat-grey, 700),
// disabled-list-option: black,
// ),
// variables: (
// border-style: solid,
// border-color: #e3e3e3,
// separator-style: solid,
// separator-color: #fdfdfd,
// corner-border-radius: 20px,
// ... Basic theme variables ...
// )
// );
The resulting is a meta theme which will be used in the material and the application mixins as well as within the component stylesheet - see Use your custom theme.
Note: The four palettes material palette are the only one available to style the material components. If you need to style a material component with a new palette (e.g. highlight), you will need to override it with a mixin - see Customize the material elements.
The process to customize the application theme is similar to the Angular material one as describe in Theming your Angular Material app
The main difference is that you will need to generate your theme and typography via the application custom generators instead of the Angular one. For more information on the typography, please refer to TYPOGRAPHY.md.
The most common override case is the palette override. By default, the generator uses the Amadeus palettes stored in
~@o3r/styling/scss/theming/palettes/_amadeus.scss
, you can override it by passing the palette name
as a parameter:
// _styling.scss
@use '@angular/material' as mat;
@use '@o3r/styling/otter-theme' as otter-theme;
$primary: mat.$mat-indigo;
$highlight: mat.$mat-pink, A200, A100, A400;
// Override the amadeus theme:
$candy-app-primary: mat.palette(mat.$mat-indigo);
$candy-app-accent: mat.palette(mat.$mat-pink, A200, A100, A400);
// Generate Meta Theme
$candy-meta-theme: otter-theme.generate-otter-theme($primary: $candy-app-primary, $highlight: $candy-app-accent);
// Convert Meta theme to Material Design Theme
$candy-mat-theme: otter-theme.meta-theme-to-material($candy-meta-theme) !default;
// Convert Meta theme to Otter Theme
$candy-theme: otter-theme.meta-theme-to-otter($candy-meta-theme) !default;
// index.scss
@use '@angular/material' as mat;
@use '@o3r/styling' as o3r;
// Import the application styling
@import './styling';
// The theme to apply to the whole application
@include o3r.apply-theme($candy-theme);
@include o3r.apply-typography($typography);
// See https://material.angular.io/guide/theming for details
@include mat.core($typography);
@include mat.all-component-typographies($typography);
@include mat.all-component-themes($candy-theme);
In some cases, you might need to override some specific variable - for example the panel background. This can be done by passing a map with the new theme variables.
Note that nothing prevents you from overriding both the palettes and the theme variables.
Example :// overrides
$override-original-theme: (panel-background: #AAA);
// Include the default theme styles.
$meta-theme: generate-app-theme($override: $override-original-theme);
[!IMPORTANT] The palette should always be generated with
mat.define-palette
to fit the material Angular format!
The component style should rely on variables with default value. As much as possible, the variables shall rely on the
theme properties. This way, they will always be consistent with the generated theme.
The variables shall be included in a my-component.style.theme.scss
files which shall be included in the component
stylesheet.
This will allow a component level customization.
//file: ./my-component-pres.style.scss
@import './my-component-pres.style.theme.scss';
// Move
.my-class {
color: $my-class-main-color;
border-radius: $my-class-border-radius;
}
@o3r/styling
provides functions to access the theme variables.
The principal functions are the following:
$theme
sub nodes.<css-var-name>
*, <default-value>
)**: (alias: o3r.variable) helper function that will generate and return a css-variable accessor (ex: or3.variable('my-var', #000)
will generate var(--my-var, #000)
). The purpose is to allow the override of a component variable global css-var.@o3r/styling/otter-theme
provides functions to access the theme variables.
The principal functions are the following:
<theme-palette>
*, <value>
)**: similar to mat-color, it will retrieve the color from the theme palette. Instead of printing directly the color, the function will generate a css-var (ex: otter-theme.color($primary-palette, 500)
will generate var('--primary-color-500', #050)
).<theme-palette>
*, <value>
)**: similar to mat-contrast, it will retrieve the color from the theme palette. Instead of printing directly the color, the function will generate a css-var (ex: otter-theme.contrast($primary-palette, 500)
will generate var('--primary-color-contrast-500', #505)
).//file: ./my-component-pres.style.theme.scss
@use '@o3r/styling' as o3r;
@use '@o3r/styling/otter-theme' as otter-theme;
// Will fail if $theme is not generated
$primary-palette: o3r.get-mandatory($theme, 'primary');
$page-title-color: o3r.var('page-manage-title-color', otter-theme.color($palette-highlight, 500));
$page-title-background: o3r.var('page-manage-background', otter-theme.contrast($palette-highlight, 200));
Since the Otter theming mechanism is based on CSS variable, it can easily be overridden from the application (example):
Example ::root body {
--primary-700: #000;
--page-title-color: #00A;
}
[!NOTE] The list of defined variables is accessible (at runtime) in (Chrome) DevTools and can be modified directly in the console without rebuild required. The full list of available variables of the application is accessible in the
style.metadata.json
and any CSS variable can be added during application runtime (via the DevTools).
To override the style for components you can define component override files, which will be imported by your app level styling file.
Here is an example of how your files architecture could look:
component-styling-override.scss
component-variables-override.scss
app-styling.scss: import component-styling-override.scss, component-variables-override.scss
...component
presenter
my-component-pres.component.ts: styleUrls='my-component-pres.style.scss'
my-component-pres.style.scss: component style - import style.theme.scss
my-component-pres.style.theme.scss: variables - import app-styling.scss
...
The function apply-theme
expects a specific structure of SCSS Map. To facilitate the generation of the latter, the function meta-theme-to-otter
converts an Angular Material Theme into a compatible structure.
The theme structure object should respect the following Schema:
Example :{
"$ref": "#/definitions/varNode",
"definitions": {
"varNode": {
"type": "object",
"patternProperties": {
"^[^ ]+$": {
"oneOf": [
{"$ref": "#/definitions/varNode"},
{"$ref": "#/definitions/varValue"}
]
}
}
},
"varValue": {
"type": "object",
"properties": {
"required": ["value"],
"value": {"type": "string"},
"details": {
"type": "object",
"properties": {
"description": {"type": "string"},
"label": {"type": "string"},
"type": {
"type": "string",
"enum": ["color", "string"]
},
"category": {"type": "string"},
"tags": {
"type": "array",
"item": {"type": "string"}
},
}
}
}
}
}
}
The additional information, specified in the details
property, is used at extraction time to provide variable context information to display in the CMS.
The details property contains the following information:
string
and color
types are supported)This is an example of a theme structure:
Example :@use '@o3r/styling' as o3r;
$my-theme: (
o3r: (
primary: (
color: (
value: '#000',
details: ( // Optional property
description: 'My primary color used as website main color', // Optional property
label: 'My primary color', // Optional property
type: 'color', // Optional property
category: 'main color', // Optional property
tags: ('main', 'color', 'primary') // Optional property
)
)
)
)
);
@include o3r.apply-theme($my-theme);
This will result in the following CSS:
Example ::root {
--o3r-primary-color: #000;
}
In certain cases we want to define a variable and make it part of the extracted metadata without defining a full Otter Theme.
This can be achieved via the o3r.var
mixin. If we take the previous example, the same result (in term of CSS and metadata) can be done as following:
@use '@o3r/styling' as o3r;
:root {
@include o3r.var('o3r-primary-color', #000, (description: 'My primary color used as website main color', label: 'My primary color', type: 'color', category: 'main color', tags: ('main', 'color', 'primary')));
// As details parameter is optional, it can be reduced to `@include o3r.var('o3r-primary-color', #000)`
}
[!NOTE] The mixin
o3r.var
is an alias ofo3r.define-var
.
Please beware that the mixin o3r.var
and the function o3r.var
are similar and made to work in different contexts:
@use '@o3r/styling' as o3r;
body {
$myVariable: o3r.var('my-color', #000);
background-color: $myVariable;
}
// will generate "background-color: var(--my-color, #000)" in CSS
// the purpose is to be able to use the variable --my-color without defining it value (but using the default value)
@use '@o3r/styling' as o3r;
body {
@include o3r.var('my-color', #000);
background-color: o3r.var('my-color'); // equivalent to `--my-color`
}
// will generate "--my-color: #000" in CSS
// the purpose is to be able to define the variable --my-color
In both cases the variable will be extracted to the metadata with the specified default value.
Both the function and the mixin o3r.var
can be used to override variable details.
@use '@o3r/styling' as o3r;
// generate CSS variables from a theme:
@include o3r.apply-theme($my-theme);
:root {
// override the value and details of the variable "my-variable" from the theme:
@include o3r.var('my-variable', #000, (description: 'new description'));
// override only the details of the variable "my-variable" from the theme:
@include o3r.var('my-variable', null, (description: 'new description'));
}
// override only the details of the variable "my-variable" from the theme:
$res: o3r.var('my-variable', null, (description: 'new description'));
@debug "override details of #{$res}";