@blueprintjs/core
Version:
Core styles & components
511 lines (415 loc) • 17.4 kB
text/typescript
/* !
* (c) Copyright 2026 Palantir Technologies Inc. All rights reserved.
*/
/* eslint-disable sort-keys */
import { register } from "@tokens-studio/sd-transforms";
import StyleDictionary from "style-dictionary";
import type { Config, TransformedToken } from "style-dictionary/types";
// -- Types --------------------------------------------------------------------
type DTCGColor = {
readonly colorSpace: "oklch" | "srgb";
readonly components: readonly [number, number, number];
readonly alpha?: number;
readonly hex?: string;
};
type DTCGDimension = {
readonly value: number;
readonly unit: string;
};
type DTCGShadow = {
readonly color: DTCGColor;
readonly offsetX: DTCGDimension;
readonly offsetY: DTCGDimension;
readonly blur: DTCGDimension;
readonly spread?: DTCGDimension;
readonly inset?: boolean;
};
type ChannelModification =
| { readonly _tag: "Offset"; readonly value: number }
| { readonly _tag: "Scale"; readonly factor: number };
type ColorDerivation = {
readonly alpha?: number | string;
readonly lightness?: ChannelModification;
readonly chroma?: ChannelModification;
readonly hue?: ChannelModification;
};
type BlueprintRoleTag = "stackable-layer";
type BlueprintRole = {
readonly _tag: "BlueprintRole";
readonly role: BlueprintRoleTag;
};
type TransformDefinition<TValue> = {
readonly name: string;
readonly tokenType: string;
readonly parse: (value: unknown) => TValue | undefined;
readonly format: (value: TValue) => string;
};
// -- Parsers ------------------------------------------------------------------
const parseObject = (value: unknown): Record<string, unknown> | undefined =>
typeof value === "object" && value !== null ? (value as Record<string, unknown>) : undefined;
const parseNumberTuple = (value: unknown): readonly number[] | undefined =>
Array.isArray(value) && value.every(v => typeof v === "number") ? value : undefined;
const parseStringTuple = (value: unknown): readonly string[] | undefined =>
Array.isArray(value) && value.every(v => typeof v === "string") ? value : undefined;
const parseDTCGColor = (value: unknown): DTCGColor | undefined => {
const obj = parseObject(value);
if (obj === undefined) return undefined;
const colorSpace = obj.colorSpace;
if (colorSpace !== "oklch" && colorSpace !== "srgb") return undefined;
const components = parseNumberTuple(obj.components);
if (components === undefined || components.length !== 3) return undefined;
const alpha = obj.alpha;
if (alpha !== undefined && typeof alpha !== "number") return undefined;
const hex = obj.hex;
if (hex !== undefined && typeof hex !== "string") return undefined;
return {
colorSpace,
components: [components[0], components[1], components[2]] as const,
alpha,
hex,
};
};
const parseDTCGDimension = (value: unknown): DTCGDimension | undefined => {
const obj = parseObject(value);
if (obj === undefined) return undefined;
const numValue = obj.value;
const unit = obj.unit;
if (typeof numValue !== "number" || typeof unit !== "string") return undefined;
return { value: numValue, unit };
};
const parseDTCGShadow = (value: unknown): DTCGShadow | undefined => {
const obj = parseObject(value);
if (obj === undefined) return undefined;
const color = parseDTCGColor(obj.color);
const offsetX = parseDTCGDimension(obj.offsetX);
const offsetY = parseDTCGDimension(obj.offsetY);
const blur = parseDTCGDimension(obj.blur);
if (color === undefined || offsetX === undefined || offsetY === undefined || blur === undefined) {
return undefined;
}
const spread = parseDTCGDimension(obj.spread);
const inset = obj.inset;
return {
color,
offsetX,
offsetY,
blur,
spread,
inset: typeof inset === "boolean" ? inset : undefined,
};
};
const parseCubicBezier = (value: unknown): readonly [number, number, number, number] | undefined => {
const arr = parseNumberTuple(value);
if (arr === undefined || arr.length !== 4) return undefined;
return [arr[0], arr[1], arr[2], arr[3]] as const;
};
const parseFontFamily = (value: unknown): readonly string[] | undefined => parseStringTuple(value);
const parseTokenReference = (value: unknown): string | undefined =>
typeof value === "string" && value.startsWith("{") && value.endsWith("}") ? value : undefined;
const parseColorDerivation = (ext: unknown): ColorDerivation | undefined => {
const extObj = parseObject(ext);
if (extObj === undefined) return undefined;
const derive = parseObject(extObj["com.blueprint.derive"]);
if (derive === undefined) return undefined;
const alpha = derive.alpha;
const parsedAlpha = typeof alpha === "number" ? alpha : parseTokenReference(alpha);
return {
alpha: parsedAlpha,
lightness:
typeof derive.lightnessOffset === "number"
? { _tag: "Offset", value: derive.lightnessOffset }
: typeof derive.lightnessScale === "number"
? { _tag: "Scale", factor: derive.lightnessScale }
: undefined,
chroma:
typeof derive.chromaOffset === "number"
? { _tag: "Offset", value: derive.chromaOffset }
: typeof derive.chromaScale === "number"
? { _tag: "Scale", factor: derive.chromaScale }
: undefined,
hue: typeof derive.hueOffset === "number" ? { _tag: "Offset", value: derive.hueOffset } : undefined,
};
};
const parseRole = (ext: unknown): BlueprintRole | undefined => {
const extObj = parseObject(ext);
if (extObj === undefined) return undefined;
const role = extObj["com.blueprint.role"];
if (role === "stackable-layer") {
return { _tag: "BlueprintRole", role };
}
return undefined;
};
// -- Formatters ---------------------------------------------------------------
const formatOklchToCss = (color: DTCGColor): string => {
const [l, c, h] = color.components;
return color.alpha !== undefined && color.alpha < 1
? `oklch(${l} ${c} ${h} / ${color.alpha})`
: `oklch(${l} ${c} ${h})`;
};
const formatSrgbToCss = (color: DTCGColor): string => {
const [r, g, b] = color.components.map(comp => Math.round(comp * 255));
return color.alpha !== undefined && color.alpha < 1
? `rgba(${r}, ${g}, ${b}, ${color.alpha})`
: `rgb(${r}, ${g}, ${b})`;
};
const formatColorToCss = (color: DTCGColor): string => {
switch (color.colorSpace) {
case "oklch":
return formatOklchToCss(color);
case "srgb":
return formatSrgbToCss(color);
}
};
const formatDimensionToCss = (dim: DTCGDimension): string => `${dim.value}${dim.unit}`;
const formatShadowToCss = (shadow: DTCGShadow): string => {
const parts: string[] = [];
if (shadow.inset) parts.push("inset");
parts.push(formatDimensionToCss(shadow.offsetX));
parts.push(formatDimensionToCss(shadow.offsetY));
parts.push(formatDimensionToCss(shadow.blur));
if (shadow.spread) parts.push(formatDimensionToCss(shadow.spread));
parts.push(formatColorToCss(shadow.color));
return parts.join(" ");
};
const formatCubicBezierToCss = (points: readonly [number, number, number, number]): string =>
`cubic-bezier(${points.join(", ")})`;
const formatFontFamilyToCss = (families: readonly string[]): string =>
families.map(f => (f.includes(" ") ? `"${f}"` : f)).join(", ");
const formatChannelModification = (channel: string, mod: ChannelModification | undefined): string => {
if (mod === undefined) return channel;
// eslint-disable-next-line no-underscore-dangle
switch (mod._tag) {
case "Offset":
return `calc(${channel} + ${mod.value})`;
case "Scale":
return `calc(${channel} * ${mod.factor})`;
}
};
const tokenReferenceToVar = (ref: string): string => {
const path = ref.slice(1, -1).split(".");
return `var(--bp-${path.join("-")})`;
};
const formatAlpha = (alpha: number | string): string =>
typeof alpha === "number" ? String(alpha) : tokenReferenceToVar(alpha);
const formatDerivedColorToCss = (baseVar: string, derivation: ColorDerivation): string => {
const l = formatChannelModification("l", derivation.lightness);
const c = formatChannelModification("c", derivation.chroma);
const h = formatChannelModification("h", derivation.hue);
return derivation.alpha !== undefined
? `oklch(from ${baseVar} ${l} ${c} ${h} / ${formatAlpha(derivation.alpha)})`
: `oklch(from ${baseVar} ${l} ${c} ${h})`;
};
// -- Transform Definitions ----------------------------------------------------
const getTokenValue = (token: TransformedToken): unknown => token.$value ?? token.value;
const makeTransformConfig = <TValue>(
def: TransformDefinition<TValue>,
): Parameters<typeof StyleDictionary.registerTransform>[0] => ({
name: def.name,
type: "value",
transitive: true,
filter: token => token.$type === def.tokenType || token.type === def.tokenType,
transform: token => {
const value = getTokenValue(token);
if (typeof value === "string") return value;
const parsed = def.parse(value);
return parsed !== undefined ? def.format(parsed) : value;
},
});
const colorTransform: TransformDefinition<DTCGColor> = {
name: "dtcg/color/css",
tokenType: "color",
parse: parseDTCGColor,
format: formatColorToCss,
};
const dimensionTransform: TransformDefinition<DTCGDimension> = {
name: "dtcg/dimension/css",
tokenType: "dimension",
parse: parseDTCGDimension,
format: formatDimensionToCss,
};
const durationTransform: TransformDefinition<DTCGDimension> = {
name: "dtcg/duration/css",
tokenType: "duration",
parse: parseDTCGDimension,
format: formatDimensionToCss,
};
const fontFamilyTransform: TransformDefinition<readonly string[]> = {
name: "dtcg/fontFamily/css",
tokenType: "fontFamily",
parse: parseFontFamily,
format: formatFontFamilyToCss,
};
const fontWeightTransform: TransformDefinition<number> = {
name: "dtcg/fontWeight/css",
tokenType: "fontWeight",
parse: v => (typeof v === "number" ? v : undefined),
format: String,
};
const numberTransform: TransformDefinition<number> = {
name: "dtcg/number/css",
tokenType: "number",
parse: v => (typeof v === "number" ? v : undefined),
format: String,
};
const cubicBezierTransform: TransformDefinition<readonly [number, number, number, number]> = {
name: "dtcg/cubicBezier/css",
tokenType: "cubicBezier",
parse: parseCubicBezier,
format: formatCubicBezierToCss,
};
const shadowTransformConfig: Parameters<typeof StyleDictionary.registerTransform>[0] = {
name: "dtcg/shadow/css",
type: "value",
transitive: true,
filter: token => token.$type === "shadow" || token.type === "shadow",
transform: token => {
const value = getTokenValue(token);
if (typeof value === "string") return value;
if (!value) return value;
const shadows = Array.isArray(value) ? value : [value];
const formatted = shadows
.map(parseDTCGShadow)
.filter((s): s is DTCGShadow => s !== undefined)
.map(formatShadowToCss);
return formatted.length > 0 ? formatted.join(", ") : value;
},
};
// @see https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklch#using_relative_colors_with_oklch
const deriveTransformConfig: Parameters<typeof StyleDictionary.registerTransform>[0] = {
name: "bp/derive/css",
type: "value",
transitive: true,
filter: token => {
const ext = parseObject(token.$extensions ?? token.extensions);
return ext !== undefined && ext["com.blueprint.derive"] !== undefined;
},
transform: token => {
// Use original extensions to preserve token references (before tokens-studio resolves them)
const original = token.original ?? {};
const originalExt = original.$extensions ?? original.extensions;
const derivation = parseColorDerivation(originalExt);
if (derivation === undefined) {
const value = getTokenValue(token);
return typeof value === "string" ? value : JSON.stringify(value);
}
const originalValue = original.$value ?? original.value;
const tokenRef = parseTokenReference(originalValue);
if (tokenRef === undefined) {
const value = getTokenValue(token);
return typeof value === "string" ? value : JSON.stringify(value);
}
const refPath = tokenRef.slice(1, -1).split(".");
const baseVar = `var(--bp-${refPath.join("-")})`;
return formatDerivedColorToCss(baseVar, derivation);
},
};
const nameTransformConfig: Parameters<typeof StyleDictionary.registerTransform>[0] = {
name: "name/bp/kebab",
type: "name",
transform: token => "bp-" + token.path.join("-"),
};
const standardTransforms = [
colorTransform,
dimensionTransform,
durationTransform,
fontFamilyTransform,
fontWeightTransform,
numberTransform,
cubicBezierTransform,
] as const;
// -- Format Definition --------------------------------------------------------
const applyRoleForCss = (value: string, role: BlueprintRole): string => {
switch (role.role) {
case "stackable-layer":
return `linear-gradient(${value} 0 0)`;
}
};
const formatCssLine = (token: TransformedToken, outputReferences: boolean): string => {
const getValue = (): string => {
const value = token.value ?? token.$value;
return typeof value === "string" ? value : String(value);
};
const formatValue = (): string => {
const ext = parseObject(token.$extensions ?? token.extensions);
if (ext !== undefined && ext["com.blueprint.derive"] !== undefined) {
return getValue();
}
if (outputReferences) {
const originalValue = token.original?.$value ?? token.original?.value;
if (typeof originalValue === "string" && originalValue.includes("{")) {
return originalValue.replace(
/\{([^}]+)\}/g,
(_, path: string) => `var(--bp-${path.replace(/\./g, "-")})`,
);
}
}
return getValue();
};
const formattedValue = formatValue();
// Apply role-based transformations from com.blueprint.role extension
const extension = parseObject(token.$extensions ?? token.extensions);
const role = parseRole(extension);
const finalValue = role !== undefined ? applyRoleForCss(formattedValue, role) : formattedValue;
const comment = token.$description ? ` /** ${token.$description} */` : "";
return ` --${token.name}: ${finalValue};${comment}`;
};
const formatCssVariables = (tokens: readonly TransformedToken[], outputReferences: boolean): string => {
const header =
"/**\n * Do not edit directly, this file was auto-generated.\n */\n\n/* stylelint-disable @blueprintjs/no-color-literal */\n\n:root {";
const footer = "}";
const lines = tokens.map(token => formatCssLine(token, outputReferences));
return [header, ...lines, footer].join("\n") + "\n";
};
// -- Initialization -----------------------------------------------------------
const initializeStyleDictionary = (sd: typeof StyleDictionary): void => {
register(sd);
standardTransforms.forEach(def => sd.registerTransform(makeTransformConfig(def)));
sd.registerTransform(shadowTransformConfig);
sd.registerTransform(deriveTransformConfig);
sd.registerTransform(nameTransformConfig);
// bp/derive/css must run after dtcg/color/css to override resolved color references
sd.registerTransformGroup({
name: "bp/css",
transforms: [
"name/bp/kebab",
"dtcg/color/css",
"dtcg/dimension/css",
"dtcg/duration/css",
"dtcg/fontFamily/css",
"dtcg/fontWeight/css",
"dtcg/number/css",
"dtcg/cubicBezier/css",
"dtcg/shadow/css",
"bp/derive/css",
],
});
sd.registerFormat({
name: "bp/css/variables",
format: ({ dictionary, options }) => {
const outputReferences = (options?.outputReferences as boolean) ?? false;
return formatCssVariables(dictionary.allTokens, outputReferences);
},
});
};
// -- Config -------------------------------------------------------------------
const config: Config = {
source: ["src/design-tokens/tokens/**/*.tokens.json"],
preprocessors: ["tokens-studio"],
platforms: {
css: {
transformGroup: "bp/css",
buildPath: "src/design-tokens/dist/",
files: [
{
destination: "_tokens.scss",
format: "bp/css/variables",
options: {
outputReferences: true,
},
},
],
},
},
};
initializeStyleDictionary(StyleDictionary);
export { config };