apphouse
Version:
Component library for React that uses observable state management and theme-able components.
442 lines (399 loc) • 11 kB
text/typescript
import { CSSProperties } from 'glamor';
import { Palette } from '../Palette';
import { Theme } from '../Theme';
import { Token } from '../Token';
import ColorUtils from './color.utils';
import { ColorDefinition } from './color.interface';
import { CssPropertyStyle, StyleTokenReference } from '../style.interface';
import { tokenTypes } from '../token.interface';
interface ReferencePaletteTokenInfo {
paletteId: string;
paletteMode: string;
colorId: string;
}
/**
* Get palette info from reference
* @param reference Token reference
* @param selectedTheme light | dark
* @returns {
paletteId: string;
paletteMode: string;
colorId: string;
}
*/
export const getPaletteInfoFromReferenceKey = (
reference: StyleTokenReference,
selectedTheme?: 'dark' | 'light'
): ReferencePaletteTokenInfo | undefined => {
let paletteId;
let paletteMode;
let colorId;
if (reference.type === 'color') {
// key can be in 2 different formats
// base.primary.color or theme.color
const referenceKey = reference.key.split('.');
if (referenceKey.length === 2) {
// reference is a themed color
const mode = referenceKey[0];
if (mode === 'theme') {
paletteId = selectedTheme;
paletteMode = selectedTheme;
colorId = referenceKey[1];
}
} else if (referenceKey.length === 3) {
paletteId = referenceKey[1];
paletteMode = referenceKey[0];
colorId = referenceKey[2];
}
}
if (!paletteId || !paletteMode || !colorId) {
return undefined;
}
return {
paletteId,
paletteMode,
colorId
};
};
export const isValidTokenKey = (tokenStringKey: string): boolean => {
if (typeof tokenStringKey !== 'string') {
// console.warn("key must be of type string");
return false;
}
const info = tokenStringKey.split('.');
if (info[0] === '0') {
return false;
}
if (info.length < 2) {
// console.warn(
// `key does not have the minimum length for a reference key (${tokenStringKey})`
// );
return false;
}
if (info[0] === 'base') {
if (info.length !== 3) {
// console.warn("base references must have 3 tokens");
return false;
}
}
if (info[0] === 'theme') {
if (info.length !== 2) {
// console.warn("theme references must have 2 tokens");
return false;
}
}
return true;
};
export const getPaletteInfoFromStringKey = (
stringKey: string,
selectedTheme?: 'dark' | 'light'
): ReferencePaletteTokenInfo | undefined => {
if (!stringKey) {
return undefined;
}
if (!isValidTokenKey(stringKey)) {
return undefined;
}
const referenceInfo = stringKey.split('.');
if (referenceInfo.length === 0) {
return undefined;
}
if (referenceInfo.length === 2) {
if (referenceInfo[0] === 'theme') {
if (!selectedTheme) {
console.warn(
'attempted to get a themed token but no theme was provided'
);
return undefined;
}
return {
paletteId: selectedTheme,
paletteMode: selectedTheme,
colorId: referenceInfo[1]
};
}
}
if (referenceInfo.length === 3) {
const paletteMode = referenceInfo[0];
if (paletteMode === 'base') {
return {
paletteId: referenceInfo[1],
paletteMode: referenceInfo[0],
colorId: referenceInfo[2]
};
}
}
return undefined;
};
export const getTokenInfoFromReferenceKey = (
reference: StyleTokenReference
): TokenReferenceInfo | undefined => {
let tokenType;
let tokenKey;
if (reference.type === 'token') {
const referenceKey = reference.key.split('.');
if (referenceKey.length === 2) {
tokenType = referenceKey[0];
if (!isValidTokenType(tokenType)) {
console.log('getTokenInfoFromReferenceKey', reference);
return undefined;
}
tokenKey = referenceKey[1];
}
}
if (!tokenType || !tokenKey) {
return undefined;
}
const tokenId = getIdForTokenFromValue(tokenType, tokenKey);
if (!tokenId) {
return undefined;
}
return {
tokenType,
tokenKey,
tokenId
};
};
interface TokenReferenceInfo {
tokenType: string;
tokenKey: string;
tokenId: string;
}
export const getTokenInfoFromStringKey = (
tokenReferenceString: string
): TokenReferenceInfo | undefined => {
let tokenType;
let tokenKey;
if (tokenReferenceString.startsWith('rgba')) {
// this is a string color
return undefined;
}
if (!isValidTokenKey(tokenReferenceString)) {
return undefined;
}
const referenceKey = tokenReferenceString.split('.');
if (referenceKey.length === 2) {
tokenType = referenceKey[0];
if (!tokenTypes.includes(tokenType as any)) {
return undefined;
}
if (!isValidTokenType(tokenType)) {
return undefined;
}
tokenKey = referenceKey[1];
}
if (!tokenType || !tokenKey) {
return undefined;
}
const tokenId = getIdForTokenFromValue(tokenType, tokenKey);
if (!tokenId) {
return undefined;
}
return {
tokenType,
tokenKey,
tokenId
};
};
export const getIdForTokenFromValue = (type: string, key: string) => {
if (!tokenTypes.includes(type as any)) {
console.warn(
`INVALID TOKEN VALUE(getIdForTokenFromValue): Attempted to get a token reference with invalid type ${type}`
);
return undefined;
}
return `${type}.${key}`;
};
export const isValidTokenType = (tokenType: any): boolean => {
if (!tokenTypes.includes(tokenType)) {
console.warn(
`INVALID TOKEN TYPE(isValidTokenType): Attempted to get a token reference with invalid type ${tokenType}`
);
return false;
}
return true;
};
export const getCssWithThemedTokens = ({
value,
lookup,
withColorsFromPaletteId,
withRawValue
}: {
value: CssPropertyStyle[];
lookup: { [id: string]: string };
withColorsFromPaletteId?: string; // this is actually the paletted id ('dark' or 'light' for themes things defaults)
withRawValue?: boolean;
}): CSSProperties => {
const css: CSSProperties = {};
if (!value) {
return css;
}
value.map((v) => {
const property = v.property;
if (property) {
const camelCaseProperty = Theme.toCamelCase(property);
if (typeof v.value === 'string' || typeof v.value === 'number') {
// start default value
css[camelCaseProperty] = v.value;
if (withRawValue === true) {
const key =
withColorsFromPaletteId !== undefined
? `${v.value}`.replace(
'theme.',
`theme.${withColorsFromPaletteId}.`
)
: v.value;
const referenceValue = lookup[key] || v.value;
css[camelCaseProperty] = referenceValue;
}
} else {
css[camelCaseProperty] = getCssWithThemedTokens({
value: v.value,
lookup,
withColorsFromPaletteId,
withRawValue
});
}
}
});
return css;
};
export const getValueFromReference = ({
reference,
withRaw,
lookup,
theme
}: {
reference?: any;
theme: 'dark' | 'light' | undefined;
withRaw?: boolean | undefined;
lookup: { [id: string]: string };
}): string | number | undefined => {
if (!reference) {
return undefined;
}
const referenceValue = reference.key;
if (!referenceValue) {
return undefined;
}
if (reference?.type === 'palette') {
let colorValue = reference.value;
// here we need to update the reference value since it might be stale
// if we are asking for the raw value we actually want the rgba string
if (withRaw) {
const paletteInfo = getPaletteInfoFromReferenceKey(reference, theme);
// let's get the rgba string from the color palette
if (paletteInfo) {
const key = `${paletteInfo.paletteMode}.${paletteInfo.paletteId}.${paletteInfo.colorId}`;
const color = lookup[key];
// defaulting to reference, missing rgb string
colorValue = color;
} else {
// defaulting to reference
colorValue = reference;
}
} else {
const rgbValue = reference.value as ColorDefinition;
const potentialColorValue = ColorUtils.toRgbaStringFromRgbaObject(
rgbValue?.rgb
);
if (potentialColorValue) {
colorValue = potentialColorValue;
}
}
return colorValue;
}
if (reference?.type === 'token') {
let value: string | number | ColorDefinition = reference.value;
if (withRaw) {
value = reference.value;
const tokenInfo = getTokenInfoFromReferenceKey(reference);
if (tokenInfo) {
const token = lookup[tokenInfo.tokenId];
value = token || reference.value;
}
}
if (typeof value !== 'string' && typeof value !== 'number') {
return JSON.stringify(value);
}
return value;
}
return referenceValue;
};
export const getValueFromReferenceString = ({
referenceString,
withRaw,
tokens,
colors,
theme
}: {
referenceString?: any;
withRaw?: boolean | undefined;
tokens: Record<string, Token>;
colors: Record<string, Palette>;
theme?: 'dark' | 'light' | undefined;
}): string | number | undefined => {
if (typeof referenceString !== 'string') {
return undefined;
}
if (referenceString === '0') {
return referenceString;
}
if (isColorReferenceString(referenceString)) {
const paletteInfo = getPaletteInfoFromStringKey(referenceString, theme);
if (!withRaw) {
return referenceString;
}
if (paletteInfo && withRaw) {
let colorValue;
// attempt to get value from reference string
const palette = colors[paletteInfo.paletteId];
const paletteColors = palette?.colors;
if (paletteColors) {
const color = paletteColors[paletteInfo.colorId];
console.log('defaulting to reference, missing rgb string');
colorValue = color?.rgbString;
}
if (colorValue) {
return colorValue;
} else {
console.log('defaulting to property value', referenceString);
return referenceString;
}
}
}
const tokenInfo = getTokenInfoFromStringKey(referenceString);
if (tokenInfo && withRaw) {
// attempt to get value from token string
const token = tokens[tokenInfo.tokenId];
if (token) {
const tokenValue = tokens[tokenInfo.tokenId].value;
if (tokenValue) {
return tokenValue;
} else {
return referenceString;
}
} else {
return referenceString;
}
}
return referenceString;
};
export const isColorReferenceString = (referenceString: string): boolean => {
if (typeof referenceString !== 'string') {
return false;
}
const ref = referenceString.split('.');
if (ref[0].startsWith('rgba')) {
return false;
}
if (ref.length > 0) {
return (
ref[0] === 'theme' ||
ref[0] === 'light' ||
ref[0] === 'dark' ||
ref[0] === 'base'
);
}
return false;
};