@aesthetic/system
Version:
Web based building blocks for the Aesthetic design system.
162 lines (130 loc) • 3.85 kB
text/typescript
import { ColorScheme, ContrastLevel, ThemeName } from '@aesthetic/types';
import { Theme } from './Theme';
import { ThemeOptions } from './types';
export class ThemeRegistry<Block extends object> {
protected darkTheme: string = '';
protected defaultTheme: string = '';
protected lightTheme: string = '';
protected themes: Record<string, Theme<Block>> = {};
/**
* Return the default dark theme.
*/
getDarkTheme(): Theme<Block> {
return this.getTheme(this.darkTheme);
}
/**
* Return the default light theme.
*/
getLightTheme(): Theme<Block> {
return this.getTheme(this.lightTheme);
}
/**
* Find an approprite theme based on the user's or device's preferences.
* Will check for preferred color schemes and contrast levels.
*/
getPreferredTheme({
matchColorScheme,
matchContrastLevel,
}: {
matchColorScheme?: (scheme: ColorScheme) => boolean;
matchContrastLevel?: (level: ContrastLevel) => boolean;
} = {}): Theme<Block> {
const prefersDarkScheme = matchColorScheme?.('dark');
const prefersLightScheme = matchColorScheme?.('light');
const prefersHighContrast = matchContrastLevel?.('high');
const prefersLowContrast = matchContrastLevel?.('low');
const schemeCheckOrder: ColorScheme[] = [];
if (prefersDarkScheme) {
schemeCheckOrder.push('dark');
} else if (prefersLightScheme) {
schemeCheckOrder.push('light');
}
let possibleTheme: Theme<Block> | undefined;
// Find a theme based on device preferences
schemeCheckOrder.some((scheme) => {
const contrastCheckOrder: ContrastLevel[] = ['normal'];
if (prefersHighContrast) {
contrastCheckOrder.unshift('high');
} else if (prefersLowContrast) {
contrastCheckOrder.unshift('low');
}
return contrastCheckOrder.some((contrast) => {
possibleTheme = this.query({ contrast, scheme });
return !!possibleTheme;
});
});
if (possibleTheme) {
return possibleTheme;
}
if (this.defaultTheme) {
return this.getTheme(this.defaultTheme);
}
throw new Error('No themes have been registered.');
}
/**
* Return a theme by name or throw an error if not found.
*/
getTheme(name: ThemeName): Theme<Block> {
if (__DEV__ && !name) {
throw new Error('Cannot find a theme without a name.');
}
const theme = this.themes[name];
if (__DEV__ && !theme) {
throw new Error(`Theme "${name}" does not exist. Has it been registered?`);
}
return theme;
}
/**
* Query for a theme that matches the defined parameters.
*/
query(params: Partial<ThemeOptions>): Theme<Block> | undefined {
return Object.values(this.themes).find((theme) => {
const conditions: boolean[] = [];
if (params.contrast) {
conditions.push(theme.contrast === params.contrast);
}
if (params.scheme) {
conditions.push(theme.scheme === params.scheme);
}
return conditions.every((c) => !!c);
});
}
/**
* Register a theme with a unique name. Can optionally mark a theme
* as default for their defined color scheme.
*/
register(name: string, theme: Theme<Block>, isDefault: boolean = false): this {
if (__DEV__ && !(theme instanceof Theme)) {
throw new TypeError('Only a `Theme` object can be registered.');
}
if (isDefault) {
if (theme.scheme === 'dark') {
this.darkTheme = name;
} else {
this.lightTheme = name;
}
}
if (theme.name && theme.name !== name) {
if (__DEV__) {
throw new Error(`Theme "${name}" has already been registered under "${theme.name}".`);
}
} else {
// eslint-disable-next-line no-param-reassign
theme.name = name;
}
if (!this.defaultTheme) {
this.defaultTheme = name;
}
this.themes[name] = theme;
return this;
}
/**
* Reset the registry to initial state.
*/
reset() {
this.darkTheme = '';
this.lightTheme = '';
this.defaultTheme = '';
this.themes = {};
}
}