@depthark/css-first
Version:
Advanced MCP server for intelligent, context-aware CSS suggestions with logical units, container queries, and automated feature discovery
514 lines (445 loc) • 17.9 kB
text/typescript
/**
* Enhanced CSS suggestion engine with semantic analysis
*/
import { CSSPropertySuggestion, CSSFeatureCategory } from './types.js';
import { searchFeatures, getCarouselFeatures } from './features.js';
import { fetchBrowserSupportFromMDN, fetchMDNData } from './mdnClient.js';
import { analyzeProjectContext, getFrameworkSpecificRecommendations, getCSSFrameworkRecommendations } from './contextAnalyzer.js';
import { rankByLogicalPreference, getWritingModeRecommendations } from './logicalUnitsPreference.js';
/** Intent patterns for semantic analysis */
const INTENT_PATTERNS = {
layout: {
patterns: [
/(?:arrange|organize|position|place|layout)/i,
/(?:center|align|justify|distribute)/i,
/(?:column|row|grid|flex)/i,
/(?:beside|above|below|next to|in a row)/i
],
categories: [CSSFeatureCategory.LAYOUT]
},
animation: {
patterns: [
/(?:animate|transition|move|slide|fade|hover)/i,
/(?:smooth|ease|duration|timing)/i,
/(?:transform|translate|rotate|scale)/i
],
categories: [CSSFeatureCategory.ANIMATION]
},
spacing: {
patterns: [
/(?:space|spacing|gap|margin|padding)/i,
/(?:between|around|inside|outside)/i,
/(?:tight|loose|compressed|expanded)/i
],
categories: [CSSFeatureCategory.LOGICAL]
},
responsive: {
patterns: [
/(?:responsive|mobile|tablet|desktop|breakpoint)/i,
/(?:small screen|large screen|different sizes)/i,
/(?:adapt|resize|scale|fit)/i
],
categories: [CSSFeatureCategory.RESPONSIVE]
},
visual: {
patterns: [
/(?:color|background|border|shadow|gradient)/i,
/(?:appearance|style|design|look)/i,
/(?:opacity|transparency|blur)/i
],
categories: [CSSFeatureCategory.VISUAL]
},
interaction: {
patterns: [
/(?:click|hover|focus|active|disabled)/i,
/(?:interactive|button|link|form)/i,
/(?:state|feedback|response)/i
],
categories: [CSSFeatureCategory.INTERACTION]
}
};
/** Context keywords for project awareness */
const FRAMEWORK_INDICATORS = {
react: ['component', 'jsx', 'react', 'useState', 'useEffect'],
vue: ['template', 'v-if', 'v-for', 'vue', 'composition'],
angular: ['angular', 'component', 'directive', 'ngIf', 'ngFor'],
tailwind: ['tailwind', 'tw-', 'class=', 'className='],
bootstrap: ['bootstrap', 'btn-', 'col-', 'row', 'container']
};
/**
* Analyzes user intent and extracts semantic keywords with context awareness
* @param description - The UI task description to analyze
* @param projectContext - Optional project context (framework, existing CSS, etc.)
* @returns Enhanced analysis with keywords, intent, and context
*/
export function analyzeTaskIntent(description: string, projectContext?: any): {
keywords: string[];
intent: string[];
confidence: number;
suggestedCategories: CSSFeatureCategory[];
frameworkHints: string[];
contextAnalysis?: any;
recommendations?: string[];
} {
const keywords: string[] = [];
const intent: string[] = [];
const suggestedCategories: CSSFeatureCategory[] = [];
const frameworkHints: string[] = [];
let totalMatches = 0;
let totalPatterns = 0;
// Analyze project context
const contextAnalysis = analyzeProjectContext(projectContext);
// Analyze intent patterns
for (const [intentType, config] of Object.entries(INTENT_PATTERNS)) {
let intentMatches = 0;
for (const pattern of config.patterns) {
totalPatterns++;
if (pattern.test(description)) {
intentMatches++;
totalMatches++;
}
}
if (intentMatches > 0) {
intent.push(intentType);
suggestedCategories.push(...config.categories);
}
}
// Extract framework hints from project context and description
if (contextAnalysis.framework) {
frameworkHints.push(contextAnalysis.framework);
}
if (contextAnalysis.cssFramework) {
frameworkHints.push(contextAnalysis.cssFramework);
}
// Additional framework detection from description
for (const [framework, indicators] of Object.entries(FRAMEWORK_INDICATORS)) {
if (indicators.some(indicator =>
description.toLowerCase().includes(indicator)
)) {
frameworkHints.push(framework);
}
}
// Legacy keyword extraction for backward compatibility
keywords.push(...extractLegacyKeywords(description));
const confidence = totalPatterns > 0 ? totalMatches / totalPatterns : 0;
// Generate context-aware recommendations
const recommendations: string[] = [];
if (contextAnalysis.framework) {
recommendations.push(...getFrameworkSpecificRecommendations(contextAnalysis.framework));
}
if (contextAnalysis.cssFramework) {
recommendations.push(...getCSSFrameworkRecommendations(contextAnalysis.cssFramework));
}
// Add logical units recommendations
const writingModeRecommendations = getWritingModeRecommendations(projectContext);
recommendations.push(...writingModeRecommendations);
return {
keywords: [...new Set(keywords)],
intent: [...new Set(intent)],
confidence,
suggestedCategories: [...new Set(suggestedCategories)],
frameworkHints: [...new Set(frameworkHints)],
contextAnalysis,
recommendations: recommendations.length > 0 ? recommendations : undefined
};
}
/**
* Enhanced keyword extraction with semantic understanding
*/
function extractLegacyKeywords(description: string): string[] {
const keywords: string[] = [];
const lowerDescription = description.toLowerCase();
// Split description into words for better analysis
const words = lowerDescription.split(/\s+/);
// Core CSS property keywords - add direct properties
const cssProperties = ['height', 'width', 'position', 'display', 'flex', 'grid', 'margin', 'padding', 'border', 'background', 'color', 'font', 'text', 'transform', 'transition', 'animation'];
for (const prop of cssProperties) {
if (lowerDescription.includes(prop)) {
keywords.push(prop);
}
}
// Viewport and sizing patterns
if (lowerDescription.includes('full') && lowerDescription.includes('height')) {
keywords.push('height', '100vh', '100dvh', 'dvh', 'svh', 'lvh', 'block-size', 'min-height');
}
// Mobile specific patterns
if (lowerDescription.includes('mobile') || lowerDescription.includes('phone')) {
keywords.push('dvh', 'svh', 'lvh', 'mobile-viewport', 'browser-ui', '@media');
if (lowerDescription.includes('height') || lowerDescription.includes('screen') || lowerDescription.includes('full')) {
keywords.push('100dvh', 'height', 'block-size', 'viewport-height');
}
}
// Header specific patterns
if (lowerDescription.includes('header')) {
keywords.push('header');
if (lowerDescription.includes('stick') || lowerDescription.includes('fix')) {
keywords.push('sticky', 'fixed', 'position');
}
if (lowerDescription.includes('full') || lowerDescription.includes('height')) {
keywords.push('height', 'min-height', 'dvh', '100dvh');
}
}
// Layout patterns
if (lowerDescription.includes('center') || lowerDescription.includes('align')) {
keywords.push('center', 'align', 'justify-content', 'align-items', 'flex', 'grid');
}
// Carousel patterns
if (lowerDescription.includes('carousel') || lowerDescription.includes('slider') || lowerDescription.includes('gallery')) {
keywords.push('scroll-snap-type', 'overflow-inline', 'scroll-behavior', '::scroll-marker', '::scroll-button', 'flex');
}
// Theme patterns
if (lowerDescription.includes('theme') || lowerDescription.includes('dark') || lowerDescription.includes('light')) {
keywords.push('light-dark', 'color-scheme', 'prefers-color-scheme', '@media');
}
// Form patterns
if (lowerDescription.includes('form')) {
keywords.push('form');
if (lowerDescription.includes('valid') || lowerDescription.includes('error')) {
keywords.push(':user-valid', ':user-invalid', ':invalid', ':valid');
}
}
// Modal/dialog patterns
if (lowerDescription.includes('modal') || lowerDescription.includes('dialog') || lowerDescription.includes('popup')) {
keywords.push('dialog', ':target', 'position', 'fixed', 'backdrop-filter');
}
// Navigation patterns
if (lowerDescription.includes('nav') || lowerDescription.includes('menu')) {
keywords.push('nav', 'menu');
if (lowerDescription.includes('hamburger') || lowerDescription.includes('mobile')) {
keywords.push(':checked', 'position', 'transform');
}
}
// Animation patterns
if (lowerDescription.includes('animate') || lowerDescription.includes('transition')) {
keywords.push('animation', 'transition', '@keyframes');
if (lowerDescription.includes('scroll')) {
keywords.push('scroll-timeline', 'animation-timeline');
}
}
// Responsive patterns
if (lowerDescription.includes('responsive') || lowerDescription.includes('breakpoint')) {
keywords.push('@media', 'container', 'clamp', 'min', 'max');
}
// Add specific keywords based on common tasks
if (lowerDescription.includes('always')) {
keywords.push('min-height', '100%', 'vh', 'dvh');
}
if (lowerDescription.includes('section')) {
keywords.push('section', 'height', 'min-height');
}
if (lowerDescription.includes('device')) {
keywords.push('dvh', 'svh', '@media', 'viewport');
}
return [...new Set(keywords)]; // Remove duplicates
}
/**
* Backward compatibility wrapper for extractCSSKeywords
*/
export function extractCSSKeywords(description: string): string[] {
const analysis = analyzeTaskIntent(description);
return analysis.keywords;
}
/**
* Enhanced CSS property search with intelligent ranking
* @param description - Task description for semantic analysis
* @param approach - Preferred CSS approach (modern, compatible, progressive)
* @param projectContext - Optional project context for better suggestions
* @returns Promise that resolves to ranked CSS property suggestions
*/
export async function searchMDNForCSSProperties(
description: string | string[],
approach: 'modern' | 'compatible' | 'progressive' = 'modern',
projectContext?: any
): Promise<CSSPropertySuggestion[]> {
// Handle backward compatibility with keyword array
if (Array.isArray(description)) {
return searchLegacyKeywords(description, approach);
}
// New semantic analysis approach
const analysis = analyzeTaskIntent(description, projectContext);
return searchWithIntelligentRanking(analysis, approach, projectContext);
}
/**
* Intelligent ranking system for CSS suggestions
*/
async function searchWithIntelligentRanking(
analysis: {
keywords: string[];
intent: string[];
confidence: number;
suggestedCategories: CSSFeatureCategory[];
frameworkHints: string[];
},
approach: 'modern' | 'compatible' | 'progressive',
projectContext?: any
): Promise<CSSPropertySuggestion[]> {
const suggestions: CSSPropertySuggestion[] = [];
// Get features based on suggested categories with higher priority
for (const _category of analysis.suggestedCategories) {
const categoryFeatures = searchFeatures(analysis.keywords);
// Categories are used implicitly for prioritization in the search algorithm
for (const feature of categoryFeatures) {
const mdnData = await fetchMDNData(feature.properties[0]);
const browserSupport = await fetchBrowserSupportFromMDN(feature.properties[0]);
if (shouldIncludeBasedOnApproach(feature.support_level, approach)) {
const suggestion: CSSPropertySuggestion = {
property: feature.properties[0],
description: feature.description,
syntax: mdnData.syntax || getFeatureSyntax(feature.name),
browser_support: {
overall_support: browserSupport.overall_support,
modern_browsers: browserSupport.overall_support >= 85,
legacy_support: browserSupport.overall_support >= 70 ? 'good' : 'limited'
},
use_cases: getFeatureUseCases(feature.name),
mdn_url: feature.mdn_url
};
// Add ranking score based on relevance
(suggestion as any).relevanceScore = calculateRelevanceScore(
feature,
analysis,
projectContext
);
suggestions.push(suggestion);
}
}
}
// Fallback to keyword-based search if no category matches
if (suggestions.length === 0) {
const fallbackFeatures = searchFeatures(analysis.keywords);
for (const feature of fallbackFeatures.slice(0, 5)) {
const browserSupport = await fetchBrowserSupportFromMDN(feature.properties[0]);
suggestions.push(await createSuggestionFromFeature(feature, browserSupport));
}
}
// Apply logical units preference ranking and sort by relevance score
const logicallyRankedSuggestions = rankByLogicalPreference(suggestions);
return logicallyRankedSuggestions
.sort((a, b) => ((b as any).relevanceScore || 0) - ((a as any).relevanceScore || 0))
.slice(0, 5);
}
/**
* Calculate relevance score for intelligent ranking
*/
function calculateRelevanceScore(
feature: any,
analysis: any,
_projectContext?: any
): number {
let score = 0;
// Intent match bonus
if (analysis.intent.includes('layout') && feature.category === CSSFeatureCategory.LAYOUT) score += 10;
if (analysis.intent.includes('animation') && feature.category === CSSFeatureCategory.ANIMATION) score += 10;
if (analysis.intent.includes('spacing') && feature.category === CSSFeatureCategory.LOGICAL) score += 10;
// Browser support bonus
if (feature.support_level === 'excellent') score += 5;
if (feature.support_level === 'good') score += 3;
// Framework compatibility bonus
if (analysis.frameworkHints.includes('react') && feature.properties.includes('flex')) score += 3;
if (analysis.frameworkHints.includes('tailwind') && feature.properties.includes('grid')) score += 3;
// Confidence multiplier
score *= (1 + analysis.confidence);
return score;
}
/**
* Legacy search function for backward compatibility
*/
async function searchLegacyKeywords(
keywords: string[],
approach: 'modern' | 'compatible' | 'progressive'
): Promise<CSSPropertySuggestion[]> {
const suggestions: CSSPropertySuggestion[] = [];
// Check for carousel-specific requests
if (keywords.includes('carousel') || keywords.includes('slider')) {
const carouselFeatures = getCarouselFeatures();
for (const feature of carouselFeatures) {
const browserSupport = await fetchBrowserSupportFromMDN(feature.properties[0]);
// Filter based on approach
if (shouldIncludeBasedOnApproach(feature.support_level, approach)) {
suggestions.push({
property: feature.properties[0],
description: feature.description,
syntax: getFeatureSyntax(feature.name),
browser_support: {
overall_support: browserSupport.overall_support,
modern_browsers: browserSupport.overall_support >= 85,
legacy_support: browserSupport.overall_support >= 70 ? 'good' : 'limited'
},
use_cases: getFeatureUseCases(feature.name),
mdn_url: feature.mdn_url
});
}
}
}
// Search for other features
const matchingFeatures = searchFeatures(keywords);
for (const feature of matchingFeatures) {
// Skip if already added (carousel features)
if (suggestions.some(s => s.property === feature.properties[0])) continue;
const browserSupport = await fetchBrowserSupportFromMDN(feature.properties[0]);
if (shouldIncludeBasedOnApproach(feature.support_level, approach)) {
suggestions.push({
property: feature.properties[0],
description: feature.description,
syntax: getFeatureSyntax(feature.name),
browser_support: {
overall_support: browserSupport.overall_support,
modern_browsers: browserSupport.overall_support >= 85,
legacy_support: browserSupport.overall_support >= 70 ? 'good' : 'limited'
},
use_cases: getFeatureUseCases(feature.name),
mdn_url: feature.mdn_url
});
}
}
return suggestions;
}
/**
* Helper function to create suggestion from feature
*/
async function createSuggestionFromFeature(feature: any, browserSupport: any): Promise<CSSPropertySuggestion> {
return {
property: feature.properties[0],
description: feature.description,
syntax: getFeatureSyntax(feature.name),
browser_support: {
overall_support: browserSupport.overall_support,
modern_browsers: browserSupport.overall_support >= 85,
legacy_support: browserSupport.overall_support >= 70 ? 'good' : 'limited'
},
use_cases: getFeatureUseCases(feature.name),
mdn_url: feature.mdn_url
};
}
/**
* Determines if a feature should be included based on the approach
*/
function shouldIncludeBasedOnApproach(
supportLevel: 'excellent' | 'good' | 'moderate' | 'limited' | 'experimental',
approach: 'modern' | 'compatible' | 'progressive'
): boolean {
switch (approach) {
case 'modern':
return ['excellent', 'good', 'experimental'].includes(supportLevel);
case 'compatible':
return supportLevel === 'excellent';
case 'progressive':
return true; // Include all, with fallbacks
default:
return true;
}
}
/**
* Gets syntax example for a CSS feature from MDN data
*/
function getFeatureSyntax(featureName: string): string {
// All syntax should come from MDN data, not hardcoded
return `${featureName}: value;`;
}
/**
* Gets use cases for a CSS feature from MDN data
*/
function getFeatureUseCases(featureName: string): string[] {
// All use cases should come from MDN data, not hardcoded
return ['General styling', 'UI enhancement'];
}