npm package: @opengeoweb/theme
Description: GeoWeb Theme library for the opengeoweb project
Latest version: 17.0.1

# Theme

The aim of this library is to make sure all MUI components look as specified in the designs found on Zeplin. There are multiple themes (dark and light) with color and styling which can be added and all values are accessible throughout the whole application.

## Content

- [Quick start](#quick-start)
  - [Running storybook](#running-storybook)
  - [Adding styles/colors](#adding-styles-colors)
  - [Using styles/colors to (React/MUI) components](#using-styles-colors-to--react-mui--components)
  - [Using styles/colors in canvas components](#using-styles-colors-in-non--canvas--mui-components)
  - [Using themes in stories](#using-themes-in-stories)
- [Rules of theme lib](#rules-of-theme-lib)
- [Folder structure](#folder-structure)
  - [Icons `(lib/components/Icons/Icons.tsx)`](#icons---lib-components-icons-iconstsx--)
  - [types `(lib/components/Theme/types.ts)`](#types---lib-components-theme-typests--)
  - [themes `(lib/components/Theme/darkTheme.ts, .../lightTheme.ts)`](#themes---lib-components-theme-darkthemets---lightthemets--)
  - [ThemeContext `(lib/components/Theme/ThemeContext)`](#themecontext---lib-components-theme-themecontext--)
  - [utils `(lib/components/Theme/utils.ts)`](#utils---lib-components-theme-utilsts--)
  - [stories `(lib/stories)`](#stories---lib-stories--)
- [Naming convention](#naming-convention)
- [Theme Stories `(lib/stories)`](#theme-stories---lib-stories--)
  - [Overwrite default MUI component styling](#overwrite-default-mui-component-styling)
- [important note about Material UI v5](#important-note-about-material-ui-v5)
- [Guidelines and tips for translating design to code](#guidelines-and-tips-for-translating-design-to-code)
  - [Layer manager example](#layer-manager-example)
    - [Wrapper](#wrapper)
    - [Top bar](#top-bar)
    - [Table](#table)
    - [Footer](#footer)
- [Do's and dont's](#do-s-and-dont-s)
  - [Do's](#do-s)
  - [Don'ts](#don-ts)
- [Known issues](#known-issues)
- [Roadmap](#roadmap)
- [Unit testing](#unit-testing)
- [Image snapshot testing](#image-snapshot-testing)
  - [Add a new snapshot test](#add-a-new-snapshot-test)
  - [Running snapshot tests and updating snapshots locally](#running-snapshot-tests-and-updating-snapshots-locally)
- [Questions and feedback](#questions-and-feedback)
- [Documentation](#documentation)

<small><i><a href='http://ecotrust-canada.github.io/markdown-toc/'>Table of contents generated with markdown-toc</a></i></small>

## Quick start

### Running storybook

```
nx storybook theme
```

### Adding styles/colors

1. Add the name and type on the GeowebColorPalette type

```javascript
// libs/theme/src/lib/components/Theme/types.ts
export type GeowebColorPalette = {
  buttons: {
    primaryMouseOver: CSSProperties,
  },
  // ... rest of type
};
```

2. Add the style/color values in lightTheme and darkTheme

```javascript
/// libs/theme/src/lib/components/Theme/lightTheme.ts
export const colors: GeowebColorPalette = {
  buttons: {
    primaryMouseOver: {
      fill: '#186DFF',
      border: '#71A6FF',
    },
  },
```

3. Verify it works by navigating to the Color story demo and see the new colors work in all themes. Every property you fill in is accessible throughout every app or library.

### Using styles/colors to (React/MUI) components

1. Make sure the theme ThemeProvider is used

```javascript
import { ThemeProvider } from '@opengeoweb/theme';

export const Wrapper: React.FC = () => (
  <ThemeProvider>
    <MyComponent />
  </ThemeProvider>
);
```

> **note** make sure to use the ThemeProvider in unit tests too, as they will fail otherwise

2.  Use the style/color in your component. You can do this directly by passing an style object as `sx` props or create an object with styles which could be handy if you need to reuse styles. The theme palette can be found as `geowebColor` property inside the `theme.palette` of the MUI Theme

```javascript
const styles = {
  button: {
    border: 'geowebColors.buttons.flatMouseOver.border',
    '&:hover': {
      backgroundColor: 'geowebColors.buttons.flatMouseOver.fill',
    },
  },
};

const MyButton: React.FC = () => {
  return <Button sx={styles.button}>content</Button>;
};
```

### Using styles/colors in canvas components

Since the color values are depending on the current selected theme, we can't use a static object for color values. A simple solution for this is to access the theme via `useTheme` hook selector, and pass it as a param to the canvas drawing function.

1. Use `useTheme` selector and pass theme as argument to the draw method:

```javascript
// libs/core/src/lib/components/TimeSlider/TimeSliderLegend/TimeSliderLegend.tsx
const TimeSliderLegend: React.FC<TimeSliderLegendProps> = (
  props: TimeSliderLegendProps,
) => {
  const theme = useTheme();
  return (
    <div>
      <CanvasComponent
        onRenderCanvas={(ctx: CanvasRenderingContext2D): void => {
          renderTimeSliderLegend(
            ctx,
            theme,
            // ...props and other methods
          );
        }}
      />
    </div>
  );
};
```

2. Extend the render method with theme as argument, and pass if necessary the theme further to the other render methods. Most easy way is to just pass the whole theme as object, so you can use the `Theme` type from MUI.

```javascript
// libs/core/src/lib/components/TimeSlider/TimeSliderLegend/TimeSliderLegendRenderFunctions.tsx
import { Theme } from  '@mui/material';

export const renderTimeSliderLegend = (
  context: CanvasRenderingContext2D,
  theme: Theme,
  canvasWidth: number,
  height: number,
  centerTime: number,
  secondsPerPx: number,
  selectedTimeUnix: number,
  scale: Scale,
  currentTimeUnix: number,
): void => {
  const ctx = context;

  drawBackground(
    ctx,
    theme,
    visibleTimeStart,
    visibleTimeEnd,
    canvasWidth,
    height,
    scale,
  );
```

3. Use the color (or any other given property) by deconstructing the passed theme if needed, and if needed use the `rgba` property.

```javascript
// libs/core/src/lib/components/TimeSlider/TimeSliderLegend/TimeSliderLegendRenderFunctions.tsx
const drawBackground = (
  context: CanvasRenderingContext2D,
  theme: Theme,
  visibleTimeStart: number,
  visibleTimeEnd: number,
  canvasWidth: number,
  height: number,
  scale: Scale,
): void => {
  const ctx = context;
  const { timelineTimelineSurface, timelineNightTime} = theme.palette.geowebColors.timeSlider

    ctx.fillStyle = isColorIntervalEven(scale, timestep)
      ? timelineTimelineSurface.rgba
      : timelineNightTime.rgba;
  });
};
```

### Using themes in stories

When using a component which relies on theme styling, wrap the main component of your story with the `ThemeWrapper` component instead of `ThemeProvider`. `ThemeWrapper` will later get an extra `StyleEngineProvider` parent component (see https://mui.com/guides/migration-v4/#style-library).

If you need to show multiple stories and don't want to see the default background, use the `disableCssBaseline` property to prevent that.

```javascript
import { darkTheme, ThemeWrapper } from '../../components/Theme';

export const TableDark = (): React.ReactElement => (
  <ThemeWrapper theme={darkTheme}>
    <TableDemo />
  </ThemeWrapper>
);
```

## Rules of theme lib

- It should not export any other component than the themes, ThemeProvider and corresponding hooks and the icons. This lib is only showing the MUI components and icons with the correct default style. If you need a component which needs custom props and should be used across different applications, consider making a new component in the `shared` lib.
- It should not import any library other than MUI.

## Folder structure

```
// lib/components
// lib/components/Icons/Icons.tsx
// lib/components/Theme/darkTheme.ts
// lib/components/Theme/lighTheme.ts
// lib/components/Theme/utils.ts
// lib/components/Theme/types.ts
// lib/components/Theme/ThemeContext.tsx

// lib/stories
// lib/stories/story.stories.tsx
```

#### Icons `(lib/components/Icons/Icons.tsx)`

All icons are defined here.

#### types `(lib/components/Theme/types.ts)`

Defines the color palette of the theme.

#### themes `(lib/components/Theme/darkTheme.ts, .../lightTheme.ts)`

Exports files containing all the values per theme.

#### ThemeContext `(lib/components/Theme/ThemeContext)`

- exports `ThemeProvider` wrapper for all MUI components including a `CssBaseline` component of MUI.
- exports an `useThemeContext` to switch themes.
- exports `ThemeWrapper` wrapper for stories

#### utils `(lib/components/Theme/utils.ts)`

- **hex2rgba** handles hex values to rgba
- **parseColors** parses the theme values and when opacity and fill are given, adds a rgba value. This is handy for canvas components, as they expect one value to render a fill.

  If in future another parser is needed (for example a font parser: when fontSize and fontFamily is given, return font which combines those values), it can be added here,

- **createShadows** creates a list of shadows (elevations) as specified by design . These can be accessed through the theming by `theme.shadows`.
- **createTheme** function that creates the theme with given colors and shadows. This also override default MUI components. It sets the values of the theme as `geowebColors` inside the palette of the MUI Theme.

#### stories `(lib/stories)`

Contains demos of MUI components within the theme. [More information](#theme-stories---lib-stories--)

## Naming convention

When adding a new color, it needs to follow the names provided by design. It follows the pattern segmentName.elementName.value or segmentName.elementName.elementProp1.value.

![naming convention](docs/namingConvention.png?raw=true)

If you look at the example above, you can see two segments: Background and Buttons. Backgrounds don't need much properties other then fill, so we don't need deep value. Buttons on the other hand have next to a fill a border, so it makes sense to specify it a bit more:

```javascript
// lib/components/Theme/lightTheme.ts
  background: {
    surface: '#FFFFFF',
    surfaceApp: '#F5F5F5',
    surfaceBrowser: '#CFCFCF',
  },
  buttons: {
    primary: {
      fill: '#F186DFF',
      border: 'none',
    },
    tertiary: {
      fill: 'none',
      border: '#0075A9',
    }
  }
```

Some Components have more colors sections. Take a look at some of the colors of the Timeslider.

![enter image description here](docs/timesliderColors.png?raw=true)

> **Note:** Some color names have (D) after their name (currently only in Dark theme). This means the color already defined in the main Color Palette and you don't have to define it again.

The segment is TimeSlider, and a sub segment is Player and Time span. We don't want to create another level of depth so we solve this by adding this 'sub segment' as a prefix to the name of the element.

```javascript
// lib/components/Theme/darkTheme.ts
 timeSlider: {
    playerNeedlePlayerTop: {
      fill: '#E3004F',
      opacity: 100,
    },
	timeScaleText: {
      fontSize: 12,
    },
    timeScaleTimeIndicatiors: {
      fill: '#A2A2A2',
    },
  },
```

> **Note:** fill is often used for elements but color for example is also possible for font elements

If there are mistakes in naming, colors are missing in the Design, contact [Didier](https://gitlab.com/didier.dezanet). Other way around as well, if any names have been changed [Didier](https://gitlab.com/didier.dezanet) should create a ticket to fix it here as well to keep consistency.

## Theme Stories `(lib/stories)`

Contains demo stories with MUI components. Every story is wrapped with a decorator defined in `libs/theme/.storybook/preview.js` component and can be toggled from light to dark theme. It does not export anything, it only shows the MUI components in light or dark theme.

- The **Color** story shows all the colors, styles and other properties of the defined theme. Could be handy for reviewing styling values. On the right the `geowebColor` object is shown, and these values are accessible throughout all components with the ThemeProvider wrapper. Not that more values are shown than given, this is because of the parsers that adds in some cases extra properties as for example `rgba`
- The **Elevation** shows all elevations we currently have. These can be used for defining `box-shadow` for elements. If you want to use the elevation in code:

```javascript
const styles = {
  header: {
    boxShadow: 1, // elevation_01
  },
};
```

- The rest of the stories are for showing the MUI elements without any styling, other then the given theme styling.

### Overwrite default MUI component styling

When developing and using a new component of the MUI library which has no theme story, it could be a good idea to create a story, add the component there, and add some theme styling. That way there is a clear example how the component will look in the MUI environment.

In the function below `createTheme` takes the value of theme and shadows (elevation) and creates the theme. Some default components are overwritten as MuiCssBaseline and MuiRadio with given theme styling.

```javascript
// libs/theme/src/lib/components/Theme/utils.ts
export const createTheme = (
  paletteType: PaletteType,
  geowebColors: GeowebColorPalette,
  shadows: Shadows,
): Theme =>
  createMuiTheme({
    palette: {
      background: {
        paper: geowebColors.background.surfaceApp,
        default: geowebColors.background.surfaceBrowser,
      },
      text: {
        primary: geowebColors.typographyAndIcons.text,
      },
      // geoweb color palette
      geowebColors,
    },
    shape: { borderRadius: BORDER_RADIUS },
    typography: {
      fontFamily: ['Roboto', 'Helvetica', 'Arial', 'sans-serif'].join(','),
    },
    shadows,
    overrides: {
      MuiCssBaseline: {
        '@global': {
          body: {
            fontSmoothing: 'auto',
          },
        },
      },
      MuiRadio: {
        root: {
          color: geowebColors.typographyAndIcons.iconLinkActive,
        },
      },
    }
```

> **Note:** when you add or change a style of a MUI component, every component will look default that way. If you need for example need some more custom styling or props, consider creating a reusable component in the `shared` library. Remember this library does not export components.

## important note about Material UI v5

Material UI v5 is just around the corner. This will making theming much easier with for example the possibility of [adding custom variants](https://next.material-ui.com/customization/theme-components/#adding-new-component-variants).

For example, take a look in the designs of the [Buttons]. (https://app.zeplin.io/project/5ecf84a3c6ae1047a368f393/screen/5ecf85c60f301e47ca4eee55) There are multiple variants of the Button specified, but the MUI Button only accepts the default variant names supplied by MUI (contained, outlined, text). In future we could add custom new variants as primary, secondary and tertiary.

If you want to have a reusable Button in this case there are two options:

- Create a Button story in `theme` lib, add some buttons with variants of props that are matching with design. For example `<Button variant='filled' color="primary" />`. You probably need some overriding of styles so that can be added in `createTheme` at the override section. This is not ideal, as the it's still a bit of matching and combining with props, and not all names of design we can add.
- Create a Button story in `shared` lib, import the Button of MUI, and add there all the (custom) variants you need. Colors and style can be retrieved by `useTheme` hook selector, this is a better solution as it won't break anything, and you name all props like provided in design to keep it consistent.

## Guidelines and tips for translating design to code

The designs found in Zeplin can roughly be split in two:

1. Components
   - Base components following MUI names as Container, Buttons, Elevation, Cards, Elevation, Table
   - Grouped components combining Base components as Header, Top Bar
2. Modules
   - TimeSlider, Sigmet, LayerManager etc

### Layer manager example

In this example, we're going to have a look at the Layer manager.

![enter image description here](./docs/layerManager.png?raw=true)

Looking at the design, we can see it's consisting mainly out these elements:

- Wrapper
- Top bar
- Table
- Footer

#### Wrapper

This is the first component which holds all sub components which compose the Layermanager. Looking it from a MUI perspective, the first element should be a Paper because it's a surface, and has the background name `Background Surface app`. The box-shadow is a side effect of elevation, the higher the elevation, the more shadow. Elevation is a default property of Paper, and since the elevations are also defined in the theme we can use those.

#### Top bar

If you look at the header and forget the left group, you can see it's a header that is used on multiple places like Sigmet dialog header, LayerManager and MultiDimensionSelect but in different sizes. It makes sense to create a reusable component in the `shared` library with a property size to ensure all sizes work correctly.

> link to design: https://app.zeplin.io/project/5ecf84a3c6ae1047a368f393/screen/60f9319044360a123ca42552

#### Table

The elements of the layermanager are build on top of the table design. In this case, it would make sense to create a new Table\* story in the `theme` library. There we make a story where we are using the MUI Table components, and make sure it has all the correct colors and styling. When that is working correctly, we can use the Table component everywhere and it will look the same everywhere.

Every row renders columns with different inputs; for example for the layers list we can use the MUI `MenuItem` component. It would make sense to create a separate story for `MenuItem`, and make sure all base colors are correct.

By wrapping them all together, there's probably some additional styling needed specific for the LayerManager but that's perfectly fine (it can be done in for example the `Wrapper` described above). The goal is that the `MenuItem` and `Table` will have a good default look to use in other places as well.

> link to design: https://app.zeplin.io/project/5ecf84a3c6ae1047a368f393/screen/6093e69005029c358090bd4e

> **\*note** it's a assumption the Table component will work for the LayerManager. Currently there's a small story demo that shows's the styling so far, but not with for example max-height functionality. This is possible with adding `position:sticky` to the `th` elements, but needs further investigation if this component can fully suit our needs.

#### Footer

This footer is only used for when the wrapper is resizable. Therefore it would make sense to make it part of a Resizable component. It can use the default values from the theme by using `useTheme` hook.

## Do's and dont's

### Do's

- do follow design names, if you feel the name has too much repeat in it or can be changed, ask [Didier](https://gitlab.com/didier.dezanet)! Same for missing colors .
- do add links to design in code
- do create shared components that are used in opengeoweb application in the `shared` lib
- do add a ThemeProvider for components with a new theme styling. Otherwise unit tests will break
- do discuss the library and the usage of it. If you think it can be approved please let us know!

### Don'ts

- don't add double colors (see TimeSlider example). In dark theme designs, color names suffixed with a (D) are color names defined in the main Color page.
- don't override the theme for MUI components when they are specific for a design. For example the buttons in the Timeslider don't follow the exact rules of the main Buttons, so it's a good idea to make a custom TimeSliderButton component which uses the colors defined in the theme palette.

## Known issues

- **Expect unit tests to fail** when using components with the theme. This is easily fixed by wrapping your test with the `ThemeProvider` from the `theme` lib. Don't import the ThemeProvider wrapper in the failed test, but use the `CoreThemeProvider` or `CoreThemeStoreProvider` found in [`libs/core/src/lib/components/Providers/Providers.tsx`](https://gitlab.com/opengeoweb/opengeoweb/-/blob/master/libs/core/src/lib/components/Providers/Providers.tsx#L36)
- **MUI 5** makes it easy to add custom variants to components, so we should wait for that release before investing more heavy in theme stories: https://next.material-ui.com/customization/theme-components/#adding-new-component-variants . Check the progress of the release of [v5 here](https://github.com/mui-org/material-ui/milestone/44)
- The `ThemeProvider` imports the **CssBaseline** components which allows us to set a body background and also resets some initial browser styling values. One thing it's resetting is the css **box-sizing** property. If you experience misaligning in (canvas) components, this might be the property you want to check out.

## Unit testing

```
nx test theme
```

### Running snapshot tests and updating snapshots locally

[Read more about snapshot testing](https://gitlab.com/opengeoweb/opengeoweb/#image-snapshot-testing)

1. You need to have [docker](https://docs.docker.com/get-docker/) installed and running.
2. Start Chromium by running: `npm run start-snapshot-server`. (This will start a docker container with chromium, to run snapshot tests in. We need this to make sure everyone gets the same snapshot results.)
3. Run the snapshot tests: `npm run test:image-snap-theme`. This will first create a new static storybook build and then run the tests.
4. If a snapshot test fails, you can find and inspect the differences in `libs/theme/src/lib/__image_snapshots__/__diff_output__/`.
5. To update the snapshots, run `npm run test:image-snap-theme-update`. Snapshots are saved under `libs/theme/src/lib/__image_snapshots__/`. Make sure to commit the new snapshots.
6. Stop Chromium by running: `npm run stop-snapshot-server`.

## Questions and feedback

Everything written here and coded is open for feedback. If you have any code related questions, please contact the GeoWeb team, if you have any questions about the design, naming and or guidelines please contact the designer [Didier](https://gitlab.com/didier.dezanet)

## Documentation

https://opengeoweb.gitlab.io/opengeoweb/docs/theme/

> Written with [StackEdit](https://stackedit.io/).