@opengeoweb/theme
v9.33.0
Published
GeoWeb Theme library for the opengeoweb project
Downloads
2,281
Keywords
Readme
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
- Rules of theme lib
- Folder structure
- Naming convention
- Theme Stories
(lib/stories)
- important note about Material UI v5
- Guidelines and tips for translating design to code
- Do's and dont's
- Known issues
- Roadmap
- Unit testing
- Image snapshot testing
- Questions and feedback
- Documentation
Table of contents generated with markdown-toc
Quick start
Running storybook
nx storybook theme
Adding styles/colors
- Add the name and type on the GeowebColorPalette type
// libs/theme/src/lib/components/Theme/types.ts
export type GeowebColorPalette = {
buttons: {
primaryMouseOver: CSSProperties,
},
// ... rest of type
};
- Add the style/color values in lightTheme and darkTheme
/// libs/theme/src/lib/components/Theme/lightTheme.ts
export const colors: GeowebColorPalette = {
buttons: {
primaryMouseOver: {
fill: '#186DFF',
border: '#71A6FF',
},
},
- 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
- Make sure the theme ThemeProvider is used
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
- 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 asgeowebColor
property inside thetheme.palette
of the MUI Theme
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.
- Use
useTheme
selector and pass theme as argument to the draw method:
// 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>
);
};
- 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.
// 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,
);
- Use the color (or any other given property) by deconstructing the passed theme if needed, and if needed use the
rgba
property.
// 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.
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/StoryWrapper.tsx
// 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 aCssBaseline
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
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.
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:
// 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.
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.
// 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. Other way around as well, if any names have been changed Didier 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 build with the StoryWrapper
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 examplergba
- 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:
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.
// 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.
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 increateTheme
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 byuseTheme
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:
- Components
- Base components following MUI names as Container, Buttons, Elevation, Cards, Elevation, Table
- Grouped components combining Base components as Header, Top Bar
- Modules
- TimeSlider, Sigmet, LayerManager etc
Layer manager example
In this example, we're going to have a look at the Layer manager.
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 theth
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! 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 thetheme
lib. Don't import the ThemeProvider wrapper in the failed test, but use theCoreThemeProvider
orCoreThemeStoreProvider
found inlibs/core/src/lib/components/Providers/Providers.tsx
- 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
- 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
- You need to have docker installed and running.
- 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.) - Run the snapshot tests:
npm run test:image-snap-theme
. This will first create a new static storybook build and then run the tests. - If a snapshot test fails, you can find and inspect the differences in
libs/theme/src/lib/__image_snapshots__/__diff_output__/
. - To update the snapshots, run
npm run test:image-snap-theme-update
. Snapshots are saved underlibs/theme/src/lib/__image_snapshots__/
. Make sure to commit the new snapshots. - 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
Documentation
https://opengeoweb.gitlab.io/opengeoweb/docs/theme/
Written with StackEdit.