Skip to content

Latest commit

 

History

History
556 lines (442 loc) · 20.4 KB

THEME.md

File metadata and controls

556 lines (442 loc) · 20.4 KB

Theme

About the theme

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:

  • Are the properties highly customizable? This concerns the colours palette used within the components. The client will necessarily have to modify them to follow their brand.
  • Are the properties mostly customizable? This concerns a set of properties and variables such as the shadows, background and foreground that might be custom to change the general look and feels of the application. For instance, if the client wants to impact the shadow colors on a darker theme, the border radius on the buttons, on the list or on the different containers.
  • Are these changes specific to a component? If the client changes are really specific and impact the product designs, this will be done on component level. This is not part of the theme customization.

For more information on the Amadeus palettes you can refer to Amadeus Color Guidelines.

Create your theme

You can find below the process to generate your own theme.

Generate your own theme variables

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
// )

Apply your theme on the material theme

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:

  • primary palette for form, active button etc.
  • accent palette to highlight elements
  • foreground palette with border, text, shadows etc.
  • background palette with the different background in material components

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.

Use your custom theme

Customize the Otter theme

Process

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.

// 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!

Architecture

Style your components

Variables

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;
}

Access to variables

@o3r/styling provides functions to access the theme variables. The principal functions are the following:

  • o3r.get-mandatory: similar to map-get from native SCSS but will fail at build time if the variable is not accessible in the map. This is useful to access to $theme sub nodes.
  • o3r.var(<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.

Access to Otter theme properties

@o3r/styling/otter-theme provides functions to access the theme variables. The principal functions are the following:

  • otter-theme.color(<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)).
  • otter-theme.contrast(<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));

Style Override

Since the Otter theming mechanism is based on CSS variable, it can easily be overridden from the application (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).

Component style override

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
...

Technical structure (advance)

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:

{
  "$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"}
            },
          }
        }
      }
    }
  }
}

Details property

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:

  • description: Medium/long description of the variable
  • label: Caption describing the variable, if not provided the CMS will use the variable name instead
  • type: Additional context to help the CMS display the most relevant input widget (currently only string and color types are supported)
  • category: Way to group different variables in the CMS
  • tags: List of tags associated to the variable. It is a way of grouping variables or flagging them.

Example

This is an example of a theme structure:

@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:

:root {
  --o3r-primary-color: #000;
}

Define variable outside of theme mechanism

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 of o3r.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.

Override variable details

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}";