amboss-design-system
📄 Browse the design system (Storyboard documentation)
Quick start
Development
Ensure that you are using npm
>= 7.0.0 before running npm install
To start the project run npm run start
To generate a new component run npm run component myComponentName
Building the package
To build the package run npm run build
Building storybook
To build storybook run npm run build-storybook
Publishing
- Add
minor
, major
or patch
label to your PR to specify release type. - Add
skip-release
label to skip the release - If you want your commit to skip ci, add "[skip ci]" to your commit message.
Generating mobile tokens
- Install Node.js and npm by following the official installation guide
- Install dependencies:
npm install
- Generate the tokens:
npm run tokens:ios
npm run tokens:android
Generating mobile icons
- Install Node.js and npm by following the official installation guide
- Install dependencies:
npm install
- Generate the tokens:
npm run icons:ios
npm run icons:android
Codebase Overview
1. Tech stack
2. Linting
3. Folder structure
4. Build
5. Tokens, styles and theming
6. Assets
7. Media queries
Tech stack
- Code: typescript
- Styles: emotion.js
- Framework: react
- Bundler: webpack
- Environment: storybook
- Tokens: style-dictionary
- Testing: jest + react testing library
Linting
As for linting and formatting, you can configure your editor to automatically lint and format your code on save. For this purpose, we're using Prettier and ESLint. If you need to do it manually, you can run:
npm run lint
This command is also run as a pre-push hook and in the pipeline.
Folder structure
|-- .storybook
|-- components
|-- ColorGrid.tsx
|-- SizeGrid.tsx
|-- main.js
|-- preview.js
|-- assets
|-- fonts
|-- Lato.woff
|-- build (autogenerated)
|-- build-icons (autogenerated)
|-- android
|-- ic_activity_16.xml
|-- ic_activity_24.xml
|-- ios
|-- activity.imageset
|-- activity.pdf
|-- activity16.pdf
|-- Contents.json
|-- build-tokens (autogenerated)
|-- android
|-- design_system_color.xml
|-- ios
|-- DesignSystemColorTokens.swift
|-- scss
|-- _theming.scss
|-- _variables.scss
|-- assets
|-- icons.json
|-- icons16.json
|-- _colors.json
|-- _sizes.json
|-- visualConfig.ts
|-- docs
|-- Design-principles.mdx
|-- static
|-- image-for-storybook.png
|-- tokens
|-- global
|-- themes
|-- dark.json
|-- light.json
|-- assets.json
|-- colors.json
|-- web
|-- component
|-- dark.json
|-- light.json
|-- android
|-- opacity.json
|-- ios
|-- opacity.json
|-- src
|-- index.ts
|-- components
|-- Button
|-- Button.tsx
|-- Button.stories.tsx
|-- Button.test.tsx
|-- shared
-- mediaQueries.ts
|-- types
|-- index.ts
Build
There are 3 stages of a build.
- Generate variables, theme values and assets from tokens
- Build all exported components with babel (preset-react, preset-typescript)
- Bundle all built components with webpack
Generate variables, theme values and assets from tokens
In order to structure our tokens we use style-dictionary.
This tool is used for creation a content of ./build-tokens
folder where we keep all the scss variables and mixins.
New variables and mixins gets generated automaticaly:
- in watch mode while
npm run start
is running - after npm install
- before build
You can generate tokens manually using npm run tokens:web
and npm run tokens:watch
We use tokens to control the most atomic values in styles and themes.
How does it work?
We have a set of .json files that are located in ./tokens
folder, and we have a style-dictionary
as a tool for converting .json in to various formats.
For example:
We have a file - ./tokens/global/light|dark.json
that gets converted into part of ./build-tokens/visualConfig.ts
.
visualConfig.ts
is used later as a source for emotion.js theme.
The configuration for style-dictionary
is inside ./style-dictionary/{platformName}/{platformName}.sd.config.json
. In this config we use some custom transforms, filters and formatters, all of them defined in corresponding files of platform folders.
custom/format/emotion-visual-config - a formatter that generates visualConfig.ts that contains all variables and themed values.
custom/filter/emotion-build-tokens - a filter for all token categories that should go to visutal config.
custom/format/json - a formatter for generation .json files per category. This formatter also add isUsed
flag to according token.
custom/format/sub-theme-types - a formatter that generates type definitions for sub-themes.
custom/assets/svg - a transformer that adds a path name to dictionary of svg icons.
More about tokens, themes and styles.
Bundle all exported components
Before publishing the design system for NPM, we run npm run build
. This runs the script at ``bundles everything that is exported from.ts
and.tsx
files within the source.
For this, we use tsup
, which runs esbuild
under the hood. We also use a lenient browserslist config to ensure compatibility with older browsers.
For styles, we use emotion.js.
What in the bundle?
- JS code that is compiled from
.tsx
files (includes common components and emotion.js styled components) - SVG icons that are inlined
fonts that are base64-ed currently taken off the build
Tokens, styles and theming
Currently, we have 4 types of tokens:
- Atomic values, like colors, sizes, shadows.
- Font assets, that are basically base64 of font files. (not used in production)
- Theme values, that are referencing atomic different atomic values
- Svg icon assets, that are inlined to a
.json
file
As we use Emotion.js all tokenized values are accessible via theme provider.
Basically during the build phase we generate big objects of variables and themed values for each theme (currently light and dark).
We call this object ambossVisualConfiguration
.
Particular themes from ambossVisualConfiguration
has to be imported and passed to the theme provider at the consumer side.
Atomic values are straight forward and simply converted to variables
.
After build all atomic values are passed to theme.variables
by emotion.
Example:
const StyledContainer - styled.div(
({ theme }) => ({
marginTop: theme.variables.size.spacing.s
})
);
Theme tokens are little more complex. We generate separate VisualConfig for each theme, but together with that we generate type definition that describe any of the themes. This enables developers not care about current theme and how many themes are there.
After build all themes values are passed to theme.values
by emotion.
Example:
const StyledContainer - styled.div(
({ theme }) => ({
backgroundColor: theme.values.color.background.primary.default
})
);
Sub-themes are following the same concept as themes. Developer can fully rely on theme.values
object. As long as component in runtime is wrapped in SubThemeProvider
and its tokens listed in "tokens/themes/sub-themes/...", all values will be set correctly and automatically.
Note that not all tokens are enabled in sub-themes. Make sure to add missing ones to "tokens/themes/sub-themes/...".
Components that have a "Sub-themed" badge are already in.
Assets
- assets are defined in
tokens/assets.json
- with the help from style-dictionary we create a
json
file with tokens (/build-tokens/assets/
) - those
json
files supposed to be imported in the ts module (see Icons.tsx
) - in order to secure
d.ts
filed in the build we "manually" copy asset json
s on the postbuild
phase (see package.json
)
Media queries
There is an API for media-query related props: an array of 3 elements [small, medium, large], for respective screen sizes.
For example <Box space=["xs", "xl", "xxl"] />
will set xs
for small screens, xl
for medium screens and xxl
for large.
Basically, if you see a type of MQ<Whatever>
you can provide an array of 3 Whatever
s.
In order to add a media query support to your components:
import React from "react";
import styled from "@emotion/styled";
import { mq } from "../../shared/mediaQueries";
type TextAlignment = "left" | "center" | "right";
type BoxProps = {
alignText: TextAlignment | MQ<TextAlignment>;
};
export const Box = styled.div<BoxProps>(({ alignText = "left" }) =>
mq({
textAlign: [alignText, { left: "left", right: "right", center: "center" }],
})
);
Shadow-DOM
In order to use emotion-enabled DS components in the shadow-dom, you need to take some steps to 'link' the styles in the light-dom (these are added by emotion using CSSStyleSheet.insertRule() which is why appears empty in the DOM).
emotion-js/emotion#1366
https://emotion.sh/docs/cache-provider
https://stackblitz.com/edit/emotion-shadow-dom-example?file=ShadowDomContainer.js
https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/insertRule
import {
ThemeProvider,
light,
Card,
H1,
CacheProvider,
createCache,
} from "@amboss/design-system";
class SuchShadow extends HTMLElement {
connectedCallback() {
const root = document.createElement("div");
this.attachShadow({ mode: "open" });
this.emotionCache = createCache({ container: this.shadowRoot });
render(
<ThemeProvider theme={light}>
<CacheProvider value={this.emotionCache}>
<Card>
<H1>I love you shadow unicorn!!!</H1>
</Card>
</CacheProvider>
</ThemeProvider>,
root
);
}
}
customElements.define("shadow-element", SuchShadow);