@vapor-ui/color-generator
Version:
접근성을 고려한 대비비 기반 색상 팔레트 생성 라이브러리
600 lines (591 loc) • 18.9 kB
JavaScript
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
// src/generators/system-color-palette.ts
import { formatCss as formatCss2, oklch as oklch3 } from "culori";
// src/constants/colors.ts
var BASE_COLORS = {
white: {
name: "color-white",
hex: "#FFFFFF",
oklch: "oklch(1 0 0)",
codeSyntax: "vapor-color-white"
},
black: {
name: "color-black",
hex: "#000000",
oklch: "oklch(0 0 0)",
codeSyntax: "vapor-color-black"
}
};
var DEFAULT_PRIMITIVE_COLORS = {
red: "#F5535E",
pink: "#F26394",
grape: "#CC5DE8",
violet: "#8662F3",
blue: "#448EFE",
cyan: "#1EBAD2",
green: "#04A37E",
lime: "#8FD327",
yellow: "#FFC107",
orange: "#ED670C"
};
// src/constants/thresholds.ts
var DEFAULT_MAIN_BACKGROUND_LIGHTNESS = {
light: 100,
dark: 14
};
var DEFAULT_CONTRAST_RATIOS = {
"050": 1.07,
"100": 1.3,
"200": 1.7,
"300": 2.5,
"400": 3,
"500": 4.5,
"600": 6.5,
"700": 8.5,
"800": 11.5,
"900": 15
};
var ADAPTIVE_COLOR_GENERATION = {
LIGHTNESS_THRESHOLD: 0.5,
DARK_LIGHTNESS_FACTOR: 0.55,
LIGHT_LIGHTNESS_FACTOR: 0.85,
CHROMA_REDUCTION_FACTOR: 0.85
};
var BUTTON_FOREGROUND_LIGHTNESS_THRESHOLD = 0.65;
// src/libs/adobe-leonardo.ts
import { BackgroundColor, Color, Theme } from "@adobe/leonardo-contrast-colors";
import { differenceCiede2000, formatCss, formatHex, oklch as oklch2 } from "culori";
// src/utils/color.ts
import { lch, oklch } from "culori";
var formatOklchForWeb = (oklchString) => {
const match = oklchString.match(/oklch\(([^\s]+)\s+([^\s]+)\s+([^)]+)\)/);
if (match) {
const [, l, c, h] = match;
const roundedL = parseFloat(l).toFixed(3);
const roundedC = parseFloat(c).toFixed(3);
let roundedH;
if (h === "none" || isNaN(parseFloat(h))) {
roundedH = "0.0";
} else {
roundedH = parseFloat(h).toFixed(1);
}
return `oklch(${roundedL} ${roundedC} ${roundedH})`;
}
return oklchString;
};
var generateCodeSyntax = (keyPath) => {
const topLevelKeys = ["base", "light", "dark"];
const filteredPath = keyPath.filter((key) => !topLevelKeys.includes(key));
return `vapor-color-${filteredPath.join("-")}`;
};
var generateTokenName = (keyPath) => {
const topLevelKeys = ["base", "light", "dark"];
const filteredPath = keyPath.filter((key) => !topLevelKeys.includes(key));
return `color-${filteredPath.join("-")}`;
};
var getContrastingForegroundColor = (backgroundOklch, threshold = BUTTON_FOREGROUND_LIGHTNESS_THRESHOLD) => {
var _a;
const colorObj = oklch(backgroundOklch);
const lightness = (_a = colorObj == null ? void 0 : colorObj.l) != null ? _a : 0;
return lightness > threshold ? __spreadValues({}, BASE_COLORS.black) : __spreadValues({}, BASE_COLORS.white);
};
function getSortedScales(palette) {
return Object.keys(palette).sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
}
function findClosestScale(palette) {
const scaleKeys = Object.keys(palette);
if (scaleKeys.length === 0) return null;
return scaleKeys.reduce((closestKey, currentKey) => {
var _a, _b, _c, _d;
const closestDeltaE = (_b = (_a = palette[closestKey]) == null ? void 0 : _a.deltaE) != null ? _b : Infinity;
const currentDeltaE = (_d = (_c = palette[currentKey]) == null ? void 0 : _c.deltaE) != null ? _d : Infinity;
return currentDeltaE < closestDeltaE ? currentKey : closestKey;
});
}
var getColorLightness = (colorHex) => {
const lchColor = lch(colorHex);
if (lchColor && typeof lchColor.l === "number") {
return Math.round(lchColor.l);
}
return null;
};
// src/libs/adobe-leonardo.ts
var createAdaptiveColorKeys = (brandColorOklch, originalHex) => {
const isLightColor = brandColorOklch.l > ADAPTIVE_COLOR_GENERATION.LIGHTNESS_THRESHOLD;
if (isLightColor) {
const darkerKeyOklch = __spreadProps(__spreadValues({}, brandColorOklch), {
mode: "oklch",
l: brandColorOklch.l * ADAPTIVE_COLOR_GENERATION.DARK_LIGHTNESS_FACTOR,
c: brandColorOklch.c * ADAPTIVE_COLOR_GENERATION.CHROMA_REDUCTION_FACTOR
});
const darkKeyHex = formatHex(darkerKeyOklch);
return {
lightKey: originalHex,
darkKey: darkKeyHex != null ? darkKeyHex : originalHex
};
} else {
const lighterKeyOklch = __spreadProps(__spreadValues({}, brandColorOklch), {
mode: "oklch",
l: Math.min(
brandColorOklch.l / ADAPTIVE_COLOR_GENERATION.DARK_LIGHTNESS_FACTOR,
ADAPTIVE_COLOR_GENERATION.LIGHT_LIGHTNESS_FACTOR
),
c: brandColorOklch.c * ADAPTIVE_COLOR_GENERATION.CHROMA_REDUCTION_FACTOR
});
const lightKeyHex = formatHex(lighterKeyOklch);
return {
lightKey: lightKeyHex != null ? lightKeyHex : originalHex,
darkKey: originalHex
};
}
};
var createColorDefinition = ({
name,
colorHex,
contrastRatios
}) => {
const brandColorOklch = oklch2(colorHex);
if (!brandColorOklch) {
console.warn(`Invalid brand color: ${name} - ${colorHex}. Skipping.`);
return null;
}
const { lightKey, darkKey } = createAdaptiveColorKeys(brandColorOklch, colorHex);
return new Color({
name,
colorKeys: [lightKey, darkKey],
colorspace: "OKLCH",
ratios: contrastRatios
});
};
var createLeonardoTheme = ({
colorDefinitions,
backgroundColor,
backgroundName,
lightness,
contrastRatios
}) => {
const backgroundColorObj = new BackgroundColor({
name: backgroundName,
colorKeys: [backgroundColor],
ratios: contrastRatios
});
return new Theme({
colors: [...colorDefinitions, backgroundColorObj],
backgroundColor: backgroundColorObj,
lightness,
output: "HEX"
});
};
var generateThemeTokens = ({
colors,
contrastRatios,
backgroundColor,
backgroundName,
lightness
}) => {
const colorDefinitions = Object.entries(colors).map(([name, hex]) => createColorDefinition({ name, colorHex: hex, contrastRatios })).filter((def) => def !== null);
const theme = createLeonardoTheme({
colorDefinitions,
backgroundColor,
backgroundName,
lightness,
contrastRatios
});
const [backgroundObj, ...themeColors] = theme.contrastColors;
const calculateDeltaE = differenceCiede2000();
const tokens = {};
if ("background" in backgroundObj) {
const oklchColor = oklch2(backgroundObj.background);
const oklchValue = formatCss(oklchColor);
if (oklchValue) {
const canvasToken = {
name: generateTokenName(["background", "canvas"]),
hex: backgroundObj.background,
oklch: formatOklchForWeb(oklchValue),
codeSyntax: generateCodeSyntax(["background", "canvas"])
};
tokens[canvasToken.name] = canvasToken;
}
}
themeColors.forEach((color) => {
if ("name" in color && "values" in color && color.values.length > 0) {
const colorName = color.name;
const originalColorHex = colors[colorName];
const shadeData = [];
color.values.forEach((instance) => {
const oklchColor = oklch2(instance.value);
const oklchValue = formatCss(oklchColor);
if (oklchValue) {
let deltaE = void 0;
if (originalColorHex) {
deltaE = Math.round(calculateDeltaE(originalColorHex, instance.value) * 100) / 100;
}
shadeData.push({
name: instance.name,
hex: instance.value,
oklch: formatOklchForWeb(oklchValue),
deltaE: deltaE || 0,
tokenName: generateTokenName([colorName, instance.name]),
codeSyntax: generateCodeSyntax([colorName, instance.name])
});
}
});
shadeData.sort((a, b) => {
const numA = parseInt(a.name, 10);
const numB = parseInt(b.name, 10);
return numA - numB;
});
shadeData.forEach((shade) => {
const token = {
name: shade.tokenName,
hex: shade.hex,
oklch: shade.oklch,
deltaE: shade.deltaE,
codeSyntax: shade.codeSyntax
};
tokens[shade.tokenName] = token;
});
}
});
return tokens;
};
// src/generators/system-color-palette.ts
var createBaseColorTokens = (formatter) => {
return Object.entries(BASE_COLORS).reduce(
(tokens, [colorName, colorData]) => {
var _a;
const oklchColor = oklch3(colorData.hex);
tokens[colorName] = {
name: colorData.name,
hex: colorData.hex,
oklch: formatter((_a = formatCss2(oklchColor)) != null ? _a : ""),
codeSyntax: colorData.codeSyntax
};
return tokens;
},
{}
);
};
function generateSystemColorPalette(config = {}) {
const colors = config.colors || DEFAULT_PRIMITIVE_COLORS;
const contrastRatios = config.contrastRatios || DEFAULT_CONTRAST_RATIOS;
const background = config.background || {
color: "#FFFFFF",
lightness: DEFAULT_MAIN_BACKGROUND_LIGHTNESS,
name: "gray"
};
const lightTokens = generateThemeTokens({
colors,
contrastRatios,
backgroundColor: background.color,
backgroundName: background.name,
lightness: background.lightness.light
});
const darkTokens = generateThemeTokens({
colors,
contrastRatios,
backgroundColor: background.color,
backgroundName: background.name,
lightness: background.lightness.dark
});
const lightContainer = {
tokens: lightTokens,
metadata: {
type: "primitive",
theme: "light"
}
};
const darkContainer = {
tokens: darkTokens,
metadata: {
type: "primitive",
theme: "dark"
}
};
const baseTokens = createBaseColorTokens(formatOklchForWeb);
const baseContainer = {
tokens: Object.entries(baseTokens).reduce(
(acc, [, token]) => {
acc[token.name] = token;
return acc;
},
{}
),
metadata: {
type: "primitive",
theme: "base"
}
};
return {
base: baseContainer,
light: lightContainer,
dark: darkContainer
};
}
// src/generators/brand-color-palette.ts
import { formatCss as formatCss3, oklch as oklch4 } from "culori";
function overrideCustomColors(tokens, customColors) {
var _a;
const newTokens = __spreadValues({}, tokens);
for (const [colorName, hexValue] of Object.entries(customColors)) {
if (colorName === "background") continue;
const colorTokens = {};
Object.entries(newTokens).forEach(([tokenName, token]) => {
const isRelatedColorToken = token && typeof token === "object" && tokenName.includes(`-${colorName}-`);
if (isRelatedColorToken) {
const scaleMatch = tokenName.match(/-(\d{3})$/);
if (scaleMatch) {
const scale = scaleMatch[1];
colorTokens[scale] = token;
}
}
});
if (Object.keys(colorTokens).length === 0) {
console.warn(`Palette for "${colorName}" not found in tokens.`);
continue;
}
const closestScaleKey = findClosestScale(colorTokens);
if (!closestScaleKey) continue;
const oklchColor = oklch4(hexValue);
const oklchValue = (_a = formatCss3(oklchColor)) != null ? _a : "";
const oklchString = formatOklchForWeb(oklchValue);
const tokenIdentifier = [colorName, closestScaleKey];
const updatedTokenName = generateTokenName(tokenIdentifier);
const updatedToken = {
name: updatedTokenName,
deltaE: 0,
hex: hexValue,
oklch: oklchString,
codeSyntax: generateCodeSyntax(tokenIdentifier)
};
newTokens[updatedTokenName] = updatedToken;
}
return newTokens;
}
function generateBrandColorPalette(config) {
const contrastRatios = config.contrastRatios || DEFAULT_CONTRAST_RATIOS;
const background = config.background || {
color: "#FFFFFF",
name: "gray",
lightness: DEFAULT_MAIN_BACKGROUND_LIGHTNESS
};
const lightTokens = generateThemeTokens({
colors: config.colors,
contrastRatios,
backgroundColor: background.color,
backgroundName: background.name,
lightness: background.lightness.light
});
const darkTokens = generateThemeTokens({
colors: config.colors,
contrastRatios,
backgroundColor: background.color,
backgroundName: background.name,
lightness: background.lightness.dark
});
const adjustedLightTokens = overrideCustomColors(lightTokens, config.colors);
return {
light: {
tokens: adjustedLightTokens,
metadata: {
type: "primitive",
theme: "light"
}
},
dark: {
tokens: darkTokens,
metadata: {
type: "primitive",
theme: "dark"
}
}
};
}
// src/generators/semantic-mapping.ts
function reconstructPalette(sourceTokens, colorName) {
const palette = {};
Object.entries(sourceTokens).forEach(([tokenName, token]) => {
const isRelatedToken = typeof token === "object" && tokenName.includes(`-${colorName}-`);
if (isRelatedToken) {
const scaleMatch = tokenName.match(/-(\d{3})$/);
if (scaleMatch) {
const scale = scaleMatch[1];
palette[scale] = token;
}
}
});
return palette;
}
function determineButtonForegroundColor(backgroundToken) {
if (backgroundToken == null ? void 0 : backgroundToken.oklch) {
return getContrastingForegroundColor(backgroundToken.oklch);
}
return __spreadValues({}, BASE_COLORS.white);
}
function createSemanticTokenMapping({
themeName,
semanticRole,
brandColorName,
scaleInfo,
buttonForegroundColor
}) {
const background100Scale = themeName === "dark" ? 800 : 100;
return {
semantic: {
[`color-background-${semanticRole}-100`]: `color-${brandColorName}-${background100Scale}`,
[`color-background-${semanticRole}-200`]: `color-${brandColorName}-${scaleInfo.backgroundScale}`,
[`color-foreground-${semanticRole}-100`]: `color-${brandColorName}-${scaleInfo.foregroundScale}`,
[`color-foreground-${semanticRole}-200`]: `color-${brandColorName}-${scaleInfo.alternativeScale}`,
[`color-border-${semanticRole}`]: `color-${brandColorName}-${scaleInfo.backgroundScale}`
},
componentSpecific: {
[`color-button-foreground-${semanticRole}`]: buttonForegroundColor.name
}
};
}
function findLightThemeScales(palette, scales) {
var _a, _b;
const deltaEZeroScale = scales.find((scale) => {
var _a2;
return ((_a2 = palette[scale]) == null ? void 0 : _a2.deltaE) === 0;
});
if (!deltaEZeroScale) {
throw new Error("No scale with deltaE 0 found for light theme");
}
const backgroundScale = deltaEZeroScale;
const backgroundIndex = scales.indexOf(backgroundScale);
const foregroundScale = (_a = scales[backgroundIndex + 1]) != null ? _a : backgroundScale;
const alternativeScale = (_b = scales[backgroundIndex + 2]) != null ? _b : foregroundScale;
return { backgroundScale, foregroundScale, alternativeScale };
}
function findDarkThemeScales(palette, scales) {
var _a, _b;
const backgroundScale = findClosestScale(palette);
if (!backgroundScale) {
throw new Error("Could not find a valid background scale in the palette for dark theme.");
}
const backgroundIndex = scales.indexOf(backgroundScale);
const foregroundScale = (_a = scales[backgroundIndex + 1]) != null ? _a : backgroundScale;
const alternativeScale = (_b = scales[backgroundIndex + 2]) != null ? _b : foregroundScale;
return {
backgroundScale,
foregroundScale,
alternativeScale
};
}
function getSemanticDependentTokens(mappingConfig) {
const lightSemanticMapping = {};
const lightComponentMapping = {};
const darkSemanticMapping = {};
const darkComponentMapping = {};
const brandColors = {};
Object.entries(mappingConfig).forEach(([semanticRole, config]) => {
if (semanticRole !== "background") {
brandColors[config.name] = config.color;
}
});
const brandPalette = generateBrandColorPalette({
colors: brandColors,
background: mappingConfig.background
});
Object.entries(mappingConfig).forEach(([semanticRole, config]) => {
if (semanticRole === "background") return;
const themes = [
{
name: "light",
tokens: brandPalette.light.tokens,
findScales: findLightThemeScales,
semanticMappingTarget: lightSemanticMapping,
componentMappingTarget: lightComponentMapping
},
{
name: "dark",
tokens: brandPalette.dark.tokens,
findScales: findDarkThemeScales,
semanticMappingTarget: darkSemanticMapping,
componentMappingTarget: darkComponentMapping
}
];
for (const theme of themes) {
const palette = reconstructPalette(theme.tokens, config.name);
const scales = getSortedScales(palette);
const scaleInfo = theme.findScales(palette, scales);
const backgroundToken = palette[scaleInfo.backgroundScale];
const buttonForegroundColor = determineButtonForegroundColor(backgroundToken);
const tokenMappings = createSemanticTokenMapping({
themeName: theme.name,
semanticRole,
brandColorName: config.name,
scaleInfo,
buttonForegroundColor
});
Object.assign(theme.semanticMappingTarget, tokenMappings.semantic);
Object.assign(theme.componentMappingTarget, tokenMappings.componentSpecific);
}
});
return {
semantic: {
light: {
tokens: lightSemanticMapping,
metadata: {
type: "semantic",
theme: "light"
}
},
dark: {
tokens: darkSemanticMapping,
metadata: {
type: "semantic",
theme: "dark"
}
}
},
componentSpecific: {
light: {
tokens: lightComponentMapping,
metadata: {
type: "component-specific",
theme: "light"
}
},
dark: {
tokens: darkComponentMapping,
metadata: {
type: "component-specific",
theme: "dark"
}
}
}
};
}
export {
ADAPTIVE_COLOR_GENERATION,
BASE_COLORS,
BUTTON_FOREGROUND_LIGHTNESS_THRESHOLD,
DEFAULT_CONTRAST_RATIOS,
DEFAULT_MAIN_BACKGROUND_LIGHTNESS,
DEFAULT_PRIMITIVE_COLORS,
generateBrandColorPalette,
generateSystemColorPalette,
getColorLightness,
getSemanticDependentTokens
};
//# sourceMappingURL=index.js.map