@terrazzo/token-tools
Version:
Various utilities for token types
358 lines (339 loc) • 15.9 kB
JavaScript
import { CSS_TO_CULORI, kebabCase, parseColor, tokenToCulori } from "./string-BwN3ybNv.js";
import { displayable, formatCss, formatHex, formatHex8, modeA98, modeHsl, modeHwb, modeLab, modeLab65, modeLch, modeLrgb, modeOkhsv, modeOklab, modeOklch, modeP3, modeProphoto, modeRec2020, modeRgb, modeXyz50, modeXyz65, toGamut, useMode } from "culori/fn";
import { displayable as displayable$1 } from "culori";
//#region src/css/lib.ts
function defaultAliasTransform(token) {
if (!token) throw new Error("Undefined token");
return `var(${makeCSSVar(token.id)})`;
}
/** Generate shorthand CSS for select token types */
function generateShorthand({ $type, localID }) {
switch ($type) {
case "transition": return [
"duration",
"delay",
"timing-function"
].map((p) => makeCSSVar(`${localID}-${p}`, { wrapVar: true })).join(" ");
}
}
const CSS_VAR_RE = /(?:(\p{Uppercase_Letter}?[\p{Lowercase_Letter}\p{Number}]+|[\p{Uppercase_Letter}\p{Number}]+|[\u{80}-\u{10FFFF}\p{Number}]+)|.)/u;
/**
* Generate a valid CSS variable from any string
* Code by @dfrankland
*/
function makeCSSVar(name, { prefix, wrapVar = false } = {}) {
if (typeof name !== "string") throw new Error(`makeCSSVar() Expected string, received ${name}`);
let property = name.split(CSS_VAR_RE).filter(Boolean).join("-");
if (prefix && !property.startsWith(`${prefix}-`)) property = `${prefix}-${property}`;
const finalProperty = `--${property}`.toLocaleLowerCase();
return wrapVar ? `var(${finalProperty})` : finalProperty;
}
//#endregion
//#region src/css/boolean.ts
/** Convert boolean value to CSS string */
function transformBoolean(token, { tokensSet, transformAlias = defaultAliasTransform }) {
if (token.aliasChain?.[0]) return transformAlias(tokensSet[token.aliasChain[0]]);
return token.$value === true ? "1" : "0";
}
//#endregion
//#region src/css/color.ts
/** Convert color value to CSS string */
function transformColor(token, options) {
const { transformAlias = defaultAliasTransform, tokensSet } = options;
if (token.aliasChain?.[0]) return transformAlias(tokensSet[token.aliasChain[0]]);
const { colorSpace, components, alpha = 1 } = typeof token.$value === "string" ? parseColor(token.$value) : token.$value;
const color = tokenToCulori({
colorSpace,
components,
alpha
});
if (!color) throw new Error(`Can’t convert color ${JSON.stringify(token.$value)} to Culori color`);
let formatColor = formatCss;
if (options.color?.legacyHex) formatColor = color.alpha !== 1 ? formatHex8 : formatHex;
return displayable(color) ? formatColor(color) : downsample({
colorSpace,
components,
alpha
}, color);
}
const converters = {
a98: useMode(modeA98),
hsl: useMode(modeHsl),
hwb: useMode(modeHwb),
lab: useMode(modeLab),
lab65: useMode(modeLab65),
lch: useMode(modeLch),
lrgb: useMode(modeLrgb),
oklab: useMode(modeOklab),
oklch: useMode(modeOklch),
okhsv: useMode(modeOkhsv),
p3: useMode(modeP3),
prophoto: useMode(modeProphoto),
rec2020: useMode(modeRec2020),
rgb: useMode(modeRgb),
xyz50: useMode(modeXyz50),
xyz65: useMode(modeXyz65)
};
/**
* Downsample color to sRGB/Display P3/Rec2020 colorspaces.
* Note: because Culori tends to convert to RGB color spaces to ensure the operation,
* we have to do an additional step of converting back. So we’re not really converting;
* we’re just preserving the original colorspace.
*/
function downsample($value, culoriColor) {
if (!($value.colorSpace in CSS_TO_CULORI)) throw new Error(`Invalid colorSpace "${$value.colorSpace}". Expected one of: ${Object.keys(CSS_TO_CULORI).join(", ")}`);
const conversionSpace = CSS_TO_CULORI[$value.colorSpace] || "oklab";
let gamutSpace = conversionSpace;
if (gamutSpace === "lab" || gamutSpace === "lab65") gamutSpace = "lch";
else if (gamutSpace === "oklab") gamutSpace = "oklch";
const srgb = converters[conversionSpace](toGamut("rgb", gamutSpace)(culoriColor));
const p3 = converters[conversionSpace](toGamut("p3", gamutSpace)(culoriColor));
const rec2020 = converters[conversionSpace](toGamut("rec2020", gamutSpace)(culoriColor));
return {
".": formatCss(culoriColor),
srgb: formatCss(srgb),
p3: formatCss(p3),
rec2020: formatCss(rec2020)
};
}
//#endregion
//#region src/css/dimension.ts
/** Convert dimension value to CSS */
function transformDimension(token, options) {
const { tokensSet, transformAlias = defaultAliasTransform } = options;
if (token.aliasChain?.[0]) return transformAlias(tokensSet[token.aliasChain[0]]);
return token.$value.value === 0 ? "0" : `${token.$value.value}${token.$value.unit}`;
}
//#endregion
//#region src/css/stroke-style.ts
/** Convert strokeStyle value to CSS */
function transformStrokeStyle(token, options) {
const { tokensSet, transformAlias = defaultAliasTransform } = options;
if (token.aliasChain?.[0]) return transformAlias(tokensSet[token.aliasChain[0]]);
return typeof token.$value === "string" ? token.$value : "dashed";
}
//#endregion
//#region src/css/border.ts
/** Convert border value to multiple CSS values */
function transformBorder(token, options) {
const { tokensSet, transformAlias = defaultAliasTransform } = options;
if (token.aliasChain?.[0]) return transformAlias(tokensSet[token.aliasChain[0]]);
const width = token.partialAliasOf?.width ? transformAlias(tokensSet[token.partialAliasOf.width]) : transformDimension({ $value: token.$value.width }, options);
const color = token.partialAliasOf?.color ? transformAlias(tokensSet[token.partialAliasOf.color]) : transformColor({ $value: token.$value.color }, options);
const style = token.partialAliasOf?.style ? transformAlias(tokensSet[token.partialAliasOf.style]) : transformStrokeStyle({ $value: token.$value.style }, options);
const formatBorder = (colorKey) => [
width,
style,
typeof color === "string" ? color : color[colorKey]
].join(" ");
return typeof color === "string" || displayable$1(color.p3) ? formatBorder(".") : {
".": formatBorder("."),
srgb: formatBorder("srgb"),
p3: formatBorder("p3"),
rec2020: formatBorder("rec2020")
};
}
//#endregion
//#region src/css/cubic-bezier.ts
/** Convert cubicBezier value to CSS */
function transformCubicBezier(token, options) {
const { tokensSet, transformAlias = defaultAliasTransform } = options;
if (token.aliasChain?.[0]) return transformAlias(tokensSet[token.aliasChain[0]]);
return `cubic-bezier(${token.$value.map((v, i) => token.partialAliasOf?.[i] ? transformAlias(tokensSet[token.partialAliasOf[i]]) : v).join(", ")})`;
}
//#endregion
//#region src/css/duration.ts
/** Convert duration value to CSS */
function transformDuration(token, options) {
const { tokensSet, transformAlias = defaultAliasTransform } = options;
if (token.aliasChain?.[0]) return transformAlias(tokensSet[token.aliasChain[0]]);
return `${token.$value.value}${token.$value.unit}`;
}
//#endregion
//#region src/css/font-family.ts
const FONT_NAME_KEYWORD = /^[a-z-]+$/;
function transformFontFamily(token, options) {
const { tokensSet, transformAlias = defaultAliasTransform } = options;
if (token.aliasChain?.[0]) return transformAlias(tokensSet[token.aliasChain[0]]);
return token.$value.map((fontName) => FONT_NAME_KEYWORD.test(fontName) ? fontName : `"${fontName}"`).join(", ");
}
//#endregion
//#region src/css/font-weight.ts
/** Convert fontWeight value to CSS */
function transformFontWeight(token, options) {
const { tokensSet, transformAlias = defaultAliasTransform } = options;
if (token.aliasChain?.[0]) return transformAlias(tokensSet[token.aliasChain[0]]);
return String(token.$value);
}
//#endregion
//#region src/css/gradient.ts
/** Convert gradient value to CSS */
function transformGradient(token, options) {
const { tokensSet, transformAlias = defaultAliasTransform } = options;
if (token.aliasChain?.[0]) return transformAlias(tokensSet[token.aliasChain[0]]);
let isHDR = false;
const colors = [];
const positions = [];
for (let i = 0; i < token.$value.length; i++) {
const { color, position } = token.$value[i];
const colorValue = token.partialAliasOf?.[i]?.color ? transformAlias(tokensSet[token.partialAliasOf[i].color]) : transformColor({ $value: color }, options);
if (typeof colorValue !== "string") isHDR = true;
colors.push(colorValue);
positions.push(token.partialAliasOf?.[i]?.position ? transformAlias(tokensSet[token.partialAliasOf[i].position]) : `${100 * position}%`);
}
function formatStop(i, colorKey = ".") {
return `${typeof colors[i] === "string" ? colors[i] : colors[i][colorKey]} ${positions[i]}`;
}
return !isHDR ? token.$value.map((_, i) => formatStop(i, positions[i])).join(", ") : {
".": token.$value.map((_, i) => formatStop(i, ".")).join(", "),
srgb: token.$value.map((_, i) => formatStop(i, "srgb")).join(", "),
p3: token.$value.map((_, i) => formatStop(i, "p3")).join(", "),
rec2020: token.$value.map((_, i) => formatStop(i, "rec2020")).join(", ")
};
}
//#endregion
//#region src/css/link.ts
/** Convert link value to CSS */
function transformLink(token, options) {
const { tokensSet, transformAlias = defaultAliasTransform } = options;
if (token.aliasChain?.[0]) return transformAlias(tokensSet[token.aliasChain[0]]);
return `url("${token.$value}")`;
}
//#endregion
//#region src/css/number.ts
/** Convert number value to CSS */
function transformNumber(token, options) {
const { tokensSet, transformAlias = defaultAliasTransform } = options;
if (token.aliasChain?.[0]) return transformAlias(tokensSet[token.aliasChain[0]]);
return String(token.$value);
}
//#endregion
//#region src/css/shadow.ts
/** Convert shadow subvalue to CSS */
function transformShadowLayer(value, options) {
const { tokensSet, colorValue, partialAliasOf, transformAlias = defaultAliasTransform } = options;
const offsetX = partialAliasOf?.offsetX ? transformAlias(tokensSet[partialAliasOf.offsetX]) : transformDimension({ $value: value.offsetX }, options);
const offsetY = partialAliasOf?.offsetY ? transformAlias(tokensSet[partialAliasOf.offsetY]) : transformDimension({ $value: value.offsetY }, options);
const blur = partialAliasOf?.blur ? transformAlias(tokensSet[partialAliasOf.blur]) : transformDimension({ $value: value.blur }, options);
const spread = partialAliasOf?.spread ? transformAlias(tokensSet[partialAliasOf.spread]) : transformDimension({ $value: value.spread }, options);
const inset = value?.inset === true ? "inset" : void 0;
return [
inset,
offsetX,
offsetY,
blur,
spread,
colorValue
].filter(Boolean).join(" ");
}
/** Convert shadow value to CSS */
function transformShadow(token, options) {
const { tokensSet, transformAlias = defaultAliasTransform } = options;
if (token.aliasChain?.[0]) return transformAlias(tokensSet[token.aliasChain[0]]);
const colors = token.$value.map((v, i) => token.partialAliasOf?.[i]?.color ? transformAlias(tokensSet[token.partialAliasOf[i].color]) : transformColor({ $value: v.color }, options));
const isHDR = colors.some((c) => typeof c === "object");
const formatShadow = (colorKey) => token.$value.map((v, i) => transformShadowLayer(v, {
tokensSet,
colorValue: typeof colors[i] === "string" ? colors[i] : colors[i][colorKey],
partialAliasOf: token.partialAliasOf?.[i],
transformAlias
})).join(", ");
return !isHDR ? formatShadow(".") : {
".": formatShadow("."),
srgb: formatShadow("srgb"),
p3: formatShadow("p3"),
rec2020: formatShadow("rec2020")
};
}
//#endregion
//#region src/css/string.ts
/** Convert string value to CSS */
function transformString(token, options) {
const { tokensSet, transformAlias = defaultAliasTransform } = options;
if (token.aliasChain?.[0]) return transformAlias(tokensSet[token.aliasChain[0]]);
return String(token.$value);
}
//#endregion
//#region src/css/transition.ts
/** Convert transition value to shorthand */
function transformTransition(token, options) {
const { tokensSet, transformAlias = defaultAliasTransform } = options;
if (token.aliasChain?.[0]) return transformAlias(tokensSet[token.aliasChain[0]]);
const duration = token.partialAliasOf?.duration ? transformAlias(tokensSet[token.partialAliasOf.duration]) : transformDuration({ $value: token.$value.duration }, options);
const delay = token.partialAliasOf?.delay ? transformAlias(tokensSet[token.partialAliasOf.delay]) : transformDuration({ $value: token.$value.delay }, options);
const timingFunction = token.partialAliasOf?.timingFunction ? transformAlias(tokensSet[token.partialAliasOf.timingFunction]) : transformCubicBezier({ $value: token.$value.timingFunction }, options);
return `${duration} ${delay} ${timingFunction}`;
}
//#endregion
//#region src/css/typography.ts
/** Convert typography value to multiple CSS values */
function transformTypography(token, options) {
const { tokensSet, transformAlias = defaultAliasTransform } = options;
const output = {};
for (const [property, subvalue] of Object.entries(token.$value)) {
let transformedValue;
const aliasedID = token.aliasChain?.[0] ?? token.partialAliasOf?.[property];
if (aliasedID) {
const resolvedToken = tokensSet[aliasedID];
transformedValue = transformAlias(resolvedToken.$type === "typography" ? { id: `${aliasedID}-${property}` } : resolvedToken);
} else switch (property) {
case "fontFamily": {
transformedValue = transformFontFamily({ $value: subvalue }, options);
break;
}
case "fontSize":
case "letterSpacing": {
transformedValue = transformDimension({ $value: subvalue }, options);
break;
}
case "fontWeight": {
transformedValue = transformFontWeight({ $value: subvalue }, options);
break;
}
case "lineHeight": {
if (typeof subvalue === "number") transformedValue = transformNumber({ $value: subvalue }, options);
else transformedValue = transformDimension({ $value: subvalue }, options);
break;
}
default: {
transformedValue = transformString({ $value: subvalue }, options);
break;
}
}
output[kebabCase(property)] = transformedValue;
}
return output;
}
//#endregion
//#region src/css/index.ts
/** Main CSS Transform */
function transformCSSValue(token, { mode,...options }) {
const selectedMode = token.mode[mode];
if (!selectedMode) return;
const tokenWithModeValue = {
id: token.id,
$type: token.$type,
...selectedMode
};
switch (tokenWithModeValue.$type) {
case "boolean": return transformBoolean(tokenWithModeValue, options);
case "border": return transformBorder(tokenWithModeValue, options);
case "color": return transformColor(tokenWithModeValue, options);
case "cubicBezier": return transformCubicBezier(tokenWithModeValue, options);
case "dimension": return transformDimension(tokenWithModeValue, options);
case "duration": return transformDuration(tokenWithModeValue, options);
case "fontFamily": return transformFontFamily(tokenWithModeValue, options);
case "fontWeight": return transformFontWeight(tokenWithModeValue, options);
case "gradient": return transformGradient(tokenWithModeValue, options);
case "link": return transformLink(tokenWithModeValue, options);
case "number": return transformNumber(tokenWithModeValue, options);
case "shadow": return transformShadow(tokenWithModeValue, options);
case "string": return transformString(tokenWithModeValue, options);
case "strokeStyle": return transformStrokeStyle(tokenWithModeValue, options);
case "transition": return transformTransition(tokenWithModeValue, options);
case "typography": return transformTypography(tokenWithModeValue, options);
}
}
//#endregion
export { defaultAliasTransform, generateShorthand, makeCSSVar, transformBoolean, transformBorder, transformCSSValue, transformColor, transformCubicBezier, transformDimension, transformDuration, transformFontFamily, transformFontWeight, transformGradient, transformLink, transformNumber, transformShadow, transformShadowLayer, transformString, transformStrokeStyle, transformTransition, transformTypography };
//# sourceMappingURL=css.js.map