UNPKG

@vapor-ui/color-generator

Version:

접근성을 고려한 대비비 기반 색상 팔레트 생성 라이브러리

600 lines (591 loc) 18.9 kB
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