igniteui-theming
Version:
A set of Sass variables, mixins, and functions for generating palettes, typography, and elevations used by Ignite UI components.
251 lines (250 loc) • 9.92 kB
JavaScript
import { LUMINANCE_THRESHOLD, SUGGESTED_COLORS, analyzeColorForPalette, analyzeSurfaceGrayColors } from "../utils/color.js";
import { formatValidationMessages } from "../utils/result.js";
//#region src/validators/palette.ts
/**
* Palette validation logic.
* Validates surface and gray colors against the theme variant.
*
* Uses the unified ValidationResult type from result.ts while maintaining
* specialized warning types for palette-specific validation.
*/
/**
* Validate surface and gray colors against the theme variant.
*
* Rules:
* - Light variant: surface should be light (luminance > 0.5), gray should be dark (luminance <= 0.5)
* - Dark variant: surface should be dark (luminance <= 0.5), gray should be light (luminance > 0.5)
*
* The gray logic is inverted because the Sass palette() function generates gray shades
* that contrast against the surface.
*
* @param input - Validation input parameters
* @returns Validation result with warnings and tips
*/
async function validatePaletteColors(input) {
const { variant, surface, gray, minimumContrastRatio = 3 } = input;
const warnings = [];
const tips = [];
if (!surface && !gray) return {
isValid: true,
errors: [],
warnings: [],
tips: [],
analysis: {},
metadata: { analysis: {} }
};
let analysis;
try {
analysis = await analyzeSurfaceGrayColors({
surface,
gray
});
} catch (error) {
return {
isValid: false,
errors: [],
warnings: [{
field: "surface",
severity: "warning",
message: `Failed to analyze colors: ${error instanceof Error ? error.message : String(error)}`
}],
tips: ["Ensure color values are valid CSS colors (hex, rgb, hsl, or named colors)"],
analysis: {},
metadata: { analysis: {} }
};
}
if (surface && analysis.surface) {
const surfaceWarning = validateSurfaceColor(analysis.surface, variant);
if (surfaceWarning) warnings.push(surfaceWarning);
}
if (gray && analysis.gray) {
const grayWarning = validateGrayColor(analysis.gray, variant);
if (grayWarning) warnings.push(grayWarning);
}
if (surface && gray && analysis.contrastRatio !== void 0) {
if (analysis.contrastRatio < minimumContrastRatio) warnings.push({
field: "contrast",
severity: "warning",
message: `Contrast ratio between surface and gray is ${analysis.contrastRatio.toFixed(2)}:1, which is below the recommended ${minimumContrastRatio}:1`,
details: { contrastRatio: analysis.contrastRatio }
});
}
if (warnings.some((w) => w.field === "gray")) tips.push("Consider omitting the gray parameter to let the palette() function auto-calculate an appropriate gray base from the surface color");
if (warnings.some((w) => w.field === "surface")) tips.push(`For a ${variant} theme, use a ${variant === "light" ? "light" : "dark"} surface color like ${SUGGESTED_COLORS[variant].surface.slice(0, 3).join(", ")}`);
return {
isValid: warnings.length === 0,
errors: [],
warnings,
tips,
analysis,
metadata: { analysis }
};
}
/**
* Validate surface color against variant.
*/
function validateSurfaceColor(surface, variant) {
const expectLight = variant === "light";
if (surface.isLight === expectLight) return null;
const colorType = surface.isLight ? "light" : "dark";
const expectedType = expectLight ? "light" : "dark";
return {
field: "surface",
severity: "warning",
message: `Surface color "${surface.color}" is a ${colorType} color (luminance: ${surface.luminance.toFixed(2)}), but variant is '${variant}'. Expected a ${expectedType} surface color (luminance ${expectLight ? `> ${LUMINANCE_THRESHOLD}` : `<= ${LUMINANCE_THRESHOLD}`}).`,
currentValue: surface.color,
suggestedValues: SUGGESTED_COLORS[variant].surface.slice(0, 3),
details: {
luminance: surface.luminance,
isLight: surface.isLight,
expectedLuminance: expectLight ? `> ${LUMINANCE_THRESHOLD}` : `<= ${LUMINANCE_THRESHOLD}`
}
};
}
/**
* Validate gray color against variant.
*
* Note: Gray logic is INVERTED from surface because the palette() function
* generates gray shades that need to contrast with the surface.
* - Light variant (light surface) needs dark gray base
* - Dark variant (dark surface) needs light gray base
*/
function validateGrayColor(gray, variant) {
const expectLightGray = variant === "dark";
if (gray.isLight === expectLightGray) return null;
const colorType = gray.isLight ? "light" : "dark";
const expectedType = expectLightGray ? "light" : "dark";
return {
field: "gray",
severity: "warning",
message: `Gray base "${gray.color}" is a ${colorType} color (luminance: ${gray.luminance.toFixed(2)}), but variant is '${variant}'. For ${variant} themes, the gray base should be ${expectedType} (luminance ${expectLightGray ? `> ${LUMINANCE_THRESHOLD}` : `<= ${LUMINANCE_THRESHOLD}`}) to ensure proper contrast with the ${variant} surface.`,
currentValue: gray.color,
suggestedValues: SUGGESTED_COLORS[variant].gray.slice(0, 3),
details: {
luminance: gray.luminance,
isLight: gray.isLight,
expectedLuminance: expectLightGray ? `> ${LUMINANCE_THRESHOLD}` : `<= ${LUMINANCE_THRESHOLD}`
}
};
}
/**
* Format validation result as markdown for display.
*
* @param result - Validation result to format
* @returns Markdown string with warnings and tips
*/
function formatValidationResult(result) {
if (result.isValid) return "";
return formatValidationMessages(result);
}
/**
* Generate Sass comment warnings for code generation.
*
* @param result - Validation result
* @returns Array of Sass comment lines
*/
function generateWarningComments(result) {
if (result.isValid) return [];
return result.warnings.map((warning) => {
return `// ⚠️ Warning: ${warning.field === "contrast" ? "Contrast" : `${capitalize(warning.field)} color`} may not be optimal for this variant`;
});
}
/**
* Capitalize the first letter of a string.
*/
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
/**
* Analyze theme colors for palette shade generation suitability.
* Checks if colors have extreme luminance that would produce poor
* automatic shade generation results with the palette() function.
*
* @param params - Theme colors to analyze
* @returns Analysis result with suitability status and any problematic colors
*/
async function analyzeThemeColorsForPalette(params) {
const colors = {};
const problematicColors = [];
colors.primary = await analyzeColorForPalette(params.primary);
if (!colors.primary.suitable) problematicColors.push({
name: "primary",
color: params.primary,
luminance: colors.primary.luminance,
issue: colors.primary.issue,
description: colors.primary.description
});
if (params.secondary) {
colors.secondary = await analyzeColorForPalette(params.secondary);
if (!colors.secondary.suitable) problematicColors.push({
name: "secondary",
color: params.secondary,
luminance: colors.secondary.luminance,
issue: colors.secondary.issue,
description: colors.secondary.description
});
}
if (params.surface) {
colors.surface = await analyzeColorForPalette(params.surface);
if (!colors.surface.suitable) problematicColors.push({
name: "surface",
color: params.surface,
luminance: colors.surface.luminance,
issue: colors.surface.issue,
description: colors.surface.description
});
}
return {
allSuitable: problematicColors.length === 0,
colors,
problematicColors
};
}
/**
* Format palette suitability warnings as markdown table.
*
* @param result - Suitability analysis result
* @returns Formatted markdown string with warnings table and recommendations
*/
function formatPaletteSuitabilityWarnings(result) {
if (result.allSuitable) return "";
const lines = [];
lines.push("**Color Luminance Warnings:**");
lines.push("");
lines.push("The following colors have extreme luminance values that may produce suboptimal shade generation:");
lines.push("");
lines.push("| Color | Value | Luminance | Issue |");
lines.push("|-------|-------|-----------|-------|");
for (const pc of result.problematicColors) {
const issueText = pc.issue === "too-light" ? "Too light - darker shades (600-900) will appear washed out" : "Too dark - lighter shades (50-200) will lack contrast range";
lines.push(`| ${pc.name} | \`${pc.color}\` | ${pc.luminance.toFixed(2)} | ${issueText} |`);
}
lines.push("");
lines.push("**Recommendation:** For production-quality results, use the `create_custom_palette` tool from the Ignite UI Theming MCP with explicit shade values for these colors. This gives you fine-grained control over each shade level (50-900, A100-A700).");
lines.push("");
lines.push("The generated code below uses the standard `palette()` function, which may produce limited shade ranges for the flagged colors.");
return lines.join("\n");
}
/**
* Generate Sass block comment for palette suitability warnings.
* This comment is inserted into the generated Sass code to warn developers.
*
* @param result - Suitability analysis result
* @returns Array of Sass comment lines
*/
function generatePaletteSuitabilityComments(result) {
if (result.allSuitable) return [];
const lines = [];
lines.push("/*");
lines.push(" * ⚠️ PALETTE SUITABILITY WARNINGS");
lines.push(" * The following colors have extreme luminance that may produce suboptimal shade generation:");
for (const pc of result.problematicColors) {
const issueText = pc.issue === "too-light" ? "too light - darker shades may be washed out" : "too dark - lighter shades may lack range";
lines.push(` * - ${pc.name} (${pc.color}): luminance ${pc.luminance.toFixed(2)} - ${issueText}`);
}
lines.push(" * Consider using the create_custom_palette tool of the Ignite UI Theming MCP server with explicit shade values for better results.");
lines.push(" */");
return lines;
}
//#endregion
export { analyzeThemeColorsForPalette, formatPaletteSuitabilityWarnings, formatValidationResult, generatePaletteSuitabilityComments, generateWarningComments, validatePaletteColors };