-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add multi-theme support to Polaris (#10290)
This PR consolidates the integration of three feature branches that collectively introduce a new base/variant theme architecture to Shopify Polaris. The merged PRs introduce multi-theme support, updated build artifacts, and a new `useTheme` hook for runtime Polaris tokens access. Below, you will find a brief overview of each PR, with links to the original PRs for a more detailed review. ## [Add multi-theme support to Polaris tokens](#10103) This PR introduces multi-theme support to `@shopify/polaris-tokens` by implementing a base/variant architecture. The base theme serves as the foundation for multiple variant themes, which extend the base theme and are the only themes exposed to consumers. Simplifying system changes by flattening the relationship between themes is the primary advantage of this pattern. The PR also includes implementation details like directory structure, stylesheet generation, and variant theme functionality. **Example `styles.scss` updates** ```scss :root { /* Default variant theme custom properties (light) */ } html.Polaris-Summer-Editions-2023 { /* Variant theme custom properties */ /* Note: The above selector is special cased for backward compatibility. See below for updated theme selector structure. */ } html.p-theme-light-high-contrast { /* Variant theme custom properties */ /* Note: Variant theme selectors contain a subset of custom properties overriding the base theme */ } html.p-theme-dark {/* ... */} html.p-theme-dark-high-contrast {/* ... */} html.p-theme-dim {/* ... */} ``` ## [Add multi-theme build artifacts to Polaris tokens](#10250) This PR is a rework of #10153, redefining how the `themes` are accessed from `@shopify/polaris-tokens`. It introduces changes on how asset builder functions are written and replaces reference to `themes.light` with a `themeDefault` alias. ```ts import {themes} from '@shopify/polaris-tokens'; themes['Polaris-Summer-Editions-2023'].color['color-bg'] ``` > Note: Future releases can reduce the bundle size impact of including all themes on one object and enable more flexibility (for example with a `createTheme` util that deep merges variant theme partials). However, that is not needed at this time and can be introduced in a minor release. ## [Add `useTheme` hook for runtime Polaris tokens access](#10252) This PR is a rework of #10229, brings updates to the `AppProvider` by incorporating a new `ThemeContext.Provider` and an associated `useTheme` hook. ```tsx import {useTheme} from '@shopify/polaris'; function App() { const theme = useTheme() theme.color['color-bg'] // '#123' } ``` --------- Co-authored-by: Laura Griffee <laura@mailzone.com>
- Loading branch information
1 parent
88b3ea3
commit 5939b49
Showing
28 changed files
with
585 additions
and
343 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
--- | ||
'@shopify/polaris': minor | ||
'@shopify/polaris-tokens': minor | ||
--- | ||
|
||
- Added multi-theme support | ||
- Added multi-theme build artifacts | ||
- Added multi-theme runtime access |
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
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,23 @@ | ||
import {createContext, useContext} from 'react'; | ||
import type {ThemeName} from '@shopify/polaris-tokens'; | ||
import {themes} from '@shopify/polaris-tokens'; | ||
|
||
export type Theme = typeof themes[ThemeName]; | ||
|
||
export function getTheme(themeName: ThemeName): Theme { | ||
return themes[themeName]; | ||
} | ||
|
||
export const ThemeContext = createContext<Theme | null>(null); | ||
|
||
export function useTheme() { | ||
const theme = useContext(ThemeContext); | ||
|
||
if (!theme) { | ||
throw new Error( | ||
'No theme was provided. Your application must be wrapped in an <AppProvider> component. See https://polaris.shopify.com/components/app-provider for implementation instructions.', | ||
); | ||
} | ||
|
||
return theme; | ||
} |
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 |
---|---|---|
|
@@ -55,5 +55,8 @@ | |
], | ||
"files": [ | ||
"dist" | ||
] | ||
], | ||
"dependencies": { | ||
"deepmerge": "^4.3.1" | ||
} | ||
} |
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 |
---|---|---|
@@ -1,15 +1,13 @@ | ||
import {metadata} from '../src'; | ||
|
||
import {toTokenValues} from './toTokenValues'; | ||
import {toJSON} from './toJSON'; | ||
import {toMediaConditions} from './toMediaConditions'; | ||
import {toStyleSheet} from './toStyleSheet'; | ||
import {toValues} from './toValues'; | ||
|
||
(async () => { | ||
await Promise.all([ | ||
toTokenValues(metadata), | ||
toJSON(metadata), | ||
toMediaConditions(metadata.breakpoints), | ||
toStyleSheet(metadata), | ||
toJSON(), | ||
toMediaConditions(), | ||
toStyleSheet(), | ||
toValues(), | ||
]); | ||
})(); |
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 |
---|---|---|
@@ -1,22 +1,22 @@ | ||
import fs from 'fs'; | ||
import path from 'path'; | ||
|
||
import type {Metadata, MetadataGroup} from '../src'; | ||
import {metaThemeDefault} from '../src/themes'; | ||
|
||
const outputDir = path.join(__dirname, '../dist/json'); | ||
|
||
export async function toJSON(metadata: Metadata) { | ||
if (!fs.existsSync(outputDir)) { | ||
await fs.promises.mkdir(outputDir, {recursive: true}); | ||
} | ||
export async function toJSON() { | ||
await fs.promises.mkdir(outputDir, {recursive: true}).catch((error) => { | ||
if (error.code !== 'EEXIST') { | ||
throw error; | ||
} | ||
}); | ||
|
||
for (const entry of Object.entries(metadata)) { | ||
const [tokenGroupName, tokenGroup] = entry as [ | ||
keyof Metadata, | ||
MetadataGroup, | ||
]; | ||
for (const [tokenGroupName, metaTokenGroup] of Object.entries( | ||
metaThemeDefault, | ||
)) { | ||
const filePath = path.join(outputDir, `${tokenGroupName}.json`); | ||
|
||
await fs.promises.writeFile(filePath, JSON.stringify(tokenGroup)); | ||
await fs.promises.writeFile(filePath, JSON.stringify(metaTokenGroup)); | ||
} | ||
} |
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 |
---|---|---|
@@ -1,91 +1,78 @@ | ||
import fs from 'fs'; | ||
import path from 'path'; | ||
|
||
import type {Metadata, MetadataGroup} from '../src'; | ||
import type {MetadataBase} from '../src/types'; | ||
import type {MetaThemeShape, MetaTokenGroupShape} from '../src/themes/types'; | ||
import {metaThemeVariantPartials, metaThemeDefault} from '../src/themes'; | ||
import {themeNameDefault} from '../src/themes/constants'; | ||
import {createThemeSelector} from '../src/themes/utils'; | ||
import {createVar} from '../src/utilities'; | ||
import type {Entries} from '../src/types'; | ||
|
||
const cssOutputDir = path.join(__dirname, '../dist/css'); | ||
const sassOutputDir = path.join(__dirname, '../dist/scss'); | ||
const scssOutputDir = path.join(__dirname, '../dist/scss'); | ||
const cssOutputPath = path.join(cssOutputDir, 'styles.css'); | ||
const sassOutputPath = path.join(sassOutputDir, 'styles.scss'); | ||
const scssOutputPath = path.join(scssOutputDir, 'styles.scss'); | ||
|
||
/** | ||
* Creates static CSS custom properties. | ||
* Note: These values don't vary by color-scheme. | ||
*/ | ||
export function getStaticCustomProperties(metadata: Metadata) { | ||
return Object.entries(metadata) | ||
.map(([_, tokenGroup]) => getCustomProperties(tokenGroup)) | ||
/** Creates CSS declarations from a base or variant partial theme. */ | ||
export function getMetaThemeDecls(metaTheme: MetaThemeShape) { | ||
return Object.values(metaTheme) | ||
.map((metaTokenGroup) => getMetaTokenGroupDecls(metaTokenGroup)) | ||
.join(''); | ||
} | ||
|
||
/** | ||
* Creates static CSS custom properties overrides. | ||
* Note: These values don't vary by color-scheme. | ||
*/ | ||
export function getStaticCustomPropertiesExperimental(metadata: MetadataBase) { | ||
return Object.entries(metadata) | ||
.map(([_, tokenGroup]) => | ||
getCustomProperties( | ||
Object.fromEntries( | ||
Object.entries(tokenGroup) | ||
// Only include tokens with `valueExperimental` prop | ||
.filter(([_, metadataProperties]) => | ||
Boolean(metadataProperties.valueExperimental), | ||
) | ||
// Move `valueExperimental` to `value` position | ||
.map(([tokenName, metadataProperties]) => [ | ||
tokenName, | ||
{value: metadataProperties.valueExperimental!}, | ||
]), | ||
), | ||
), | ||
/** Creates CSS declarations from a token group. */ | ||
export function getMetaTokenGroupDecls(metaTokenGroup: MetaTokenGroupShape) { | ||
return Object.entries(metaTokenGroup) | ||
.map(([tokenName, {value}]) => | ||
tokenName.startsWith('motion-keyframes') | ||
? `${createVar(tokenName)}:p-${tokenName};` | ||
: `${createVar(tokenName)}:${value};`, | ||
) | ||
.join(''); | ||
} | ||
|
||
/** | ||
* Creates CSS custom properties for a given metadata object. | ||
*/ | ||
export function getCustomProperties(tokenGroup: MetadataGroup) { | ||
return Object.entries(tokenGroup) | ||
.map(([token, {value}]) => | ||
token.startsWith('motion-keyframes') || token.startsWith('keyframes') | ||
? `--p-${token}:p-${token};` | ||
: `--p-${token}:${value};`, | ||
) | ||
.join(''); | ||
} | ||
|
||
/** | ||
* Concatenates the `keyframes` token-group into a single string. | ||
*/ | ||
export function getKeyframes(motion: MetadataGroup) { | ||
/** Creates `@keyframes` rules for `motion-keyframes-*` tokens. */ | ||
export function getKeyframes(motion: MetaTokenGroupShape) { | ||
return Object.entries(motion) | ||
.filter( | ||
([token]) => | ||
token.startsWith('motion-keyframes') || token.startsWith('keyframes'), | ||
) | ||
.map(([token, {value}]) => `@keyframes p-${token}${value}`) | ||
.filter(([tokenName]) => tokenName.startsWith('motion-keyframes')) | ||
.map(([tokenName, {value}]) => `@keyframes p-${tokenName}${value}`) | ||
.join(''); | ||
} | ||
|
||
export async function toStyleSheet(metadata: Metadata) { | ||
if (!fs.existsSync(cssOutputDir)) { | ||
await fs.promises.mkdir(cssOutputDir, {recursive: true}); | ||
} | ||
if (!fs.existsSync(sassOutputDir)) { | ||
await fs.promises.mkdir(sassOutputDir, {recursive: true}); | ||
} | ||
export async function toStyleSheet() { | ||
await fs.promises.mkdir(cssOutputDir, {recursive: true}).catch((error) => { | ||
if (error.code !== 'EEXIST') { | ||
throw error; | ||
} | ||
}); | ||
|
||
await fs.promises.mkdir(scssOutputDir, {recursive: true}).catch((error) => { | ||
if (error.code !== 'EEXIST') { | ||
throw error; | ||
} | ||
}); | ||
|
||
const metaThemeVariantPartialsEntries = Object.entries( | ||
metaThemeVariantPartials, | ||
).filter(([themeName]) => themeName !== themeNameDefault) as Entries< | ||
Omit<typeof metaThemeVariantPartials, typeof themeNameDefault> | ||
>; | ||
|
||
const styles = ` | ||
:root{color-scheme:light;${getStaticCustomProperties(metadata)}} | ||
html.Polaris-Summer-Editions-2023{${getStaticCustomPropertiesExperimental( | ||
metadata, | ||
)}} | ||
${getKeyframes(metadata.motion)} | ||
`; | ||
const styles = [ | ||
`:root{color-scheme:light;${getMetaThemeDecls(metaThemeDefault)}}`, | ||
metaThemeVariantPartialsEntries.map( | ||
([themeName, metaThemeVariantPartial]) => | ||
`${createThemeSelector(themeName)}{${getMetaThemeDecls( | ||
metaThemeVariantPartial, | ||
)}}`, | ||
), | ||
getKeyframes(metaThemeDefault.motion), | ||
// Newline terminator | ||
'', | ||
] | ||
.flat() | ||
.join('\n'); | ||
|
||
await fs.promises.writeFile(cssOutputPath, styles); | ||
await fs.promises.writeFile(sassOutputPath, styles); | ||
await fs.promises.writeFile(scssOutputPath, styles); | ||
} |
Oops, something went wrong.