UNPKG

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
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 };