-
Notifications
You must be signed in to change notification settings - Fork 322
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
bf7c1e9
commit cb4a9cf
Showing
17 changed files
with
842 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import cx from "classnames"; | ||
import React, { FC, ReactElement, useEffect, useMemo, useState } from "react"; | ||
import { SystemTheme, Theme, ThemeColor } from "./ThemeProviderConstants"; | ||
import { generateRandomAlphaString, generateThemeCssOverride, shouldGenerateTheme } from "./ThemeProviderUtils"; | ||
import { withStaticProps } from "../../types"; | ||
|
||
export interface ThemeProviderProps { | ||
/** | ||
* The theme to apply, consists of name - name of css class that will be added to the children, which should be unique - and the object of colors overrides for each system theme. | ||
*/ | ||
theme?: Theme; | ||
/** | ||
* The children to render with the theme | ||
*/ | ||
children: ReactElement; | ||
/** | ||
* String which adds up to theme name selector to make it more specific (in case if theme.name is colliding with some other class name) | ||
*/ | ||
themeClassSpecifier?: string; | ||
} | ||
|
||
const ThemeProvider: FC<ThemeProviderProps> & { | ||
systemThemes?: typeof SystemTheme; | ||
colors?: typeof ThemeColor; | ||
} = ({ theme, children, themeClassSpecifier: customThemeClassSpecifier }) => { | ||
const [stylesLoaded, setStylesLoaded] = useState(false); | ||
const themeClassSpecifier = useMemo( | ||
() => customThemeClassSpecifier || generateRandomAlphaString(), | ||
[customThemeClassSpecifier] | ||
); | ||
|
||
useEffect(() => { | ||
if (!shouldGenerateTheme(theme)) { | ||
return; | ||
} | ||
if (document.getElementById(theme.name)) { | ||
setStylesLoaded(true); | ||
return; | ||
} | ||
|
||
const styleElement = document.createElement("style"); | ||
styleElement.type = "text/css"; | ||
styleElement.id = theme.name; | ||
const themeCssOverride = generateThemeCssOverride(theme, themeClassSpecifier); | ||
|
||
try { | ||
styleElement.appendChild(document.createTextNode(themeCssOverride)); | ||
document.head.appendChild(styleElement); | ||
setStylesLoaded(true); | ||
} catch (error) { | ||
console.error("vibe ThemeProvider: error inserting theme-generated css - ", error); | ||
} | ||
|
||
return () => { | ||
document.head.removeChild(styleElement); | ||
}; | ||
}, [themeClassSpecifier, theme]); | ||
|
||
if (!stylesLoaded && shouldGenerateTheme(theme)) { | ||
// Waiting for styles to load before children render | ||
return null; | ||
} | ||
|
||
// Pass the theme name as a class to the children - to scope the effect of the theme | ||
return React.cloneElement(children, { | ||
className: cx(theme?.name, themeClassSpecifier, children?.props?.className) | ||
}); | ||
}; | ||
|
||
export default withStaticProps(ThemeProvider, { | ||
systemThemes: SystemTheme, | ||
colors: ThemeColor | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
/** | ||
* System themes: dark, light, black, hacker | ||
*/ | ||
export enum SystemTheme { | ||
LIGHT = "light", | ||
DARK = "dark", | ||
BLACK = "black", | ||
HACKER = "hacker" | ||
} | ||
|
||
/** | ||
* Colors which are eligible for theming | ||
*/ | ||
export enum ThemeColor { | ||
primaryColor = "primary-color", | ||
primaryHoverColor = "primary-hover-color", | ||
primarySelectedColor = "primary-selected-color", | ||
primarySelectedHoverColor = "primary-selected-hover-color", | ||
primarySelectedOnSecondaryColor = "primary-selected-on-secondary-color", | ||
textColorOnPrimary = "text-color-on-primary", | ||
brandColor = "brand-color", | ||
brandHoverColor = "brand-hover-color", | ||
brandSelectedColor = "brand-selected-color", | ||
brandSelectedHoverColor = "brand-selected-hover-color", | ||
textColorOnBrand = "text-color-on-brand" | ||
} | ||
|
||
export type Theme = { | ||
/** | ||
* The name of the theme - name of css class that will be added to the children - should be unique | ||
*/ | ||
name: string; | ||
colors: SystemThemeColorMap; | ||
}; | ||
|
||
type SystemThemeColorMap = { | ||
[key in SystemTheme]?: ThemeColorTokenValueMap; | ||
}; | ||
|
||
export type ThemeColorTokenValueMap = ThemeColorTokenValue | ThemeCustomClassValue; | ||
|
||
export type ThemeColorTokenValue = { | ||
[key in ThemeColor]?: string; | ||
}; | ||
|
||
type ThemeCustomClassValue = { | ||
[key: string]: ThemeColorTokenValue | ThemeCustomClassValue; | ||
}; | ||
|
||
export const SystemThemeClassMap: SystemThemeClassMapType = { | ||
[SystemTheme.LIGHT]: "light-app-theme", | ||
[SystemTheme.DARK]: "dark-app-theme", | ||
[SystemTheme.BLACK]: "black-app-theme", | ||
[SystemTheme.HACKER]: "hacker_theme-app-theme" | ||
}; | ||
|
||
type SystemThemeClassMapType = { | ||
[key in SystemTheme]: string; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { Theme, ThemeColorTokenValueMap, SystemTheme, SystemThemeClassMap } from "./ThemeProviderConstants"; | ||
|
||
const generateCss = (object: ThemeColorTokenValueMap, stack: string, parentSelector: string) => { | ||
for (const key of Object.keys(object)) { | ||
if (typeof object[key as keyof ThemeColorTokenValueMap] === "string") { | ||
stack += `--${key}: ${object[key as keyof ThemeColorTokenValueMap]};`; | ||
} | ||
} | ||
|
||
if (stack !== "") { | ||
stack = parentSelector + " {" + stack + "}"; | ||
} | ||
|
||
for (const key of Object.keys(object)) { | ||
if (typeof object[key as keyof ThemeColorTokenValueMap] === "object") { | ||
const selector = `${parentSelector} .${key}`; | ||
stack += | ||
"\n" + generateCss(object[key as keyof ThemeColorTokenValueMap] as ThemeColorTokenValueMap, "", selector); | ||
} | ||
} | ||
|
||
return stack; | ||
}; | ||
|
||
export const shouldGenerateTheme = (theme: Theme) => { | ||
return !!theme?.colors && !!theme?.name; | ||
}; | ||
|
||
export const generateThemeCssOverride = (theme: Theme, themeClassSpecifier: string) => { | ||
if (!shouldGenerateTheme(theme)) { | ||
return null; | ||
} | ||
|
||
let css = ""; | ||
for (const systemTheme of Object.keys(theme.colors) as SystemTheme[]) { | ||
css += | ||
generateCss( | ||
theme.colors[systemTheme], | ||
"", | ||
`.${SystemThemeClassMap[systemTheme]} .${themeClassSpecifier}.${theme.name}` | ||
) + "\n"; | ||
} | ||
|
||
return css; | ||
}; | ||
|
||
export const generateRandomAlphaString = (length = 6) => { | ||
let result = ""; | ||
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; | ||
|
||
for (let i = 0; i < length; i++) { | ||
result += characters.charAt(Math.floor(Math.random() * characters.length)); | ||
} | ||
|
||
return result; | ||
}; |
165 changes: 165 additions & 0 deletions
165
src/components/ThemeProvider/__stories__/ThemeProvider.stories.mdx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
import ThemeProvider from "../ThemeProvider"; | ||
import { ArgsTable, Story, Canvas, Meta } from "@storybook/addon-docs"; | ||
import { createStoryMetaSettingsDecorator } from "../../../storybook"; | ||
import { | ||
ThemeProviderCustomClassTemplate, | ||
ThemeProviderFoldedThemingTemplate, | ||
ThemeProviderNegativeExampleTemplate, | ||
ThemeProviderPositiveExampleTemplate, | ||
ThemeProviderProductThemingTemplate, | ||
ThemeProviderTemplateOverview, | ||
ThemeProviderThemingScopeTemplate | ||
} from "./ThemeProvider.stories"; | ||
import { ColorsEligibleForTheming } from "./colors-eligible-for-theming"; | ||
|
||
export const metaSettings = createStoryMetaSettingsDecorator({ | ||
component: ThemeProvider, | ||
enumPropNamesArray: [], // List enum props here | ||
iconPropNamesArray: [], // List props that are typed as icons here | ||
actionPropsArray: [] // List the component's actions here | ||
}); | ||
|
||
<Meta | ||
title="Components/ThemeProvider [beta]" | ||
component={ThemeProvider} | ||
argTypes={metaSettings.argTypes} | ||
decorators={metaSettings.decorators} | ||
/> | ||
|
||
# ThemeProvider | ||
|
||
<AlphaWarning /> | ||
|
||
- [Overview](#overview) | ||
- [Props](#props) | ||
- [Usage](#usage) | ||
- [Variants](#variants) | ||
- [Do’s and don’ts](#dos-and-donts) | ||
- [Feedback](#feedback) | ||
|
||
## Overview | ||
|
||
This component helps to customize the colors of library's components by overriding a specific css variables with a values provided within theme object. | ||
There are 2 levels of theming: **system theme** and **product theme**. | ||
System theme is a one of a 4 predefined themes: <code>light</code>(<code>.light-app-theme</code>), <code>dark</code>(<code>.dark-app-theme</code>), <code>black</code>(<code>.black-app-theme</code>) and <code>hacker</code>(<code>.hacker_theme-app-theme</code>). | ||
Product theme is a custom theme that can be provided by a user, there you can specify the values of specific color tokens for each of the system themes. | ||
|
||
<Canvas> | ||
<Story | ||
name="Overview" | ||
args={{ | ||
theme: { | ||
name: "overview-theme", | ||
colors: { | ||
[ThemeProvider.systemThemes.LIGHT]: { | ||
[ThemeProvider.colors.primaryColor]: "green", | ||
[ThemeProvider.colors.primaryHoverColor]: "darkgreen" | ||
}, | ||
[ThemeProvider.systemThemes.DARK]: { | ||
[ThemeProvider.colors.primaryColor]: "salmon", | ||
[ThemeProvider.colors.primaryHoverColor]: "darksalmon" | ||
}, | ||
[ThemeProvider.systemThemes.BLACK]: { | ||
[ThemeProvider.colors.primaryColor]: "slateblue", | ||
[ThemeProvider.colors.primaryHoverColor]: "darkslateblue" | ||
}, | ||
[ThemeProvider.systemThemes.HACKER]: { | ||
[ThemeProvider.colors.primaryColor]: "purple", | ||
[ThemeProvider.colors.primaryHoverColor]: "darkmagenta" | ||
} | ||
} | ||
} | ||
}} | ||
> | ||
{ThemeProviderTemplateOverview.bind({})} | ||
</Story> | ||
</Canvas> | ||
|
||
## Props | ||
|
||
<ArgsTable story="Overview" /> | ||
|
||
## Usage | ||
|
||
<UsageGuidelines | ||
guidelines={[ | ||
<> | ||
Control themes in your application by setting theme classes (e.g. <code>.light-app-theme</code>) on your{" "} | ||
<code>body</code> and render everything else inside it | ||
</>, | ||
<> | ||
In most common case ThemeProvider should be rendered only once on the root level of the application - below the{" "} | ||
<code>body</code> | ||
</>, | ||
<> | ||
ThemeProvider is populating theme name <code>className</code> to it's child, so don't put{" "} | ||
<code>{"\<Fragment\>"}</code> (<code>{"\<\>"}</code>) inside - it's not accepting <code>className</code> prop | ||
</> | ||
]} | ||
/> | ||
|
||
<Tip title="Dev tip"> | ||
Use <code>ThemeProvider.systemThemes</code> and <code>ThemeProvider.colors</code> enums to unleash the power of | ||
auto-completion | ||
</Tip> | ||
|
||
## Variants | ||
|
||
There are four system themes <code>light</code>, <code>dark</code>, <code>black</code> and <code>hacker</code> and in each you can redefine the values of the following color tokens: | ||
|
||
### Colors eligible for theming | ||
|
||
<Story name="Colors eligible for theming"> | ||
<ColorsEligibleForTheming /> | ||
</Story> | ||
|
||
## Use cases and examples | ||
|
||
### Theming scope | ||
|
||
Only components wrapped with ThemeProvider will be affected by the theme. | ||
|
||
<Canvas> | ||
<Story name="Theming scope">{ThemeProviderThemingScopeTemplate.bind({})}</Story> | ||
</Canvas> | ||
|
||
### Folded theming | ||
|
||
If component is wrapped with multiple ThemeProviders, the most nested one will override the values of the outer one, but if the nested ThemeProvider doesn't provide a value for a specific color token, the outer ThemeProvider will be used. | ||
|
||
<Canvas> | ||
<Story name="Folded theming">{ThemeProviderFoldedThemingTemplate.bind({})}</Story> | ||
</Canvas> | ||
|
||
### Product theming | ||
|
||
These are theme-definitions, which are used in monday.com products. | ||
|
||
<Canvas> | ||
<Story name="Product theming">{ThemeProviderProductThemingTemplate.bind({})}</Story> | ||
</Canvas> | ||
|
||
### Custom class selector | ||
|
||
If you need to apply some of the tokens overrides only on elements under specific class you can declare theme like that: | ||
|
||
<Canvas> | ||
<Story name="Custom class selector">{ThemeProviderCustomClassTemplate.bind({})}</Story> | ||
</Canvas> | ||
|
||
## Do’s and Don’ts | ||
|
||
<ComponentRules | ||
rules={[ | ||
{ | ||
positive: { | ||
component: [<ThemeProviderPositiveExampleTemplate />], | ||
description: "Pay attention and override all semantically close tokens, if needed." | ||
}, | ||
negative: { | ||
component: [<ThemeProviderNegativeExampleTemplate />], | ||
description: "Don’t override only specific tokens to avoid inconsistent appearance" | ||
} | ||
} | ||
]} | ||
/> |
8 changes: 8 additions & 0 deletions
8
src/components/ThemeProvider/__stories__/ThemeProvider.stories.module.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
.productTheming { | ||
&Container { | ||
height: 200px; | ||
} | ||
&Dropdown { | ||
width: 250px; | ||
} | ||
} |
Oops, something went wrong.