igniteui-theming
Version:
A set of Sass variables, mixins, and functions for generating palettes, typography, and elevations used by Ignite UI components.
221 lines (207 loc) • 9 kB
JavaScript
import { COMPONENT_METADATA, getComponentPlatformAvailability, getCompoundComponentInfo, getThemingSelector, getVariants, hasVariants, isComponentAvailable } from "../../knowledge/component-metadata.js";
import { COMPONENT_NAMES, getComponentTheme, searchComponents, validateTokens } from "../../knowledge/component-themes.js";
import { PLATFORM_METADATA } from "../../knowledge/platforms/index.js";
import "../../knowledge/index.js";
import { SASS_USE_ASSEMBLY_NOTE } from "../../utils/sass.js";
import { generateComponentTheme } from "../../generators/sass.js";
//#region src/tools/handlers/component-theme.ts
/**
* Handler for create_component_theme tool.
* Generates Sass code to customize a component's appearance.
*/
/**
* Build a warning note when a composed component is themed with non-primary tokens.
* Returns the warning string or empty string if not applicable.
*/
function getComposedTokenWarning(componentName, tokenNames) {
if (!getCompoundComponentInfo(componentName)?.composed) return "";
const componentTheme = getComponentTheme(componentName);
const primaryNames = new Set((componentTheme?.primaryTokens ?? []).map((pt) => pt.name));
const nonPrimaryTokens = tokenNames.filter((t) => !primaryNames.has(t));
if (nonPrimaryTokens.length === 0) return "";
return `
⚠️ **Composed component notice:** \`${componentName}\` only needs the primary tokens (${[...primaryNames].map((t) => `\`${t}\``).join(", ")}). The other ${nonPrimaryTokens.length} token(s) override auto-derived values, which may cause visual inconsistencies. If the user did not explicitly request these tokens, consider re-generating with only the primary tokens.`;
}
async function handleCreateComponentTheme(params) {
const { platform, component, tokens, selector, name, output = "sass", designSystem = "material", variant = "light" } = params;
const normalizedComponent = component.toLowerCase().trim();
if (!platform) return {
content: [{
type: "text",
text: `**Error:** The \`platform\` parameter is required.
**Valid platforms:**
- \`angular\` - Ignite UI for Angular
- \`webcomponents\` - Ignite UI for Web Components
- \`react\` - Ignite UI for React
- \`blazor\` - Ignite UI for Blazor
Please specify which platform you're using to generate the correct variable prefixes and selectors.`
}],
isError: true
};
if (platform === "generic") return {
content: [{
type: "text",
text: `**Error:** \`create_component_theme\` requires a specific Ignite UI product platform.
The \`"generic"\` platform is not supported for component theming because component selectors and variable prefixes are platform-specific and do not exist in generic mode.
**Valid platforms:**
- \`angular\` - Ignite UI for Angular
- \`webcomponents\` - Ignite UI for Web Components
- \`react\` - Ignite UI for React
- \`blazor\` - Ignite UI for Blazor`
}],
isError: true
};
if (!getComponentTheme(normalizedComponent)) {
const suggestions = searchComponents(normalizedComponent);
const componentList = suggestions.length > 0 ? suggestions.slice(0, 10) : COMPONENT_NAMES.slice(0, 15);
return {
content: [{
type: "text",
text: `**Error:** Component "${component}" not found.
${suggestions.length > 0 ? "**Similar components:**" : "**Available components:**"}
${componentList.map((c) => `- ${c}`).join("\n")}
**Tip:** Use \`get_component_design_tokens\` first to discover valid component names and their tokens.`
}],
isError: true
};
}
if (hasVariants(normalizedComponent)) {
const variants = getVariants(normalizedComponent);
return {
content: [{
type: "text",
text: `**Error:** The \`${component}\` component has multiple variants and requires a specific variant for theming.
**Available variants:**
${variants.map((v) => `- \`${v}\``).join("\n")}
Please use \`create_component_theme\` with one of the specific variant names above.
**Tip:** Use \`get_component_design_tokens\` with a specific variant (e.g., \`${variants[0]}\`) to see available tokens.`
}],
isError: true
};
}
if (platform) {
const availabilityTarget = COMPONENT_METADATA[normalizedComponent]?.childOf ?? normalizedComponent;
if (!isComponentAvailable(availabilityTarget, platform)) {
const availability = getComponentPlatformAvailability(availabilityTarget);
const availablePlatforms = [];
if (availability?.angular) availablePlatforms.push("Angular");
if (availability?.webcomponents) availablePlatforms.push("Web Components");
return {
content: [{
type: "text",
text: `**Error:** The \`${component}\` component is not available on ${PLATFORM_METADATA[platform]?.name ?? platform}. ${availablePlatforms.length > 0 ? `It is available on: ${availablePlatforms.join(", ")}.` : ""}`
}],
isError: true
};
}
}
const tokenNames = Object.keys(tokens);
if (tokenNames.length === 0) return {
content: [{
type: "text",
text: `**Error:** No tokens provided. At least one token must be specified.
Use \`get_component_design_tokens\` with component "${component}" to see available tokens.`
}],
isError: true
};
const validation = validateTokens(normalizedComponent, tokenNames);
if (!validation.isValid) return {
content: [{
type: "text",
text: `**Error:** Invalid token(s) for component "${component}":
${validation.invalidTokens.map((t) => `- \`${t}\``).join("\n")}
**Valid tokens for ${component}:**
${validation.validTokens.slice(0, 20).map((t) => `- \`${t}\``).join("\n")}${validation.validTokens.length > 20 ? `\n... and ${validation.validTokens.length - 20} more` : ""}
Use \`get_component_design_tokens\` to see all tokens with descriptions.`
}],
isError: true
};
let finalSelector = selector;
if (!finalSelector && platform) {
const selectors = getThemingSelector(normalizedComponent, platform);
if (selectors.length > 0) finalSelector = selectors[0];
}
if (output === "css") try {
const { generateComponentThemeCss, formatCssOutput } = await import("../../generators/css.js");
const result = await generateComponentThemeCss({
platform,
component: normalizedComponent,
tokens,
selector: finalSelector,
name,
designSystem,
variant
});
const responseParts = [];
responseParts.push(result.description);
responseParts.push("");
const platformNote = platform ? `Platform: ${PLATFORM_METADATA[platform]?.name ?? platform}` : "Platform: Not specified (generic output). Specify `platform` for optimized imports.";
responseParts.push(platformNote);
responseParts.push(`Design System: ${designSystem.charAt(0).toUpperCase() + designSystem.slice(1)} (${variant})`);
if (finalSelector) responseParts.push(`Selector: \`${finalSelector}\``);
responseParts.push("");
responseParts.push("```css");
responseParts.push(formatCssOutput(result.css, result.description).trimEnd());
responseParts.push("```");
responseParts.push("");
responseParts.push("---");
responseParts.push("**Usage:** Include this CSS in your stylesheet or add it to your application's global styles.");
const cssWarning = getComposedTokenWarning(normalizedComponent, Object.keys(tokens));
return { content: [{
type: "text",
text: responseParts.join("\n") + cssWarning
}] };
} catch (error) {
return {
content: [{
type: "text",
text: `**Error generating CSS:** ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
try {
const result = generateComponentTheme({
platform,
licensed: params.licensed,
component: normalizedComponent,
tokens,
selector: finalSelector,
name,
designSystem,
variant
});
const responseParts = [];
responseParts.push(result.description);
responseParts.push("");
const platformNote = platform ? `Platform: ${PLATFORM_METADATA[platform]?.name ?? platform}` : "Platform: Not specified (generic output). Specify `platform` for optimized imports.";
responseParts.push(platformNote);
responseParts.push(`Design System: ${designSystem.charAt(0).toUpperCase() + designSystem.slice(1)} (${variant})`);
if (finalSelector) responseParts.push(`Selector: \`${finalSelector}\``);
responseParts.push("");
responseParts.push(`Variables created: ${result.variables.join(", ")}`);
responseParts.push("");
responseParts.push("```scss");
responseParts.push(result.code.trimEnd());
responseParts.push("```");
responseParts.push(SASS_USE_ASSEMBLY_NOTE);
responseParts.push("");
responseParts.push("---");
responseParts.push("**Usage:** Import this Sass file in your main styles file, or include the code in your theme file.");
const sassWarning = getComposedTokenWarning(normalizedComponent, Object.keys(tokens));
return { content: [{
type: "text",
text: responseParts.join("\n") + sassWarning
}] };
} catch (error) {
return {
content: [{
type: "text",
text: `**Error generating theme:** ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
}
//#endregion
export { handleCreateComponentTheme };