From bb6f8604b6189525214e511d2dbe70c8923eb747 Mon Sep 17 00:00:00 2001 From: lemone112 Date: Fri, 13 Mar 2026 16:37:21 +0300 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20Sprint=206=20=E2=80=94=20Advanced?= =?UTF-8?q?=20Analysis=20Engine=20(18=20features)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive design analysis capabilities spanning deterministic lint rules, AI-powered review pipelines, and design system compliance: **Deterministic Lint Modules (plugin sandbox):** - Fitts's Law calculator (target size, consecutive click difficulty, thumb zones) - Gestalt proximity/continuity checker (spatial clustering, alignment) - Detached instance detection (via detachedInfo API + heuristic fallback) - Dark mode validation (pure black/white, missing mode values, contrast) - Responsive design prediction (fixed widths, truncation risk, layoutWrap) - Real-time incremental linting via documentchange events **Design System Compliance (plugin sandbox):** - DTCG token parser (W3C .tokens.json with $type inheritance) - Token compliance engine (Levenshtein fuzzy matching, adoption scoring) - Variable system collector (collections, modes, consumers, adoption rate) - Mode comparator (light/dark value diffing) - Design debt composite score (orphaned styles, detached instances, hardcoded values) **AI Analysis Pipelines (backend):** - Attention prediction + Nielsen's 10 Heuristics (6 automatable) - Cognitive Walkthrough (multi-image, 4 CW questions per step) - PURE scoring (3 parallel evaluators: UX Designer, A11y, Business Analyst) - Brand consistency analysis (color tolerance, typography, spacing, personality) - Copy/tone consistency across flow (text-only, no screenshots) - Persona-based mock research (5 parallel personas, universal barrier detection) - Dark mode AI validation (side-by-side light/dark comparison) - Responsive design AI validation - Accessibility spec generator - Whole-page sweep (lint + screenshot all frames, File Health Report) **UI:** - PageSweepCard component (file health, frame grid, AI insights) - Sweep Page button in QuickActions and bottom bar Co-Authored-By: Claude Opus 4.6 --- backend/src/index.ts | 18 + backend/src/prompts/a11y-spec.ts | 67 ++++ backend/src/prompts/attention.ts | 51 +++ backend/src/prompts/brand-consistency.ts | 122 ++++++ backend/src/prompts/cognitive-walkthrough.ts | 72 ++++ backend/src/prompts/copy-tone.ts | 90 +++++ backend/src/prompts/dark-mode.ts | 68 ++++ backend/src/prompts/nielsen-heuristics.ts | 112 ++++++ backend/src/prompts/page-sweep.ts | 33 ++ backend/src/prompts/persona-research.ts | 76 ++++ backend/src/prompts/pure-scoring.ts | 109 ++++++ backend/src/prompts/responsive.ts | 64 ++++ backend/src/routes/a11y-spec.ts | 98 +++++ backend/src/routes/analyze.ts | 42 ++- backend/src/routes/brand-consistency.ts | 146 ++++++++ backend/src/routes/cognitive-walkthrough.ts | 123 ++++++ backend/src/routes/copy-tone.ts | 118 ++++++ backend/src/routes/dark-mode.ts | 193 ++++++++++ backend/src/routes/page-sweep.ts | 27 ++ backend/src/routes/persona-research.ts | 84 +++++ backend/src/routes/pure-scoring.ts | 75 ++++ backend/src/routes/responsive.ts | 70 ++++ backend/src/services/a11y-spec-generator.ts | 227 ++++++++++++ backend/src/services/brand-consistency.ts | 190 ++++++++++ backend/src/services/cognitive-walkthrough.ts | 204 ++++++++++ backend/src/services/copy-tone.ts | 187 ++++++++++ backend/src/services/extended-analyzer.ts | 272 ++++++++++++++ backend/src/services/page-sweep-analyzer.ts | 238 ++++++++++++ backend/src/services/persona-research.ts | 319 ++++++++++++++++ backend/src/services/pure-scoring.ts | 309 ++++++++++++++++ backend/src/services/responsive-validator.ts | 115 ++++++ dist/code.js | 60 +-- dist/ui.html | 50 +-- figma.d.ts | 38 ++ src/baseline/design-debt.ts | 171 +++++++++ src/baseline/dtcg-parser.ts | 65 ++++ src/baseline/storage.ts | 2 + src/baseline/token-compliance.ts | 207 +++++++++++ src/core/design-lint.ts | 108 +++++- src/extract/mode-comparator.ts | 121 ++++++ src/extract/variable-collector.ts | 231 ++++++++++++ src/lint/dark-mode.ts | 239 ++++++++++++ src/lint/detached-instance.ts | 117 ++++++ src/lint/fitts-law.ts | 107 ++++++ src/lint/gestalt.ts | 128 +++++++ src/lint/realtime-lint.ts | 130 +++++++ src/lint/responsive.ts | 349 ++++++++++++++++++ src/lint/types.ts | 6 +- src/types.ts | 19 +- src/ui/message-handler.ts | 227 ++++++++++++ ui/src/App.tsx | 115 +++++- ui/src/components/chat/MessageList.tsx | 3 + ui/src/components/messages/PageSweepCard.tsx | 203 ++++++++++ ui/src/components/shared/QuickActions.tsx | 7 + ui/src/lib/api.ts | 50 +++ ui/src/lib/messages.ts | 162 +++++++- 56 files changed, 6765 insertions(+), 69 deletions(-) create mode 100644 backend/src/prompts/a11y-spec.ts create mode 100644 backend/src/prompts/attention.ts create mode 100644 backend/src/prompts/brand-consistency.ts create mode 100644 backend/src/prompts/cognitive-walkthrough.ts create mode 100644 backend/src/prompts/copy-tone.ts create mode 100644 backend/src/prompts/dark-mode.ts create mode 100644 backend/src/prompts/nielsen-heuristics.ts create mode 100644 backend/src/prompts/page-sweep.ts create mode 100644 backend/src/prompts/persona-research.ts create mode 100644 backend/src/prompts/pure-scoring.ts create mode 100644 backend/src/prompts/responsive.ts create mode 100644 backend/src/routes/a11y-spec.ts create mode 100644 backend/src/routes/brand-consistency.ts create mode 100644 backend/src/routes/cognitive-walkthrough.ts create mode 100644 backend/src/routes/copy-tone.ts create mode 100644 backend/src/routes/dark-mode.ts create mode 100644 backend/src/routes/page-sweep.ts create mode 100644 backend/src/routes/persona-research.ts create mode 100644 backend/src/routes/pure-scoring.ts create mode 100644 backend/src/routes/responsive.ts create mode 100644 backend/src/services/a11y-spec-generator.ts create mode 100644 backend/src/services/brand-consistency.ts create mode 100644 backend/src/services/cognitive-walkthrough.ts create mode 100644 backend/src/services/copy-tone.ts create mode 100644 backend/src/services/extended-analyzer.ts create mode 100644 backend/src/services/page-sweep-analyzer.ts create mode 100644 backend/src/services/persona-research.ts create mode 100644 backend/src/services/pure-scoring.ts create mode 100644 backend/src/services/responsive-validator.ts create mode 100644 src/baseline/design-debt.ts create mode 100644 src/baseline/dtcg-parser.ts create mode 100644 src/baseline/token-compliance.ts create mode 100644 src/extract/mode-comparator.ts create mode 100644 src/extract/variable-collector.ts create mode 100644 src/lint/dark-mode.ts create mode 100644 src/lint/detached-instance.ts create mode 100644 src/lint/fitts-law.ts create mode 100644 src/lint/gestalt.ts create mode 100644 src/lint/realtime-lint.ts create mode 100644 src/lint/responsive.ts create mode 100644 ui/src/components/messages/PageSweepCard.tsx diff --git a/backend/src/index.ts b/backend/src/index.ts index 4377d35..bb9edfa 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -11,6 +11,15 @@ import chat from './routes/chat.js'; import stream from './routes/stream.js'; import session from './routes/session.js'; import flow from './routes/flow.js'; +import cognitiveWalkthrough from './routes/cognitive-walkthrough.js'; +import pureScoring from './routes/pure-scoring.js'; +import brandConsistency from './routes/brand-consistency.js'; +import copyTone from './routes/copy-tone.js'; +import personaResearch from './routes/persona-research.js'; +import responsive from './routes/responsive.js'; +import a11ySpec from './routes/a11y-spec.js'; +import darkMode from './routes/dark-mode.js'; +import pageSweep from './routes/page-sweep.js'; import { cleanupExpiredSessions } from './db/queries.js'; import { mkdirSync, existsSync } from 'fs'; import { dirname } from 'path'; @@ -65,6 +74,15 @@ app.route('/api', chat); app.route('/api', stream); app.route('/api', session); app.route('/api', flow); +app.route('/api', cognitiveWalkthrough); +app.route('/api', pureScoring); +app.route('/api', brandConsistency); +app.route('/api', copyTone); +app.route('/api', personaResearch); +app.route('/api', responsive); +app.route('/api', a11ySpec); +app.route('/api', darkMode); +app.route('/api', pageSweep); // Root app.get('/', (c) => c.json({ name: 'FigmaLint Design Review API', version: '1.0.0' })); diff --git a/backend/src/prompts/a11y-spec.ts b/backend/src/prompts/a11y-spec.ts new file mode 100644 index 0000000..e073cbc --- /dev/null +++ b/backend/src/prompts/a11y-spec.ts @@ -0,0 +1,67 @@ +/** + * Build the AI prompt for generating a comprehensive accessibility specification. + */ +export function buildA11ySpecPrompt( + componentInfo: string, + lintSummary: string, +): string { + return `You are a WCAG 2.2 accessibility specialist generating a comprehensive accessibility specification from a UI design. + +## Component Information +${componentInfo} + +## Lint Summary (deterministic analysis) +${lintSummary} + +## Your Task +Analyze the screenshot and generate a complete accessibility specification. This spec will be used by developers to implement proper ARIA attributes, keyboard navigation, focus management, and screen reader support. + +For each section, be specific to what you see in the design — don't generate generic boilerplate. + +Respond in this exact JSON format: +{ + "landmarks": [ + { "role": "", "label": "", "element": "" } + ], + "headingStructure": [ + { "level": 1, "text": "", "element": "" } + ], + "focusOrder": [ + { "order": 1, "element": "", "type": "", "notes": "" } + ], + "ariaAnnotations": [ + { + "element": "", + "role": "", + "ariaLabel": "", + "ariaDescribedBy": "", + "ariaLive": "", + "notes": "" + } + ], + "keyboardShortcuts": [ + { "key": "", "action": "", "element": "" } + ], + "liveRegions": [ + { "element": "", "type": "polite|assertive", "trigger": "" } + ], + "colorContrastReport": [ + { "element": "", "foreground": "", "background": "", "ratio": 4.5, "passes": "AA|AAA|fail" } + ], + "recommendations": [ + { + "title": "", + "description": "", + "wcagCriterion": "", + "level": "A|AA|AAA" + } + ] +} + +Important: +- For focusOrder, list ALL interactive elements in the logical tab order you observe +- For ariaAnnotations, include every element that needs non-default ARIA attributes +- For colorContrastReport, estimate contrast ratios from what you see — flag anything that appears to have low contrast +- For recommendations, reference specific WCAG 2.2 success criteria +- Be thorough but avoid false positives — only flag real issues`; +} diff --git a/backend/src/prompts/attention.ts b/backend/src/prompts/attention.ts new file mode 100644 index 0000000..2159d2f --- /dev/null +++ b/backend/src/prompts/attention.ts @@ -0,0 +1,51 @@ +/** + * Visual Attention Prediction prompt. + * Asks Claude Vision to predict where users will look and how attention flows. + */ +export function buildAttentionPrompt(lintContext: string): string { + return `Analyze this UI screenshot for visual attention patterns. Use established eye-tracking research (Nielsen Norman Group, Gutenberg diagram) to predict user gaze behavior. + +Context from automated lint: +${lintContext} + +Evaluate all of the following: + +## 1. Focal Point Identification +What element draws the eye first? Consider size, contrast, color saturation, isolation (whitespace), and position. Is this element the intended primary CTA or key content? + +## 2. Reading Flow Pattern +Does the layout guide the eye in an F-pattern (typical for content-heavy pages with left-aligned text), Z-pattern (typical for landing pages with hero + CTA), linear (single-column scroll), or scattered (no clear flow)? + +## 3. Attention Competition +Identify elements that compete for attention simultaneously. Look for: multiple high-contrast elements at similar visual weight, competing CTAs, clashing colors, or animation-suggesting elements (spinners, progress bars) that would pull focus. + +## 4. Attention Dead Zones +Identify areas users are likely to skip. Common dead zones: right sidebar content (banner blindness), below-the-fold content with no scroll affordance, low-contrast text blocks, dense text without headings. + +## 5. Visual Weight Distribution +Assess whether the overall visual weight is balanced or skewed. Consider element density, color weight, and whitespace distribution across the four quadrants. + +Respond in this exact JSON format: +{ + "focalPoint": { + "element": "", + "strength": "strong|moderate|weak", + "isIntendedCTA": true|false + }, + "readingFlow": { + "pattern": "F|Z|linear|scattered", + "confidence": "high|medium|low", + "description": "<1-2 sentence explanation of how the eye moves through the design>" + }, + "competingElements": [ + { "element": "", "reason": "" } + ], + "deadZones": [ + { "area": "", "suggestion": "" } + ], + "visualWeightBalance": "balanced|left-heavy|right-heavy|top-heavy|bottom-heavy", + "recommendations": [ + { "title": "", "description": "", "severity": "critical|warning|info" } + ] +}`; +} diff --git a/backend/src/prompts/brand-consistency.ts b/backend/src/prompts/brand-consistency.ts new file mode 100644 index 0000000..147c8cf --- /dev/null +++ b/backend/src/prompts/brand-consistency.ts @@ -0,0 +1,122 @@ +/** + * Brand Consistency Analysis prompt. + * Evaluates a design screenshot against a structured brand guide. + */ + +export interface BrandGuide { + colors: Record; + typography: { + heading: { family: string; weights: number[] }; + body: { family: string; weights: number[] }; + }; + spacing: { base: number; scale: number[] }; + personality: string[]; // e.g., ['professional', 'approachable', 'modern'] + rules?: Array<{ id: string; description: string; severity: 'error' | 'warning' }>; +} + +export function buildBrandConsistencyPrompt( + brandGuide: BrandGuide, + lintContext: string, +): string { + // Serialize color palette for the prompt + const colorLines = Object.entries(brandGuide.colors) + .map( + ([name, c]) => + ` - ${name}: ${c.hex} (tolerance: +/-${c.tolerance}%, usage: ${c.usage})`, + ) + .join('\n'); + + // Serialize typography + const typoLines = [ + ` Headings: ${brandGuide.typography.heading.family} [weights: ${brandGuide.typography.heading.weights.join(', ')}]`, + ` Body: ${brandGuide.typography.body.family} [weights: ${brandGuide.typography.body.weights.join(', ')}]`, + ].join('\n'); + + // Serialize spacing + const spacingLine = ` Base unit: ${brandGuide.spacing.base}px, scale: [${brandGuide.spacing.scale.join(', ')}]`; + + // Serialize personality + const personalityLine = brandGuide.personality.join(', '); + + // Serialize custom rules + const rulesBlock = + brandGuide.rules && brandGuide.rules.length > 0 + ? `\n## Custom Brand Rules\n${brandGuide.rules.map((r) => ` - [${r.severity.toUpperCase()}] ${r.id}: ${r.description}`).join('\n')}\n` + : ''; + + return `You are a brand design auditor. Evaluate the screenshot against the brand guidelines provided below. Be precise and cite specific visual evidence from the screenshot. + +## Brand Color Palette +${colorLines} + +## Typography Specifications +${typoLines} + +## Spacing System +${spacingLine} + +## Brand Personality Keywords +${personalityLine} +${rulesBlock} +## Automated Lint Context +${lintContext} + +## Evaluation Criteria + +### 1. Color Palette Adherence +Examine every visible color in the UI. Compare each against the brand palette. Flag colors that fall outside the specified tolerance. Note which element uses the off-brand color and what the expected color should be. + +### 2. Typography Compliance +Check all visible text for correct font family and weight usage. Headings must use the heading family/weights. Body text must use the body family/weights. Flag any text that appears to use a non-specified font or weight. + +### 3. Spacing System Consistency +Check that spacing between elements aligns with the base grid and scale values. Flag values that do not correspond to any value in the spacing scale. Estimate spacing in pixels from the screenshot. + +### 4. Brand Personality Match +Assess whether the overall visual impression matches the listed personality keywords. Consider color warmth/coolness, typography tone, whitespace usage, imagery style, and overall aesthetic. Rate the match as "strong", "moderate", or "weak" with specific evidence. + +### 5. Visual Language Consistency +Evaluate iconography style (outlined vs filled, rounded vs sharp), illustration style if present, and photography treatment. Flag inconsistencies within the screenshot. + +Respond in this exact JSON format: +{ + "overallScore": <0-100>, + "colorCompliance": { + "score": <0-100>, + "violations": [ + { + "element": "", + "found": "", + "expected": "", + "tolerance": + } + ] + }, + "typographyCompliance": { + "score": <0-100>, + "violations": [ + { + "element": "", + "found": "", + "expected": "" + } + ] + }, + "spacingCompliance": { + "score": <0-100>, + "offGridValues": [] + }, + "personalityMatch": { + "rating": "strong|moderate|weak", + "evidence": ["", ""] + }, + "recommendations": [ + { + "title": "", + "description": "", + "severity": "error|warning|info" + } + ], + "summary": "<3-5 sentence overall brand consistency assessment>" +}`; +} diff --git a/backend/src/prompts/cognitive-walkthrough.ts b/backend/src/prompts/cognitive-walkthrough.ts new file mode 100644 index 0000000..278cde4 --- /dev/null +++ b/backend/src/prompts/cognitive-walkthrough.ts @@ -0,0 +1,72 @@ +/** + * Build the AI prompt for a cognitive walkthrough analysis. + * Sent alongside screenshots of all frames in the flow. + */ +export function buildCognitiveWalkthroughPrompt( + taskDescription: string, + frameLabels: string[], + edgeDescriptions: string[], + interactiveElementDescriptions: string[], +): string { + return `You are conducting a formal Cognitive Walkthrough (CW) of a user interface flow. + +## Task +The user is trying to: "${taskDescription}" + +## Flow Structure +The flow consists of ${frameLabels.length} screens, presented in order: +${frameLabels.map((label, i) => ` ${i + 1}. ${label}`).join('\n')} + +## Transitions (edges) +${edgeDescriptions.length > 0 ? edgeDescriptions.join('\n') : 'No explicit transitions provided.'} + +## Interactive Elements Per Screen +${interactiveElementDescriptions.length > 0 ? interactiveElementDescriptions.join('\n\n') : 'No interactive element data provided.'} + +## Your Task +For EACH step (transition from one frame to the next), answer the 4 standard Cognitive Walkthrough questions. Base your answers on what is visually evident in the screenshots and the interactive element data provided. + +### The 4 CW Questions +1. **Will the user try to achieve the right effect?** - Is the user's goal clear from this screen? Does the screen communicate what the user should do next to advance toward their task? +2. **Will the user notice that the correct action is available?** - Is the interactive element (button, link, input) that the user needs visible, prominent, and affordant? Could the user miss it? +3. **Will the user associate the correct action with the expected outcome?** - Does the label, icon, or visual treatment of the interactive element clearly indicate what will happen when activated? Is there ambiguity? +4. **Will the user see that progress is being made?** - After performing the action, does the next screen provide clear feedback that the action succeeded and the user is closer to their goal? + +### Rating Scale +For each question, answer: +- "yes" - The design clearly supports this +- "partially" - The design somewhat supports this but there are concerns +- "no" - The design fails to support this; users will likely struggle + +### Overall Step Success +Rate each step: +- "likely" - All 4 questions answered "yes" or at most one "partially" +- "uncertain" - Multiple "partially" answers or one "no" +- "unlikely" - Multiple "no" answers; users will likely fail at this step + +Respond in this exact JSON format: +{ + "steps": [ + { + "stepNumber": 1, + "fromFrame": "", + "toFrame": "", + "action": "", + "questions": { + "q1_willTry": { "answer": "yes|partially|no", "explanation": "<2-3 sentences>" }, + "q2_willNotice": { "answer": "yes|partially|no", "explanation": "<2-3 sentences>" }, + "q3_willAssociate": { "answer": "yes|partially|no", "explanation": "<2-3 sentences>" }, + "q4_willSeeProgress": { "answer": "yes|partially|no", "explanation": "<2-3 sentences>" } + }, + "overallSuccess": "likely|uncertain|unlikely", + "barriers": ["", ""], + "suggestions": ["", ""] + } + ], + "overallAssessment": { + "taskCompletionLikelihood": "high|medium|low", + "criticalBarriers": [""], + "summary": "<3-5 sentence summary of the walkthrough findings>" + } +}`; +} diff --git a/backend/src/prompts/copy-tone.ts b/backend/src/prompts/copy-tone.ts new file mode 100644 index 0000000..13090e8 --- /dev/null +++ b/backend/src/prompts/copy-tone.ts @@ -0,0 +1,90 @@ +/** + * Copy & Tone Consistency prompt. + * Analyzes text content across multiple screens for voice/terminology consistency. + */ + +export function buildCopyTonePrompt( + screens: Array<{ name: string; textContent: string[] }>, + personality?: string[], +): string { + const screensBlock = screens + .map((s, i) => { + const texts = s.textContent.map((t) => ` "${t}"`).join('\n'); + return ` Screen ${i + 1}: "${s.name}"\n${texts}`; + }) + .join('\n\n'); + + const personalityBlock = + personality && personality.length > 0 + ? `\n## Brand Voice / Personality Keywords\n${personality.join(', ')}\n\nEvaluate whether the copy aligns with these personality traits.\n` + : ''; + + return `You are a senior UX copywriter and content strategist. Analyze the text content below, extracted from multiple screens of the same product. Your goal is to find inconsistencies in terminology, tone, voice, and readability across the flow. + +## Screens and Text Content +${screensBlock} +${personalityBlock} +## Evaluation Criteria + +### 1. Terminology Consistency +Find cases where the same concept is referred to with different terms across screens (e.g., "Sign up" vs "Register", "Delete" vs "Remove", "Settings" vs "Preferences"). Each inconsistency should name both terms, list the screens they appear on, and recommend which term to standardize on. + +### 2. Tone / Voice Consistency +Detect shifts in formality (formal vs casual), person (first-person "I/My" vs second-person "You/Your" vs third-person), sentence structure (imperative vs declarative), and emotional register (friendly vs clinical). Flag specific text that breaks the dominant tone. + +### 3. CTA Language Patterns +Examine all calls-to-action (buttons, links, prompts). Check for consistent use of action verbs (e.g., always "Create" or always "Add"), consistent casing (title case vs sentence case), and consistent structure (verb-first vs noun-first). + +### 4. Readability Issues +Flag text that is overly long for its context, uses jargon or technical terms without explanation, contains passive voice where active would be clearer, or has grammatical issues. Consider the user's likely reading context (quick scan vs focused reading). + +### 5. Accessibility of Language +Flag idioms, cultural references, abbreviations without expansion, or overly complex sentence structures that may be difficult for non-native speakers or users with cognitive disabilities. + +Respond in this exact JSON format: +{ + "overallConsistency": "consistent|mostly_consistent|inconsistent", + "terminologyIssues": [ + { + "term1": "", + "term2": "", + "screens": ["", ""], + "recommendation": "" + } + ], + "toneIssues": [ + { + "screen": "", + "text": "", + "tone": "", + "expectedTone": "" + } + ], + "ctaPatterns": { + "consistent": true|false, + "patterns": ["", ""], + "violations": [ + { + "screen": "", + "cta": "", + "issue": "" + } + ] + }, + "readabilityIssues": [ + { + "screen": "", + "text": "", + "issue": "" + } + ], + "recommendations": [ + { + "title": "", + "description": "", + "priority": "high|medium|low" + } + ], + "summary": "<3-5 sentence overall copy consistency assessment>" +}`; +} diff --git a/backend/src/prompts/dark-mode.ts b/backend/src/prompts/dark-mode.ts new file mode 100644 index 0000000..4b836dd --- /dev/null +++ b/backend/src/prompts/dark-mode.ts @@ -0,0 +1,68 @@ +export const DARK_MODE_SYSTEM_PROMPT = `You are a senior product designer specializing in dark mode design and accessibility. You evaluate side-by-side screenshots of light and dark mode UI to identify issues with the dark mode implementation. + +Expertise: +- Material Design 3 dark theme guidelines +- Apple HIG dark mode best practices +- WCAG 2.1 AA contrast requirements in both light and dark contexts +- Dark mode elevation and depth perception +- Image and illustration adaptation for dark contexts +- Color semantics preservation across themes + +Your reviews are: +- Grounded in evidence from both screenshots +- Specific about which elements have issues +- Focused on user impact and readability +- Actionable with clear remediation steps`; + +export function buildDarkModePrompt(deterministicSummary: string): string { + return `Compare these two screenshots — the first is the Light mode and the second is the Dark mode of the same UI. +${deterministicSummary ? `\nDeterministic checks already found:\n${deterministicSummary}\n` : ''} +Evaluate the dark mode implementation across these 5 dimensions: + +## 1. Visibility +Check that ALL elements visible in light mode remain visible in dark mode. Look for: +- Text that disappears against dark backgrounds +- Icons or graphics that become invisible +- Borders or dividers that lose visibility +- Form fields that blend into the background + +## 2. Semantic Color Mapping +Verify that semantic colors translate correctly: +- Error states still read as errors (red tones) +- Success states still read as success (green tones) +- Warning states still read as warnings (amber/yellow tones) +- Info states still read as informational (blue tones) +- Primary/accent colors remain identifiable + +## 3. Elevation Hierarchy +Check that visual depth is maintained: +- Cards/surfaces should use lighter shades to convey elevation (not shadows) +- Modal overlays should be distinguishable from the base surface +- Nested containers should show clear boundaries + +## 4. Image & Illustration Adaptation +Check how images and illustrations handle dark mode: +- Do images have dark halos or harsh edges against dark backgrounds? +- Are illustrations adapted (dimmed, recolored, or have dark variants)? +- Do logos maintain legibility? + +## 5. Overall Assessment +Consider the holistic experience — does the dark mode feel intentional and polished, or does it feel like an automated inversion? + +Respond in this exact JSON format: +{ + "overallRating": "pass|needs_improvement|fail", + "visibilityIssues": [ + { "element": "", "description": "" } + ], + "semanticColorIssues": [ + { "element": "", "lightValue": "", "darkValue": "", "issue": "" } + ], + "elevationIssues": [""], + "imageAdaptation": "good|needs_attention|missing", + "recommendations": [ + { "title": "", "description": "", "severity": "critical|warning|info" } + ], + "summary": "<2-3 sentence overall assessment>" +}`; +} diff --git a/backend/src/prompts/nielsen-heuristics.ts b/backend/src/prompts/nielsen-heuristics.ts new file mode 100644 index 0000000..0bd940f --- /dev/null +++ b/backend/src/prompts/nielsen-heuristics.ts @@ -0,0 +1,112 @@ +/** + * Nielsen's Heuristics Evaluation prompt. + * Evaluates the 6 heuristics that can be assessed from static screenshots + * (skips H2 Real World Match, H7 Flexibility, H9 Error Recovery, H10 Help). + */ +export function buildNielsenHeuristicsPrompt(lintContext: string, flowContext?: string): string { + const flowBlock = flowContext + ? `\nFlow context (multiple screens in this flow):\n${flowContext}\n` + : ''; + + return `Evaluate this UI screenshot against Nielsen's 10 Usability Heuristics. Focus on the 6 heuristics that can be assessed visually. Skip H2 (Real World Match), H7 (Flexibility & Efficiency), H9 (Error Recovery), and H10 (Help & Documentation) — these require domain knowledge or interaction testing. + +Context from automated lint: +${lintContext} +${flowBlock} +Evaluate each heuristic below. For each one, provide a rating, 2-3 specific evidence items from the screenshot, and a recommendation if the rating is not "pass". + +## H1: Visibility of System Status +The system should always keep users informed about what is going on through appropriate feedback within reasonable time. +Look for: loading indicators, progress bars, active/selected states on navigation, current step indicators in multi-step flows, feedback after actions (success/error badges), real-time status updates. +PASS: Clear system status indicators present where needed. +NEEDS_IMPROVEMENT: Some status indicators present but gaps exist (e.g., no loading state, unclear active tab). +FAIL: No visible system status — user cannot tell what state the system is in. + +## H3: User Control & Freedom +Users often perform actions by mistake. They need a clearly marked "emergency exit" to leave the unwanted action. +Look for: back/close buttons on modals and overlays, undo affordances, cancel buttons alongside confirm, breadcrumbs for navigation history, clear exit paths from flows. +PASS: All modals/overlays have close buttons; destructive actions have cancel options; navigation provides back paths. +NEEDS_IMPROVEMENT: Most controls present but 1-2 exit paths missing (e.g., modal without close button, no cancel on form). +FAIL: Users appear trapped — no visible way to go back, close, or undo. + +## H4: Consistency & Standards +Users should not have to wonder whether different words, situations, or actions mean the same thing. +Look for: consistent button styles for same-level actions, consistent iconography, platform conventions followed (iOS/Android/Web), consistent terminology, consistent spacing and alignment patterns. +PASS: Visual language is consistent throughout; platform conventions followed. +NEEDS_IMPROVEMENT: Generally consistent but 1-2 deviations (mixed icon styles, inconsistent button hierarchy). +FAIL: Significant inconsistencies — mixed visual languages, contradictory conventions. + +## H5: Error Prevention +Even better than good error messages is a careful design which prevents a problem from occurring in the first place. +Look for: confirmation dialogs for destructive actions (delete, discard), input constraints (character counters, format hints), safe defaults (opt-out rather than opt-in for risky actions), disabled states for unavailable actions, inline validation hints. +PASS: Destructive actions have safeguards; inputs show constraints; defaults are safe. +NEEDS_IMPROVEMENT: Some error prevention present but gaps (e.g., delete without confirmation, no input hints). +FAIL: No error prevention visible — destructive actions lack confirmation, no input guidance. + +## H6: Recognition Rather Than Recall +Minimize the user's memory load by making objects, actions, and options visible or easily retrievable. +Look for: visible labels (not icon-only buttons without tooltips), breadcrumbs showing path, recently used items, visible options rather than hidden menus, search with suggestions, placeholder text that explains expected input. +PASS: All actions labeled; navigation context visible; options discoverable. +NEEDS_IMPROVEMENT: Most elements labeled but some icon-only buttons without clear meaning; some navigation context missing. +FAIL: Heavy reliance on recall — unlabeled icons, hidden options, no navigation context. + +## H8: Aesthetic & Minimalist Design +Every extra unit of information in an interface competes with relevant units of information and diminishes their relative visibility. +Look for: information density appropriate for the context, noise-to-signal ratio, visual clutter (unnecessary borders, shadows, decorations), content hierarchy that surfaces what matters, purposeful use of whitespace. +PASS: Clean design with only relevant information; clear content hierarchy; purposeful whitespace. +NEEDS_IMPROVEMENT: Mostly clean but some unnecessary elements or slightly cluttered areas. +FAIL: Cluttered — excessive decorations, too much information competing for attention, poor signal-to-noise ratio. + +Respond in this exact JSON format: +{ + "heuristics": [ + { + "id": "H1", + "name": "Visibility of System Status", + "rating": "pass|needs_improvement|fail", + "evidence": ["", ""], + "recommendation": "" + }, + { + "id": "H3", + "name": "User Control & Freedom", + "rating": "pass|needs_improvement|fail", + "evidence": ["", ""], + "recommendation": "" + }, + { + "id": "H4", + "name": "Consistency & Standards", + "rating": "pass|needs_improvement|fail", + "evidence": ["", ""], + "recommendation": "" + }, + { + "id": "H5", + "name": "Error Prevention", + "rating": "pass|needs_improvement|fail", + "evidence": ["", ""], + "recommendation": "" + }, + { + "id": "H6", + "name": "Recognition Rather Than Recall", + "rating": "pass|needs_improvement|fail", + "evidence": ["", ""], + "recommendation": "" + }, + { + "id": "H8", + "name": "Aesthetic & Minimalist Design", + "rating": "pass|needs_improvement|fail", + "evidence": ["", ""], + "recommendation": "" + } + ], + "overallCompliance": 0-100, + "criticalViolations": [ + { "heuristic": "", "description": "" } + ], + "summary": "<2-3 sentence summary of heuristic compliance>" +}`; +} diff --git a/backend/src/prompts/page-sweep.ts b/backend/src/prompts/page-sweep.ts new file mode 100644 index 0000000..a2d9610 --- /dev/null +++ b/backend/src/prompts/page-sweep.ts @@ -0,0 +1,33 @@ +/** + * Build the AI prompt for aggregating a whole-page sweep into a File Health Report. + */ +export function buildPageSweepPrompt( + frameSummaries: string, + aggregatedStats: string, +): string { + return `You are reviewing an entire Figma page consisting of multiple top-level frames. Each frame has been linted for design quality issues. Your task is to produce a holistic File Health Report. + +## Aggregated Statistics +${aggregatedStats} + +## Per-Frame Summaries +${frameSummaries} + +## Your Task +Based on the screenshots and lint data for ALL frames, provide a holistic assessment: + +1. **Strengths**: What does this file do well across frames? (consistent patterns, good practices, etc.) +2. **Weaknesses**: What recurring problems exist? (inconsistencies, common mistakes, etc.) +3. **Recommendations**: Actionable improvements that would have the biggest impact. For each, list which frames are affected. +4. **Summary**: A 2-3 sentence overall assessment. + +Respond in this exact JSON format: +{ + "strengths": ["", "", ""], + "weaknesses": ["", ""], + "recommendations": [ + { "title": "", "description": "", "affectedFrames": ["", ""] } + ], + "summary": "<2-3 sentence overall assessment>" +}`; +} diff --git a/backend/src/prompts/persona-research.ts b/backend/src/prompts/persona-research.ts new file mode 100644 index 0000000..48ee5d1 --- /dev/null +++ b/backend/src/prompts/persona-research.ts @@ -0,0 +1,76 @@ +/** + * Persona-Based Mock Research prompts. + * Five distinct user personas evaluate a design screenshot. + */ + +export interface PersonaPrompt { + name: string; + role: string; + systemPrompt: string; +} + +export const PERSONAS: PersonaPrompt[] = [ + { + name: 'Alex', + role: 'Novice User', + systemPrompt: + 'You are a first-time user who has never seen this product before. You have basic computer literacy but no domain expertise. You are easily confused by jargon, unclear navigation, and complex workflows. You tend to read every label carefully and feel anxious when you are unsure what will happen after clicking something. You prefer explicit instructions and confirmation of success.', + }, + { + name: 'Priya', + role: 'Expert Power User', + systemPrompt: + 'You are a daily power user of this type of product. You value efficiency, keyboard shortcuts, and advanced features. You are frustrated by unnecessary confirmations, hidden settings, and dumbed-down interfaces. You often try to skip onboarding, use bulk actions, and expect dense information layouts. You judge products by how fast you can accomplish frequent tasks.', + }, + { + name: 'Margaret', + role: 'Elderly User (65+)', + systemPrompt: + 'You are a 68-year-old user with reduced vision acuity and motor control. You need larger text, clear contrast, generous touch targets, and simple navigation. You prefer familiar patterns and are uncomfortable with gestures beyond tap and scroll. You read slowly and prefer step-by-step processes over multi-function screens. Small icons without labels frustrate you.', + }, + { + name: 'James', + role: 'Screen Reader User', + systemPrompt: + 'You are a blind user navigating with a screen reader (VoiceOver/NVDA). You need semantic HTML, logical heading hierarchy, descriptive alt text, clear focus management, and no reliance on visual cues alone. You navigate by headings, landmarks, and tab order. Unlabeled buttons, decorative images without alt="", and focus traps are your biggest barriers. You evaluate interfaces by their structural markup, not their visual appearance.', + }, + { + name: 'Yuki', + role: 'Non-Native English Speaker', + systemPrompt: + 'You are an intermediate English speaker from Japan. You need clear, simple language, and you struggle with idioms, slang, and cultural references specific to English-speaking countries. You prefer icons alongside text labels for disambiguation. Abbreviations and acronyms without expansion confuse you. You read more slowly than native speakers and rely heavily on visual context clues.', + }, +]; + +export function buildPersonaUserPrompt( + taskDescription: string, + lintContext?: string, +): string { + const lintBlock = lintContext + ? `\n## Automated Lint Context\n${lintContext}\n` + : ''; + + return `Look at this screenshot of a user interface. You are being asked to complete the following task: + +"${taskDescription}" +${lintBlock} +Based on your perspective and abilities, evaluate this design. Consider: +1. Can you understand what this screen is for? +2. Can you figure out how to start and complete the task? +3. What barriers or frustrations do you encounter? +4. What works well for someone like you? +5. What specific changes would help you? + +Rate the design from 1 (terrible - cannot use at all) to 5 (great - easy and pleasant to use). + +Respond in this exact JSON format: +{ + "rating": <1-5>, + "canCompleteTask": "yes|with_difficulty|no", + "barriers": ["", ""], + "positives": ["", ""], + "frustrations": ["", ""], + "suggestions": ["", ""], + "quote": "" +}`; +} diff --git a/backend/src/prompts/pure-scoring.ts b/backend/src/prompts/pure-scoring.ts new file mode 100644 index 0000000..87e0948 --- /dev/null +++ b/backend/src/prompts/pure-scoring.ts @@ -0,0 +1,109 @@ +/** + * PURE Multi-Evaluator scoring prompts. + * Three independent AI "experts" evaluate the same design from different perspectives. + */ + +export interface EvaluatorPrompt { + role: string; + systemPrompt: string; + buildUserPrompt: (taskDescription: string, lintContext?: string, extractedData?: string) => string; +} + +const SHARED_RESPONSE_FORMAT = ` +Rate the design on the PURE usability scale for the given task: +- 1 = Easy (user can complete without difficulty) +- 2 = Moderate (user can complete but with some friction) +- 3 = Difficult (user is likely to fail or abandon) + +Respond in this exact JSON format: +{ + "rating": 1|2|3, + "confidence": "high|medium|low", + "rationale": "<2-3 sentences explaining your rating>", + "strengths": ["", ""], + "issues": [ + { "description": "", "severity": "critical|warning|info" } + ] +}`; + +export const UX_DESIGNER: EvaluatorPrompt = { + role: 'UX Designer', + systemPrompt: + 'You are a senior UX designer with 15 years of experience. You evaluate designs for usability, learnability, and user satisfaction. You are particularly sensitive to navigation patterns, information architecture, and interaction design. You respond in JSON format when asked for structured output.', + buildUserPrompt(taskDescription: string, lintContext?: string, extractedData?: string): string { + return `Evaluate this UI design from a **UX design** perspective. + +## Task to Evaluate +"${taskDescription}" + +${lintContext ? `## Lint Context (for reference)\n${lintContext}\n` : ''} +${extractedData ? `## Extracted Design Data\n${extractedData}\n` : ''} + +## Evaluation Criteria (UX Designer) +Focus on: +- **Learnability**: Can a first-time user figure out how to complete the task? +- **Navigation clarity**: Is it obvious where to go and what to do next? +- **Information architecture**: Is content organized logically for this task? +- **Interaction patterns**: Are buttons, links, and controls where users expect them? +- **Error prevention**: Does the design help users avoid mistakes? +- **Feedback**: Does the interface communicate state changes clearly? +${SHARED_RESPONSE_FORMAT}`; + }, +}; + +export const ACCESSIBILITY_SPECIALIST: EvaluatorPrompt = { + role: 'Accessibility Specialist', + systemPrompt: + 'You are a WCAG expert and accessibility consultant. You evaluate designs for inclusive design, assistive technology compatibility, cognitive accessibility, and universal usability. You are particularly sensitive to contrast, text size, touch targets, focus management, and color-only information. You respond in JSON format when asked for structured output.', + buildUserPrompt(taskDescription: string, lintContext?: string, extractedData?: string): string { + return `Evaluate this UI design from an **accessibility** perspective. + +## Task to Evaluate +"${taskDescription}" + +${lintContext ? `## Lint Context (for reference)\n${lintContext}\n` : ''} +${extractedData ? `## Extracted Design Data\n${extractedData}\n` : ''} + +## Evaluation Criteria (Accessibility Specialist) +Focus on: +- **Visual accessibility**: Sufficient contrast ratios (4.5:1 for text, 3:1 for UI components), readable text sizes (minimum 12px) +- **Motor accessibility**: Touch/click targets at least 44x44px, adequate spacing between interactive elements +- **Cognitive accessibility**: Clear labels, simple language, predictable patterns, not too many choices at once +- **Color independence**: Information is not conveyed by color alone; icons, text, or patterns supplement color +- **Focus and reading order**: Logical tab order implied by visual layout, clear focus indicators +- **Alternative text potential**: Images and icons appear to have meaningful labels; decorative vs informative distinction +${SHARED_RESPONSE_FORMAT}`; + }, +}; + +export const BUSINESS_ANALYST: EvaluatorPrompt = { + role: 'Business Analyst', + systemPrompt: + 'You are a product strategist focused on conversion optimization. You evaluate designs for business goal achievement, conversion funnel efficiency, trust signals, and value proposition clarity. You are particularly sensitive to CTA placement, friction points, and persuasive design patterns. You respond in JSON format when asked for structured output.', + buildUserPrompt(taskDescription: string, lintContext?: string, extractedData?: string): string { + return `Evaluate this UI design from a **business/conversion** perspective. + +## Task to Evaluate +"${taskDescription}" + +${lintContext ? `## Lint Context (for reference)\n${lintContext}\n` : ''} +${extractedData ? `## Extracted Design Data\n${extractedData}\n` : ''} + +## Evaluation Criteria (Business Analyst) +Focus on: +- **CTA effectiveness**: Is the primary call-to-action prominent, compelling, and clearly labeled? +- **Friction reduction**: Are there unnecessary steps, fields, or decisions that could cause drop-off? +- **Trust signals**: Does the design convey credibility (security indicators, social proof, professional polish)? +- **Value proposition**: Is it clear what the user gets and why they should complete this task? +- **Urgency and motivation**: Does the design create appropriate motivation without dark patterns? +- **Funnel clarity**: Is there a single clear path to task completion, or are there distracting detours? +${SHARED_RESPONSE_FORMAT}`; + }, +}; + +/** All three evaluators in order. */ +export const EVALUATORS: EvaluatorPrompt[] = [ + UX_DESIGNER, + ACCESSIBILITY_SPECIALIST, + BUSINESS_ANALYST, +]; diff --git a/backend/src/prompts/responsive.ts b/backend/src/prompts/responsive.ts new file mode 100644 index 0000000..c80cad4 --- /dev/null +++ b/backend/src/prompts/responsive.ts @@ -0,0 +1,64 @@ +/** + * Build the AI prompt for responsive design comparison. + * Sent alongside screenshots of detected breakpoint variant frames. + */ +export function buildResponsiveComparisonPrompt( + variantLabels: string[], + lintSummary: string, +): string { + return `You are a responsive design expert reviewing a UI that has been designed at multiple breakpoints. + +## Breakpoint Variants Detected +${variantLabels.map((label, i) => ` ${i + 1}. ${label}`).join('\n')} + +## Lint Summary +${lintSummary} + +## Your Task +Compare the breakpoint variants shown in the screenshots and verify: + +1. **Content Consistency**: Is the same content present across all breakpoints? Flag any content that appears in one breakpoint but is missing in another. +2. **Appropriate Adaptations**: Do the layouts adapt correctly for each breakpoint? + - Does the navigation collapse to a hamburger menu on mobile? + - Do multi-column grids stack to a single column on narrow screens? + - Do images resize or reflow appropriately? +3. **Text Readability**: Is text readable at all sizes? Check for: + - Font sizes that are too small on mobile (below 14px) + - Line lengths that are too long on desktop (over 75 characters) + - Adequate line height at all sizes +4. **Touch Targets**: On mobile breakpoints, are interactive elements at least 44x44px for adequate touch targets? +5. **Spacing Consistency**: Is the spacing system maintained across breakpoints? Are there abrupt spacing changes? + +Respond in this exact JSON format: +{ + "contentConsistency": { + "rating": "pass|needs_improvement|fail", + "missingContent": [{ "breakpoint": "", "description": "" }], + "evidence": [""] + }, + "layoutAdaptation": { + "rating": "pass|needs_improvement|fail", + "issues": [{ "breakpoint": "", "description": "" }], + "evidence": [""] + }, + "textReadability": { + "rating": "pass|needs_improvement|fail", + "issues": [{ "breakpoint": "", "description": "" }], + "evidence": [""] + }, + "touchTargets": { + "rating": "pass|needs_improvement|fail", + "issues": [{ "breakpoint": "", "element": "", "description": "" }], + "evidence": [""] + }, + "spacingConsistency": { + "rating": "pass|needs_improvement|fail", + "issues": [{ "description": "" }], + "evidence": [""] + }, + "recommendations": [ + { "title": "", "description": "<description>", "severity": "critical|warning|info", "breakpoints": ["<affected>"] } + ], + "summary": "<3-5 sentence overall assessment>" +}`; +} diff --git a/backend/src/routes/a11y-spec.ts b/backend/src/routes/a11y-spec.ts new file mode 100644 index 0000000..133cee5 --- /dev/null +++ b/backend/src/routes/a11y-spec.ts @@ -0,0 +1,98 @@ +import { Hono } from 'hono'; +import { + generateA11ySpec, + type A11ySpecRequest, +} from '../services/a11y-spec-generator.js'; + +const app = new Hono(); + +app.post('/generate-a11y-spec', async (c) => { + try { + let body: A11ySpecRequest; + try { + body = await c.req.json<A11ySpecRequest>(); + } catch { + return c.json({ error: 'Invalid JSON body' }, 400); + } + + if (!body || typeof body !== 'object') { + return c.json({ error: 'Request body must be a JSON object' }, 400); + } + + // Validate screenshot + if (!body.screenshot || typeof body.screenshot !== 'string') { + return c.json( + { error: 'screenshot is required and must be a base64 string' }, + 400, + ); + } + + // Validate extractedData + if (!body.extractedData || typeof body.extractedData !== 'object') { + return c.json( + { error: 'extractedData is required and must be an object' }, + 400, + ); + } + + if ( + !body.extractedData.componentName || + typeof body.extractedData.componentName !== 'string' + ) { + return c.json( + { error: 'extractedData.componentName is required' }, + 400, + ); + } + + // Validate lintResult + if (!body.lintResult || typeof body.lintResult !== 'object') { + return c.json( + { error: 'lintResult is required and must be an object' }, + 400, + ); + } + + if (!body.lintResult.summary || typeof body.lintResult.summary !== 'object') { + return c.json( + { error: 'lintResult.summary is required' }, + 400, + ); + } + + if (!Array.isArray(body.lintResult.errors)) { + return c.json( + { error: 'lintResult.errors must be an array' }, + 400, + ); + } + + if (!process.env.ANTHROPIC_API_KEY) { + return c.json( + { error: 'AI analysis unavailable — ANTHROPIC_API_KEY not configured' }, + 503, + ); + } + + const sessionId = body.sessionId || 'anonymous'; + const spec = await generateA11ySpec( + body.screenshot, + body.extractedData, + body.lintResult, + sessionId, + ); + + return c.json({ success: true, spec }); + } catch (error) { + console.error( + 'A11y spec generation error:', + error instanceof Error ? error.message : 'Unknown', + ); + return c.json( + { error: 'Accessibility spec generation failed. Please try again.' }, + 500, + ); + } +}); + +export default app; diff --git a/backend/src/routes/analyze.ts b/backend/src/routes/analyze.ts index a4eb9f4..c8a8160 100644 --- a/backend/src/routes/analyze.ts +++ b/backend/src/routes/analyze.ts @@ -1,13 +1,22 @@ import { Hono } from 'hono'; import { runAnalysis, type AnalyzeRequest } from '../services/analyzer.js'; +import { + runExtendedAnalysis, + type ExtendedAnalysisResult, + type ExtendedFeatures, +} from '../services/extended-analyzer.js'; + +interface AnalyzeRequestBody extends AnalyzeRequest { + features?: ExtendedFeatures; +} const app = new Hono(); app.post('/analyze', async (c) => { try { - let body: AnalyzeRequest; + let body: AnalyzeRequestBody; try { - body = await c.req.json<AnalyzeRequest>(); + body = await c.req.json<AnalyzeRequestBody>(); } catch { return c.json({ error: 'Invalid JSON body' }, 400); } @@ -28,8 +37,33 @@ app.post('/analyze', async (c) => { return c.json({ error: 'extractedData.componentName is required' }, 400); } - const result = await runAnalysis(body); - return c.json(result); + // Determine which extended features are requested + const features: ExtendedFeatures = { + attention: body.features?.attention === true, + nielsen: body.features?.nielsen === true, + }; + const hasExtended = features.attention || features.nielsen; + + // Run core analysis and extended analysis in parallel + const [coreResult, extendedResult] = await Promise.all([ + runAnalysis(body), + hasExtended + ? runExtendedAnalysis( + body.screenshot, + body.lintResult, + body.extractedData, + body.sessionId ?? '', + features, + ) + : Promise.resolve(undefined as ExtendedAnalysisResult | undefined), + ]); + + // Merge extended results into the response + return c.json({ + ...coreResult, + ...(extendedResult?.attention && { attention: extendedResult.attention }), + ...(extendedResult?.nielsen && { nielsen: extendedResult.nielsen }), + }); } catch (error) { console.error('Analysis error:', error); return c.json({ error: 'Analysis failed. Please try again.' }, 500); diff --git a/backend/src/routes/brand-consistency.ts b/backend/src/routes/brand-consistency.ts new file mode 100644 index 0000000..bfb023a --- /dev/null +++ b/backend/src/routes/brand-consistency.ts @@ -0,0 +1,146 @@ +import { Hono } from 'hono'; +import { + analyzeBrandConsistency, + type BrandConsistencyResult, +} from '../services/brand-consistency.js'; +import type { BrandGuide } from '../prompts/brand-consistency.js'; + +interface BrandConsistencyRequestBody { + screenshot: string; + brandGuide: BrandGuide; + lintResult?: unknown; + sessionId?: string; +} + +const app = new Hono(); + +app.post('/brand-consistency', async (c) => { + try { + let body: BrandConsistencyRequestBody; + try { + body = await c.req.json<BrandConsistencyRequestBody>(); + } catch { + return c.json({ error: 'Invalid JSON body' }, 400); + } + + if (!body || typeof body !== 'object') { + return c.json({ error: 'Request body must be a JSON object' }, 400); + } + + // Validate screenshot + if (!body.screenshot || typeof body.screenshot !== 'string') { + return c.json( + { error: 'screenshot is required and must be a base64 string' }, + 400, + ); + } + + // Validate brandGuide + if (!body.brandGuide || typeof body.brandGuide !== 'object') { + return c.json( + { error: 'brandGuide is required and must be an object' }, + 400, + ); + } + + const bg = body.brandGuide; + + // Validate brandGuide.colors + if (!bg.colors || typeof bg.colors !== 'object') { + return c.json( + { error: 'brandGuide.colors is required and must be an object' }, + 400, + ); + } + + // Validate brandGuide.typography + if (!bg.typography || typeof bg.typography !== 'object') { + return c.json( + { error: 'brandGuide.typography is required and must be an object' }, + 400, + ); + } + if ( + !bg.typography.heading || + typeof bg.typography.heading.family !== 'string' || + !Array.isArray(bg.typography.heading.weights) + ) { + return c.json( + { + error: + 'brandGuide.typography.heading must have family (string) and weights (number[])', + }, + 400, + ); + } + if ( + !bg.typography.body || + typeof bg.typography.body.family !== 'string' || + !Array.isArray(bg.typography.body.weights) + ) { + return c.json( + { + error: + 'brandGuide.typography.body must have family (string) and weights (number[])', + }, + 400, + ); + } + + // Validate brandGuide.spacing + if ( + !bg.spacing || + typeof bg.spacing.base !== 'number' || + !Array.isArray(bg.spacing.scale) + ) { + return c.json( + { + error: + 'brandGuide.spacing must have base (number) and scale (number[])', + }, + 400, + ); + } + + // Validate brandGuide.personality + if (!Array.isArray(bg.personality) || bg.personality.length === 0) { + return c.json( + { + error: + 'brandGuide.personality is required and must be a non-empty array of strings', + }, + 400, + ); + } + + if (!process.env.ANTHROPIC_API_KEY) { + return c.json( + { + error: + 'AI analysis unavailable — ANTHROPIC_API_KEY not configured', + }, + 503, + ); + } + + const result: BrandConsistencyResult = await analyzeBrandConsistency( + body.screenshot, + body.brandGuide, + body.lintResult ?? null, + body.sessionId ?? '', + ); + + return c.json({ success: true, brandConsistency: result }); + } catch (error) { + console.error( + 'Brand consistency error:', + error instanceof Error ? error.message : 'Unknown', + ); + return c.json( + { error: 'Brand consistency analysis failed. Please try again.' }, + 500, + ); + } +}); + +export default app; diff --git a/backend/src/routes/cognitive-walkthrough.ts b/backend/src/routes/cognitive-walkthrough.ts new file mode 100644 index 0000000..9226579 --- /dev/null +++ b/backend/src/routes/cognitive-walkthrough.ts @@ -0,0 +1,123 @@ +import { Hono } from 'hono'; +import { + runCognitiveWalkthrough, + type CognitiveWalkthroughRequest, +} from '../services/cognitive-walkthrough.js'; + +const app = new Hono(); + +app.post('/cognitive-walkthrough', async (c) => { + try { + let body: CognitiveWalkthroughRequest; + try { + body = await c.req.json<CognitiveWalkthroughRequest>(); + } catch { + return c.json({ error: 'Invalid JSON body' }, 400); + } + + if (!body || typeof body !== 'object') { + return c.json({ error: 'Request body must be a JSON object' }, 400); + } + + // Validate taskDescription + if (!body.taskDescription || typeof body.taskDescription !== 'string') { + return c.json( + { error: 'taskDescription is required and must be a string' }, + 400, + ); + } + + // Validate frames + if (!Array.isArray(body.frames) || body.frames.length === 0) { + return c.json( + { error: 'frames is required and must be a non-empty array' }, + 400, + ); + } + + if (body.frames.length > 10) { + return c.json( + { error: 'Maximum 10 frames per request' }, + 400, + ); + } + + for (let i = 0; i < body.frames.length; i++) { + const frame = body.frames[i]; + if (!frame.id || typeof frame.id !== 'string') { + return c.json({ error: `frames[${i}].id is required` }, 400); + } + if (!frame.name || typeof frame.name !== 'string') { + return c.json({ error: `frames[${i}].name is required` }, 400); + } + if (!frame.screenshot || typeof frame.screenshot !== 'string') { + return c.json({ error: `frames[${i}].screenshot is required` }, 400); + } + } + + // Validate edges + if (!Array.isArray(body.edges)) { + return c.json({ error: 'edges must be an array' }, 400); + } + + for (let i = 0; i < body.edges.length; i++) { + const edge = body.edges[i]; + if (!edge.sourceFrameId || typeof edge.sourceFrameId !== 'string') { + return c.json( + { error: `edges[${i}].sourceFrameId is required` }, + 400, + ); + } + if ( + !edge.destinationFrameId || + typeof edge.destinationFrameId !== 'string' + ) { + return c.json( + { error: `edges[${i}].destinationFrameId is required` }, + 400, + ); + } + if (!edge.trigger || typeof edge.trigger !== 'string') { + return c.json({ error: `edges[${i}].trigger is required` }, 400); + } + } + + // Validate interactiveElements (optional but must be correct shape if present) + if ( + body.interactiveElements !== undefined && + body.interactiveElements !== null && + typeof body.interactiveElements !== 'object' + ) { + return c.json( + { error: 'interactiveElements must be an object if provided' }, + 400, + ); + } + + // Default interactiveElements to empty object if not provided + if (!body.interactiveElements) { + body.interactiveElements = {}; + } + + if (!process.env.ANTHROPIC_API_KEY) { + return c.json( + { error: 'AI analysis unavailable — ANTHROPIC_API_KEY not configured' }, + 503, + ); + } + + const result = await runCognitiveWalkthrough(body); + return c.json({ success: true, walkthrough: result }); + } catch (error) { + console.error( + 'Cognitive walkthrough error:', + error instanceof Error ? error.message : 'Unknown', + ); + return c.json( + { error: 'Cognitive walkthrough failed. Please try again.' }, + 500, + ); + } +}); + +export default app; diff --git a/backend/src/routes/copy-tone.ts b/backend/src/routes/copy-tone.ts new file mode 100644 index 0000000..e510970 --- /dev/null +++ b/backend/src/routes/copy-tone.ts @@ -0,0 +1,118 @@ +import { Hono } from 'hono'; +import { analyzeCopyTone } from '../services/copy-tone.js'; + +interface CopyToneRequestBody { + screens: Array<{ name: string; textContent: string[] }>; + personality?: string[]; + sessionId?: string; +} + +const app = new Hono(); + +app.post('/copy-tone', async (c) => { + try { + let body: CopyToneRequestBody; + try { + body = await c.req.json<CopyToneRequestBody>(); + } catch { + return c.json({ error: 'Invalid JSON body' }, 400); + } + + if (!body || typeof body !== 'object') { + return c.json({ error: 'Request body must be a JSON object' }, 400); + } + + // Validate screens + if (!Array.isArray(body.screens) || body.screens.length === 0) { + return c.json( + { error: 'screens is required and must be a non-empty array' }, + 400, + ); + } + + if (body.screens.length > 20) { + return c.json( + { error: 'Maximum 20 screens per request' }, + 400, + ); + } + + for (let i = 0; i < body.screens.length; i++) { + const screen = body.screens[i]; + if (!screen.name || typeof screen.name !== 'string') { + return c.json( + { error: `screens[${i}].name is required and must be a string` }, + 400, + ); + } + if ( + !Array.isArray(screen.textContent) || + screen.textContent.length === 0 + ) { + return c.json( + { + error: `screens[${i}].textContent is required and must be a non-empty array of strings`, + }, + 400, + ); + } + for (let j = 0; j < screen.textContent.length; j++) { + if (typeof screen.textContent[j] !== 'string') { + return c.json( + { + error: `screens[${i}].textContent[${j}] must be a string`, + }, + 400, + ); + } + } + } + + // Validate personality (optional) + if (body.personality !== undefined && body.personality !== null) { + if (!Array.isArray(body.personality)) { + return c.json( + { error: 'personality must be an array of strings if provided' }, + 400, + ); + } + for (let i = 0; i < body.personality.length; i++) { + if (typeof body.personality[i] !== 'string') { + return c.json( + { error: `personality[${i}] must be a string` }, + 400, + ); + } + } + } + + if (!process.env.ANTHROPIC_API_KEY) { + return c.json( + { + error: + 'AI analysis unavailable — ANTHROPIC_API_KEY not configured', + }, + 503, + ); + } + + const result = await analyzeCopyTone( + body.screens, + body.personality, + body.sessionId, + ); + + return c.json({ success: true, copyTone: result }); + } catch (error) { + console.error( + 'Copy tone error:', + error instanceof Error ? error.message : 'Unknown', + ); + return c.json( + { error: 'Copy tone analysis failed. Please try again.' }, + 500, + ); + } +}); + +export default app; diff --git a/backend/src/routes/dark-mode.ts b/backend/src/routes/dark-mode.ts new file mode 100644 index 0000000..5e27270 --- /dev/null +++ b/backend/src/routes/dark-mode.ts @@ -0,0 +1,193 @@ +import { Hono } from 'hono'; +import { getAnthropicClient, MODEL } from '../services/claude.js'; +import { DARK_MODE_SYSTEM_PROMPT, buildDarkModePrompt } from '../prompts/dark-mode.js'; + +interface ModeComparisonData { + collection: string; + modes: Array<{ + modeId: string; + modeName: string; + screenshot?: string; + }>; + variableDiffs: Array<{ + variableName: string; + type: string; + values: Record<string, unknown>; + }>; + missingValues: Array<{ + variableName: string; + missingModes: string[]; + }>; +} + +interface DarkModeIssue { + id: string; + type: string; + severity: string; + nodeId: string; + nodeName: string; + message: string; + currentValue?: string; + suggestions?: string[]; + autoFixable: boolean; +} + +interface DarkModeResult { + issues: DarkModeIssue[]; + metrics: { + pureBlackBackgrounds: number; + pureWhiteText: number; + lowContrastOnDark: number; + missingModeValues: number; + }; + summary: { totalChecked: number; passed: number; failed: number }; +} + +interface DarkModeRequest { + lightScreenshot: string; + darkScreenshot: string; + modeData: ModeComparisonData; + deterministicIssues?: DarkModeResult; + sessionId?: string; +} + +interface AiComparisonResult { + overallRating: 'pass' | 'needs_improvement' | 'fail'; + visibilityIssues: Array<{ element: string; description: string }>; + semanticColorIssues: Array<{ element: string; lightValue: string; darkValue: string; issue: string }>; + elevationIssues: string[]; + imageAdaptation: 'good' | 'needs_attention' | 'missing'; + recommendations: Array<{ title: string; description: string; severity: string }>; + summary: string; +} + +const app = new Hono(); + +app.post('/validate-dark-mode', async (c) => { + try { + let body: DarkModeRequest; + try { + body = await c.req.json<DarkModeRequest>(); + } catch { + return c.json({ error: 'Invalid JSON body' }, 400); + } + + if (!body || typeof body !== 'object') { + return c.json({ error: 'Request body must be a JSON object' }, 400); + } + if (!body.lightScreenshot || typeof body.lightScreenshot !== 'string') { + return c.json({ error: 'lightScreenshot is required and must be a base64 string' }, 400); + } + if (!body.darkScreenshot || typeof body.darkScreenshot !== 'string') { + return c.json({ error: 'darkScreenshot is required and must be a base64 string' }, 400); + } + if (!body.modeData || typeof body.modeData !== 'object') { + return c.json({ error: 'modeData is required' }, 400); + } + + // Build deterministic summary for prompt context + const deterministicSummary = body.deterministicIssues + ? buildDeterministicSummary(body.deterministicIssues) + : ''; + + // Call Claude with both screenshots for side-by-side comparison + const anthropic = getAnthropicClient(); + const prompt = buildDarkModePrompt(deterministicSummary); + + const response = await anthropic.messages.create({ + model: MODEL, + max_tokens: 3000, + system: DARK_MODE_SYSTEM_PROMPT, + messages: [ + { + role: 'user', + content: [ + { + type: 'image', + source: { type: 'base64', media_type: 'image/png', data: body.lightScreenshot }, + }, + { + type: 'image', + source: { type: 'base64', media_type: 'image/png', data: body.darkScreenshot }, + }, + { type: 'text', text: prompt }, + ], + }, + ], + }); + + if (!response.content.length || response.content[0].type !== 'text') { + return c.json({ error: 'Empty response from AI' }, 500); + } + + const text = response.content[0].text; + const jsonMatch = text.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + return c.json({ error: 'No JSON in AI response' }, 500); + } + + const parsed = JSON.parse(jsonMatch[0]) as AiComparisonResult; + + // Normalize the AI response + const comparison: AiComparisonResult = { + overallRating: normalizeRating(parsed.overallRating), + visibilityIssues: Array.isArray(parsed.visibilityIssues) ? parsed.visibilityIssues : [], + semanticColorIssues: Array.isArray(parsed.semanticColorIssues) ? parsed.semanticColorIssues : [], + elevationIssues: Array.isArray(parsed.elevationIssues) ? parsed.elevationIssues : [], + imageAdaptation: normalizeAdaptation(parsed.imageAdaptation), + recommendations: Array.isArray(parsed.recommendations) ? parsed.recommendations : [], + summary: typeof parsed.summary === 'string' ? parsed.summary : '', + }; + + return c.json({ + comparison, + deterministicIssues: body.deterministicIssues || null, + }); + } catch (error) { + console.error('Dark mode validation error:', error); + return c.json({ error: 'Dark mode validation failed. Please try again.' }, 500); + } +}); + +export default app; + +// ────────────────────────────────────────────── +// Helpers +// ────────────────────────────────────────────── + +const VALID_RATINGS = new Set(['pass', 'needs_improvement', 'fail']); +function normalizeRating(value: unknown): 'pass' | 'needs_improvement' | 'fail' { + return typeof value === 'string' && VALID_RATINGS.has(value) + ? value as 'pass' | 'needs_improvement' | 'fail' + : 'fail'; +} + +const VALID_ADAPTATIONS = new Set(['good', 'needs_attention', 'missing']); +function normalizeAdaptation(value: unknown): 'good' | 'needs_attention' | 'missing' { + return typeof value === 'string' && VALID_ADAPTATIONS.has(value) + ? value as 'good' | 'needs_attention' | 'missing' + : 'missing'; +} + +function buildDeterministicSummary(result: DarkModeResult): string { + const m = result.metrics; + const lines: string[] = []; + + if (m.pureBlackBackgrounds > 0) { + lines.push(`- ${m.pureBlackBackgrounds} pure black (#000000) background variable(s)`); + } + if (m.pureWhiteText > 0) { + lines.push(`- ${m.pureWhiteText} pure white (#FFFFFF) text variable(s)`); + } + if (m.lowContrastOnDark > 0) { + lines.push(`- ${m.lowContrastOnDark} variable(s) with identical light/dark values`); + } + if (m.missingModeValues > 0) { + lines.push(`- ${m.missingModeValues} variable(s) missing dark mode values`); + } + + if (lines.length === 0) { + return 'Deterministic checks found no issues.'; + } + return `Deterministic analysis found ${result.issues.length} issue(s):\n${lines.join('\n')}`; +} diff --git a/backend/src/routes/page-sweep.ts b/backend/src/routes/page-sweep.ts new file mode 100644 index 0000000..d2cbf79 --- /dev/null +++ b/backend/src/routes/page-sweep.ts @@ -0,0 +1,27 @@ +import { Hono } from 'hono'; +import { analyzePageSweep, type PageSweepRequest } from '../services/page-sweep-analyzer.js'; + +const app = new Hono(); + +app.post('/analyze-page', async (c) => { + try { + let body: PageSweepRequest; + try { + body = await c.req.json<PageSweepRequest>(); + } catch { + return c.json({ error: 'Invalid JSON body' }, 400); + } + + if (!body.frames || !Array.isArray(body.frames) || body.frames.length === 0) { + return c.json({ error: 'Missing frames array' }, 400); + } + + const result = await analyzePageSweep(body); + return c.json({ success: true, ...result }); + } catch (error) { + console.error('Page sweep error:', error instanceof Error ? error.message : 'Unknown'); + return c.json({ error: 'Page sweep analysis failed. Please try again.' }, 500); + } +}); + +export default app; diff --git a/backend/src/routes/persona-research.ts b/backend/src/routes/persona-research.ts new file mode 100644 index 0000000..ec9812d --- /dev/null +++ b/backend/src/routes/persona-research.ts @@ -0,0 +1,84 @@ +import { Hono } from 'hono'; +import { runPersonaResearch } from '../services/persona-research.js'; + +interface PersonaResearchRequestBody { + screenshot: string; + taskDescription: string; + lintContext?: string; + sessionId?: string; +} + +const app = new Hono(); + +app.post('/persona-research', async (c) => { + try { + let body: PersonaResearchRequestBody; + try { + body = await c.req.json<PersonaResearchRequestBody>(); + } catch { + return c.json({ error: 'Invalid JSON body' }, 400); + } + + if (!body || typeof body !== 'object') { + return c.json({ error: 'Request body must be a JSON object' }, 400); + } + + // Validate screenshot + if (!body.screenshot || typeof body.screenshot !== 'string') { + return c.json( + { error: 'screenshot is required and must be a base64 string' }, + 400, + ); + } + + // Validate taskDescription + if (!body.taskDescription || typeof body.taskDescription !== 'string') { + return c.json( + { error: 'taskDescription is required and must be a string' }, + 400, + ); + } + + // Validate lintContext (optional) + if ( + body.lintContext !== undefined && + body.lintContext !== null && + typeof body.lintContext !== 'string' + ) { + return c.json( + { error: 'lintContext must be a string if provided' }, + 400, + ); + } + + if (!process.env.ANTHROPIC_API_KEY) { + return c.json( + { + error: + 'AI analysis unavailable — ANTHROPIC_API_KEY not configured', + }, + 503, + ); + } + + const result = await runPersonaResearch( + body.screenshot, + body.taskDescription, + body.lintContext, + body.sessionId, + ); + + return c.json({ success: true, personaResearch: result }); + } catch (error) { + console.error( + 'Persona research error:', + error instanceof Error ? error.message : 'Unknown', + ); + return c.json( + { error: 'Persona research failed. Please try again.' }, + 500, + ); + } +}); + +export default app; diff --git a/backend/src/routes/pure-scoring.ts b/backend/src/routes/pure-scoring.ts new file mode 100644 index 0000000..4dec612 --- /dev/null +++ b/backend/src/routes/pure-scoring.ts @@ -0,0 +1,75 @@ +import { Hono } from 'hono'; +import { runPureScoring, type PureRequest } from '../services/pure-scoring.js'; + +const app = new Hono(); + +app.post('/pure-scoring', async (c) => { + try { + let body: PureRequest; + try { + body = await c.req.json<PureRequest>(); + } catch { + return c.json({ error: 'Invalid JSON body' }, 400); + } + + if (!body || typeof body !== 'object') { + return c.json({ error: 'Request body must be a JSON object' }, 400); + } + + // Validate screenshot + if (!body.screenshot || typeof body.screenshot !== 'string') { + return c.json( + { error: 'screenshot is required and must be a string' }, + 400, + ); + } + + // Validate taskDescription + if (!body.taskDescription || typeof body.taskDescription !== 'string') { + return c.json( + { error: 'taskDescription is required and must be a string' }, + 400, + ); + } + + // Validate optional fields + if (body.lintContext !== undefined && typeof body.lintContext !== 'string') { + return c.json( + { error: 'lintContext must be a string if provided' }, + 400, + ); + } + + if ( + body.extractedData !== undefined && + body.extractedData !== null && + typeof body.extractedData !== 'object' + ) { + return c.json( + { error: 'extractedData must be an object if provided' }, + 400, + ); + } + + if (!process.env.ANTHROPIC_API_KEY) { + return c.json( + { error: 'AI analysis unavailable — ANTHROPIC_API_KEY not configured' }, + 503, + ); + } + + const result = await runPureScoring(body); + return c.json({ success: true, pureScoring: result }); + } catch (error) { + console.error( + 'PURE scoring error:', + error instanceof Error ? error.message : 'Unknown', + ); + return c.json( + { error: 'PURE scoring failed. Please try again.' }, + 500, + ); + } +}); + +export default app; diff --git a/backend/src/routes/responsive.ts b/backend/src/routes/responsive.ts new file mode 100644 index 0000000..099221c --- /dev/null +++ b/backend/src/routes/responsive.ts @@ -0,0 +1,70 @@ +import { Hono } from 'hono'; +import { validateResponsiveDesign, type ResponsiveValidationRequest } from '../services/responsive-validator.js'; + +const app = new Hono(); + +app.post('/validate-responsive', async (c) => { + try { + let body: ResponsiveValidationRequest; + try { + body = await c.req.json<ResponsiveValidationRequest>(); + } catch { + return c.json({ error: 'Invalid JSON body' }, 400); + } + + if (!body || typeof body !== 'object') { + return c.json({ error: 'Request body must be a JSON object' }, 400); + } + + // Validate variants + if (!Array.isArray(body.variants) || body.variants.length === 0) { + return c.json( + { error: 'variants is required and must be a non-empty array' }, + 400, + ); + } + + if (body.variants.length > 6) { + return c.json( + { error: 'Maximum 6 breakpoint variants per request' }, + 400, + ); + } + + for (let i = 0; i < body.variants.length; i++) { + const variant = body.variants[i]; + if (!variant.name || typeof variant.name !== 'string') { + return c.json({ error: `variants[${i}].name is required` }, 400); + } + if (!variant.screenshot || typeof variant.screenshot !== 'string') { + return c.json({ error: `variants[${i}].screenshot is required` }, 400); + } + } + + // Validate lintSummary (optional) + if (body.lintSummary !== undefined && typeof body.lintSummary !== 'string') { + return c.json({ error: 'lintSummary must be a string if provided' }, 400); + } + + if (!process.env.ANTHROPIC_API_KEY) { + return c.json( + { error: 'AI analysis unavailable — ANTHROPIC_API_KEY not configured' }, + 503, + ); + } + + const result = await validateResponsiveDesign(body); + return c.json({ success: true, responsive: result }); + } catch (error) { + console.error( + 'Responsive validation error:', + error instanceof Error ? error.message : 'Unknown', + ); + return c.json( + { error: 'Responsive validation failed. Please try again.' }, + 500, + ); + } +}); + +export default app; diff --git a/backend/src/services/a11y-spec-generator.ts b/backend/src/services/a11y-spec-generator.ts new file mode 100644 index 0000000..43768c7 --- /dev/null +++ b/backend/src/services/a11y-spec-generator.ts @@ -0,0 +1,227 @@ +import Anthropic from '@anthropic-ai/sdk'; +import { buildA11ySpecPrompt } from '../prompts/a11y-spec.js'; +import { getAnthropicClient, MODEL } from './claude.js'; + +// ── Types ────────────────────────────────────── + +export interface A11ySpec { + landmarks: Array<{ role: string; label: string; element: string }>; + headingStructure: Array<{ level: number; text: string; element: string }>; + focusOrder: Array<{ order: number; element: string; type: string; notes: string }>; + ariaAnnotations: Array<{ + element: string; + role: string; + ariaLabel?: string; + ariaDescribedBy?: string; + ariaLive?: string; + notes: string; + }>; + keyboardShortcuts: Array<{ key: string; action: string; element: string }>; + liveRegions: Array<{ element: string; type: 'polite' | 'assertive'; trigger: string }>; + colorContrastReport: Array<{ + element: string; + foreground: string; + background: string; + ratio: number; + passes: 'AA' | 'AAA' | 'fail'; + }>; + recommendations: Array<{ + title: string; + description: string; + wcagCriterion: string; + level: 'A' | 'AA' | 'AAA'; + }>; +} + +export interface A11ySpecRequest { + screenshot: string; + extractedData: { + componentName: string; + componentDescription?: string; + properties?: Array<{ name: string; type: string }>; + states?: string[]; + metadata?: { + nodeId: string; + nodeType: string; + width: number; + height: number; + hasAutoLayout: boolean; + childCount: number; + }; + }; + lintResult: { + summary: { + totalErrors: number; + byType: Record<string, number>; + totalNodes: number; + nodesWithErrors: number; + }; + errors: Array<{ + nodeId: string; + nodeName: string; + errorType: string; + message: string; + value: string; + }>; + }; + sessionId?: string; +} + +// ── Validation helpers ────────────────────────── + +const VALID_CONTRAST = new Set(['AA', 'AAA', 'fail']); +const VALID_LEVEL = new Set(['A', 'AA', 'AAA']); +const VALID_LIVE = new Set(['polite', 'assertive']); + +function normalizeA11ySpec(parsed: Record<string, unknown>): A11ySpec { + return { + landmarks: Array.isArray(parsed.landmarks) + ? (parsed.landmarks as A11ySpec['landmarks']).map((l) => ({ + role: typeof l.role === 'string' ? l.role : '', + label: typeof l.label === 'string' ? l.label : '', + element: typeof l.element === 'string' ? l.element : '', + })) + : [], + + headingStructure: Array.isArray(parsed.headingStructure) + ? (parsed.headingStructure as A11ySpec['headingStructure']).map((h) => ({ + level: typeof h.level === 'number' ? h.level : 1, + text: typeof h.text === 'string' ? h.text : '', + element: typeof h.element === 'string' ? h.element : '', + })) + : [], + + focusOrder: Array.isArray(parsed.focusOrder) + ? (parsed.focusOrder as A11ySpec['focusOrder']).map((f) => ({ + order: typeof f.order === 'number' ? f.order : 0, + element: typeof f.element === 'string' ? f.element : '', + type: typeof f.type === 'string' ? f.type : 'interactive', + notes: typeof f.notes === 'string' ? f.notes : '', + })) + : [], + + ariaAnnotations: Array.isArray(parsed.ariaAnnotations) + ? (parsed.ariaAnnotations as A11ySpec['ariaAnnotations']).map((a) => ({ + element: typeof a.element === 'string' ? a.element : '', + role: typeof a.role === 'string' ? a.role : '', + ariaLabel: typeof a.ariaLabel === 'string' ? a.ariaLabel : undefined, + ariaDescribedBy: + typeof a.ariaDescribedBy === 'string' ? a.ariaDescribedBy : undefined, + ariaLive: typeof a.ariaLive === 'string' ? a.ariaLive : undefined, + notes: typeof a.notes === 'string' ? a.notes : '', + })) + : [], + + keyboardShortcuts: Array.isArray(parsed.keyboardShortcuts) + ? (parsed.keyboardShortcuts as A11ySpec['keyboardShortcuts']).map( + (k) => ({ + key: typeof k.key === 'string' ? k.key : '', + action: typeof k.action === 'string' ? k.action : '', + element: typeof k.element === 'string' ? k.element : '', + }), + ) + : [], + + liveRegions: Array.isArray(parsed.liveRegions) + ? (parsed.liveRegions as A11ySpec['liveRegions']).map((lr) => ({ + element: typeof lr.element === 'string' ? lr.element : '', + type: VALID_LIVE.has(lr.type) ? lr.type : 'polite', + trigger: typeof lr.trigger === 'string' ? lr.trigger : '', + })) + : [], + + colorContrastReport: Array.isArray(parsed.colorContrastReport) + ? (parsed.colorContrastReport as A11ySpec['colorContrastReport']).map( + (cc) => ({ + element: typeof cc.element === 'string' ? cc.element : '', + foreground: typeof cc.foreground === 'string' ? cc.foreground : '', + background: typeof cc.background === 'string' ? cc.background : '', + ratio: typeof cc.ratio === 'number' ? cc.ratio : 0, + passes: VALID_CONTRAST.has(cc.passes) + ? cc.passes + : 'fail', + }), + ) + : [], + + recommendations: Array.isArray(parsed.recommendations) + ? (parsed.recommendations as A11ySpec['recommendations']).map((r) => ({ + title: typeof r.title === 'string' ? r.title : '', + description: typeof r.description === 'string' ? r.description : '', + wcagCriterion: + typeof r.wcagCriterion === 'string' ? r.wcagCriterion : '', + level: VALID_LEVEL.has(r.level) ? r.level : 'AA', + })) + : [], + }; +} + +// ── Main Generator ────────────────────────────── + +export async function generateA11ySpec( + screenshot: string, + extractedData: A11ySpecRequest['extractedData'], + lintResult: A11ySpecRequest['lintResult'], + _sessionId: string, +): Promise<A11ySpec> { + const client = getAnthropicClient(); + + // Build component info text + const meta = extractedData.metadata; + const componentInfo = [ + `Name: ${extractedData.componentName}`, + extractedData.componentDescription + ? `Description: ${extractedData.componentDescription}` + : '', + meta + ? `Type: ${meta.nodeType}, Size: ${meta.width}x${meta.height}, Auto-layout: ${meta.hasAutoLayout ? 'yes' : 'no'}, Children: ${meta.childCount}` + : '', + extractedData.states?.length + ? `States: ${extractedData.states.join(', ')}` + : '', + ] + .filter(Boolean) + .join('\n'); + + // Build lint summary + const bt = lintResult.summary.byType || {}; + const lintSummary = `${lintResult.summary.totalErrors} issues: ${bt.accessibility ?? 0} accessibility, ${bt.fill ?? 0} fills, ${bt.text ?? 0} text, ${bt.cognitive ?? 0} cognitive`; + + const prompt = buildA11ySpecPrompt(componentInfo, lintSummary); + + const response = await client.messages.create({ + model: MODEL, + max_tokens: 5000, + system: + 'You are a WCAG 2.2 accessibility expert. You generate thorough, actionable accessibility specifications from UI design screenshots. Respond in JSON format.', + messages: [ + { + role: 'user', + content: [ + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/png', + data: screenshot, + }, + }, + { type: 'text', text: prompt }, + ], + }, + ], + }); + + if (!response.content.length || response.content[0].type !== 'text') { + throw new Error('Empty response from a11y spec generation'); + } + + const text = response.content[0].text; + const jsonMatch = text.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + throw new Error('No JSON in a11y spec response'); + } + + const parsed = JSON.parse(jsonMatch[0]) as Record<string, unknown>; + return normalizeA11ySpec(parsed); +} diff --git a/backend/src/services/brand-consistency.ts b/backend/src/services/brand-consistency.ts new file mode 100644 index 0000000..a7b7ce6 --- /dev/null +++ b/backend/src/services/brand-consistency.ts @@ -0,0 +1,190 @@ +import { + buildBrandConsistencyPrompt, + type BrandGuide, +} from '../prompts/brand-consistency.js'; +import { getAnthropicClient, MODEL } from './claude.js'; + +// ── Types ────────────────────────────────────── + +export interface BrandConsistencyResult { + overallScore: number; // 0-100 + colorCompliance: { + score: number; + violations: Array<{ + element: string; + found: string; + expected: string; + tolerance: number; + }>; + }; + typographyCompliance: { + score: number; + violations: Array<{ + element: string; + found: string; + expected: string; + }>; + }; + spacingCompliance: { + score: number; + offGridValues: number[]; + }; + personalityMatch: { + rating: 'strong' | 'moderate' | 'weak'; + evidence: string[]; + }; + recommendations: Array<{ + title: string; + description: string; + severity: string; + }>; + summary: string; +} + +// ── Validation helpers ────────────────────────── + +const VALID_PERSONALITY_RATINGS = new Set(['strong', 'moderate', 'weak']); +const VALID_SEVERITIES = new Set(['error', 'warning', 'info']); + +function normalizeColorViolation( + raw: unknown, +): BrandConsistencyResult['colorCompliance']['violations'][number] { + const obj = raw as Record<string, unknown> | undefined; + return { + element: typeof obj?.element === 'string' ? obj.element : '', + found: typeof obj?.found === 'string' ? obj.found : '', + expected: typeof obj?.expected === 'string' ? obj.expected : '', + tolerance: typeof obj?.tolerance === 'number' ? obj.tolerance : 0, + }; +} + +function normalizeTypoViolation( + raw: unknown, +): BrandConsistencyResult['typographyCompliance']['violations'][number] { + const obj = raw as Record<string, unknown> | undefined; + return { + element: typeof obj?.element === 'string' ? obj.element : '', + found: typeof obj?.found === 'string' ? obj.found : '', + expected: typeof obj?.expected === 'string' ? obj.expected : '', + }; +} + +function normalizeRecommendation( + raw: unknown, +): BrandConsistencyResult['recommendations'][number] { + const obj = raw as Record<string, unknown> | undefined; + return { + title: typeof obj?.title === 'string' ? obj.title : '', + description: typeof obj?.description === 'string' ? obj.description : '', + severity: VALID_SEVERITIES.has(obj?.severity as string) + ? (obj!.severity as string) + : 'warning', + }; +} + +function clampScore(value: unknown): number { + if (typeof value !== 'number' || Number.isNaN(value)) return 0; + return Math.max(0, Math.min(100, Math.round(value))); +} + +// ── Main Analysis ────────────────────────────── + +export async function analyzeBrandConsistency( + screenshot: string, + brandGuide: BrandGuide, + lintResult: unknown, + sessionId: string, +): Promise<BrandConsistencyResult> { + const client = getAnthropicClient(); + + // Build a compact lint context string from the lint result + let lintContext = 'No lint data provided.'; + if (lintResult && typeof lintResult === 'object') { + const lr = lintResult as Record<string, unknown>; + const summary = lr.summary as Record<string, unknown> | undefined; + if (summary) { + lintContext = `${summary.totalErrors ?? 0} lint issues across ${summary.totalNodes ?? 0} nodes.`; + } + } + + const prompt = buildBrandConsistencyPrompt(brandGuide, lintContext); + + const response = await client.messages.create({ + model: MODEL, + max_tokens: 3000, + system: + 'You are a brand design auditor specializing in visual brand compliance. You evaluate user interfaces against brand guidelines with precision and cite specific visual evidence. You respond in JSON format when asked for structured output.', + messages: [ + { + role: 'user', + content: [ + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/png', + data: screenshot, + }, + }, + { type: 'text', text: prompt }, + ], + }, + ], + }); + + if (!response.content.length || response.content[0].type !== 'text') { + throw new Error('Empty response from brand consistency analysis'); + } + + const text = response.content[0].text; + const jsonMatch = text.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + throw new Error('No JSON in brand consistency response'); + } + + const parsed = JSON.parse(jsonMatch[0]) as Record<string, unknown>; + + // Normalize the response + const rawColor = parsed.colorCompliance as Record<string, unknown> | undefined; + const rawTypo = parsed.typographyCompliance as Record<string, unknown> | undefined; + const rawSpacing = parsed.spacingCompliance as Record<string, unknown> | undefined; + const rawPersonality = parsed.personalityMatch as Record<string, unknown> | undefined; + + return { + overallScore: clampScore(parsed.overallScore), + colorCompliance: { + score: clampScore(rawColor?.score), + violations: Array.isArray(rawColor?.violations) + ? (rawColor!.violations as unknown[]).map(normalizeColorViolation) + : [], + }, + typographyCompliance: { + score: clampScore(rawTypo?.score), + violations: Array.isArray(rawTypo?.violations) + ? (rawTypo!.violations as unknown[]).map(normalizeTypoViolation) + : [], + }, + spacingCompliance: { + score: clampScore(rawSpacing?.score), + offGridValues: Array.isArray(rawSpacing?.offGridValues) + ? (rawSpacing!.offGridValues as unknown[]).filter( + (v): v is number => typeof v === 'number', + ) + : [], + }, + personalityMatch: { + rating: VALID_PERSONALITY_RATINGS.has(rawPersonality?.rating as string) + ? (rawPersonality!.rating as 'strong' | 'moderate' | 'weak') + : 'weak', + evidence: Array.isArray(rawPersonality?.evidence) + ? (rawPersonality!.evidence as unknown[]).filter( + (e): e is string => typeof e === 'string', + ) + : [], + }, + recommendations: Array.isArray(parsed.recommendations) + ? (parsed.recommendations as unknown[]).map(normalizeRecommendation) + : [], + summary: typeof parsed.summary === 'string' ? parsed.summary : '', + }; +} diff --git a/backend/src/services/cognitive-walkthrough.ts b/backend/src/services/cognitive-walkthrough.ts new file mode 100644 index 0000000..42daac7 --- /dev/null +++ b/backend/src/services/cognitive-walkthrough.ts @@ -0,0 +1,204 @@ +import Anthropic from '@anthropic-ai/sdk'; +import { buildCognitiveWalkthroughPrompt } from '../prompts/cognitive-walkthrough.js'; +import { getAnthropicClient, MODEL } from './claude.js'; + +// ── Types ────────────────────────────────────── + +export interface CognitiveWalkthroughRequest { + taskDescription: string; + frames: Array<{ id: string; name: string; screenshot: string }>; + edges: Array<{ + sourceFrameId: string; + destinationFrameId: string; + trigger: string; + }>; + interactiveElements: Record< + string, + Array<{ + name: string; + type: string; + x: number; + y: number; + width: number; + height: number; + }> + >; + sessionId?: string; +} + +interface CWQuestionResult { + answer: 'yes' | 'partially' | 'no'; + explanation: string; +} + +interface CWStep { + stepNumber: number; + fromFrame: string; + toFrame: string; + action: string; + questions: { + q1_willTry: CWQuestionResult; + q2_willNotice: CWQuestionResult; + q3_willAssociate: CWQuestionResult; + q4_willSeeProgress: CWQuestionResult; + }; + overallSuccess: 'likely' | 'uncertain' | 'unlikely'; + barriers: string[]; + suggestions: string[]; +} + +export interface CognitiveWalkthroughResult { + taskDescription: string; + steps: CWStep[]; + overallAssessment: { + taskCompletionLikelihood: 'high' | 'medium' | 'low'; + criticalBarriers: string[]; + summary: string; + }; +} + +// ── Validation helpers ────────────────────────── + +const VALID_CW_ANSWERS = new Set(['yes', 'partially', 'no']); +const VALID_SUCCESS = new Set(['likely', 'uncertain', 'unlikely']); +const VALID_LIKELIHOOD = new Set(['high', 'medium', 'low']); + +function normalizeQuestionResult(raw: unknown): CWQuestionResult { + const obj = raw as Record<string, unknown> | undefined; + return { + answer: VALID_CW_ANSWERS.has(obj?.answer as string) + ? (obj!.answer as CWQuestionResult['answer']) + : 'no', + explanation: typeof obj?.explanation === 'string' ? obj.explanation : '', + }; +} + +function normalizeStep(raw: unknown, index: number): CWStep { + const obj = raw as Record<string, unknown> | undefined; + const questions = obj?.questions as Record<string, unknown> | undefined; + + return { + stepNumber: typeof obj?.stepNumber === 'number' ? obj.stepNumber : index + 1, + fromFrame: typeof obj?.fromFrame === 'string' ? obj.fromFrame : '', + toFrame: typeof obj?.toFrame === 'string' ? obj.toFrame : '', + action: typeof obj?.action === 'string' ? obj.action : '', + questions: { + q1_willTry: normalizeQuestionResult(questions?.q1_willTry), + q2_willNotice: normalizeQuestionResult(questions?.q2_willNotice), + q3_willAssociate: normalizeQuestionResult(questions?.q3_willAssociate), + q4_willSeeProgress: normalizeQuestionResult(questions?.q4_willSeeProgress), + }, + overallSuccess: VALID_SUCCESS.has(obj?.overallSuccess as string) + ? (obj!.overallSuccess as CWStep['overallSuccess']) + : 'unlikely', + barriers: Array.isArray(obj?.barriers) ? (obj.barriers as string[]) : [], + suggestions: Array.isArray(obj?.suggestions) ? (obj.suggestions as string[]) : [], + }; +} + +// ── Main Analysis ────────────────────────────── + +export async function runCognitiveWalkthrough( + req: CognitiveWalkthroughRequest, +): Promise<CognitiveWalkthroughResult> { + const client = getAnthropicClient(); + + // Build frame label list + const frameLabels = req.frames.map((f, i) => `[Frame ${i + 1}: ${f.name}]`); + + // Build edge descriptions using frame names + const frameNameById = new Map(req.frames.map((f) => [f.id, f.name])); + const edgeDescriptions = req.edges.map((e) => { + const src = frameNameById.get(e.sourceFrameId) || e.sourceFrameId; + const dst = frameNameById.get(e.destinationFrameId) || e.destinationFrameId; + return ` "${src}" -> "${dst}" (trigger: ${e.trigger})`; + }); + + // Build interactive element descriptions per frame + const interactiveElementDescriptions: string[] = []; + for (const frame of req.frames) { + const elements = req.interactiveElements[frame.id]; + if (elements && elements.length > 0) { + const lines = elements.map( + (el) => + ` - ${el.name} (${el.type}) at (${el.x}, ${el.y}), ${el.width}x${el.height}`, + ); + interactiveElementDescriptions.push( + `[${frame.name}]\n${lines.join('\n')}`, + ); + } + } + + const prompt = buildCognitiveWalkthroughPrompt( + req.taskDescription, + frameLabels, + edgeDescriptions, + interactiveElementDescriptions, + ); + + // Build content array: label + screenshot for each frame, then the prompt + const content: Anthropic.ContentBlockParam[] = []; + + for (let i = 0; i < req.frames.length; i++) { + const frame = req.frames[i]; + content.push({ + type: 'text', + text: `--- [Frame ${i + 1}: ${frame.name}] ---`, + }); + content.push({ + type: 'image', + source: { + type: 'base64', + media_type: 'image/png', + data: frame.screenshot, + }, + }); + } + + content.push({ type: 'text', text: prompt }); + + const response = await client.messages.create({ + model: MODEL, + max_tokens: 6000, + system: + 'You are a UX researcher specializing in usability evaluation methods. You conduct rigorous cognitive walkthroughs grounded in evidence from the interface screenshots. You respond in JSON format when asked for structured output.', + messages: [{ role: 'user', content }], + }); + + if (!response.content.length || response.content[0].type !== 'text') { + throw new Error('Empty response from cognitive walkthrough analysis'); + } + + const text = response.content[0].text; + const jsonMatch = text.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + throw new Error('No JSON in cognitive walkthrough response'); + } + + const parsed = JSON.parse(jsonMatch[0]) as Record<string, unknown>; + + // Normalize and validate the response + const rawSteps = Array.isArray(parsed.steps) ? parsed.steps : []; + const steps = rawSteps.map((s: unknown, i: number) => normalizeStep(s, i)); + + const rawAssessment = parsed.overallAssessment as Record<string, unknown> | undefined; + + return { + taskDescription: req.taskDescription, + steps, + overallAssessment: { + taskCompletionLikelihood: VALID_LIKELIHOOD.has( + rawAssessment?.taskCompletionLikelihood as string, + ) + ? (rawAssessment!.taskCompletionLikelihood as 'high' | 'medium' | 'low') + : 'low', + criticalBarriers: Array.isArray(rawAssessment?.criticalBarriers) + ? (rawAssessment.criticalBarriers as string[]) + : [], + summary: + typeof rawAssessment?.summary === 'string' + ? rawAssessment.summary + : '', + }, + }; +} diff --git a/backend/src/services/copy-tone.ts b/backend/src/services/copy-tone.ts new file mode 100644 index 0000000..232572d --- /dev/null +++ b/backend/src/services/copy-tone.ts @@ -0,0 +1,187 @@ +import { buildCopyTonePrompt } from '../prompts/copy-tone.js'; +import { getAnthropicClient, MODEL } from './claude.js'; + +// ── Types ────────────────────────────────────── + +export interface CopyToneResult { + overallConsistency: 'consistent' | 'mostly_consistent' | 'inconsistent'; + terminologyIssues: Array<{ + term1: string; + term2: string; + screens: string[]; + recommendation: string; + }>; + toneIssues: Array<{ + screen: string; + text: string; + tone: string; + expectedTone: string; + }>; + ctaPatterns: { + consistent: boolean; + patterns: string[]; + violations: Array<{ + screen: string; + cta: string; + issue: string; + }>; + }; + readabilityIssues: Array<{ + screen: string; + text: string; + issue: string; + }>; + recommendations: Array<{ + title: string; + description: string; + priority: 'high' | 'medium' | 'low'; + }>; + summary: string; +} + +// ── Validation helpers ────────────────────────── + +const VALID_CONSISTENCY = new Set([ + 'consistent', + 'mostly_consistent', + 'inconsistent', +]); +const VALID_PRIORITY = new Set(['high', 'medium', 'low']); + +function normalizeTerminologyIssue( + raw: unknown, +): CopyToneResult['terminologyIssues'][number] { + const obj = raw as Record<string, unknown> | undefined; + return { + term1: typeof obj?.term1 === 'string' ? obj.term1 : '', + term2: typeof obj?.term2 === 'string' ? obj.term2 : '', + screens: Array.isArray(obj?.screens) + ? (obj!.screens as unknown[]).filter( + (s): s is string => typeof s === 'string', + ) + : [], + recommendation: + typeof obj?.recommendation === 'string' ? obj.recommendation : '', + }; +} + +function normalizeToneIssue( + raw: unknown, +): CopyToneResult['toneIssues'][number] { + const obj = raw as Record<string, unknown> | undefined; + return { + screen: typeof obj?.screen === 'string' ? obj.screen : '', + text: typeof obj?.text === 'string' ? obj.text : '', + tone: typeof obj?.tone === 'string' ? obj.tone : '', + expectedTone: typeof obj?.expectedTone === 'string' ? obj.expectedTone : '', + }; +} + +function normalizeCtaViolation( + raw: unknown, +): CopyToneResult['ctaPatterns']['violations'][number] { + const obj = raw as Record<string, unknown> | undefined; + return { + screen: typeof obj?.screen === 'string' ? obj.screen : '', + cta: typeof obj?.cta === 'string' ? obj.cta : '', + issue: typeof obj?.issue === 'string' ? obj.issue : '', + }; +} + +function normalizeReadabilityIssue( + raw: unknown, +): CopyToneResult['readabilityIssues'][number] { + const obj = raw as Record<string, unknown> | undefined; + return { + screen: typeof obj?.screen === 'string' ? obj.screen : '', + text: typeof obj?.text === 'string' ? obj.text : '', + issue: typeof obj?.issue === 'string' ? obj.issue : '', + }; +} + +function normalizeRecommendation( + raw: unknown, +): CopyToneResult['recommendations'][number] { + const obj = raw as Record<string, unknown> | undefined; + return { + title: typeof obj?.title === 'string' ? obj.title : '', + description: typeof obj?.description === 'string' ? obj.description : '', + priority: VALID_PRIORITY.has(obj?.priority as string) + ? (obj!.priority as 'high' | 'medium' | 'low') + : 'medium', + }; +} + +// ── Main Analysis ────────────────────────────── + +export async function analyzeCopyTone( + screens: Array<{ name: string; textContent: string[] }>, + personality?: string[], + sessionId?: string, +): Promise<CopyToneResult> { + const client = getAnthropicClient(); + + const prompt = buildCopyTonePrompt(screens, personality); + + // Text-only analysis — no screenshot needed, saves tokens + const response = await client.messages.create({ + model: MODEL, + max_tokens: 3000, + system: + 'You are a senior UX copywriter and content strategist. You specialize in cross-screen copy consistency, voice and tone auditing, and plain language principles. You respond in JSON format when asked for structured output.', + messages: [ + { + role: 'user', + content: prompt, + }, + ], + }); + + if (!response.content.length || response.content[0].type !== 'text') { + throw new Error('Empty response from copy tone analysis'); + } + + const text = response.content[0].text; + const jsonMatch = text.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + throw new Error('No JSON in copy tone response'); + } + + const parsed = JSON.parse(jsonMatch[0]) as Record<string, unknown>; + + // Normalize the response + const rawCta = parsed.ctaPatterns as Record<string, unknown> | undefined; + + return { + overallConsistency: VALID_CONSISTENCY.has( + parsed.overallConsistency as string, + ) + ? (parsed.overallConsistency as CopyToneResult['overallConsistency']) + : 'inconsistent', + terminologyIssues: Array.isArray(parsed.terminologyIssues) + ? (parsed.terminologyIssues as unknown[]).map(normalizeTerminologyIssue) + : [], + toneIssues: Array.isArray(parsed.toneIssues) + ? (parsed.toneIssues as unknown[]).map(normalizeToneIssue) + : [], + ctaPatterns: { + consistent: + typeof rawCta?.consistent === 'boolean' ? rawCta.consistent : false, + patterns: Array.isArray(rawCta?.patterns) + ? (rawCta!.patterns as unknown[]).filter( + (p): p is string => typeof p === 'string', + ) + : [], + violations: Array.isArray(rawCta?.violations) + ? (rawCta!.violations as unknown[]).map(normalizeCtaViolation) + : [], + }, + readabilityIssues: Array.isArray(parsed.readabilityIssues) + ? (parsed.readabilityIssues as unknown[]).map(normalizeReadabilityIssue) + : [], + recommendations: Array.isArray(parsed.recommendations) + ? (parsed.recommendations as unknown[]).map(normalizeRecommendation) + : [], + summary: typeof parsed.summary === 'string' ? parsed.summary : '', + }; +} diff --git a/backend/src/services/extended-analyzer.ts b/backend/src/services/extended-analyzer.ts new file mode 100644 index 0000000..2399b9d --- /dev/null +++ b/backend/src/services/extended-analyzer.ts @@ -0,0 +1,272 @@ +import { getAnthropicClient, MODEL } from './claude.js'; +import { SYSTEM_PROMPT } from '../prompts/system.js'; +import { buildAttentionPrompt } from '../prompts/attention.js'; +import { buildNielsenHeuristicsPrompt } from '../prompts/nielsen-heuristics.js'; + +// ── Attention analysis types ───────────────────────────────────────── + +export interface AttentionAnalysis { + focalPoint: { + element: string; + strength: 'strong' | 'moderate' | 'weak'; + isIntendedCTA: boolean; + }; + readingFlow: { + pattern: 'F' | 'Z' | 'linear' | 'scattered'; + confidence: 'high' | 'medium' | 'low'; + description: string; + }; + competingElements: Array<{ element: string; reason: string }>; + deadZones: Array<{ area: string; suggestion: string }>; + visualWeightBalance: + | 'balanced' + | 'left-heavy' + | 'right-heavy' + | 'top-heavy' + | 'bottom-heavy'; + recommendations: Array<{ + title: string; + description: string; + severity: 'critical' | 'warning' | 'info'; + }>; +} + +// ── Nielsen heuristics types ───────────────────────────────────────── + +export interface NielsenHeuristic { + id: string; + name: string; + rating: 'pass' | 'needs_improvement' | 'fail'; + evidence: string[]; + recommendation: string | null; +} + +export interface NielsenEvaluation { + heuristics: NielsenHeuristic[]; + overallCompliance: number; + criticalViolations: Array<{ heuristic: string; description: string }>; + summary: string; +} + +// ── Combined result ────────────────────────────────────────────────── + +export interface ExtendedAnalysisResult { + attention?: AttentionAnalysis; + nielsen?: NielsenEvaluation; +} + +export interface ExtendedFeatures { + attention?: boolean; + nielsen?: boolean; +} + +// ── Validation helpers ─────────────────────────────────────────────── + +const VALID_STRENGTHS = new Set(['strong', 'moderate', 'weak']); +const VALID_PATTERNS = new Set(['F', 'Z', 'linear', 'scattered']); +const VALID_CONFIDENCE = new Set(['high', 'medium', 'low']); +const VALID_BALANCE = new Set([ + 'balanced', + 'left-heavy', + 'right-heavy', + 'top-heavy', + 'bottom-heavy', +]); +const VALID_SEVERITY = new Set(['critical', 'warning', 'info']); +const VALID_RATINGS = new Set(['pass', 'needs_improvement', 'fail']); + +function normalizeAttention(raw: any): AttentionAnalysis { + const fp = raw?.focalPoint ?? {}; + const rf = raw?.readingFlow ?? {}; + + return { + focalPoint: { + element: typeof fp.element === 'string' ? fp.element : 'unknown', + strength: VALID_STRENGTHS.has(fp.strength) ? fp.strength : 'weak', + isIntendedCTA: typeof fp.isIntendedCTA === 'boolean' ? fp.isIntendedCTA : false, + }, + readingFlow: { + pattern: VALID_PATTERNS.has(rf.pattern) ? rf.pattern : 'scattered', + confidence: VALID_CONFIDENCE.has(rf.confidence) ? rf.confidence : 'low', + description: typeof rf.description === 'string' ? rf.description : '', + }, + competingElements: Array.isArray(raw?.competingElements) + ? raw.competingElements.map((e: any) => ({ + element: typeof e?.element === 'string' ? e.element : '', + reason: typeof e?.reason === 'string' ? e.reason : '', + })) + : [], + deadZones: Array.isArray(raw?.deadZones) + ? raw.deadZones.map((d: any) => ({ + area: typeof d?.area === 'string' ? d.area : '', + suggestion: typeof d?.suggestion === 'string' ? d.suggestion : '', + })) + : [], + visualWeightBalance: VALID_BALANCE.has(raw?.visualWeightBalance) + ? raw.visualWeightBalance + : 'balanced', + recommendations: Array.isArray(raw?.recommendations) + ? raw.recommendations + .filter((r: any) => r?.title && r?.description) + .map((r: any) => ({ + title: String(r.title), + description: String(r.description), + severity: VALID_SEVERITY.has(r.severity) ? r.severity : 'info', + })) + : [], + }; +} + +function normalizeNielsen(raw: any): NielsenEvaluation { + const heuristics: NielsenHeuristic[] = Array.isArray(raw?.heuristics) + ? raw.heuristics.map((h: any) => ({ + id: typeof h?.id === 'string' ? h.id : 'unknown', + name: typeof h?.name === 'string' ? h.name : 'unknown', + rating: VALID_RATINGS.has(h?.rating) ? h.rating : 'fail', + evidence: Array.isArray(h?.evidence) ? h.evidence.map(String) : [], + recommendation: + typeof h?.recommendation === 'string' ? h.recommendation : null, + })) + : []; + + const compliance = + typeof raw?.overallCompliance === 'number' + ? Math.max(0, Math.min(100, Math.round(raw.overallCompliance))) + : 0; + + return { + heuristics, + overallCompliance: compliance, + criticalViolations: Array.isArray(raw?.criticalViolations) + ? raw.criticalViolations + .filter((v: any) => v?.heuristic && v?.description) + .map((v: any) => ({ + heuristic: String(v.heuristic), + description: String(v.description), + })) + : [], + summary: typeof raw?.summary === 'string' ? raw.summary : '', + }; +} + +// ── Core API call ──────────────────────────────────────────────────── + +async function callClaude( + screenshotBase64: string, + prompt: string, +): Promise<any> { + const anthropic = getAnthropicClient(); + + const response = await anthropic.messages.create({ + model: MODEL, + max_tokens: 3000, + system: SYSTEM_PROMPT, + messages: [ + { + role: 'user', + content: [ + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/png', + data: screenshotBase64, + }, + }, + { type: 'text', text: prompt }, + ], + }, + ], + }); + + if (!response.content.length || response.content[0].type !== 'text') { + throw new Error('Empty response from Claude'); + } + + const text = response.content[0].text; + const jsonMatch = text.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + throw new Error('No JSON found in Claude response'); + } + + return JSON.parse(jsonMatch[0]); +} + +// ── Public orchestrator ────────────────────────────────────────────── + +/** + * Run extended analyses (attention prediction, Nielsen heuristics) in parallel. + * Only runs features that are explicitly enabled. + */ +export async function runExtendedAnalysis( + screenshot: string, + lintResult: any, + extractedData: any, + sessionId: string, + features: ExtendedFeatures, +): Promise<ExtendedAnalysisResult> { + const result: ExtendedAnalysisResult = {}; + + // Build shared lint context string + const bt = lintResult?.summary?.byType || {}; + const lintContext = [ + `Component: ${extractedData?.componentName ?? 'unknown'}`, + extractedData?.metadata + ? `Size: ${extractedData.metadata.width}x${extractedData.metadata.height}, Type: ${extractedData.metadata.nodeType}` + : '', + `Lint issues: ${lintResult?.summary?.totalErrors ?? 0} total — ${bt.fill ?? 0} fills, ${bt.stroke ?? 0} strokes, ${bt.spacing ?? 0} spacing, ${bt.autoLayout ?? 0} auto-layout`, + ] + .filter(Boolean) + .join('\n'); + + // Build task list — only enabled features + const tasks: Array<Promise<void>> = []; + + if (features.attention) { + tasks.push( + (async () => { + const start = Date.now(); + try { + const prompt = buildAttentionPrompt(lintContext); + const raw = await callClaude(screenshot, prompt); + result.attention = normalizeAttention(raw); + console.log( + `[extended-analyzer] attention completed in ${Date.now() - start}ms (session=${sessionId})`, + ); + } catch (err) { + console.error( + `[extended-analyzer] attention failed after ${Date.now() - start}ms (session=${sessionId}):`, + err instanceof Error ? err.message : err, + ); + } + })(), + ); + } + + if (features.nielsen) { + tasks.push( + (async () => { + const start = Date.now(); + try { + const prompt = buildNielsenHeuristicsPrompt(lintContext); + const raw = await callClaude(screenshot, prompt); + result.nielsen = normalizeNielsen(raw); + console.log( + `[extended-analyzer] nielsen completed in ${Date.now() - start}ms (session=${sessionId})`, + ); + } catch (err) { + console.error( + `[extended-analyzer] nielsen failed after ${Date.now() - start}ms (session=${sessionId}):`, + err instanceof Error ? err.message : err, + ); + } + })(), + ); + } + + if (tasks.length > 0) { + await Promise.all(tasks); + } + + return result; +} diff --git a/backend/src/services/page-sweep-analyzer.ts b/backend/src/services/page-sweep-analyzer.ts new file mode 100644 index 0000000..2905ac3 --- /dev/null +++ b/backend/src/services/page-sweep-analyzer.ts @@ -0,0 +1,238 @@ +import Anthropic from '@anthropic-ai/sdk'; +import { SYSTEM_PROMPT } from '../prompts/system.js'; +import { buildPageSweepPrompt } from '../prompts/page-sweep.js'; +import { getAnthropicClient, MODEL } from './claude.js'; + +// ── Types ────────────────────────────────────── + +interface FrameInput { + id: string; + name: string; + screenshot: string; // base64 PNG + lintResult: { + summary: { + totalErrors: number; + byType: Record<string, number>; + totalNodes: number; + nodesWithErrors: number; + }; + errors: Array<{ + nodeId: string; + nodeName: string; + errorType: string; + message: string; + value: string; + severity?: string; + }>; + }; + width: number; + height: number; +} + +export interface PageSweepRequest { + frames: FrameInput[]; + sessionId?: string; +} + +export interface PageSweepFileHealth { + overallScore: number; + grade: 'excellent' | 'needs-work' | 'poor'; + totalFrames: number; + totalIssues: number; + topIssues: Array<{ type: string; count: number; severity: string }>; + consistencyScore: number; +} + +export interface PageSweepFrameResult { + id: string; + name: string; + score: number; + issueCount: number; + topIssues: string[]; +} + +export interface PageSweepAiInsights { + strengths: string[]; + weaknesses: string[]; + recommendations: Array<{ title: string; description: string; affectedFrames: string[] }>; + summary: string; +} + +export interface PageSweepResult { + fileHealth: PageSweepFileHealth; + frames: PageSweepFrameResult[]; + aiInsights: PageSweepAiInsights; +} + +// ── Score Computation ────────────────────────── + +const SEVERITY_WEIGHT: Record<string, number> = { critical: 10, warning: 3, info: 1 }; + +function computeFrameScore(frame: FrameInput): number { + const errors = frame.lintResult.errors; + const total = Math.max(frame.lintResult.summary.totalNodes, 1); + const weightedFailed = errors.reduce( + (sum, e) => sum + (SEVERITY_WEIGHT[e.severity || 'warning'] || 3), + 0, + ); + const weightedPassed = Math.max(0, total - errors.length) * 10; + const t = weightedPassed + weightedFailed; + return t > 0 ? Math.round((weightedPassed / t) * 100) : 100; +} + +function getGrade(score: number): 'excellent' | 'needs-work' | 'poor' { + if (score >= 90) return 'excellent'; + if (score >= 70) return 'needs-work'; + return 'poor'; +} + +// ── Main Analysis ────────────────────────────── + +export async function analyzePageSweep(req: PageSweepRequest): Promise<PageSweepResult> { + // 1. Compute per-frame scores + const frameResults: PageSweepFrameResult[] = req.frames.map((frame) => { + const score = computeFrameScore(frame); + const issueCount = frame.lintResult.summary.totalErrors; + + // Top issues by type count for this frame + const typeCounts: Record<string, number> = {}; + for (const err of frame.lintResult.errors) { + typeCounts[err.errorType] = (typeCounts[err.errorType] || 0) + 1; + } + const topIssues = Object.entries(typeCounts) + .sort((a, b) => b[1] - a[1]) + .slice(0, 3) + .map(([type, count]) => `${type} (${count})`); + + return { id: frame.id, name: frame.name, score, issueCount, topIssues }; + }); + + // 2. Aggregate file health + const scores = frameResults.map((f) => f.score); + const overallScore = scores.length > 0 + ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) + : 100; + + // Consistency score: 100 - standard deviation (how uniform are the scores?) + const mean = overallScore; + const variance = scores.length > 0 + ? scores.reduce((sum, s) => sum + Math.pow(s - mean, 2), 0) / scores.length + : 0; + const stdDev = Math.sqrt(variance); + const consistencyScore = Math.max(0, Math.round(100 - stdDev)); + + const totalIssues = frameResults.reduce((sum, f) => sum + f.issueCount, 0); + + // Aggregate top issues across all frames + const issueTypeCounts: Record<string, { count: number; severity: string }> = {}; + for (const frame of req.frames) { + for (const err of frame.lintResult.errors) { + const key = err.errorType; + if (!issueTypeCounts[key]) { + issueTypeCounts[key] = { count: 0, severity: err.severity || 'warning' }; + } + issueTypeCounts[key].count++; + } + } + const topIssues = Object.entries(issueTypeCounts) + .sort((a, b) => b[1].count - a[1].count) + .slice(0, 10) + .map(([type, { count, severity }]) => ({ type, count, severity })); + + const fileHealth: PageSweepFileHealth = { + overallScore, + grade: getGrade(overallScore), + totalFrames: req.frames.length, + totalIssues, + topIssues, + consistencyScore, + }; + + // 3. AI insights (skip if no API key) + let aiInsights: PageSweepAiInsights = { + strengths: [], + weaknesses: [], + recommendations: [], + summary: 'AI analysis unavailable (no API key configured).', + }; + + if (process.env.ANTHROPIC_API_KEY) { + try { + aiInsights = await generatePageSweepInsights(req.frames, frameResults, fileHealth); + } catch (error) { + console.error('Page sweep AI error:', error); + aiInsights.summary = 'AI analysis failed. See deterministic results above.'; + } + } + + return { fileHealth, frames: frameResults, aiInsights }; +} + +// ── AI Insights Generation ────────────────────── + +async function generatePageSweepInsights( + frames: FrameInput[], + frameResults: PageSweepFrameResult[], + fileHealth: PageSweepFileHealth, +): Promise<PageSweepAiInsights> { + const client = getAnthropicClient(); + + // Build frame summaries text + const frameSummaries = frameResults.map((fr) => { + const frame = frames.find((f) => f.id === fr.id); + const bt = frame?.lintResult.summary.byType || {}; + const issues = Object.entries(bt) + .filter(([, v]) => v > 0) + .map(([k, v]) => `${k}: ${v}`) + .join(', '); + return `- "${fr.name}" (${frame?.width}x${frame?.height}): score ${fr.score}/100, ${fr.issueCount} issues [${issues || 'none'}]`; + }).join('\n'); + + const aggregatedStats = [ + `Total frames: ${fileHealth.totalFrames}`, + `Total issues: ${fileHealth.totalIssues}`, + `Average score: ${fileHealth.overallScore}/100 (${fileHealth.grade})`, + `Consistency score: ${fileHealth.consistencyScore}/100`, + `Top issue types: ${fileHealth.topIssues.map((i) => `${i.type} (${i.count})`).join(', ') || 'none'}`, + ].join('\n'); + + const prompt = buildPageSweepPrompt(frameSummaries, aggregatedStats); + + // Build content with screenshots (batch up to 15 for token budget) + const content: Anthropic.ContentBlockParam[] = []; + const screenshotFrames = frames.filter((f) => f.screenshot).slice(0, 15); + + for (const frame of screenshotFrames) { + content.push({ type: 'text', text: `--- Frame: "${frame.name}" ---` }); + content.push({ + type: 'image', + source: { type: 'base64', media_type: 'image/png', data: frame.screenshot }, + }); + } + + content.push({ type: 'text', text: prompt }); + + const response = await client.messages.create({ + model: MODEL, + max_tokens: 3000, + system: SYSTEM_PROMPT, + messages: [{ role: 'user', content }], + }); + + if (!response.content.length || response.content[0].type !== 'text') { + throw new Error('Empty response from AI'); + } + + const text = response.content[0].text; + const jsonMatch = text.match(/\{[\s\S]*\}/); + if (!jsonMatch) throw new Error('No JSON in page sweep response'); + + const parsed = JSON.parse(jsonMatch[0]); + + return { + strengths: Array.isArray(parsed.strengths) ? parsed.strengths : [], + weaknesses: Array.isArray(parsed.weaknesses) ? parsed.weaknesses : [], + recommendations: Array.isArray(parsed.recommendations) ? parsed.recommendations : [], + summary: typeof parsed.summary === 'string' ? parsed.summary : '', + }; +} diff --git a/backend/src/services/persona-research.ts b/backend/src/services/persona-research.ts new file mode 100644 index 0000000..06b4bdd --- /dev/null +++ b/backend/src/services/persona-research.ts @@ -0,0 +1,319 @@ +import { + PERSONAS, + buildPersonaUserPrompt, + type PersonaPrompt, +} from '../prompts/persona-research.js'; +import { getAnthropicClient, MODEL } from './claude.js'; + +// ── Types ────────────────────────────────────── + +export interface PersonaEvaluation { + name: string; + role: string; + rating: 1 | 2 | 3 | 4 | 5; + canCompleteTask: 'yes' | 'with_difficulty' | 'no'; + barriers: string[]; + positives: string[]; + frustrations: string[]; + suggestions: string[]; + quote: string; +} + +export interface PersonaResult { + personas: PersonaEvaluation[]; + aggregated: { + averageRating: number; + universalBarriers: string[]; // barriers shared by 3+ personas + accessibilityGaps: string[]; + quickWins: string[]; // fixes that help 3+ personas + summary: string; + }; +} + +// ── Validation helpers ────────────────────────── + +const VALID_RATINGS = new Set([1, 2, 3, 4, 5]); +const VALID_TASK_COMPLETION = new Set(['yes', 'with_difficulty', 'no']); + +function normalizePersonaResult( + raw: Record<string, unknown>, + persona: PersonaPrompt, +): PersonaEvaluation { + const rawRating = typeof raw.rating === 'number' ? raw.rating : 3; + const rating = VALID_RATINGS.has(rawRating) + ? (rawRating as 1 | 2 | 3 | 4 | 5) + : 3; + + return { + name: persona.name, + role: persona.role, + rating, + canCompleteTask: VALID_TASK_COMPLETION.has(raw.canCompleteTask as string) + ? (raw.canCompleteTask as PersonaEvaluation['canCompleteTask']) + : 'no', + barriers: Array.isArray(raw.barriers) + ? raw.barriers.filter((b): b is string => typeof b === 'string') + : [], + positives: Array.isArray(raw.positives) + ? raw.positives.filter((p): p is string => typeof p === 'string') + : [], + frustrations: Array.isArray(raw.frustrations) + ? raw.frustrations.filter((f): f is string => typeof f === 'string') + : [], + suggestions: Array.isArray(raw.suggestions) + ? raw.suggestions.filter((s): s is string => typeof s === 'string') + : [], + quote: typeof raw.quote === 'string' ? raw.quote : '', + }; +} + +// ── Single Persona Call ────────────────────────── + +async function runSinglePersona( + persona: PersonaPrompt, + screenshotBase64: string, + taskDescription: string, + lintContext?: string, +): Promise<PersonaEvaluation> { + const client = getAnthropicClient(); + + const userPrompt = buildPersonaUserPrompt(taskDescription, lintContext); + + const response = await client.messages.create({ + model: MODEL, + max_tokens: 1500, + system: persona.systemPrompt, + messages: [ + { + role: 'user', + content: [ + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/png', + data: screenshotBase64, + }, + }, + { type: 'text', text: userPrompt }, + ], + }, + ], + }); + + if (!response.content.length || response.content[0].type !== 'text') { + throw new Error(`Empty response from ${persona.role} persona`); + } + + const text = response.content[0].text; + const jsonMatch = text.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + throw new Error(`No JSON in ${persona.role} persona response`); + } + + const parsed = JSON.parse(jsonMatch[0]) as Record<string, unknown>; + return normalizePersonaResult(parsed, persona); +} + +// ── Aggregation ───────────────────────────────── + +function findUniversalBarriers(personas: PersonaEvaluation[]): string[] { + // Count how many personas mention each barrier (fuzzy match via word overlap) + const barrierGroups: Array<{ text: string; count: number; personas: string[] }> = []; + + for (const persona of personas) { + for (const barrier of persona.barriers) { + const barrierLower = barrier.toLowerCase(); + const barrierWords = new Set( + barrierLower.split(/\s+/).filter((w) => w.length > 3), + ); + + const existing = barrierGroups.find((g) => { + const gWords = new Set( + g.text.toLowerCase().split(/\s+/).filter((w) => w.length > 3), + ); + if (gWords.size === 0 || barrierWords.size === 0) return false; + let overlap = 0; + for (const word of barrierWords) { + if (gWords.has(word)) overlap++; + } + const overlapRatio = overlap / Math.min(gWords.size, barrierWords.size); + return overlapRatio >= 0.4; + }); + + if (existing) { + if (!existing.personas.includes(persona.name)) { + existing.count++; + existing.personas.push(persona.name); + } + } else { + barrierGroups.push({ + text: barrier, + count: 1, + personas: [persona.name], + }); + } + } + } + + // Return barriers mentioned by 3+ personas + return barrierGroups + .filter((g) => g.count >= 3) + .sort((a, b) => b.count - a.count) + .map((g) => g.text); +} + +function findQuickWins(personas: PersonaEvaluation[]): string[] { + // Same logic as barriers but for suggestions + const suggestionGroups: Array<{ + text: string; + count: number; + personas: string[]; + }> = []; + + for (const persona of personas) { + for (const suggestion of persona.suggestions) { + const sugLower = suggestion.toLowerCase(); + const sugWords = new Set( + sugLower.split(/\s+/).filter((w) => w.length > 3), + ); + + const existing = suggestionGroups.find((g) => { + const gWords = new Set( + g.text.toLowerCase().split(/\s+/).filter((w) => w.length > 3), + ); + if (gWords.size === 0 || sugWords.size === 0) return false; + let overlap = 0; + for (const word of sugWords) { + if (gWords.has(word)) overlap++; + } + const overlapRatio = overlap / Math.min(gWords.size, sugWords.size); + return overlapRatio >= 0.4; + }); + + if (existing) { + if (!existing.personas.includes(persona.name)) { + existing.count++; + existing.personas.push(persona.name); + } + } else { + suggestionGroups.push({ + text: suggestion, + count: 1, + personas: [persona.name], + }); + } + } + } + + return suggestionGroups + .filter((g) => g.count >= 3) + .sort((a, b) => b.count - a.count) + .map((g) => g.text); +} + +function findAccessibilityGaps(personas: PersonaEvaluation[]): string[] { + // Collect barriers from accessibility-sensitive personas + const a11yPersonas = ['Screen Reader User', 'Elderly User (65+)']; + const gaps: string[] = []; + + for (const persona of personas) { + if (a11yPersonas.includes(persona.role)) { + gaps.push(...persona.barriers); + } + } + + return gaps; +} + +function buildAggregatedSummary( + personas: PersonaEvaluation[], + avgRating: number, + universalBarriers: string[], + quickWins: string[], +): string { + const parts: string[] = []; + + if (avgRating >= 4) { + parts.push( + 'The design performs well across diverse user profiles.', + ); + } else if (avgRating >= 3) { + parts.push( + 'The design is generally usable but presents challenges for some user groups.', + ); + } else if (avgRating >= 2) { + parts.push( + 'The design has significant usability barriers affecting multiple user profiles.', + ); + } else { + parts.push( + 'The design is difficult to use for most user profiles and needs substantial improvement.', + ); + } + + const cantComplete = personas.filter((p) => p.canCompleteTask === 'no'); + if (cantComplete.length > 0) { + parts.push( + `${cantComplete.length} of ${personas.length} personas cannot complete the task (${cantComplete.map((p) => p.role).join(', ')}).`, + ); + } + + if (universalBarriers.length > 0) { + parts.push( + `${universalBarriers.length} universal barrier${universalBarriers.length > 1 ? 's' : ''} identified across 3+ personas.`, + ); + } + + if (quickWins.length > 0) { + parts.push( + `${quickWins.length} quick win${quickWins.length > 1 ? 's' : ''} would improve the experience for the majority of users.`, + ); + } + + return parts.join(' '); +} + +// ── Main Orchestrator ────────────────────────── + +export async function runPersonaResearch( + screenshot: string, + taskDescription: string, + lintContext?: string, + sessionId?: string, +): Promise<PersonaResult> { + // Run all 5 personas in PARALLEL + const results = await Promise.all( + PERSONAS.map((persona) => + runSinglePersona(persona, screenshot, taskDescription, lintContext), + ), + ); + + // Aggregate results + const ratings = results.map((r) => r.rating); + const averageRating = + Math.round( + (ratings.reduce((a, b) => a + b, 0) / ratings.length) * 100, + ) / 100; + + const universalBarriers = findUniversalBarriers(results); + const accessibilityGaps = findAccessibilityGaps(results); + const quickWins = findQuickWins(results); + const summary = buildAggregatedSummary( + results, + averageRating, + universalBarriers, + quickWins, + ); + + return { + personas: results, + aggregated: { + averageRating, + universalBarriers, + accessibilityGaps, + quickWins, + summary, + }, + }; +} diff --git a/backend/src/services/pure-scoring.ts b/backend/src/services/pure-scoring.ts new file mode 100644 index 0000000..7220240 --- /dev/null +++ b/backend/src/services/pure-scoring.ts @@ -0,0 +1,309 @@ +import { EVALUATORS, type EvaluatorPrompt } from '../prompts/pure-scoring.js'; +import { getAnthropicClient, MODEL } from './claude.js'; + +// ── Types ────────────────────────────────────── + +export interface PureRequest { + screenshot: string; + taskDescription: string; + lintContext?: string; + extractedData?: Record<string, unknown>; + sessionId?: string; +} + +interface EvaluatorIssue { + description: string; + severity: 'critical' | 'warning' | 'info'; +} + +interface EvaluatorResult { + role: string; + rating: 1 | 2 | 3; + confidence: 'high' | 'medium' | 'low'; + rationale: string; + strengths: string[]; + issues: EvaluatorIssue[]; +} + +interface CombinedIssue { + description: string; + severity: string; + flaggedBy: string[]; +} + +export interface PureResult { + evaluators: EvaluatorResult[]; + aggregated: { + averageRating: number; + pureScore: number; + consensus: 'unanimous' | 'majority' | 'split'; + combinedIssues: CombinedIssue[]; + summary: string; + }; +} + +// ── Validation helpers ────────────────────────── + +const VALID_RATINGS = new Set([1, 2, 3]); +const VALID_CONFIDENCE = new Set(['high', 'medium', 'low']); +const VALID_SEVERITY = new Set(['critical', 'warning', 'info']); + +function normalizeEvaluatorResult( + raw: Record<string, unknown>, + role: string, +): EvaluatorResult { + const rawRating = typeof raw.rating === 'number' ? raw.rating : 3; + const rating = VALID_RATINGS.has(rawRating) + ? (rawRating as 1 | 2 | 3) + : 3; + + const rawIssues = Array.isArray(raw.issues) ? raw.issues : []; + const issues: EvaluatorIssue[] = rawIssues + .filter( + (issue): issue is Record<string, unknown> => + typeof issue === 'object' && issue !== null, + ) + .map((issue) => ({ + description: + typeof issue.description === 'string' ? issue.description : '', + severity: VALID_SEVERITY.has(issue.severity as string) + ? (issue.severity as EvaluatorIssue['severity']) + : 'warning', + })); + + return { + role, + rating, + confidence: VALID_CONFIDENCE.has(raw.confidence as string) + ? (raw.confidence as EvaluatorResult['confidence']) + : 'medium', + rationale: typeof raw.rationale === 'string' ? raw.rationale : '', + strengths: Array.isArray(raw.strengths) + ? raw.strengths.filter((s): s is string => typeof s === 'string') + : [], + issues, + }; +} + +// ── Single Evaluator Call ────────────────────── + +async function runSingleEvaluator( + evaluator: EvaluatorPrompt, + screenshotBase64: string, + taskDescription: string, + lintContext?: string, + extractedData?: string, +): Promise<EvaluatorResult> { + const client = getAnthropicClient(); + + const userPrompt = evaluator.buildUserPrompt( + taskDescription, + lintContext, + extractedData, + ); + + const response = await client.messages.create({ + model: MODEL, + max_tokens: 1500, + system: evaluator.systemPrompt, + messages: [ + { + role: 'user', + content: [ + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/png', + data: screenshotBase64, + }, + }, + { type: 'text', text: userPrompt }, + ], + }, + ], + }); + + if (!response.content.length || response.content[0].type !== 'text') { + throw new Error(`Empty response from ${evaluator.role} evaluator`); + } + + const text = response.content[0].text; + const jsonMatch = text.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + throw new Error(`No JSON in ${evaluator.role} evaluator response`); + } + + const parsed = JSON.parse(jsonMatch[0]) as Record<string, unknown>; + return normalizeEvaluatorResult(parsed, evaluator.role); +} + +// ── Aggregation ───────────────────────────────── + +function computeConsensus( + ratings: number[], +): 'unanimous' | 'majority' | 'split' { + const unique = new Set(ratings); + if (unique.size === 1) return 'unanimous'; + + // Check if at least 2 out of 3 agree + const counts = new Map<number, number>(); + for (const r of ratings) { + counts.set(r, (counts.get(r) || 0) + 1); + } + for (const count of counts.values()) { + if (count >= 2) return 'majority'; + } + return 'split'; +} + +function deduplicateIssues(evaluators: EvaluatorResult[]): CombinedIssue[] { + const combined: CombinedIssue[] = []; + + for (const evaluator of evaluators) { + for (const issue of evaluator.issues) { + // Check if a similar issue already exists (simple substring match) + const existing = combined.find((c) => { + const aLower = c.description.toLowerCase(); + const bLower = issue.description.toLowerCase(); + // Consider issues similar if they share significant word overlap + const aWords = new Set(aLower.split(/\s+/).filter((w) => w.length > 3)); + const bWords = new Set(bLower.split(/\s+/).filter((w) => w.length > 3)); + if (aWords.size === 0 || bWords.size === 0) return false; + let overlap = 0; + for (const word of aWords) { + if (bWords.has(word)) overlap++; + } + const overlapRatio = overlap / Math.min(aWords.size, bWords.size); + return overlapRatio >= 0.5; + }); + + if (existing) { + if (!existing.flaggedBy.includes(evaluator.role)) { + existing.flaggedBy.push(evaluator.role); + } + // Escalate severity: critical > warning > info + const severityOrder = { critical: 3, warning: 2, info: 1 }; + const existingSev = + severityOrder[existing.severity as keyof typeof severityOrder] || 0; + const newSev = + severityOrder[issue.severity as keyof typeof severityOrder] || 0; + if (newSev > existingSev) { + existing.severity = issue.severity; + } + } else { + combined.push({ + description: issue.description, + severity: issue.severity, + flaggedBy: [evaluator.role], + }); + } + } + } + + // Sort: critical first, then by number of evaluators who flagged it + const severityOrder = { critical: 3, warning: 2, info: 1 }; + combined.sort((a, b) => { + const sevDiff = + (severityOrder[b.severity as keyof typeof severityOrder] || 0) - + (severityOrder[a.severity as keyof typeof severityOrder] || 0); + if (sevDiff !== 0) return sevDiff; + return b.flaggedBy.length - a.flaggedBy.length; + }); + + return combined; +} + +function buildAggregatedSummary( + evaluators: EvaluatorResult[], + avgRating: number, + consensus: string, +): string { + const ratingLabels: Record<number, string> = { + 1: 'Easy', + 2: 'Moderate', + 3: 'Difficult', + }; + + const parts: string[] = []; + + // Overall rating description + if (avgRating <= 1.33) { + parts.push( + 'The design performs well across all evaluator perspectives.', + ); + } else if (avgRating <= 2.0) { + parts.push( + 'The design is usable but has areas of friction identified by evaluators.', + ); + } else { + parts.push( + 'The design has significant usability concerns that need attention.', + ); + } + + // Per-evaluator summary + for (const ev of evaluators) { + parts.push( + `${ev.role}: rated ${ratingLabels[ev.rating] || ev.rating} (${ev.confidence} confidence).`, + ); + } + + // Consensus note + if (consensus === 'unanimous') { + parts.push('All evaluators agree on the difficulty level.'); + } else if (consensus === 'split') { + parts.push( + 'Evaluators disagree on difficulty, suggesting the design has mixed strengths and weaknesses.', + ); + } + + return parts.join(' '); +} + +// ── Main Orchestrator ────────────────────────── + +export async function runPureScoring( + req: PureRequest, +): Promise<PureResult> { + const extractedDataStr = req.extractedData + ? JSON.stringify(req.extractedData, null, 2) + : undefined; + + // Run all 3 evaluators in parallel + const results = await Promise.all( + EVALUATORS.map((evaluator) => + runSingleEvaluator( + evaluator, + req.screenshot, + req.taskDescription, + req.lintContext, + extractedDataStr, + ), + ), + ); + + // Aggregate + const ratings = results.map((r) => r.rating); + const avgRating = + Math.round((ratings.reduce((a, b) => a + b, 0) / ratings.length) * 100) / + 100; + + // Map PURE score: (3 - avgRating) / 2 * 100 + const pureScore = Math.round(((3 - avgRating) / 2) * 100); + + const consensus = computeConsensus(ratings); + const combinedIssues = deduplicateIssues(results); + const summary = buildAggregatedSummary(results, avgRating, consensus); + + return { + evaluators: results, + aggregated: { + averageRating: avgRating, + pureScore, + consensus, + combinedIssues, + summary, + }, + }; +} diff --git a/backend/src/services/responsive-validator.ts b/backend/src/services/responsive-validator.ts new file mode 100644 index 0000000..f8a9eb4 --- /dev/null +++ b/backend/src/services/responsive-validator.ts @@ -0,0 +1,115 @@ +import Anthropic from '@anthropic-ai/sdk'; +import { buildResponsiveComparisonPrompt } from '../prompts/responsive.js'; +import { getAnthropicClient, MODEL } from './claude.js'; + +// ── Types ────────────────────────────────────── + +export interface ResponsiveValidationRequest { + variants: Array<{ name: string; screenshot: string }>; + lintSummary?: string; +} + +interface ResponsiveRatingCategory { + rating: 'pass' | 'needs_improvement' | 'fail'; + issues?: Array<Record<string, string>>; + missingContent?: Array<{ breakpoint: string; description: string }>; + evidence: string[]; +} + +export interface ResponsiveValidationResult { + contentConsistency: ResponsiveRatingCategory; + layoutAdaptation: ResponsiveRatingCategory; + textReadability: ResponsiveRatingCategory; + touchTargets: ResponsiveRatingCategory; + spacingConsistency: ResponsiveRatingCategory; + recommendations: Array<{ + title: string; + description: string; + severity: string; + breakpoints: string[]; + }>; + summary: string; +} + +// ── Validation helpers ────────────────────────── + +const VALID_RATINGS = new Set(['pass', 'needs_improvement', 'fail']); + +function normalizeRatingCategory(raw: unknown): ResponsiveRatingCategory { + const obj = raw as Record<string, unknown> | undefined; + return { + rating: VALID_RATINGS.has(obj?.rating as string) + ? (obj!.rating as ResponsiveRatingCategory['rating']) + : 'fail', + issues: Array.isArray(obj?.issues) ? (obj.issues as Array<Record<string, string>>) : [], + missingContent: Array.isArray(obj?.missingContent) + ? (obj.missingContent as Array<{ breakpoint: string; description: string }>) + : undefined, + evidence: Array.isArray(obj?.evidence) ? (obj.evidence as string[]) : [], + }; +} + +// ── Main Analysis ────────────────────────────── + +export async function validateResponsiveDesign( + req: ResponsiveValidationRequest, +): Promise<ResponsiveValidationResult> { + const client = getAnthropicClient(); + + const variantLabels = req.variants.map((v) => v.name); + const lintSummary = req.lintSummary || 'No lint data provided.'; + const prompt = buildResponsiveComparisonPrompt(variantLabels, lintSummary); + + // Build content: label + screenshot per variant, then prompt + const content: Anthropic.ContentBlockParam[] = []; + + for (let i = 0; i < req.variants.length; i++) { + const variant = req.variants[i]; + content.push({ + type: 'text', + text: `--- [Breakpoint ${i + 1}: ${variant.name}] ---`, + }); + content.push({ + type: 'image', + source: { + type: 'base64', + media_type: 'image/png', + data: variant.screenshot, + }, + }); + } + + content.push({ type: 'text', text: prompt }); + + const response = await client.messages.create({ + model: MODEL, + max_tokens: 4000, + system: + 'You are a responsive design expert. You evaluate UI designs across breakpoints for consistency, adaptability, readability, and mobile-friendliness. Respond in JSON format when asked.', + messages: [{ role: 'user', content }], + }); + + if (!response.content.length || response.content[0].type !== 'text') { + throw new Error('Empty response from responsive validation'); + } + + const text = response.content[0].text; + const jsonMatch = text.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + throw new Error('No JSON in responsive validation response'); + } + + const parsed = JSON.parse(jsonMatch[0]) as Record<string, unknown>; + + return { + contentConsistency: normalizeRatingCategory(parsed.contentConsistency), + layoutAdaptation: normalizeRatingCategory(parsed.layoutAdaptation), + textReadability: normalizeRatingCategory(parsed.textReadability), + touchTargets: normalizeRatingCategory(parsed.touchTargets), + spacingConsistency: normalizeRatingCategory(parsed.spacingConsistency), + recommendations: Array.isArray(parsed.recommendations) + ? (parsed.recommendations as ResponsiveValidationResult['recommendations']) + : [], + summary: typeof parsed.summary === 'string' ? parsed.summary : '', + }; +} diff --git a/dist/code.js b/dist/code.js index 09aa8ac..f07ddca 100644 --- a/dist/code.js +++ b/dist/code.js @@ -1,4 +1,4 @@ -"use strict";(()=>{var Ds=Object.create;var Ee=Object.defineProperty,Vs=Object.defineProperties,_s=Object.getOwnPropertyDescriptor,Bs=Object.getOwnPropertyDescriptors,Us=Object.getOwnPropertyNames,Ut=Object.getOwnPropertySymbols,Gs=Object.getPrototypeOf,zt=Object.prototype.hasOwnProperty,zs=Object.prototype.propertyIsEnumerable;var Gt=(e,t,n)=>t in e?Ee(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,R=(e,t)=>{for(var n in t||(t={}))zt.call(t,n)&&Gt(e,n,t[n]);if(Ut)for(var n of Ut(t))zs.call(t,n)&&Gt(e,n,t[n]);return e},z=(e,t)=>Vs(e,Bs(t));var H=(e,t)=>()=>(e&&(t=e(e=0)),t);var Ws=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),Wt=(e,t)=>{for(var n in t)Ee(e,n,{get:t[n],enumerable:!0})},Ks=(e,t,n,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of Us(t))!zt.call(e,o)&&o!==n&&Ee(e,o,{get:()=>t[o],enumerable:!(s=_s(t,o))||s.enumerable});return e};var Hs=(e,t,n)=>(n=e!=null?Ds(Gs(e)):{},Ks(t||!e||!e.__esModule?Ee(n,"default",{value:e,enumerable:!0}):n,e));function ge(e){return["FRAME","COMPONENT","COMPONENT_SET","INSTANCE","GROUP"].includes(e.type)?(e.type==="COMPONENT_SET",!0):!1}function F(e,t,n){let s=o=>{let r=Math.round(o*255).toString(16);return r.length===1?"0"+r:r};return`#${s(e)}${s(t)}${s(n)}`}async function Ke(e){try{let t=await figma.variables.getVariableByIdAsync(e);return t?t.name:null}catch(t){return console.warn("Could not access variable:",e,t),null}}async function Kt(e,t){try{let n=await figma.variables.getVariableByIdAsync(e);if(!n)return null;if(t&&n.resolveForConsumer)try{let s=n.resolveForConsumer(t);if(s&&typeof s.value=="object"&&"r"in s.value){let o=s.value;return F(o.r,o.g,o.b)}else if(s&&s.value!==void 0)return String(s.value)}catch(s){console.warn("Could not resolve variable value:",s)}return n.name}catch(n){return console.warn("Could not access variable:",e,n),null}}function N(e,t){try{figma.ui.postMessage({type:e,data:t})}catch(n){console.error("Failed to send message to UI:",n)}}function Te(e){let t=[e];if("children"in e)for(let n of e.children)t.push(...Te(n));return t}function He(e){let t=[];if(e.type==="TEXT"){let n=e;n.characters&&t.push(n.characters)}if("children"in e)for(let n of e.children)t.push(...He(n));return t}function te(e,t,n){let[s,o,r]=[e,t,n].map(i=>i<=.03928?i/12.92:Math.pow((i+.055)/1.055,2.4));return .2126*s+.7152*o+.0722*r}function ye(e,t){let n=Math.max(e,t),s=Math.min(e,t);return(n+.05)/(s+.05)}function he(e){let t=e.parent;for(;t&&"type"in t;){let n=t;if("fills"in n){let s=n.fills;if(Array.isArray(s)){for(let o of s)if(o.type==="SOLID"&&o.visible!==!1&&o.color){if(o.boundVariables&&o.boundVariables.color)continue;return o.color}}}t=t.parent}return null}function js(e){var s;let t=[],n=e;for(;n&&n.type!=="DOCUMENT"&&n.type!=="PAGE";)n.type==="COMPONENT"&&((s=n.parent)==null?void 0:s.type)==="COMPONENT_SET"?t.unshift(`${n.name}`):t.unshift(n.name),n=n.parent;return t.join(" \u2192 ")}function oe(e){var s,o;let t=js(e),n=`Found in "${e.name}"`;if(((s=e.parent)==null?void 0:s.type)==="COMPONENT_SET"||e.parent&&((o=e.parent.parent)==null?void 0:o.type)==="COMPONENT_SET")n=`Found in variant: "${e.name}"`;else if(t.includes("\u2192")){let r=t.split(" \u2192 ");r.length>1&&(n=`Found in "${r[r.length-1]}" (${r[r.length-2]})`)}return{path:t,description:n}}var J=H(()=>{"use strict"});function Re(e,t=ve){if(t.includes(e))return[];let n=[...t].map(o=>({v:o,diff:Math.abs(o-e)})).sort((o,r)=>o.diff-r.diff),s=[];for(let o of n){if(s.length>=2)break;s.includes(o.v)||s.push(o.v)}return s.sort((o,r)=>o-r)}var ve,it,at=H(()=>{"use strict";ve=[0,2,4,8,12,16,20,24,32,40,48,64,80,96],it=ve});function fo(){return`spacing-${++sn}`}function go(e){return ct.includes(e)}function yo(e){return{itemSpacing:"Gap",paddingTop:"Padding Top",paddingBottom:"Padding Bottom",paddingLeft:"Padding Left",paddingRight:"Padding Right",counterAxisSpacing:"Counter-axis Gap"}[e]||e}function ho(e,t){var o;if(e.layoutMode==="NONE")return 0;let n=0,s=[{prop:"itemSpacing",value:e.itemSpacing},{prop:"paddingTop",value:e.paddingTop},{prop:"paddingBottom",value:e.paddingBottom},{prop:"paddingLeft",value:e.paddingLeft},{prop:"paddingRight",value:e.paddingRight}];"counterAxisSpacing"in e&&typeof e.counterAxisSpacing=="number"&&s.push({prop:"counterAxisSpacing",value:e.counterAxisSpacing});for(let{prop:r,value:i}of s)if(n++,!go(i)){let a=Re(i,ct);t.push({id:fo(),type:"spacing",severity:"warning",nodeId:e.id,nodeName:e.name,message:`${yo(r)} is ${i}px \u2014 not in spacing scale`,currentValue:`${i}px`,suggestions:a.map(c=>`${c}px`),autoFixable:!0,fixAction:{type:"fixSpacing",params:{nodeId:e.id,property:r,currentValue:i,suggestedValue:(o=a[0])!=null?o:i}}})}return n}function on(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,passed:0};if(s&&i)return{checked:0,passed:0};let a=0,c=0;if(e.type==="FRAME"||e.type==="COMPONENT"||e.type==="INSTANCE"){let l=t.length,d=ho(e,t);a+=d,c+=d-(t.length-l)}if("children"in e)for(let l of e.children){let d=on(l,t,n,s,r);a+=d.checked,c+=d.passed}return{checked:a,passed:c}}function rn(e,t={}){let{skipLocked:n=!0,skipHidden:s=!0,scale:o}=t;ct=o||ve,sn=0;let r=[],i=0,a=0;for(let c of e){let{checked:l,passed:d}=on(c,r,n,s,!1);i+=l,a+=d}return{issues:r,summary:{totalChecked:i,passed:a,failed:r.length}}}var sn,ct,an=H(()=>{"use strict";at();sn=0;ct=ve});function bo(){return`autolayout-${++cn}`}function ln(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{totalFrames:0,withAutoLayout:0};if(s&&i)return{totalFrames:0,withAutoLayout:0};let a=0,c=0;if((e.type==="FRAME"||e.type==="COMPONENT"||e.type==="INSTANCE")&&"children"in e){let d=e.children;d.length>=2&&(a++,e.layoutMode!=="NONE"?c++:t.push({id:bo(),type:"autoLayout",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Frame "${e.name}" has ${d.length} children but no Auto Layout`,currentValue:"No Auto Layout",suggestions:["HORIZONTAL","VERTICAL"],autoFixable:!1}))}if("children"in e)for(let d of e.children){let p=ln(d,t,n,s,r);a+=p.totalFrames,c+=p.withAutoLayout}return{totalFrames:a,withAutoLayout:c}}function dn(e,t={}){let{skipLocked:n=!0,skipHidden:s=!0}=t;cn=0;let o=[],r=0,i=0;for(let l of e){let d=ln(l,o,n,s,!1);r+=d.totalFrames,i+=d.withAutoLayout}let a=r-i,c=r>0?Math.round(i/r*100):100;return{issues:o,summary:{totalFrames:r,withAutoLayout:i,withoutAutoLayout:a,percentage:c}}}var cn,un=H(()=>{"use strict";cn=0});function ne(){return`a11y-${++pn}`}function lt(e){return vo.test(e)}function No(e,t){if(e.type!=="TEXT")return;let n=e,s=n.fills;if(s===figma.mixed||!Array.isArray(s))return;let o=s.find(m=>{var v;return m.type==="SOLID"&&m.visible!==!1&&m.color&&!((v=m.boundVariables)!=null&&v.color)});if(!o||o.type!=="SOLID")return;let r=he(e);if(!r)return;let i=o.color,a=te(i.r,i.g,i.b),c=te(r.r,r.g,r.b),l=ye(a,c),d=n.fontSize!==figma.mixed?n.fontSize:0,p=n.fontName!==figma.mixed?n.fontName.style:"",u=p.toLowerCase().includes("bold")||p.toLowerCase().includes("black"),f=d>=18||d>=14&&u,y=f?3:4.5;if(l<y){let m=l.toFixed(1);t.push({id:ne(),type:"accessibility",severity:"critical",nodeId:e.id,nodeName:e.name,message:`Contrast ratio ${m}:1 below WCAG AA ${f?"large text":""} minimum of ${y}:1`,currentValue:`${m}:1`,suggestions:[`Increase contrast to at least ${y}:1`],autoFixable:!1})}}function wo(e,t){if(e.type!=="FRAME"&&e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!lt(e.name))return;let n=e.width,s=e.height;(n<44||s<44)&&t.push({id:ne(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Touch target ${Math.round(n)}x${Math.round(s)}px is below 44x44px recommended minimum`,currentValue:`${Math.round(n)}x${Math.round(s)}px`,suggestions:["Increase to at least 44x44px"],autoFixable:!1})}function ko(e,t){if(e.type!=="TEXT")return;let s=e.fontSize;s===figma.mixed||typeof s!="number"||s>0&&s<12&&t.push({id:ne(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Text size ${s}px is below 12px readability minimum`,currentValue:`${s}px`,suggestions:["12px","14px"],autoFixable:!1})}function xo(e,t){if(e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!lt(e.name))return;let n=e;if(!("children"in n)||n.children.length===0)return;n.children.some(o=>{var r,i;return o.type==="TEXT"?!0:"children"in o?(i=(r=o.children)==null?void 0:r.some)==null?void 0:i.call(r,a=>a.type==="TEXT"):!1})||t.push({id:ne(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Interactive element "${e.name}" has no visible text label`,currentValue:"No text child",suggestions:["Add a text label or ensure screen reader label is provided"],autoFixable:!1})}function Co(e,t){e.type!=="FRAME"&&e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!("children"in e)||e.children.length===0||So.test(e.name)&&t.push({id:ne(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`Generic layer name "${e.name}" \u2014 use a descriptive name`,currentValue:e.name,suggestions:["Rename to describe the layer purpose"],autoFixable:!1})}function Io(e,t){if(e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!lt(e.name))return;let n=e,s=null,o=n.strokes;if(Array.isArray(o)){let l=o.find(d=>d.type==="SOLID"&&d.visible!==!1);l&&l.type==="SOLID"&&(s=l.color)}if(!s){let l=n.fills;if(l!==figma.mixed&&Array.isArray(l)){let d=l.find(p=>p.type==="SOLID"&&p.visible!==!1);d&&d.type==="SOLID"&&(s=d.color)}}if(!s)return;let r=he(e);if(!r)return;let i=te(s.r,s.g,s.b),a=te(r.r,r.g,r.b),c=ye(i,a);c<3&&t.push({id:ne(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Non-text contrast ${c.toFixed(1)}:1 below WCAG 1.4.11 minimum of 3:1`,currentValue:`${c.toFixed(1)}:1`,suggestions:["Increase boundary contrast to at least 3:1 against background"],autoFixable:!1})}function Eo(e,t){if(e.type!=="FRAME"&&e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!Ao.test(e.name))return;let n=e;if(!("children"in n)||n.children.length===0)return;n.children.some(o=>{var r,i;return o.type==="TEXT"||/icon|svg|symbol|glyph/i.test(o.name)?!0:"children"in o?(i=(r=o.children)==null?void 0:r.some)==null?void 0:i.call(r,a=>a.type==="TEXT"||/icon|svg|symbol|glyph/i.test(a.name)):!1})||t.push({id:ne(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`"${e.name}" may rely on color alone to convey status (WCAG 1.4.1)`,currentValue:"No text or icon indicator",suggestions:["Add a text label or icon to supplement the color indicator"],autoFixable:!1})}function To(e,t){if(e.type!=="COMPONENT")return;let n=e.parent;if(!n||n.type!=="COMPONENT_SET")return;let s=n,r=s.children.map(c=>c.name.toLowerCase()).join(" "),a=["hover","focus","disabled","pressed"].filter(c=>!r.includes(c));a.length>0&&t.push({id:ne(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`Component set missing states: ${a.join(", ")}`,currentValue:`${s.children.length} variants`,suggestions:a.map(c=>`Add ${c} variant`),autoFixable:!1})}function mn(e,t,n,s,o,r){var l;let i=o||"locked"in e&&e.locked,a="visible"in e&&!e.visible;if(n&&i||s&&a)return 0;let c=0;if(No(e,t),wo(e,t),ko(e,t),c++,xo(e,t),Co(e,t),Io(e,t),Eo(e,t),e.type==="COMPONENT"&&((l=e.parent)==null?void 0:l.type)==="COMPONENT_SET"){let d=e.parent.id;r.has(d)||(r.add(d),To(e,t))}if("children"in e)for(let d of e.children)c+=mn(d,t,n,s,i,r);return c}function fn(e,t={}){let{skipLocked:n=!0,skipHidden:s=!0}=t;pn=0;let o=[],r=new Set,i=0;for(let a of e)i+=mn(a,o,n,s,!1,r);return{issues:o,summary:{totalChecked:i,contrastIssues:o.filter(a=>a.message.includes("Contrast")).length,touchTargetIssues:o.filter(a=>a.message.includes("Touch target")).length,textSizeIssues:o.filter(a=>a.message.includes("Text size")).length,namingIssues:o.filter(a=>a.message.includes("text label")||a.message.includes("Generic")).length,stateIssues:o.filter(a=>a.message.includes("missing states")).length,nonTextContrastIssues:o.filter(a=>a.message.includes("Non-text contrast")).length,colorOnlyIssues:o.filter(a=>a.message.includes("color alone")).length}}}var pn,vo,So,Ao,gn=H(()=>{"use strict";J();pn=0;vo=/\b(button|btn|input|link|checkbox|toggle|switch|tab|radio|select|dropdown|menu-item|slider|chip)\b/i;So=/^(Frame|Group|Rectangle|Ellipse|Vector|Line|Polygon|Star)\s*\d+$/i;Ao=/\b(error|success|warning|status|alert|badge|danger|info)\b/i});function pe(){return`vq-${++yn}`}function Lo(e){return Po.some(t=>t.includes(e))}function Ro(e,t){let n=e.width*e.height;if(n===0)return;let o=("children"in e?e.children.filter(i=>i.visible!==!1):[]).length,r=o/n*1e3;r>3&&t.push({id:pe(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`High visual density: ${o} elements in ${Math.round(n/1e3)}k px\xB2 (${r.toFixed(2)}/1000px\xB2). Consider simplifying or using progressive disclosure.`,currentValue:`${r.toFixed(2)} elements/1000px\xB2`,suggestions:["Reduce visible elements to under 15 per viewport","Group related items","Use progressive disclosure"],autoFixable:!1})}function hn(e,t,n,s,o){if(!(s&&"locked"in e&&e.locked)&&!(o&&"visible"in e&&!e.visible)){if(e.type==="TEXT"){let r=e,i=r.fontSize;if(i!==figma.mixed&&typeof i=="number"){t.add(i);let a=r.lineHeight;if(a!==figma.mixed&&typeof a=="object"&&a.unit==="PIXELS"){let c=a.value/i;n.push({fontSize:i,lineHeight:a.value,ratio:c})}}}if("children"in e)for(let r of e.children)hn(r,t,n,s,o)}}function $o(e,t,n,s){let o=new Set,r=[];hn(e,o,r,n,s);let i=Array.from(o).sort((l,d)=>l-d),a=i.filter(l=>!Lo(l));a.length>0&&t.push({id:pe(),type:"accessibility",severity:"info",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`Non-standard font sizes: ${a.join(", ")}px. Consider using a type scale (e.g., 12/14/16/20/24/32).`,currentValue:a.map(l=>`${l}px`).join(", "),suggestions:a.map(l=>{let d=[10,12,14,16,18,20,24,28,32,36,40,48].reduce((p,u)=>Math.abs(u-l)<Math.abs(p-l)?u:p);return`${l}px \u2192 ${d}px`}),autoFixable:!1});let c=r.filter(l=>l.ratio<1.2||l.ratio>2);if(c.length>0){let l=c.reduce((d,p)=>Math.abs(p.ratio-1.5)>Math.abs(d.ratio-1.5)?p:d);t.push({id:pe(),type:"accessibility",severity:"info",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`Line height ratio ${l.ratio.toFixed(2)} (${l.lineHeight}px / ${l.fontSize}px) is outside optimal range 1.3\u20131.6.`,currentValue:`${l.ratio.toFixed(2)}`,suggestions:[`Set line height to ${Math.round(l.fontSize*1.5)}px (1.5\xD7 body) or ${Math.round(l.fontSize*1.3)}px (1.3\xD7 headings)`],autoFixable:!1})}return{sizes:i,lineHeightData:r}}function bn(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if("fills"in e){let o=e.fills;if(o!==figma.mixed&&Array.isArray(o))for(let r of o)r.visible!==!1&&r.type==="SOLID"&&t.add(F(r.color.r,r.color.g,r.color.b))}if("strokes"in e){let o=e.strokes;if(Array.isArray(o))for(let r of o)r.visible!==!1&&r.type==="SOLID"&&t.add(F(r.color.r,r.color.g,r.color.b))}if("children"in e)for(let o of e.children)bn(o,t,n,s)}}function Mo(e,t,n,s){let o=new Set;bn(e,o,n,s);let r=Array.from(o);return r.length>8&&t.push({id:pe(),type:"accessibility",severity:"warning",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`${r.length} unique colors detected. A cohesive palette typically uses 5\u20137 colors (primary, secondary, accent, neutrals).`,currentValue:`${r.length} colors`,suggestions:["Consolidate similar colors into design tokens","Limit palette to primary, secondary, accent, and 2-3 neutrals"],autoFixable:!1}),r}function Oo(e,t,n=4){if(!("children"in e))return 0;let s=e.children.filter(r=>r.visible!==!1),o=0;for(let r of s){if(!("x"in r)||!("y"in r))continue;let i=r.x,a=r.y,c=Math.round(i)%n,l=Math.round(a)%n;(c!==0||l!==0)&&o++}return o>0&&o/Math.max(s.length,1)>.3&&t.push({id:pe(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`${o}/${s.length} direct children are misaligned from ${n}px grid.`,currentValue:`${o} misaligned`,suggestions:[`Snap elements to ${n}px grid for visual consistency`],autoFixable:!1}),o}function vn(e,t,n,s){if(n&&"locked"in e&&e.locked||s&&"visible"in e&&!e.visible)return;if(/button|btn|cta/i.test(e.name)&&"width"in e&&"height"in e&&t.push({nodeId:e.id,nodeName:e.name,width:e.width,height:e.height}),"children"in e)for(let r of e.children)vn(r,t,n,s)}function Fo(e,t,n,s){let o=[];if(vn(e,o,n,s),o.length<2)return;let r=o.map(l=>l.height),i=r.reduce((l,d)=>l+d,0)/r.length,c=Math.max(...r.map(l=>Math.abs(l-i)))/i*100;if(c>15){let l=Math.min(...r),d=Math.max(...r);t.push({id:pe(),type:"accessibility",severity:"warning",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`Button height inconsistency: ${l}px to ${d}px (${Math.round(c)}% variance). Standardize to 2-3 size tiers.`,currentValue:`${l}\u2013${d}px`,suggestions:["Use consistent button heights: 32px (small), 40px (medium), 48px (large)"],autoFixable:!1})}}function Sn(e,t={}){var f,y;yn=0;let n=[],s=(f=t.skipLocked)!=null?f:!0,o=(y=t.skipHidden)!=null?y:!0,r=0,i=[],a=[],c=[],l=0,d=0,p=0;for(let m of e){"children"in m&&"width"in m&&"height"in m&&(Ro(m,n),d+=m.children.length,p+=m.width*m.height,r++);let v=$o(m,n,s,o);i=[...new Set([...i,...v.sizes])],a=[...a,...v.lineHeightData],r++;let A=Mo(m,n,s,o);c=[...new Set([...c,...A])],r++,"children"in m&&(l+=Oo(m,n),r++),Fo(m,n,s,o),r++}let u=p>0?d/p*1e3:0;return{issues:n,metrics:{childCount:d,areaPx:p,density:u,uniqueFontSizes:i,lineHeightRatios:a,uniqueColors:c,misalignedCount:l},summary:{totalChecked:r,passed:r-n.length,failed:n.length}}}var yn,Po,Nn=H(()=>{"use strict";J();yn=0;Po=[[10,12,14,16,18,20,24,28,32,36,40,48,56,64,72],[12,14,16,20,24,32,40,48],[12,14,16,18,21,24,30,36,48,60,72]]});function ee(){return`mc-${++wn}`}function kn(e){if(dt.test(e.name))return!0;let t=e.parent,n=0;for(;t&&n<4;){if("name"in t&&dt.test(t.name)||"type"in t&&(t.type==="COMPONENT"||t.type==="INSTANCE")&&"name"in t&&dt.test(t.name))return!0;t=t.parent,n++}return!1}function xn(e){return e.trim().split(/\s+/).filter(Boolean).length}function Wo(e,t){let n=e.characters;if(!n||n.trim().length===0){t.push({id:ee(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:"Empty text node \u2014 remove or add content.",currentValue:"(empty)",autoFixable:!1});return}let s=n.trim(),o=xn(s),r=kn(e);if((Do.test(s)||_o.test(s))&&t.push({id:ee(),type:"naming",severity:"warning",nodeId:e.id,nodeName:e.name,message:`"${s.substring(0,40)}" \u2014 avoid "click/tap here". Use descriptive action: "Download report", "View details".`,currentValue:s.substring(0,60),suggestions:['Use verb + object: "Download PDF", "View pricing", "Start trial"'],autoFixable:!1}),Vo.test(s)&&t.push({id:ee(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:'"Learn more" is vague \u2014 specify what the user will learn: "Learn more about pricing".',currentValue:s,suggestions:['Add specificity: "Learn more about [topic]"'],autoFixable:!1}),r&&o<=2){let i=s.toLowerCase().replace(/[.!]/g,"");Bo.has(i)&&t.push({id:ee(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`Generic CTA "${s}" \u2014 use a specific action: "Save changes", "Send message", "Create account".`,currentValue:s,suggestions:["Replace with verb + object describing the outcome"],autoFixable:!1})}if(r&&o>5&&t.push({id:ee(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`CTA too long (${o} words): "${s.substring(0,50)}\u2026". Keep CTAs to 2\u20135 words.`,currentValue:`${o} words`,suggestions:["Shorten to verb + object (2-5 words)"],autoFixable:!1}),(Uo.test(s)||Go.test(s))&&t.push({id:ee(),type:"naming",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Placeholder text detected: "${s.substring(0,40)}\u2026". Replace with real content.`,currentValue:s.substring(0,60),suggestions:["Replace with actual copy or realistic sample data"],autoFixable:!1}),o>80&&!r&&t.push({id:ee(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`Long text block (${o} words). Break into shorter paragraphs or use bullet points for readability.`,currentValue:`${o} words`,suggestions:["Break into paragraphs of \u226450 words","Use bullet points for lists","Add subheadings"],autoFixable:!1}),s===s.toUpperCase()&&s!==s.toLowerCase()&&o>3&&t.push({id:ee(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`All-caps text with ${o} words: "${s.substring(0,40)}\u2026". ALL CAPS reduces readability \u2014 use sentence case or title case.`,currentValue:s.substring(0,60),suggestions:["Use sentence case for readability","Reserve ALL CAPS for short labels (1-2 words)"],autoFixable:!1}),zo.test(s)){let a=(s.match(/\b\d{4,}\b/g)||[]).filter(c=>{let l=parseInt(c,10);return l<1900||l>2099});a.length>0&&t.push({id:ee(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`Unformatted number${a.length>1?"s":""}: ${a.join(", ")}. Use thousand separators for readability.`,currentValue:a.join(", "),suggestions:["Format as 1,000,000 or 1 000 000"],autoFixable:!1})}}function Cn(e,t,n,s,o){var r;if(!(s&&"locked"in e&&e.locked)&&!(o&&"visible"in e&&!e.visible)){if(e.type==="TEXT"){n.totalTextNodes++;let i=((r=e.characters)==null?void 0:r.trim())||"",a=xn(i);a>0&&(n.wordCounts.push(a),a>n.longestParagraph&&(n.longestParagraph=a)),kn(e)&&n.ctaNodes++,Wo(e,t)}if("children"in e)for(let i of e.children)Cn(i,t,n,s,o)}}function In(e,t={}){var a,c;wn=0;let n=[],s=(a=t.skipLocked)!=null?a:!0,o=(c=t.skipHidden)!=null?c:!0,r={totalTextNodes:0,ctaNodes:0,wordCounts:[],longestParagraph:0};for(let l of e)Cn(l,n,r,s,o);let i=r.wordCounts.length>0?r.wordCounts.reduce((l,d)=>l+d,0)/r.wordCounts.length:0;return{issues:n,metrics:{totalTextNodes:r.totalTextNodes,ctaNodes:r.ctaNodes,avgWordCount:Math.round(i*10)/10,longestParagraph:r.longestParagraph},summary:{totalChecked:r.totalTextNodes,passed:r.totalTextNodes-n.length,failed:n.length}}}var wn,Do,Vo,_o,Bo,Uo,Go,zo,dt,An=H(()=>{"use strict";wn=0;Do=/\bclick\s+here\b/i,Vo=/^learn\s+more\.?$/i,_o=/\btap\s+here\b/i,Bo=new Set(["submit","ok","okay","next","continue","go","yes","no","done","send","save","apply"]),Uo=/\blorem\s+ipsum\b/i,Go=/^(enter\s+text|type\s+here|placeholder|sample\s+text|your\s+text|add\s+text)\.?$/i,zo=/\b\d{4,}\b/,dt=/button|btn|cta|action|submit|link/i});function me(){return`conv-${++Ln}`}function jo(e){if(En.test(e.name))return!0;let t=e.parent,n=0;for(;t&&n<3;){if("name"in t&&En.test(t.name))return!0;t=t.parent,n++}return!1}function qo(e){return Rn.test(e.name)}function Tn(e){if(!("fills"in e))return null;let t=e.fills;if(t===figma.mixed||!Array.isArray(t))return null;let n=t.find(s=>s.type==="SOLID"&&s.visible!==!1);return n?n.color:null}function Pn(e,t,n){let s=[e,t,n].map(o=>o<=.03928?o/12.92:Math.pow((o+.055)/1.055,2.4));return .2126*s[0]+.7152*s[1]+.0722*s[2]}function Jo(e,t){let n=Math.max(e,t),s=Math.min(e,t);return(n+.05)/(s+.05)}function $n(e,t,n,s,o){if(!(s&&"locked"in e&&e.locked)&&!(o&&"visible"in e&&!e.visible)&&(jo(e)&&"width"in e&&"height"in e&&t.push({node:e,x:"x"in e?e.x:0,y:"y"in e?e.y:0,width:e.width,height:e.height,absoluteY:n+("y"in e?e.y:0)}),"children"in e)){let r=n+("y"in e?e.y:0);for(let i of e.children)$n(i,t,r,s,o)}}function Mn(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if(qo(e)){let o=!1,r=e.parent;if(r&&"children"in r){for(let i of r.children)if(i.type==="TEXT"&&i.id!==e.id){o=!0;break}}t.push({node:e,hasLabel:o})}if("children"in e)for(let o of e.children)Mn(o,t,n,s)}}function $e(e,t,n,s){if(n&&"locked"in e&&e.locked||s&&"visible"in e&&!e.visible)return!1;if(t.test(e.name))return!0;if("children"in e){for(let o of e.children)if($e(o,t,n,s))return!0}return!1}function Xo(e,t,n){if(t.length===0||!("height"in e))return!1;let s=e.height*.7,o=t.some(r=>r.y+r.height<s);return o||n.push({id:me(),type:"accessibility",severity:"warning",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:"No primary CTA visible above the fold (top 70% of frame). Move the main action higher for better conversion.",currentValue:`CTA at ${Math.round(t[0].y)}px, fold at ${Math.round(s)}px`,suggestions:["Place primary CTA within top 70% of the viewport","Add a secondary CTA near the top if main CTA must stay below"],autoFixable:!1}),o}function Yo(e,t,n){let s=Tn(e);if(!s)return;let o=Pn(s.r,s.g,s.b);for(let r of t){let i=Tn(r.node);if(!i)continue;let a=Pn(i.r,i.g,i.b),c=Jo(o,a);if(c<3){let l=F(i.r,i.g,i.b),d=F(s.r,s.g,s.b);n.push({id:me(),type:"accessibility",severity:"warning",nodeId:r.node.id,nodeName:r.node.name,message:`CTA contrast ratio ${c.toFixed(1)}:1 (${l} on ${d}) \u2014 too low. CTAs should stand out with \u22653:1 contrast against background.`,currentValue:`${c.toFixed(1)}:1`,suggestions:["Increase CTA background contrast to at least 3:1","Use a bolder accent color for the primary action"],autoFixable:!1})}}}function Qo(e,t,n){t.length>5&&n.push({id:me(),type:"accessibility",severity:"warning",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`${t.length} form fields on one screen. More than 5 fields increases abandonment \u2014 consider splitting into steps or removing optional fields.`,currentValue:`${t.length} fields`,suggestions:["Split into multi-step form with progress indicator",'Remove optional fields or move to "Advanced" section',"Expedia gained $12M/year by removing one field"],autoFixable:!1});let s=t.filter(o=>!o.hasLabel);s.length>0&&n.push({id:me(),type:"accessibility",severity:"warning",nodeId:s[0].node.id,nodeName:s[0].node.name,message:`${s.length} form field${s.length===1?"":"s"} without visible labels. Labels improve completion rate and accessibility.`,currentValue:`${s.length} unlabeled`,suggestions:["Add visible label text above or beside each input","Don't rely on placeholder text alone as labels"],autoFixable:!1})}function Zo(e,t,n,s,o){if(t.length<=3)return!1;let r=$e(e,Ko,s,o);return!r&&t.length>5&&n.push({id:me(),type:"accessibility",severity:"info",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:"Long form without progress indicator. A step counter or progress bar reduces perceived effort.",suggestions:['Add "Step 1 of 3" or a progress bar',"Show users how far they've come and what's left"],autoFixable:!1}),r}function er(e,t,n,s,o){if(t.length===0||!$e(e,Rn,s,o))return;$e(e,Ho,s,o)||n.push({id:me(),type:"accessibility",severity:"info",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:"Form with CTA but no trust signals (security badges, reviews, guarantees). Trust elements near CTAs increase conversion.",suggestions:["Add security badge or lock icon near submit button","Show testimonials, ratings, or guarantees near the CTA"],autoFixable:!1})}function On(e,t={}){var d,p;Ln=0;let n=[],s=(d=t.skipLocked)!=null?d:!0,o=(p=t.skipHidden)!=null?p:!0,r=0,i=0,a=!1,c=!1,l=0;for(let u of e){let f=[];$n(u,f,0,s,o),r+=f.length;let y=[];Mn(u,y,s,o),i+=y.length,f.length>0&&(Xo(u,f,n)&&(a=!0),Yo(u,f,n),l+=2),y.length>0&&(Qo(u,y,n),Zo(u,y,n,s,o)&&(c=!0),l+=2),er(u,f,n,s,o),l++}return{issues:n,metrics:{ctaCount:r,formFieldCount:i,ctaAboveFold:a,hasProgressIndicator:c},summary:{totalChecked:l,passed:l-n.length,failed:n.length}}}var Ln,En,Rn,Ko,Ho,Fn=H(()=>{"use strict";J();Ln=0;En=/button|btn|cta|action|submit|primary/i,Rn=/input|field|text.?area|select|dropdown|picker|combo|search|email|password|phone|number.?field/i,Ko=/progress|step|stepper|breadcrumb|wizard|indicator|pagination/i,Ho=/badge|trust|security|lock|shield|guarantee|verified|secure|ssl|certification|review|rating|star/i});function Se(){return`cog-${++Vn}`}function rr(e){return tr.test(e.name)}function ir(e){return nr.test(e.name)}function Bn(e){return _n.test(e.name)}function ar(e){return sr.test(e.name)}function cr(e){return or.test(e.name)}function lr(e){if(!Dn.test(e.name)&&!_n.test(e.name)||!("children"in e))return!1;let t=e.children,n=t.some(o=>o.type==="TEXT");return t.some(o=>o.type==="VECTOR"||o.type==="BOOLEAN_OPERATION"||Dn.test(o.name))&&!n}function Un(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if(ir(e)){t.push(e);return}if("children"in e)for(let o of e.children)Un(o,t,n,s)}}function Gn(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)&&(Bn(e)&&t.push(e),"children"in e))for(let o of e.children)Gn(o,t,n,s)}function dr(e){let t=e.match(/h(\d)/i);return t?parseInt(t[1],10):/title|headline/i.test(e)?1:/subtitle|subhead/i.test(e)||/heading/i.test(e)?2:null}function zn(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if(ar(e)){let o=dr(e.name);o!==null&&t.push({node:e,level:o})}if("children"in e)for(let o of e.children)zn(o,t,n,s)}}function Wn(e,t,n,s){if(n&&"locked"in e&&e.locked||s&&"visible"in e&&!e.visible)return t;let o=t;if("children"in e)for(let r of e.children){let i=Wn(r,t+1,n,s);i>o&&(o=i)}return o}function Kn(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)&&(cr(e)&&"opacity"in e&&e.opacity<1&&t.push(e),"children"in e))for(let o of e.children)Kn(o,t,n,s)}function Hn(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)&&(Bn(e)&&lr(e)&&t.push(e),"children"in e))for(let o of e.children)Hn(o,t,n,s)}function ur(e,t,n,s){let o=[];jn(e,o,n,s);let r=0;for(let i of o){let a=[];Un(i,a,n,s),r+=a.length,a.length>7&&t.push({id:Se(),type:"accessibility",severity:"warning",nodeId:i.id,nodeName:i.name,message:`Navigation has ${a.length} items \u2014 Miller's Law suggests 7\xB12 is the working memory limit. Consider grouping or progressive disclosure.`,currentValue:`${a.length} nav items`,suggestions:["Group related items under expandable sections",'Use "More" menu for less-used items',"Limit primary navigation to 5-7 items"],autoFixable:!1})}return r}function jn(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if(rr(e)){t.push(e);return}if("children"in e)for(let o of e.children)jn(o,t,n,s)}}function pr(e,t,n,s){let o=[];return Gn(e,o,n,s),o.length>5&&t.push({id:Se(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`${o.length} CTAs/buttons on one screen \u2014 choice overload reduces decision-making ability (Hick's Law). Prioritize one primary action.`,currentValue:`${o.length} CTAs`,suggestions:["Establish clear primary/secondary/tertiary action hierarchy","Reduce to 1 primary CTA per viewport","Group related actions in a dropdown or overflow menu"],autoFixable:!1}),o.length}function mr(e,t,n,s){let o=[];if(zn(e,o,n,s),o.length<2)return o.map(i=>i.level);let r=o.sort((i,a)=>{let c="y"in i.node?i.node.y:0,l="y"in a.node?a.node.y:0;return c-l});for(let i=1;i<r.length;i++){let a=r[i-1].level,c=r[i].level;c>a+1&&t.push({id:Se(),type:"accessibility",severity:"info",nodeId:r[i].node.id,nodeName:r[i].node.name,message:`Heading hierarchy gap: jumps from level ${a} to level ${c}. Screen readers and users rely on sequential heading structure.`,currentValue:`h${a} \u2192 h${c}`,suggestions:[`Add an h${a+1} between these levels`,"Ensure headings follow a logical descending order"],autoFixable:!1})}return r.map(i=>i.level)}function fr(e,t,n,s){var r;let o=[];Kn(e,o,n,s);for(let i of o){let a=i.parent,c=!1;if(a&&"children"in a){for(let l of a.children)if(l.type==="TEXT"&&l.id!==i.id){let d=((r=l.characters)==null?void 0:r.toLowerCase())||"";if(d.includes("required")||d.includes("complete")||d.includes("fill")||d.includes("select")||d.includes("first")){c=!0;break}}}c||t.push({id:Se(),type:"accessibility",severity:"info",nodeId:i.id,nodeName:i.name,message:`Disabled element "${i.name}" without visible explanation. Users should understand WHY an action is unavailable and how to enable it.`,suggestions:["Add helper text explaining what needs to happen first","Use a tooltip on hover explaining the disabled state",'Show a brief inline message (e.g., "Complete all fields to continue")'],autoFixable:!1})}}function gr(e,t,n,s){let o=[];return Hn(e,o,n,s),o.length>3&&t.push({id:Se(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`${o.length} icon-only buttons without text labels. Icons alone are ambiguous \u2014 add labels or ensure tooltips are present.`,currentValue:`${o.length} icon-only`,suggestions:["Add visible text labels to icon buttons","Add tooltips that appear on hover/focus","Use aria-label for accessibility (ensure design indicates this)"],autoFixable:!1}),o.length}function qn(e,t={}){var p,u;Vn=0;let n=[],s=(p=t.skipLocked)!=null?p:!0,o=(u=t.skipHidden)!=null?u:!0,r=0,i=0,a=0,c=[],l=0,d=0;for(let f of e){r+=ur(f,n,s,o),d++,i+=pr(f,n,s,o),d++;let y=mr(f,n,s,o);c=[...c,...y],d++,fr(f,n,s,o),d++,l+=gr(f,n,s,o),d++;let m=Wn(f,0,s,o);m>a&&(a=m)}return{issues:n,metrics:{navItemCount:r,ctaCount:i,maxNestingDepth:a,headingLevels:[...new Set(c)].sort(),iconOnlyButtons:l},summary:{totalChecked:d,passed:d-n.length,failed:n.length}}}var Vn,tr,nr,_n,sr,or,Dn,Jn=H(()=>{"use strict";Vn=0;tr=/nav|menu|sidebar|tab.?bar|bottom.?bar|header.?nav|navigation|top.?bar/i,nr=/nav.?item|menu.?item|tab(?!le)|link/i,_n=/button|btn|cta|action|submit|primary/i,sr=/heading|title|h[1-6]|headline/i,or=/disabled|inactive|dimmed|greyed/i,Dn=/icon|ico|svg|glyph/i});var Zn={};Wt(Zn,{DEFAULT_LINT_SETTINGS:()=>X,clearIgnored:()=>yt,findNodesWithSameValue:()=>vt,getIgnoredState:()=>ht,ignoreAllOfType:()=>gt,ignoreError:()=>ft,ignoreNode:()=>mt,lintSelection:()=>we,restoreIgnoredState:()=>bt,runDesignLint:()=>Ne});function K(e,t,n){return n?`${e}::${t}::${n}`:`${e}::${t}`}function mt(e){W.add(e)}function ft(e,t,n){_.add(K(e,t,n))}function gt(e,t){for(let n of e)n.errorType===t&&_.add(K(n.nodeId,n.errorType))}function yt(){W.clear(),_.clear()}function ht(){return{nodeIds:Array.from(W),errorKeys:Array.from(_)}}function bt(e){W=new Set(e.nodeIds),_=new Set(e.errorKeys)}function Yn(e){if(e.type==="SOLID"){let{r:t,g:n,b:s}=e.color,o=F(t,n,s),r=e.opacity!==void 0&&e.opacity<1?` (${Math.round(e.opacity*100)}%)`:"";return o+r}return e.type==="IMAGE"?"Image fill":e.type==="VIDEO"?"Video fill":e.type.includes("GRADIENT")?`${e.type.replace("GRADIENT_","").toLowerCase()} gradient`:e.type}function Oe(e,t){try{if("boundVariables"in e){let n=e.boundVariables;if(n&&n[t])return!0}}catch(n){}return!1}function Me(e,t,n){if(!("fills"in e))return;let s=e.fills;if(s===figma.mixed||!Array.isArray(s))return;let o=s.filter(r=>r.visible!==!1);if(o.length!==0&&!Oe(e,"fills")){if("fillStyleId"in e){let r=e.fillStyleId;if(r&&r!==""&&r!==figma.mixed)return}for(let r of o){try{let a=r.boundVariables;if(a&&a.color)continue}catch(a){}let i=Yn(r);t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"fill",message:`Missing fill style: ${i}`,value:i,path:n})}}}function ut(e,t,n){if(!("strokes"in e))return;let s=e.strokes;if(!Array.isArray(s))return;let o=s.filter(r=>r.visible!==!1);if(o.length!==0&&!Oe(e,"strokes")){if("strokeStyleId"in e){let r=e.strokeStyleId;if(r&&r!==""&&r!==figma.mixed)return}for(let r of o){let i=Yn(r),a="strokeWeight"in e?` (${e.strokeWeight}px)`:"";t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"stroke",message:`Missing stroke style: ${i}${a}`,value:i+a,path:n})}}}function pt(e,t,n){if(!("effects"in e))return;let s=e.effects;if(!Array.isArray(s)||s.length===0)return;let o=s.filter(i=>i.visible!==!1);if(o.length===0)return;if("effectStyleId"in e){let i=e.effectStyleId;if(i&&i!==""&&i!==figma.mixed)return}let r=o.map(i=>{let a=[i.type.replace(/_/g," ").toLowerCase()];if("radius"in i&&a.push(`r:${i.radius}`),"color"in i&&i.color){let c=i.color;a.push(F(c.r,c.g,c.b))}return a.join(" ")});t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"effect",message:`Missing effect style: ${r.join(", ")}`,value:r.join(", "),path:n})}function yr(e,t,n){if("textStyleId"in e){let a=e.textStyleId;if(a&&a!==""&&a!==figma.mixed)return}let s=e.fontName!==figma.mixed?e.fontName:null,o=e.fontSize!==figma.mixed?e.fontSize:null,r=[];s&&r.push(`${s.family} ${s.style}`),o&&r.push(`${o}px`);let i=r.join(" / ")||"unknown text style";t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"text",message:`Missing text style: ${i}`,value:i,path:n})}function Xn(e,t,n,s){if(!("cornerRadius"in e)||Oe(e,"topLeftRadius")||Oe(e,"cornerRadius"))return;let o=e.cornerRadius;if(o===figma.mixed){let r=[e.topLeftRadius,e.topRightRadius,e.bottomLeftRadius,e.bottomRightRadius].filter(i=>i!=null);for(let i of r)if(!s.includes(i)){t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"radius",message:`Non-standard border radius: ${i}px (allowed: ${s.join(", ")})`,value:`${i}px`,path:n});break}return}typeof o=="number"&&o>0&&!s.includes(o)&&t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"radius",message:`Non-standard border radius: ${o}px (allowed: ${s.join(", ")})`,value:`${o}px`,path:n})}function hr(e,t,n,s){if(!(e.type==="GROUP"||e.type==="SLICE"||e.type==="CONNECTOR")&&e.type!=="COMPONENT_SET")switch(e.type){case"TEXT":t.checkTextStyles&&yr(e,n,s),t.checkFills&&Me(e,n,s);break;case"FRAME":case"SECTION":t.checkFills&&Me(e,n,s),t.checkStrokes&&ut(e,n,s),t.checkEffects&&pt(e,n,s),t.checkRadius&&Xn(e,n,s,t.allowedRadii);break;case"RECTANGLE":case"COMPONENT":case"INSTANCE":t.checkFills&&Me(e,n,s),t.checkStrokes&&ut(e,n,s),t.checkEffects&&pt(e,n,s),t.checkRadius&&Xn(e,n,s,t.allowedRadii);break;case"ELLIPSE":case"POLYGON":case"STAR":case"VECTOR":case"LINE":case"BOOLEAN_OPERATION":t.checkFills&&Me(e,n,s),t.checkStrokes&&ut(e,n,s),t.checkEffects&&pt(e,n,s);break}}function Qn(e,t,n,s,o){let r=0,i=o||"locked"in e&&e.locked,a="visible"in e&&!e.visible;if(t.skipLockedLayers&&i||t.skipHiddenLayers&&a)return 0;let c=s?`${s} > ${e.name}`:e.name;if(r++,!W.has(e.id)){let l=n.length;hr(e,t,n,c);for(let d=n.length-1;d>=l;d--){let p=n[d];(_.has(K(p.nodeId,p.errorType))||_.has(K(p.nodeId,p.errorType,p.value)))&&n.splice(d,1)}}if("children"in e)for(let l of e.children)r+=Qn(l,t,n,c,i);return r}function se(e,t){for(let n of t)if(new RegExp("^"+n.replace(/[.+^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*").replace(/\?/g,".")+"$").test(e))return!0;return!1}function Ne(e,t=X){var u,f;let n=[],s=0,o=t.ignorePatterns||[],r=t.severityOverrides||{};for(let y of e)s+=Qn(y,t,n,"",!1);let i={skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers,scale:t.spacingScale};if(t.checkSpacing&&r.spacing!=="off"){let y=rn(e,i);for(let m of y.issues){let v=m.currentValue||"";W.has(m.nodeId)||_.has(K(m.nodeId,"spacing"))||_.has(K(m.nodeId,"spacing",v))||se(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"spacing",message:m.message,value:v,path:m.nodeName,property:(f=(u=m.fixAction)==null?void 0:u.params)==null?void 0:f.property})}}if(t.checkAutoLayout&&r.autoLayout!=="off"){let y=dn(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of y.issues)W.has(m.nodeId)||_.has(K(m.nodeId,"autoLayout"))||se(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"autoLayout",message:m.message,value:m.currentValue||"",path:m.nodeName})}if(t.checkAccessibility&&r.accessibility!=="off"){let y=fn(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of y.issues)W.has(m.nodeId)||_.has(K(m.nodeId,"accessibility"))||se(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"accessibility",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkVisualQuality&&r.visualQuality!=="off"){let y=Sn(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of y.issues)W.has(m.nodeId)||_.has(K(m.nodeId,"visualQuality"))||se(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"visualQuality",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkMicrocopy&&r.microcopy!=="off"){let y=In(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of y.issues)W.has(m.nodeId)||_.has(K(m.nodeId,"microcopy"))||se(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"TEXT",errorType:"microcopy",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkConversion&&r.conversion!=="off"){let y=On(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of y.issues)W.has(m.nodeId)||_.has(K(m.nodeId,"conversion"))||se(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"conversion",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkCognitive&&r.cognitive!=="off"){let y=qn(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of y.issues)W.has(m.nodeId)||_.has(K(m.nodeId,"cognitive"))||se(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"cognitive",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}let a=n.filter(y=>r[y.errorType]!=="off"),c=o.length>0?a.filter(y=>!se(y.nodeName,o)):a;for(let y of c){let m=r[y.errorType];if(m&&m!=="off")y.severity=m;else if(!y.severity)switch(y.errorType){case"fill":case"stroke":case"effect":case"text":case"spacing":y.severity="warning";break;case"radius":case"autoLayout":y.severity="info";break;case"accessibility":y.severity="critical";break;case"visualQuality":y.severity="warning";break;case"microcopy":y.severity="info";break;case"conversion":y.severity="warning";break;case"cognitive":y.severity="info";break}}let l=new Set(c.map(y=>y.nodeId)).size,d={fill:0,stroke:0,effect:0,text:0,radius:0,spacing:0,autoLayout:0,accessibility:0,visualQuality:0,microcopy:0,conversion:0,cognitive:0};for(let y of c)d[y.errorType]++;let p={totalErrors:c.length,byType:d,totalNodes:s,nodesWithErrors:l};return{errors:c,ignoredNodeIds:Array.from(W),ignoredErrorKeys:Array.from(_),summary:p}}function we(e){let t=figma.currentPage.selection;return t.length===0?{errors:[],ignoredNodeIds:[],ignoredErrorKeys:[],summary:{totalErrors:0,byType:{fill:0,stroke:0,effect:0,text:0,radius:0,spacing:0,autoLayout:0,accessibility:0,visualQuality:0,microcopy:0,conversion:0,cognitive:0},totalNodes:0,nodesWithErrors:0}}:Ne(t,e)}function vt(e,t,n,s=X){return Ne(e,s).errors.filter(r=>r.errorType===t&&r.value===n)}var X,W,_,Fe=H(()=>{"use strict";J();an();un();gn();Nn();An();Fn();Jn();X={checkFills:!0,checkStrokes:!0,checkEffects:!0,checkTextStyles:!0,checkRadius:!0,checkSpacing:!0,checkAutoLayout:!0,checkAccessibility:!0,checkVisualQuality:!0,checkMicrocopy:!0,checkConversion:!0,checkCognitive:!0,allowedRadii:[0,2,4,8,12,16,24,32],skipLockedLayers:!0,skipHiddenLayers:!0},W=new Set,_=new Set});var bs=Ws((xc,Ve)=>{var Rt=function(){var e=String.fromCharCode,t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",s={};function o(i,a){if(!s[i]){s[i]={};for(var c=0;c<i.length;c++)s[i][i.charAt(c)]=c}return s[i][a]}var r={compressToBase64:function(i){if(i==null)return"";var a=r._compress(i,6,function(c){return t.charAt(c)});switch(a.length%4){default:case 0:return a;case 1:return a+"===";case 2:return a+"==";case 3:return a+"="}},decompressFromBase64:function(i){return i==null?"":i==""?null:r._decompress(i.length,32,function(a){return o(t,i.charAt(a))})},compressToUTF16:function(i){return i==null?"":r._compress(i,15,function(a){return e(a+32)})+" "},decompressFromUTF16:function(i){return i==null?"":i==""?null:r._decompress(i.length,16384,function(a){return i.charCodeAt(a)-32})},compressToUint8Array:function(i){for(var a=r.compress(i),c=new Uint8Array(a.length*2),l=0,d=a.length;l<d;l++){var p=a.charCodeAt(l);c[l*2]=p>>>8,c[l*2+1]=p%256}return c},decompressFromUint8Array:function(i){if(i==null)return r.decompress(i);for(var a=new Array(i.length/2),c=0,l=a.length;c<l;c++)a[c]=i[c*2]*256+i[c*2+1];var d=[];return a.forEach(function(p){d.push(e(p))}),r.decompress(d.join(""))},compressToEncodedURIComponent:function(i){return i==null?"":r._compress(i,6,function(a){return n.charAt(a)})},decompressFromEncodedURIComponent:function(i){return i==null?"":i==""?null:(i=i.replace(/ /g,"+"),r._decompress(i.length,32,function(a){return o(n,i.charAt(a))}))},compress:function(i){return r._compress(i,16,function(a){return e(a)})},_compress:function(i,a,c){if(i==null)return"";var l,d,p={},u={},f="",y="",m="",v=2,A=3,w=2,k=[],g=0,h=0,x;for(x=0;x<i.length;x+=1)if(f=i.charAt(x),Object.prototype.hasOwnProperty.call(p,f)||(p[f]=A++,u[f]=!0),y=m+f,Object.prototype.hasOwnProperty.call(p,y))m=y;else{if(Object.prototype.hasOwnProperty.call(u,m)){if(m.charCodeAt(0)<256){for(l=0;l<w;l++)g=g<<1,h==a-1?(h=0,k.push(c(g)),g=0):h++;for(d=m.charCodeAt(0),l=0;l<8;l++)g=g<<1|d&1,h==a-1?(h=0,k.push(c(g)),g=0):h++,d=d>>1}else{for(d=1,l=0;l<w;l++)g=g<<1|d,h==a-1?(h=0,k.push(c(g)),g=0):h++,d=0;for(d=m.charCodeAt(0),l=0;l<16;l++)g=g<<1|d&1,h==a-1?(h=0,k.push(c(g)),g=0):h++,d=d>>1}v--,v==0&&(v=Math.pow(2,w),w++),delete u[m]}else for(d=p[m],l=0;l<w;l++)g=g<<1|d&1,h==a-1?(h=0,k.push(c(g)),g=0):h++,d=d>>1;v--,v==0&&(v=Math.pow(2,w),w++),p[y]=A++,m=String(f)}if(m!==""){if(Object.prototype.hasOwnProperty.call(u,m)){if(m.charCodeAt(0)<256){for(l=0;l<w;l++)g=g<<1,h==a-1?(h=0,k.push(c(g)),g=0):h++;for(d=m.charCodeAt(0),l=0;l<8;l++)g=g<<1|d&1,h==a-1?(h=0,k.push(c(g)),g=0):h++,d=d>>1}else{for(d=1,l=0;l<w;l++)g=g<<1|d,h==a-1?(h=0,k.push(c(g)),g=0):h++,d=0;for(d=m.charCodeAt(0),l=0;l<16;l++)g=g<<1|d&1,h==a-1?(h=0,k.push(c(g)),g=0):h++,d=d>>1}v--,v==0&&(v=Math.pow(2,w),w++),delete u[m]}else for(d=p[m],l=0;l<w;l++)g=g<<1|d&1,h==a-1?(h=0,k.push(c(g)),g=0):h++,d=d>>1;v--,v==0&&(v=Math.pow(2,w),w++)}for(d=2,l=0;l<w;l++)g=g<<1|d&1,h==a-1?(h=0,k.push(c(g)),g=0):h++,d=d>>1;for(;;)if(g=g<<1,h==a-1){k.push(c(g));break}else h++;return k.join("")},decompress:function(i){return i==null?"":i==""?null:r._decompress(i.length,32768,function(a){return i.charCodeAt(a)})},_decompress:function(i,a,c){var l=[],d,p=4,u=4,f=3,y="",m=[],v,A,w,k,g,h,x,S={val:c(0),position:a,index:1};for(v=0;v<3;v+=1)l[v]=v;for(w=0,g=Math.pow(2,2),h=1;h!=g;)k=S.val&S.position,S.position>>=1,S.position==0&&(S.position=a,S.val=c(S.index++)),w|=(k>0?1:0)*h,h<<=1;switch(d=w){case 0:for(w=0,g=Math.pow(2,8),h=1;h!=g;)k=S.val&S.position,S.position>>=1,S.position==0&&(S.position=a,S.val=c(S.index++)),w|=(k>0?1:0)*h,h<<=1;x=e(w);break;case 1:for(w=0,g=Math.pow(2,16),h=1;h!=g;)k=S.val&S.position,S.position>>=1,S.position==0&&(S.position=a,S.val=c(S.index++)),w|=(k>0?1:0)*h,h<<=1;x=e(w);break;case 2:return""}for(l[3]=x,A=x,m.push(x);;){if(S.index>i)return"";for(w=0,g=Math.pow(2,f),h=1;h!=g;)k=S.val&S.position,S.position>>=1,S.position==0&&(S.position=a,S.val=c(S.index++)),w|=(k>0?1:0)*h,h<<=1;switch(x=w){case 0:for(w=0,g=Math.pow(2,8),h=1;h!=g;)k=S.val&S.position,S.position>>=1,S.position==0&&(S.position=a,S.val=c(S.index++)),w|=(k>0?1:0)*h,h<<=1;l[u++]=e(w),x=u-1,p--;break;case 1:for(w=0,g=Math.pow(2,16),h=1;h!=g;)k=S.val&S.position,S.position>>=1,S.position==0&&(S.position=a,S.val=c(S.index++)),w|=(k>0?1:0)*h,h<<=1;l[u++]=e(w),x=u-1,p--;break;case 2:return m.join("")}if(p==0&&(p=Math.pow(2,f),f++),l[x])y=l[x];else if(x===u)y=A+A.charAt(0);else return null;m.push(y),l[u++]=A+y.charAt(0),p--,A=y,p==0&&(p=Math.pow(2,f),f++)}}};return r}();typeof define=="function"&&define.amd?define(function(){return Rt}):typeof Ve!="undefined"&&Ve!=null?Ve.exports=Rt:typeof angular!="undefined"&&angular!=null&&angular.module("LZString",[]).factory("LZString",function(){return Rt})});var Es={};Wt(Es,{applyEffectStyle:()=>Vt,applyFillStyle:()=>Ot,applyStrokeStyle:()=>Ft,applyTextStyle:()=>Dt});async function Ot(e,t){let n=figma.getNodeById(e);if(!n||!("fillStyleId"in n))return{success:!1,nodeId:e,nodeName:"",property:"fillStyle",oldValue:"",newValue:"",error:"Node not found or does not support fill styles"};try{let s=await figma.importStyleByKeyAsync(t),o=n.fillStyleId||"";return n.fillStyleId=s.id,{success:!0,nodeId:e,nodeName:n.name,property:"fillStyle",oldValue:o?"existing style":"no style",newValue:s.name}}catch(s){return{success:!1,nodeId:e,nodeName:n.name,property:"fillStyle",oldValue:"",newValue:t,error:s instanceof Error?s.message:String(s)}}}async function Ft(e,t){let n=figma.getNodeById(e);if(!n||!("strokeStyleId"in n))return{success:!1,nodeId:e,nodeName:"",property:"strokeStyle",oldValue:"",newValue:"",error:"Node not found or does not support stroke styles"};try{let s=await figma.importStyleByKeyAsync(t),o=n.strokeStyleId||"";return n.strokeStyleId=s.id,{success:!0,nodeId:e,nodeName:n.name,property:"strokeStyle",oldValue:o?"existing style":"no style",newValue:s.name}}catch(s){return{success:!1,nodeId:e,nodeName:n.name,property:"strokeStyle",oldValue:"",newValue:t,error:s instanceof Error?s.message:String(s)}}}async function Dt(e,t){let n=figma.getNodeById(e);if(!n||n.type!=="TEXT")return{success:!1,nodeId:e,nodeName:"",property:"textStyle",oldValue:"",newValue:"",error:"Node not found or is not a text node"};try{let s=await figma.importStyleByKeyAsync(t),o=n,r=o.textStyleId||"";return o.textStyleId=s.id,{success:!0,nodeId:e,nodeName:n.name,property:"textStyle",oldValue:r?"existing style":"no style",newValue:s.name}}catch(s){return{success:!1,nodeId:e,nodeName:n.name,property:"textStyle",oldValue:"",newValue:t,error:s instanceof Error?s.message:String(s)}}}async function Vt(e,t){let n=figma.getNodeById(e);if(!n||!("effectStyleId"in n))return{success:!1,nodeId:e,nodeName:"",property:"effectStyle",oldValue:"",newValue:"",error:"Node not found or does not support effect styles"};try{let s=await figma.importStyleByKeyAsync(t),o=n.effectStyleId||"";return n.effectStyleId=s.id,{success:!0,nodeId:e,nodeName:n.name,property:"effectStyle",oldValue:o?"existing style":"no style",newValue:s.name}}catch(s){return{success:!1,nodeId:e,nodeName:n.name,property:"effectStyle",oldValue:"",newValue:t,error:s instanceof Error?s.message:String(s)}}}var _t=H(()=>{"use strict"});J();J();J();function ae(e){var l;let t=e,n=!1;for(;t;){if(t.type==="COMPONENT_SET"){n=!0;break}if(t.parent&&t.parent.type==="COMPONENT_SET"){n=!0;break}t=t.parent}if(!n||!("strokes"in e)||!("cornerRadius"in e)||!("strokeWeight"in e))return!1;let s=e.cornerRadius===5||"topLeftRadius"in e&&"topRightRadius"in e&&"bottomLeftRadius"in e&&"bottomRightRadius"in e&&e.topLeftRadius===5&&e.topRightRadius===5&&e.bottomLeftRadius===5&&e.bottomRightRadius===5,o=e.strokeWeight===1,r=e.strokes,i=r.length>0&&r.some(d=>d.type==="SOLID"&&d.visible!==!1&&d.color?F(d.color.r,d.color.g,d.color.b).toUpperCase()==="#9747FF":!1),a="paddingLeft"in e&&"paddingRight"in e&&"paddingTop"in e&&"paddingBottom"in e&&e.paddingLeft===16&&e.paddingRight===16&&e.paddingTop===16&&e.paddingBottom===16,c=s&&o&&i&&a;return c&&(console.log(`\u{1F3AF} [FILTER] Detected default variant frame styles in ${e.name} - filtering out`),console.log(` Type: ${e.type}, Parent: ${(l=e.parent)==null?void 0:l.type}`),console.log(` Radius: ${String(e.cornerRadius)}, Weight: ${String(e.strokeWeight)}, Color: ${r.length>0&&r[0].type==="SOLID"?F(r[0].color.r,r[0].color.g,r[0].color.b):"none"}`),console.log(` Padding: L=${e.paddingLeft}, R=${e.paddingRight}, T=${e.paddingTop}, B=${e.paddingBottom}`)),c}function be(e){let t=e;for(;t;){if(t.type==="COMPONENT_SET"||t.parent&&t.parent.type==="COMPONENT_SET")return!0;t=t.parent}return!1}async function ce(e){let t=[],n=[],s=[],o=[],r=[],i=new Set,a=new Set,c=new Set,l=new Set,d=new Set;async function p(u){console.log("\u{1F50D} Analyzing node:",u.name,"Type:",u.type);let f=[];if("fillStyleId"in u&&typeof u.fillStyleId=="string"&&f.push(figma.getStyleByIdAsync(u.fillStyleId).then(g=>{if(g!=null&&g.name&&!i.has(g.name)){i.add(g.name);let h=g.name;if("fills"in u&&Array.isArray(u.fills)&&u.fills.length>0){let x=u.fills[0];x.type==="SOLID"&&x.color&&(h=F(x.color.r,x.color.g,x.color.b))}t.push({name:g.name,value:h,type:"fill-style",isToken:!0,isActualToken:!0,source:"figma-style"})}}).catch(console.warn)),"strokeStyleId"in u&&typeof u.strokeStyleId=="string"&&f.push(figma.getStyleByIdAsync(u.strokeStyleId).then(g=>{g!=null&&g.name&&!i.has(g.name)&&(i.add(g.name),t.push({name:g.name,value:g.name,type:"stroke-style",isToken:!0,isActualToken:!0,source:"figma-style"}))}).catch(console.warn)),u.type==="TEXT"&&"textStyleId"in u&&typeof u.textStyleId=="string"&&f.push(figma.getStyleByIdAsync(u.textStyleId).then(g=>{g!=null&&g.name&&!c.has(g.name)&&(c.add(g.name),s.push({name:g.name,value:g.name,type:"text-style",isToken:!0,isActualToken:!0,source:"figma-style"}))}).catch(console.warn)),"effectStyleId"in u&&typeof u.effectStyleId=="string"&&f.push(figma.getStyleByIdAsync(u.effectStyleId).then(g=>{g!=null&&g.name&&!l.has(g.name)&&(l.add(g.name),o.push({name:g.name,value:g.name,type:"effect-style",isToken:!0,isActualToken:!0,source:"figma-style"}))}).catch(console.warn)),await Promise.all(f),"boundVariables"in u&&u.boundVariables){let g=u.boundVariables;console.log(`\u{1F50D} [VARIABLES] Checking bound variables for ${u.name}:`,Object.keys(g));let h=async(C,$,b,T,L)=>{try{let I=Array.isArray(C)?C:[C];for(let Z of I)if(Z!=null&&Z.id&&typeof Z.id=="string"){let q=await Ke(Z.id);if(console.log(` \u{1F3AF} Found ${$} variable:`,q),q&&!b.has(q)){b.add(q);let fe=q;if(L==="color"&&($==="fills"||$==="strokes")){let Ae=await Kt(Z.id,u);Ae&&Ae.startsWith("#")&&(fe=Ae)}T.push({name:q,value:fe,type:`${$}-variable`,isToken:!0,isActualToken:!0,source:"figma-variable"}),console.log(` \u2705 Added ${L} token: ${q} (value: ${fe})`)}}}catch(I){console.warn(`Error processing ${$} variables:`,I)}},x=async(C,$,b,T,L)=>{if(C&&typeof C=="object"&&"id"in C&&typeof C.id=="string"){let I=await Ke(C.id);console.log(` \u{1F3AF} Found ${$} variable:`,I),I&&!b.has(I)&&(b.add(I),T.push({name:I,value:I,type:`${$}-variable`,isToken:!0,isActualToken:!0,source:"figma-variable"}),console.log(` \u2705 Added ${L} token: ${I}`))}},S=[];g.fills&&(console.log(" \u{1F3A8} Processing fills variables..."),S.push(h(g.fills,"fills",i,t,"color"))),g.strokes&&(console.log(" \u{1F58A}\uFE0F Processing strokes variables..."),S.push(h(g.strokes,"strokes",i,t,"color"))),g.effects&&(console.log(" \u2728 Processing effects variables..."),S.push(h(g.effects,"effects",l,o,"effect"))),["strokeWeight","strokeTopWeight","strokeRightWeight","strokeBottomWeight","strokeLeftWeight"].forEach(C=>{g[C]&&(console.log(` \u{1F4CF} Processing ${C} variable...`),S.push(x(g[C],C,d,r,"border")))}),["topLeftRadius","topRightRadius","bottomLeftRadius","bottomRightRadius"].forEach(C=>{g[C]&&(console.log(` \u{1F504} Processing ${C} variable...`),S.push(x(g[C],C,d,r,"border")))}),["paddingTop","paddingRight","paddingBottom","paddingLeft","itemSpacing","counterAxisSpacing"].forEach(C=>{g[C]&&(console.log(` \u{1F4D0} Processing ${C} variable...`),S.push(x(g[C],C,a,n,"spacing")))}),["width","height","minWidth","maxWidth","minHeight","maxHeight"].forEach(C=>{g[C]&&(console.log(` \u{1F4E6} Processing ${C} variable...`),S.push(x(g[C],C,a,n,"size")))}),g.opacity&&(console.log(" \u{1F47B} Processing opacity variable..."),S.push(x(g.opacity,"opacity",l,o,"effect"))),u.type==="TEXT"&&["fontSize","lineHeight","letterSpacing","paragraphSpacing"].forEach($=>{g[$]&&(console.log(` \u{1F4DD} Processing ${$} variable...`),S.push(x(g[$],$,c,s,"typography")))}),await Promise.all(S),console.log(`\u{1F50D} [VARIABLES] Total variables found for ${u.name}: ${Object.keys(g).length}`)}let y="boundVariables"in u&&u.boundVariables&&u.boundVariables.fills,m="fillStyleId"in u&&u.fillStyleId;"fills"in u&&Array.isArray(u.fills)&&!m&&!y?(console.log(`\u{1F50D} [HARD-CODED] Checking fills for ${u.name} (no variables, no style)`),u.fills.forEach(g=>{if(g.type==="SOLID"&&g.visible!==!1&&g.color){let h=F(g.color.r,g.color.g,g.color.b),x=`${h}:${u.id}`;if(!i.has(x)){console.log(` \u26A0\uFE0F Found hard-coded fill: ${h}`),i.add(x);let S=oe(u);t.push({name:`hard-coded-fill-${t.length+1}`,value:h,type:"fill",isToken:!1,source:"hard-coded",context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,path:S.path,description:S.description,property:"fills"}})}}})):y?console.log(`\u{1F50D} [VARIABLES] ${u.name} has fill variables - skipping hard-coded detection`):m&&console.log(`\u{1F50D} [STYLES] ${u.name} has fill style - skipping hard-coded detection`);let v="boundVariables"in u&&u.boundVariables&&u.boundVariables.strokes,A="strokeStyleId"in u&&u.strokeStyleId;if("strokes"in u&&Array.isArray(u.strokes)&&!A&&!v?(console.log(`\u{1F50D} [HARD-CODED] Checking strokes for ${u.name} (no variables, no style)`),ae(u)?console.log(" \u{1F6AB} Skipping default variant frame stroke colors"):u.strokes.forEach(g=>{if(g.type==="SOLID"&&g.visible!==!1&&g.color){let h=F(g.color.r,g.color.g,g.color.b),x=`${h}:${u.id}`;if(!i.has(x)){console.log(` \u26A0\uFE0F Found hard-coded stroke: ${h}`),i.add(x);let S=oe(u);t.push({name:`hard-coded-stroke-${t.length+1}`,value:h,type:"stroke",isToken:!1,source:"hard-coded",isDefaultVariantStyle:h.toUpperCase()==="#9747FF"&&be(u),context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,path:S.path,description:S.description,property:"strokes"}})}}})):v?console.log(`\u{1F50D} [VARIABLES] ${u.name} has stroke variables - skipping hard-coded detection`):A&&console.log(`\u{1F50D} [STYLES] ${u.name} has stroke style - skipping hard-coded detection`),"strokeWeight"in u&&typeof u.strokeWeight=="number"){console.log(`\u{1F50D} Node ${u.name} has strokeWeight: ${u.strokeWeight}`);let g="strokes"in u&&Array.isArray(u.strokes)&&u.strokes.length>0,h=g&&u.strokes.some(P=>P.visible!==!1),x="boundVariables"in u&&u.boundVariables&&["strokeWeight","strokeTopWeight","strokeRightWeight","strokeBottomWeight","strokeLeftWeight"].some(P=>u.boundVariables[P]),S="boundVariables"in u&&u.boundVariables?Object.keys(u.boundVariables):[];if(console.log(` Has strokes: ${g}, Has visible strokes: ${h}, Has strokeWeight variable: ${!!x}, boundVariable keys: [${S.join(", ")}]`),x)console.log(` \u{1F517} ${u.name} has strokeWeight bound to variable - skipping hard-coded detection`);else if(u.strokeWeight>0&&h&&!ae(u)){let P=`${u.strokeWeight}px`,O,M=u.strokes.find(C=>C.visible!==!1&&C.type==="SOLID");M&&M.type==="SOLID"&&M.color&&(O=F(M.color.r,M.color.g,M.color.b));let B=`${P}:${u.id}`;if(!d.has(B)){console.log(` \u2705 Adding stroke weight: ${P}`),d.add(B);let C=oe(u);r.push({name:`hard-coded-stroke-weight-${u.strokeWeight}`,value:P,type:"stroke-weight",isToken:!1,source:"hard-coded",strokeColor:O,isDefaultVariantStyle:u.strokeWeight===1&&(O==null?void 0:O.toUpperCase())==="#9747FF"&&be(u),context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,hasVisibleStroke:!0,path:C.path,description:C.description,property:"strokeWeight"}})}}else u.strokeWeight>0&&h&&ae(u)&&console.log(" \u{1F6AB} Skipping default variant frame stroke weight")}let w="boundVariables"in u&&u.boundVariables&&["topLeftRadius","topRightRadius","bottomLeftRadius","bottomRightRadius","cornerRadius"].some(g=>u.boundVariables[g]);if("cornerRadius"in u&&typeof u.cornerRadius=="number"&&!w)if(console.log(`\u{1F50D} [HARD-CODED] Checking corner radius for ${u.name} (no variables)`),ae(u))console.log(" \u{1F6AB} Skipping default variant frame corner radius");else{let g=u.cornerRadius;if(g>0){let h=`${g}px`,x=`${h}:${u.id}`;if(!d.has(x)){console.log(` \u26A0\uFE0F Found hard-coded corner radius: ${h}`),d.add(x);let S=oe(u);r.push({name:`hard-coded-corner-radius-${g}`,value:h,type:"corner-radius",isToken:!1,source:"hard-coded",isDefaultVariantStyle:g===5&&be(u),context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,path:S.path,description:S.description,property:"cornerRadius"}})}}}else w&&console.log(`\u{1F50D} [VARIABLES] ${u.name} has radius variables - skipping hard-coded detection`);!w&&"topLeftRadius"in u&&(console.log(`\u{1F50D} [HARD-CODED] Checking individual corner radius for ${u.name} (no variables)`),ae(u)?console.log(" \u{1F6AB} Skipping default variant frame individual corner radii"):[{prop:"topLeftRadius",name:"top-left"},{prop:"topRightRadius",name:"top-right"},{prop:"bottomLeftRadius",name:"bottom-left"},{prop:"bottomRightRadius",name:"bottom-right"}].forEach(({prop:h,name:x})=>{if(h in u&&typeof u[h]=="number"){let S=u[h];if(S>0){let P=`${S}px`,O=`${P}:${u.id}:${h}`;if(!d.has(O)){console.log(` \u26A0\uFE0F Found hard-coded ${x} radius: ${P}`),d.add(O);let M=oe(u);r.push({name:`hard-coded-${x}-radius-${S}`,value:P,type:`${x}-radius`,isToken:!1,source:"hard-coded",isDefaultVariantStyle:S===5&&be(u),context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,path:M.path,description:M.description,property:h}})}}}}));let k="boundVariables"in u&&u.boundVariables&&["paddingTop","paddingRight","paddingBottom","paddingLeft"].some(g=>u.boundVariables[g]);if("paddingLeft"in u&&typeof u.paddingLeft=="number"&&!k){console.log(`\u{1F50D} [HARD-CODED] Checking padding for ${u.name} (no variables)`);let g=u;[{value:g.paddingLeft,name:"left"},{value:g.paddingRight,name:"right"},{value:g.paddingTop,name:"top"},{value:g.paddingBottom,name:"bottom"}].forEach(x=>{let S=`${x.value}:${u.id}:${x.name}`;if(typeof x.value=="number"&&x.value>1&&!a.has(S)){console.log(` \u26A0\uFE0F Found hard-coded padding-${x.name}: ${x.value}px`),a.add(S);let P=oe(u),O=x.value===16&&be(u)&&ae(u);n.push({name:`hard-coded-padding-${x.name}-${x.value}`,value:`${x.value}px`,type:"padding",isToken:!1,source:"hard-coded",isDefaultVariantStyle:O,context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,path:P.path,description:P.description,property:`padding${x.name.charAt(0).toUpperCase()+x.name.slice(1)}`}})}})}else k&&console.log(`\u{1F50D} [VARIABLES] ${u.name} has padding variables - skipping hard-coded detection`);if("children"in u)for(let g of u.children)await p(g)}return await p(e),qs({colors:t,spacing:n,typography:s,effects:o,borders:r})}function qs(e){let t=["colors","spacing","typography","effects","borders"],n={totalTokens:0,actualTokens:0,hardCodedValues:0,aiSuggestions:0,byCategory:{}};return t.forEach(s=>{let o=e[s].map(c=>z(R({},c),{isActualToken:c.source==="figma-style"||c.source==="figma-variable",recommendation:Js(c,s),suggestion:Xs(c,s)})),r=o.filter(c=>!c.isDefaultVariantStyle),i=r.filter(c=>c.isActualToken).length,a=r.filter(c=>c.source==="hard-coded").length;n.byCategory[s]={total:r.length,tokens:i,hardCoded:a,suggestions:0},n.totalTokens+=r.length,n.actualTokens+=i,n.hardCodedValues+=a,e[s]=o}),z(R({},e),{summary:n})}function Js(e,t){if(e.isToken)return`Using ${e.name} token`;switch(t){case"colors":return`Consider using a color token instead of ${e.value}`;case"spacing":return`Consider using spacing token instead of ${e.value}`;case"typography":return"Consider using typography token";case"effects":return"Consider using effect token";case"borders":return"Consider using border radius token";default:return"Consider using a design token"}}function Xs(e,t){var n,s;switch(t){case"colors":return(n=e.value)!=null&&n.startsWith("#000")?"Use semantic color token (e.g., text.primary)":(s=e.value)!=null&&s.startsWith("#FFF")?"Use semantic color token (e.g., background.primary)":"Create or use existing color token";case"spacing":let o=parseInt(e.value||"0");return o%8===0?"Create or use existing spacing token (follows 8px grid)":o%4===0?"Create or use existing spacing token (follows 4px grid)":"Create or use existing spacing token";case"typography":return"Use semantic typography token (e.g., heading.large, body.regular)";case"effects":return"Use semantic shadow token (e.g., shadow.small, shadow.medium)";case"borders":return"Use appropriate radius token (e.g., radius.small, radius.medium)";default:return"Create or use existing design token"}}function Ht(e){return`You are an expert design system architect analyzing a Figma component for comprehensive metadata and design token recommendations. +"use strict";(()=>{var No=Object.create;var Me=Object.defineProperty,wo=Object.defineProperties,Co=Object.getOwnPropertyDescriptor,Io=Object.getOwnPropertyDescriptors,xo=Object.getOwnPropertyNames,Yt=Object.getOwnPropertySymbols,Ao=Object.getPrototypeOf,Zt=Object.prototype.hasOwnProperty,Eo=Object.prototype.propertyIsEnumerable;var Qt=(e,t,n)=>t in e?Me(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,R=(e,t)=>{for(var n in t||(t={}))Zt.call(t,n)&&Qt(e,n,t[n]);if(Yt)for(var n of Yt(t))Eo.call(t,n)&&Qt(e,n,t[n]);return e},K=(e,t)=>wo(e,Io(t));var G=(e,t)=>()=>(e&&(t=e(e=0)),t);var To=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),en=(e,t)=>{for(var n in t)Me(e,n,{get:t[n],enumerable:!0})},Po=(e,t,n,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of xo(t))!Zt.call(e,o)&&o!==n&&Me(e,o,{get:()=>t[o],enumerable:!(s=Co(t,o))||s.enumerable});return e};var Lo=(e,t,n)=>(n=e!=null?No(Ao(e)):{},Po(t||!e||!e.__esModule?Me(n,"default",{value:e,enumerable:!0}):n,e));function Se(e){return["FRAME","COMPONENT","COMPONENT_SET","INSTANCE","GROUP"].includes(e.type)?(e.type==="COMPONENT_SET",!0):!1}function F(e,t,n){let s=o=>{let r=Math.round(o*255).toString(16);return r.length===1?"0"+r:r};return`#${s(e)}${s(t)}${s(n)}`}async function Qe(e){try{let t=await figma.variables.getVariableByIdAsync(e);return t?t.name:null}catch(t){return console.warn("Could not access variable:",e,t),null}}async function tn(e,t){try{let n=await figma.variables.getVariableByIdAsync(e);if(!n)return null;if(t&&n.resolveForConsumer)try{let s=n.resolveForConsumer(t);if(s&&typeof s.value=="object"&&"r"in s.value){let o=s.value;return F(o.r,o.g,o.b)}else if(s&&s.value!==void 0)return String(s.value)}catch(s){console.warn("Could not resolve variable value:",s)}return n.name}catch(n){return console.warn("Could not access variable:",e,n),null}}function k(e,t){try{figma.ui.postMessage({type:e,data:t})}catch(n){console.error("Failed to send message to UI:",n)}}function Oe(e){let t=[e];if("children"in e)for(let n of e.children)t.push(...Oe(n));return t}function Ze(e){let t=[];if(e.type==="TEXT"){let n=e;n.characters&&t.push(n.characters)}if("children"in e)for(let n of e.children)t.push(...Ze(n));return t}function ne(e,t,n){let[s,o,r]=[e,t,n].map(i=>i<=.03928?i/12.92:Math.pow((i+.055)/1.055,2.4));return .2126*s+.7152*o+.0722*r}function ke(e,t){let n=Math.max(e,t),s=Math.min(e,t);return(n+.05)/(s+.05)}function Ne(e){let t=e.parent;for(;t&&"type"in t;){let n=t;if("fills"in n){let s=n.fills;if(Array.isArray(s)){for(let o of s)if(o.type==="SOLID"&&o.visible!==!1&&o.color){if(o.boundVariables&&o.boundVariables.color)continue;return o.color}}}t=t.parent}return null}function Ro(e){var s;let t=[],n=e;for(;n&&n.type!=="DOCUMENT"&&n.type!=="PAGE";)n.type==="COMPONENT"&&((s=n.parent)==null?void 0:s.type)==="COMPONENT_SET"?t.unshift(`${n.name}`):t.unshift(n.name),n=n.parent;return t.join(" \u2192 ")}function oe(e){var s,o;let t=Ro(e),n=`Found in "${e.name}"`;if(((s=e.parent)==null?void 0:s.type)==="COMPONENT_SET"||e.parent&&((o=e.parent.parent)==null?void 0:o.type)==="COMPONENT_SET")n=`Found in variant: "${e.name}"`;else if(t.includes("\u2192")){let r=t.split(" \u2192 ");r.length>1&&(n=`Found in "${r[r.length-1]}" (${r[r.length-2]})`)}return{path:t,description:n}}var J=G(()=>{"use strict"});function Ve(e,t=Ce){if(t.includes(e))return[];let n=[...t].map(o=>({v:o,diff:Math.abs(o-e)})).sort((o,r)=>o.diff-r.diff),s=[];for(let o of n){if(s.length>=2)break;s.includes(o.v)||s.push(o.v)}return s.sort((o,r)=>o-r)}var Ce,mt,ft=G(()=>{"use strict";Ce=[0,2,4,8,12,16,20,24,32,40,48,64,80,96],mt=Ce});function Qo(){return`spacing-${++fn}`}function Zo(e){return gt.includes(e)}function er(e){return{itemSpacing:"Gap",paddingTop:"Padding Top",paddingBottom:"Padding Bottom",paddingLeft:"Padding Left",paddingRight:"Padding Right",counterAxisSpacing:"Counter-axis Gap"}[e]||e}function tr(e,t){var o;if(e.layoutMode==="NONE")return 0;let n=0,s=[{prop:"itemSpacing",value:e.itemSpacing},{prop:"paddingTop",value:e.paddingTop},{prop:"paddingBottom",value:e.paddingBottom},{prop:"paddingLeft",value:e.paddingLeft},{prop:"paddingRight",value:e.paddingRight}];"counterAxisSpacing"in e&&typeof e.counterAxisSpacing=="number"&&s.push({prop:"counterAxisSpacing",value:e.counterAxisSpacing});for(let{prop:r,value:i}of s)if(n++,!Zo(i)){let a=Ve(i,gt);t.push({id:Qo(),type:"spacing",severity:"warning",nodeId:e.id,nodeName:e.name,message:`${er(r)} is ${i}px \u2014 not in spacing scale`,currentValue:`${i}px`,suggestions:a.map(c=>`${c}px`),autoFixable:!0,fixAction:{type:"fixSpacing",params:{nodeId:e.id,property:r,currentValue:i,suggestedValue:(o=a[0])!=null?o:i}}})}return n}function gn(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,passed:0};if(s&&i)return{checked:0,passed:0};let a=0,c=0;if(e.type==="FRAME"||e.type==="COMPONENT"||e.type==="INSTANCE"){let d=t.length,l=tr(e,t);a+=l,c+=l-(t.length-d)}if("children"in e)for(let d of e.children){let l=gn(d,t,n,s,r);a+=l.checked,c+=l.passed}return{checked:a,passed:c}}function yn(e,t={}){let{skipLocked:n=!0,skipHidden:s=!0,scale:o}=t;gt=o||Ce,fn=0;let r=[],i=0,a=0;for(let c of e){let{checked:d,passed:l}=gn(c,r,n,s,!1);i+=d,a+=l}return{issues:r,summary:{totalChecked:i,passed:a,failed:r.length}}}var fn,gt,hn=G(()=>{"use strict";ft();fn=0;gt=Ce});function nr(){return`autolayout-${++bn}`}function vn(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{totalFrames:0,withAutoLayout:0};if(s&&i)return{totalFrames:0,withAutoLayout:0};let a=0,c=0;if((e.type==="FRAME"||e.type==="COMPONENT"||e.type==="INSTANCE")&&"children"in e){let l=e.children;l.length>=2&&(a++,e.layoutMode!=="NONE"?c++:t.push({id:nr(),type:"autoLayout",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Frame "${e.name}" has ${l.length} children but no Auto Layout`,currentValue:"No Auto Layout",suggestions:["HORIZONTAL","VERTICAL"],autoFixable:!1}))}if("children"in e)for(let l of e.children){let p=vn(l,t,n,s,r);a+=p.totalFrames,c+=p.withAutoLayout}return{totalFrames:a,withAutoLayout:c}}function Sn(e,t={}){let{skipLocked:n=!0,skipHidden:s=!0}=t;bn=0;let o=[],r=0,i=0;for(let d of e){let l=vn(d,o,n,s,!1);r+=l.totalFrames,i+=l.withAutoLayout}let a=r-i,c=r>0?Math.round(i/r*100):100;return{issues:o,summary:{totalFrames:r,withAutoLayout:i,withoutAutoLayout:a,percentage:c}}}var bn,kn=G(()=>{"use strict";bn=0});function se(){return`a11y-${++Nn}`}function yt(e){return sr.test(e)}function rr(e,t){if(e.type!=="TEXT")return;let n=e,s=n.fills;if(s===figma.mixed||!Array.isArray(s))return;let o=s.find(m=>{var h;return m.type==="SOLID"&&m.visible!==!1&&m.color&&!((h=m.boundVariables)!=null&&h.color)});if(!o||o.type!=="SOLID")return;let r=Ne(e);if(!r)return;let i=o.color,a=ne(i.r,i.g,i.b),c=ne(r.r,r.g,r.b),d=ke(a,c),l=n.fontSize!==figma.mixed?n.fontSize:0,p=n.fontName!==figma.mixed?n.fontName.style:"",u=p.toLowerCase().includes("bold")||p.toLowerCase().includes("black"),g=l>=18||l>=14&&u,f=g?3:4.5;if(d<f){let m=d.toFixed(1);t.push({id:se(),type:"accessibility",severity:"critical",nodeId:e.id,nodeName:e.name,message:`Contrast ratio ${m}:1 below WCAG AA ${g?"large text":""} minimum of ${f}:1`,currentValue:`${m}:1`,suggestions:[`Increase contrast to at least ${f}:1`],autoFixable:!1})}}function ir(e,t){if(e.type!=="FRAME"&&e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!yt(e.name))return;let n=e.width,s=e.height;(n<44||s<44)&&t.push({id:se(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Touch target ${Math.round(n)}x${Math.round(s)}px is below 44x44px recommended minimum`,currentValue:`${Math.round(n)}x${Math.round(s)}px`,suggestions:["Increase to at least 44x44px"],autoFixable:!1})}function ar(e,t){if(e.type!=="TEXT")return;let s=e.fontSize;s===figma.mixed||typeof s!="number"||s>0&&s<12&&t.push({id:se(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Text size ${s}px is below 12px readability minimum`,currentValue:`${s}px`,suggestions:["12px","14px"],autoFixable:!1})}function cr(e,t){if(e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!yt(e.name))return;let n=e;if(!("children"in n)||n.children.length===0)return;n.children.some(o=>{var r,i;return o.type==="TEXT"?!0:"children"in o?(i=(r=o.children)==null?void 0:r.some)==null?void 0:i.call(r,a=>a.type==="TEXT"):!1})||t.push({id:se(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Interactive element "${e.name}" has no visible text label`,currentValue:"No text child",suggestions:["Add a text label or ensure screen reader label is provided"],autoFixable:!1})}function lr(e,t){e.type!=="FRAME"&&e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!("children"in e)||e.children.length===0||or.test(e.name)&&t.push({id:se(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`Generic layer name "${e.name}" \u2014 use a descriptive name`,currentValue:e.name,suggestions:["Rename to describe the layer purpose"],autoFixable:!1})}function dr(e,t){if(e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!yt(e.name))return;let n=e,s=null,o=n.strokes;if(Array.isArray(o)){let d=o.find(l=>l.type==="SOLID"&&l.visible!==!1);d&&d.type==="SOLID"&&(s=d.color)}if(!s){let d=n.fills;if(d!==figma.mixed&&Array.isArray(d)){let l=d.find(p=>p.type==="SOLID"&&p.visible!==!1);l&&l.type==="SOLID"&&(s=l.color)}}if(!s)return;let r=Ne(e);if(!r)return;let i=ne(s.r,s.g,s.b),a=ne(r.r,r.g,r.b),c=ke(i,a);c<3&&t.push({id:se(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Non-text contrast ${c.toFixed(1)}:1 below WCAG 1.4.11 minimum of 3:1`,currentValue:`${c.toFixed(1)}:1`,suggestions:["Increase boundary contrast to at least 3:1 against background"],autoFixable:!1})}function pr(e,t){if(e.type!=="FRAME"&&e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!ur.test(e.name))return;let n=e;if(!("children"in n)||n.children.length===0)return;n.children.some(o=>{var r,i;return o.type==="TEXT"||/icon|svg|symbol|glyph/i.test(o.name)?!0:"children"in o?(i=(r=o.children)==null?void 0:r.some)==null?void 0:i.call(r,a=>a.type==="TEXT"||/icon|svg|symbol|glyph/i.test(a.name)):!1})||t.push({id:se(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`"${e.name}" may rely on color alone to convey status (WCAG 1.4.1)`,currentValue:"No text or icon indicator",suggestions:["Add a text label or icon to supplement the color indicator"],autoFixable:!1})}function mr(e,t){if(e.type!=="COMPONENT")return;let n=e.parent;if(!n||n.type!=="COMPONENT_SET")return;let s=n,r=s.children.map(c=>c.name.toLowerCase()).join(" "),a=["hover","focus","disabled","pressed"].filter(c=>!r.includes(c));a.length>0&&t.push({id:se(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`Component set missing states: ${a.join(", ")}`,currentValue:`${s.children.length} variants`,suggestions:a.map(c=>`Add ${c} variant`),autoFixable:!1})}function wn(e,t,n,s,o,r){var d;let i=o||"locked"in e&&e.locked,a="visible"in e&&!e.visible;if(n&&i||s&&a)return 0;let c=0;if(rr(e,t),ir(e,t),ar(e,t),c++,cr(e,t),lr(e,t),dr(e,t),pr(e,t),e.type==="COMPONENT"&&((d=e.parent)==null?void 0:d.type)==="COMPONENT_SET"){let l=e.parent.id;r.has(l)||(r.add(l),mr(e,t))}if("children"in e)for(let l of e.children)c+=wn(l,t,n,s,i,r);return c}function Cn(e,t={}){let{skipLocked:n=!0,skipHidden:s=!0}=t;Nn=0;let o=[],r=new Set,i=0;for(let a of e)i+=wn(a,o,n,s,!1,r);return{issues:o,summary:{totalChecked:i,contrastIssues:o.filter(a=>a.message.includes("Contrast")).length,touchTargetIssues:o.filter(a=>a.message.includes("Touch target")).length,textSizeIssues:o.filter(a=>a.message.includes("Text size")).length,namingIssues:o.filter(a=>a.message.includes("text label")||a.message.includes("Generic")).length,stateIssues:o.filter(a=>a.message.includes("missing states")).length,nonTextContrastIssues:o.filter(a=>a.message.includes("Non-text contrast")).length,colorOnlyIssues:o.filter(a=>a.message.includes("color alone")).length}}}var Nn,sr,or,ur,In=G(()=>{"use strict";J();Nn=0;sr=/\b(button|btn|input|link|checkbox|toggle|switch|tab|radio|select|dropdown|menu-item|slider|chip)\b/i;or=/^(Frame|Group|Rectangle|Ellipse|Vector|Line|Polygon|Star)\s*\d+$/i;ur=/\b(error|success|warning|status|alert|badge|danger|info)\b/i});function ge(){return`vq-${++xn}`}function gr(e){return fr.some(t=>t.includes(e))}function yr(e,t){let n=e.width*e.height;if(n===0)return;let o=("children"in e?e.children.filter(i=>i.visible!==!1):[]).length,r=o/n*1e3;r>3&&t.push({id:ge(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`High visual density: ${o} elements in ${Math.round(n/1e3)}k px\xB2 (${r.toFixed(2)}/1000px\xB2). Consider simplifying or using progressive disclosure.`,currentValue:`${r.toFixed(2)} elements/1000px\xB2`,suggestions:["Reduce visible elements to under 15 per viewport","Group related items","Use progressive disclosure"],autoFixable:!1})}function An(e,t,n,s,o){if(!(s&&"locked"in e&&e.locked)&&!(o&&"visible"in e&&!e.visible)){if(e.type==="TEXT"){let r=e,i=r.fontSize;if(i!==figma.mixed&&typeof i=="number"){t.add(i);let a=r.lineHeight;if(a!==figma.mixed&&typeof a=="object"&&a.unit==="PIXELS"){let c=a.value/i;n.push({fontSize:i,lineHeight:a.value,ratio:c})}}}if("children"in e)for(let r of e.children)An(r,t,n,s,o)}}function hr(e,t,n,s){let o=new Set,r=[];An(e,o,r,n,s);let i=Array.from(o).sort((d,l)=>d-l),a=i.filter(d=>!gr(d));a.length>0&&t.push({id:ge(),type:"accessibility",severity:"info",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`Non-standard font sizes: ${a.join(", ")}px. Consider using a type scale (e.g., 12/14/16/20/24/32).`,currentValue:a.map(d=>`${d}px`).join(", "),suggestions:a.map(d=>{let l=[10,12,14,16,18,20,24,28,32,36,40,48].reduce((p,u)=>Math.abs(u-d)<Math.abs(p-d)?u:p);return`${d}px \u2192 ${l}px`}),autoFixable:!1});let c=r.filter(d=>d.ratio<1.2||d.ratio>2);if(c.length>0){let d=c.reduce((l,p)=>Math.abs(p.ratio-1.5)>Math.abs(l.ratio-1.5)?p:l);t.push({id:ge(),type:"accessibility",severity:"info",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`Line height ratio ${d.ratio.toFixed(2)} (${d.lineHeight}px / ${d.fontSize}px) is outside optimal range 1.3\u20131.6.`,currentValue:`${d.ratio.toFixed(2)}`,suggestions:[`Set line height to ${Math.round(d.fontSize*1.5)}px (1.5\xD7 body) or ${Math.round(d.fontSize*1.3)}px (1.3\xD7 headings)`],autoFixable:!1})}return{sizes:i,lineHeightData:r}}function En(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if("fills"in e){let o=e.fills;if(o!==figma.mixed&&Array.isArray(o))for(let r of o)r.visible!==!1&&r.type==="SOLID"&&t.add(F(r.color.r,r.color.g,r.color.b))}if("strokes"in e){let o=e.strokes;if(Array.isArray(o))for(let r of o)r.visible!==!1&&r.type==="SOLID"&&t.add(F(r.color.r,r.color.g,r.color.b))}if("children"in e)for(let o of e.children)En(o,t,n,s)}}function br(e,t,n,s){let o=new Set;En(e,o,n,s);let r=Array.from(o);return r.length>8&&t.push({id:ge(),type:"accessibility",severity:"warning",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`${r.length} unique colors detected. A cohesive palette typically uses 5\u20137 colors (primary, secondary, accent, neutrals).`,currentValue:`${r.length} colors`,suggestions:["Consolidate similar colors into design tokens","Limit palette to primary, secondary, accent, and 2-3 neutrals"],autoFixable:!1}),r}function vr(e,t,n=4){if(!("children"in e))return 0;let s=e.children.filter(r=>r.visible!==!1),o=0;for(let r of s){if(!("x"in r)||!("y"in r))continue;let i=r.x,a=r.y,c=Math.round(i)%n,d=Math.round(a)%n;(c!==0||d!==0)&&o++}return o>0&&o/Math.max(s.length,1)>.3&&t.push({id:ge(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`${o}/${s.length} direct children are misaligned from ${n}px grid.`,currentValue:`${o} misaligned`,suggestions:[`Snap elements to ${n}px grid for visual consistency`],autoFixable:!1}),o}function Tn(e,t,n,s){if(n&&"locked"in e&&e.locked||s&&"visible"in e&&!e.visible)return;if(/button|btn|cta/i.test(e.name)&&"width"in e&&"height"in e&&t.push({nodeId:e.id,nodeName:e.name,width:e.width,height:e.height}),"children"in e)for(let r of e.children)Tn(r,t,n,s)}function Sr(e,t,n,s){let o=[];if(Tn(e,o,n,s),o.length<2)return;let r=o.map(d=>d.height),i=r.reduce((d,l)=>d+l,0)/r.length,c=Math.max(...r.map(d=>Math.abs(d-i)))/i*100;if(c>15){let d=Math.min(...r),l=Math.max(...r);t.push({id:ge(),type:"accessibility",severity:"warning",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`Button height inconsistency: ${d}px to ${l}px (${Math.round(c)}% variance). Standardize to 2-3 size tiers.`,currentValue:`${d}\u2013${l}px`,suggestions:["Use consistent button heights: 32px (small), 40px (medium), 48px (large)"],autoFixable:!1})}}function Pn(e,t={}){var g,f;xn=0;let n=[],s=(g=t.skipLocked)!=null?g:!0,o=(f=t.skipHidden)!=null?f:!0,r=0,i=[],a=[],c=[],d=0,l=0,p=0;for(let m of e){"children"in m&&"width"in m&&"height"in m&&(yr(m,n),l+=m.children.length,p+=m.width*m.height,r++);let h=hr(m,n,s,o);i=[...new Set([...i,...h.sizes])],a=[...a,...h.lineHeightData],r++;let C=br(m,n,s,o);c=[...new Set([...c,...C])],r++,"children"in m&&(d+=vr(m,n),r++),Sr(m,n,s,o),r++}let u=p>0?l/p*1e3:0;return{issues:n,metrics:{childCount:l,areaPx:p,density:u,uniqueFontSizes:i,lineHeightRatios:a,uniqueColors:c,misalignedCount:d},summary:{totalChecked:r,passed:r-n.length,failed:n.length}}}var xn,fr,Ln=G(()=>{"use strict";J();xn=0;fr=[[10,12,14,16,18,20,24,28,32,36,40,48,56,64,72],[12,14,16,20,24,32,40,48],[12,14,16,18,21,24,30,36,48,60,72]]});function te(){return`mc-${++Rn}`}function $n(e){if(ht.test(e.name))return!0;let t=e.parent,n=0;for(;t&&n<4;){if("name"in t&&ht.test(t.name)||"type"in t&&(t.type==="COMPONENT"||t.type==="INSTANCE")&&"name"in t&&ht.test(t.name))return!0;t=t.parent,n++}return!1}function Mn(e){return e.trim().split(/\s+/).filter(Boolean).length}function Er(e,t){let n=e.characters;if(!n||n.trim().length===0){t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:"Empty text node \u2014 remove or add content.",currentValue:"(empty)",autoFixable:!1});return}let s=n.trim(),o=Mn(s),r=$n(e);if((kr.test(s)||wr.test(s))&&t.push({id:te(),type:"naming",severity:"warning",nodeId:e.id,nodeName:e.name,message:`"${s.substring(0,40)}" \u2014 avoid "click/tap here". Use descriptive action: "Download report", "View details".`,currentValue:s.substring(0,60),suggestions:['Use verb + object: "Download PDF", "View pricing", "Start trial"'],autoFixable:!1}),Nr.test(s)&&t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:'"Learn more" is vague \u2014 specify what the user will learn: "Learn more about pricing".',currentValue:s,suggestions:['Add specificity: "Learn more about [topic]"'],autoFixable:!1}),r&&o<=2){let i=s.toLowerCase().replace(/[.!]/g,"");Cr.has(i)&&t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`Generic CTA "${s}" \u2014 use a specific action: "Save changes", "Send message", "Create account".`,currentValue:s,suggestions:["Replace with verb + object describing the outcome"],autoFixable:!1})}if(r&&o>5&&t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`CTA too long (${o} words): "${s.substring(0,50)}\u2026". Keep CTAs to 2\u20135 words.`,currentValue:`${o} words`,suggestions:["Shorten to verb + object (2-5 words)"],autoFixable:!1}),(Ir.test(s)||xr.test(s))&&t.push({id:te(),type:"naming",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Placeholder text detected: "${s.substring(0,40)}\u2026". Replace with real content.`,currentValue:s.substring(0,60),suggestions:["Replace with actual copy or realistic sample data"],autoFixable:!1}),o>80&&!r&&t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`Long text block (${o} words). Break into shorter paragraphs or use bullet points for readability.`,currentValue:`${o} words`,suggestions:["Break into paragraphs of \u226450 words","Use bullet points for lists","Add subheadings"],autoFixable:!1}),s===s.toUpperCase()&&s!==s.toLowerCase()&&o>3&&t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`All-caps text with ${o} words: "${s.substring(0,40)}\u2026". ALL CAPS reduces readability \u2014 use sentence case or title case.`,currentValue:s.substring(0,60),suggestions:["Use sentence case for readability","Reserve ALL CAPS for short labels (1-2 words)"],autoFixable:!1}),Ar.test(s)){let a=(s.match(/\b\d{4,}\b/g)||[]).filter(c=>{let d=parseInt(c,10);return d<1900||d>2099});a.length>0&&t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`Unformatted number${a.length>1?"s":""}: ${a.join(", ")}. Use thousand separators for readability.`,currentValue:a.join(", "),suggestions:["Format as 1,000,000 or 1 000 000"],autoFixable:!1})}}function On(e,t,n,s,o){var r;if(!(s&&"locked"in e&&e.locked)&&!(o&&"visible"in e&&!e.visible)){if(e.type==="TEXT"){n.totalTextNodes++;let i=((r=e.characters)==null?void 0:r.trim())||"",a=Mn(i);a>0&&(n.wordCounts.push(a),a>n.longestParagraph&&(n.longestParagraph=a)),$n(e)&&n.ctaNodes++,Er(e,t)}if("children"in e)for(let i of e.children)On(i,t,n,s,o)}}function Fn(e,t={}){var a,c;Rn=0;let n=[],s=(a=t.skipLocked)!=null?a:!0,o=(c=t.skipHidden)!=null?c:!0,r={totalTextNodes:0,ctaNodes:0,wordCounts:[],longestParagraph:0};for(let d of e)On(d,n,r,s,o);let i=r.wordCounts.length>0?r.wordCounts.reduce((d,l)=>d+l,0)/r.wordCounts.length:0;return{issues:n,metrics:{totalTextNodes:r.totalTextNodes,ctaNodes:r.ctaNodes,avgWordCount:Math.round(i*10)/10,longestParagraph:r.longestParagraph},summary:{totalChecked:r.totalTextNodes,passed:r.totalTextNodes-n.length,failed:n.length}}}var Rn,kr,Nr,wr,Cr,Ir,xr,Ar,ht,Dn=G(()=>{"use strict";Rn=0;kr=/\bclick\s+here\b/i,Nr=/^learn\s+more\.?$/i,wr=/\btap\s+here\b/i,Cr=new Set(["submit","ok","okay","next","continue","go","yes","no","done","send","save","apply"]),Ir=/\blorem\s+ipsum\b/i,xr=/^(enter\s+text|type\s+here|placeholder|sample\s+text|your\s+text|add\s+text)\.?$/i,Ar=/\b\d{4,}\b/,ht=/button|btn|cta|action|submit|link/i});function ye(){return`conv-${++Un}`}function Lr(e){if(Vn.test(e.name))return!0;let t=e.parent,n=0;for(;t&&n<3;){if("name"in t&&Vn.test(t.name))return!0;t=t.parent,n++}return!1}function Rr(e){return Gn.test(e.name)}function _n(e){if(!("fills"in e))return null;let t=e.fills;if(t===figma.mixed||!Array.isArray(t))return null;let n=t.find(s=>s.type==="SOLID"&&s.visible!==!1);return n?n.color:null}function Bn(e,t,n){let s=[e,t,n].map(o=>o<=.03928?o/12.92:Math.pow((o+.055)/1.055,2.4));return .2126*s[0]+.7152*s[1]+.0722*s[2]}function $r(e,t){let n=Math.max(e,t),s=Math.min(e,t);return(n+.05)/(s+.05)}function zn(e,t,n,s,o){if(!(s&&"locked"in e&&e.locked)&&!(o&&"visible"in e&&!e.visible)&&(Lr(e)&&"width"in e&&"height"in e&&t.push({node:e,x:"x"in e?e.x:0,y:"y"in e?e.y:0,width:e.width,height:e.height,absoluteY:n+("y"in e?e.y:0)}),"children"in e)){let r=n+("y"in e?e.y:0);for(let i of e.children)zn(i,t,r,s,o)}}function Wn(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if(Rr(e)){let o=!1,r=e.parent;if(r&&"children"in r){for(let i of r.children)if(i.type==="TEXT"&&i.id!==e.id){o=!0;break}}t.push({node:e,hasLabel:o})}if("children"in e)for(let o of e.children)Wn(o,t,n,s)}}function _e(e,t,n,s){if(n&&"locked"in e&&e.locked||s&&"visible"in e&&!e.visible)return!1;if(t.test(e.name))return!0;if("children"in e){for(let o of e.children)if(_e(o,t,n,s))return!0}return!1}function Mr(e,t,n){if(t.length===0||!("height"in e))return!1;let s=e.height*.7,o=t.some(r=>r.y+r.height<s);return o||n.push({id:ye(),type:"accessibility",severity:"warning",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:"No primary CTA visible above the fold (top 70% of frame). Move the main action higher for better conversion.",currentValue:`CTA at ${Math.round(t[0].y)}px, fold at ${Math.round(s)}px`,suggestions:["Place primary CTA within top 70% of the viewport","Add a secondary CTA near the top if main CTA must stay below"],autoFixable:!1}),o}function Or(e,t,n){let s=_n(e);if(!s)return;let o=Bn(s.r,s.g,s.b);for(let r of t){let i=_n(r.node);if(!i)continue;let a=Bn(i.r,i.g,i.b),c=$r(o,a);if(c<3){let d=F(i.r,i.g,i.b),l=F(s.r,s.g,s.b);n.push({id:ye(),type:"accessibility",severity:"warning",nodeId:r.node.id,nodeName:r.node.name,message:`CTA contrast ratio ${c.toFixed(1)}:1 (${d} on ${l}) \u2014 too low. CTAs should stand out with \u22653:1 contrast against background.`,currentValue:`${c.toFixed(1)}:1`,suggestions:["Increase CTA background contrast to at least 3:1","Use a bolder accent color for the primary action"],autoFixable:!1})}}}function Fr(e,t,n){t.length>5&&n.push({id:ye(),type:"accessibility",severity:"warning",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`${t.length} form fields on one screen. More than 5 fields increases abandonment \u2014 consider splitting into steps or removing optional fields.`,currentValue:`${t.length} fields`,suggestions:["Split into multi-step form with progress indicator",'Remove optional fields or move to "Advanced" section',"Expedia gained $12M/year by removing one field"],autoFixable:!1});let s=t.filter(o=>!o.hasLabel);s.length>0&&n.push({id:ye(),type:"accessibility",severity:"warning",nodeId:s[0].node.id,nodeName:s[0].node.name,message:`${s.length} form field${s.length===1?"":"s"} without visible labels. Labels improve completion rate and accessibility.`,currentValue:`${s.length} unlabeled`,suggestions:["Add visible label text above or beside each input","Don't rely on placeholder text alone as labels"],autoFixable:!1})}function Dr(e,t,n,s,o){if(t.length<=3)return!1;let r=_e(e,Tr,s,o);return!r&&t.length>5&&n.push({id:ye(),type:"accessibility",severity:"info",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:"Long form without progress indicator. A step counter or progress bar reduces perceived effort.",suggestions:['Add "Step 1 of 3" or a progress bar',"Show users how far they've come and what's left"],autoFixable:!1}),r}function Vr(e,t,n,s,o){if(t.length===0||!_e(e,Gn,s,o))return;_e(e,Pr,s,o)||n.push({id:ye(),type:"accessibility",severity:"info",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:"Form with CTA but no trust signals (security badges, reviews, guarantees). Trust elements near CTAs increase conversion.",suggestions:["Add security badge or lock icon near submit button","Show testimonials, ratings, or guarantees near the CTA"],autoFixable:!1})}function Hn(e,t={}){var l,p;Un=0;let n=[],s=(l=t.skipLocked)!=null?l:!0,o=(p=t.skipHidden)!=null?p:!0,r=0,i=0,a=!1,c=!1,d=0;for(let u of e){let g=[];zn(u,g,0,s,o),r+=g.length;let f=[];Wn(u,f,s,o),i+=f.length,g.length>0&&(Mr(u,g,n)&&(a=!0),Or(u,g,n),d+=2),f.length>0&&(Fr(u,f,n),Dr(u,f,n,s,o)&&(c=!0),d+=2),Vr(u,g,n,s,o),d++}return{issues:n,metrics:{ctaCount:r,formFieldCount:i,ctaAboveFold:a,hasProgressIndicator:c},summary:{totalChecked:d,passed:d-n.length,failed:n.length}}}var Un,Vn,Gn,Tr,Pr,Kn=G(()=>{"use strict";J();Un=0;Vn=/button|btn|cta|action|submit|primary/i,Gn=/input|field|text.?area|select|dropdown|picker|combo|search|email|password|phone|number.?field/i,Tr=/progress|step|stepper|breadcrumb|wizard|indicator|pagination/i,Pr=/badge|trust|security|lock|shield|guarantee|verified|secure|ssl|certification|review|rating|star/i});function Ie(){return`cog-${++qn}`}function zr(e){return _r.test(e.name)}function Wr(e){return Br.test(e.name)}function Xn(e){return Jn.test(e.name)}function Hr(e){return Ur.test(e.name)}function Kr(e){return Gr.test(e.name)}function jr(e){if(!jn.test(e.name)&&!Jn.test(e.name)||!("children"in e))return!1;let t=e.children,n=t.some(o=>o.type==="TEXT");return t.some(o=>o.type==="VECTOR"||o.type==="BOOLEAN_OPERATION"||jn.test(o.name))&&!n}function Yn(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if(Wr(e)){t.push(e);return}if("children"in e)for(let o of e.children)Yn(o,t,n,s)}}function Qn(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)&&(Xn(e)&&t.push(e),"children"in e))for(let o of e.children)Qn(o,t,n,s)}function qr(e){let t=e.match(/h(\d)/i);return t?parseInt(t[1],10):/title|headline/i.test(e)?1:/subtitle|subhead/i.test(e)||/heading/i.test(e)?2:null}function Zn(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if(Hr(e)){let o=qr(e.name);o!==null&&t.push({node:e,level:o})}if("children"in e)for(let o of e.children)Zn(o,t,n,s)}}function es(e,t,n,s){if(n&&"locked"in e&&e.locked||s&&"visible"in e&&!e.visible)return t;let o=t;if("children"in e)for(let r of e.children){let i=es(r,t+1,n,s);i>o&&(o=i)}return o}function ts(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)&&(Kr(e)&&"opacity"in e&&e.opacity<1&&t.push(e),"children"in e))for(let o of e.children)ts(o,t,n,s)}function ns(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)&&(Xn(e)&&jr(e)&&t.push(e),"children"in e))for(let o of e.children)ns(o,t,n,s)}function Jr(e,t,n,s){let o=[];ss(e,o,n,s);let r=0;for(let i of o){let a=[];Yn(i,a,n,s),r+=a.length,a.length>7&&t.push({id:Ie(),type:"accessibility",severity:"warning",nodeId:i.id,nodeName:i.name,message:`Navigation has ${a.length} items \u2014 Miller's Law suggests 7\xB12 is the working memory limit. Consider grouping or progressive disclosure.`,currentValue:`${a.length} nav items`,suggestions:["Group related items under expandable sections",'Use "More" menu for less-used items',"Limit primary navigation to 5-7 items"],autoFixable:!1})}return r}function ss(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if(zr(e)){t.push(e);return}if("children"in e)for(let o of e.children)ss(o,t,n,s)}}function Xr(e,t,n,s){let o=[];return Qn(e,o,n,s),o.length>5&&t.push({id:Ie(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`${o.length} CTAs/buttons on one screen \u2014 choice overload reduces decision-making ability (Hick's Law). Prioritize one primary action.`,currentValue:`${o.length} CTAs`,suggestions:["Establish clear primary/secondary/tertiary action hierarchy","Reduce to 1 primary CTA per viewport","Group related actions in a dropdown or overflow menu"],autoFixable:!1}),o.length}function Yr(e,t,n,s){let o=[];if(Zn(e,o,n,s),o.length<2)return o.map(i=>i.level);let r=o.sort((i,a)=>{let c="y"in i.node?i.node.y:0,d="y"in a.node?a.node.y:0;return c-d});for(let i=1;i<r.length;i++){let a=r[i-1].level,c=r[i].level;c>a+1&&t.push({id:Ie(),type:"accessibility",severity:"info",nodeId:r[i].node.id,nodeName:r[i].node.name,message:`Heading hierarchy gap: jumps from level ${a} to level ${c}. Screen readers and users rely on sequential heading structure.`,currentValue:`h${a} \u2192 h${c}`,suggestions:[`Add an h${a+1} between these levels`,"Ensure headings follow a logical descending order"],autoFixable:!1})}return r.map(i=>i.level)}function Qr(e,t,n,s){var r;let o=[];ts(e,o,n,s);for(let i of o){let a=i.parent,c=!1;if(a&&"children"in a){for(let d of a.children)if(d.type==="TEXT"&&d.id!==i.id){let l=((r=d.characters)==null?void 0:r.toLowerCase())||"";if(l.includes("required")||l.includes("complete")||l.includes("fill")||l.includes("select")||l.includes("first")){c=!0;break}}}c||t.push({id:Ie(),type:"accessibility",severity:"info",nodeId:i.id,nodeName:i.name,message:`Disabled element "${i.name}" without visible explanation. Users should understand WHY an action is unavailable and how to enable it.`,suggestions:["Add helper text explaining what needs to happen first","Use a tooltip on hover explaining the disabled state",'Show a brief inline message (e.g., "Complete all fields to continue")'],autoFixable:!1})}}function Zr(e,t,n,s){let o=[];return ns(e,o,n,s),o.length>3&&t.push({id:Ie(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`${o.length} icon-only buttons without text labels. Icons alone are ambiguous \u2014 add labels or ensure tooltips are present.`,currentValue:`${o.length} icon-only`,suggestions:["Add visible text labels to icon buttons","Add tooltips that appear on hover/focus","Use aria-label for accessibility (ensure design indicates this)"],autoFixable:!1}),o.length}function os(e,t={}){var p,u;qn=0;let n=[],s=(p=t.skipLocked)!=null?p:!0,o=(u=t.skipHidden)!=null?u:!0,r=0,i=0,a=0,c=[],d=0,l=0;for(let g of e){r+=Jr(g,n,s,o),l++,i+=Xr(g,n,s,o),l++;let f=Yr(g,n,s,o);c=[...c,...f],l++,Qr(g,n,s,o),l++,d+=Zr(g,n,s,o),l++;let m=es(g,0,s,o);m>a&&(a=m)}return{issues:n,metrics:{navItemCount:r,ctaCount:i,maxNestingDepth:a,headingLevels:[...new Set(c)].sort(),iconOnlyButtons:d},summary:{totalChecked:l,passed:l-n.length,failed:n.length}}}var qn,_r,Br,Jn,Ur,Gr,jn,rs=G(()=>{"use strict";qn=0;_r=/nav|menu|sidebar|tab.?bar|bottom.?bar|header.?nav|navigation|top.?bar/i,Br=/nav.?item|menu.?item|tab(?!le)|link/i,Jn=/button|btn|cta|action|submit|primary/i,Ur=/heading|title|h[1-6]|headline/i,Gr=/disabled|inactive|dimmed|greyed/i,jn=/icon|ico|svg|glyph/i});function ei(){return`fitts-${++is}`}function ni(e){return ti.test(e.name)}function as(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,failed:0};if(s&&i)return{checked:0,failed:0};let a=0,c=0;if(ni(e)&&"width"in e&&"height"in e){a++;let d=e.width,l=e.height;(d<he||l<he)&&(c++,t.push({id:ei(),type:"fittsLaw",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Interactive target "${e.name}" is ${Math.round(d)}x${Math.round(l)}px \u2014 minimum recommended size is ${he}x${he}px (WCAG 2.5.8)`,currentValue:`${Math.round(d)}x${Math.round(l)}px`,suggestions:[`Increase to at least ${he}x${he}px`,"Add padding to increase the hit area"],autoFixable:!1}))}if("children"in e)for(let d of e.children){let l=as(d,t,n,s,r);a+=l.checked,c+=l.failed}return{checked:a,failed:c}}function cs(e,t={}){var a,c;is=0;let n=[],s=(a=t.skipLocked)!=null?a:!0,o=(c=t.skipHidden)!=null?c:!0,r=0,i=0;for(let d of e){let l=as(d,n,s,o,!1);r+=l.checked,i+=l.failed}return{issues:n,summary:{totalChecked:r,passed:r-i,failed:i}}}var is,ti,he,ls=G(()=>{"use strict";is=0;ti=/button|btn|cta|action|submit|link|toggle|switch|checkbox|radio|tab(?!le)/i,he=44});function si(){return`gestalt-${++ds}`}function us(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,failed:0};if(s&&i)return{checked:0,failed:0};let a=0,c=0;if((e.type==="FRAME"||e.type==="COMPONENT"||e.type==="INSTANCE")&&"children"in e){let l=e;if(l.layoutMode==="NONE"&&l.children.length>=3){a++;let p=l.children.filter(u=>"visible"in u&&u.visible&&"y"in u);if(p.length>=3){let u=[...p].sort((f,m)=>f.y-m.y),g=[];for(let f=1;f<u.length;f++){let m=u[f-1].y+u[f-1].height,h=u[f].y;g.push(h-m)}if(g.length>=2){let f=new Set(g.map(m=>Math.round(m)));f.size>2&&(c++,t.push({id:si(),type:"gestalt",severity:"info",nodeId:e.id,nodeName:e.name,message:`Frame "${e.name}" has ${f.size} different spacing gaps between children (${[...f].join(", ")}px) \u2014 inconsistent proximity weakens visual grouping (Gestalt proximity principle)`,currentValue:`${f.size} distinct gaps`,suggestions:["Use auto-layout with consistent gap spacing","Standardize spacing between sibling elements"],autoFixable:!1}))}}}}if("children"in e)for(let l of e.children){let p=us(l,t,n,s,r);a+=p.checked,c+=p.failed}return{checked:a,failed:c}}function ps(e,t={}){var a,c;ds=0;let n=[],s=(a=t.skipLocked)!=null?a:!0,o=(c=t.skipHidden)!=null?c:!0,r=0,i=0;for(let d of e){let l=us(d,n,s,o,!1);r+=l.checked,i+=l.failed}return{issues:n,summary:{totalChecked:r,passed:r-i,failed:i}}}var ds,ms=G(()=>{"use strict";ds=0});function oi(){return`detach-${++fs}`}function gs(e,t,n,s,o){var d;let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,failed:0};if(s&&i)return{checked:0,failed:0};let a=0,c=0;if(e.type==="FRAME"&&"children"in e){a++;let l=ri.test(e.name),p=ii.test(e.name)&&((d=e.parent)==null?void 0:d.type)!=="PAGE"&&e.children.length>0;if(l)c++,t.push({id:oi(),type:"detachedInstance",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Frame "${e.name}" appears to be a detached component instance. Detaching breaks the link to the source component and prevents design system updates.`,currentValue:"Detached instance",suggestions:["Re-attach by replacing with the original component instance",'If intentional, rename to remove "detach" from the name'],autoFixable:!1});else if(p){let u=e.name.split(/[\s\-\/]/);u.length>=2&&u.every(g=>g.length>0)}}if("children"in e)for(let l of e.children){let p=gs(l,t,n,s,r);a+=p.checked,c+=p.failed}return{checked:a,failed:c}}function ys(e,t={}){var a,c;fs=0;let n=[],s=(a=t.skipLocked)!=null?a:!0,o=(c=t.skipHidden)!=null?c:!0,r=0,i=0;for(let d of e){let l=gs(d,n,s,o,!1);r+=l.checked,i+=l.failed}return{issues:n,summary:{totalChecked:r,passed:r-i,failed:i}}}var fs,ri,ii,hs=G(()=>{"use strict";fs=0;ri=/detach/i,ii=/^[A-Z][a-zA-Z]+(?:\s*[-\/]\s*[A-Za-z]+)*$/});function bt(){return`resp-${++vs}`}function Ss(e){return e.type==="FRAME"||e.type==="COMPONENT"||e.type==="INSTANCE"}function bs(e){for(let t of ai){let n=e.match(t);if(n){for(let s of n.slice(1))if(ci.has(s.toLowerCase()))return s.toLowerCase()}}return null}function ks(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,failed:0,fixedWidthCount:0};if(s&&i)return{checked:0,failed:0,fixedWidthCount:0};let a=0,c=0,d=0;if(Ss(e)){let l=e;a++;let p=l.layoutSizingHorizontal==="FIXED"||l.layoutSizingHorizontal===void 0,u=!l.parent||l.parent.type==="PAGE",g=l.layoutMode!=="NONE",f="minWidth"in l&&l.minWidth!==null&&l.minWidth!==void 0||"maxWidth"in l&&l.maxWidth!==null&&l.maxWidth!==void 0;p&&!u&&!f&&g&&l.width>200&&(d++,c++,t.push({id:bt(),type:"responsive",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Frame "${e.name}" has fixed width (${Math.round(l.width)}px) with auto-layout but no fill/hug sizing \u2014 may not adapt to different screen sizes`,currentValue:`${Math.round(l.width)}px fixed`,suggestions:['Set horizontal sizing to "Fill" for responsive behavior',"Add min-width/max-width constraints",'Use "Hug contents" if the frame should shrink-wrap'],autoFixable:!1}))}if("children"in e)for(let l of e.children){let p=ks(l,t,n,s,r);a+=p.checked,c+=p.failed,d+=p.fixedWidthCount}return{checked:a,failed:c,fixedWidthCount:d}}function Ns(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,failed:0,riskCount:0};if(s&&i)return{checked:0,failed:0,riskCount:0};let a=0,c=0,d=0;if(e.type==="TEXT"){let l=e;a++;let p=l.fontSize!==figma.mixed?l.fontSize:14,u=l.textAutoResize;if(u==="NONE"||u==="TRUNCATE"){let g=l.characters.length,f=g*p*li,m=l.width;g>5&&f>m*.8&&(d++,c++,t.push({id:bt(),type:"responsive",severity:"info",nodeId:e.id,nodeName:e.name,message:`Text "${e.name}" may truncate \u2014 content fills ~${Math.round(f/m*100)}% of fixed width (${Math.round(m)}px). Translations or dynamic content could overflow.`,currentValue:`${g} chars in ${Math.round(m)}px`,suggestions:["Use auto-width or auto-height text resizing","Allow text to wrap by setting textAutoResize to HEIGHT","Add ellipsis handling if truncation is intentional"],autoFixable:!1}))}}if("children"in e)for(let l of e.children){let p=Ns(l,t,n,s,r);a+=p.checked,c+=p.failed,d+=p.riskCount}return{checked:a,failed:c,riskCount:d}}function ws(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,failed:0,missingCount:0};if(s&&i)return{checked:0,failed:0,missingCount:0};let a=0,c=0,d=0;if(Ss(e)){let l=e;if(l.layoutMode==="HORIZONTAL"&&"children"in l){let p=l.children.filter(u=>"visible"in u&&u.visible);p.length>=3&&(a++,("layoutWrap"in l?l.layoutWrap:"NO_WRAP")!=="WRAP"&&(d++,c++,t.push({id:bt(),type:"responsive",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Horizontal layout "${e.name}" has ${p.length} children without wrap \u2014 content won't reflow on smaller screens`,currentValue:`${p.length} children, no wrap`,suggestions:['Enable "Wrap" on the auto-layout to allow content reflow',"Consider switching to vertical layout on mobile breakpoints","Use min-width on children to control when wrapping occurs"],autoFixable:!1})))}}if("children"in e)for(let l of e.children){let p=ws(l,t,n,s,r);a+=p.checked,c+=p.failed,d+=p.missingCount}return{checked:a,failed:c,missingCount:d}}function di(e){let t=new Set;for(let n of e)ui(n,t);return Array.from(t)}function ui(e,t){if(bs(e.name)&&t.add(e.name),"children"in e)for(let s of e.children)bs(s.name)&&t.add(s.name)}function Cs(e,t){var p,u;vs=0;let n=[],s=(p=t==null?void 0:t.skipLocked)!=null?p:!0,o=(u=t==null?void 0:t.skipHidden)!=null?u:!0,r=0,i=0,a=0,c=0,d=0;for(let g of e){let f=ks(g,n,s,o,!1);r+=f.checked,i+=f.failed,a+=f.fixedWidthCount;let m=Ns(g,n,s,o,!1);r+=m.checked,i+=m.failed,c+=m.riskCount;let h=ws(g,n,s,o,!1);r+=h.checked,i+=h.failed,d+=h.missingCount}let l=di(e);return{issues:n,metrics:{fixedWidthElements:a,textTruncationRisk:c,missingAutoLayout:d,breakpointVariants:l},summary:{totalChecked:r,passed:r-i,failed:i}}}var vs,ai,ci,li,Is=G(()=>{"use strict";vs=0;ai=[/^(.+)\s*[-–—]\s*(desktop|tablet|mobile|phone|sm|md|lg|xl|xxl|small|medium|large)\s*$/i,/^(.+)\/(desktop|tablet|mobile|phone|sm|md|lg|xl|xxl|small|medium|large)\s*$/i,/^(desktop|tablet|mobile|phone|sm|md|lg|xl|xxl|small|medium|large)\s*[-–—/]\s*(.+)$/i,/^(.+)\s*\[(desktop|tablet|mobile|phone|sm|md|lg|xl|xxl|small|medium|large)\]\s*$/i],ci=new Set(["desktop","tablet","mobile","phone","sm","md","lg","xl","xxl","small","medium","large"]),li=.5});var Et={};en(Et,{DEFAULT_LINT_SETTINGS:()=>j,clearIgnored:()=>Ct,findNodesWithSameValue:()=>At,getIgnoredState:()=>It,ignoreAllOfType:()=>wt,ignoreError:()=>Nt,ignoreNode:()=>kt,lintSelection:()=>xe,restoreIgnoredState:()=>xt,runDesignLint:()=>ae});function U(e,t,n){return n?`${e}::${t}::${n}`:`${e}::${t}`}function kt(e){B.add(e)}function Nt(e,t,n){D.add(U(e,t,n))}function wt(e,t){for(let n of e)n.errorType===t&&D.add(U(n.nodeId,n.errorType))}function Ct(){B.clear(),D.clear()}function It(){return{nodeIds:Array.from(B),errorKeys:Array.from(D)}}function xt(e){B=new Set(e.nodeIds),D=new Set(e.errorKeys)}function As(e){if(e.type==="SOLID"){let{r:t,g:n,b:s}=e.color,o=F(t,n,s),r=e.opacity!==void 0&&e.opacity<1?` (${Math.round(e.opacity*100)}%)`:"";return o+r}return e.type==="IMAGE"?"Image fill":e.type==="VIDEO"?"Video fill":e.type.includes("GRADIENT")?`${e.type.replace("GRADIENT_","").toLowerCase()} gradient`:e.type}function Ue(e,t){try{if("boundVariables"in e){let n=e.boundVariables;if(n&&n[t])return!0}}catch(n){}return!1}function Be(e,t,n){if(!("fills"in e))return;let s=e.fills;if(s===figma.mixed||!Array.isArray(s))return;let o=s.filter(r=>r.visible!==!1);if(o.length!==0&&!Ue(e,"fills")){if("fillStyleId"in e){let r=e.fillStyleId;if(r&&r!==""&&r!==figma.mixed)return}for(let r of o){try{let a=r.boundVariables;if(a&&a.color)continue}catch(a){}let i=As(r);t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"fill",message:`Missing fill style: ${i}`,value:i,path:n})}}}function vt(e,t,n){if(!("strokes"in e))return;let s=e.strokes;if(!Array.isArray(s))return;let o=s.filter(r=>r.visible!==!1);if(o.length!==0&&!Ue(e,"strokes")){if("strokeStyleId"in e){let r=e.strokeStyleId;if(r&&r!==""&&r!==figma.mixed)return}for(let r of o){let i=As(r),a="strokeWeight"in e?` (${e.strokeWeight}px)`:"";t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"stroke",message:`Missing stroke style: ${i}${a}`,value:i+a,path:n})}}}function St(e,t,n){if(!("effects"in e))return;let s=e.effects;if(!Array.isArray(s)||s.length===0)return;let o=s.filter(i=>i.visible!==!1);if(o.length===0)return;if("effectStyleId"in e){let i=e.effectStyleId;if(i&&i!==""&&i!==figma.mixed)return}let r=o.map(i=>{let a=[i.type.replace(/_/g," ").toLowerCase()];if("radius"in i&&a.push(`r:${i.radius}`),"color"in i&&i.color){let c=i.color;a.push(F(c.r,c.g,c.b))}return a.join(" ")});t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"effect",message:`Missing effect style: ${r.join(", ")}`,value:r.join(", "),path:n})}function pi(e,t,n){if("textStyleId"in e){let a=e.textStyleId;if(a&&a!==""&&a!==figma.mixed)return}let s=e.fontName!==figma.mixed?e.fontName:null,o=e.fontSize!==figma.mixed?e.fontSize:null,r=[];s&&r.push(`${s.family} ${s.style}`),o&&r.push(`${o}px`);let i=r.join(" / ")||"unknown text style";t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"text",message:`Missing text style: ${i}`,value:i,path:n})}function xs(e,t,n,s){if(!("cornerRadius"in e)||Ue(e,"topLeftRadius")||Ue(e,"cornerRadius"))return;let o=e.cornerRadius;if(o===figma.mixed){let r=[e.topLeftRadius,e.topRightRadius,e.bottomLeftRadius,e.bottomRightRadius].filter(i=>i!=null);for(let i of r)if(!s.includes(i)){t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"radius",message:`Non-standard border radius: ${i}px (allowed: ${s.join(", ")})`,value:`${i}px`,path:n});break}return}typeof o=="number"&&o>0&&!s.includes(o)&&t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"radius",message:`Non-standard border radius: ${o}px (allowed: ${s.join(", ")})`,value:`${o}px`,path:n})}function mi(e,t,n,s){if(!(e.type==="GROUP"||e.type==="SLICE"||e.type==="CONNECTOR")&&e.type!=="COMPONENT_SET")switch(e.type){case"TEXT":t.checkTextStyles&&pi(e,n,s),t.checkFills&&Be(e,n,s);break;case"FRAME":case"SECTION":t.checkFills&&Be(e,n,s),t.checkStrokes&&vt(e,n,s),t.checkEffects&&St(e,n,s),t.checkRadius&&xs(e,n,s,t.allowedRadii);break;case"RECTANGLE":case"COMPONENT":case"INSTANCE":t.checkFills&&Be(e,n,s),t.checkStrokes&&vt(e,n,s),t.checkEffects&&St(e,n,s),t.checkRadius&&xs(e,n,s,t.allowedRadii);break;case"ELLIPSE":case"POLYGON":case"STAR":case"VECTOR":case"LINE":case"BOOLEAN_OPERATION":t.checkFills&&Be(e,n,s),t.checkStrokes&&vt(e,n,s),t.checkEffects&&St(e,n,s);break}}function Es(e,t,n,s,o){let r=0,i=o||"locked"in e&&e.locked,a="visible"in e&&!e.visible;if(t.skipLockedLayers&&i||t.skipHiddenLayers&&a)return 0;let c=s?`${s} > ${e.name}`:e.name;if(r++,!B.has(e.id)){let d=n.length;mi(e,t,n,c);for(let l=n.length-1;l>=d;l--){let p=n[l];(D.has(U(p.nodeId,p.errorType))||D.has(U(p.nodeId,p.errorType,p.value)))&&n.splice(l,1)}}if("children"in e)for(let d of e.children)r+=Es(d,t,n,c,i);return r}function q(e,t){for(let n of t)if(new RegExp("^"+n.replace(/[.+^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*").replace(/\?/g,".")+"$").test(e))return!0;return!1}function ae(e,t=j){var u,g;let n=[],s=0,o=t.ignorePatterns||[],r=t.severityOverrides||{};for(let f of e)s+=Es(f,t,n,"",!1);let i={skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers,scale:t.spacingScale};if(t.checkSpacing&&r.spacing!=="off"){let f=yn(e,i);for(let m of f.issues){let h=m.currentValue||"";B.has(m.nodeId)||D.has(U(m.nodeId,"spacing"))||D.has(U(m.nodeId,"spacing",h))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"spacing",message:m.message,value:h,path:m.nodeName,property:(g=(u=m.fixAction)==null?void 0:u.params)==null?void 0:g.property})}}if(t.checkAutoLayout&&r.autoLayout!=="off"){let f=Sn(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||D.has(U(m.nodeId,"autoLayout"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"autoLayout",message:m.message,value:m.currentValue||"",path:m.nodeName})}if(t.checkAccessibility&&r.accessibility!=="off"){let f=Cn(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||D.has(U(m.nodeId,"accessibility"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"accessibility",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkVisualQuality&&r.visualQuality!=="off"){let f=Pn(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||D.has(U(m.nodeId,"visualQuality"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"visualQuality",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkMicrocopy&&r.microcopy!=="off"){let f=Fn(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||D.has(U(m.nodeId,"microcopy"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"TEXT",errorType:"microcopy",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkConversion&&r.conversion!=="off"){let f=Hn(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||D.has(U(m.nodeId,"conversion"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"conversion",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkCognitive&&r.cognitive!=="off"){let f=os(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||D.has(U(m.nodeId,"cognitive"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"cognitive",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkFittsLaw&&r.fittsLaw!=="off"){let f=cs(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||D.has(U(m.nodeId,"fittsLaw"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"fittsLaw",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkGestalt&&r.gestalt!=="off"){let f=ps(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||D.has(U(m.nodeId,"gestalt"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"gestalt",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkDetachedInstances&&r.detachedInstance!=="off"){let f=ys(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||D.has(U(m.nodeId,"detachedInstance"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"detachedInstance",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkResponsive&&r.responsive!=="off"){let f=Cs(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||D.has(U(m.nodeId,"responsive"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"responsive",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}let a=n.filter(f=>r[f.errorType]!=="off"),c=o.length>0?a.filter(f=>!q(f.nodeName,o)):a;for(let f of c){let m=r[f.errorType];if(m&&m!=="off")f.severity=m;else if(!f.severity)switch(f.errorType){case"fill":case"stroke":case"effect":case"text":case"spacing":f.severity="warning";break;case"radius":case"autoLayout":f.severity="info";break;case"accessibility":f.severity="critical";break;case"visualQuality":f.severity="warning";break;case"microcopy":f.severity="info";break;case"conversion":f.severity="warning";break;case"cognitive":f.severity="info";break;case"responsive":f.severity="warning";break;case"fittsLaw":f.severity="warning";break;case"gestalt":f.severity="info";break;case"detachedInstance":f.severity="warning";break}}let d=new Set(c.map(f=>f.nodeId)).size,l={fill:0,stroke:0,effect:0,text:0,radius:0,spacing:0,autoLayout:0,accessibility:0,visualQuality:0,microcopy:0,conversion:0,cognitive:0,fittsLaw:0,gestalt:0,detachedInstance:0,responsive:0};for(let f of c)l[f.errorType]++;let p={totalErrors:c.length,byType:l,totalNodes:s,nodesWithErrors:d};return{errors:c,ignoredNodeIds:Array.from(B),ignoredErrorKeys:Array.from(D),summary:p}}function xe(e){let t=figma.currentPage.selection;return t.length===0?{errors:[],ignoredNodeIds:[],ignoredErrorKeys:[],summary:{totalErrors:0,byType:{fill:0,stroke:0,effect:0,text:0,radius:0,spacing:0,autoLayout:0,accessibility:0,visualQuality:0,microcopy:0,conversion:0,cognitive:0,fittsLaw:0,gestalt:0,detachedInstance:0,responsive:0},totalNodes:0,nodesWithErrors:0}}:ae(t,e)}function At(e,t,n,s=j){return ae(e,s).errors.filter(r=>r.errorType===t&&r.value===n)}var j,B,D,be=G(()=>{"use strict";J();hn();kn();In();Ln();Dn();Kn();rs();ls();ms();hs();Is();j={checkFills:!0,checkStrokes:!0,checkEffects:!0,checkTextStyles:!0,checkRadius:!0,checkSpacing:!0,checkAutoLayout:!0,checkAccessibility:!0,checkVisualQuality:!0,checkMicrocopy:!0,checkConversion:!0,checkCognitive:!0,checkFittsLaw:!0,checkGestalt:!0,checkDetachedInstances:!0,checkResponsive:!0,allowedRadii:[0,2,4,8,12,16,24,32],skipLockedLayers:!0,skipHiddenLayers:!0},B=new Set,D=new Set});var js=To((Bl,We)=>{var Bt=function(){var e=String.fromCharCode,t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",s={};function o(i,a){if(!s[i]){s[i]={};for(var c=0;c<i.length;c++)s[i][i.charAt(c)]=c}return s[i][a]}var r={compressToBase64:function(i){if(i==null)return"";var a=r._compress(i,6,function(c){return t.charAt(c)});switch(a.length%4){default:case 0:return a;case 1:return a+"===";case 2:return a+"==";case 3:return a+"="}},decompressFromBase64:function(i){return i==null?"":i==""?null:r._decompress(i.length,32,function(a){return o(t,i.charAt(a))})},compressToUTF16:function(i){return i==null?"":r._compress(i,15,function(a){return e(a+32)})+" "},decompressFromUTF16:function(i){return i==null?"":i==""?null:r._decompress(i.length,16384,function(a){return i.charCodeAt(a)-32})},compressToUint8Array:function(i){for(var a=r.compress(i),c=new Uint8Array(a.length*2),d=0,l=a.length;d<l;d++){var p=a.charCodeAt(d);c[d*2]=p>>>8,c[d*2+1]=p%256}return c},decompressFromUint8Array:function(i){if(i==null)return r.decompress(i);for(var a=new Array(i.length/2),c=0,d=a.length;c<d;c++)a[c]=i[c*2]*256+i[c*2+1];var l=[];return a.forEach(function(p){l.push(e(p))}),r.decompress(l.join(""))},compressToEncodedURIComponent:function(i){return i==null?"":r._compress(i,6,function(a){return n.charAt(a)})},decompressFromEncodedURIComponent:function(i){return i==null?"":i==""?null:(i=i.replace(/ /g,"+"),r._decompress(i.length,32,function(a){return o(n,i.charAt(a))}))},compress:function(i){return r._compress(i,16,function(a){return e(a)})},_compress:function(i,a,c){if(i==null)return"";var d,l,p={},u={},g="",f="",m="",h=2,C=3,S=2,N=[],y=0,b=0,I;for(I=0;I<i.length;I+=1)if(g=i.charAt(I),Object.prototype.hasOwnProperty.call(p,g)||(p[g]=C++,u[g]=!0),f=m+g,Object.prototype.hasOwnProperty.call(p,f))m=f;else{if(Object.prototype.hasOwnProperty.call(u,m)){if(m.charCodeAt(0)<256){for(d=0;d<S;d++)y=y<<1,b==a-1?(b=0,N.push(c(y)),y=0):b++;for(l=m.charCodeAt(0),d=0;d<8;d++)y=y<<1|l&1,b==a-1?(b=0,N.push(c(y)),y=0):b++,l=l>>1}else{for(l=1,d=0;d<S;d++)y=y<<1|l,b==a-1?(b=0,N.push(c(y)),y=0):b++,l=0;for(l=m.charCodeAt(0),d=0;d<16;d++)y=y<<1|l&1,b==a-1?(b=0,N.push(c(y)),y=0):b++,l=l>>1}h--,h==0&&(h=Math.pow(2,S),S++),delete u[m]}else for(l=p[m],d=0;d<S;d++)y=y<<1|l&1,b==a-1?(b=0,N.push(c(y)),y=0):b++,l=l>>1;h--,h==0&&(h=Math.pow(2,S),S++),p[f]=C++,m=String(g)}if(m!==""){if(Object.prototype.hasOwnProperty.call(u,m)){if(m.charCodeAt(0)<256){for(d=0;d<S;d++)y=y<<1,b==a-1?(b=0,N.push(c(y)),y=0):b++;for(l=m.charCodeAt(0),d=0;d<8;d++)y=y<<1|l&1,b==a-1?(b=0,N.push(c(y)),y=0):b++,l=l>>1}else{for(l=1,d=0;d<S;d++)y=y<<1|l,b==a-1?(b=0,N.push(c(y)),y=0):b++,l=0;for(l=m.charCodeAt(0),d=0;d<16;d++)y=y<<1|l&1,b==a-1?(b=0,N.push(c(y)),y=0):b++,l=l>>1}h--,h==0&&(h=Math.pow(2,S),S++),delete u[m]}else for(l=p[m],d=0;d<S;d++)y=y<<1|l&1,b==a-1?(b=0,N.push(c(y)),y=0):b++,l=l>>1;h--,h==0&&(h=Math.pow(2,S),S++)}for(l=2,d=0;d<S;d++)y=y<<1|l&1,b==a-1?(b=0,N.push(c(y)),y=0):b++,l=l>>1;for(;;)if(y=y<<1,b==a-1){N.push(c(y));break}else b++;return N.join("")},decompress:function(i){return i==null?"":i==""?null:r._decompress(i.length,32768,function(a){return i.charCodeAt(a)})},_decompress:function(i,a,c){var d=[],l,p=4,u=4,g=3,f="",m=[],h,C,S,N,y,b,I,w={val:c(0),position:a,index:1};for(h=0;h<3;h+=1)d[h]=h;for(S=0,y=Math.pow(2,2),b=1;b!=y;)N=w.val&w.position,w.position>>=1,w.position==0&&(w.position=a,w.val=c(w.index++)),S|=(N>0?1:0)*b,b<<=1;switch(l=S){case 0:for(S=0,y=Math.pow(2,8),b=1;b!=y;)N=w.val&w.position,w.position>>=1,w.position==0&&(w.position=a,w.val=c(w.index++)),S|=(N>0?1:0)*b,b<<=1;I=e(S);break;case 1:for(S=0,y=Math.pow(2,16),b=1;b!=y;)N=w.val&w.position,w.position>>=1,w.position==0&&(w.position=a,w.val=c(w.index++)),S|=(N>0?1:0)*b,b<<=1;I=e(S);break;case 2:return""}for(d[3]=I,C=I,m.push(I);;){if(w.index>i)return"";for(S=0,y=Math.pow(2,g),b=1;b!=y;)N=w.val&w.position,w.position>>=1,w.position==0&&(w.position=a,w.val=c(w.index++)),S|=(N>0?1:0)*b,b<<=1;switch(I=S){case 0:for(S=0,y=Math.pow(2,8),b=1;b!=y;)N=w.val&w.position,w.position>>=1,w.position==0&&(w.position=a,w.val=c(w.index++)),S|=(N>0?1:0)*b,b<<=1;d[u++]=e(S),I=u-1,p--;break;case 1:for(S=0,y=Math.pow(2,16),b=1;b!=y;)N=w.val&w.position,w.position>>=1,w.position==0&&(w.position=a,w.val=c(w.index++)),S|=(N>0?1:0)*b,b<<=1;d[u++]=e(S),I=u-1,p--;break;case 2:return m.join("")}if(p==0&&(p=Math.pow(2,g),g++),d[I])f=d[I];else if(I===u)f=C+C.charAt(0);else return null;m.push(f),d[u++]=C+f.charAt(0),p--,C=f,p==0&&(p=Math.pow(2,g),g++)}}};return r}();typeof define=="function"&&define.amd?define(function(){return Bt}):typeof We!="undefined"&&We!=null?We.exports=Bt:typeof angular!="undefined"&&angular!=null&&angular.module("LZString",[]).factory("LZString",function(){return Bt})});var so={};en(so,{applyEffectStyle:()=>Kt,applyFillStyle:()=>zt,applyStrokeStyle:()=>Wt,applyTextStyle:()=>Ht});async function zt(e,t){let n=figma.getNodeById(e);if(!n||!("fillStyleId"in n))return{success:!1,nodeId:e,nodeName:"",property:"fillStyle",oldValue:"",newValue:"",error:"Node not found or does not support fill styles"};try{let s=await figma.importStyleByKeyAsync(t),o=n.fillStyleId||"";return n.fillStyleId=s.id,{success:!0,nodeId:e,nodeName:n.name,property:"fillStyle",oldValue:o?"existing style":"no style",newValue:s.name}}catch(s){return{success:!1,nodeId:e,nodeName:n.name,property:"fillStyle",oldValue:"",newValue:t,error:s instanceof Error?s.message:String(s)}}}async function Wt(e,t){let n=figma.getNodeById(e);if(!n||!("strokeStyleId"in n))return{success:!1,nodeId:e,nodeName:"",property:"strokeStyle",oldValue:"",newValue:"",error:"Node not found or does not support stroke styles"};try{let s=await figma.importStyleByKeyAsync(t),o=n.strokeStyleId||"";return n.strokeStyleId=s.id,{success:!0,nodeId:e,nodeName:n.name,property:"strokeStyle",oldValue:o?"existing style":"no style",newValue:s.name}}catch(s){return{success:!1,nodeId:e,nodeName:n.name,property:"strokeStyle",oldValue:"",newValue:t,error:s instanceof Error?s.message:String(s)}}}async function Ht(e,t){let n=figma.getNodeById(e);if(!n||n.type!=="TEXT")return{success:!1,nodeId:e,nodeName:"",property:"textStyle",oldValue:"",newValue:"",error:"Node not found or is not a text node"};try{let s=await figma.importStyleByKeyAsync(t),o=n,r=o.textStyleId||"";return o.textStyleId=s.id,{success:!0,nodeId:e,nodeName:n.name,property:"textStyle",oldValue:r?"existing style":"no style",newValue:s.name}}catch(s){return{success:!1,nodeId:e,nodeName:n.name,property:"textStyle",oldValue:"",newValue:t,error:s instanceof Error?s.message:String(s)}}}async function Kt(e,t){let n=figma.getNodeById(e);if(!n||!("effectStyleId"in n))return{success:!1,nodeId:e,nodeName:"",property:"effectStyle",oldValue:"",newValue:"",error:"Node not found or does not support effect styles"};try{let s=await figma.importStyleByKeyAsync(t),o=n.effectStyleId||"";return n.effectStyleId=s.id,{success:!0,nodeId:e,nodeName:n.name,property:"effectStyle",oldValue:o?"existing style":"no style",newValue:s.name}}catch(s){return{success:!1,nodeId:e,nodeName:n.name,property:"effectStyle",oldValue:"",newValue:t,error:s instanceof Error?s.message:String(s)}}}var jt=G(()=>{"use strict"});J();J();J();function de(e){var d;let t=e,n=!1;for(;t;){if(t.type==="COMPONENT_SET"){n=!0;break}if(t.parent&&t.parent.type==="COMPONENT_SET"){n=!0;break}t=t.parent}if(!n||!("strokes"in e)||!("cornerRadius"in e)||!("strokeWeight"in e))return!1;let s=e.cornerRadius===5||"topLeftRadius"in e&&"topRightRadius"in e&&"bottomLeftRadius"in e&&"bottomRightRadius"in e&&e.topLeftRadius===5&&e.topRightRadius===5&&e.bottomLeftRadius===5&&e.bottomRightRadius===5,o=e.strokeWeight===1,r=e.strokes,i=r.length>0&&r.some(l=>l.type==="SOLID"&&l.visible!==!1&&l.color?F(l.color.r,l.color.g,l.color.b).toUpperCase()==="#9747FF":!1),a="paddingLeft"in e&&"paddingRight"in e&&"paddingTop"in e&&"paddingBottom"in e&&e.paddingLeft===16&&e.paddingRight===16&&e.paddingTop===16&&e.paddingBottom===16,c=s&&o&&i&&a;return c&&(console.log(`\u{1F3AF} [FILTER] Detected default variant frame styles in ${e.name} - filtering out`),console.log(` Type: ${e.type}, Parent: ${(d=e.parent)==null?void 0:d.type}`),console.log(` Radius: ${String(e.cornerRadius)}, Weight: ${String(e.strokeWeight)}, Color: ${r.length>0&&r[0].type==="SOLID"?F(r[0].color.r,r[0].color.g,r[0].color.b):"none"}`),console.log(` Padding: L=${e.paddingLeft}, R=${e.paddingRight}, T=${e.paddingTop}, B=${e.paddingBottom}`)),c}function we(e){let t=e;for(;t;){if(t.type==="COMPONENT_SET"||t.parent&&t.parent.type==="COMPONENT_SET")return!0;t=t.parent}return!1}async function ue(e){let t=[],n=[],s=[],o=[],r=[],i=new Set,a=new Set,c=new Set,d=new Set,l=new Set;async function p(u){console.log("\u{1F50D} Analyzing node:",u.name,"Type:",u.type);let g=[];if("fillStyleId"in u&&typeof u.fillStyleId=="string"&&g.push(figma.getStyleByIdAsync(u.fillStyleId).then(y=>{if(y!=null&&y.name&&!i.has(y.name)){i.add(y.name);let b=y.name;if("fills"in u&&Array.isArray(u.fills)&&u.fills.length>0){let I=u.fills[0];I.type==="SOLID"&&I.color&&(b=F(I.color.r,I.color.g,I.color.b))}t.push({name:y.name,value:b,type:"fill-style",isToken:!0,isActualToken:!0,source:"figma-style"})}}).catch(console.warn)),"strokeStyleId"in u&&typeof u.strokeStyleId=="string"&&g.push(figma.getStyleByIdAsync(u.strokeStyleId).then(y=>{y!=null&&y.name&&!i.has(y.name)&&(i.add(y.name),t.push({name:y.name,value:y.name,type:"stroke-style",isToken:!0,isActualToken:!0,source:"figma-style"}))}).catch(console.warn)),u.type==="TEXT"&&"textStyleId"in u&&typeof u.textStyleId=="string"&&g.push(figma.getStyleByIdAsync(u.textStyleId).then(y=>{y!=null&&y.name&&!c.has(y.name)&&(c.add(y.name),s.push({name:y.name,value:y.name,type:"text-style",isToken:!0,isActualToken:!0,source:"figma-style"}))}).catch(console.warn)),"effectStyleId"in u&&typeof u.effectStyleId=="string"&&g.push(figma.getStyleByIdAsync(u.effectStyleId).then(y=>{y!=null&&y.name&&!d.has(y.name)&&(d.add(y.name),o.push({name:y.name,value:y.name,type:"effect-style",isToken:!0,isActualToken:!0,source:"figma-style"}))}).catch(console.warn)),await Promise.all(g),"boundVariables"in u&&u.boundVariables){let y=u.boundVariables;console.log(`\u{1F50D} [VARIABLES] Checking bound variables for ${u.name}:`,Object.keys(y));let b=async(x,$,v,T,L)=>{try{let A=Array.isArray(x)?x:[x];for(let ee of A)if(ee!=null&&ee.id&&typeof ee.id=="string"){let Y=await Qe(ee.id);if(console.log(` \u{1F3AF} Found ${$} variable:`,Y),Y&&!v.has(Y)){v.add(Y);let ve=Y;if(L==="color"&&($==="fills"||$==="strokes")){let $e=await tn(ee.id,u);$e&&$e.startsWith("#")&&(ve=$e)}T.push({name:Y,value:ve,type:`${$}-variable`,isToken:!0,isActualToken:!0,source:"figma-variable"}),console.log(` \u2705 Added ${L} token: ${Y} (value: ${ve})`)}}}catch(A){console.warn(`Error processing ${$} variables:`,A)}},I=async(x,$,v,T,L)=>{if(x&&typeof x=="object"&&"id"in x&&typeof x.id=="string"){let A=await Qe(x.id);console.log(` \u{1F3AF} Found ${$} variable:`,A),A&&!v.has(A)&&(v.add(A),T.push({name:A,value:A,type:`${$}-variable`,isToken:!0,isActualToken:!0,source:"figma-variable"}),console.log(` \u2705 Added ${L} token: ${A}`))}},w=[];y.fills&&(console.log(" \u{1F3A8} Processing fills variables..."),w.push(b(y.fills,"fills",i,t,"color"))),y.strokes&&(console.log(" \u{1F58A}\uFE0F Processing strokes variables..."),w.push(b(y.strokes,"strokes",i,t,"color"))),y.effects&&(console.log(" \u2728 Processing effects variables..."),w.push(b(y.effects,"effects",d,o,"effect"))),["strokeWeight","strokeTopWeight","strokeRightWeight","strokeBottomWeight","strokeLeftWeight"].forEach(x=>{y[x]&&(console.log(` \u{1F4CF} Processing ${x} variable...`),w.push(I(y[x],x,l,r,"border")))}),["topLeftRadius","topRightRadius","bottomLeftRadius","bottomRightRadius"].forEach(x=>{y[x]&&(console.log(` \u{1F504} Processing ${x} variable...`),w.push(I(y[x],x,l,r,"border")))}),["paddingTop","paddingRight","paddingBottom","paddingLeft","itemSpacing","counterAxisSpacing"].forEach(x=>{y[x]&&(console.log(` \u{1F4D0} Processing ${x} variable...`),w.push(I(y[x],x,a,n,"spacing")))}),["width","height","minWidth","maxWidth","minHeight","maxHeight"].forEach(x=>{y[x]&&(console.log(` \u{1F4E6} Processing ${x} variable...`),w.push(I(y[x],x,a,n,"size")))}),y.opacity&&(console.log(" \u{1F47B} Processing opacity variable..."),w.push(I(y.opacity,"opacity",d,o,"effect"))),u.type==="TEXT"&&["fontSize","lineHeight","letterSpacing","paragraphSpacing"].forEach($=>{y[$]&&(console.log(` \u{1F4DD} Processing ${$} variable...`),w.push(I(y[$],$,c,s,"typography")))}),await Promise.all(w),console.log(`\u{1F50D} [VARIABLES] Total variables found for ${u.name}: ${Object.keys(y).length}`)}let f="boundVariables"in u&&u.boundVariables&&u.boundVariables.fills,m="fillStyleId"in u&&u.fillStyleId;"fills"in u&&Array.isArray(u.fills)&&!m&&!f?(console.log(`\u{1F50D} [HARD-CODED] Checking fills for ${u.name} (no variables, no style)`),u.fills.forEach(y=>{if(y.type==="SOLID"&&y.visible!==!1&&y.color){let b=F(y.color.r,y.color.g,y.color.b),I=`${b}:${u.id}`;if(!i.has(I)){console.log(` \u26A0\uFE0F Found hard-coded fill: ${b}`),i.add(I);let w=oe(u);t.push({name:`hard-coded-fill-${t.length+1}`,value:b,type:"fill",isToken:!1,source:"hard-coded",context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,path:w.path,description:w.description,property:"fills"}})}}})):f?console.log(`\u{1F50D} [VARIABLES] ${u.name} has fill variables - skipping hard-coded detection`):m&&console.log(`\u{1F50D} [STYLES] ${u.name} has fill style - skipping hard-coded detection`);let h="boundVariables"in u&&u.boundVariables&&u.boundVariables.strokes,C="strokeStyleId"in u&&u.strokeStyleId;if("strokes"in u&&Array.isArray(u.strokes)&&!C&&!h?(console.log(`\u{1F50D} [HARD-CODED] Checking strokes for ${u.name} (no variables, no style)`),de(u)?console.log(" \u{1F6AB} Skipping default variant frame stroke colors"):u.strokes.forEach(y=>{if(y.type==="SOLID"&&y.visible!==!1&&y.color){let b=F(y.color.r,y.color.g,y.color.b),I=`${b}:${u.id}`;if(!i.has(I)){console.log(` \u26A0\uFE0F Found hard-coded stroke: ${b}`),i.add(I);let w=oe(u);t.push({name:`hard-coded-stroke-${t.length+1}`,value:b,type:"stroke",isToken:!1,source:"hard-coded",isDefaultVariantStyle:b.toUpperCase()==="#9747FF"&&we(u),context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,path:w.path,description:w.description,property:"strokes"}})}}})):h?console.log(`\u{1F50D} [VARIABLES] ${u.name} has stroke variables - skipping hard-coded detection`):C&&console.log(`\u{1F50D} [STYLES] ${u.name} has stroke style - skipping hard-coded detection`),"strokeWeight"in u&&typeof u.strokeWeight=="number"){console.log(`\u{1F50D} Node ${u.name} has strokeWeight: ${u.strokeWeight}`);let y="strokes"in u&&Array.isArray(u.strokes)&&u.strokes.length>0,b=y&&u.strokes.some(P=>P.visible!==!1),I="boundVariables"in u&&u.boundVariables&&["strokeWeight","strokeTopWeight","strokeRightWeight","strokeBottomWeight","strokeLeftWeight"].some(P=>u.boundVariables[P]),w="boundVariables"in u&&u.boundVariables?Object.keys(u.boundVariables):[];if(console.log(` Has strokes: ${y}, Has visible strokes: ${b}, Has strokeWeight variable: ${!!I}, boundVariable keys: [${w.join(", ")}]`),I)console.log(` \u{1F517} ${u.name} has strokeWeight bound to variable - skipping hard-coded detection`);else if(u.strokeWeight>0&&b&&!de(u)){let P=`${u.strokeWeight}px`,O,M=u.strokes.find(x=>x.visible!==!1&&x.type==="SOLID");M&&M.type==="SOLID"&&M.color&&(O=F(M.color.r,M.color.g,M.color.b));let z=`${P}:${u.id}`;if(!l.has(z)){console.log(` \u2705 Adding stroke weight: ${P}`),l.add(z);let x=oe(u);r.push({name:`hard-coded-stroke-weight-${u.strokeWeight}`,value:P,type:"stroke-weight",isToken:!1,source:"hard-coded",strokeColor:O,isDefaultVariantStyle:u.strokeWeight===1&&(O==null?void 0:O.toUpperCase())==="#9747FF"&&we(u),context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,hasVisibleStroke:!0,path:x.path,description:x.description,property:"strokeWeight"}})}}else u.strokeWeight>0&&b&&de(u)&&console.log(" \u{1F6AB} Skipping default variant frame stroke weight")}let S="boundVariables"in u&&u.boundVariables&&["topLeftRadius","topRightRadius","bottomLeftRadius","bottomRightRadius","cornerRadius"].some(y=>u.boundVariables[y]);if("cornerRadius"in u&&typeof u.cornerRadius=="number"&&!S)if(console.log(`\u{1F50D} [HARD-CODED] Checking corner radius for ${u.name} (no variables)`),de(u))console.log(" \u{1F6AB} Skipping default variant frame corner radius");else{let y=u.cornerRadius;if(y>0){let b=`${y}px`,I=`${b}:${u.id}`;if(!l.has(I)){console.log(` \u26A0\uFE0F Found hard-coded corner radius: ${b}`),l.add(I);let w=oe(u);r.push({name:`hard-coded-corner-radius-${y}`,value:b,type:"corner-radius",isToken:!1,source:"hard-coded",isDefaultVariantStyle:y===5&&we(u),context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,path:w.path,description:w.description,property:"cornerRadius"}})}}}else S&&console.log(`\u{1F50D} [VARIABLES] ${u.name} has radius variables - skipping hard-coded detection`);!S&&"topLeftRadius"in u&&(console.log(`\u{1F50D} [HARD-CODED] Checking individual corner radius for ${u.name} (no variables)`),de(u)?console.log(" \u{1F6AB} Skipping default variant frame individual corner radii"):[{prop:"topLeftRadius",name:"top-left"},{prop:"topRightRadius",name:"top-right"},{prop:"bottomLeftRadius",name:"bottom-left"},{prop:"bottomRightRadius",name:"bottom-right"}].forEach(({prop:b,name:I})=>{if(b in u&&typeof u[b]=="number"){let w=u[b];if(w>0){let P=`${w}px`,O=`${P}:${u.id}:${b}`;if(!l.has(O)){console.log(` \u26A0\uFE0F Found hard-coded ${I} radius: ${P}`),l.add(O);let M=oe(u);r.push({name:`hard-coded-${I}-radius-${w}`,value:P,type:`${I}-radius`,isToken:!1,source:"hard-coded",isDefaultVariantStyle:w===5&&we(u),context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,path:M.path,description:M.description,property:b}})}}}}));let N="boundVariables"in u&&u.boundVariables&&["paddingTop","paddingRight","paddingBottom","paddingLeft"].some(y=>u.boundVariables[y]);if("paddingLeft"in u&&typeof u.paddingLeft=="number"&&!N){console.log(`\u{1F50D} [HARD-CODED] Checking padding for ${u.name} (no variables)`);let y=u;[{value:y.paddingLeft,name:"left"},{value:y.paddingRight,name:"right"},{value:y.paddingTop,name:"top"},{value:y.paddingBottom,name:"bottom"}].forEach(I=>{let w=`${I.value}:${u.id}:${I.name}`;if(typeof I.value=="number"&&I.value>1&&!a.has(w)){console.log(` \u26A0\uFE0F Found hard-coded padding-${I.name}: ${I.value}px`),a.add(w);let P=oe(u),O=I.value===16&&we(u)&&de(u);n.push({name:`hard-coded-padding-${I.name}-${I.value}`,value:`${I.value}px`,type:"padding",isToken:!1,source:"hard-coded",isDefaultVariantStyle:O,context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,path:P.path,description:P.description,property:`padding${I.name.charAt(0).toUpperCase()+I.name.slice(1)}`}})}})}else N&&console.log(`\u{1F50D} [VARIABLES] ${u.name} has padding variables - skipping hard-coded detection`);if("children"in u)for(let y of u.children)await p(y)}return await p(e),$o({colors:t,spacing:n,typography:s,effects:o,borders:r})}function $o(e){let t=["colors","spacing","typography","effects","borders"],n={totalTokens:0,actualTokens:0,hardCodedValues:0,aiSuggestions:0,byCategory:{}};return t.forEach(s=>{let o=e[s].map(c=>K(R({},c),{isActualToken:c.source==="figma-style"||c.source==="figma-variable",recommendation:Mo(c,s),suggestion:Oo(c,s)})),r=o.filter(c=>!c.isDefaultVariantStyle),i=r.filter(c=>c.isActualToken).length,a=r.filter(c=>c.source==="hard-coded").length;n.byCategory[s]={total:r.length,tokens:i,hardCoded:a,suggestions:0},n.totalTokens+=r.length,n.actualTokens+=i,n.hardCodedValues+=a,e[s]=o}),K(R({},e),{summary:n})}function Mo(e,t){if(e.isToken)return`Using ${e.name} token`;switch(t){case"colors":return`Consider using a color token instead of ${e.value}`;case"spacing":return`Consider using spacing token instead of ${e.value}`;case"typography":return"Consider using typography token";case"effects":return"Consider using effect token";case"borders":return"Consider using border radius token";default:return"Consider using a design token"}}function Oo(e,t){var n,s;switch(t){case"colors":return(n=e.value)!=null&&n.startsWith("#000")?"Use semantic color token (e.g., text.primary)":(s=e.value)!=null&&s.startsWith("#FFF")?"Use semantic color token (e.g., background.primary)":"Create or use existing color token";case"spacing":let o=parseInt(e.value||"0");return o%8===0?"Create or use existing spacing token (follows 8px grid)":o%4===0?"Create or use existing spacing token (follows 4px grid)":"Create or use existing spacing token";case"typography":return"Use semantic typography token (e.g., heading.large, body.regular)";case"effects":return"Use semantic shadow token (e.g., shadow.small, shadow.medium)";case"borders":return"Use appropriate radius token (e.g., radius.small, radius.medium)";default:return"Create or use existing design token"}}function nn(e){return`You are an expert design system architect analyzing a Figma component for comprehensive metadata and design token recommendations. **Component Analysis Context:** - Component Name: ${e.name} @@ -21,7 +21,7 @@ ${e.additionalContext?` ${e.existingDescription?`"${e.existingDescription}" (Build upon this if present, or create a comprehensive new description)`:"None set \u2014 create a comprehensive description from scratch"} -- Nested Component Instances: ${je(e.hierarchy).join(", ")||"None detected"} +- Nested Component Instances: ${et(e.hierarchy).join(", ")||"None detected"} **IMPORTANT: Focus on what makes this component ready for CODE GENERATION via MCP.** Evaluate based on these criteria that actually matter for development: @@ -197,27 +197,27 @@ For the "recommendedProperties" field, compare the component's EXISTING properti - Effects: \`[effect]-[intensity]-[purpose]\` (e.g., "shadow-md-default", "blur-backdrop-light") - Borders: \`radius-[size]-[value]\` (e.g., "radius-md-8px", "radius-full-999px") -Focus on creating a comprehensive DESIGN analysis that helps designers build scalable, consistent, and well-structured Figma components.`}function le(e){try{console.log("\u{1F50D} Starting JSON extraction from LLM response..."),console.log("\u{1F4DD} Response length:",e.length),console.log("\u{1F4DD} Response preview (first 200 chars):",e.substring(0,200));try{let n=JSON.parse(e.trim());return console.log("\u2705 Successfully parsed entire response as JSON"),n}catch(n){console.log("\u26A0\uFE0F Full response is not valid JSON, trying to extract JSON block...")}let t=[()=>jt(e),()=>Zs(e),()=>eo(e),()=>to(e)];for(let n=0;n<t.length;n++)try{console.log(`\u{1F50D} Trying extraction strategy ${n+1}...`);let s=t[n]();if(s)return console.log("\u2705 Successfully extracted JSON with strategy",n+1),s}catch(s){let o=s instanceof Error?s.message:"Unknown error";console.log(`\u26A0\uFE0F Strategy ${n+1} failed:`,o);continue}throw new Error("No valid JSON found in response after trying all strategies")}catch(t){throw console.error("\u274C Failed to parse JSON from LLM response:",t),console.log("\u{1F4DD} Full response for debugging:",e),new Error("Invalid JSON response from LLM API")}}function jt(e){let t=e.indexOf("{");if(t===-1)return null;let n=0,s=!1,o=!1;for(let r=t;r<e.length;r++){let i=e[r];if(o){o=!1;continue}if(i==="\\"){o=!0;continue}if(i==='"'){s=!s;continue}if(!s){if(i==="{")n++;else if(i==="}"&&(n--,n===0)){let a=e.substring(t,r+1);try{return JSON.parse(a)}catch(c){return console.log("\u26A0\uFE0F Balanced JSON extraction found malformed JSON:",c instanceof Error?c.message:"Parse error"),null}}}}return console.log("\u26A0\uFE0F JSON appears to be truncated, attempting reconstruction..."),Ys(e,t)}function Ys(e,t){try{let s=e.substring(t).split(` -`),o="",r=0,i=!1,a=!1;for(let l=0;l<s.length;l++){let d=s[l];for(let p=0;p<d.length;p++){let u=d[p];if(a){a=!1;continue}if(u==="\\"){a=!0;continue}if(u==='"'){i=!i;continue}i||(u==="{"?r++:u==="}"&&r--)}if(i||d.trim().endsWith(",")===!1&&l<s.length-1)break;o+=d+` +Focus on creating a comprehensive DESIGN analysis that helps designers build scalable, consistent, and well-structured Figma components.`}function pe(e){try{console.log("\u{1F50D} Starting JSON extraction from LLM response..."),console.log("\u{1F4DD} Response length:",e.length),console.log("\u{1F4DD} Response preview (first 200 chars):",e.substring(0,200));try{let n=JSON.parse(e.trim());return console.log("\u2705 Successfully parsed entire response as JSON"),n}catch(n){console.log("\u26A0\uFE0F Full response is not valid JSON, trying to extract JSON block...")}let t=[()=>sn(e),()=>Vo(e),()=>_o(e),()=>Bo(e)];for(let n=0;n<t.length;n++)try{console.log(`\u{1F50D} Trying extraction strategy ${n+1}...`);let s=t[n]();if(s)return console.log("\u2705 Successfully extracted JSON with strategy",n+1),s}catch(s){let o=s instanceof Error?s.message:"Unknown error";console.log(`\u26A0\uFE0F Strategy ${n+1} failed:`,o);continue}throw new Error("No valid JSON found in response after trying all strategies")}catch(t){throw console.error("\u274C Failed to parse JSON from LLM response:",t),console.log("\u{1F4DD} Full response for debugging:",e),new Error("Invalid JSON response from LLM API")}}function sn(e){let t=e.indexOf("{");if(t===-1)return null;let n=0,s=!1,o=!1;for(let r=t;r<e.length;r++){let i=e[r];if(o){o=!1;continue}if(i==="\\"){o=!0;continue}if(i==='"'){s=!s;continue}if(!s){if(i==="{")n++;else if(i==="}"&&(n--,n===0)){let a=e.substring(t,r+1);try{return JSON.parse(a)}catch(c){return console.log("\u26A0\uFE0F Balanced JSON extraction found malformed JSON:",c instanceof Error?c.message:"Parse error"),null}}}}return console.log("\u26A0\uFE0F JSON appears to be truncated, attempting reconstruction..."),Fo(e,t)}function Fo(e,t){try{let s=e.substring(t).split(` +`),o="",r=0,i=!1,a=!1;for(let d=0;d<s.length;d++){let l=s[d];for(let p=0;p<l.length;p++){let u=l[p];if(a){a=!1;continue}if(u==="\\"){a=!0;continue}if(u==='"'){i=!i;continue}i||(u==="{"?r++:u==="}"&&r--)}if(i||l.trim().endsWith(",")===!1&&d<s.length-1)break;o+=l+` `}for(;r>0;)o+=`} -`,r--;let c=JSON.parse(o.trim());return console.log("\u2705 Successfully reconstructed truncated JSON"),c}catch(n){return console.log("\u26A0\uFE0F Failed to reconstruct truncated JSON:",n instanceof Error?n.message:"Unknown error"),Qs(e)}}function Qs(e){try{console.log("\u{1F504} Attempting to extract basic component info as fallback...");let t=e.match(/"component":\s*"([^"]+)"/),n=e.match(/"description":\s*"([^"]+)"/);if(t&&n){let s={component:t[1],description:n[1],props:[],states:["default"],variants:{},tokens:{colors:[],spacing:[],typography:[]},audit:{tokenOpportunities:["Review and simplify component analysis"]},mcpReadiness:{score:60,strengths:["Component has basic structure"],gaps:["Analysis was incomplete due to response size"],recommendations:["Simplify component structure","Use MCP-enhanced analysis for better results"]},propertyCheatSheet:[]};return console.log("\u2705 Extracted basic component info as fallback"),s}return null}catch(t){return console.log("\u26A0\uFE0F Failed to extract basic component info:",t instanceof Error?t.message:"Unknown error"),null}}function Zs(e){let t=[["```json","```"],["```","```"],["JSON:",` +`,r--;let c=JSON.parse(o.trim());return console.log("\u2705 Successfully reconstructed truncated JSON"),c}catch(n){return console.log("\u26A0\uFE0F Failed to reconstruct truncated JSON:",n instanceof Error?n.message:"Unknown error"),Do(e)}}function Do(e){try{console.log("\u{1F504} Attempting to extract basic component info as fallback...");let t=e.match(/"component":\s*"([^"]+)"/),n=e.match(/"description":\s*"([^"]+)"/);if(t&&n){let s={component:t[1],description:n[1],props:[],states:["default"],variants:{},tokens:{colors:[],spacing:[],typography:[]},audit:{tokenOpportunities:["Review and simplify component analysis"]},mcpReadiness:{score:60,strengths:["Component has basic structure"],gaps:["Analysis was incomplete due to response size"],recommendations:["Simplify component structure","Use MCP-enhanced analysis for better results"]},propertyCheatSheet:[]};return console.log("\u2705 Extracted basic component info as fallback"),s}return null}catch(t){return console.log("\u26A0\uFE0F Failed to extract basic component info:",t instanceof Error?t.message:"Unknown error"),null}}function Vo(e){let t=[["```json","```"],["```","```"],["JSON:",` `],["Response:",` `],["{",`} `]];for(let[n,s]of t){let o=e.indexOf(n);if(o===-1)continue;let r=o+n.length,i=e.indexOf(s,r);if(i===-1&&s===` -`&&(i=e.length),i===-1)continue;let a=e.substring(r,i).trim();try{return JSON.parse(a)}catch(c){if(a.startsWith("{"))try{return jt(a)}catch(l){continue}}}return null}function eo(e){let t=/```(?:json)?\s*(\{[\s\S]*?\})\s*```/gi,n;for(;(n=t.exec(e))!==null;)try{return JSON.parse(n[1])}catch(s){continue}return null}function to(e){let t=e.match(/\{[\s\S]*\}/);return t?JSON.parse(t[0]):null}function Pe(e){if(!e||typeof e!="object")return e;let t=["aria","accessibility api","semantic html","keyboard navigation","event handler","interactive behavior","onclick","onchange","state management","controlled component","uncontrolled component","props","responsive breakpoint","css implementation","@media","animation token","transition timing","programmatic animation","keyframe","api integration","data binding","dynamic content","fetch","axios","implement","add handler","bind event","attach listener","programming pattern","functional pattern","react hook","usestate","useeffect"],n=r=>{let i=r.toLowerCase();return t.some(a=>i.includes(a))},s=r=>Array.isArray(r)?r.filter(i=>{if(typeof i=="string"){let a=!n(i);return a||console.log("\u{1F6AB} [FILTER] Removed development-focused recommendation:",i),a}return!0}):r,o=JSON.parse(JSON.stringify(e));return o.mcpReadiness&&(o.mcpReadiness.recommendations&&(o.mcpReadiness.recommendations=s(o.mcpReadiness.recommendations)),o.mcpReadiness.gaps&&(o.mcpReadiness.gaps=s(o.mcpReadiness.gaps))),o.audit&&(o.audit.tokenOpportunities&&(o.audit.tokenOpportunities=s(o.audit.tokenOpportunities)),o.audit.structureIssues&&(o.audit.structureIssues=s(o.audit.structureIssues))),o.accessibility&&(o.accessibility.designConsiderations&&(o.accessibility.designConsiderations=s(o.accessibility.designConsiderations)),o.accessibility.visualIndicators&&(o.accessibility.visualIndicators=s(o.accessibility.visualIndicators))),o}var E=class extends Error{constructor(n,s,o,r){super(n);this.code=s;this.statusCode=o;this.retryAfter=r;this.name="LLMError"}};var qe={anthropic:"claude-sonnet-4-5-20250929",openai:"gpt-5.2",google:"gemini-2.5-pro"};var no=[{id:"claude-opus-4-5-20251218",name:"Claude Opus 4.5",description:"Flagship model - Most capable, best for complex analysis and reasoning",contextWindow:2e5,isDefault:!1},{id:"claude-sonnet-4-5-20250929",name:"Claude Sonnet 4.5",description:"Standard model - Balanced performance and cost, recommended for most tasks",contextWindow:2e5,isDefault:!0},{id:"claude-haiku-4-5-20251001",name:"Claude Haiku 4.5",description:"Economy model - Fastest responses, ideal for quick analysis",contextWindow:2e5,isDefault:!1}],Je=class{constructor(){this.name="Anthropic";this.id="anthropic";this.endpoint="https://api.anthropic.com/v1/messages";this.keyPrefix="sk-ant-";this.keyPlaceholder="sk-ant-...";this.models=no}formatRequest(t){let n={model:t.model,messages:[{role:"user",content:t.prompt.trim()}],max_tokens:t.maxTokens};return t.temperature!==void 0&&(n.temperature=t.temperature),t.additionalParams&&Object.assign(n,t.additionalParams),n}parseResponse(t){let n=t;if(!n.content||!Array.isArray(n.content))throw new E("Invalid response format from Anthropic API: missing content array","INVALID_REQUEST");let s=n.content.filter(o=>o.type==="text").map(o=>o.text).join(` -`);if(!s)throw new E("Invalid response format from Anthropic API: no text content found","INVALID_REQUEST");return{content:s.trim(),model:n.model,usage:n.usage?{promptTokens:n.usage.input_tokens,completionTokens:n.usage.output_tokens,totalTokens:n.usage.input_tokens+n.usage.output_tokens}:void 0,metadata:{id:n.id,stopReason:n.stop_reason}}}validateApiKey(t){if(!t||typeof t!="string")return{isValid:!1,error:"API Key Required: Please provide a valid Claude API key."};let n=t.trim();return n.length===0?{isValid:!1,error:"API Key Required: The Claude API key cannot be empty."}:n.startsWith(this.keyPrefix)?n.length<40?{isValid:!1,error:"Invalid API Key Format: The API key appears to be too short. Please verify you copied the complete key."}:{isValid:!0}:{isValid:!1,error:`Invalid API Key Format: Claude API keys should start with "${this.keyPrefix}". Please check your API key.`}}getHeaders(t){return{"content-type":"application/json","x-api-key":t.trim(),"anthropic-version":"2023-06-01","anthropic-dangerous-direct-browser-access":"true"}}getDefaultModel(){return this.models.find(n=>n.isDefault)||this.models[1]}handleError(t,n){var r;let s=n,o=((r=s==null?void 0:s.error)==null?void 0:r.message)||(typeof n=="string"?n:"Unknown error");switch(t){case 400:return new E(`Claude API Error (400): ${o}. Please check your request format.`,"INVALID_REQUEST",400);case 401:return new E("Claude API Error (401): Invalid API key. Please check your Claude API key in settings.","INVALID_API_KEY",401);case 403:return new E("Claude API Error (403): Access forbidden. Please check your API key permissions.","INVALID_API_KEY",403);case 404:return new E(`Claude API Error (404): ${o}. The requested model may not be available.`,"MODEL_NOT_FOUND",404);case 429:return new E("Claude API Error (429): Rate limit exceeded. Please try again later.","RATE_LIMIT_EXCEEDED",429);case 500:return new E("Claude API Error (500): Server error. The Claude API is experiencing issues. Please try again later.","SERVER_ERROR",500);case 503:return new E("Claude API Error (503): Service unavailable. The Claude API is temporarily down. Please try again later.","SERVICE_UNAVAILABLE",503);default:return new E(`Claude API Error (${t}): ${o}`,"UNKNOWN_ERROR",t)}}},Ye=new Je;var so=[{id:"gpt-5.2",name:"GPT-5.2",description:"Flagship model with advanced reasoning capabilities",contextWindow:128e3,isDefault:!0},{id:"gpt-5.2-pro",name:"GPT-5.2 Pro",description:"Premium model with extended reasoning for complex tasks",contextWindow:128e3,isDefault:!1},{id:"gpt-5-mini",name:"GPT-5 Mini",description:"Economy model - fast and cost-effective",contextWindow:128e3,isDefault:!1}],Qe=class{constructor(){this.name="OpenAI";this.id="openai";this.endpoint="https://api.openai.com/v1/chat/completions";this.keyPrefix="sk-";this.keyPlaceholder="sk-...";this.models=so}formatRequest(t){let n={model:t.model,messages:[{role:"user",content:t.prompt}],max_completion_tokens:t.maxTokens,temperature:t.temperature};return t.additionalParams&&Object.assign(n,t.additionalParams),n}parseResponse(t){let n=t;if(!n.choices||n.choices.length===0)throw new E("Invalid response format: no choices returned","INVALID_REQUEST");let s=n.choices[0];if(!s.message||typeof s.message.content!="string")throw new E("Invalid response format: missing message content","INVALID_REQUEST");let o={content:s.message.content.trim(),model:n.model};return n.usage&&(o.usage={promptTokens:n.usage.prompt_tokens,completionTokens:n.usage.completion_tokens,totalTokens:n.usage.total_tokens}),o.metadata={id:n.id,finishReason:s.finish_reason,created:n.created},o}validateApiKey(t){if(!t||typeof t!="string")return{isValid:!1,error:"API Key Required: Please provide a valid OpenAI API key."};let n=t.trim();return n.length===0?{isValid:!1,error:"API Key Required: The OpenAI API key cannot be empty."}:n.startsWith(this.keyPrefix)?n.length<20?{isValid:!1,error:"Invalid API Key Format: The API key appears to be too short. Please verify you copied the complete key."}:{isValid:!0}:{isValid:!1,error:`Invalid API Key Format: OpenAI API keys should start with "${this.keyPrefix}". Please check your API key.`}}getHeaders(t){return{"Content-Type":"application/json",Authorization:`Bearer ${t.trim()}`}}getDefaultModel(){return this.models.find(n=>n.isDefault)||this.models[0]}handleError(t,n){var r;let s=n,o=((r=s==null?void 0:s.error)==null?void 0:r.message)||(s==null?void 0:s.message)||"Unknown error occurred";switch(t){case 400:return o.toLowerCase().includes("context_length_exceeded")||o.toLowerCase().includes("maximum context length")?new E(`OpenAI API Error (400): Context length exceeded. ${o}`,"CONTEXT_LENGTH_EXCEEDED",t):new E(`OpenAI API Error (400): ${o}. Please check your request format.`,"INVALID_REQUEST",t);case 401:return new E("OpenAI API Error (401): Invalid API key. Please check your OpenAI API key in settings.","INVALID_API_KEY",t);case 403:return new E("OpenAI API Error (403): Access forbidden. Please check your API key permissions or account status.","INVALID_API_KEY",t);case 404:return new E(`OpenAI API Error (404): Model not found. ${o}`,"MODEL_NOT_FOUND",t);case 429:let i=o.match(/try again in (\d+)/i),a=i?parseInt(i[1],10):void 0;return new E(`OpenAI API Error (429): Rate limit exceeded. ${a?`Please try again in ${a} seconds.`:"Please try again later."}`,"RATE_LIMIT_EXCEEDED",t,a);case 500:return new E("OpenAI API Error (500): Server error. The OpenAI API is experiencing issues. Please try again later.","SERVER_ERROR",t);case 502:return new E("OpenAI API Error (502): Bad gateway. The OpenAI API is temporarily unavailable. Please try again later.","SERVICE_UNAVAILABLE",t);case 503:return new E("OpenAI API Error (503): Service unavailable. The OpenAI API is temporarily down. Please try again later.","SERVICE_UNAVAILABLE",t);case 504:return new E("OpenAI API Error (504): Gateway timeout. The request took too long. Please try again.","SERVICE_UNAVAILABLE",t);default:return new E(`OpenAI API Error (${t}): ${o}`,"UNKNOWN_ERROR",t)}}},Ze=new Qe;var oo=[{id:"gemini-3-pro-preview",name:"Gemini 3 Pro",description:"Flagship model with advanced reasoning and multimodal capabilities",contextWindow:1e6,isDefault:!0},{id:"gemini-2.5-pro",name:"Gemini 2.5 Pro",description:"Standard reasoning model with excellent performance",contextWindow:1e6,isDefault:!1},{id:"gemini-2.5-flash",name:"Gemini 2.5 Flash",description:"Economy model optimized for speed and efficiency",contextWindow:1e6,isDefault:!1}],et=class{constructor(){this.name="Google";this.id="google";this.endpoint="https://generativelanguage.googleapis.com/v1beta/models";this.keyPrefix="AIza";this.keyPlaceholder="AIza...";this.models=oo}formatRequest(t){let n={contents:[{parts:[{text:t.prompt}]}],generationConfig:{maxOutputTokens:t.maxTokens,temperature:t.temperature}};if(t.additionalParams){let{topP:s,topK:o,stopSequences:r}=t.additionalParams;s!==void 0&&(n.generationConfig.topP=s),o!==void 0&&(n.generationConfig.topK=o),r!==void 0&&(n.generationConfig.stopSequences=r)}return n}parseResponse(t){var c;let n=t;if(n.error)throw new E(n.error.message||"Unknown Gemini API error",this.mapErrorCodeToLLMErrorCode(n.error.code,n.error.status),n.error.code);if(!n.candidates||n.candidates.length===0){let l=Object.keys(n);throw new E(`No candidates in Gemini response. Response keys: [${l.join(", ")}]${n.error?`. Error: ${n.error.message}`:""}`,"INVALID_REQUEST")}let s=n.candidates[0];if(s.finishReason==="SAFETY")throw new E("Gemini response blocked by safety filters. Try rephrasing the prompt.","INVALID_REQUEST");let o=(c=s.content)==null?void 0:c.parts;if(!o||o.length===0)throw new E(`No content parts in Gemini response. Finish reason: ${s.finishReason||"unknown"}. Has content: ${!!s.content}`,"INVALID_REQUEST");let r=o.find(l=>typeof l.text=="string");if(!r||!r.text){let l=o.map(d=>Object.keys(d).join(",")).join("; ");throw new E(`No text content in Gemini response parts. Part types: [${l}]. Finish reason: ${s.finishReason||"unknown"}`,"INVALID_REQUEST")}let a={content:r.text,model:"gemini"};return n.usageMetadata&&(a.usage={promptTokens:n.usageMetadata.promptTokenCount||0,completionTokens:n.usageMetadata.candidatesTokenCount||0,totalTokens:n.usageMetadata.totalTokenCount||0}),a}validateApiKey(t){if(!t||typeof t!="string")return{isValid:!1,error:"API key is required"};let n=t.trim();return n.length===0?{isValid:!1,error:"API key cannot be empty"}:n.startsWith(this.keyPrefix)?n.length<30||n.length>50?{isValid:!1,error:"API key appears to have an invalid length. Please verify you copied the complete key."}:/^[A-Za-z0-9_-]+$/.test(n)?{isValid:!0}:{isValid:!1,error:"API key contains invalid characters"}:{isValid:!1,error:`Google API keys should start with "${this.keyPrefix}". Please check your API key.`}}getHeaders(t){return{"Content-Type":"application/json"}}getEndpoint(t,n){let s=n.trim();return`${this.endpoint}/${t}:generateContent?key=${s}`}getDefaultModel(){return this.models.find(n=>n.isDefault)||this.models[0]}handleError(t,n){var p,u;let s=n,o=s==null?void 0:s.error,r=(o==null?void 0:o.message)||"Unknown Google API error",i=o==null?void 0:o.status,a=(o==null?void 0:o.code)||t,c=this.mapErrorCodeToLLMErrorCode(a,i),l;if(t===429){l=6e4;let f=(p=o==null?void 0:o.details)==null?void 0:p.find(y=>{var m;return(m=y["@type"])==null?void 0:m.includes("RetryInfo")});if((u=f==null?void 0:f.metadata)!=null&&u.retryDelay){let y=f.metadata.retryDelay.match(/(\d+)s/);y&&(l=parseInt(y[1],10)*1e3)}}let d=r;switch(c){case"INVALID_API_KEY":d="Google API Error: Invalid API key. Please check your API key in settings.";break;case"RATE_LIMIT_EXCEEDED":d=`Google API Error: Rate limit exceeded. ${l?`Please try again in ${Math.ceil(l/1e3)} seconds.`:"Please try again later."}`;break;case"MODEL_NOT_FOUND":d="Google API Error: Model not found. Please select a valid model.";break;case"CONTEXT_LENGTH_EXCEEDED":d="Google API Error: Input too long. Please reduce the size of your request.";break;case"SERVER_ERROR":d="Google API Error: Server error. Please try again later.";break;case"SERVICE_UNAVAILABLE":d="Google API Error: Service temporarily unavailable. Please try again later.";break}return new E(d,c,t,l)}mapErrorCodeToLLMErrorCode(t,n){if(n){let s=n.toUpperCase();if(s==="INVALID_ARGUMENT")return"INVALID_REQUEST";if(s==="PERMISSION_DENIED"||s==="UNAUTHENTICATED")return"INVALID_API_KEY";if(s==="NOT_FOUND")return"MODEL_NOT_FOUND";if(s==="RESOURCE_EXHAUSTED")return"RATE_LIMIT_EXCEEDED";if(s==="UNAVAILABLE")return"SERVICE_UNAVAILABLE"}switch(t){case 400:return"INVALID_REQUEST";case 401:case 403:return"INVALID_API_KEY";case 404:return"MODEL_NOT_FOUND";case 429:return"RATE_LIMIT_EXCEEDED";case 500:return"SERVER_ERROR";case 503:return"SERVICE_UNAVAILABLE";default:return"UNKNOWN_ERROR"}}},tt=new et;var ro={anthropic:Ye,openai:Ze,google:tt};function re(e){let t=ro[e];if(!t)throw new E(`Unknown provider: ${e}`,"INVALID_REQUEST",400);return t}async function ie(e,t,n){var c,l;let s=re(e),o=s.validateApiKey(t);if(!o.isValid)throw new E(o.error||"Invalid API key format","INVALID_API_KEY",401);let r=s.formatRequest(n),i=s.getHeaders(t),a=s.endpoint;e==="google"&&(a=`${s.endpoint}/${n.model}:generateContent?key=${t.trim()}`);try{console.log(`Making ${s.name} API call to ${a}...`);let d=await fetch(a,{method:"POST",headers:i,body:JSON.stringify(r)});if(!d.ok){let u;try{u=await d.json()}catch(f){u=await d.text()}throw s.handleError(d.status,u)}let p=await d.json();return console.log(`${s.name} API response status: ${d.status}`),console.log(`${s.name} API response keys:`,Object.keys(p)),e==="google"&&(console.log("Gemini response candidates:",p.candidates?p.candidates.length:"none"),(c=p.candidates)!=null&&c[0]&&(console.log("Gemini candidate[0] keys:",Object.keys(p.candidates[0])),p.candidates[0].content&&console.log("Gemini content parts:",((l=p.candidates[0].content.parts)==null?void 0:l.length)||"none")),p.error&&console.log("Gemini error:",JSON.stringify(p.error))),s.parseResponse(p)}catch(d){throw d instanceof E?d:d instanceof Error&&(d.message.includes("Failed to fetch")||d.message.includes("NetworkError"))?new E(`Network error connecting to ${s.name}. Please check your internet connection.`,"NETWORK_ERROR"):new E(`Unexpected error calling ${s.name}: ${d instanceof Error?d.message:"Unknown error"}`,"UNKNOWN_ERROR")}}var U={SELECTED_PROVIDER:"selected-provider",SELECTED_MODEL:"selected-model",apiKey:e=>`${e}-api-key`,LEGACY_CLAUDE_KEY:"claude-api-key",LEGACY_CLAUDE_MODEL:"claude-model"},io={provider:"anthropic",model:qe.anthropic};async function ao(){try{let e=await figma.clientStorage.getAsync(U.LEGACY_CLAUDE_KEY),t=await figma.clientStorage.getAsync(U.LEGACY_CLAUDE_MODEL);return e?{needsMigration:!0,legacyKey:e,legacyModel:t}:{needsMigration:!1}}catch(e){return{needsMigration:!1}}}async function nt(){let e=await ao();e.needsMigration&&(console.log("Migrating legacy Claude storage to multi-provider format..."),e.legacyKey&&await figma.clientStorage.setAsync(U.apiKey("anthropic"),e.legacyKey),await figma.clientStorage.setAsync(U.SELECTED_PROVIDER,"anthropic"),e.legacyModel&&await figma.clientStorage.setAsync(U.SELECTED_MODEL,e.legacyModel),await figma.clientStorage.deleteAsync(U.LEGACY_CLAUDE_KEY),await figma.clientStorage.deleteAsync(U.LEGACY_CLAUDE_MODEL),console.log("Migration complete"))}async function st(){await nt();let e=await figma.clientStorage.getAsync(U.SELECTED_PROVIDER)||io.provider,t=await figma.clientStorage.getAsync(U.SELECTED_MODEL)||qe[e],n=await figma.clientStorage.getAsync(U.apiKey(e));return{providerId:e,modelId:t,apiKey:n}}async function ot(e,t,n){await figma.clientStorage.setAsync(U.SELECTED_PROVIDER,e),await figma.clientStorage.setAsync(U.SELECTED_MODEL,t),n!==void 0&&await figma.clientStorage.setAsync(U.apiKey(e),n)}async function qt(e){await figma.clientStorage.deleteAsync(U.apiKey(e))}var Jt=/^(Frame|Rectangle|Ellipse|Group|Vector|Line|Polygon|Star|Text|Component|Instance|Slice|Boolean|Union|Subtract|Intersect|Exclude)\s*\d*$/i,Xt=/\s+\d+$/,Yt={button:"btn",icon:"ico",input:"input",text:"txt",image:"img",container:"container",card:"card",list:"list","list-item":"list-item",nav:"nav",header:"header",footer:"footer",modal:"modal",dropdown:"dropdown",checkbox:"checkbox",radio:"radio",toggle:"toggle",avatar:"avatar",badge:"badge",divider:"divider",spacer:"spacer",link:"link",tab:"tab",tooltip:"tooltip",alert:"alert",progress:"progress",skeleton:"skeleton",unknown:"layer"},de=[["btn","button"],["button","button"],["cta","button"],["submit","button"],["icon","icon"],["ico","icon"],["glyph","icon"],["symbol","icon"],["arrow","icon"],["chevron","icon"],["close","icon"],["plus","icon"],["minus","icon"],["txt","text"],["label","text"],["title","text"],["heading","text"],["paragraph","text"],["description","text"],["caption","text"],["subtitle","text"],["input","input"],["field","input"],["textfield","input"],["textarea","input"],["searchfield","input"],["searchbox","input"],["image","image"],["img","image"],["photo","image"],["picture","image"],["thumbnail","image"],["cover","image"],["container","container"],["wrapper","container"],["content","container"],["section","container"],["block","container"],["box","container"],["card","card"],["tile","card"],["panel","card"],["list","list"],["items","list"],["item","list-item"],["row","list-item"],["listitem","list-item"],["nav","nav"],["navbar","nav"],["navigation","nav"],["sidebar","nav"],["breadcrumb","nav"],["menu","nav"],["header","header"],["topbar","header"],["footer","footer"],["bottombar","footer"],["modal","modal"],["dialog","modal"],["popup","modal"],["overlay","modal"],["dropdown","dropdown"],["select","dropdown"],["picker","dropdown"],["combobox","dropdown"],["checkbox","checkbox"],["checkmark","checkbox"],["radio","radio"],["toggle","toggle"],["switch","toggle"],["avatar","avatar"],["profile","avatar"],["userpic","avatar"],["badge","badge"],["tag","badge"],["chip","badge"],["pill","badge"],["status","badge"],["divider","divider"],["separator","divider"],["hr","divider"],["spacer","spacer"],["gap","spacer"],["link","link"],["anchor","link"],["href","link"],["tab","tab"],["tabs","tab"],["tabbar","tab"],["tooltip","tooltip"],["hint","tooltip"],["popover","tooltip"],["alert","alert"],["notification","alert"],["toast","alert"],["message","alert"],["snackbar","alert"],["banner","alert"],["progress","progress"],["loader","progress"],["loading","progress"],["spinner","progress"],["progressbar","progress"],["skeleton","skeleton"],["placeholder","skeleton"],["shimmer","skeleton"]];function Qt(e){if(!e||typeof e!="string")return!0;let t=e.trim();return!!(Jt.test(t)||t.length===1||/^\d+$/.test(t))}function co(e){return Xt.test(e.trim())}function Le(e){let t=e.name.toLowerCase();for(let n=0;n<de.length;n++){let s=de[n];if(t.indexOf(s[0])!==-1)return s[1]}switch(e.type){case"TEXT":return"text";case"VECTOR":case"STAR":case"POLYGON":case"BOOLEAN_OPERATION":return"icon";case"RECTANGLE":case"ELLIPSE":case"LINE":if("fills"in e&&Array.isArray(e.fills)){let n=e.fills,s=!1;for(let o=0;o<n.length;o++){let r=n[o];if(r.type==="IMAGE"&&r.visible!==!1){s=!0;break}}if(s)return"image"}if("width"in e&&"height"in e){let n=e.width,s=e.height,o=n/s;if(s<=2&&n>20||n<=2&&s>20)return"divider";if(n<=32&&s<=32&&o>.5&&o<2)return"spacer"}return"unknown";case"FRAME":case"GROUP":return Zt(e);case"COMPONENT":case"INSTANCE":return en(e);case"COMPONENT_SET":return lo(e);default:return"unknown"}}function Zt(e){if(!("children"in e)||e.children.length===0)return"container";let t=e.children,n=[],s=[];for(let c=0;c<t.length;c++)n.push(t[c].type),s.push(t[c].name.toLowerCase());let o=!1,r=!1;for(let c=0;c<n.length;c++)n[c]==="TEXT"&&(o=!0),(n[c]==="VECTOR"||s[c].indexOf("icon")!==-1)&&(r=!0);let i="width"in e&&"height"in e&&e.width<300&&e.height<100;if(o&&i&&(r||t.length<=3)&&"layoutMode"in e&&e.layoutMode!=="NONE")return"button";let a=!1;for(let c=0;c<n.length;c++)if(n[c]==="RECTANGLE"||s[c].indexOf("image")!==-1){a=!0;break}if(o&&a&&t.length>=2)return"card";if(t.length>=3){let c=t[0].type,l=!0;for(let d=1;d<t.length;d++)if(t[d].type!==c){l=!1;break}if(l&&(c==="FRAME"||c==="INSTANCE"))return"list"}if("cornerRadius"in e&&e.cornerRadius&&t.length<=2&&o&&i)return"input";if("layoutMode"in e&&e.layoutMode==="HORIZONTAL"){let c=0;for(let l=0;l<t.length;l++){let d=t[l].type;(d==="FRAME"||d==="INSTANCE"||d==="TEXT")&&c++}if(c>=3&&i)return"nav"}return"container"}function en(e){let t=e.name.toLowerCase();for(let n=0;n<de.length;n++){let s=de[n];if(t.indexOf(s[0])!==-1)return s[1]}return"children"in e?Zt(e):"unknown"}function lo(e){let t=e.name.toLowerCase();for(let n=0;n<de.length;n++){let s=de[n];if(t.indexOf(s[0])!==-1)return s[1]}return"children"in e&&e.children.length>0?en(e.children[0]):"unknown"}function tn(e,t=10){let n=[];function s(o,r,i){if(r>t)return;let a=i?`${i} > ${o.name}`:o.name,c=Le(o);if(Qt(o.name)){let l=ue(o);n.push({nodeId:o.id,nodeName:o.name,currentName:o.name,suggestedName:l,severity:"error",reason:"Generic layer name detected",layerType:c,depth:r,path:a})}else if(co(o.name)){let l=o.name.replace(Xt,"").trim(),d=ue(o);n.push({nodeId:o.id,nodeName:o.name,currentName:o.name,suggestedName:d!==o.name?d:l,severity:"warning",reason:"Layer name has numbered suffix (possible duplicate)",layerType:c,depth:r,path:a})}if("children"in o)for(let l=0;l<o.children.length;l++)s(o.children[l],r+1,a)}return s(e,0,""),n}function ue(e){let t=Le(e);return e.type==="TEXT"?po(e):e.type==="VECTOR"||e.type==="STAR"||e.type==="POLYGON"||e.type==="BOOLEAN_OPERATION"?uo(e):"children"in e&&e.children.length>0?mo(e):Yt[t]||"layer"}function uo(e){let n=e.name.toLowerCase().replace(Jt,"").replace(/[_\-\s]+/g,"-").replace(/^-|-$/g,"").trim();if(n&&n.length>1)return`icon-${j(n)}`;if("children"in e&&e.children.length>0){let s=[];for(let o=0;o<e.children.length;o++)s.push(e.children[o].type);for(let o=0;o<s.length;o++){if(s[o]==="ELLIPSE")return"icon-circle";if(s[o]==="STAR")return"icon-star";if(s[o]==="POLYGON")return"icon-shape"}}if("width"in e&&"height"in e){let s=e.width/e.height;if(s>1.5||s<.67)return"icon-arrow"}return"icon"}function po(e){let n=(e.characters||"").trim();if(!n)return"text-empty";let s=n.split(/\s+/);if(s.length<=2&&n.length<=30){let f=j(n);return f?`text-${f}`:"text-content"}let o=s[0].toLowerCase(),r=["welcome","about","contact","services","features","pricing"],i=["name","email","password","username","address","phone"],a=["submit","cancel","save","delete","edit","add","remove","ok","yes","no"],c=["learn","read","view","see","click","here","more"],l=["error","invalid","required","failed","wrong"],d=["success","done","complete","saved","updated"],p=n.toLowerCase();for(let f=0;f<r.length;f++)if(o.indexOf(r[f])!==-1||p.indexOf(r[f])!==-1)return`text-heading-${j(s.slice(0,2).join(" "))}`;for(let f=0;f<i.length;f++)if(o.indexOf(i[f])!==-1||p.indexOf(i[f])!==-1)return`text-label-${j(s.slice(0,2).join(" "))}`;for(let f=0;f<a.length;f++)if(o.indexOf(a[f])!==-1||p.indexOf(a[f])!==-1)return`text-button-${j(s.slice(0,2).join(" "))}`;for(let f=0;f<c.length;f++)if(o.indexOf(c[f])!==-1||p.indexOf(c[f])!==-1)return`text-link-${j(s.slice(0,2).join(" "))}`;for(let f=0;f<l.length;f++)if(o.indexOf(l[f])!==-1||p.indexOf(l[f])!==-1)return`text-error-${j(s.slice(0,2).join(" "))}`;for(let f=0;f<d.length;f++)if(o.indexOf(d[f])!==-1||p.indexOf(d[f])!==-1)return`text-success-${j(s.slice(0,2).join(" "))}`;let u=j(s.slice(0,2).join(" "));return u?`text-${u}`:"text-content"}function mo(e){let t=Le(e),n=Yt[t];if("children"in e&&e.children.length>0){let s;for(let o=0;o<e.children.length;o++)if(e.children[o].type==="TEXT"){s=e.children[o];break}if(s&&s.characters){let r=s.characters.trim().split(/\s+/).slice(0,2);if(r.length>0&&r[0].length>0)return`${n}-${j(r.join(" "))}`}if(t==="button"||t==="input"){let o;for(let r=0;r<e.children.length;r++){let i=e.children[r];if(i.type==="VECTOR"||i.name.toLowerCase().indexOf("icon")!==-1){o=i;break}}if(o){let r=o.name.toLowerCase().replace(/icon[-_\s]*/gi,"");if(r&&!Qt(r))return`${n}-${j(r)}`}}}return n}function rt(e,t){if(!e||!t||typeof t!="string")return!1;let n=t.trim();if(n.length===0)return!1;try{return e.name=n,!0}catch(s){return console.error("Failed to rename layer:",s),!1}}function nn(e,t){return{nodeId:e.id,currentName:e.name,newName:t.trim(),layerType:Le(e),willChange:e.name!==t.trim()}}function j(e){return e.replace(/([a-z])([A-Z])/g,"$1-$2").replace(/[\s_]+/g,"-").replace(/[^a-zA-Z0-9-]/g,"").replace(/-+/g,"-").replace(/^-|-$/g,"").toLowerCase()}Fe();async function wt(e){let t=ts(e),n=Sr(t),s=He(e).join(" "),o={width:"width"in e?e.width:0,height:"height"in e?e.height:0,layoutMode:"layoutMode"in e&&e.layoutMode||"NONE"},r={hasFills:ns(e),hasStrokes:ss(e),hasEffects:os(e),cornerRadius:"cornerRadius"in e&&e.cornerRadius||0},i=wr(e),{isComponentSet:a,potentialVariants:c}=Nr(e),l=await br(e);return{name:e.name,type:e.type,hierarchy:t,textContent:s||void 0,frameStructure:o,detectedStyles:r,detectedSlots:i,isComponentSet:a,potentialVariants:c,nestedLayers:n,additionalContext:l}}async function br(e){let t={hasInteractiveElements:!1,possibleUseCase:"",designPatterns:[],componentFamily:"",suggestedConsiderations:[]},n=e.name.toLowerCase(),o=["tabs","tab-group","tabset","nav","navbar","navigation","menu","menubar","dropdown","form","form-group","fieldset","list","grid","collection","gallery","group","container","wrapper","layout","toolbar","panel","sidebar","header","footer","card-group","button-group","radio-group","checkbox-group"].some(a=>n.includes(a)),r=await vr(e),i=o||r;return console.log(`\u{1F50D} [CONTAINER DETECTION] ${e.name}:`),console.log(` Name-based: ${o}`),console.log(` Structure-based: ${r}`),console.log(` Final result: ${i}`),n.includes("avatar")||n.includes("profile")?(t.componentFamily="avatar",t.possibleUseCase="User representation, often clickable for profile access or dropdown menus",t.hasInteractiveElements=!0,t.suggestedConsiderations.push("Consider if this avatar will be clickable/interactive"),t.suggestedConsiderations.push("May need hover/focus states for navigation"),t.designPatterns.push("profile-navigation","user-menu-trigger")):n.includes("button")||n.includes("btn")?(t.componentFamily="button",t.possibleUseCase="Interactive element for user actions",t.hasInteractiveElements=!0,t.suggestedConsiderations.push("Requires all interactive states"),t.designPatterns.push("action-trigger","form-submission")):n.includes("badge")||n.includes("tag")?(t.componentFamily="badge",t.possibleUseCase="Status indicator or label",t.hasInteractiveElements=!1,t.suggestedConsiderations.push("Typically non-interactive unless used as a filter"),t.designPatterns.push("status-indicator","category-label")):n.includes("input")||n.includes("field")?(t.componentFamily="input",t.possibleUseCase="Form input element",t.hasInteractiveElements=!0,t.suggestedConsiderations.push("Needs focus, error, and disabled states"),t.designPatterns.push("form-control","data-entry")):n.includes("card")?(t.componentFamily="card",t.possibleUseCase="Content container",t.hasInteractiveElements=n.includes("clickable")||n.includes("interactive"),t.suggestedConsiderations.push("May be interactive if used for navigation"),t.designPatterns.push("content-container","information-display")):n.includes("icon")?(t.componentFamily="icon",t.possibleUseCase="Visual indicator or decoration",t.hasInteractiveElements=!1,t.suggestedConsiderations.push("Usually decorative, but may be interactive if part of a button"),t.designPatterns.push("visual-indicator","decoration")):i&&(t.componentFamily="container",t.possibleUseCase="Layout container for organizing child components",t.hasInteractiveElements=!1,t.suggestedConsiderations.push("Focus on layout and organization rather than interaction states"),t.suggestedConsiderations.push("Child components handle individual interactions"),t.designPatterns.push("layout-container","component-organization")),"children"in e&&e.findAll(c=>c.type==="TEXT"&&(c.name.toLowerCase().includes("click")||c.name.toLowerCase().includes("action")||c.name.toLowerCase().includes("link"))).length>0&&(t.hasInteractiveElements=!0),e.parent&&e.parent.name.toLowerCase().includes("button")&&(t.hasInteractiveElements=!0,t.suggestedConsiderations.push("Part of a button component - needs interactive states")),t}async function vr(e){if(!("children"in e)||!e.children||e.children.length===0)return!1;let t=e.children.filter(l=>l.type==="INSTANCE");if(t.length===0)return console.log(`\u{1F50D} [STRUCTURE] No child instances found in ${e.name}`),!1;console.log(`\u{1F50D} [STRUCTURE] Analyzing ${e.name} with ${t.length} child instances`);let n=new Map;await Promise.all(t.map(async l=>{try{let d=await l.getMainComponentAsync();if(d){let p=d.name;n.has(p)||n.set(p,[]),n.get(p).push(l)}}catch(d){console.log("\u26A0\uFE0F [STRUCTURE] Could not access main component for instance:",d)}})),console.log("\u{1F50D} [STRUCTURE] Instance groups:",Array.from(n.entries()).map(([l,d])=>`${l}: ${d.length}`));let s=Array.from(n.values()).some(l=>l.length>1),o=Array.from(n.keys()).some(l=>{let d=l.toLowerCase();return d.includes("item")||d.includes("panel")||d.includes("content")||d.includes("section")||d.includes("group")||d.includes("wrapper")||d.includes("tab")&&!d.includes("button")||d.includes("nav-item")||d.includes("menu-item")||d.includes("list-item")||d.includes("card-item")}),r=t.length/e.children.length,i=r>.6,a=n.size>=2&&s;return console.log(`\u{1F50D} [STRUCTURE] Analysis for ${e.name}:`),console.log(` Repeated components: ${s}`),console.log(` Organizational components: ${o}`),console.log(` Instance ratio: ${r.toFixed(2)} (${i?"high":"low"})`),console.log(` Collection pattern: ${a}`),s||o||i&&n.size>=2}function ts(e,t=0){let n=[],s={name:e.name,type:e.type,depth:t};if("children"in e&&e.children.length>0){s.children=[];for(let o of e.children)s.children.push(...ts(o,t+1))}return n.push(s),n}function Sr(e){let t=[];function n(s){for(let o of s)t.push(o.name),o.children&&n(o.children)}return n(e),t}function je(e){let t=new Set;function n(s){for(let o of s)o.type==="INSTANCE"&&t.add(o.name),o.children&&n(o.children)}return n(e),Array.from(t)}function Nr(e){let t=[],n=!1;if(e.type==="COMPONENT_SET"){n=!0;try{let s=e,o;try{o=s.variantGroupProperties}catch(r){console.warn("Component set has errors, cannot access variantGroupProperties:",r),o=void 0}o&&t.push(...Object.keys(o))}catch(s){console.warn("Error analyzing component set:",s)}}else{let s=Te(e).map(r=>r.name.toLowerCase());["primary","secondary","tertiary","small","medium","large","xl","xs","default","hover","focus","active","disabled","filled","outline","ghost","link","light","dark"].forEach(r=>{s.some(i=>i.includes(r))&&(t.includes(r)||t.push(r))})}return{isComponentSet:n,potentialVariants:t}}function wr(e){let t=[],n=Te(e),s=e.name.toLowerCase(),o=["radiobutton","checkbox","icon","button","input","focusring","focus","indicator","background","border","outline","shadow","ring","control","handle","thumb","track","progress","slider","arrow","chevron","close","minimize","maximize"];n.filter(c=>c.type==="TEXT").forEach(c=>{let l=c.name.toLowerCase();o.some(d=>l.includes(d))||s.includes(l)||l.includes(s.split(" ")[0])||(l.includes("title")||l.includes("label")||l.includes("text")||l.includes("content"))&&l.length>2&&t.push(c.name)}),n.filter(c=>c.type==="FRAME").forEach(c=>{let l=c.name.toLowerCase();o.some(d=>l.includes(d))||(l.includes("content")&&!l.includes("background")||l.includes("slot")||l.includes("container")&&!l.includes("main"))&&t.push(c.name)});let a=[...new Set(t)].filter(c=>{let l=c.toLowerCase();return l.length>2&&!["text","label","content"].includes(l)&&!o.some(d=>l.includes(d))});return console.log(`\u{1F50D} [SLOTS] Detected ${a.length} legitimate content slots from ${t.length} candidates:`,a),a}function ns(e){return"fills"in e&&Array.isArray(e.fills)&&e.fills.length>0?e.fills.some(t=>t.visible!==!1):"children"in e?e.children.some(t=>ns(t)):!1}function ss(e){return"strokes"in e&&Array.isArray(e.strokes)&&e.strokes.length>0?e.strokes.some(t=>t.visible!==!1):"children"in e?e.children.some(t=>ss(t)):!1}function os(e){return"effects"in e&&Array.isArray(e.effects)&&e.effects.length>0?e.effects.some(t=>t.visible!==!1):"children"in e?e.children.some(t=>os(t)):!1}function kr(e){let t=new Map;e.children.forEach(s=>{s.type==="COMPONENT"&&s.name.split(",").map(i=>i.trim()).forEach(i=>{let[a,c]=i.split("=").map(l=>l.trim());a&&c&&(t.has(a)||t.set(a,new Set),t.get(a).add(c))})});let n=[];return t.forEach((s,o)=>{let r=Array.from(s);n.push({name:o,values:r,default:r[0]||"default"})}),n}async function rs(e,t){let n=[];if(console.log("\u{1F50D} [DEBUG] Starting property extraction for node:",e.name,"type:",e.type),console.log("\u{1F50D} [DEBUG] Originally selected node:",t==null?void 0:t.name,"type:",t==null?void 0:t.type),t&&t.type==="INSTANCE"){let o=t;console.log("\u{1F50D} [DEBUG] Extracting from selected instance componentProperties...");try{if("componentProperties"in o&&o.componentProperties){let r=o.componentProperties;console.log("\u{1F50D} [DEBUG] Found componentProperties on selected instance:",Object.keys(r));let i=await o.getMainComponentAsync();if(i&&i.parent&&i.parent.type==="COMPONENT_SET"){let a=i.parent,c=null;try{"componentPropertyDefinitions"in a&&(c=a.componentPropertyDefinitions,console.log("\u{1F50D} [DEBUG] Got componentPropertyDefinitions from component set"))}catch(l){console.log("\u{1F50D} [DEBUG] Could not access componentPropertyDefinitions, using instance properties only")}for(let l in r){let d=r[l];console.log(`\u{1F50D} [DEBUG] Processing instance property "${l}":`,d);let p=l,u=[],f="";if(l.includes("#")&&(p=l.split("#")[0]),d&&typeof d=="object"&&"value"in d?f=String(d.value):f=String(d),c&&c[l]){let y=c[l];switch(console.log(`\u{1F50D} [DEBUG] Found property definition for "${l}":`,y),y.type){case"VARIANT":u=y.variantOptions||[];break;case"BOOLEAN":u=["true","false"];break;case"TEXT":u=[f||"Text content"];break;case"INSTANCE_SWAP":y.preferredValues&&Array.isArray(y.preferredValues)?u=y.preferredValues.map(m=>m.key||m.name||"Component instance"):u=["Component instance"];break;default:u=[f||"Property value"]}}else console.log(`\u{1F50D} [DEBUG] No property definition for "${l}", inferring from value`),f==="true"||f==="false"?u=["true","false"]:u=[f||"Property value"];n.push({name:p,values:u,default:f||u[0]||"default"}),console.log("\u{1F50D} [DEBUG] Added instance property:",{name:p,values:u,default:f})}if(n.length>0)return console.log(`\u{1F50D} [DEBUG] Successfully extracted ${n.length} properties from selected instance`),n}}}catch(r){console.log("\u{1F50D} [DEBUG] Could not extract from instance componentProperties:",r)}}if(e.type==="COMPONENT_SET"){let o=e;console.log("\u{1F50D} [DEBUG] Attempting to access componentPropertyDefinitions...");try{if("componentPropertyDefinitions"in o){console.log("\u{1F50D} [DEBUG] componentPropertyDefinitions property exists on componentSet");let r=o.componentPropertyDefinitions;if(console.log("\u{1F50D} [DEBUG] Raw componentPropertyDefinitions:",r),console.log("\u{1F50D} [DEBUG] Type of componentPropertyDefinitions:",typeof r),r&&typeof r=="object"){let i=Object.keys(r);console.log("\u{1F50D} [DEBUG] Found componentPropertyDefinitions with keys:",i);for(let a in r){let c=r[a];console.log(`\u{1F50D} [DEBUG] Processing property "${a}":`,c);let l=a,d=[],p="";switch(a.includes("#")&&(l=a.split("#")[0],console.log(`\u{1F50D} [DEBUG] Cleaned display name: "${l}" from "${a}"`)),c.type){case"VARIANT":d=c.variantOptions||[],p=String(c.defaultValue)||d[0]||"default",console.log(`\u{1F50D} [DEBUG] VARIANT property "${l}": values=${d}, default=${p}`);break;case"BOOLEAN":d=["true","false"],p=c.defaultValue?"true":"false",console.log(`\u{1F50D} [DEBUG] BOOLEAN property "${l}": default=${p}`);break;case"TEXT":d=[String(c.defaultValue||"Text content")],p=String(c.defaultValue||"Text content"),console.log(`\u{1F50D} [DEBUG] TEXT property "${l}": value=${p}`);break;case"INSTANCE_SWAP":c.preferredValues&&Array.isArray(c.preferredValues)?d=c.preferredValues.map(u=>(console.log("\u{1F50D} [DEBUG] INSTANCE_SWAP preferred value:",u),u.key||u.name||"Component instance")):d=["Component instance"],p=d[0]||"Component instance",console.log(`\u{1F50D} [DEBUG] INSTANCE_SWAP property "${l}": values=${d}, default=${p}`);break;default:console.log(`\u{1F50D} [DEBUG] Unknown property type "${c.type}" for "${l}"`),d=["Property value"],p="Default"}n.push({name:l,values:d,default:p}),console.log("\u{1F50D} [DEBUG] Added property:",{name:l,values:d,default:p})}}else console.log("\u{1F50D} [DEBUG] componentPropertyDefinitions is not a valid object:",r)}else console.log("\u{1F50D} [DEBUG] componentPropertyDefinitions property does not exist on componentSet")}catch(r){console.error("\u{1F50D} [ERROR] Could not access componentPropertyDefinitions:",r),console.error("\u{1F50D} [ERROR] Error stack:",r instanceof Error?r.stack:"No stack trace")}if(n.length===0){console.log("\u{1F50D} [DEBUG] No properties found, trying variantGroupProperties fallback...");try{let r=o.variantGroupProperties;if(console.log("\u{1F50D} [DEBUG] variantGroupProperties:",r),r){let i=Object.keys(r);console.log("\u{1F50D} [DEBUG] Found variantGroupProperties with keys:",i);for(let a in r){let c=r[a];console.log(`\u{1F50D} [DEBUG] Processing variant property "${a}":`,c),n.push({name:a,values:c.values,default:c.values[0]||"default"})}}else console.log("\u{1F50D} [DEBUG] variantGroupProperties is null/undefined")}catch(r){console.warn("\u{1F50D} [WARN] Component set has errors, cannot access variantGroupProperties:",r)}}if(n.length===0&&o.children.length>0){console.log("\u{1F50D} [DEBUG] Analyzing variant structure to infer properties...");let r=new Map,i=new Map;o.children.forEach((a,c)=>{if(a.type==="COMPONENT"){let l=a.name;console.log(`\u{1F50D} [DEBUG] Analyzing variant ${c}: ${l}`),l.split(",").map(u=>u.trim()).forEach(u=>{let[f,y]=u.split("=").map(m=>m.trim());f&&y&&(r.has(f)||r.set(f,new Set),r.get(f).add(y))});let p=(u,f="")=>{let y=f?`${f}/${u.name}`:u.name;i.has(y)||i.set(y,[]),i.get(y).push(u.visible),"children"in u&&u.children.forEach(m=>p(m,y))};p(a)}}),r.forEach((a,c)=>{n.find(l=>l.name===c)||n.push({name:c,values:Array.from(a),default:Array.from(a)[0]||"default"})}),i.forEach((a,c)=>{let l=a.includes(!0),d=a.includes(!1);if(l&&d){let u=(c.split("/").pop()||"").replace(/\s*(layer|group|frame|icon|text)?\s*/gi,"").trim();u&&!n.find(f=>f.name===u)&&(n.push({name:u,values:["true","false"],default:"false"}),console.log(`\u{1F50D} [DEBUG] Inferred boolean property from visibility: ${u}`))}}),console.log(`\u{1F50D} [DEBUG] Inferred ${n.length} properties from variant analysis`)}if(n.length===0){console.log("\u{1F50D} [DEBUG] All Figma APIs failed, using comprehensive structural analysis...");let r=xr(o);console.log("\u{1F50D} [DEBUG] Properties from structural analysis:",r),n.push(...r)}}else if(e.type==="COMPONENT"){let o=e;console.log("\u{1F50D} [DEBUG] Processing COMPONENT node:",o.name);try{if("componentPropertyDefinitions"in o){let r=o.componentPropertyDefinitions;if(console.log("\u{1F50D} [DEBUG] Component componentPropertyDefinitions:",r),r&&typeof r=="object"){let i=Object.keys(r);console.log("\u{1F50D} [DEBUG] Found componentPropertyDefinitions on component with keys:",i);for(let a in r){let c=r[a],l=a,d=[],p="";switch(a.includes("#")&&(l=a.split("#")[0]),c.type){case"BOOLEAN":d=["true","false"],p=c.defaultValue?"true":"false";break;case"TEXT":d=[String(c.defaultValue||"Text content")],p=String(c.defaultValue||"Text content");break;case"INSTANCE_SWAP":c.preferredValues&&Array.isArray(c.preferredValues)?d=c.preferredValues.map(u=>u.key||u.name||"Component instance"):d=["Component instance"],p=d[0]||"Component instance";break;default:d=["Property value"],p="Default"}n.push({name:l,values:d,default:p})}}}else console.log("\u{1F50D} [DEBUG] componentPropertyDefinitions does not exist on component")}catch(r){console.warn("\u{1F50D} [WARN] Could not access componentPropertyDefinitions on component:",r)}if(o.parent&&o.parent.type==="COMPONENT_SET"){let r=o.parent;console.log("\u{1F50D} [DEBUG] Component is part of a component set, getting variant properties...");try{let i=r.variantGroupProperties;if(i)for(let a in i){let c=i[a];n.find(l=>l.name===a)||n.push({name:a,values:c.values,default:c.values[0]||"default"})}}catch(i){console.warn("\u{1F50D} [WARN] Component set has errors, cannot access variantGroupProperties:",i)}}}else if(e.type==="INSTANCE"){let o=e;if(console.log("\u{1F50D} [DEBUG] Processing INSTANCE node (fallback \u2014 Priority 1 may have been skipped)"),n.length===0)try{let r=await o.getMainComponentAsync();if(r)if(r.parent&&r.parent.type==="COMPONENT_SET"){let i=r.parent;console.log("\u{1F50D} [DEBUG] Instance fallback: extracting from parent component set:",i.name);try{if("componentPropertyDefinitions"in i){let a=i.componentPropertyDefinitions;if(a&&typeof a=="object"){for(let c in a){let l=a[c],d=c,p=[],u="";switch(c.includes("#")&&(d=c.split("#")[0]),l.type){case"VARIANT":p=l.variantOptions||[],u=String(l.defaultValue)||p[0]||"default";break;case"BOOLEAN":p=["true","false"],u=l.defaultValue?"true":"false";break;case"TEXT":p=[String(l.defaultValue||"Text content")],u=String(l.defaultValue||"Text content");break;case"INSTANCE_SWAP":l.preferredValues&&Array.isArray(l.preferredValues)?p=l.preferredValues.map(f=>f.key||f.name||"Component instance"):p=["Component instance"],u=p[0]||"Component instance";break;default:p=["Property value"],u="Default"}n.push({name:d,values:p,default:u})}console.log(`\u{1F50D} [DEBUG] Instance fallback: extracted ${n.length} properties from component set`)}}}catch(a){console.warn("\u{1F50D} [WARN] Instance fallback: could not access componentPropertyDefinitions:",a)}if(n.length===0)try{let a=i.variantGroupProperties;if(a)for(let c in a){let l=a[c];n.find(d=>d.name===c)||n.push({name:c,values:l.values,default:l.values[0]||"default"})}}catch(a){console.warn("\u{1F50D} [WARN] Instance fallback: could not access variantGroupProperties:",a)}}else{console.log("\u{1F50D} [DEBUG] Instance fallback: extracting from standalone main component");try{if("componentPropertyDefinitions"in r){let i=r.componentPropertyDefinitions;if(i&&typeof i=="object"){for(let a in i){let c=i[a],l=a,d=[],p="";switch(a.includes("#")&&(l=a.split("#")[0]),c.type){case"BOOLEAN":d=["true","false"],p=c.defaultValue?"true":"false";break;case"TEXT":d=[String(c.defaultValue||"Text content")],p=String(c.defaultValue||"Text content");break;case"INSTANCE_SWAP":c.preferredValues&&Array.isArray(c.preferredValues)?d=c.preferredValues.map(u=>u.key||u.name||"Component instance"):d=["Component instance"],p=d[0]||"Component instance";break;default:d=["Property value"],p="Default"}n.push({name:l,values:d,default:p})}console.log(`\u{1F50D} [DEBUG] Instance fallback: extracted ${n.length} properties from main component`)}}}catch(i){console.warn("\u{1F50D} [WARN] Instance fallback: could not access componentPropertyDefinitions on main component:",i)}}}catch(r){console.warn("\u{1F50D} [WARN] Instance fallback: could not get main component:",r)}}let s=[];return n.forEach(o=>{s.find(r=>r.name===o.name)||s.push(o)}),console.log(`\u{1F50D} [DEBUG] Final result: Extracted ${s.length} unique properties:`,s.map(o=>({name:o.name,valueCount:o.values.length,default:o.default}))),s}function xr(e){let t=[];console.log("\u{1F50D} [STRUCTURAL] Starting comprehensive structural analysis of component set:",e.name);let n=kr(e);t.push(...n);let s=new Set,o=new Set,r=new Set,i=new Set;e.children.forEach(l=>{if(l.type==="COMPONENT"){console.log(`\u{1F50D} [STRUCTURAL] Analyzing variant: ${l.name}`);let d=(p,u=0)=>{let f=" ".repeat(u);console.log(`\u{1F50D} [STRUCTURAL] ${f}Found child: ${p.name} (type: ${p.type})`),s.add(p.name),p.type==="TEXT"?o.add(p.name):p.type==="INSTANCE"&&r.add(p.name),(p.visible===!1||p.name.toLowerCase().includes("hidden"))&&i.add(p.name),"children"in p&&p.children&&p.children.forEach(y=>d(y,u+1))};d(l)}}),console.log("\u{1F50D} [STRUCTURAL] Analysis results:"),console.log("\u{1F50D} [STRUCTURAL] - All child names:",Array.from(s)),console.log("\u{1F50D} [STRUCTURAL] - Text layers:",Array.from(o)),console.log("\u{1F50D} [STRUCTURAL] - Instance layers:",Array.from(r)),console.log("\u{1F50D} [STRUCTURAL] - Boolean indicators:",Array.from(i)),o.forEach(l=>{let d=l.replace(/\s*(layer|text|label)?\s*/gi,"").trim();d&&!t.find(p=>p.name.toLowerCase()===d.toLowerCase())&&(t.push({name:d,values:["Text content"],default:"Label"}),console.log(`\u{1F50D} [STRUCTURAL] Added TEXT property: ${d}`))}),r.forEach(l=>{let d=l.replace(/\s*(layer|instance)?\s*/gi,"").trim();d&&!t.find(p=>p.name.toLowerCase()===d.toLowerCase())&&(t.push({name:d,values:["Component instance"],default:"Default component"}),console.log(`\u{1F50D} [STRUCTURAL] Added INSTANCE_SWAP property: ${d}`))}),["icon before","icon after","slot before","slot after","before","after","prefix","suffix","leading","trailing"].forEach(l=>{if(Array.from(s).find(p=>p.toLowerCase().includes(l.toLowerCase()))&&!t.find(p=>p.name.toLowerCase().includes(l.toLowerCase()))){let p=l.split(" ").map(u=>u.charAt(0).toUpperCase()+u.slice(1)).join(" ");t.push({name:p,values:["true","false"],default:"false"}),console.log(`\u{1F50D} [STRUCTURAL] Added BOOLEAN property: ${p}`)}});let c=e.name.toLowerCase();return(c.includes("button")||c.includes("btn"))&&[{name:"Slot Before",type:"BOOLEAN"},{name:"Text",type:"TEXT"},{name:"Icon Before",type:"INSTANCE_SWAP"},{name:"Icon After",type:"INSTANCE_SWAP"}].forEach(({name:d,type:p})=>{if(!t.find(u=>u.name.toLowerCase()===d.toLowerCase())){let u,f;switch(p){case"BOOLEAN":u=["true","false"],f="false";break;case"TEXT":u=["Text content"],f="Label";break;case"INSTANCE_SWAP":u=["Component instance"],f="Default icon";break;default:u=["Property value"],f="Default"}t.push({name:d,values:u,default:f}),console.log(`\u{1F50D} [STRUCTURAL] Added common ${p} property: ${d}`)}}),console.log(`\u{1F50D} [STRUCTURAL] Final structural analysis result: ${t.length} properties found`),t}async function De(e){let t=[];if(e.type==="COMPONENT_SET"){let s=e,o;try{o=s.variantGroupProperties}catch(r){console.warn("Component set has errors, cannot access variantGroupProperties:",r),o=void 0}if(o)for(let r in o){let i=r.toLowerCase();(i==="state"||i==="states"||i==="status")&&t.push(...o[r].values)}s.children.forEach(r=>{let i=r.name.toLowerCase();["default","hover","focus","disabled","pressed","active","selected"].forEach(a=>{let c=t.find(l=>l.toLowerCase()===a.toLowerCase());i.includes(a)&&!c&&t.push(a)})})}else if(e.type==="COMPONENT"){let s=e;if(s.parent&&s.parent.type==="COMPONENT_SET")return await De(s.parent)}else if(e.type==="INSTANCE"){let o=await e.getMainComponentAsync();if(o)return await De(o)}let n=[];return t.forEach(s=>{s&&typeof s=="string"&&s.trim()!==""&&(n.find(r=>r.toLowerCase()===s.toLowerCase())||n.push(s.trim()))}),n}async function is(e,t,n,s={},o="anthropic"){console.log("\u{1F3AF} Starting enhanced component analysis...");let r=figma.currentPage.selection[0],i=s.node||r;if(!i)throw new Error("No node selected");let a=await rs(i,r),c=await De(i),l=await ce(i),d="";if(i.type==="COMPONENT"||i.type==="COMPONENT_SET")d=i.description||"";else if(i.type==="INSTANCE"){let A=await i.getMainComponentAsync();A&&(d=A.description||"")}e.existingDescription=d;let p=Ne([i],s.lintSettings||X);console.log(`\u{1F50D} [LINT] Deterministic lint: ${p.summary.totalErrors} issues in ${p.summary.nodesWithErrors} nodes`),console.log("\u{1F4CA} [ANALYSIS] Extracted from Figma API:"),console.log(` Properties: ${a.length}`),console.log(` States: ${c.length}`),console.log(` Tokens: ${Object.keys(l).length} categories`),console.log(` Description: ${d?"Present":"Missing"}`);let u=s.mcpServerUrl||"http://localhost:3000/mcp",f=s.useMCP!==!1&&u,y;if(f){console.log(`\u{1F504} Using hybrid LLM + MCP approach (${o})...`);let v=Cr(e,a,c,l,d,p),A=await ie(o,t,{prompt:v,model:n,maxTokens:2048,temperature:.1}),w=le(A.content);if(!w)throw new Error("Failed to extract JSON from LLM response");let k=null;try{k=await Ir(e,u,w),console.log("\u2705 MCP enhancements received")}catch(g){console.warn("\u26A0\uFE0F MCP enhancement failed, continuing with LLM data only:",g)}y=Ar(w,k,{node:i,context:e,actualProperties:a,actualStates:c,tokens:l,componentDescription:d})}else{console.log(`\u{1F4DD} Using ${o}-only analysis...`);let v=Ht(e),A=await ie(o,t,{prompt:v,model:n,maxTokens:2048,temperature:.1});if(y=le(A.content),!y)throw new Error("Failed to extract JSON from response")}let m=Pe(y);return await kt(m,e,s,p,t,n,o)}function Cr(e,t,n,s,o,r){var l;let i=((l=e.additionalContext)==null?void 0:l.componentFamily)||"generic",a=je(e.hierarchy),c="";if(r&&r.summary.totalErrors>0){let d=r.summary.byType,p=r.errors.slice(0,15).map(u=>` - [${u.errorType.toUpperCase()}] ${u.nodeName}: ${u.message}`).join(` +`&&(i=e.length),i===-1)continue;let a=e.substring(r,i).trim();try{return JSON.parse(a)}catch(c){if(a.startsWith("{"))try{return sn(a)}catch(d){continue}}}return null}function _o(e){let t=/```(?:json)?\s*(\{[\s\S]*?\})\s*```/gi,n;for(;(n=t.exec(e))!==null;)try{return JSON.parse(n[1])}catch(s){continue}return null}function Bo(e){let t=e.match(/\{[\s\S]*\}/);return t?JSON.parse(t[0]):null}function Fe(e){if(!e||typeof e!="object")return e;let t=["aria","accessibility api","semantic html","keyboard navigation","event handler","interactive behavior","onclick","onchange","state management","controlled component","uncontrolled component","props","responsive breakpoint","css implementation","@media","animation token","transition timing","programmatic animation","keyframe","api integration","data binding","dynamic content","fetch","axios","implement","add handler","bind event","attach listener","programming pattern","functional pattern","react hook","usestate","useeffect"],n=r=>{let i=r.toLowerCase();return t.some(a=>i.includes(a))},s=r=>Array.isArray(r)?r.filter(i=>{if(typeof i=="string"){let a=!n(i);return a||console.log("\u{1F6AB} [FILTER] Removed development-focused recommendation:",i),a}return!0}):r,o=JSON.parse(JSON.stringify(e));return o.mcpReadiness&&(o.mcpReadiness.recommendations&&(o.mcpReadiness.recommendations=s(o.mcpReadiness.recommendations)),o.mcpReadiness.gaps&&(o.mcpReadiness.gaps=s(o.mcpReadiness.gaps))),o.audit&&(o.audit.tokenOpportunities&&(o.audit.tokenOpportunities=s(o.audit.tokenOpportunities)),o.audit.structureIssues&&(o.audit.structureIssues=s(o.audit.structureIssues))),o.accessibility&&(o.accessibility.designConsiderations&&(o.accessibility.designConsiderations=s(o.accessibility.designConsiderations)),o.accessibility.visualIndicators&&(o.accessibility.visualIndicators=s(o.accessibility.visualIndicators))),o}var E=class extends Error{constructor(n,s,o,r){super(n);this.code=s;this.statusCode=o;this.retryAfter=r;this.name="LLMError"}};var tt={anthropic:"claude-sonnet-4-5-20250929",openai:"gpt-5.2",google:"gemini-2.5-pro"};var Uo=[{id:"claude-opus-4-5-20251218",name:"Claude Opus 4.5",description:"Flagship model - Most capable, best for complex analysis and reasoning",contextWindow:2e5,isDefault:!1},{id:"claude-sonnet-4-5-20250929",name:"Claude Sonnet 4.5",description:"Standard model - Balanced performance and cost, recommended for most tasks",contextWindow:2e5,isDefault:!0},{id:"claude-haiku-4-5-20251001",name:"Claude Haiku 4.5",description:"Economy model - Fastest responses, ideal for quick analysis",contextWindow:2e5,isDefault:!1}],nt=class{constructor(){this.name="Anthropic";this.id="anthropic";this.endpoint="https://api.anthropic.com/v1/messages";this.keyPrefix="sk-ant-";this.keyPlaceholder="sk-ant-...";this.models=Uo}formatRequest(t){let n={model:t.model,messages:[{role:"user",content:t.prompt.trim()}],max_tokens:t.maxTokens};return t.temperature!==void 0&&(n.temperature=t.temperature),t.additionalParams&&Object.assign(n,t.additionalParams),n}parseResponse(t){let n=t;if(!n.content||!Array.isArray(n.content))throw new E("Invalid response format from Anthropic API: missing content array","INVALID_REQUEST");let s=n.content.filter(o=>o.type==="text").map(o=>o.text).join(` +`);if(!s)throw new E("Invalid response format from Anthropic API: no text content found","INVALID_REQUEST");return{content:s.trim(),model:n.model,usage:n.usage?{promptTokens:n.usage.input_tokens,completionTokens:n.usage.output_tokens,totalTokens:n.usage.input_tokens+n.usage.output_tokens}:void 0,metadata:{id:n.id,stopReason:n.stop_reason}}}validateApiKey(t){if(!t||typeof t!="string")return{isValid:!1,error:"API Key Required: Please provide a valid Claude API key."};let n=t.trim();return n.length===0?{isValid:!1,error:"API Key Required: The Claude API key cannot be empty."}:n.startsWith(this.keyPrefix)?n.length<40?{isValid:!1,error:"Invalid API Key Format: The API key appears to be too short. Please verify you copied the complete key."}:{isValid:!0}:{isValid:!1,error:`Invalid API Key Format: Claude API keys should start with "${this.keyPrefix}". Please check your API key.`}}getHeaders(t){return{"content-type":"application/json","x-api-key":t.trim(),"anthropic-version":"2023-06-01","anthropic-dangerous-direct-browser-access":"true"}}getDefaultModel(){return this.models.find(n=>n.isDefault)||this.models[1]}handleError(t,n){var r;let s=n,o=((r=s==null?void 0:s.error)==null?void 0:r.message)||(typeof n=="string"?n:"Unknown error");switch(t){case 400:return new E(`Claude API Error (400): ${o}. Please check your request format.`,"INVALID_REQUEST",400);case 401:return new E("Claude API Error (401): Invalid API key. Please check your Claude API key in settings.","INVALID_API_KEY",401);case 403:return new E("Claude API Error (403): Access forbidden. Please check your API key permissions.","INVALID_API_KEY",403);case 404:return new E(`Claude API Error (404): ${o}. The requested model may not be available.`,"MODEL_NOT_FOUND",404);case 429:return new E("Claude API Error (429): Rate limit exceeded. Please try again later.","RATE_LIMIT_EXCEEDED",429);case 500:return new E("Claude API Error (500): Server error. The Claude API is experiencing issues. Please try again later.","SERVER_ERROR",500);case 503:return new E("Claude API Error (503): Service unavailable. The Claude API is temporarily down. Please try again later.","SERVICE_UNAVAILABLE",503);default:return new E(`Claude API Error (${t}): ${o}`,"UNKNOWN_ERROR",t)}}},ot=new nt;var Go=[{id:"gpt-5.2",name:"GPT-5.2",description:"Flagship model with advanced reasoning capabilities",contextWindow:128e3,isDefault:!0},{id:"gpt-5.2-pro",name:"GPT-5.2 Pro",description:"Premium model with extended reasoning for complex tasks",contextWindow:128e3,isDefault:!1},{id:"gpt-5-mini",name:"GPT-5 Mini",description:"Economy model - fast and cost-effective",contextWindow:128e3,isDefault:!1}],rt=class{constructor(){this.name="OpenAI";this.id="openai";this.endpoint="https://api.openai.com/v1/chat/completions";this.keyPrefix="sk-";this.keyPlaceholder="sk-...";this.models=Go}formatRequest(t){let n={model:t.model,messages:[{role:"user",content:t.prompt}],max_completion_tokens:t.maxTokens,temperature:t.temperature};return t.additionalParams&&Object.assign(n,t.additionalParams),n}parseResponse(t){let n=t;if(!n.choices||n.choices.length===0)throw new E("Invalid response format: no choices returned","INVALID_REQUEST");let s=n.choices[0];if(!s.message||typeof s.message.content!="string")throw new E("Invalid response format: missing message content","INVALID_REQUEST");let o={content:s.message.content.trim(),model:n.model};return n.usage&&(o.usage={promptTokens:n.usage.prompt_tokens,completionTokens:n.usage.completion_tokens,totalTokens:n.usage.total_tokens}),o.metadata={id:n.id,finishReason:s.finish_reason,created:n.created},o}validateApiKey(t){if(!t||typeof t!="string")return{isValid:!1,error:"API Key Required: Please provide a valid OpenAI API key."};let n=t.trim();return n.length===0?{isValid:!1,error:"API Key Required: The OpenAI API key cannot be empty."}:n.startsWith(this.keyPrefix)?n.length<20?{isValid:!1,error:"Invalid API Key Format: The API key appears to be too short. Please verify you copied the complete key."}:{isValid:!0}:{isValid:!1,error:`Invalid API Key Format: OpenAI API keys should start with "${this.keyPrefix}". Please check your API key.`}}getHeaders(t){return{"Content-Type":"application/json",Authorization:`Bearer ${t.trim()}`}}getDefaultModel(){return this.models.find(n=>n.isDefault)||this.models[0]}handleError(t,n){var r;let s=n,o=((r=s==null?void 0:s.error)==null?void 0:r.message)||(s==null?void 0:s.message)||"Unknown error occurred";switch(t){case 400:return o.toLowerCase().includes("context_length_exceeded")||o.toLowerCase().includes("maximum context length")?new E(`OpenAI API Error (400): Context length exceeded. ${o}`,"CONTEXT_LENGTH_EXCEEDED",t):new E(`OpenAI API Error (400): ${o}. Please check your request format.`,"INVALID_REQUEST",t);case 401:return new E("OpenAI API Error (401): Invalid API key. Please check your OpenAI API key in settings.","INVALID_API_KEY",t);case 403:return new E("OpenAI API Error (403): Access forbidden. Please check your API key permissions or account status.","INVALID_API_KEY",t);case 404:return new E(`OpenAI API Error (404): Model not found. ${o}`,"MODEL_NOT_FOUND",t);case 429:let i=o.match(/try again in (\d+)/i),a=i?parseInt(i[1],10):void 0;return new E(`OpenAI API Error (429): Rate limit exceeded. ${a?`Please try again in ${a} seconds.`:"Please try again later."}`,"RATE_LIMIT_EXCEEDED",t,a);case 500:return new E("OpenAI API Error (500): Server error. The OpenAI API is experiencing issues. Please try again later.","SERVER_ERROR",t);case 502:return new E("OpenAI API Error (502): Bad gateway. The OpenAI API is temporarily unavailable. Please try again later.","SERVICE_UNAVAILABLE",t);case 503:return new E("OpenAI API Error (503): Service unavailable. The OpenAI API is temporarily down. Please try again later.","SERVICE_UNAVAILABLE",t);case 504:return new E("OpenAI API Error (504): Gateway timeout. The request took too long. Please try again.","SERVICE_UNAVAILABLE",t);default:return new E(`OpenAI API Error (${t}): ${o}`,"UNKNOWN_ERROR",t)}}},it=new rt;var zo=[{id:"gemini-3-pro-preview",name:"Gemini 3 Pro",description:"Flagship model with advanced reasoning and multimodal capabilities",contextWindow:1e6,isDefault:!0},{id:"gemini-2.5-pro",name:"Gemini 2.5 Pro",description:"Standard reasoning model with excellent performance",contextWindow:1e6,isDefault:!1},{id:"gemini-2.5-flash",name:"Gemini 2.5 Flash",description:"Economy model optimized for speed and efficiency",contextWindow:1e6,isDefault:!1}],at=class{constructor(){this.name="Google";this.id="google";this.endpoint="https://generativelanguage.googleapis.com/v1beta/models";this.keyPrefix="AIza";this.keyPlaceholder="AIza...";this.models=zo}formatRequest(t){let n={contents:[{parts:[{text:t.prompt}]}],generationConfig:{maxOutputTokens:t.maxTokens,temperature:t.temperature}};if(t.additionalParams){let{topP:s,topK:o,stopSequences:r}=t.additionalParams;s!==void 0&&(n.generationConfig.topP=s),o!==void 0&&(n.generationConfig.topK=o),r!==void 0&&(n.generationConfig.stopSequences=r)}return n}parseResponse(t){var c;let n=t;if(n.error)throw new E(n.error.message||"Unknown Gemini API error",this.mapErrorCodeToLLMErrorCode(n.error.code,n.error.status),n.error.code);if(!n.candidates||n.candidates.length===0){let d=Object.keys(n);throw new E(`No candidates in Gemini response. Response keys: [${d.join(", ")}]${n.error?`. Error: ${n.error.message}`:""}`,"INVALID_REQUEST")}let s=n.candidates[0];if(s.finishReason==="SAFETY")throw new E("Gemini response blocked by safety filters. Try rephrasing the prompt.","INVALID_REQUEST");let o=(c=s.content)==null?void 0:c.parts;if(!o||o.length===0)throw new E(`No content parts in Gemini response. Finish reason: ${s.finishReason||"unknown"}. Has content: ${!!s.content}`,"INVALID_REQUEST");let r=o.find(d=>typeof d.text=="string");if(!r||!r.text){let d=o.map(l=>Object.keys(l).join(",")).join("; ");throw new E(`No text content in Gemini response parts. Part types: [${d}]. Finish reason: ${s.finishReason||"unknown"}`,"INVALID_REQUEST")}let a={content:r.text,model:"gemini"};return n.usageMetadata&&(a.usage={promptTokens:n.usageMetadata.promptTokenCount||0,completionTokens:n.usageMetadata.candidatesTokenCount||0,totalTokens:n.usageMetadata.totalTokenCount||0}),a}validateApiKey(t){if(!t||typeof t!="string")return{isValid:!1,error:"API key is required"};let n=t.trim();return n.length===0?{isValid:!1,error:"API key cannot be empty"}:n.startsWith(this.keyPrefix)?n.length<30||n.length>50?{isValid:!1,error:"API key appears to have an invalid length. Please verify you copied the complete key."}:/^[A-Za-z0-9_-]+$/.test(n)?{isValid:!0}:{isValid:!1,error:"API key contains invalid characters"}:{isValid:!1,error:`Google API keys should start with "${this.keyPrefix}". Please check your API key.`}}getHeaders(t){return{"Content-Type":"application/json"}}getEndpoint(t,n){let s=n.trim();return`${this.endpoint}/${t}:generateContent?key=${s}`}getDefaultModel(){return this.models.find(n=>n.isDefault)||this.models[0]}handleError(t,n){var p,u;let s=n,o=s==null?void 0:s.error,r=(o==null?void 0:o.message)||"Unknown Google API error",i=o==null?void 0:o.status,a=(o==null?void 0:o.code)||t,c=this.mapErrorCodeToLLMErrorCode(a,i),d;if(t===429){d=6e4;let g=(p=o==null?void 0:o.details)==null?void 0:p.find(f=>{var m;return(m=f["@type"])==null?void 0:m.includes("RetryInfo")});if((u=g==null?void 0:g.metadata)!=null&&u.retryDelay){let f=g.metadata.retryDelay.match(/(\d+)s/);f&&(d=parseInt(f[1],10)*1e3)}}let l=r;switch(c){case"INVALID_API_KEY":l="Google API Error: Invalid API key. Please check your API key in settings.";break;case"RATE_LIMIT_EXCEEDED":l=`Google API Error: Rate limit exceeded. ${d?`Please try again in ${Math.ceil(d/1e3)} seconds.`:"Please try again later."}`;break;case"MODEL_NOT_FOUND":l="Google API Error: Model not found. Please select a valid model.";break;case"CONTEXT_LENGTH_EXCEEDED":l="Google API Error: Input too long. Please reduce the size of your request.";break;case"SERVER_ERROR":l="Google API Error: Server error. Please try again later.";break;case"SERVICE_UNAVAILABLE":l="Google API Error: Service temporarily unavailable. Please try again later.";break}return new E(l,c,t,d)}mapErrorCodeToLLMErrorCode(t,n){if(n){let s=n.toUpperCase();if(s==="INVALID_ARGUMENT")return"INVALID_REQUEST";if(s==="PERMISSION_DENIED"||s==="UNAUTHENTICATED")return"INVALID_API_KEY";if(s==="NOT_FOUND")return"MODEL_NOT_FOUND";if(s==="RESOURCE_EXHAUSTED")return"RATE_LIMIT_EXCEEDED";if(s==="UNAVAILABLE")return"SERVICE_UNAVAILABLE"}switch(t){case 400:return"INVALID_REQUEST";case 401:case 403:return"INVALID_API_KEY";case 404:return"MODEL_NOT_FOUND";case 429:return"RATE_LIMIT_EXCEEDED";case 500:return"SERVER_ERROR";case 503:return"SERVICE_UNAVAILABLE";default:return"UNKNOWN_ERROR"}}},ct=new at;var Wo={anthropic:ot,openai:it,google:ct};function re(e){let t=Wo[e];if(!t)throw new E(`Unknown provider: ${e}`,"INVALID_REQUEST",400);return t}async function ie(e,t,n){var c,d;let s=re(e),o=s.validateApiKey(t);if(!o.isValid)throw new E(o.error||"Invalid API key format","INVALID_API_KEY",401);let r=s.formatRequest(n),i=s.getHeaders(t),a=s.endpoint;e==="google"&&(a=`${s.endpoint}/${n.model}:generateContent?key=${t.trim()}`);try{console.log(`Making ${s.name} API call to ${a}...`);let l=await fetch(a,{method:"POST",headers:i,body:JSON.stringify(r)});if(!l.ok){let u;try{u=await l.json()}catch(g){u=await l.text()}throw s.handleError(l.status,u)}let p=await l.json();return console.log(`${s.name} API response status: ${l.status}`),console.log(`${s.name} API response keys:`,Object.keys(p)),e==="google"&&(console.log("Gemini response candidates:",p.candidates?p.candidates.length:"none"),(c=p.candidates)!=null&&c[0]&&(console.log("Gemini candidate[0] keys:",Object.keys(p.candidates[0])),p.candidates[0].content&&console.log("Gemini content parts:",((d=p.candidates[0].content.parts)==null?void 0:d.length)||"none")),p.error&&console.log("Gemini error:",JSON.stringify(p.error))),s.parseResponse(p)}catch(l){throw l instanceof E?l:l instanceof Error&&(l.message.includes("Failed to fetch")||l.message.includes("NetworkError"))?new E(`Network error connecting to ${s.name}. Please check your internet connection.`,"NETWORK_ERROR"):new E(`Unexpected error calling ${s.name}: ${l instanceof Error?l.message:"Unknown error"}`,"UNKNOWN_ERROR")}}var W={SELECTED_PROVIDER:"selected-provider",SELECTED_MODEL:"selected-model",apiKey:e=>`${e}-api-key`,LEGACY_CLAUDE_KEY:"claude-api-key",LEGACY_CLAUDE_MODEL:"claude-model"},Ho={provider:"anthropic",model:tt.anthropic};async function Ko(){try{let e=await figma.clientStorage.getAsync(W.LEGACY_CLAUDE_KEY),t=await figma.clientStorage.getAsync(W.LEGACY_CLAUDE_MODEL);return e?{needsMigration:!0,legacyKey:e,legacyModel:t}:{needsMigration:!1}}catch(e){return{needsMigration:!1}}}async function lt(){let e=await Ko();e.needsMigration&&(console.log("Migrating legacy Claude storage to multi-provider format..."),e.legacyKey&&await figma.clientStorage.setAsync(W.apiKey("anthropic"),e.legacyKey),await figma.clientStorage.setAsync(W.SELECTED_PROVIDER,"anthropic"),e.legacyModel&&await figma.clientStorage.setAsync(W.SELECTED_MODEL,e.legacyModel),await figma.clientStorage.deleteAsync(W.LEGACY_CLAUDE_KEY),await figma.clientStorage.deleteAsync(W.LEGACY_CLAUDE_MODEL),console.log("Migration complete"))}async function dt(){await lt();let e=await figma.clientStorage.getAsync(W.SELECTED_PROVIDER)||Ho.provider,t=await figma.clientStorage.getAsync(W.SELECTED_MODEL)||tt[e],n=await figma.clientStorage.getAsync(W.apiKey(e));return{providerId:e,modelId:t,apiKey:n}}async function ut(e,t,n){await figma.clientStorage.setAsync(W.SELECTED_PROVIDER,e),await figma.clientStorage.setAsync(W.SELECTED_MODEL,t),n!==void 0&&await figma.clientStorage.setAsync(W.apiKey(e),n)}async function on(e){await figma.clientStorage.deleteAsync(W.apiKey(e))}var rn=/^(Frame|Rectangle|Ellipse|Group|Vector|Line|Polygon|Star|Text|Component|Instance|Slice|Boolean|Union|Subtract|Intersect|Exclude)\s*\d*$/i,an=/\s+\d+$/,cn={button:"btn",icon:"ico",input:"input",text:"txt",image:"img",container:"container",card:"card",list:"list","list-item":"list-item",nav:"nav",header:"header",footer:"footer",modal:"modal",dropdown:"dropdown",checkbox:"checkbox",radio:"radio",toggle:"toggle",avatar:"avatar",badge:"badge",divider:"divider",spacer:"spacer",link:"link",tab:"tab",tooltip:"tooltip",alert:"alert",progress:"progress",skeleton:"skeleton",unknown:"layer"},me=[["btn","button"],["button","button"],["cta","button"],["submit","button"],["icon","icon"],["ico","icon"],["glyph","icon"],["symbol","icon"],["arrow","icon"],["chevron","icon"],["close","icon"],["plus","icon"],["minus","icon"],["txt","text"],["label","text"],["title","text"],["heading","text"],["paragraph","text"],["description","text"],["caption","text"],["subtitle","text"],["input","input"],["field","input"],["textfield","input"],["textarea","input"],["searchfield","input"],["searchbox","input"],["image","image"],["img","image"],["photo","image"],["picture","image"],["thumbnail","image"],["cover","image"],["container","container"],["wrapper","container"],["content","container"],["section","container"],["block","container"],["box","container"],["card","card"],["tile","card"],["panel","card"],["list","list"],["items","list"],["item","list-item"],["row","list-item"],["listitem","list-item"],["nav","nav"],["navbar","nav"],["navigation","nav"],["sidebar","nav"],["breadcrumb","nav"],["menu","nav"],["header","header"],["topbar","header"],["footer","footer"],["bottombar","footer"],["modal","modal"],["dialog","modal"],["popup","modal"],["overlay","modal"],["dropdown","dropdown"],["select","dropdown"],["picker","dropdown"],["combobox","dropdown"],["checkbox","checkbox"],["checkmark","checkbox"],["radio","radio"],["toggle","toggle"],["switch","toggle"],["avatar","avatar"],["profile","avatar"],["userpic","avatar"],["badge","badge"],["tag","badge"],["chip","badge"],["pill","badge"],["status","badge"],["divider","divider"],["separator","divider"],["hr","divider"],["spacer","spacer"],["gap","spacer"],["link","link"],["anchor","link"],["href","link"],["tab","tab"],["tabs","tab"],["tabbar","tab"],["tooltip","tooltip"],["hint","tooltip"],["popover","tooltip"],["alert","alert"],["notification","alert"],["toast","alert"],["message","alert"],["snackbar","alert"],["banner","alert"],["progress","progress"],["loader","progress"],["loading","progress"],["spinner","progress"],["progressbar","progress"],["skeleton","skeleton"],["placeholder","skeleton"],["shimmer","skeleton"]];function ln(e){if(!e||typeof e!="string")return!0;let t=e.trim();return!!(rn.test(t)||t.length===1||/^\d+$/.test(t))}function jo(e){return an.test(e.trim())}function De(e){let t=e.name.toLowerCase();for(let n=0;n<me.length;n++){let s=me[n];if(t.indexOf(s[0])!==-1)return s[1]}switch(e.type){case"TEXT":return"text";case"VECTOR":case"STAR":case"POLYGON":case"BOOLEAN_OPERATION":return"icon";case"RECTANGLE":case"ELLIPSE":case"LINE":if("fills"in e&&Array.isArray(e.fills)){let n=e.fills,s=!1;for(let o=0;o<n.length;o++){let r=n[o];if(r.type==="IMAGE"&&r.visible!==!1){s=!0;break}}if(s)return"image"}if("width"in e&&"height"in e){let n=e.width,s=e.height,o=n/s;if(s<=2&&n>20||n<=2&&s>20)return"divider";if(n<=32&&s<=32&&o>.5&&o<2)return"spacer"}return"unknown";case"FRAME":case"GROUP":return dn(e);case"COMPONENT":case"INSTANCE":return un(e);case"COMPONENT_SET":return qo(e);default:return"unknown"}}function dn(e){if(!("children"in e)||e.children.length===0)return"container";let t=e.children,n=[],s=[];for(let c=0;c<t.length;c++)n.push(t[c].type),s.push(t[c].name.toLowerCase());let o=!1,r=!1;for(let c=0;c<n.length;c++)n[c]==="TEXT"&&(o=!0),(n[c]==="VECTOR"||s[c].indexOf("icon")!==-1)&&(r=!0);let i="width"in e&&"height"in e&&e.width<300&&e.height<100;if(o&&i&&(r||t.length<=3)&&"layoutMode"in e&&e.layoutMode!=="NONE")return"button";let a=!1;for(let c=0;c<n.length;c++)if(n[c]==="RECTANGLE"||s[c].indexOf("image")!==-1){a=!0;break}if(o&&a&&t.length>=2)return"card";if(t.length>=3){let c=t[0].type,d=!0;for(let l=1;l<t.length;l++)if(t[l].type!==c){d=!1;break}if(d&&(c==="FRAME"||c==="INSTANCE"))return"list"}if("cornerRadius"in e&&e.cornerRadius&&t.length<=2&&o&&i)return"input";if("layoutMode"in e&&e.layoutMode==="HORIZONTAL"){let c=0;for(let d=0;d<t.length;d++){let l=t[d].type;(l==="FRAME"||l==="INSTANCE"||l==="TEXT")&&c++}if(c>=3&&i)return"nav"}return"container"}function un(e){let t=e.name.toLowerCase();for(let n=0;n<me.length;n++){let s=me[n];if(t.indexOf(s[0])!==-1)return s[1]}return"children"in e?dn(e):"unknown"}function qo(e){let t=e.name.toLowerCase();for(let n=0;n<me.length;n++){let s=me[n];if(t.indexOf(s[0])!==-1)return s[1]}return"children"in e&&e.children.length>0?un(e.children[0]):"unknown"}function pn(e,t=10){let n=[];function s(o,r,i){if(r>t)return;let a=i?`${i} > ${o.name}`:o.name,c=De(o);if(ln(o.name)){let d=fe(o);n.push({nodeId:o.id,nodeName:o.name,currentName:o.name,suggestedName:d,severity:"error",reason:"Generic layer name detected",layerType:c,depth:r,path:a})}else if(jo(o.name)){let d=o.name.replace(an,"").trim(),l=fe(o);n.push({nodeId:o.id,nodeName:o.name,currentName:o.name,suggestedName:l!==o.name?l:d,severity:"warning",reason:"Layer name has numbered suffix (possible duplicate)",layerType:c,depth:r,path:a})}if("children"in o)for(let d=0;d<o.children.length;d++)s(o.children[d],r+1,a)}return s(e,0,""),n}function fe(e){let t=De(e);return e.type==="TEXT"?Xo(e):e.type==="VECTOR"||e.type==="STAR"||e.type==="POLYGON"||e.type==="BOOLEAN_OPERATION"?Jo(e):"children"in e&&e.children.length>0?Yo(e):cn[t]||"layer"}function Jo(e){let n=e.name.toLowerCase().replace(rn,"").replace(/[_\-\s]+/g,"-").replace(/^-|-$/g,"").trim();if(n&&n.length>1)return`icon-${X(n)}`;if("children"in e&&e.children.length>0){let s=[];for(let o=0;o<e.children.length;o++)s.push(e.children[o].type);for(let o=0;o<s.length;o++){if(s[o]==="ELLIPSE")return"icon-circle";if(s[o]==="STAR")return"icon-star";if(s[o]==="POLYGON")return"icon-shape"}}if("width"in e&&"height"in e){let s=e.width/e.height;if(s>1.5||s<.67)return"icon-arrow"}return"icon"}function Xo(e){let n=(e.characters||"").trim();if(!n)return"text-empty";let s=n.split(/\s+/);if(s.length<=2&&n.length<=30){let g=X(n);return g?`text-${g}`:"text-content"}let o=s[0].toLowerCase(),r=["welcome","about","contact","services","features","pricing"],i=["name","email","password","username","address","phone"],a=["submit","cancel","save","delete","edit","add","remove","ok","yes","no"],c=["learn","read","view","see","click","here","more"],d=["error","invalid","required","failed","wrong"],l=["success","done","complete","saved","updated"],p=n.toLowerCase();for(let g=0;g<r.length;g++)if(o.indexOf(r[g])!==-1||p.indexOf(r[g])!==-1)return`text-heading-${X(s.slice(0,2).join(" "))}`;for(let g=0;g<i.length;g++)if(o.indexOf(i[g])!==-1||p.indexOf(i[g])!==-1)return`text-label-${X(s.slice(0,2).join(" "))}`;for(let g=0;g<a.length;g++)if(o.indexOf(a[g])!==-1||p.indexOf(a[g])!==-1)return`text-button-${X(s.slice(0,2).join(" "))}`;for(let g=0;g<c.length;g++)if(o.indexOf(c[g])!==-1||p.indexOf(c[g])!==-1)return`text-link-${X(s.slice(0,2).join(" "))}`;for(let g=0;g<d.length;g++)if(o.indexOf(d[g])!==-1||p.indexOf(d[g])!==-1)return`text-error-${X(s.slice(0,2).join(" "))}`;for(let g=0;g<l.length;g++)if(o.indexOf(l[g])!==-1||p.indexOf(l[g])!==-1)return`text-success-${X(s.slice(0,2).join(" "))}`;let u=X(s.slice(0,2).join(" "));return u?`text-${u}`:"text-content"}function Yo(e){let t=De(e),n=cn[t];if("children"in e&&e.children.length>0){let s;for(let o=0;o<e.children.length;o++)if(e.children[o].type==="TEXT"){s=e.children[o];break}if(s&&s.characters){let r=s.characters.trim().split(/\s+/).slice(0,2);if(r.length>0&&r[0].length>0)return`${n}-${X(r.join(" "))}`}if(t==="button"||t==="input"){let o;for(let r=0;r<e.children.length;r++){let i=e.children[r];if(i.type==="VECTOR"||i.name.toLowerCase().indexOf("icon")!==-1){o=i;break}}if(o){let r=o.name.toLowerCase().replace(/icon[-_\s]*/gi,"");if(r&&!ln(r))return`${n}-${X(r)}`}}}return n}function pt(e,t){if(!e||!t||typeof t!="string")return!1;let n=t.trim();if(n.length===0)return!1;try{return e.name=n,!0}catch(s){return console.error("Failed to rename layer:",s),!1}}function mn(e,t){return{nodeId:e.id,currentName:e.name,newName:t.trim(),layerType:De(e),willChange:e.name!==t.trim()}}function X(e){return e.replace(/([a-z])([A-Z])/g,"$1-$2").replace(/[\s_]+/g,"-").replace(/[^a-zA-Z0-9-]/g,"").replace(/-+/g,"-").replace(/^-|-$/g,"").toLowerCase()}be();async function Lt(e){let t=Ps(e),n=yi(t),s=Ze(e).join(" "),o={width:"width"in e?e.width:0,height:"height"in e?e.height:0,layoutMode:"layoutMode"in e&&e.layoutMode||"NONE"},r={hasFills:Ls(e),hasStrokes:Rs(e),hasEffects:$s(e),cornerRadius:"cornerRadius"in e&&e.cornerRadius||0},i=bi(e),{isComponentSet:a,potentialVariants:c}=hi(e),d=await fi(e);return{name:e.name,type:e.type,hierarchy:t,textContent:s||void 0,frameStructure:o,detectedStyles:r,detectedSlots:i,isComponentSet:a,potentialVariants:c,nestedLayers:n,additionalContext:d}}async function fi(e){let t={hasInteractiveElements:!1,possibleUseCase:"",designPatterns:[],componentFamily:"",suggestedConsiderations:[]},n=e.name.toLowerCase(),o=["tabs","tab-group","tabset","nav","navbar","navigation","menu","menubar","dropdown","form","form-group","fieldset","list","grid","collection","gallery","group","container","wrapper","layout","toolbar","panel","sidebar","header","footer","card-group","button-group","radio-group","checkbox-group"].some(a=>n.includes(a)),r=await gi(e),i=o||r;return console.log(`\u{1F50D} [CONTAINER DETECTION] ${e.name}:`),console.log(` Name-based: ${o}`),console.log(` Structure-based: ${r}`),console.log(` Final result: ${i}`),n.includes("avatar")||n.includes("profile")?(t.componentFamily="avatar",t.possibleUseCase="User representation, often clickable for profile access or dropdown menus",t.hasInteractiveElements=!0,t.suggestedConsiderations.push("Consider if this avatar will be clickable/interactive"),t.suggestedConsiderations.push("May need hover/focus states for navigation"),t.designPatterns.push("profile-navigation","user-menu-trigger")):n.includes("button")||n.includes("btn")?(t.componentFamily="button",t.possibleUseCase="Interactive element for user actions",t.hasInteractiveElements=!0,t.suggestedConsiderations.push("Requires all interactive states"),t.designPatterns.push("action-trigger","form-submission")):n.includes("badge")||n.includes("tag")?(t.componentFamily="badge",t.possibleUseCase="Status indicator or label",t.hasInteractiveElements=!1,t.suggestedConsiderations.push("Typically non-interactive unless used as a filter"),t.designPatterns.push("status-indicator","category-label")):n.includes("input")||n.includes("field")?(t.componentFamily="input",t.possibleUseCase="Form input element",t.hasInteractiveElements=!0,t.suggestedConsiderations.push("Needs focus, error, and disabled states"),t.designPatterns.push("form-control","data-entry")):n.includes("card")?(t.componentFamily="card",t.possibleUseCase="Content container",t.hasInteractiveElements=n.includes("clickable")||n.includes("interactive"),t.suggestedConsiderations.push("May be interactive if used for navigation"),t.designPatterns.push("content-container","information-display")):n.includes("icon")?(t.componentFamily="icon",t.possibleUseCase="Visual indicator or decoration",t.hasInteractiveElements=!1,t.suggestedConsiderations.push("Usually decorative, but may be interactive if part of a button"),t.designPatterns.push("visual-indicator","decoration")):i&&(t.componentFamily="container",t.possibleUseCase="Layout container for organizing child components",t.hasInteractiveElements=!1,t.suggestedConsiderations.push("Focus on layout and organization rather than interaction states"),t.suggestedConsiderations.push("Child components handle individual interactions"),t.designPatterns.push("layout-container","component-organization")),"children"in e&&e.findAll(c=>c.type==="TEXT"&&(c.name.toLowerCase().includes("click")||c.name.toLowerCase().includes("action")||c.name.toLowerCase().includes("link"))).length>0&&(t.hasInteractiveElements=!0),e.parent&&e.parent.name.toLowerCase().includes("button")&&(t.hasInteractiveElements=!0,t.suggestedConsiderations.push("Part of a button component - needs interactive states")),t}async function gi(e){if(!("children"in e)||!e.children||e.children.length===0)return!1;let t=e.children.filter(d=>d.type==="INSTANCE");if(t.length===0)return console.log(`\u{1F50D} [STRUCTURE] No child instances found in ${e.name}`),!1;console.log(`\u{1F50D} [STRUCTURE] Analyzing ${e.name} with ${t.length} child instances`);let n=new Map;await Promise.all(t.map(async d=>{try{let l=await d.getMainComponentAsync();if(l){let p=l.name;n.has(p)||n.set(p,[]),n.get(p).push(d)}}catch(l){console.log("\u26A0\uFE0F [STRUCTURE] Could not access main component for instance:",l)}})),console.log("\u{1F50D} [STRUCTURE] Instance groups:",Array.from(n.entries()).map(([d,l])=>`${d}: ${l.length}`));let s=Array.from(n.values()).some(d=>d.length>1),o=Array.from(n.keys()).some(d=>{let l=d.toLowerCase();return l.includes("item")||l.includes("panel")||l.includes("content")||l.includes("section")||l.includes("group")||l.includes("wrapper")||l.includes("tab")&&!l.includes("button")||l.includes("nav-item")||l.includes("menu-item")||l.includes("list-item")||l.includes("card-item")}),r=t.length/e.children.length,i=r>.6,a=n.size>=2&&s;return console.log(`\u{1F50D} [STRUCTURE] Analysis for ${e.name}:`),console.log(` Repeated components: ${s}`),console.log(` Organizational components: ${o}`),console.log(` Instance ratio: ${r.toFixed(2)} (${i?"high":"low"})`),console.log(` Collection pattern: ${a}`),s||o||i&&n.size>=2}function Ps(e,t=0){let n=[],s={name:e.name,type:e.type,depth:t};if("children"in e&&e.children.length>0){s.children=[];for(let o of e.children)s.children.push(...Ps(o,t+1))}return n.push(s),n}function yi(e){let t=[];function n(s){for(let o of s)t.push(o.name),o.children&&n(o.children)}return n(e),t}function et(e){let t=new Set;function n(s){for(let o of s)o.type==="INSTANCE"&&t.add(o.name),o.children&&n(o.children)}return n(e),Array.from(t)}function hi(e){let t=[],n=!1;if(e.type==="COMPONENT_SET"){n=!0;try{let s=e,o;try{o=s.variantGroupProperties}catch(r){console.warn("Component set has errors, cannot access variantGroupProperties:",r),o=void 0}o&&t.push(...Object.keys(o))}catch(s){console.warn("Error analyzing component set:",s)}}else{let s=Oe(e).map(r=>r.name.toLowerCase());["primary","secondary","tertiary","small","medium","large","xl","xs","default","hover","focus","active","disabled","filled","outline","ghost","link","light","dark"].forEach(r=>{s.some(i=>i.includes(r))&&(t.includes(r)||t.push(r))})}return{isComponentSet:n,potentialVariants:t}}function bi(e){let t=[],n=Oe(e),s=e.name.toLowerCase(),o=["radiobutton","checkbox","icon","button","input","focusring","focus","indicator","background","border","outline","shadow","ring","control","handle","thumb","track","progress","slider","arrow","chevron","close","minimize","maximize"];n.filter(c=>c.type==="TEXT").forEach(c=>{let d=c.name.toLowerCase();o.some(l=>d.includes(l))||s.includes(d)||d.includes(s.split(" ")[0])||(d.includes("title")||d.includes("label")||d.includes("text")||d.includes("content"))&&d.length>2&&t.push(c.name)}),n.filter(c=>c.type==="FRAME").forEach(c=>{let d=c.name.toLowerCase();o.some(l=>d.includes(l))||(d.includes("content")&&!d.includes("background")||d.includes("slot")||d.includes("container")&&!d.includes("main"))&&t.push(c.name)});let a=[...new Set(t)].filter(c=>{let d=c.toLowerCase();return d.length>2&&!["text","label","content"].includes(d)&&!o.some(l=>d.includes(l))});return console.log(`\u{1F50D} [SLOTS] Detected ${a.length} legitimate content slots from ${t.length} candidates:`,a),a}function Ls(e){return"fills"in e&&Array.isArray(e.fills)&&e.fills.length>0?e.fills.some(t=>t.visible!==!1):"children"in e?e.children.some(t=>Ls(t)):!1}function Rs(e){return"strokes"in e&&Array.isArray(e.strokes)&&e.strokes.length>0?e.strokes.some(t=>t.visible!==!1):"children"in e?e.children.some(t=>Rs(t)):!1}function $s(e){return"effects"in e&&Array.isArray(e.effects)&&e.effects.length>0?e.effects.some(t=>t.visible!==!1):"children"in e?e.children.some(t=>$s(t)):!1}function vi(e){let t=new Map;e.children.forEach(s=>{s.type==="COMPONENT"&&s.name.split(",").map(i=>i.trim()).forEach(i=>{let[a,c]=i.split("=").map(d=>d.trim());a&&c&&(t.has(a)||t.set(a,new Set),t.get(a).add(c))})});let n=[];return t.forEach((s,o)=>{let r=Array.from(s);n.push({name:o,values:r,default:r[0]||"default"})}),n}async function Ms(e,t){let n=[];if(console.log("\u{1F50D} [DEBUG] Starting property extraction for node:",e.name,"type:",e.type),console.log("\u{1F50D} [DEBUG] Originally selected node:",t==null?void 0:t.name,"type:",t==null?void 0:t.type),t&&t.type==="INSTANCE"){let o=t;console.log("\u{1F50D} [DEBUG] Extracting from selected instance componentProperties...");try{if("componentProperties"in o&&o.componentProperties){let r=o.componentProperties;console.log("\u{1F50D} [DEBUG] Found componentProperties on selected instance:",Object.keys(r));let i=await o.getMainComponentAsync();if(i&&i.parent&&i.parent.type==="COMPONENT_SET"){let a=i.parent,c=null;try{"componentPropertyDefinitions"in a&&(c=a.componentPropertyDefinitions,console.log("\u{1F50D} [DEBUG] Got componentPropertyDefinitions from component set"))}catch(d){console.log("\u{1F50D} [DEBUG] Could not access componentPropertyDefinitions, using instance properties only")}for(let d in r){let l=r[d];console.log(`\u{1F50D} [DEBUG] Processing instance property "${d}":`,l);let p=d,u=[],g="";if(d.includes("#")&&(p=d.split("#")[0]),l&&typeof l=="object"&&"value"in l?g=String(l.value):g=String(l),c&&c[d]){let f=c[d];switch(console.log(`\u{1F50D} [DEBUG] Found property definition for "${d}":`,f),f.type){case"VARIANT":u=f.variantOptions||[];break;case"BOOLEAN":u=["true","false"];break;case"TEXT":u=[g||"Text content"];break;case"INSTANCE_SWAP":f.preferredValues&&Array.isArray(f.preferredValues)?u=f.preferredValues.map(m=>m.key||m.name||"Component instance"):u=["Component instance"];break;default:u=[g||"Property value"]}}else console.log(`\u{1F50D} [DEBUG] No property definition for "${d}", inferring from value`),g==="true"||g==="false"?u=["true","false"]:u=[g||"Property value"];n.push({name:p,values:u,default:g||u[0]||"default"}),console.log("\u{1F50D} [DEBUG] Added instance property:",{name:p,values:u,default:g})}if(n.length>0)return console.log(`\u{1F50D} [DEBUG] Successfully extracted ${n.length} properties from selected instance`),n}}}catch(r){console.log("\u{1F50D} [DEBUG] Could not extract from instance componentProperties:",r)}}if(e.type==="COMPONENT_SET"){let o=e;console.log("\u{1F50D} [DEBUG] Attempting to access componentPropertyDefinitions...");try{if("componentPropertyDefinitions"in o){console.log("\u{1F50D} [DEBUG] componentPropertyDefinitions property exists on componentSet");let r=o.componentPropertyDefinitions;if(console.log("\u{1F50D} [DEBUG] Raw componentPropertyDefinitions:",r),console.log("\u{1F50D} [DEBUG] Type of componentPropertyDefinitions:",typeof r),r&&typeof r=="object"){let i=Object.keys(r);console.log("\u{1F50D} [DEBUG] Found componentPropertyDefinitions with keys:",i);for(let a in r){let c=r[a];console.log(`\u{1F50D} [DEBUG] Processing property "${a}":`,c);let d=a,l=[],p="";switch(a.includes("#")&&(d=a.split("#")[0],console.log(`\u{1F50D} [DEBUG] Cleaned display name: "${d}" from "${a}"`)),c.type){case"VARIANT":l=c.variantOptions||[],p=String(c.defaultValue)||l[0]||"default",console.log(`\u{1F50D} [DEBUG] VARIANT property "${d}": values=${l}, default=${p}`);break;case"BOOLEAN":l=["true","false"],p=c.defaultValue?"true":"false",console.log(`\u{1F50D} [DEBUG] BOOLEAN property "${d}": default=${p}`);break;case"TEXT":l=[String(c.defaultValue||"Text content")],p=String(c.defaultValue||"Text content"),console.log(`\u{1F50D} [DEBUG] TEXT property "${d}": value=${p}`);break;case"INSTANCE_SWAP":c.preferredValues&&Array.isArray(c.preferredValues)?l=c.preferredValues.map(u=>(console.log("\u{1F50D} [DEBUG] INSTANCE_SWAP preferred value:",u),u.key||u.name||"Component instance")):l=["Component instance"],p=l[0]||"Component instance",console.log(`\u{1F50D} [DEBUG] INSTANCE_SWAP property "${d}": values=${l}, default=${p}`);break;default:console.log(`\u{1F50D} [DEBUG] Unknown property type "${c.type}" for "${d}"`),l=["Property value"],p="Default"}n.push({name:d,values:l,default:p}),console.log("\u{1F50D} [DEBUG] Added property:",{name:d,values:l,default:p})}}else console.log("\u{1F50D} [DEBUG] componentPropertyDefinitions is not a valid object:",r)}else console.log("\u{1F50D} [DEBUG] componentPropertyDefinitions property does not exist on componentSet")}catch(r){console.error("\u{1F50D} [ERROR] Could not access componentPropertyDefinitions:",r),console.error("\u{1F50D} [ERROR] Error stack:",r instanceof Error?r.stack:"No stack trace")}if(n.length===0){console.log("\u{1F50D} [DEBUG] No properties found, trying variantGroupProperties fallback...");try{let r=o.variantGroupProperties;if(console.log("\u{1F50D} [DEBUG] variantGroupProperties:",r),r){let i=Object.keys(r);console.log("\u{1F50D} [DEBUG] Found variantGroupProperties with keys:",i);for(let a in r){let c=r[a];console.log(`\u{1F50D} [DEBUG] Processing variant property "${a}":`,c),n.push({name:a,values:c.values,default:c.values[0]||"default"})}}else console.log("\u{1F50D} [DEBUG] variantGroupProperties is null/undefined")}catch(r){console.warn("\u{1F50D} [WARN] Component set has errors, cannot access variantGroupProperties:",r)}}if(n.length===0&&o.children.length>0){console.log("\u{1F50D} [DEBUG] Analyzing variant structure to infer properties...");let r=new Map,i=new Map;o.children.forEach((a,c)=>{if(a.type==="COMPONENT"){let d=a.name;console.log(`\u{1F50D} [DEBUG] Analyzing variant ${c}: ${d}`),d.split(",").map(u=>u.trim()).forEach(u=>{let[g,f]=u.split("=").map(m=>m.trim());g&&f&&(r.has(g)||r.set(g,new Set),r.get(g).add(f))});let p=(u,g="")=>{let f=g?`${g}/${u.name}`:u.name;i.has(f)||i.set(f,[]),i.get(f).push(u.visible),"children"in u&&u.children.forEach(m=>p(m,f))};p(a)}}),r.forEach((a,c)=>{n.find(d=>d.name===c)||n.push({name:c,values:Array.from(a),default:Array.from(a)[0]||"default"})}),i.forEach((a,c)=>{let d=a.includes(!0),l=a.includes(!1);if(d&&l){let u=(c.split("/").pop()||"").replace(/\s*(layer|group|frame|icon|text)?\s*/gi,"").trim();u&&!n.find(g=>g.name===u)&&(n.push({name:u,values:["true","false"],default:"false"}),console.log(`\u{1F50D} [DEBUG] Inferred boolean property from visibility: ${u}`))}}),console.log(`\u{1F50D} [DEBUG] Inferred ${n.length} properties from variant analysis`)}if(n.length===0){console.log("\u{1F50D} [DEBUG] All Figma APIs failed, using comprehensive structural analysis...");let r=Si(o);console.log("\u{1F50D} [DEBUG] Properties from structural analysis:",r),n.push(...r)}}else if(e.type==="COMPONENT"){let o=e;console.log("\u{1F50D} [DEBUG] Processing COMPONENT node:",o.name);try{if("componentPropertyDefinitions"in o){let r=o.componentPropertyDefinitions;if(console.log("\u{1F50D} [DEBUG] Component componentPropertyDefinitions:",r),r&&typeof r=="object"){let i=Object.keys(r);console.log("\u{1F50D} [DEBUG] Found componentPropertyDefinitions on component with keys:",i);for(let a in r){let c=r[a],d=a,l=[],p="";switch(a.includes("#")&&(d=a.split("#")[0]),c.type){case"BOOLEAN":l=["true","false"],p=c.defaultValue?"true":"false";break;case"TEXT":l=[String(c.defaultValue||"Text content")],p=String(c.defaultValue||"Text content");break;case"INSTANCE_SWAP":c.preferredValues&&Array.isArray(c.preferredValues)?l=c.preferredValues.map(u=>u.key||u.name||"Component instance"):l=["Component instance"],p=l[0]||"Component instance";break;default:l=["Property value"],p="Default"}n.push({name:d,values:l,default:p})}}}else console.log("\u{1F50D} [DEBUG] componentPropertyDefinitions does not exist on component")}catch(r){console.warn("\u{1F50D} [WARN] Could not access componentPropertyDefinitions on component:",r)}if(o.parent&&o.parent.type==="COMPONENT_SET"){let r=o.parent;console.log("\u{1F50D} [DEBUG] Component is part of a component set, getting variant properties...");try{let i=r.variantGroupProperties;if(i)for(let a in i){let c=i[a];n.find(d=>d.name===a)||n.push({name:a,values:c.values,default:c.values[0]||"default"})}}catch(i){console.warn("\u{1F50D} [WARN] Component set has errors, cannot access variantGroupProperties:",i)}}}else if(e.type==="INSTANCE"){let o=e;if(console.log("\u{1F50D} [DEBUG] Processing INSTANCE node (fallback \u2014 Priority 1 may have been skipped)"),n.length===0)try{let r=await o.getMainComponentAsync();if(r)if(r.parent&&r.parent.type==="COMPONENT_SET"){let i=r.parent;console.log("\u{1F50D} [DEBUG] Instance fallback: extracting from parent component set:",i.name);try{if("componentPropertyDefinitions"in i){let a=i.componentPropertyDefinitions;if(a&&typeof a=="object"){for(let c in a){let d=a[c],l=c,p=[],u="";switch(c.includes("#")&&(l=c.split("#")[0]),d.type){case"VARIANT":p=d.variantOptions||[],u=String(d.defaultValue)||p[0]||"default";break;case"BOOLEAN":p=["true","false"],u=d.defaultValue?"true":"false";break;case"TEXT":p=[String(d.defaultValue||"Text content")],u=String(d.defaultValue||"Text content");break;case"INSTANCE_SWAP":d.preferredValues&&Array.isArray(d.preferredValues)?p=d.preferredValues.map(g=>g.key||g.name||"Component instance"):p=["Component instance"],u=p[0]||"Component instance";break;default:p=["Property value"],u="Default"}n.push({name:l,values:p,default:u})}console.log(`\u{1F50D} [DEBUG] Instance fallback: extracted ${n.length} properties from component set`)}}}catch(a){console.warn("\u{1F50D} [WARN] Instance fallback: could not access componentPropertyDefinitions:",a)}if(n.length===0)try{let a=i.variantGroupProperties;if(a)for(let c in a){let d=a[c];n.find(l=>l.name===c)||n.push({name:c,values:d.values,default:d.values[0]||"default"})}}catch(a){console.warn("\u{1F50D} [WARN] Instance fallback: could not access variantGroupProperties:",a)}}else{console.log("\u{1F50D} [DEBUG] Instance fallback: extracting from standalone main component");try{if("componentPropertyDefinitions"in r){let i=r.componentPropertyDefinitions;if(i&&typeof i=="object"){for(let a in i){let c=i[a],d=a,l=[],p="";switch(a.includes("#")&&(d=a.split("#")[0]),c.type){case"BOOLEAN":l=["true","false"],p=c.defaultValue?"true":"false";break;case"TEXT":l=[String(c.defaultValue||"Text content")],p=String(c.defaultValue||"Text content");break;case"INSTANCE_SWAP":c.preferredValues&&Array.isArray(c.preferredValues)?l=c.preferredValues.map(u=>u.key||u.name||"Component instance"):l=["Component instance"],p=l[0]||"Component instance";break;default:l=["Property value"],p="Default"}n.push({name:d,values:l,default:p})}console.log(`\u{1F50D} [DEBUG] Instance fallback: extracted ${n.length} properties from main component`)}}}catch(i){console.warn("\u{1F50D} [WARN] Instance fallback: could not access componentPropertyDefinitions on main component:",i)}}}catch(r){console.warn("\u{1F50D} [WARN] Instance fallback: could not get main component:",r)}}let s=[];return n.forEach(o=>{s.find(r=>r.name===o.name)||s.push(o)}),console.log(`\u{1F50D} [DEBUG] Final result: Extracted ${s.length} unique properties:`,s.map(o=>({name:o.name,valueCount:o.values.length,default:o.default}))),s}function Si(e){let t=[];console.log("\u{1F50D} [STRUCTURAL] Starting comprehensive structural analysis of component set:",e.name);let n=vi(e);t.push(...n);let s=new Set,o=new Set,r=new Set,i=new Set;e.children.forEach(d=>{if(d.type==="COMPONENT"){console.log(`\u{1F50D} [STRUCTURAL] Analyzing variant: ${d.name}`);let l=(p,u=0)=>{let g=" ".repeat(u);console.log(`\u{1F50D} [STRUCTURAL] ${g}Found child: ${p.name} (type: ${p.type})`),s.add(p.name),p.type==="TEXT"?o.add(p.name):p.type==="INSTANCE"&&r.add(p.name),(p.visible===!1||p.name.toLowerCase().includes("hidden"))&&i.add(p.name),"children"in p&&p.children&&p.children.forEach(f=>l(f,u+1))};l(d)}}),console.log("\u{1F50D} [STRUCTURAL] Analysis results:"),console.log("\u{1F50D} [STRUCTURAL] - All child names:",Array.from(s)),console.log("\u{1F50D} [STRUCTURAL] - Text layers:",Array.from(o)),console.log("\u{1F50D} [STRUCTURAL] - Instance layers:",Array.from(r)),console.log("\u{1F50D} [STRUCTURAL] - Boolean indicators:",Array.from(i)),o.forEach(d=>{let l=d.replace(/\s*(layer|text|label)?\s*/gi,"").trim();l&&!t.find(p=>p.name.toLowerCase()===l.toLowerCase())&&(t.push({name:l,values:["Text content"],default:"Label"}),console.log(`\u{1F50D} [STRUCTURAL] Added TEXT property: ${l}`))}),r.forEach(d=>{let l=d.replace(/\s*(layer|instance)?\s*/gi,"").trim();l&&!t.find(p=>p.name.toLowerCase()===l.toLowerCase())&&(t.push({name:l,values:["Component instance"],default:"Default component"}),console.log(`\u{1F50D} [STRUCTURAL] Added INSTANCE_SWAP property: ${l}`))}),["icon before","icon after","slot before","slot after","before","after","prefix","suffix","leading","trailing"].forEach(d=>{if(Array.from(s).find(p=>p.toLowerCase().includes(d.toLowerCase()))&&!t.find(p=>p.name.toLowerCase().includes(d.toLowerCase()))){let p=d.split(" ").map(u=>u.charAt(0).toUpperCase()+u.slice(1)).join(" ");t.push({name:p,values:["true","false"],default:"false"}),console.log(`\u{1F50D} [STRUCTURAL] Added BOOLEAN property: ${p}`)}});let c=e.name.toLowerCase();return(c.includes("button")||c.includes("btn"))&&[{name:"Slot Before",type:"BOOLEAN"},{name:"Text",type:"TEXT"},{name:"Icon Before",type:"INSTANCE_SWAP"},{name:"Icon After",type:"INSTANCE_SWAP"}].forEach(({name:l,type:p})=>{if(!t.find(u=>u.name.toLowerCase()===l.toLowerCase())){let u,g;switch(p){case"BOOLEAN":u=["true","false"],g="false";break;case"TEXT":u=["Text content"],g="Label";break;case"INSTANCE_SWAP":u=["Component instance"],g="Default icon";break;default:u=["Property value"],g="Default"}t.push({name:l,values:u,default:g}),console.log(`\u{1F50D} [STRUCTURAL] Added common ${p} property: ${l}`)}}),console.log(`\u{1F50D} [STRUCTURAL] Final structural analysis result: ${t.length} properties found`),t}async function Ge(e){let t=[];if(e.type==="COMPONENT_SET"){let s=e,o;try{o=s.variantGroupProperties}catch(r){console.warn("Component set has errors, cannot access variantGroupProperties:",r),o=void 0}if(o)for(let r in o){let i=r.toLowerCase();(i==="state"||i==="states"||i==="status")&&t.push(...o[r].values)}s.children.forEach(r=>{let i=r.name.toLowerCase();["default","hover","focus","disabled","pressed","active","selected"].forEach(a=>{let c=t.find(d=>d.toLowerCase()===a.toLowerCase());i.includes(a)&&!c&&t.push(a)})})}else if(e.type==="COMPONENT"){let s=e;if(s.parent&&s.parent.type==="COMPONENT_SET")return await Ge(s.parent)}else if(e.type==="INSTANCE"){let o=await e.getMainComponentAsync();if(o)return await Ge(o)}let n=[];return t.forEach(s=>{s&&typeof s=="string"&&s.trim()!==""&&(n.find(r=>r.toLowerCase()===s.toLowerCase())||n.push(s.trim()))}),n}async function Os(e,t,n,s={},o="anthropic"){console.log("\u{1F3AF} Starting enhanced component analysis...");let r=figma.currentPage.selection[0],i=s.node||r;if(!i)throw new Error("No node selected");let a=await Ms(i,r),c=await Ge(i),d=await ue(i),l="";if(i.type==="COMPONENT"||i.type==="COMPONENT_SET")l=i.description||"";else if(i.type==="INSTANCE"){let C=await i.getMainComponentAsync();C&&(l=C.description||"")}e.existingDescription=l;let p=ae([i],s.lintSettings||j);console.log(`\u{1F50D} [LINT] Deterministic lint: ${p.summary.totalErrors} issues in ${p.summary.nodesWithErrors} nodes`),console.log("\u{1F4CA} [ANALYSIS] Extracted from Figma API:"),console.log(` Properties: ${a.length}`),console.log(` States: ${c.length}`),console.log(` Tokens: ${Object.keys(d).length} categories`),console.log(` Description: ${l?"Present":"Missing"}`);let u=s.mcpServerUrl||"http://localhost:3000/mcp",g=s.useMCP!==!1&&u,f;if(g){console.log(`\u{1F504} Using hybrid LLM + MCP approach (${o})...`);let h=ki(e,a,c,d,l,p),C=await ie(o,t,{prompt:h,model:n,maxTokens:2048,temperature:.1}),S=pe(C.content);if(!S)throw new Error("Failed to extract JSON from LLM response");let N=null;try{N=await Ni(e,u,S),console.log("\u2705 MCP enhancements received")}catch(y){console.warn("\u26A0\uFE0F MCP enhancement failed, continuing with LLM data only:",y)}f=wi(S,N,{node:i,context:e,actualProperties:a,actualStates:c,tokens:d,componentDescription:l})}else{console.log(`\u{1F4DD} Using ${o}-only analysis...`);let h=nn(e),C=await ie(o,t,{prompt:h,model:n,maxTokens:2048,temperature:.1});if(f=pe(C.content),!f)throw new Error("Failed to extract JSON from response")}let m=Fe(f);return await Rt(m,e,s,p,t,n,o)}function ki(e,t,n,s,o,r){var d;let i=((d=e.additionalContext)==null?void 0:d.componentFamily)||"generic",a=et(e.hierarchy),c="";if(r&&r.summary.totalErrors>0){let l=r.summary.byType,p=r.errors.slice(0,15).map(u=>` - [${u.errorType.toUpperCase()}] ${u.nodeName}: ${u.message}`).join(` `);c=` **Design Lint Findings (${r.summary.totalErrors} issues):** -- Missing fill styles: ${d.fill||0} -- Missing stroke styles: ${d.stroke||0} -- Missing effect styles: ${d.effect||0} -- Missing text styles: ${d.text||0} -- Non-standard border radius: ${d.radius||0} -- Off-grid spacing: ${d.spacing||0} -- Missing auto-layout: ${d.autoLayout||0} +- Missing fill styles: ${l.fill||0} +- Missing stroke styles: ${l.stroke||0} +- Missing effect styles: ${l.effect||0} +- Missing text styles: ${l.text||0} +- Non-standard border radius: ${l.radius||0} +- Off-grid spacing: ${l.spacing||0} +- Missing auto-layout: ${l.autoLayout||0} Top issues: ${p} `}else c=` @@ -232,7 +232,7 @@ ${p} - Nested Component Instances: ${a.length>0?a.join(", "):"None detected"} **Actual Figma Properties (${t.length} total):** -${t.slice(0,10).map(d=>`- ${d.name}: ${d.values.join(", ")} (default: ${d.default})`).join(` +${t.slice(0,10).map(l=>`- ${l.name}: ${l.values.join(", ")} (default: ${l.default})`).join(` `)} ${t.length>10?`... and ${t.length-10} more properties`:""} @@ -294,19 +294,19 @@ Return JSON in this exact format: For "recommendedProperties": Compare the EXISTING properties listed above against design system best practices (Material Design, Carbon, Ant Design, Polaris, etc.). Only recommend Figma component properties that do NOT already exist. Use Figma property types (VARIANT, BOOLEAN, TEXT, INSTANCE_SWAP). If the component already has comprehensive properties, return an empty array. -Focus ONLY on what's actually in the Figma component for existing data. Recommendations should draw from your knowledge of design system best practices.`}async function Ir(e,t,n){var o,r;let s=((o=e.additionalContext)==null?void 0:o.componentFamily)||((r=n.component)==null?void 0:r.toLowerCase())||"generic";try{let[i,a,c]=await Promise.all([St(t,"search_design_knowledge",{query:`${s} component essential properties states variants`,category:"components",limit:2},3e3),St(t,"search_design_knowledge",{query:`design tokens ${s} semantic naming`,category:"tokens",limit:2},3e3),St(t,"search_chunks",{query:`component assessment scoring criteria ${s}`,limit:1},3e3)]);return{bestPractices:(i==null?void 0:i.entries)||[],tokenGuidance:(a==null?void 0:a.entries)||[],scoringCriteria:(c==null?void 0:c.chunks)||[],success:!0}}catch(i){return console.warn("\u26A0\uFE0F MCP queries failed:",i),{bestPractices:[],tokenGuidance:[],scoringCriteria:[],success:!1,error:i instanceof Error?i.message:"Unknown error"}}}async function St(e,t,n,s=5e3){var i,a;let o=new AbortController,r=setTimeout(()=>o.abort(),s);try{let c={jsonrpc:"2.0",id:Math.floor(Math.random()*1e3)+100,method:"tools/call",params:{name:`mcp_design-systems_${t}`,arguments:n}},l=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c),signal:o.signal});if(clearTimeout(r),!l.ok)throw new Error(`MCP ${t} failed: ${l.status}`);return((a=(i=(await l.json()).result)==null?void 0:i.content)==null?void 0:a[0])||{}}catch(c){throw clearTimeout(r),c instanceof Error&&c.name==="AbortError"?new Error(`MCP ${t} timeout after ${s}ms`):c}}function Ar(e,t,n){var o;let s=R({},e);return s.propertyCheatSheet=Tr(n.actualProperties,e.component||n.context.name),s.audit={designIssues:[],tokenOpportunities:[],structureIssues:[]},(!n.componentDescription||n.componentDescription.trim().length===0)&&s.audit.structureIssues.push("Component lacks description - Add a description in component properties to help MCP and AI understand the component's purpose and usage"),t!=null&&t.success?s.mcpReadiness=Er(t,e,n):s.mcpReadiness=as(n),s.component=s.component||n.context.name,s.description=s.description||`${((o=n.context.additionalContext)==null?void 0:o.componentFamily)||"Component"} with ${n.actualProperties.length} properties`,s.props=s.props||n.actualProperties.map(r=>({name:r.name,type:"select",description:`Controls ${r.name}`,values:r.values,default:r.default})),s.states=s.states||n.actualStates,s.recommendedProperties=e.recommendedProperties||[],s}function Er(e,t,n){var d,p;let s=[],o=[],r=[];((d=e.bestPractices)==null?void 0:d.length)>0&&e.bestPractices.forEach(u=>{var f,y;((f=u.title)!=null&&f.includes("best practice")||(y=u.title)!=null&&y.includes("pattern"))&&r.push(`Follow ${u.title}`)});let i=n.actualStates.length>=3,a=n.tokens.summary&&n.tokens.summary.actualTokens>n.tokens.summary.hardCodedValues,c=((p=t.structure)==null?void 0:p.complexity)!=="high";return i?s.push("Component has comprehensive states"):o.push("Missing interactive states"),a?s.push("Good token usage"):o.push("Improve token adoption"),c?s.push("Well-structured component"):o.push("Complex structure may need simplification"),{score:Math.round((i?35:15)+(a?35:15)+(c?30:20)),strengths:s,gaps:o,recommendations:r.slice(0,3)}}function Tr(e,t){let n=[],s=e.filter(c=>c.name.toLowerCase().includes("size")||c.values.some(l=>["small","medium","large"].includes(l.toLowerCase()))),o=e.filter(c=>c.name.toLowerCase().includes("variant")||c.name.toLowerCase().includes("type")),r=e.filter(c=>c.name.toLowerCase().includes("state")||c.values.some(l=>["hover","active","disabled"].includes(l.toLowerCase())));s.length>0&&n.push(`\u{1F4CF} Sizes: ${s.map(c=>c.values.join("/")).join(", ")}`),o.length>0&&n.push(`\u{1F3A8} Variants: ${o.map(c=>`${c.name}(${c.values.length})`).join(", ")}`),r.length>0&&n.push(`\u{1F504} States: ${r.map(c=>c.values.join("/")).join(", ")}`);let i=new Set([...s,...o,...r].map(c=>c.name)),a=e.filter(c=>!i.has(c.name)).slice(0,3).map(c=>`${c.name}: ${c.values.slice(0,3).join("/")}`);return a.length>0&&n.push(`\u2699\uFE0F Other: ${a.join(", ")}`),n.slice(0,5)}async function kt(e,t,n,s,o,r,i){var a;try{console.log("\u{1F504} Processing analysis result..."),console.log("\u{1F4CA} Filtered data received:",JSON.stringify(e,null,2).substring(0,500)+"...");let c=figma.currentPage.selection,l=null;if(c.length>0)l=c[0];else throw new Error("No component selected");let d=await rs(l,l),p=await De(l),u="";if(l.type==="COMPONENT"||l.type==="COMPONENT_SET")u=l.description||"";else if(l.type==="INSTANCE"){let g=await l.getMainComponentAsync();g&&(u=g.description||"")}let f={colors:[],spacing:[],typography:[],effects:[],borders:[],summary:{totalTokens:0,actualTokens:0,hardCodedValues:0,aiSuggestions:0,byCategory:{}}};n.includeTokenAnalysis!==!1&&(f=await ce(l));let y={component:e.component||t.name||"Component",description:e.description||`A ${t.type} component with ${d.length} properties`,props:e.props&&e.props.length>0?e.props:d.map(k=>({name:k.name,type:"select",description:`Controls ${k.name}`,values:k.values,defaultValue:k.default,required:!1})),states:e.states&&e.states.length>0?e.states.map(k=>typeof k=="string"?k:k.name):p.length>0?p:["default"],variants:e.variants||{},slots:e.slots||[],tokens:e.tokens||{colors:f.colors.filter(k=>k.isActualToken).map(k=>k.name),spacing:f.spacing.filter(k=>k.isActualToken).map(k=>k.name),typography:f.typography.filter(k=>k.isActualToken).map(k=>k.name)},usage:e.usage||"General purpose component for design systems",accessibility:e.accessibility||{keyboardNavigation:"Standard keyboard navigation support",screenReader:"Screen reader accessible",colorContrast:"WCAG compliant contrast ratios"},audit:e.audit||{accessibilityIssues:[],namingIssues:[],consistencyIssues:[],tokenOpportunities:[]},propertyCheatSheet:e.propertyCheatSheet||d.map(k=>({name:k.name,values:k.values,default:k.default,description:`Property for ${k.name} configuration`})),mcpReadiness:e.mcpReadiness||as({node:l,context:t,actualProperties:d,actualStates:p,tokens:f,componentDescription:u})};console.log("\u{1F4E4} Sending to UI - metadata.props:",(a=y.props)==null?void 0:a.length),console.log("\u{1F4E4} Sending to UI - metadata.states:",y.states),console.log("\u{1F4E4} Sending to UI - metadata.mcpReadiness:",y.mcpReadiness);let m=await Rr(e,t,l,d,p,f,u),v=(e.recommendedProperties||[]).map(k=>({name:k.name||"",type:k.type||"VARIANT",description:k.description||"",examples:k.examples||[]})).filter(k=>k.name);console.log(`\u{1F4A1} AI-generated property recommendations: ${v.length}`);let A=tn(l,5);console.log(`\u{1F4DB} Found ${A.length} naming issues`),s&&s.errors.length>0&&(m.designLint=Pr(s));let w;if(o&&r&&i)try{w=await Lr(t,s,m,f,A,v,o,r,i),console.log(`\u{1F4CB} Design review generated: ${w.verdict} \u2014 ${w.findings.length} findings`)}catch(k){console.warn("\u26A0\uFE0F Design review generation failed, continuing without it:",k),w=Nt(s,m,f,A)}else w=Nt(s,m,f,A);return console.log("\u2705 Analysis result processed successfully"),{metadata:y,tokens:f,audit:m,properties:d,recommendations:v,namingIssues:A,existingDescription:u,lintResult:s,designReview:w}}catch(c){throw console.error("Error processing analysis result:",c),c}}function Pr(e){let t=[],n=e.summary.byType;return n.fill>0?t.push({check:`Fill styles (${n.fill} missing)`,status:"fail",suggestion:`${n.fill} layer${n.fill>1?"s use":" uses"} hard-coded fills instead of design styles`}):t.push({check:"Fill styles",status:"pass",suggestion:"All fills use design styles"}),n.stroke>0?t.push({check:`Stroke styles (${n.stroke} missing)`,status:"fail",suggestion:`${n.stroke} layer${n.stroke>1?"s use":" uses"} hard-coded strokes instead of design styles`}):t.push({check:"Stroke styles",status:"pass",suggestion:"All strokes use design styles"}),n.effect>0?t.push({check:`Effect styles (${n.effect} missing)`,status:"fail",suggestion:`${n.effect} layer${n.effect>1?"s use":" uses"} hard-coded effects instead of design styles`}):t.push({check:"Effect styles",status:"pass",suggestion:"All effects use design styles"}),n.text>0?t.push({check:`Text styles (${n.text} missing)`,status:"fail",suggestion:`${n.text} text layer${n.text>1?"s lack":" lacks"} applied text styles`}):t.push({check:"Text styles",status:"pass",suggestion:"All text uses design styles"}),n.radius>0?t.push({check:`Border radius (${n.radius} non-standard)`,status:"warning",suggestion:`${n.radius} layer${n.radius>1?"s use":" uses"} non-standard border radius values`}):t.push({check:"Border radius",status:"pass",suggestion:"All radii match design system standards"}),n.spacing>0?t.push({check:`Spacing rhythm (${n.spacing} off-grid)`,status:"warning",suggestion:`${n.spacing} spacing value${n.spacing>1?"s are":" is"} not on the 4/8px grid`}):t.push({check:"Spacing rhythm",status:"pass",suggestion:"All spacing values follow the design grid"}),n.autoLayout>0?t.push({check:`Auto Layout (${n.autoLayout} missing)`,status:"warning",suggestion:`${n.autoLayout} frame${n.autoLayout>1?"s lack":" lacks"} auto-layout`}):t.push({check:"Auto Layout",status:"pass",suggestion:"All container frames use auto-layout"}),t}function Nt(e,t,n,s){let o=[];if(e)for(let u of e.errors)o.push({severity:u.errorType==="radius"||u.errorType==="spacing"||u.errorType==="autoLayout"?"warning":"critical",category:"Style Consistency",title:u.message,description:`Layer "${u.nodeName}" (${u.nodeType}) at ${u.path}`,nodeId:u.nodeId,nodeName:u.nodeName,autoFixable:!1});let r=n.summary.hardCodedValues;r>0&&o.push({severity:"warning",category:"Design Tokens",title:`${r} hard-coded value${r>1?"s":""} found`,description:"These values should be replaced with design tokens for consistency across the design system.",autoFixable:!0});for(let u of t.accessibility||[])u.status==="fail"?o.push({severity:"critical",category:"Accessibility",title:u.check,description:u.suggestion,autoFixable:!1}):u.status==="warning"&&o.push({severity:"warning",category:"Accessibility",title:u.check,description:u.suggestion,autoFixable:!1});for(let u of s.slice(0,10))o.push({severity:u.severity==="error"?"warning":"info",category:"Naming",title:`"${u.currentName}" should be "${u.suggestedName}"`,description:u.reason,nodeId:u.nodeId,nodeName:u.currentName,autoFixable:!0});for(let u of t.componentReadiness||[])u.status==="fail"&&o.push({severity:"suggestion",category:"Component Readiness",title:u.check,description:u.suggestion,autoFixable:!1});let i=(t.states||[]).filter(u=>!u.found);i.length>0&&o.push({severity:"suggestion",category:"Interactive States",title:`${i.length} state${i.length>1?"s":""} not detected`,description:`Missing: ${i.map(u=>u.name).join(", ")}`,autoFixable:!1});let a=o.filter(u=>u.severity==="critical").length,c=o.filter(u=>u.severity==="warning").length,l=a>0?"fail":c>3?"warn":"pass",d;l==="pass"?d="Component follows design system conventions well.":l==="warn"?d=`${c} issues need attention before this component is production-ready.`:d=`${a} critical issue${a>1?"s":""} found \u2014 missing design styles affect consistency.`;let p=[];return e&&e.summary.byType.fill>0&&p.push("Apply fill styles to layers using hard-coded colors"),e&&e.summary.byType.text>0&&p.push("Apply text styles to text layers"),e&&e.summary.byType.stroke>0&&p.push("Apply stroke styles to layers with hard-coded strokes"),e&&e.summary.byType.spacing>0&&p.push("Fix off-grid spacing values to match the 4/8px grid"),e&&e.summary.byType.autoLayout>0&&p.push("Apply auto-layout to container frames"),r>0&&p.push("Replace hard-coded values with design tokens"),s.length>0&&p.push("Rename generic layers to semantic names"),i.length>0&&p.push(`Add missing states: ${i.map(u=>u.name).join(", ")}`),p.length===0&&p.push("Component looks great \u2014 consider documenting it for the team"),{verdict:l,headline:d,findings:o,nextSteps:p}}async function Lr(e,t,n,s,o,r,i,a,c){var A;let l=t?`${t.summary.totalErrors} lint issues (${t.summary.byType.fill} fills, ${t.summary.byType.stroke} strokes, ${t.summary.byType.effect} effects, ${t.summary.byType.text} text, ${t.summary.byType.radius} radius, ${t.summary.byType.spacing||0} spacing, ${t.summary.byType.autoLayout||0} auto-layout)`:"0 lint issues",d=(n.accessibility||[]).filter(w=>w.status==="fail").length,p=(n.componentReadiness||[]).filter(w=>w.status==="fail").length,u=(n.states||[]).filter(w=>!w.found),f=t?t.errors.slice(0,8).map(w=>`- [${w.errorType}] ${w.nodeName}: ${w.message}`).join(` -`):"None",y=`You are a design system reviewer (like CodeRabbit but for Figma designs). Review this component and produce a structured JSON design review. +Focus ONLY on what's actually in the Figma component for existing data. Recommendations should draw from your knowledge of design system best practices.`}async function Ni(e,t,n){var o,r;let s=((o=e.additionalContext)==null?void 0:o.componentFamily)||((r=n.component)==null?void 0:r.toLowerCase())||"generic";try{let[i,a,c]=await Promise.all([Tt(t,"search_design_knowledge",{query:`${s} component essential properties states variants`,category:"components",limit:2},3e3),Tt(t,"search_design_knowledge",{query:`design tokens ${s} semantic naming`,category:"tokens",limit:2},3e3),Tt(t,"search_chunks",{query:`component assessment scoring criteria ${s}`,limit:1},3e3)]);return{bestPractices:(i==null?void 0:i.entries)||[],tokenGuidance:(a==null?void 0:a.entries)||[],scoringCriteria:(c==null?void 0:c.chunks)||[],success:!0}}catch(i){return console.warn("\u26A0\uFE0F MCP queries failed:",i),{bestPractices:[],tokenGuidance:[],scoringCriteria:[],success:!1,error:i instanceof Error?i.message:"Unknown error"}}}async function Tt(e,t,n,s=5e3){var i,a;let o=new AbortController,r=setTimeout(()=>o.abort(),s);try{let c={jsonrpc:"2.0",id:Math.floor(Math.random()*1e3)+100,method:"tools/call",params:{name:`mcp_design-systems_${t}`,arguments:n}},d=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c),signal:o.signal});if(clearTimeout(r),!d.ok)throw new Error(`MCP ${t} failed: ${d.status}`);return((a=(i=(await d.json()).result)==null?void 0:i.content)==null?void 0:a[0])||{}}catch(c){throw clearTimeout(r),c instanceof Error&&c.name==="AbortError"?new Error(`MCP ${t} timeout after ${s}ms`):c}}function wi(e,t,n){var o;let s=R({},e);return s.propertyCheatSheet=Ii(n.actualProperties,e.component||n.context.name),s.audit={designIssues:[],tokenOpportunities:[],structureIssues:[]},(!n.componentDescription||n.componentDescription.trim().length===0)&&s.audit.structureIssues.push("Component lacks description - Add a description in component properties to help MCP and AI understand the component's purpose and usage"),t!=null&&t.success?s.mcpReadiness=Ci(t,e,n):s.mcpReadiness=Fs(n),s.component=s.component||n.context.name,s.description=s.description||`${((o=n.context.additionalContext)==null?void 0:o.componentFamily)||"Component"} with ${n.actualProperties.length} properties`,s.props=s.props||n.actualProperties.map(r=>({name:r.name,type:"select",description:`Controls ${r.name}`,values:r.values,default:r.default})),s.states=s.states||n.actualStates,s.recommendedProperties=e.recommendedProperties||[],s}function Ci(e,t,n){var l,p;let s=[],o=[],r=[];((l=e.bestPractices)==null?void 0:l.length)>0&&e.bestPractices.forEach(u=>{var g,f;((g=u.title)!=null&&g.includes("best practice")||(f=u.title)!=null&&f.includes("pattern"))&&r.push(`Follow ${u.title}`)});let i=n.actualStates.length>=3,a=n.tokens.summary&&n.tokens.summary.actualTokens>n.tokens.summary.hardCodedValues,c=((p=t.structure)==null?void 0:p.complexity)!=="high";return i?s.push("Component has comprehensive states"):o.push("Missing interactive states"),a?s.push("Good token usage"):o.push("Improve token adoption"),c?s.push("Well-structured component"):o.push("Complex structure may need simplification"),{score:Math.round((i?35:15)+(a?35:15)+(c?30:20)),strengths:s,gaps:o,recommendations:r.slice(0,3)}}function Ii(e,t){let n=[],s=e.filter(c=>c.name.toLowerCase().includes("size")||c.values.some(d=>["small","medium","large"].includes(d.toLowerCase()))),o=e.filter(c=>c.name.toLowerCase().includes("variant")||c.name.toLowerCase().includes("type")),r=e.filter(c=>c.name.toLowerCase().includes("state")||c.values.some(d=>["hover","active","disabled"].includes(d.toLowerCase())));s.length>0&&n.push(`\u{1F4CF} Sizes: ${s.map(c=>c.values.join("/")).join(", ")}`),o.length>0&&n.push(`\u{1F3A8} Variants: ${o.map(c=>`${c.name}(${c.values.length})`).join(", ")}`),r.length>0&&n.push(`\u{1F504} States: ${r.map(c=>c.values.join("/")).join(", ")}`);let i=new Set([...s,...o,...r].map(c=>c.name)),a=e.filter(c=>!i.has(c.name)).slice(0,3).map(c=>`${c.name}: ${c.values.slice(0,3).join("/")}`);return a.length>0&&n.push(`\u2699\uFE0F Other: ${a.join(", ")}`),n.slice(0,5)}async function Rt(e,t,n,s,o,r,i){var a;try{console.log("\u{1F504} Processing analysis result..."),console.log("\u{1F4CA} Filtered data received:",JSON.stringify(e,null,2).substring(0,500)+"...");let c=figma.currentPage.selection,d=null;if(c.length>0)d=c[0];else throw new Error("No component selected");let l=await Ms(d,d),p=await Ge(d),u="";if(d.type==="COMPONENT"||d.type==="COMPONENT_SET")u=d.description||"";else if(d.type==="INSTANCE"){let y=await d.getMainComponentAsync();y&&(u=y.description||"")}let g={colors:[],spacing:[],typography:[],effects:[],borders:[],summary:{totalTokens:0,actualTokens:0,hardCodedValues:0,aiSuggestions:0,byCategory:{}}};n.includeTokenAnalysis!==!1&&(g=await ue(d));let f={component:e.component||t.name||"Component",description:e.description||`A ${t.type} component with ${l.length} properties`,props:e.props&&e.props.length>0?e.props:l.map(N=>({name:N.name,type:"select",description:`Controls ${N.name}`,values:N.values,defaultValue:N.default,required:!1})),states:e.states&&e.states.length>0?e.states.map(N=>typeof N=="string"?N:N.name):p.length>0?p:["default"],variants:e.variants||{},slots:e.slots||[],tokens:e.tokens||{colors:g.colors.filter(N=>N.isActualToken).map(N=>N.name),spacing:g.spacing.filter(N=>N.isActualToken).map(N=>N.name),typography:g.typography.filter(N=>N.isActualToken).map(N=>N.name)},usage:e.usage||"General purpose component for design systems",accessibility:e.accessibility||{keyboardNavigation:"Standard keyboard navigation support",screenReader:"Screen reader accessible",colorContrast:"WCAG compliant contrast ratios"},audit:e.audit||{accessibilityIssues:[],namingIssues:[],consistencyIssues:[],tokenOpportunities:[]},propertyCheatSheet:e.propertyCheatSheet||l.map(N=>({name:N.name,values:N.values,default:N.default,description:`Property for ${N.name} configuration`})),mcpReadiness:e.mcpReadiness||Fs({node:d,context:t,actualProperties:l,actualStates:p,tokens:g,componentDescription:u})};console.log("\u{1F4E4} Sending to UI - metadata.props:",(a=f.props)==null?void 0:a.length),console.log("\u{1F4E4} Sending to UI - metadata.states:",f.states),console.log("\u{1F4E4} Sending to UI - metadata.mcpReadiness:",f.mcpReadiness);let m=await Ei(e,t,d,l,p,g,u),h=(e.recommendedProperties||[]).map(N=>({name:N.name||"",type:N.type||"VARIANT",description:N.description||"",examples:N.examples||[]})).filter(N=>N.name);console.log(`\u{1F4A1} AI-generated property recommendations: ${h.length}`);let C=pn(d,5);console.log(`\u{1F4DB} Found ${C.length} naming issues`),s&&s.errors.length>0&&(m.designLint=xi(s));let S;if(o&&r&&i)try{S=await Ai(t,s,m,g,C,h,o,r,i),console.log(`\u{1F4CB} Design review generated: ${S.verdict} \u2014 ${S.findings.length} findings`)}catch(N){console.warn("\u26A0\uFE0F Design review generation failed, continuing without it:",N),S=Pt(s,m,g,C)}else S=Pt(s,m,g,C);return console.log("\u2705 Analysis result processed successfully"),{metadata:f,tokens:g,audit:m,properties:l,recommendations:h,namingIssues:C,existingDescription:u,lintResult:s,designReview:S}}catch(c){throw console.error("Error processing analysis result:",c),c}}function xi(e){let t=[],n=e.summary.byType;return n.fill>0?t.push({check:`Fill styles (${n.fill} missing)`,status:"fail",suggestion:`${n.fill} layer${n.fill>1?"s use":" uses"} hard-coded fills instead of design styles`}):t.push({check:"Fill styles",status:"pass",suggestion:"All fills use design styles"}),n.stroke>0?t.push({check:`Stroke styles (${n.stroke} missing)`,status:"fail",suggestion:`${n.stroke} layer${n.stroke>1?"s use":" uses"} hard-coded strokes instead of design styles`}):t.push({check:"Stroke styles",status:"pass",suggestion:"All strokes use design styles"}),n.effect>0?t.push({check:`Effect styles (${n.effect} missing)`,status:"fail",suggestion:`${n.effect} layer${n.effect>1?"s use":" uses"} hard-coded effects instead of design styles`}):t.push({check:"Effect styles",status:"pass",suggestion:"All effects use design styles"}),n.text>0?t.push({check:`Text styles (${n.text} missing)`,status:"fail",suggestion:`${n.text} text layer${n.text>1?"s lack":" lacks"} applied text styles`}):t.push({check:"Text styles",status:"pass",suggestion:"All text uses design styles"}),n.radius>0?t.push({check:`Border radius (${n.radius} non-standard)`,status:"warning",suggestion:`${n.radius} layer${n.radius>1?"s use":" uses"} non-standard border radius values`}):t.push({check:"Border radius",status:"pass",suggestion:"All radii match design system standards"}),n.spacing>0?t.push({check:`Spacing rhythm (${n.spacing} off-grid)`,status:"warning",suggestion:`${n.spacing} spacing value${n.spacing>1?"s are":" is"} not on the 4/8px grid`}):t.push({check:"Spacing rhythm",status:"pass",suggestion:"All spacing values follow the design grid"}),n.autoLayout>0?t.push({check:`Auto Layout (${n.autoLayout} missing)`,status:"warning",suggestion:`${n.autoLayout} frame${n.autoLayout>1?"s lack":" lacks"} auto-layout`}):t.push({check:"Auto Layout",status:"pass",suggestion:"All container frames use auto-layout"}),t}function Pt(e,t,n,s){let o=[];if(e)for(let u of e.errors)o.push({severity:u.errorType==="radius"||u.errorType==="spacing"||u.errorType==="autoLayout"?"warning":"critical",category:"Style Consistency",title:u.message,description:`Layer "${u.nodeName}" (${u.nodeType}) at ${u.path}`,nodeId:u.nodeId,nodeName:u.nodeName,autoFixable:!1});let r=n.summary.hardCodedValues;r>0&&o.push({severity:"warning",category:"Design Tokens",title:`${r} hard-coded value${r>1?"s":""} found`,description:"These values should be replaced with design tokens for consistency across the design system.",autoFixable:!0});for(let u of t.accessibility||[])u.status==="fail"?o.push({severity:"critical",category:"Accessibility",title:u.check,description:u.suggestion,autoFixable:!1}):u.status==="warning"&&o.push({severity:"warning",category:"Accessibility",title:u.check,description:u.suggestion,autoFixable:!1});for(let u of s.slice(0,10))o.push({severity:u.severity==="error"?"warning":"info",category:"Naming",title:`"${u.currentName}" should be "${u.suggestedName}"`,description:u.reason,nodeId:u.nodeId,nodeName:u.currentName,autoFixable:!0});for(let u of t.componentReadiness||[])u.status==="fail"&&o.push({severity:"suggestion",category:"Component Readiness",title:u.check,description:u.suggestion,autoFixable:!1});let i=(t.states||[]).filter(u=>!u.found);i.length>0&&o.push({severity:"suggestion",category:"Interactive States",title:`${i.length} state${i.length>1?"s":""} not detected`,description:`Missing: ${i.map(u=>u.name).join(", ")}`,autoFixable:!1});let a=o.filter(u=>u.severity==="critical").length,c=o.filter(u=>u.severity==="warning").length,d=a>0?"fail":c>3?"warn":"pass",l;d==="pass"?l="Component follows design system conventions well.":d==="warn"?l=`${c} issues need attention before this component is production-ready.`:l=`${a} critical issue${a>1?"s":""} found \u2014 missing design styles affect consistency.`;let p=[];return e&&e.summary.byType.fill>0&&p.push("Apply fill styles to layers using hard-coded colors"),e&&e.summary.byType.text>0&&p.push("Apply text styles to text layers"),e&&e.summary.byType.stroke>0&&p.push("Apply stroke styles to layers with hard-coded strokes"),e&&e.summary.byType.spacing>0&&p.push("Fix off-grid spacing values to match the 4/8px grid"),e&&e.summary.byType.autoLayout>0&&p.push("Apply auto-layout to container frames"),r>0&&p.push("Replace hard-coded values with design tokens"),s.length>0&&p.push("Rename generic layers to semantic names"),i.length>0&&p.push(`Add missing states: ${i.map(u=>u.name).join(", ")}`),p.length===0&&p.push("Component looks great \u2014 consider documenting it for the team"),{verdict:d,headline:l,findings:o,nextSteps:p}}async function Ai(e,t,n,s,o,r,i,a,c){var C;let d=t?`${t.summary.totalErrors} lint issues (${t.summary.byType.fill} fills, ${t.summary.byType.stroke} strokes, ${t.summary.byType.effect} effects, ${t.summary.byType.text} text, ${t.summary.byType.radius} radius, ${t.summary.byType.spacing||0} spacing, ${t.summary.byType.autoLayout||0} auto-layout)`:"0 lint issues",l=(n.accessibility||[]).filter(S=>S.status==="fail").length,p=(n.componentReadiness||[]).filter(S=>S.status==="fail").length,u=(n.states||[]).filter(S=>!S.found),g=t?t.errors.slice(0,8).map(S=>`- [${S.errorType}] ${S.nodeName}: ${S.message}`).join(` +`):"None",f=`You are a design system reviewer (like CodeRabbit but for Figma designs). Review this component and produce a structured JSON design review. -**Component:** ${e.name} (${e.type}, family: ${((A=e.additionalContext)==null?void 0:A.componentFamily)||"generic"}) +**Component:** ${e.name} (${e.type}, family: ${((C=e.additionalContext)==null?void 0:C.componentFamily)||"generic"}) -**Deterministic Lint Results:** ${l} -${f!=="None"?`Top issues: -${f}`:""} +**Deterministic Lint Results:** ${d} +${g!=="None"?`Top issues: +${g}`:""} **Token Usage:** ${s.summary.actualTokens} tokens used, ${s.summary.hardCodedValues} hard-coded values -**Accessibility Failures:** ${d} +**Accessibility Failures:** ${l} **Component Readiness Failures:** ${p} -**Missing States:** ${u.map(w=>w.name).join(", ")||"None"} +**Missing States:** ${u.map(S=>S.name).join(", ")||"None"} **Naming Issues:** ${o.length} **AI Recommendations:** ${r.length} property suggestions @@ -333,7 +333,7 @@ Rules: - Group similar lint errors (e.g. "5 layers missing fill styles" not 5 separate findings) - Max 10 findings, prioritized by severity - nextSteps: max 5, ordered by impact -- Be specific and actionable, not generic`,m=await ie(c,i,{prompt:y,model:a,maxTokens:1024,temperature:.1}),v=le(m.content);return v?{verdict:v.verdict||"warn",headline:v.headline||"Review completed",findings:(v.findings||[]).map(w=>({severity:w.severity||"info",category:w.category||"General",title:w.title||"",description:w.description||"",nodeId:w.nodeId,nodeName:w.nodeName,autoFixable:w.autoFixable||!1})),nextSteps:v.nextSteps||[]}:Nt(t,n,s,o)}async function Rr(e,t,n,s,o,r,i){var y;let a=!1,c="";n.type==="COMPONENT"&&((y=n.parent)==null?void 0:y.type)==="COMPONENT_SET"?(c=n.parent.description||"",a=c.trim().length>0):n.type==="COMPONENT_SET"&&(a=!!(i&&i.trim().length>0));let l=!!(i&&i.trim().length>0),d=l?"pass":"warning",p="";l?p="Component has description for MCP/AI context":a?(d="pass",p="Component set has a description. Consider adding a variant-specific description for richer context."):p="Add a component description to help MCP and AI understand the component purpose and usage";let u=[{check:"Property configuration",status:s.length>0?"pass":"warning",suggestion:s.length>0?"Component has configurable properties":"Consider adding properties for component customization"},{check:"Component description",status:d,suggestion:p}],f=Or(n,o);return{states:o.map(m=>({name:m,found:!0})),componentReadiness:u,accessibility:f}}var $r=["button","btn","link","anchor","checkbox","check-box","radio","toggle","switch","tab","chip","tag","input","select","dropdown","menu-item","menuitem","slider","stepper","icon-button","fab","action"];function Mr(e,t){let n=e.name.toLowerCase();if($r.some(o=>n.includes(o)))return!0;let s=["hover","pressed","focus","focused","active","disabled"];return!!t.some(o=>s.includes(o.toLowerCase()))}function Or(e,t){let n=[],s=Mr(e,t);if(s){let o="width"in e?e.width:0,r="height"in e?e.height:0,i=Math.min(o,r);i>=44?n.push({check:"Touch target size",status:"pass",suggestion:`Target size ${Math.round(o)}\xD7${Math.round(r)}px meets recommended 44px minimum`}):i>=24?n.push({check:"Touch target size",status:"warning",suggestion:`Target size ${Math.round(o)}\xD7${Math.round(r)}px meets WCAG minimum (24px) but is below recommended 44px`}):n.push({check:"Touch target size",status:"fail",suggestion:`Target size ${Math.round(o)}\xD7${Math.round(r)}px is below WCAG 2.5.8 minimum of 24\xD724px`})}if(s){let o=t.some(r=>{let i=r.toLowerCase();return i==="focus"||i==="focused"||i.includes("focus")});n.push({check:"Focus state",status:o?"pass":"warning",suggestion:o?"Component has a focus state for keyboard navigation":"Add a visible focus state to support keyboard navigation (WCAG 2.4.7)"})}if("findAll"in e){let r=e.findAll(i=>i.type==="TEXT");if(r.length>0){let i=1/0,a=!1;for(let c of r){let l=typeof c.fontSize=="number"?c.fontSize:0;l>0&&l<i&&(i=l),l>0&&l<12&&(a=!0)}a?n.push({check:"Minimum font size",status:"warning",suggestion:`Text as small as ${i}px detected. Consider using 12px minimum for readability`}):i!==1/0&&n.push({check:"Minimum font size",status:"pass",suggestion:`Smallest text is ${i}px, meets readability guidelines`})}}if("findAll"in e){let r=e.findAll(l=>l.type==="TEXT"),i=1/0,a=0,c="";for(let l of r){let d=l.fills;if(!Array.isArray(d)||d.length===0)continue;let p=d.find(v=>v.type==="SOLID"&&v.visible!==!1&&v.color&&!(v.boundVariables&&v.boundVariables.color));if(!p)continue;let u=he(l);if(!u)continue;let f=te(p.color.r,p.color.g,p.color.b),y=te(u.r,u.g,u.b),m=ye(f,y);a++,m<i&&(i=m,c=l.name||"text")}if(a>0&&i!==1/0){let l=i.toFixed(1);i>=4.5?n.push({check:"Color contrast",status:"pass",suggestion:`Lowest contrast ratio is ${l}:1, meets WCAG AA (4.5:1)`}):i>=3?n.push({check:"Color contrast",status:"warning",suggestion:`"${c}" has ${l}:1 contrast. Meets large text AA (3:1) but not normal text (4.5:1)`}):n.push({check:"Color contrast",status:"fail",suggestion:`"${c}" has ${l}:1 contrast, below WCAG AA minimum of 3:1`})}}return n.length===0&&n.push({check:"Accessibility review",status:"pass",suggestion:"No accessibility issues detected for this component type"}),n}function as(e){var x,S,P,O,M,B,C,$,b,T,L;let{node:t,context:n,actualProperties:s,actualStates:o,tokens:r,componentDescription:i}=e,a=n.componentFamily||"generic",c=[],l=[],d=[];i&&i.trim().length>0?c.push("Has component description for better MCP/AI context"):(l.push("Missing component description - AI cannot understand component purpose and intent"),d.push("Add a descriptive explanation in component properties to help AI understand the component's purpose, behavior, and usage patterns")),s.length>0?c.push(`Has ${s.length} configurable properties`):(l.push("No configurable properties - component cannot be customized for different use cases"),d.push("Add component properties for customization (size, variant, text content, etc.)"));let p=n.hasInteractiveElements&&a!=="badge"&&a!=="icon";p&&(o.length>1?c.push("Includes multiple component states"):(l.push("Missing interactive states - users won't receive proper feedback for interactions"),d.push("Add hover, focus, and disabled states with clear visual feedback")));let u={colors:((S=(x=r==null?void 0:r.colors)==null?void 0:x.filter(I=>I.isActualToken))==null?void 0:S.length)||0,spacing:((O=(P=r==null?void 0:r.spacing)==null?void 0:P.filter(I=>I.isActualToken))==null?void 0:O.length)||0,typography:((B=(M=r==null?void 0:r.typography)==null?void 0:M.filter(I=>I.isActualToken))==null?void 0:B.length)||0,hardCoded:[...((C=r==null?void 0:r.colors)==null?void 0:C.filter(I=>!I.isActualToken&&!I.isDefaultVariantStyle))||[],...(($=r==null?void 0:r.spacing)==null?void 0:$.filter(I=>!I.isActualToken&&!I.isDefaultVariantStyle))||[],...((b=r==null?void 0:r.typography)==null?void 0:b.filter(I=>!I.isActualToken&&!I.isDefaultVariantStyle))||[],...((T=r==null?void 0:r.effects)==null?void 0:T.filter(I=>!I.isActualToken&&!I.isDefaultVariantStyle))||[],...((L=r==null?void 0:r.borders)==null?void 0:L.filter(I=>!I.isActualToken&&!I.isDefaultVariantStyle))||[]].length},f=u.colors+u.spacing+u.typography;f>0?(c.push("Uses design tokens for consistency"),u.hardCoded>0&&(l.push("Found hard-coded values - inconsistent with design system"),d.push("Replace remaining hard-coded colors and spacing with design tokens"))):u.hardCoded>2&&(l.push("No design tokens used - component styling is inconsistent with design system"),d.push("Replace hard-coded values with design tokens for colors, spacing, and typography"));let y=s.some(I=>I.name.toLowerCase().includes("size")||I.name.toLowerCase().includes("scale")||I.name.toLowerCase().includes("dimension")),m=s.some(I=>I.name.toLowerCase().includes("variant")||I.name.toLowerCase().includes("style")||I.name.toLowerCase().includes("type"));a==="avatar"?!y&&s.length>0&&(l.push("No size variants defined - limits reusability across different contexts"),d.push("Add size property (xs, sm, md, lg, xl) for headers, lists, and profiles")):a==="button"?(o.length<=1&&(l.push("Missing interactive states - reduces accessibility and user feedback"),d.push("Add hover, focus, and disabled states with clear visual feedback")),!m&&s.length>0&&(l.push("No visual hierarchy variants - limits design flexibility"),d.push("Add variant property (primary, secondary, danger) for proper hierarchy"))):a==="input"?o.length<=1&&(l.push("Missing form states - poor accessibility and user experience"),d.push("Add focus, error, and disabled states with clear visual indicators")):a==="container"&&(!m&&s.length>0&&(l.push("No layout variants defined - limits flexibility for different use cases"),d.push("Add orientation property (horizontal, vertical) or density variants")),s.length>0&&!s.some(I=>I.name.toLowerCase().includes("spacing"))&&(l.push("No spacing customization - may not fit all design contexts"),d.push("Add spacing property to control internal padding and gaps"))),s.length===0?(l.push("No configurable properties - component lacks flexibility for different use cases"),a==="container"?d.push("Add layout properties for customization (orientation, spacing, alignment)"):d.push("Add component properties to enable customization and reuse")):s.length===1&&!y&&!m&&(l.push("Limited customization options - consider adding more properties for flexibility"),a!=="container"&&p&&o.length<=1?d.push("Add interactive states and additional variant options"):a==="container"&&d.push("Consider adding layout variant properties (orientation, density)")),c.length===0&&c.push("Component follows basic Figma structure patterns"),l.length===0&&l.push("Well-structured component - consider minor enhancements for broader usage"),d.length===0&&d.push("Component is well-configured - ready for code generation");let v=0,A=s.length>0,w=f>0,k=f>0?f/(f+u.hardCoded):0;if(A&&(v+=22),i&&i.trim().length>0&&(v+=3),v+=Math.round(25*k),n.hasInteractiveElements&&a!=="badge"&&a!=="icon"){let I=Math.min(o.length/3,1);v+=Math.round(20*I)}else v+=20;return(t.type==="COMPONENT"||t.type==="COMPONENT_SET"||t.type==="INSTANCE")&&(v+=10),n.name&&!n.name.toLowerCase().includes("untitled")&&(v+=10),(A||w||o.length>0)&&(v+=10),v=Math.max(0,Math.min(100,v)),{score:v,strengths:c,gaps:es(l),recommendations:es(d),implementationNotes:Fr(a,c,l,s,o,u)}}function Fr(e,t,n,s,o,r){let i=[];return e==="button"?(o.length<3&&i.push("Implement hover, focus, and active states for better interactivity"),s.length===0&&i.push("Add variant and size properties to support different use cases")):e==="input"?(o.includes("error")||i.push("Add error state with clear visual indicators for form validation"),i.push("Ensure proper label association and placeholder text patterns")):e==="card"?(i.push("Consider implementing click handlers for interactive cards"),s.length===0&&i.push("Add elevation or variant properties for visual hierarchy")):e==="avatar"?(i.push("Implement fallback patterns for missing images"),s.some(a=>a.name.toLowerCase().includes("size"))||i.push("Add size variants for flexible usage across contexts")):e==="container"&&(i.push("Focus on layout flexibility and content composition"),i.push("Consider responsive behavior for different screen sizes")),r.hardCoded>r.colors+r.spacing&&i.push("Prioritize converting hard-coded values to design tokens"),s.length===0?i.push("Define component properties to enable customization without code changes"):s.length===1&&i.push("Consider additional properties for greater flexibility"),i.length===0&&(n.length>3?i.push("Focus on addressing the high-priority gaps identified above"):t.length>n.length?i.push("Component is well-structured for code generation with minor improvements needed"):i.push("Balance quick wins with systematic improvements for optimal results")),i.join(". ")+"."}function es(e){if(e.length<=1)return e;let t=[],n=new Set,s=[{pattern:/add.*component.*propert/i,message:"Add component properties for customization and reuse"},{pattern:/add.*(hover|focus|disabled|interactive).*state/i,message:"Add hover, focus, and disabled states with clear visual feedback"},{pattern:/replace.*hard.coded.*(color|spacing|token)/i,message:"Replace remaining hard-coded colors and spacing with design tokens"},{pattern:/add.*(size|variant).*propert/i,message:"Add size and style variant properties for different use cases"},{pattern:/no.*configurable.*propert.*(cannot|lacks|limited)/i,message:"No configurable properties - component lacks flexibility for different use cases"},{pattern:/(missing|no).*(interactive|hover|focus).*state/i,message:"Missing interactive states - reduces accessibility and user feedback"},{pattern:/found.*hard.coded.*value.*(inconsistent|design.*system)/i,message:"Found hard-coded values - inconsistent with design system"},{pattern:/(minimal|simple).*layer.*structure.*(lack|semantic|organization)/i,message:"Minimal layer structure - may lack semantic organization for complex use cases"}];return e.forEach(o=>{let r=o.trim();if(!r)return;let i=!0,a=r;for(let{pattern:d,message:p}of s)if(d.test(r))if(n.has(d.source)){i=!1;break}else{n.add(d.source),a=p;break}let c=r.toLowerCase(),l=t.some(d=>d.toLowerCase()===c||Dr(d.toLowerCase(),c)>.8);i&&!l&&t.push(a)}),console.log(`\u{1F50D} [DEDUP] Reduced ${e.length} items to ${t.length}`),e.length!==t.length&&(console.log("\u{1F50D} [DEDUP] Original:",e),console.log("\u{1F50D} [DEDUP] Deduplicated:",t)),t}function Dr(e,t){let n=e.length>t.length?e:t,s=e.length>t.length?t:e;if(n.length===0)return 1;let o=Vr(n,s);return(n.length-o)/n.length}function Vr(e,t){let n=[];for(let s=0;s<=t.length;s++)n[s]=[s];for(let s=0;s<=e.length;s++)n[0][s]=s;for(let s=1;s<=t.length;s++)for(let o=1;o<=e.length;o++)t.charAt(s-1)===e.charAt(o-1)?n[s][o]=n[s-1][o-1]:n[s][o]=Math.min(n[s-1][o-1]+1,n[s][o-1]+1,n[s-1][o]+1);return n[t.length][e.length]}var xt=class{constructor(t={}){this.cache=new Map;this.designSystemsKnowledge=null;this.config=R({enableCaching:!0,enableMCPIntegration:!1,consistencyThreshold:.95},t)}generateComponentHash(t,n,s){var r,i;let o={name:t.name,type:t.type,hierarchy:this.normalizeHierarchy(t.hierarchy),frameStructure:t.frameStructure,detectedStyles:t.detectedStyles,tokenFingerprint:this.generateTokenFingerprint(n),staticProperties:{hasInteractiveElements:((r=t.additionalContext)==null?void 0:r.hasInteractiveElements)||!1,componentFamily:((i=t.additionalContext)==null?void 0:i.componentFamily)||"generic"},lintSettingsFingerprint:s?this.createHash(JSON.stringify(s)):""};return this.createHash(JSON.stringify(o))}getCachedAnalysis(t){if(!this.config.enableCaching)return null;let n=this.cache.get(t);return n?Date.now()-n.timestamp>24*60*60*1e3?(this.cache.delete(t),null):(console.log("\u2705 Using cached analysis for component hash:",t),n):null}cacheAnalysis(t,n){var s;this.config.enableCaching&&(this.cache.set(t,{hash:t,result:n,timestamp:Date.now(),mcpKnowledgeVersion:((s=this.designSystemsKnowledge)==null?void 0:s.version)||"1.0.0"}),console.log("\u{1F4BE} Cached analysis for component hash:",t))}setDesignSystemsKnowledge(t){this.designSystemsKnowledge=t}async loadDesignSystemsKnowledge(){this.loadFallbackKnowledge()}createDeterministicPrompt(t){let n=this.createBasePrompt(t),s=this.getMCPGuidance(t),o=this.getScoringCriteria(t);return`${n} +- Be specific and actionable, not generic`,m=await ie(c,i,{prompt:f,model:a,maxTokens:1024,temperature:.1}),h=pe(m.content);return h?{verdict:h.verdict||"warn",headline:h.headline||"Review completed",findings:(h.findings||[]).map(S=>({severity:S.severity||"info",category:S.category||"General",title:S.title||"",description:S.description||"",nodeId:S.nodeId,nodeName:S.nodeName,autoFixable:S.autoFixable||!1})),nextSteps:h.nextSteps||[]}:Pt(t,n,s,o)}async function Ei(e,t,n,s,o,r,i){var f;let a=!1,c="";n.type==="COMPONENT"&&((f=n.parent)==null?void 0:f.type)==="COMPONENT_SET"?(c=n.parent.description||"",a=c.trim().length>0):n.type==="COMPONENT_SET"&&(a=!!(i&&i.trim().length>0));let d=!!(i&&i.trim().length>0),l=d?"pass":"warning",p="";d?p="Component has description for MCP/AI context":a?(l="pass",p="Component set has a description. Consider adding a variant-specific description for richer context."):p="Add a component description to help MCP and AI understand the component purpose and usage";let u=[{check:"Property configuration",status:s.length>0?"pass":"warning",suggestion:s.length>0?"Component has configurable properties":"Consider adding properties for component customization"},{check:"Component description",status:l,suggestion:p}],g=Li(n,o);return{states:o.map(m=>({name:m,found:!0})),componentReadiness:u,accessibility:g}}var Ti=["button","btn","link","anchor","checkbox","check-box","radio","toggle","switch","tab","chip","tag","input","select","dropdown","menu-item","menuitem","slider","stepper","icon-button","fab","action"];function Pi(e,t){let n=e.name.toLowerCase();if(Ti.some(o=>n.includes(o)))return!0;let s=["hover","pressed","focus","focused","active","disabled"];return!!t.some(o=>s.includes(o.toLowerCase()))}function Li(e,t){let n=[],s=Pi(e,t);if(s){let o="width"in e?e.width:0,r="height"in e?e.height:0,i=Math.min(o,r);i>=44?n.push({check:"Touch target size",status:"pass",suggestion:`Target size ${Math.round(o)}\xD7${Math.round(r)}px meets recommended 44px minimum`}):i>=24?n.push({check:"Touch target size",status:"warning",suggestion:`Target size ${Math.round(o)}\xD7${Math.round(r)}px meets WCAG minimum (24px) but is below recommended 44px`}):n.push({check:"Touch target size",status:"fail",suggestion:`Target size ${Math.round(o)}\xD7${Math.round(r)}px is below WCAG 2.5.8 minimum of 24\xD724px`})}if(s){let o=t.some(r=>{let i=r.toLowerCase();return i==="focus"||i==="focused"||i.includes("focus")});n.push({check:"Focus state",status:o?"pass":"warning",suggestion:o?"Component has a focus state for keyboard navigation":"Add a visible focus state to support keyboard navigation (WCAG 2.4.7)"})}if("findAll"in e){let r=e.findAll(i=>i.type==="TEXT");if(r.length>0){let i=1/0,a=!1;for(let c of r){let d=typeof c.fontSize=="number"?c.fontSize:0;d>0&&d<i&&(i=d),d>0&&d<12&&(a=!0)}a?n.push({check:"Minimum font size",status:"warning",suggestion:`Text as small as ${i}px detected. Consider using 12px minimum for readability`}):i!==1/0&&n.push({check:"Minimum font size",status:"pass",suggestion:`Smallest text is ${i}px, meets readability guidelines`})}}if("findAll"in e){let r=e.findAll(d=>d.type==="TEXT"),i=1/0,a=0,c="";for(let d of r){let l=d.fills;if(!Array.isArray(l)||l.length===0)continue;let p=l.find(h=>h.type==="SOLID"&&h.visible!==!1&&h.color&&!(h.boundVariables&&h.boundVariables.color));if(!p)continue;let u=Ne(d);if(!u)continue;let g=ne(p.color.r,p.color.g,p.color.b),f=ne(u.r,u.g,u.b),m=ke(g,f);a++,m<i&&(i=m,c=d.name||"text")}if(a>0&&i!==1/0){let d=i.toFixed(1);i>=4.5?n.push({check:"Color contrast",status:"pass",suggestion:`Lowest contrast ratio is ${d}:1, meets WCAG AA (4.5:1)`}):i>=3?n.push({check:"Color contrast",status:"warning",suggestion:`"${c}" has ${d}:1 contrast. Meets large text AA (3:1) but not normal text (4.5:1)`}):n.push({check:"Color contrast",status:"fail",suggestion:`"${c}" has ${d}:1 contrast, below WCAG AA minimum of 3:1`})}}return n.length===0&&n.push({check:"Accessibility review",status:"pass",suggestion:"No accessibility issues detected for this component type"}),n}function Fs(e){var I,w,P,O,M,z,x,$,v,T,L;let{node:t,context:n,actualProperties:s,actualStates:o,tokens:r,componentDescription:i}=e,a=n.componentFamily||"generic",c=[],d=[],l=[];i&&i.trim().length>0?c.push("Has component description for better MCP/AI context"):(d.push("Missing component description - AI cannot understand component purpose and intent"),l.push("Add a descriptive explanation in component properties to help AI understand the component's purpose, behavior, and usage patterns")),s.length>0?c.push(`Has ${s.length} configurable properties`):(d.push("No configurable properties - component cannot be customized for different use cases"),l.push("Add component properties for customization (size, variant, text content, etc.)"));let p=n.hasInteractiveElements&&a!=="badge"&&a!=="icon";p&&(o.length>1?c.push("Includes multiple component states"):(d.push("Missing interactive states - users won't receive proper feedback for interactions"),l.push("Add hover, focus, and disabled states with clear visual feedback")));let u={colors:((w=(I=r==null?void 0:r.colors)==null?void 0:I.filter(A=>A.isActualToken))==null?void 0:w.length)||0,spacing:((O=(P=r==null?void 0:r.spacing)==null?void 0:P.filter(A=>A.isActualToken))==null?void 0:O.length)||0,typography:((z=(M=r==null?void 0:r.typography)==null?void 0:M.filter(A=>A.isActualToken))==null?void 0:z.length)||0,hardCoded:[...((x=r==null?void 0:r.colors)==null?void 0:x.filter(A=>!A.isActualToken&&!A.isDefaultVariantStyle))||[],...(($=r==null?void 0:r.spacing)==null?void 0:$.filter(A=>!A.isActualToken&&!A.isDefaultVariantStyle))||[],...((v=r==null?void 0:r.typography)==null?void 0:v.filter(A=>!A.isActualToken&&!A.isDefaultVariantStyle))||[],...((T=r==null?void 0:r.effects)==null?void 0:T.filter(A=>!A.isActualToken&&!A.isDefaultVariantStyle))||[],...((L=r==null?void 0:r.borders)==null?void 0:L.filter(A=>!A.isActualToken&&!A.isDefaultVariantStyle))||[]].length},g=u.colors+u.spacing+u.typography;g>0?(c.push("Uses design tokens for consistency"),u.hardCoded>0&&(d.push("Found hard-coded values - inconsistent with design system"),l.push("Replace remaining hard-coded colors and spacing with design tokens"))):u.hardCoded>2&&(d.push("No design tokens used - component styling is inconsistent with design system"),l.push("Replace hard-coded values with design tokens for colors, spacing, and typography"));let f=s.some(A=>A.name.toLowerCase().includes("size")||A.name.toLowerCase().includes("scale")||A.name.toLowerCase().includes("dimension")),m=s.some(A=>A.name.toLowerCase().includes("variant")||A.name.toLowerCase().includes("style")||A.name.toLowerCase().includes("type"));a==="avatar"?!f&&s.length>0&&(d.push("No size variants defined - limits reusability across different contexts"),l.push("Add size property (xs, sm, md, lg, xl) for headers, lists, and profiles")):a==="button"?(o.length<=1&&(d.push("Missing interactive states - reduces accessibility and user feedback"),l.push("Add hover, focus, and disabled states with clear visual feedback")),!m&&s.length>0&&(d.push("No visual hierarchy variants - limits design flexibility"),l.push("Add variant property (primary, secondary, danger) for proper hierarchy"))):a==="input"?o.length<=1&&(d.push("Missing form states - poor accessibility and user experience"),l.push("Add focus, error, and disabled states with clear visual indicators")):a==="container"&&(!m&&s.length>0&&(d.push("No layout variants defined - limits flexibility for different use cases"),l.push("Add orientation property (horizontal, vertical) or density variants")),s.length>0&&!s.some(A=>A.name.toLowerCase().includes("spacing"))&&(d.push("No spacing customization - may not fit all design contexts"),l.push("Add spacing property to control internal padding and gaps"))),s.length===0?(d.push("No configurable properties - component lacks flexibility for different use cases"),a==="container"?l.push("Add layout properties for customization (orientation, spacing, alignment)"):l.push("Add component properties to enable customization and reuse")):s.length===1&&!f&&!m&&(d.push("Limited customization options - consider adding more properties for flexibility"),a!=="container"&&p&&o.length<=1?l.push("Add interactive states and additional variant options"):a==="container"&&l.push("Consider adding layout variant properties (orientation, density)")),c.length===0&&c.push("Component follows basic Figma structure patterns"),d.length===0&&d.push("Well-structured component - consider minor enhancements for broader usage"),l.length===0&&l.push("Component is well-configured - ready for code generation");let h=0,C=s.length>0,S=g>0,N=g>0?g/(g+u.hardCoded):0;if(C&&(h+=22),i&&i.trim().length>0&&(h+=3),h+=Math.round(25*N),n.hasInteractiveElements&&a!=="badge"&&a!=="icon"){let A=Math.min(o.length/3,1);h+=Math.round(20*A)}else h+=20;return(t.type==="COMPONENT"||t.type==="COMPONENT_SET"||t.type==="INSTANCE")&&(h+=10),n.name&&!n.name.toLowerCase().includes("untitled")&&(h+=10),(C||S||o.length>0)&&(h+=10),h=Math.max(0,Math.min(100,h)),{score:h,strengths:c,gaps:Ts(d),recommendations:Ts(l),implementationNotes:Ri(a,c,d,s,o,u)}}function Ri(e,t,n,s,o,r){let i=[];return e==="button"?(o.length<3&&i.push("Implement hover, focus, and active states for better interactivity"),s.length===0&&i.push("Add variant and size properties to support different use cases")):e==="input"?(o.includes("error")||i.push("Add error state with clear visual indicators for form validation"),i.push("Ensure proper label association and placeholder text patterns")):e==="card"?(i.push("Consider implementing click handlers for interactive cards"),s.length===0&&i.push("Add elevation or variant properties for visual hierarchy")):e==="avatar"?(i.push("Implement fallback patterns for missing images"),s.some(a=>a.name.toLowerCase().includes("size"))||i.push("Add size variants for flexible usage across contexts")):e==="container"&&(i.push("Focus on layout flexibility and content composition"),i.push("Consider responsive behavior for different screen sizes")),r.hardCoded>r.colors+r.spacing&&i.push("Prioritize converting hard-coded values to design tokens"),s.length===0?i.push("Define component properties to enable customization without code changes"):s.length===1&&i.push("Consider additional properties for greater flexibility"),i.length===0&&(n.length>3?i.push("Focus on addressing the high-priority gaps identified above"):t.length>n.length?i.push("Component is well-structured for code generation with minor improvements needed"):i.push("Balance quick wins with systematic improvements for optimal results")),i.join(". ")+"."}function Ts(e){if(e.length<=1)return e;let t=[],n=new Set,s=[{pattern:/add.*component.*propert/i,message:"Add component properties for customization and reuse"},{pattern:/add.*(hover|focus|disabled|interactive).*state/i,message:"Add hover, focus, and disabled states with clear visual feedback"},{pattern:/replace.*hard.coded.*(color|spacing|token)/i,message:"Replace remaining hard-coded colors and spacing with design tokens"},{pattern:/add.*(size|variant).*propert/i,message:"Add size and style variant properties for different use cases"},{pattern:/no.*configurable.*propert.*(cannot|lacks|limited)/i,message:"No configurable properties - component lacks flexibility for different use cases"},{pattern:/(missing|no).*(interactive|hover|focus).*state/i,message:"Missing interactive states - reduces accessibility and user feedback"},{pattern:/found.*hard.coded.*value.*(inconsistent|design.*system)/i,message:"Found hard-coded values - inconsistent with design system"},{pattern:/(minimal|simple).*layer.*structure.*(lack|semantic|organization)/i,message:"Minimal layer structure - may lack semantic organization for complex use cases"}];return e.forEach(o=>{let r=o.trim();if(!r)return;let i=!0,a=r;for(let{pattern:l,message:p}of s)if(l.test(r))if(n.has(l.source)){i=!1;break}else{n.add(l.source),a=p;break}let c=r.toLowerCase(),d=t.some(l=>l.toLowerCase()===c||$i(l.toLowerCase(),c)>.8);i&&!d&&t.push(a)}),console.log(`\u{1F50D} [DEDUP] Reduced ${e.length} items to ${t.length}`),e.length!==t.length&&(console.log("\u{1F50D} [DEDUP] Original:",e),console.log("\u{1F50D} [DEDUP] Deduplicated:",t)),t}function $i(e,t){let n=e.length>t.length?e:t,s=e.length>t.length?t:e;if(n.length===0)return 1;let o=Mi(n,s);return(n.length-o)/n.length}function Mi(e,t){let n=[];for(let s=0;s<=t.length;s++)n[s]=[s];for(let s=0;s<=e.length;s++)n[0][s]=s;for(let s=1;s<=t.length;s++)for(let o=1;o<=e.length;o++)t.charAt(s-1)===e.charAt(o-1)?n[s][o]=n[s-1][o-1]:n[s][o]=Math.min(n[s-1][o-1]+1,n[s][o-1]+1,n[s-1][o]+1);return n[t.length][e.length]}var $t=class{constructor(t={}){this.cache=new Map;this.designSystemsKnowledge=null;this.config=R({enableCaching:!0,enableMCPIntegration:!1,consistencyThreshold:.95},t)}generateComponentHash(t,n,s){var r,i;let o={name:t.name,type:t.type,hierarchy:this.normalizeHierarchy(t.hierarchy),frameStructure:t.frameStructure,detectedStyles:t.detectedStyles,tokenFingerprint:this.generateTokenFingerprint(n),staticProperties:{hasInteractiveElements:((r=t.additionalContext)==null?void 0:r.hasInteractiveElements)||!1,componentFamily:((i=t.additionalContext)==null?void 0:i.componentFamily)||"generic"},lintSettingsFingerprint:s?this.createHash(JSON.stringify(s)):""};return this.createHash(JSON.stringify(o))}getCachedAnalysis(t){if(!this.config.enableCaching)return null;let n=this.cache.get(t);return n?Date.now()-n.timestamp>24*60*60*1e3?(this.cache.delete(t),null):(console.log("\u2705 Using cached analysis for component hash:",t),n):null}cacheAnalysis(t,n){var s;this.config.enableCaching&&(this.cache.set(t,{hash:t,result:n,timestamp:Date.now(),mcpKnowledgeVersion:((s=this.designSystemsKnowledge)==null?void 0:s.version)||"1.0.0"}),console.log("\u{1F4BE} Cached analysis for component hash:",t))}setDesignSystemsKnowledge(t){this.designSystemsKnowledge=t}async loadDesignSystemsKnowledge(){this.loadFallbackKnowledge()}createDeterministicPrompt(t){let n=this.createBasePrompt(t),s=this.getMCPGuidance(t),o=this.getScoringCriteria(t);return`${n} **CONSISTENCY REQUIREMENTS:** - Use DETERMINISTIC analysis based on the exact component structure provided @@ -375,7 +375,7 @@ ${o} "tokens": {...}, "audit": {...}, "mcpReadiness": {...} -}`}validateAnalysisConsistency(t,n){var r,i,a,c,l;let s=[];(r=t.metadata)!=null&&r.component||s.push("Missing component name"),(i=t.metadata)!=null&&i.description||s.push("Missing component description"),this.isValidScore((c=(a=t.metadata)==null?void 0:a.mcpReadiness)==null?void 0:c.score)||s.push("Invalid or missing MCP readiness score");let o=(l=n.additionalContext)==null?void 0:l.componentFamily;return o&&!this.validateComponentFamilyConsistency(t,o)&&s.push(`Inconsistent analysis for ${o} component family`),this.validateTokenRecommendations(t.tokens)||s.push("Inconsistent token recommendations"),s.length>0?(console.warn("\u26A0\uFE0F Analysis consistency issues found:",s),!1):!0}applyConsistencyCorrections(t,n){var o;let s=R({},t);return(o=n.additionalContext)!=null&&o.componentFamily&&(s.metadata=this.applyComponentFamilyCorrections(s.metadata,n.additionalContext.componentFamily)),s.tokens=this.applyTokenConsistencyCorrections(s.tokens),s.metadata.mcpReadiness=this.ensureConsistentScoring(s.metadata.mcpReadiness||{},n),s}normalizeHierarchy(t){return t.map(n=>({name:n.name.toLowerCase().trim(),type:n.type,depth:n.depth}))}generateTokenFingerprint(t){let n=t.map(s=>`${s.type}:${s.isToken}:${s.source}`).sort().join("|");return this.createHash(n)}createHash(t){let n=0;if(t.length===0)return n.toString();for(let s=0;s<t.length;s++){let o=t.charCodeAt(s);n=(n<<5)-n+o,n=n&n}return Math.abs(n).toString(36)}createBasePrompt(t){var n,s,o,r;return`You are an expert design system architect analyzing a Figma component for comprehensive metadata and design token recommendations. +}`}validateAnalysisConsistency(t,n){var r,i,a,c,d;let s=[];(r=t.metadata)!=null&&r.component||s.push("Missing component name"),(i=t.metadata)!=null&&i.description||s.push("Missing component description"),this.isValidScore((c=(a=t.metadata)==null?void 0:a.mcpReadiness)==null?void 0:c.score)||s.push("Invalid or missing MCP readiness score");let o=(d=n.additionalContext)==null?void 0:d.componentFamily;return o&&!this.validateComponentFamilyConsistency(t,o)&&s.push(`Inconsistent analysis for ${o} component family`),this.validateTokenRecommendations(t.tokens)||s.push("Inconsistent token recommendations"),s.length>0?(console.warn("\u26A0\uFE0F Analysis consistency issues found:",s),!1):!0}applyConsistencyCorrections(t,n){var o;let s=R({},t);return(o=n.additionalContext)!=null&&o.componentFamily&&(s.metadata=this.applyComponentFamilyCorrections(s.metadata,n.additionalContext.componentFamily)),s.tokens=this.applyTokenConsistencyCorrections(s.tokens),s.metadata.mcpReadiness=this.ensureConsistentScoring(s.metadata.mcpReadiness||{},n),s}normalizeHierarchy(t){return t.map(n=>({name:n.name.toLowerCase().trim(),type:n.type,depth:n.depth}))}generateTokenFingerprint(t){let n=t.map(s=>`${s.type}:${s.isToken}:${s.source}`).sort().join("|");return this.createHash(n)}createHash(t){let n=0;if(t.length===0)return n.toString();for(let s=0;s<t.length;s++){let o=t.charCodeAt(s);n=(n<<5)-n+o,n=n&n}return Math.abs(n).toString(36)}createBasePrompt(t){var n,s,o,r;return`You are an expert design system architect analyzing a Figma component for comprehensive metadata and design token recommendations. **Component Analysis Context:** - Component Name: ${t.name} @@ -398,9 +398,9 @@ ${o} - 70-79: Solid foundation, some important gaps - 60-69: Basic implementation, significant improvements needed - Below 60: Major issues, substantial rework required - `}loadFallbackKnowledge(){this.designSystemsKnowledge={version:"1.0.0-fallback",components:{button:"Button components require comprehensive state management",avatar:"Avatar components should support size variants and interactive states",card:"Card components need consistent spacing and content hierarchy",badge:"Badge components should use semantic colors for status indication",input:"Input components require comprehensive accessibility and validation",generic:"Generic components should follow basic design system principles"},tokens:"Use semantic token naming: semantic-color-primary, spacing-md-16px, text-size-lg-18px",accessibility:"Ensure WCAG 2.1 AA compliance with proper ARIA labels and keyboard support",scoring:this.getFallbackScoringCriteria(),lastUpdated:Date.now()}}isValidScore(t){return typeof t=="number"&&t>=0&&t<=100}validateComponentFamilyConsistency(t,n){let s=t.metadata;switch(n){case"button":return this.validateButtonComponent(s);case"avatar":return this.validateAvatarComponent(s);case"input":return this.validateInputComponent(s);default:return!0}}validateButtonComponent(t){var s;return((s=t.states)==null?void 0:s.some(o=>["hover","focus","active","disabled"].includes(o.toLowerCase())))||!1}validateAvatarComponent(t){var o,r,i;let n=((r=(o=t.variants)==null?void 0:o.size)==null?void 0:r.length)>0,s=(i=t.props)==null?void 0:i.some(a=>a.name.toLowerCase().includes("size"));return n||s||!1}validateInputComponent(t){var s;return((s=t.states)==null?void 0:s.some(o=>["focus","error","disabled","filled"].includes(o.toLowerCase())))||!1}validateTokenRecommendations(t){var s;return((s=t.colors)==null?void 0:s.some(o=>o.name.includes("semantic-")||o.name.includes("primary")||o.name.includes("secondary")))!==!1}applyComponentFamilyCorrections(t,n){var o,r,i;let s=R({},t);switch(n){case"button":(o=s.states)!=null&&o.includes("hover")||(s.states=[...s.states||[],"hover","focus","active","disabled"]);break;case"avatar":!((r=s.variants)!=null&&r.size)&&!((i=s.props)!=null&&i.some(a=>a.name.includes("size")))&&(s.variants=z(R({},s.variants),{size:["small","medium","large"]}));break}return s}applyTokenConsistencyCorrections(t){return t&&R({},t)}ensureConsistentScoring(t,n){return z(R({},t),{score:t.score||0})}},cs=xt;J();function Ct(e,t,n){let s=y=>y<=.04045?y/12.92:Math.pow((y+.055)/1.055,2.4),o=s(e),r=s(t),i=s(n),a=(o*.4124564+r*.3575761+i*.1804375)/.95047,c=o*.2126729+r*.7151522+i*.072175,l=(o*.0193339+r*.119192+i*.9503041)/1.08883,d=y=>y>.008856?Math.cbrt(y):7.787*y+16/116,p=d(a),u=d(c),f=d(l);return{L:116*u-16,a:500*(p-u),b:200*(u-f)}}function ls(e,t){let{L:n,a:s,b:o}=e,{L:r,a:i,b:a}=t,c=1,l=1,d=1,p=Math.sqrt(s*s+o*o),u=Math.sqrt(i*i+a*a),f=(p+u)/2,y=Math.pow(f,7),m=.5*(1-Math.sqrt(y/(y+6103515625))),v=s*(1+m),A=i*(1+m),w=Math.sqrt(v*v+o*o),k=Math.sqrt(A*A+a*a),g=Math.atan2(o,v)*180/Math.PI,h=Math.atan2(a,A)*180/Math.PI,x=(g%360+360)%360,S=(h%360+360)%360,P=r-n,O=k-w,M;w*k===0?M=0:Math.abs(S-x)<=180?M=S-x:S-x>180?M=S-x-360:M=S-x+360;let B=2*Math.sqrt(w*k)*Math.sin(M*Math.PI/360),C=(n+r)/2,$=(w+k)/2,b;w*k===0?b=x+S:Math.abs(x-S)<=180?b=(x+S)/2:x+S<360?b=(x+S+360)/2:b=(x+S-360)/2;let T=1-.17*Math.cos((b-30)*Math.PI/180)+.24*Math.cos(2*b*Math.PI/180)+.32*Math.cos((3*b+6)*Math.PI/180)-.2*Math.cos((4*b-63)*Math.PI/180),L=1+.015*Math.pow(C-50,2)/Math.sqrt(20+Math.pow(C-50,2)),I=1+.045*$,Z=1+.015*$*T,q=Math.pow($,7),fe=-2*Math.sqrt(q/(q+6103515625))*Math.sin(60*Math.exp(-Math.pow((b-275)/25,2))*Math.PI/180);return Math.sqrt(Math.pow(P/(c*L),2)+Math.pow(O/(l*I),2)+Math.pow(B/(d*Z),2)+fe*(O/(l*I))*(B/(d*Z)))}async function _r(e,t,n,s=0){try{if(!(t in e))return{success:!1,message:`Node does not support ${t}`,error:`Property ${t} not found on node type ${e.type}`};let o=await figma.variables.getVariableByIdAsync(n);if(!o)return{success:!1,message:"Variable not found",error:`Could not find variable with ID: ${n}`};if(o.resolvedType!=="COLOR")return{success:!1,message:"Variable is not a color type",error:`Variable ${o.name} is of type ${o.resolvedType}, expected COLOR`};let i=[...e[t]];if(s>=i.length)return{success:!1,message:"Paint index out of range",error:`Paint index ${s} does not exist. Node has ${i.length} ${t}.`};let a=i[s];if(a.type!=="SOLID")return{success:!1,message:"Can only bind to solid paints",error:`Paint at index ${s} is of type ${a.type}, expected SOLID`};let c=figma.variables.setBoundVariableForPaint(a,"color",o);return i[s]=c,t==="fills"?e.fills=i:e.strokes=i,{success:!0,message:`Successfully bound ${o.name} to ${t}[${s}]`,appliedFix:{nodeId:e.id,nodeName:e.name,propertyPath:`${t}[${s}]`,beforeValue:a.type==="SOLID"&&a.color?F(a.color.r,a.color.g,a.color.b):"unknown",afterValue:o.name,tokenId:n,tokenName:o.name,fixType:"color"}}}catch(o){return{success:!1,message:"Failed to bind color token",error:o instanceof Error?o.message:String(o)}}}async function ds(e,t,n){try{if(!(t in e))return{success:!1,message:`Node does not support ${t}`,error:`Property ${t} not found on node type ${e.type}`};let s=await figma.variables.getVariableByIdAsync(n);if(!s)return{success:!1,message:"Variable not found",error:`Could not find variable with ID: ${n}`};if(s.resolvedType!=="FLOAT")return{success:!1,message:"Variable is not a number type",error:`Variable ${s.name} is of type ${s.resolvedType}, expected FLOAT`};let o=e[t];return e.setBoundVariable(t,s),{success:!0,message:`Successfully bound ${s.name} to ${t}`,appliedFix:{nodeId:e.id,nodeName:e.name,propertyPath:t,beforeValue:typeof o=="number"?`${o}px`:String(o),afterValue:s.name,tokenId:n,tokenName:s.name,fixType:t.includes("Radius")?"border":"spacing"}}}catch(s){return{success:!1,message:"Failed to bind spacing token",error:s instanceof Error?s.message:String(s)}}}async function It(e,t=0){try{let n=Ur(e);if(!n)return[];let s=[],o=await figma.variables.getLocalVariablesAsync("COLOR"),r=await figma.variables.getLocalVariableCollectionsAsync(),i=new Map;for(let a of r)i.set(a.id,a);for(let a of o){let c=i.get(a.variableCollectionId);if(!c)continue;let l=c.modes[0].modeId,d=a.valuesByMode[l];if(!d||typeof d!="object"||!("r"in d))continue;let p=d,u=Gr(n,p);u>=1-t&&s.push({variableId:a.id,variableName:a.name,collectionName:c.name,value:F(p.r,p.g,p.b),matchScore:u,type:"color"})}return s.sort((a,c)=>c.matchScore-a.matchScore)}catch(n){return console.error("Error finding matching color variable:",n),[]}}async function Br(e,t=0){try{let n=[],s=await figma.variables.getLocalVariablesAsync("FLOAT"),o=await figma.variables.getLocalVariableCollectionsAsync(),r=new Map;for(let i of o)r.set(i.id,i);for(let i of s){let a=r.get(i.variableCollectionId);if(!a)continue;let c=a.modes[0].modeId,l=i.valuesByMode[c];if(typeof l!="number")continue;let d=Math.abs(l-e);if(d<=t){let p=d===0?1:1-d/(t||1);n.push({variableId:i.id,variableName:i.name,collectionName:a.name,value:`${l}px`,matchScore:p,type:"number"})}}return n.sort((i,a)=>a.matchScore-i.matchScore)}catch(n){return console.error("Error finding matching spacing variable:",n),[]}}async function At(e,t,n=2){let s=await Br(e,n);if(s.length===0)return s;let r={strokeWeight:["stroke","border-width","border/width","borderwidth"],cornerRadius:["radius","corner","round","border-radius"],topLeftRadius:["radius","corner","round"],topRightRadius:["radius","corner","round"],bottomLeftRadius:["radius","corner","round"],bottomRightRadius:["radius","corner","round"],paddingTop:["padding","spacing","space"],paddingRight:["padding","spacing","space"],paddingBottom:["padding","spacing","space"],paddingLeft:["padding","spacing","space"],itemSpacing:["gap","spacing","space"],counterAxisSpacing:["gap","spacing","space"]}[t]||[];return r.length===0?s:s.map(a=>{let c=a.variableName.toLowerCase(),l=r.some(d=>c.includes(d));return z(R({},a),{matchScore:l?Math.min(a.matchScore+.3,1):a.matchScore})}).sort((a,c)=>c.matchScore-a.matchScore)}async function Et(e,t,n){let s=t.match(/^(fills|strokes)\[(\d+)\]$/);if(!s)return{success:!1,message:"Invalid property path",error:`Expected format: fills[n] or strokes[n], got: ${t}`};let[,o,r]=s,i=parseInt(r,10);return _r(e,o,n,i)}async function Tt(e,t,n){if(!["paddingTop","paddingRight","paddingBottom","paddingLeft","itemSpacing","counterAxisSpacing","cornerRadius","topLeftRadius","topRightRadius","bottomLeftRadius","bottomRightRadius","strokeWeight"].includes(t))return{success:!1,message:"Invalid property path",error:`Property ${t} is not a valid spacing property`};if(t==="cornerRadius"){let o=["topLeftRadius","topRightRadius","bottomLeftRadius","bottomRightRadius"],r=[];for(let i of o){let a=await ds(e,i,n);if(r.push(a),!a.success)return{success:!1,message:`Failed to bind ${i}`,error:a.error}}return{success:!0,message:"Successfully bound variable to all 4 corner radii",appliedFix:r[0].appliedFix?z(R({},r[0].appliedFix),{propertyPath:"cornerRadius"}):void 0}}return ds(e,t,n)}async function Pt(e,t,n){try{let s=await figma.variables.getVariableByIdAsync(n);if(!s)return null;let o,r,i=t.match(/^(fills|strokes)\[(\d+)\]$/);if(i){o="color";let[,l,d]=i,p=parseInt(d,10);if(!(l in e))return null;let f=e[l];if(p>=f.length)return null;let y=f[p];y.type==="SOLID"&&y.color?r=F(y.color.r,y.color.g,y.color.b):r=y.type}else{if(!(t in e))return null;let l=e[t];r=typeof l=="number"?`${l}px`:String(l),o=t.includes("Radius")?"border":"spacing"}let a=s.name,c=await figma.variables.getVariableCollectionByIdAsync(s.variableCollectionId);if(c){let l=c.modes[0].modeId,d=s.valuesByMode[l];if(typeof d=="number")a=`${s.name} (${d}px)`;else if(d&&typeof d=="object"&&"r"in d){let p=d;a=`${s.name} (${F(p.r,p.g,p.b)})`}}return{nodeId:e.id,nodeName:e.name,propertyPath:t,beforeValue:r,afterValue:a,tokenId:n,tokenName:s.name,fixType:o}}catch(s){return console.error("Error generating fix preview:",s),null}}function Ur(e){let t=e.replace(/^#/,""),n=t;if(t.length===3&&(n=t[0]+t[0]+t[1]+t[1]+t[2]+t[2]),n.length!==6)return null;let s=parseInt(n.substring(0,2),16),o=parseInt(n.substring(2,4),16),r=parseInt(n.substring(4,6),16);return isNaN(s)||isNaN(o)||isNaN(r)?null:{r:s/255,g:o/255,b:r/255}}function Gr(e,t){let n=Ct(e.r,e.g,e.b),s=Ct(t.r,t.g,t.b),o=ls(n,s);return o<3?1:o>=10?0:1-(o-3)/7}async function Lt(e,t=1024){let n=Math.max(1,Math.min(t,Math.round(e.width))),s=await e.exportAsync({format:"PNG",constraint:{type:"WIDTH",value:n}});return zr(s)}function zr(e){let t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",n="",s=e.length;for(let o=0;o<s;o+=3){let r=e[o],i=o+1<s?e[o+1]:0,a=o+2<s?e[o+2]:0;n+=t[r>>2],n+=t[(r&3)<<4|i>>4],n+=o+1<s?t[(i&15)<<2|a>>6]:"=",n+=o+2<s?t[a&63]:"="}return n}var Wr=/button|btn|cta|link|tab|nav|menu|input|checkbox|toggle|switch|radio|select|dropdown|slider/i;function us(e){if(Wr.test(e.name))return!0;if("children"in e){for(let t of e.children)if(us(t))return!0}return!1}function ps(e,t,n){var s,o;if("reactions"in e){let r=e.reactions;if(r&&r.length>0)for(let i of r){let a=i.actions||(i.action?[i.action]:[]);for(let c of a)c.type==="NODE"&&c.destinationId&&n.push({sourceFrameId:t,sourceNodeId:e.id,sourceNodeName:e.name,destinationFrameId:c.destinationId,trigger:((s=i.trigger)==null?void 0:s.type)||"UNKNOWN",navigation:c.navigation||"NAVIGATE",hasTransition:!!c.transition}),c.type==="BACK"&&n.push({sourceFrameId:t,sourceNodeId:e.id,sourceNodeName:e.name,destinationFrameId:"__BACK__",trigger:((o=i.trigger)==null?void 0:o.type)||"UNKNOWN",navigation:"BACK",hasTransition:!!c.transition})}}if("children"in e)for(let r of e.children)ps(r,t,n)}function ms(e){var B,C,$;let t=e||figma.currentPage,n=t.children.filter(b=>b.type==="FRAME"||b.type==="COMPONENT"),s=new Set((t.flowStartingPoints||[]).map(b=>b.nodeId)),o=n.map(b=>({id:b.id,name:b.name,pageId:t.id,pageName:t.name,width:b.width,height:b.height,isFlowStartingPoint:s.has(b.id),childCount:"children"in b?b.children.length:0,hasInteractiveElements:us(b)})),r=new Set(o.map(b=>b.id)),i=[];for(let b of n)ps(b,b.id,i);let a=i.filter(b=>b.destinationFrameId==="__BACK__"||r.has(b.destinationFrameId)),c=o.filter(b=>b.isFlowStartingPoint).map(b=>b.id),l=new Map,d=new Map;for(let b of r)l.set(b,new Set),d.set(b,new Set);for(let b of a)b.destinationFrameId!=="__BACK__"&&((B=l.get(b.sourceFrameId))==null||B.add(b.destinationFrameId),(C=d.get(b.destinationFrameId))==null||C.add(b.sourceFrameId));let p=new Set;for(let b of a)b.destinationFrameId==="__BACK__"&&p.add(b.sourceFrameId);let u=o.filter(b=>{var T;return(((T=l.get(b.id))==null?void 0:T.size)||0)===0&&!p.has(b.id)}).map(b=>b.id),f=o.filter(b=>{var T;return(((T=d.get(b.id))==null?void 0:T.size)||0)===0&&!s.has(b.id)}).map(b=>b.id),y=new Set,m=[...c];if(m.length===0)for(let b of o)((($=d.get(b.id))==null?void 0:$.size)||0)===0&&m.push(b.id);for(;m.length>0;){let b=m.shift();if(y.has(b))continue;y.add(b);let T=l.get(b);if(T)for(let L of T)y.has(L)||m.push(L)}let v=o.filter(b=>!y.has(b.id)).map(b=>b.id),A=[],w=new Set,k=new Set,g=[];function h(b){if(k.has(b)){let L=g.indexOf(b);L!==-1&&A.push(g.slice(L));return}if(w.has(b))return;w.add(b),k.add(b),g.push(b);let T=l.get(b);if(T)for(let L of T)h(L);g.pop(),k.delete(b)}for(let b of r)h(b);let x=o.map(b=>{var T;return((T=l.get(b.id))==null?void 0:T.size)||0}),S=x.length>0?x.reduce((b,T)=>b+T,0)/x.length:0,P=0,O=c.map(b=>({id:b,depth:0})),M=new Set;for(;O.length>0;){let{id:b,depth:T}=O.shift();if(M.has(b))continue;M.add(b),T>P&&(P=T);let L=l.get(b);if(L)for(let I of L)M.has(I)||O.push({id:I,depth:T+1})}return{frames:o,edges:a,entryPoints:c,deadEnds:u,orphans:f,unreachable:v,loops:A,stats:{totalFrames:o.length,totalEdges:a.length,totalEntryPoints:c.length,maxDepth:P,avgBranching:Math.round(S*100)/100}}}function fs(e){let t=[],n=new Map(e.frames.map(i=>[i.id,i.name])),s=i=>i.map(a=>`"${n.get(a)||a}"`).join(", ");for(let i of e.deadEnds){let a=n.get(i)||"";/success|confirm|done|complete|thank|receipt|summary/i.test(a)||t.push({type:"dead-end",severity:"warning",frameIds:[i],message:`${s([i])} has no outgoing connections \u2014 user gets stuck here.`})}e.orphans.length>0&&t.push({type:"orphan",severity:"warning",frameIds:e.orphans,message:`${s(e.orphans)} ${e.orphans.length===1?"has":"have"} no incoming connections \u2014 unreachable by navigation.`});let o=e.unreachable.filter(i=>!e.orphans.includes(i));o.length>0&&t.push({type:"unreachable",severity:"critical",frameIds:o,message:`${s(o)} ${o.length===1?"is":"are"} not reachable from any flow entry point.`});for(let i of e.loops){let a=new Set(i);i.some(l=>e.edges.filter(p=>p.sourceFrameId===l).some(p=>!a.has(p.destinationFrameId)))||t.push({type:"loop",severity:"warning",frameIds:i,message:`Circular flow without exit: ${s(i)}. User cannot leave this loop.`})}e.stats.maxDepth>3&&t.push({type:"deep-navigation",severity:"info",frameIds:[],message:`Navigation depth is ${e.stats.maxDepth} levels. Consider flattening to \u22643 levels for better UX (3-click rule).`});let r=e.frames.filter(i=>{if(i.isFlowStartingPoint)return!1;let a=e.edges.some(l=>l.sourceFrameId===i.id&&(l.navigation==="BACK"||l.navigation==="CLOSE"));return e.edges.some(l=>l.destinationFrameId===i.id)&&!a});return r.length>0&&t.push({type:"missing-back",severity:"info",frameIds:r.map(i=>i.id),message:`${r.length} frame${r.length===1?"":"s"} missing back/close navigation: ${s(r.map(i=>i.id))}.`}),t}J();function ys(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if("fills"in e){let o=e.fills;if(o!==figma.mixed&&Array.isArray(o))for(let r of o)r.type==="SOLID"&&r.visible!==!1&&t.colors.add(F(r.color.r,r.color.g,r.color.b))}if("strokes"in e){let o=e.strokes;if(Array.isArray(o))for(let r of o)r.type==="SOLID"&&r.visible!==!1&&t.colors.add(F(r.color.r,r.color.g,r.color.b))}if(e.type==="TEXT"){let o=e;o.fontName!==figma.mixed&&t.fontFamilies.add(o.fontName.family),o.fontSize!==figma.mixed&&t.fontSizes.add(o.fontSize)}if("layoutMode"in e&&e.layoutMode!=="NONE"){let o=e;typeof o.itemSpacing=="number"&&t.spacingValues.add(o.itemSpacing),typeof o.paddingTop=="number"&&t.spacingValues.add(o.paddingTop),typeof o.paddingBottom=="number"&&t.spacingValues.add(o.paddingBottom),typeof o.paddingLeft=="number"&&t.spacingValues.add(o.paddingLeft),typeof o.paddingRight=="number"&&t.spacingValues.add(o.paddingRight)}if(e.type==="INSTANCE"){let o=e.mainComponent;o&&t.componentNames.add(o.name)}if("children"in e)for(let o of e.children)ys(o,t,n,s)}}function gs(e,t){let n=new Set;for(let s of e)t.has(s)||n.add(s);return n}function hs(e,t={}){var f,y;let n=(f=t.skipLocked)!=null?f:!0,s=(y=t.skipHidden)!=null?y:!0,o=[];if(e.length<2)return o;let r=e.map(({frame:m,node:v})=>{let A={frameId:m.id,frameName:m.name,colors:new Set,fontFamilies:new Set,fontSizes:new Set,spacingValues:new Set,componentNames:new Set};return ys(v,A,n,s),A}),i=new Map;for(let m of r)for(let v of m.colors)i.set(v,(i.get(v)||0)+1);let a=r.length*.5,c=new Set;for(let[m,v]of i)v>=a&&c.add(m);for(let m of r){let v=gs(m.colors,c);v.size>3&&o.push({type:"dead-end",severity:"warning",frameIds:[m.frameId],message:`"${m.frameName}" uses ${v.size} colors not found in other screens (${[...v].slice(0,3).join(", ")}${v.size>3?"...":""}). Check for color inconsistency.`})}let l=new Set;for(let m of r)for(let v of m.fontFamilies)l.add(v);if(l.size>3){let m=[...l].join(", ");o.push({type:"dead-end",severity:"warning",frameIds:r.map(v=>v.frameId),message:`${l.size} different font families across flow: ${m}. Flows should use 1-2 font families for consistency.`})}for(let m of r){let v=new Set;for(let w of r)if(w.frameId!==m.frameId)for(let k of w.fontFamilies)v.add(k);let A=gs(m.fontFamilies,v);A.size>0&&r.length>2&&o.push({type:"dead-end",severity:"info",frameIds:[m.frameId],message:`"${m.frameName}" uses font${A.size>1?"s":""} not seen elsewhere: ${[...A].join(", ")}.`})}let d=new Set;for(let m of r)for(let v of m.fontSizes)d.add(v);d.size>10&&o.push({type:"dead-end",severity:"info",frameIds:r.map(m=>m.frameId),message:`${d.size} unique font sizes across the flow. Consider using a type scale with fewer sizes for consistency.`});let p=new Set;for(let m of r)for(let v of m.spacingValues)v>0&&p.add(v);let u=[...p].filter(m=>m%4!==0&&m!==2);return u.length>3&&o.push({type:"dead-end",severity:"info",frameIds:r.map(m=>m.frameId),message:`${u.length} non-standard spacing values across flow (${u.slice(0,4).join(", ")}px). Consider aligning to a 4px/8px grid.`}),o}var _e=Hs(bs()),vs=8e4,Ss="baseline::";function Be(e){return`${Ss}${e}::meta`}function $t(e,t){return`${Ss}${e}::chunk_${t}`}function Ns(e){let t=JSON.stringify(e),n=(0,_e.compressToUTF16)(t),s=[];for(let r=0;r<n.length;r+=vs)s.push(n.slice(r,r+vs));let o=e.nodeId;Cs(o),figma.root.setPluginData(Be(o),JSON.stringify({chunkCount:s.length,timestamp:e.timestamp,nodeName:e.nodeName,overall:e.overall}));for(let r=0;r<s.length;r++)figma.root.setPluginData($t(o,r),s[r])}function ws(e){let t=figma.root.getPluginData(Be(e));if(!t)return null;let n;try{n=JSON.parse(t)}catch(i){return null}let s=[];for(let i=0;i<n.chunkCount;i++){let a=figma.root.getPluginData($t(e,i));if(!a)return null;s.push(a)}let o=s.join(""),r=(0,_e.decompressFromUTF16)(o);if(!r)return null;try{return JSON.parse(r)}catch(i){return null}}function ks(e){Cs(e),figma.root.setPluginData(Be(e),"")}function xs(e){let t=figma.root.getPluginData(Be(e));if(!t)return null;try{return JSON.parse(t)}catch(n){return null}}function Cs(e){for(let t=0;t<100;t++){let n=$t(e,t);if(!figma.root.getPluginData(n))break;figma.root.setPluginData(n,"")}}function Ue(e){return`${e.errorType}::${e.nodeId}::${e.message}`}function Is(e,t){var d,p,u,f;let n=Date.now(),s=new Set([...Object.keys(e.categories),...Object.keys(t.categories)]),o=[];for(let y of s){let m=(p=(d=e.categories[y])==null?void 0:d.score)!=null?p:100,v=(f=(u=t.categories[y])==null?void 0:u.score)!=null?f:100;o.push({category:y,oldScore:m,newScore:v,delta:v-m})}o.sort((y,m)=>Math.abs(m.delta)-Math.abs(y.delta));let r=new Set(e.errors.map(Ue)),i=new Set(t.errors.map(Ue)),a=[],c=[],l=[];for(let y of t.errors){let m=Ue(y);r.has(m)?l.push({errorType:y.errorType,severity:y.severity,nodeId:y.nodeId,message:y.message}):a.push({errorType:y.errorType,severity:y.severity,nodeId:y.nodeId,message:y.message})}for(let y of e.errors){let m=Ue(y);i.has(m)||c.push({errorType:y.errorType,severity:y.severity,nodeId:y.nodeId,message:y.message})}return{baselineTimestamp:e.timestamp,currentTimestamp:n,scoreDelta:{overall:t.overall-e.overall,oldOverall:e.overall,newOverall:t.overall,categories:o},newIssues:a,fixedIssues:c,remainingIssues:l,summary:{totalNew:a.length,totalFixed:c.length,totalRemaining:l.length,oldTotal:e.errors.length,newTotal:t.errors.length}}}at();var Kr=["itemSpacing","paddingTop","paddingBottom","paddingLeft","paddingRight","counterAxisSpacing"];function Mt(e,t,n){let s=figma.getNodeById(e);if(!s)return{success:!1,nodeId:e,nodeName:"",property:t,oldValue:0,newValue:n,error:"Node not found"};if(s.type!=="FRAME"&&s.type!=="COMPONENT"&&s.type!=="INSTANCE")return{success:!1,nodeId:e,nodeName:s.name,property:t,oldValue:0,newValue:n,error:"Node is not a frame"};let o=s;if(o.layoutMode==="NONE")return{success:!1,nodeId:e,nodeName:s.name,property:t,oldValue:0,newValue:n,error:"Frame has no auto-layout"};try{let r=o[t];return o[t]=n,{success:!0,nodeId:e,nodeName:s.name,property:t,oldValue:r,newValue:n}}catch(r){return{success:!1,nodeId:e,nodeName:s.name,property:t,oldValue:0,newValue:n,error:r instanceof Error?r.message:String(r)}}}function ke(e,t){let n=figma.getNodeById(e);if(!n||n.type!=="FRAME"&&n.type!=="COMPONENT"&&n.type!=="INSTANCE")return{success:!1,nodeId:e,nodeName:(n==null?void 0:n.name)||"",property:t,oldValue:0,newValue:0,error:"Invalid node"};let o=n[t];if(typeof o!="number")return{success:!1,nodeId:e,nodeName:n.name,property:t,oldValue:0,newValue:0,error:"Property is not a number"};if(it.includes(o))return{success:!0,nodeId:e,nodeName:n.name,property:t,oldValue:o,newValue:o};let r=Re(o);if(r.length===0)return{success:!1,nodeId:e,nodeName:n.name,property:t,oldValue:o,newValue:o,error:"No suggestion found"};let i=r.reduce((a,c)=>Math.abs(a-o)<=Math.abs(c-o)?a:c);return Mt(e,t,i)}function As(e){let t=figma.getNodeById(e);if(!t||t.type!=="FRAME"&&t.type!=="COMPONENT"&&t.type!=="INSTANCE")return[];let n=t;if(n.layoutMode==="NONE")return[];let s=[];for(let o of Kr){if(!(o in n))continue;let r=n[o];if(typeof r!="number"||it.includes(r))continue;let i=ke(e,o);s.push(i)}return s}function xe(e,t){return t.length===0?e:t.reduce((n,s)=>Math.abs(s-e)<Math.abs(n-e)?s:n)}function Ge(e,t){let n=figma.getNodeById(e);if(!n)return{success:!1,nodeId:e,nodeName:"",oldValue:"",newValue:"",error:"Node not found"};if(!("cornerRadius"in n))return{success:!1,nodeId:e,nodeName:n.name,oldValue:"",newValue:"",error:"Node has no corner radius"};let s=n;try{let o=s.cornerRadius;if(o===figma.mixed){let r=s.topLeftRadius,i=s.topRightRadius,a=s.bottomLeftRadius,c=s.bottomRightRadius,l=`${r}/${i}/${c}/${a}`;s.topLeftRadius=xe(r,t),s.topRightRadius=xe(i,t),s.bottomLeftRadius=xe(a,t),s.bottomRightRadius=xe(c,t);let d=`${s.topLeftRadius}/${s.topRightRadius}/${s.bottomRightRadius}/${s.bottomLeftRadius}`;return{success:!0,nodeId:e,nodeName:n.name,oldValue:l,newValue:d}}else{let r=`${o}`,i=xe(o,t);return s.cornerRadius=i,{success:!0,nodeId:e,nodeName:n.name,oldValue:r,newValue:`${i}`}}}catch(o){return{success:!1,nodeId:e,nodeName:n.name,oldValue:"",newValue:"",error:o instanceof Error?o.message:String(o)}}}function ze(e,t){let n=figma.getNodeById(e);if(!n||n.type==="DOCUMENT"||n.type==="PAGE")return{success:!1,nodeId:e,oldName:"",newName:t,error:"Node not found"};try{let s=n.name;return n.name=t,{success:!0,nodeId:e,oldName:s,newName:t}}catch(s){return{success:!1,nodeId:e,oldName:n.name,newName:t,error:s instanceof Error?s.message:String(s)}}}_t();async function Ts(e){let t=0,n=0,s=[];for(let o=0;o<e.length;o++){let r=e[o];try{let i=await Hr(r);s.push(R({index:o},i)),i.success?t++:n++}catch(i){n++,s.push({index:o,type:r.type,success:!1,nodeId:String(r.params.nodeId||""),nodeName:"",message:"Unexpected error",error:i instanceof Error?i.message:String(i)})}}return{total:e.length,applied:t,failed:n,results:s}}async function Hr(e){let{type:t,params:n}=e;switch(t){case"applyStyle":{let s=n.styleType,o=n.nodeId,r=n.styleKey,i;switch(s){case"fill":i=await Ot(o,r);break;case"stroke":i=await Ft(o,r);break;case"text":i=await Dt(o,r);break;case"effect":i=await Vt(o,r);break;default:return{type:t,success:!1,nodeId:o,nodeName:"",message:`Unknown style type: ${s}`}}return{type:t,success:i.success,nodeId:i.nodeId,nodeName:i.nodeName,message:i.success?`Applied ${i.property}: ${i.newValue}`:i.error||"Failed",oldValue:i.oldValue,newValue:i.newValue,error:i.error}}case"fixSpacing":{let s=n.nodeId,o=n.property,r=n.value,i=Mt(s,o,r);return{type:t,success:i.success,nodeId:i.nodeId,nodeName:i.nodeName,message:i.success?`${i.property}: ${i.oldValue}px \u2192 ${i.newValue}px`:i.error||"Failed",oldValue:`${i.oldValue}px`,newValue:`${i.newValue}px`,error:i.error}}case"fixSpacingToNearest":{let s=n.nodeId,o=n.property,r=ke(s,o);return{type:t,success:r.success,nodeId:r.nodeId,nodeName:r.nodeName,message:r.success?`${r.property}: ${r.oldValue}px \u2192 ${r.newValue}px`:r.error||"Failed",oldValue:`${r.oldValue}px`,newValue:`${r.newValue}px`,error:r.error}}case"fixRadiusToNearest":{let s=n.nodeId,o=n.allowedRadii,r=Ge(s,o);return{type:t,success:r.success,nodeId:r.nodeId,nodeName:r.nodeName,message:r.success?`radius: ${r.oldValue}px \u2192 ${r.newValue}px`:r.error||"Failed",oldValue:`${r.oldValue}px`,newValue:`${r.newValue}px`,error:r.error}}case"renameLayer":{let s=n.nodeId,o=n.newName,r=ze(s,o);return{type:t,success:r.success,nodeId:r.nodeId,nodeName:r.newName,message:r.success?`Renamed "${r.oldName}" \u2192 "${r.newName}"`:r.error||"Failed",oldValue:r.oldName,newValue:r.newName,error:r.error}}default:return{type:t,success:!1,nodeId:"",nodeName:"",message:`Unknown fix type: ${t}`}}}Fe();var Q=null,G="claude-sonnet-4-5-20250929",V="anthropic";function Ps(e,t=V){let n=(e==null?void 0:e.trim())||"";switch(t){case"anthropic":return n.startsWith("sk-ant-")&&n.length>=40;case"openai":return n.startsWith("sk-")&&n.length>=20;case"google":return n.startsWith("AIza")&&n.length>=35;default:return!1}}var Ls=null,Rs=null,Y=new cs({enableCaching:!0,enableMCPIntegration:!0,mcpServerUrl:"https://design-systems-mcp.southleft-llc.workers.dev/mcp"});async function $s(e){let{type:t,data:n}=e,s=t==="save-api-key"?`${t} [redacted]`:t;console.log("Received message:",s);try{switch(t){case"check-api-key":await jr();break;case"save-api-key":await qr(n.apiKey,n.model,n.provider);break;case"update-model":await Jr(n.model);break;case"analyze":await Xr();break;case"analyze-enhanced":await Ms(n);break;case"clear-api-key":await Qr();break;case"chat-message":await Zr(n);break;case"chat-clear-history":await ei();break;case"select-node":await ti(n);break;case"preview-fix":await Ri(n);break;case"apply-token-fix":await $i(n);break;case"apply-naming-fix":await Mi(n);break;case"apply-batch-fix":await Oi(n);break;case"update-description":await Fi(n);break;case"add-component-property":await Di(n);break;case"run-design-lint":Ie(n);break;case"lint-ignore-node":ai(n);break;case"lint-ignore-error":ci(n);break;case"lint-ignore-all-of-type":li(n);break;case"lint-clear-ignored":di();break;case"lint-select-node":ui(n);break;case"lint-select-all-with-value":pi(n);break;case"lint-save-settings":mi(n);break;case"lint-load-settings":fi();break;case"lint-save-team-config":gi(n);break;case"lint-load-team-config":yi();break;case"jump-to-node":hi(n);break;case"fix-spacing":vi(n);break;case"fix-spacing-to-nearest":Ni(n);break;case"fix-all-spacing":ki(n);break;case"apply-style-fix":await xi(n);break;case"rename-layer-fix":Ci(n);break;case"fix-radius-to-nearest":wi(n);break;case"batch-fix-v2":await Ii(n);break;case"rescan-lint":Os();break;case"export-screenshot":await Si(n);break;case"analyze-flow":await Ai();break;case"save-baseline":Ei(n);break;case"load-baseline":Ti(n);break;case"compare-baseline":Pi(n);break;case"delete-baseline":Li(n);break;default:console.warn("Unknown message type:",t)}}catch(o){console.error("Error handling message:",o);let r=o instanceof Error?o.message:"Unknown error occurred";N("analysis-error",{error:r})}}async function jr(){try{await nt();let e=await st();if(V=e.providerId,G=e.modelId,Q){N("api-key-status",{hasKey:!0,provider:V,model:G});return}e.apiKey&&Ps(e.apiKey,e.providerId)?(Q=e.apiKey,N("api-key-status",{hasKey:!0,provider:V,model:G})):N("api-key-status",{hasKey:!1,provider:V,model:G})}catch(e){console.error("Error checking API key:",e),N("api-key-status",{hasKey:!1,provider:"anthropic"})}}async function qr(e,t,n){try{let s=n||V;if(!Ps(e,s)){let r=re(s);throw new Error(`Invalid API key format for ${r.name}. Expected format: ${r.keyPlaceholder}`)}V=s,Q=e,t&&(G=t),await ot(s,G,e),console.log(`${s} API key and model saved successfully`);let o=re(s);N("api-key-saved",{success:!0,provider:s}),figma.notify(`${o.name} API key saved successfully`,{timeout:2e3})}catch(s){console.error("Error saving API key:",s);let o=s instanceof Error?s.message:"Unknown error occurred";N("api-key-saved",{success:!1,error:o}),figma.notify(`Failed to save API key: ${o}`,{error:!0})}}async function Jr(e){try{G=e,await ot(V,e),console.log("Model updated to:",e),figma.notify(`Model updated to ${e}`,{timeout:2e3})}catch(t){console.error("Error updating model:",t),figma.notify("Failed to update model",{error:!0})}}async function Ms(e){var t,n;try{if(!Q){let c=re(V).name;throw new Error(`API key not found. Please save your ${c} API key first.`)}let s=figma.currentPage.selection;if(s.length===0)throw new Error("No component selected. Please select a Figma component to analyze.");if(e.batchMode&&s.length>1){await Yr(s,e);return}let o=s[0];if(o.type==="INSTANCE"){let c=o;try{let l=await c.getMainComponentAsync();if(l)figma.notify("Analyzing main component instead of instance...",{timeout:2e3}),o=l;else throw new Error("This instance has no main component. Please select a component directly.")}catch(l){throw console.error("Error accessing main component:",l),new Error("Could not access main component. Please select a component directly.")}}if(o.type==="COMPONENT"&&((t=o.parent)==null?void 0:t.type)==="COMPONENT_SET"){let l=o.parent;figma.notify("Analyzing parent component set to include all variants...",{timeout:2e3}),o=l}if(!ge(o)){let c=new Set(["COMPONENT_SET","COMPONENT","INSTANCE"]),l=null,d=null,p=o.parent;for(;p&&"type"in p;){let f=p;if(c.has(f.type)&&!l){l=f;break}!d&&ge(f)&&(d=f),p=p.parent}let u=l||d;u&&(figma.notify(`Analyzing parent ${u.type.toLowerCase()} "${u.name}"...`,{timeout:2e3}),o=u)}if(o.type==="INSTANCE"){let c=o;try{let l=await c.getMainComponentAsync();l&&(figma.notify("Analyzing main component instead of instance...",{timeout:2e3}),o=l)}catch(l){}}if(o.type==="COMPONENT"&&((n=o.parent)==null?void 0:n.type)==="COMPONENT_SET"){let c=o.parent;figma.notify("Analyzing parent component set to include all variants...",{timeout:2e3}),o=c}if(!ge(o))throw new Error("Please select a Frame, Component, Component Set, or Instance to analyze");await Y.loadDesignSystemsKnowledge();let r=await wt(o),i=R({enableMCPEnhancement:!0,batchMode:e.batchMode||!1,enableAudit:e.enableAudit!==!1,includeTokenAnalysis:e.includeTokenAnalysis!==!1},e);figma.notify("Performing enhanced analysis with design systems knowledge...",{timeout:3e3});let a=await is(r,Q,G,i,V);Ls=a.metadata,Rs=o,N("enhanced-analysis-result",z(R({},a),{analyzedNodeId:o.id})),figma.notify("Enhanced analysis complete! Check the results panel.",{timeout:3e3})}catch(s){console.error("Error during enhanced analysis:",s);let o=s instanceof Error?s.message:"Unknown error occurred";figma.notify(`Analysis failed: ${o}`,{error:!0}),N("analysis-error",{error:o})}}async function Xr(){await Ms({batchMode:!1})}async function Yr(e,t){let n=[];await Y.loadDesignSystemsKnowledge();for(let r of e)if(ge(r))try{let i=await wt(r),a=await ce(r),c=[...a.colors,...a.spacing,...a.typography,...a.effects,...a.borders],l=Y.generateComponentHash(i,c,D),d=Y.getCachedAnalysis(l);if(d){console.log(`\u2705 Using cached analysis for ${r.name}`),n.push({node:r.name,success:!0,data:d.result.metadata,cached:!0});continue}let p=Y.createDeterministicPrompt(i),u=await ie(V,Q,{prompt:p,model:G,maxTokens:2048,temperature:.1}),f=le(u.content),y=Pe(f),m=await kt(y,i,{batchMode:!0});Y.validateAnalysisConsistency(m,i)||(m=Y.applyConsistencyCorrections(m,i)),Y.cacheAnalysis(l,m),n.push({node:r.name,success:!0,data:m.metadata,cached:!1})}catch(i){n.push({node:r.name,success:!1,error:i instanceof Error?i.message:"Analysis failed"})}let s=n.filter(r=>r.success&&r.cached).length,o=n.filter(r=>r.success&&!r.cached).length;N("batch-analysis-result",{results:n}),figma.notify(`Batch analysis complete: ${o} analyzed, ${s} from cache`,{timeout:3e3})}async function Qr(){try{Q=null,await qt(V),await figma.clientStorage.setAsync("claude-api-key","");let e=re(V).name;N("api-key-cleared",{success:!0}),figma.notify(`${e} API key cleared`,{timeout:2e3})}catch(e){console.error("Error clearing API key:",e)}}async function Zr(e){try{if(console.log("Processing chat message:",e.message),!Q){let i=re(V).name;throw new Error(`API key not found. Please save your ${i} API key first.`)}N("chat-response-loading",{isLoading:!0});let t=ri(),n=await oi(e.message),s=ii(e.message,n,e.history,t),r={message:(await ie(V,Q,{prompt:s,model:G,maxTokens:2048,temperature:.7})).content,sources:n.sources||[]};N("chat-response",{response:r})}catch(t){console.error("Error handling chat message:",t);let n=t instanceof Error?t.message:"Unknown error occurred";N("chat-error",{error:n})}}async function ei(){try{N("chat-history-cleared",{success:!0}),figma.notify("Chat history cleared",{timeout:2e3})}catch(e){console.error("Error clearing chat history:",e)}}async function ti(e){try{console.log("\u{1F3AF} Attempting to select node:",e.nodeId);let t=await figma.getNodeByIdAsync(e.nodeId);if(!t){console.warn("\u26A0\uFE0F Node not found:",e.nodeId),figma.notify("Node not found - it may have been deleted or moved",{error:!0});return}if(!ni(t)){console.warn("\u26A0\uFE0F Node is not on current page:",e.nodeId),figma.notify("Node is on a different page",{error:!0});return}figma.currentPage.selection=[t],figma.viewport.scrollAndZoomIntoView([t]),console.log("\u2705 Successfully selected and zoomed to node:",t.name),figma.notify(`Selected "${t.name}"`,{timeout:2e3})}catch(t){console.error("Error selecting node:",t);let n=t instanceof Error?t.message:"Unknown error occurred";figma.notify(`Failed to select node: ${n}`,{error:!0})}}function ni(e){try{let t=e,n=50,s=0;for(;t&&t.parent&&s<n;)if(t=t.parent,s++,t===figma.currentPage)return!0;if(t===figma.currentPage||e.parent===figma.currentPage)return!0;let o=figma.currentPage;return e.type==="COMPONENT"||e.type==="COMPONENT_SET"?si(o,e.id):!1}catch(t){return console.warn("Error checking node page:",t),!1}}function si(e,t){try{return e.findAll().some(s=>s.id===t)}catch(n){return!1}}async function oi(e){var t;try{console.log("\u{1F50D} Querying MCP for chat:",e);let n=((t=Y.config)==null?void 0:t.mcpServerUrl)||"https://design-systems-mcp.southleft-llc.workers.dev/mcp",s=[Bt(n,e,{category:"general",limit:3}),e.toLowerCase().includes("component")?Bt(n,e,{category:"components",limit:2}):Promise.resolve({results:[]}),e.toLowerCase().includes("token")||e.toLowerCase().includes("design token")?Bt(n,e,{category:"tokens",limit:2}):Promise.resolve({results:[]})],o=await Promise.allSettled(s),r=[];return o.forEach(i=>{i.status==="fulfilled"&&i.value.results&&r.push(...i.value.results)}),console.log(`\u2705 Found ${r.length} relevant sources for chat query`),{sources:r.slice(0,5)}}catch(n){return console.warn("\u26A0\uFE0F MCP query failed for chat:",n),{sources:[]}}}async function Bt(e,t,n={}){let s={jsonrpc:"2.0",id:Math.floor(Math.random()*1e3)+100,method:"tools/call",params:{name:"search_design_knowledge",arguments:R({query:t,limit:n.limit||5},n.category&&{category:n.category})}},o=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!o.ok)throw new Error(`MCP search failed: ${o.status}`);let r=await o.json();return r.result&&r.result.content?{results:r.result.content.map(i=>({title:i.title||"Design System Knowledge",content:i.content||i.description||"",category:i.category||"general"}))}:{results:[]}}function ri(){try{let e=Ls,t=Rs;if(!e&&!t)return null;let n={hasCurrentComponent:!0,timestamp:Date.now()};if(t){n.component={name:t.name,type:t.type,id:t.id};let s=figma.currentPage.selection;s.length>0&&(n.selection={count:s.length,types:s.map(o=>o.type),names:s.map(o=>o.name)})}return e&&(n.analysis={component:e.component,description:e.description,props:e.props||[],states:e.states||[],accessibility:e.accessibility,audit:e.audit,mcpReadiness:e.mcpReadiness}),n}catch(e){return console.warn("Failed to get component context:",e),null}}function ii(e,t,n,s){let o="";n.length>0&&(o=` + `}loadFallbackKnowledge(){this.designSystemsKnowledge={version:"1.0.0-fallback",components:{button:"Button components require comprehensive state management",avatar:"Avatar components should support size variants and interactive states",card:"Card components need consistent spacing and content hierarchy",badge:"Badge components should use semantic colors for status indication",input:"Input components require comprehensive accessibility and validation",generic:"Generic components should follow basic design system principles"},tokens:"Use semantic token naming: semantic-color-primary, spacing-md-16px, text-size-lg-18px",accessibility:"Ensure WCAG 2.1 AA compliance with proper ARIA labels and keyboard support",scoring:this.getFallbackScoringCriteria(),lastUpdated:Date.now()}}isValidScore(t){return typeof t=="number"&&t>=0&&t<=100}validateComponentFamilyConsistency(t,n){let s=t.metadata;switch(n){case"button":return this.validateButtonComponent(s);case"avatar":return this.validateAvatarComponent(s);case"input":return this.validateInputComponent(s);default:return!0}}validateButtonComponent(t){var s;return((s=t.states)==null?void 0:s.some(o=>["hover","focus","active","disabled"].includes(o.toLowerCase())))||!1}validateAvatarComponent(t){var o,r,i;let n=((r=(o=t.variants)==null?void 0:o.size)==null?void 0:r.length)>0,s=(i=t.props)==null?void 0:i.some(a=>a.name.toLowerCase().includes("size"));return n||s||!1}validateInputComponent(t){var s;return((s=t.states)==null?void 0:s.some(o=>["focus","error","disabled","filled"].includes(o.toLowerCase())))||!1}validateTokenRecommendations(t){var s;return((s=t.colors)==null?void 0:s.some(o=>o.name.includes("semantic-")||o.name.includes("primary")||o.name.includes("secondary")))!==!1}applyComponentFamilyCorrections(t,n){var o,r,i;let s=R({},t);switch(n){case"button":(o=s.states)!=null&&o.includes("hover")||(s.states=[...s.states||[],"hover","focus","active","disabled"]);break;case"avatar":!((r=s.variants)!=null&&r.size)&&!((i=s.props)!=null&&i.some(a=>a.name.includes("size")))&&(s.variants=K(R({},s.variants),{size:["small","medium","large"]}));break}return s}applyTokenConsistencyCorrections(t){return t&&R({},t)}ensureConsistentScoring(t,n){return K(R({},t),{score:t.score||0})}},Ds=$t;J();function Mt(e,t,n){let s=f=>f<=.04045?f/12.92:Math.pow((f+.055)/1.055,2.4),o=s(e),r=s(t),i=s(n),a=(o*.4124564+r*.3575761+i*.1804375)/.95047,c=o*.2126729+r*.7151522+i*.072175,d=(o*.0193339+r*.119192+i*.9503041)/1.08883,l=f=>f>.008856?Math.cbrt(f):7.787*f+16/116,p=l(a),u=l(c),g=l(d);return{L:116*u-16,a:500*(p-u),b:200*(u-g)}}function Vs(e,t){let{L:n,a:s,b:o}=e,{L:r,a:i,b:a}=t,c=1,d=1,l=1,p=Math.sqrt(s*s+o*o),u=Math.sqrt(i*i+a*a),g=(p+u)/2,f=Math.pow(g,7),m=.5*(1-Math.sqrt(f/(f+6103515625))),h=s*(1+m),C=i*(1+m),S=Math.sqrt(h*h+o*o),N=Math.sqrt(C*C+a*a),y=Math.atan2(o,h)*180/Math.PI,b=Math.atan2(a,C)*180/Math.PI,I=(y%360+360)%360,w=(b%360+360)%360,P=r-n,O=N-S,M;S*N===0?M=0:Math.abs(w-I)<=180?M=w-I:w-I>180?M=w-I-360:M=w-I+360;let z=2*Math.sqrt(S*N)*Math.sin(M*Math.PI/360),x=(n+r)/2,$=(S+N)/2,v;S*N===0?v=I+w:Math.abs(I-w)<=180?v=(I+w)/2:I+w<360?v=(I+w+360)/2:v=(I+w-360)/2;let T=1-.17*Math.cos((v-30)*Math.PI/180)+.24*Math.cos(2*v*Math.PI/180)+.32*Math.cos((3*v+6)*Math.PI/180)-.2*Math.cos((4*v-63)*Math.PI/180),L=1+.015*Math.pow(x-50,2)/Math.sqrt(20+Math.pow(x-50,2)),A=1+.045*$,ee=1+.015*$*T,Y=Math.pow($,7),ve=-2*Math.sqrt(Y/(Y+6103515625))*Math.sin(60*Math.exp(-Math.pow((v-275)/25,2))*Math.PI/180);return Math.sqrt(Math.pow(P/(c*L),2)+Math.pow(O/(d*A),2)+Math.pow(z/(l*ee),2)+ve*(O/(d*A))*(z/(l*ee)))}async function Oi(e,t,n,s=0){try{if(!(t in e))return{success:!1,message:`Node does not support ${t}`,error:`Property ${t} not found on node type ${e.type}`};let o=await figma.variables.getVariableByIdAsync(n);if(!o)return{success:!1,message:"Variable not found",error:`Could not find variable with ID: ${n}`};if(o.resolvedType!=="COLOR")return{success:!1,message:"Variable is not a color type",error:`Variable ${o.name} is of type ${o.resolvedType}, expected COLOR`};let i=[...e[t]];if(s>=i.length)return{success:!1,message:"Paint index out of range",error:`Paint index ${s} does not exist. Node has ${i.length} ${t}.`};let a=i[s];if(a.type!=="SOLID")return{success:!1,message:"Can only bind to solid paints",error:`Paint at index ${s} is of type ${a.type}, expected SOLID`};let c=figma.variables.setBoundVariableForPaint(a,"color",o);return i[s]=c,t==="fills"?e.fills=i:e.strokes=i,{success:!0,message:`Successfully bound ${o.name} to ${t}[${s}]`,appliedFix:{nodeId:e.id,nodeName:e.name,propertyPath:`${t}[${s}]`,beforeValue:a.type==="SOLID"&&a.color?F(a.color.r,a.color.g,a.color.b):"unknown",afterValue:o.name,tokenId:n,tokenName:o.name,fixType:"color"}}}catch(o){return{success:!1,message:"Failed to bind color token",error:o instanceof Error?o.message:String(o)}}}async function _s(e,t,n){try{if(!(t in e))return{success:!1,message:`Node does not support ${t}`,error:`Property ${t} not found on node type ${e.type}`};let s=await figma.variables.getVariableByIdAsync(n);if(!s)return{success:!1,message:"Variable not found",error:`Could not find variable with ID: ${n}`};if(s.resolvedType!=="FLOAT")return{success:!1,message:"Variable is not a number type",error:`Variable ${s.name} is of type ${s.resolvedType}, expected FLOAT`};let o=e[t];return e.setBoundVariable(t,s),{success:!0,message:`Successfully bound ${s.name} to ${t}`,appliedFix:{nodeId:e.id,nodeName:e.name,propertyPath:t,beforeValue:typeof o=="number"?`${o}px`:String(o),afterValue:s.name,tokenId:n,tokenName:s.name,fixType:t.includes("Radius")?"border":"spacing"}}}catch(s){return{success:!1,message:"Failed to bind spacing token",error:s instanceof Error?s.message:String(s)}}}async function Ot(e,t=0){try{let n=Di(e);if(!n)return[];let s=[],o=await figma.variables.getLocalVariablesAsync("COLOR"),r=await figma.variables.getLocalVariableCollectionsAsync(),i=new Map;for(let a of r)i.set(a.id,a);for(let a of o){let c=i.get(a.variableCollectionId);if(!c)continue;let d=c.modes[0].modeId,l=a.valuesByMode[d];if(!l||typeof l!="object"||!("r"in l))continue;let p=l,u=Vi(n,p);u>=1-t&&s.push({variableId:a.id,variableName:a.name,collectionName:c.name,value:F(p.r,p.g,p.b),matchScore:u,type:"color"})}return s.sort((a,c)=>c.matchScore-a.matchScore)}catch(n){return console.error("Error finding matching color variable:",n),[]}}async function Fi(e,t=0){try{let n=[],s=await figma.variables.getLocalVariablesAsync("FLOAT"),o=await figma.variables.getLocalVariableCollectionsAsync(),r=new Map;for(let i of o)r.set(i.id,i);for(let i of s){let a=r.get(i.variableCollectionId);if(!a)continue;let c=a.modes[0].modeId,d=i.valuesByMode[c];if(typeof d!="number")continue;let l=Math.abs(d-e);if(l<=t){let p=l===0?1:1-l/(t||1);n.push({variableId:i.id,variableName:i.name,collectionName:a.name,value:`${d}px`,matchScore:p,type:"number"})}}return n.sort((i,a)=>a.matchScore-i.matchScore)}catch(n){return console.error("Error finding matching spacing variable:",n),[]}}async function Ft(e,t,n=2){let s=await Fi(e,n);if(s.length===0)return s;let r={strokeWeight:["stroke","border-width","border/width","borderwidth"],cornerRadius:["radius","corner","round","border-radius"],topLeftRadius:["radius","corner","round"],topRightRadius:["radius","corner","round"],bottomLeftRadius:["radius","corner","round"],bottomRightRadius:["radius","corner","round"],paddingTop:["padding","spacing","space"],paddingRight:["padding","spacing","space"],paddingBottom:["padding","spacing","space"],paddingLeft:["padding","spacing","space"],itemSpacing:["gap","spacing","space"],counterAxisSpacing:["gap","spacing","space"]}[t]||[];return r.length===0?s:s.map(a=>{let c=a.variableName.toLowerCase(),d=r.some(l=>c.includes(l));return K(R({},a),{matchScore:d?Math.min(a.matchScore+.3,1):a.matchScore})}).sort((a,c)=>c.matchScore-a.matchScore)}async function Dt(e,t,n){let s=t.match(/^(fills|strokes)\[(\d+)\]$/);if(!s)return{success:!1,message:"Invalid property path",error:`Expected format: fills[n] or strokes[n], got: ${t}`};let[,o,r]=s,i=parseInt(r,10);return Oi(e,o,n,i)}async function Vt(e,t,n){if(!["paddingTop","paddingRight","paddingBottom","paddingLeft","itemSpacing","counterAxisSpacing","cornerRadius","topLeftRadius","topRightRadius","bottomLeftRadius","bottomRightRadius","strokeWeight"].includes(t))return{success:!1,message:"Invalid property path",error:`Property ${t} is not a valid spacing property`};if(t==="cornerRadius"){let o=["topLeftRadius","topRightRadius","bottomLeftRadius","bottomRightRadius"],r=[];for(let i of o){let a=await _s(e,i,n);if(r.push(a),!a.success)return{success:!1,message:`Failed to bind ${i}`,error:a.error}}return{success:!0,message:"Successfully bound variable to all 4 corner radii",appliedFix:r[0].appliedFix?K(R({},r[0].appliedFix),{propertyPath:"cornerRadius"}):void 0}}return _s(e,t,n)}async function _t(e,t,n){try{let s=await figma.variables.getVariableByIdAsync(n);if(!s)return null;let o,r,i=t.match(/^(fills|strokes)\[(\d+)\]$/);if(i){o="color";let[,d,l]=i,p=parseInt(l,10);if(!(d in e))return null;let g=e[d];if(p>=g.length)return null;let f=g[p];f.type==="SOLID"&&f.color?r=F(f.color.r,f.color.g,f.color.b):r=f.type}else{if(!(t in e))return null;let d=e[t];r=typeof d=="number"?`${d}px`:String(d),o=t.includes("Radius")?"border":"spacing"}let a=s.name,c=await figma.variables.getVariableCollectionByIdAsync(s.variableCollectionId);if(c){let d=c.modes[0].modeId,l=s.valuesByMode[d];if(typeof l=="number")a=`${s.name} (${l}px)`;else if(l&&typeof l=="object"&&"r"in l){let p=l;a=`${s.name} (${F(p.r,p.g,p.b)})`}}return{nodeId:e.id,nodeName:e.name,propertyPath:t,beforeValue:r,afterValue:a,tokenId:n,tokenName:s.name,fixType:o}}catch(s){return console.error("Error generating fix preview:",s),null}}function Di(e){let t=e.replace(/^#/,""),n=t;if(t.length===3&&(n=t[0]+t[0]+t[1]+t[1]+t[2]+t[2]),n.length!==6)return null;let s=parseInt(n.substring(0,2),16),o=parseInt(n.substring(2,4),16),r=parseInt(n.substring(4,6),16);return isNaN(s)||isNaN(o)||isNaN(r)?null:{r:s/255,g:o/255,b:r/255}}function Vi(e,t){let n=Mt(e.r,e.g,e.b),s=Mt(t.r,t.g,t.b),o=Vs(n,s);return o<3?1:o>=10?0:1-(o-3)/7}async function ze(e,t=1024){let n=Math.max(1,Math.min(t,Math.round(e.width))),s=await e.exportAsync({format:"PNG",constraint:{type:"WIDTH",value:n}});return _i(s)}function _i(e){let t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",n="",s=e.length;for(let o=0;o<s;o+=3){let r=e[o],i=o+1<s?e[o+1]:0,a=o+2<s?e[o+2]:0;n+=t[r>>2],n+=t[(r&3)<<4|i>>4],n+=o+1<s?t[(i&15)<<2|a>>6]:"=",n+=o+2<s?t[a&63]:"="}return n}var Bi=/button|btn|cta|link|tab|nav|menu|input|checkbox|toggle|switch|radio|select|dropdown|slider/i;function Bs(e){if(Bi.test(e.name))return!0;if("children"in e){for(let t of e.children)if(Bs(t))return!0}return!1}function Us(e,t,n){var s,o;if("reactions"in e){let r=e.reactions;if(r&&r.length>0)for(let i of r){let a=i.actions||(i.action?[i.action]:[]);for(let c of a)c.type==="NODE"&&c.destinationId&&n.push({sourceFrameId:t,sourceNodeId:e.id,sourceNodeName:e.name,destinationFrameId:c.destinationId,trigger:((s=i.trigger)==null?void 0:s.type)||"UNKNOWN",navigation:c.navigation||"NAVIGATE",hasTransition:!!c.transition}),c.type==="BACK"&&n.push({sourceFrameId:t,sourceNodeId:e.id,sourceNodeName:e.name,destinationFrameId:"__BACK__",trigger:((o=i.trigger)==null?void 0:o.type)||"UNKNOWN",navigation:"BACK",hasTransition:!!c.transition})}}if("children"in e)for(let r of e.children)Us(r,t,n)}function Gs(e){var z,x,$;let t=e||figma.currentPage,n=t.children.filter(v=>v.type==="FRAME"||v.type==="COMPONENT"),s=new Set((t.flowStartingPoints||[]).map(v=>v.nodeId)),o=n.map(v=>({id:v.id,name:v.name,pageId:t.id,pageName:t.name,width:v.width,height:v.height,isFlowStartingPoint:s.has(v.id),childCount:"children"in v?v.children.length:0,hasInteractiveElements:Bs(v)})),r=new Set(o.map(v=>v.id)),i=[];for(let v of n)Us(v,v.id,i);let a=i.filter(v=>v.destinationFrameId==="__BACK__"||r.has(v.destinationFrameId)),c=o.filter(v=>v.isFlowStartingPoint).map(v=>v.id),d=new Map,l=new Map;for(let v of r)d.set(v,new Set),l.set(v,new Set);for(let v of a)v.destinationFrameId!=="__BACK__"&&((z=d.get(v.sourceFrameId))==null||z.add(v.destinationFrameId),(x=l.get(v.destinationFrameId))==null||x.add(v.sourceFrameId));let p=new Set;for(let v of a)v.destinationFrameId==="__BACK__"&&p.add(v.sourceFrameId);let u=o.filter(v=>{var T;return(((T=d.get(v.id))==null?void 0:T.size)||0)===0&&!p.has(v.id)}).map(v=>v.id),g=o.filter(v=>{var T;return(((T=l.get(v.id))==null?void 0:T.size)||0)===0&&!s.has(v.id)}).map(v=>v.id),f=new Set,m=[...c];if(m.length===0)for(let v of o)((($=l.get(v.id))==null?void 0:$.size)||0)===0&&m.push(v.id);for(;m.length>0;){let v=m.shift();if(f.has(v))continue;f.add(v);let T=d.get(v);if(T)for(let L of T)f.has(L)||m.push(L)}let h=o.filter(v=>!f.has(v.id)).map(v=>v.id),C=[],S=new Set,N=new Set,y=[];function b(v){if(N.has(v)){let L=y.indexOf(v);L!==-1&&C.push(y.slice(L));return}if(S.has(v))return;S.add(v),N.add(v),y.push(v);let T=d.get(v);if(T)for(let L of T)b(L);y.pop(),N.delete(v)}for(let v of r)b(v);let I=o.map(v=>{var T;return((T=d.get(v.id))==null?void 0:T.size)||0}),w=I.length>0?I.reduce((v,T)=>v+T,0)/I.length:0,P=0,O=c.map(v=>({id:v,depth:0})),M=new Set;for(;O.length>0;){let{id:v,depth:T}=O.shift();if(M.has(v))continue;M.add(v),T>P&&(P=T);let L=d.get(v);if(L)for(let A of L)M.has(A)||O.push({id:A,depth:T+1})}return{frames:o,edges:a,entryPoints:c,deadEnds:u,orphans:g,unreachable:h,loops:C,stats:{totalFrames:o.length,totalEdges:a.length,totalEntryPoints:c.length,maxDepth:P,avgBranching:Math.round(w*100)/100}}}function zs(e){let t=[],n=new Map(e.frames.map(i=>[i.id,i.name])),s=i=>i.map(a=>`"${n.get(a)||a}"`).join(", ");for(let i of e.deadEnds){let a=n.get(i)||"";/success|confirm|done|complete|thank|receipt|summary/i.test(a)||t.push({type:"dead-end",severity:"warning",frameIds:[i],message:`${s([i])} has no outgoing connections \u2014 user gets stuck here.`})}e.orphans.length>0&&t.push({type:"orphan",severity:"warning",frameIds:e.orphans,message:`${s(e.orphans)} ${e.orphans.length===1?"has":"have"} no incoming connections \u2014 unreachable by navigation.`});let o=e.unreachable.filter(i=>!e.orphans.includes(i));o.length>0&&t.push({type:"unreachable",severity:"critical",frameIds:o,message:`${s(o)} ${o.length===1?"is":"are"} not reachable from any flow entry point.`});for(let i of e.loops){let a=new Set(i);i.some(d=>e.edges.filter(p=>p.sourceFrameId===d).some(p=>!a.has(p.destinationFrameId)))||t.push({type:"loop",severity:"warning",frameIds:i,message:`Circular flow without exit: ${s(i)}. User cannot leave this loop.`})}e.stats.maxDepth>3&&t.push({type:"deep-navigation",severity:"info",frameIds:[],message:`Navigation depth is ${e.stats.maxDepth} levels. Consider flattening to \u22643 levels for better UX (3-click rule).`});let r=e.frames.filter(i=>{if(i.isFlowStartingPoint)return!1;let a=e.edges.some(d=>d.sourceFrameId===i.id&&(d.navigation==="BACK"||d.navigation==="CLOSE"));return e.edges.some(d=>d.destinationFrameId===i.id)&&!a});return r.length>0&&t.push({type:"missing-back",severity:"info",frameIds:r.map(i=>i.id),message:`${r.length} frame${r.length===1?"":"s"} missing back/close navigation: ${s(r.map(i=>i.id))}.`}),t}J();function Hs(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if("fills"in e){let o=e.fills;if(o!==figma.mixed&&Array.isArray(o))for(let r of o)r.type==="SOLID"&&r.visible!==!1&&t.colors.add(F(r.color.r,r.color.g,r.color.b))}if("strokes"in e){let o=e.strokes;if(Array.isArray(o))for(let r of o)r.type==="SOLID"&&r.visible!==!1&&t.colors.add(F(r.color.r,r.color.g,r.color.b))}if(e.type==="TEXT"){let o=e;o.fontName!==figma.mixed&&t.fontFamilies.add(o.fontName.family),o.fontSize!==figma.mixed&&t.fontSizes.add(o.fontSize)}if("layoutMode"in e&&e.layoutMode!=="NONE"){let o=e;typeof o.itemSpacing=="number"&&t.spacingValues.add(o.itemSpacing),typeof o.paddingTop=="number"&&t.spacingValues.add(o.paddingTop),typeof o.paddingBottom=="number"&&t.spacingValues.add(o.paddingBottom),typeof o.paddingLeft=="number"&&t.spacingValues.add(o.paddingLeft),typeof o.paddingRight=="number"&&t.spacingValues.add(o.paddingRight)}if(e.type==="INSTANCE"){let o=e.mainComponent;o&&t.componentNames.add(o.name)}if("children"in e)for(let o of e.children)Hs(o,t,n,s)}}function Ws(e,t){let n=new Set;for(let s of e)t.has(s)||n.add(s);return n}function Ks(e,t={}){var g,f;let n=(g=t.skipLocked)!=null?g:!0,s=(f=t.skipHidden)!=null?f:!0,o=[];if(e.length<2)return o;let r=e.map(({frame:m,node:h})=>{let C={frameId:m.id,frameName:m.name,colors:new Set,fontFamilies:new Set,fontSizes:new Set,spacingValues:new Set,componentNames:new Set};return Hs(h,C,n,s),C}),i=new Map;for(let m of r)for(let h of m.colors)i.set(h,(i.get(h)||0)+1);let a=r.length*.5,c=new Set;for(let[m,h]of i)h>=a&&c.add(m);for(let m of r){let h=Ws(m.colors,c);h.size>3&&o.push({type:"dead-end",severity:"warning",frameIds:[m.frameId],message:`"${m.frameName}" uses ${h.size} colors not found in other screens (${[...h].slice(0,3).join(", ")}${h.size>3?"...":""}). Check for color inconsistency.`})}let d=new Set;for(let m of r)for(let h of m.fontFamilies)d.add(h);if(d.size>3){let m=[...d].join(", ");o.push({type:"dead-end",severity:"warning",frameIds:r.map(h=>h.frameId),message:`${d.size} different font families across flow: ${m}. Flows should use 1-2 font families for consistency.`})}for(let m of r){let h=new Set;for(let S of r)if(S.frameId!==m.frameId)for(let N of S.fontFamilies)h.add(N);let C=Ws(m.fontFamilies,h);C.size>0&&r.length>2&&o.push({type:"dead-end",severity:"info",frameIds:[m.frameId],message:`"${m.frameName}" uses font${C.size>1?"s":""} not seen elsewhere: ${[...C].join(", ")}.`})}let l=new Set;for(let m of r)for(let h of m.fontSizes)l.add(h);l.size>10&&o.push({type:"dead-end",severity:"info",frameIds:r.map(m=>m.frameId),message:`${l.size} unique font sizes across the flow. Consider using a type scale with fewer sizes for consistency.`});let p=new Set;for(let m of r)for(let h of m.spacingValues)h>0&&p.add(h);let u=[...p].filter(m=>m%4!==0&&m!==2);return u.length>3&&o.push({type:"dead-end",severity:"info",frameIds:r.map(m=>m.frameId),message:`${u.length} non-standard spacing values across flow (${u.slice(0,4).join(", ")}px). Consider aligning to a 4px/8px grid.`}),o}var He=Lo(js()),qs=8e4,Js="baseline::";function Ke(e){return`${Js}${e}::meta`}function Ut(e,t){return`${Js}${e}::chunk_${t}`}function Xs(e){let t=JSON.stringify(e),n=(0,He.compressToUTF16)(t),s=[];for(let r=0;r<n.length;r+=qs)s.push(n.slice(r,r+qs));let o=e.nodeId;eo(o),figma.root.setPluginData(Ke(o),JSON.stringify({chunkCount:s.length,timestamp:e.timestamp,nodeName:e.nodeName,overall:e.overall}));for(let r=0;r<s.length;r++)figma.root.setPluginData(Ut(o,r),s[r])}function Ys(e){let t=figma.root.getPluginData(Ke(e));if(!t)return null;let n;try{n=JSON.parse(t)}catch(i){return null}let s=[];for(let i=0;i<n.chunkCount;i++){let a=figma.root.getPluginData(Ut(e,i));if(!a)return null;s.push(a)}let o=s.join(""),r=(0,He.decompressFromUTF16)(o);if(!r)return null;try{return JSON.parse(r)}catch(i){return null}}function Qs(e){eo(e),figma.root.setPluginData(Ke(e),"")}function Zs(e){let t=figma.root.getPluginData(Ke(e));if(!t)return null;try{return JSON.parse(t)}catch(n){return null}}function eo(e){for(let t=0;t<100;t++){let n=Ut(e,t);if(!figma.root.getPluginData(n))break;figma.root.setPluginData(n,"")}}function je(e){return`${e.errorType}::${e.nodeId}::${e.message}`}function to(e,t){var l,p,u,g;let n=Date.now(),s=new Set([...Object.keys(e.categories),...Object.keys(t.categories)]),o=[];for(let f of s){let m=(p=(l=e.categories[f])==null?void 0:l.score)!=null?p:100,h=(g=(u=t.categories[f])==null?void 0:u.score)!=null?g:100;o.push({category:f,oldScore:m,newScore:h,delta:h-m})}o.sort((f,m)=>Math.abs(m.delta)-Math.abs(f.delta));let r=new Set(e.errors.map(je)),i=new Set(t.errors.map(je)),a=[],c=[],d=[];for(let f of t.errors){let m=je(f);r.has(m)?d.push({errorType:f.errorType,severity:f.severity,nodeId:f.nodeId,message:f.message}):a.push({errorType:f.errorType,severity:f.severity,nodeId:f.nodeId,message:f.message})}for(let f of e.errors){let m=je(f);i.has(m)||c.push({errorType:f.errorType,severity:f.severity,nodeId:f.nodeId,message:f.message})}return{baselineTimestamp:e.timestamp,currentTimestamp:n,scoreDelta:{overall:t.overall-e.overall,oldOverall:e.overall,newOverall:t.overall,categories:o},newIssues:a,fixedIssues:c,remainingIssues:d,summary:{totalNew:a.length,totalFixed:c.length,totalRemaining:d.length,oldTotal:e.errors.length,newTotal:t.errors.length}}}ft();var Ui=["itemSpacing","paddingTop","paddingBottom","paddingLeft","paddingRight","counterAxisSpacing"];function Gt(e,t,n){let s=figma.getNodeById(e);if(!s)return{success:!1,nodeId:e,nodeName:"",property:t,oldValue:0,newValue:n,error:"Node not found"};if(s.type!=="FRAME"&&s.type!=="COMPONENT"&&s.type!=="INSTANCE")return{success:!1,nodeId:e,nodeName:s.name,property:t,oldValue:0,newValue:n,error:"Node is not a frame"};let o=s;if(o.layoutMode==="NONE")return{success:!1,nodeId:e,nodeName:s.name,property:t,oldValue:0,newValue:n,error:"Frame has no auto-layout"};try{let r=o[t];return o[t]=n,{success:!0,nodeId:e,nodeName:s.name,property:t,oldValue:r,newValue:n}}catch(r){return{success:!1,nodeId:e,nodeName:s.name,property:t,oldValue:0,newValue:n,error:r instanceof Error?r.message:String(r)}}}function Ae(e,t){let n=figma.getNodeById(e);if(!n||n.type!=="FRAME"&&n.type!=="COMPONENT"&&n.type!=="INSTANCE")return{success:!1,nodeId:e,nodeName:(n==null?void 0:n.name)||"",property:t,oldValue:0,newValue:0,error:"Invalid node"};let o=n[t];if(typeof o!="number")return{success:!1,nodeId:e,nodeName:n.name,property:t,oldValue:0,newValue:0,error:"Property is not a number"};if(mt.includes(o))return{success:!0,nodeId:e,nodeName:n.name,property:t,oldValue:o,newValue:o};let r=Ve(o);if(r.length===0)return{success:!1,nodeId:e,nodeName:n.name,property:t,oldValue:o,newValue:o,error:"No suggestion found"};let i=r.reduce((a,c)=>Math.abs(a-o)<=Math.abs(c-o)?a:c);return Gt(e,t,i)}function no(e){let t=figma.getNodeById(e);if(!t||t.type!=="FRAME"&&t.type!=="COMPONENT"&&t.type!=="INSTANCE")return[];let n=t;if(n.layoutMode==="NONE")return[];let s=[];for(let o of Ui){if(!(o in n))continue;let r=n[o];if(typeof r!="number"||mt.includes(r))continue;let i=Ae(e,o);s.push(i)}return s}function Ee(e,t){return t.length===0?e:t.reduce((n,s)=>Math.abs(s-e)<Math.abs(n-e)?s:n)}function qe(e,t){let n=figma.getNodeById(e);if(!n)return{success:!1,nodeId:e,nodeName:"",oldValue:"",newValue:"",error:"Node not found"};if(!("cornerRadius"in n))return{success:!1,nodeId:e,nodeName:n.name,oldValue:"",newValue:"",error:"Node has no corner radius"};let s=n;try{let o=s.cornerRadius;if(o===figma.mixed){let r=s.topLeftRadius,i=s.topRightRadius,a=s.bottomLeftRadius,c=s.bottomRightRadius,d=`${r}/${i}/${c}/${a}`;s.topLeftRadius=Ee(r,t),s.topRightRadius=Ee(i,t),s.bottomLeftRadius=Ee(a,t),s.bottomRightRadius=Ee(c,t);let l=`${s.topLeftRadius}/${s.topRightRadius}/${s.bottomRightRadius}/${s.bottomLeftRadius}`;return{success:!0,nodeId:e,nodeName:n.name,oldValue:d,newValue:l}}else{let r=`${o}`,i=Ee(o,t);return s.cornerRadius=i,{success:!0,nodeId:e,nodeName:n.name,oldValue:r,newValue:`${i}`}}}catch(o){return{success:!1,nodeId:e,nodeName:n.name,oldValue:"",newValue:"",error:o instanceof Error?o.message:String(o)}}}function Je(e,t){let n=figma.getNodeById(e);if(!n||n.type==="DOCUMENT"||n.type==="PAGE")return{success:!1,nodeId:e,oldName:"",newName:t,error:"Node not found"};try{let s=n.name;return n.name=t,{success:!0,nodeId:e,oldName:s,newName:t}}catch(s){return{success:!1,nodeId:e,oldName:n.name,newName:t,error:s instanceof Error?s.message:String(s)}}}jt();async function oo(e){let t=0,n=0,s=[];for(let o=0;o<e.length;o++){let r=e[o];try{let i=await Gi(r);s.push(R({index:o},i)),i.success?t++:n++}catch(i){n++,s.push({index:o,type:r.type,success:!1,nodeId:String(r.params.nodeId||""),nodeName:"",message:"Unexpected error",error:i instanceof Error?i.message:String(i)})}}return{total:e.length,applied:t,failed:n,results:s}}async function Gi(e){let{type:t,params:n}=e;switch(t){case"applyStyle":{let s=n.styleType,o=n.nodeId,r=n.styleKey,i;switch(s){case"fill":i=await zt(o,r);break;case"stroke":i=await Wt(o,r);break;case"text":i=await Ht(o,r);break;case"effect":i=await Kt(o,r);break;default:return{type:t,success:!1,nodeId:o,nodeName:"",message:`Unknown style type: ${s}`}}return{type:t,success:i.success,nodeId:i.nodeId,nodeName:i.nodeName,message:i.success?`Applied ${i.property}: ${i.newValue}`:i.error||"Failed",oldValue:i.oldValue,newValue:i.newValue,error:i.error}}case"fixSpacing":{let s=n.nodeId,o=n.property,r=n.value,i=Gt(s,o,r);return{type:t,success:i.success,nodeId:i.nodeId,nodeName:i.nodeName,message:i.success?`${i.property}: ${i.oldValue}px \u2192 ${i.newValue}px`:i.error||"Failed",oldValue:`${i.oldValue}px`,newValue:`${i.newValue}px`,error:i.error}}case"fixSpacingToNearest":{let s=n.nodeId,o=n.property,r=Ae(s,o);return{type:t,success:r.success,nodeId:r.nodeId,nodeName:r.nodeName,message:r.success?`${r.property}: ${r.oldValue}px \u2192 ${r.newValue}px`:r.error||"Failed",oldValue:`${r.oldValue}px`,newValue:`${r.newValue}px`,error:r.error}}case"fixRadiusToNearest":{let s=n.nodeId,o=n.allowedRadii,r=qe(s,o);return{type:t,success:r.success,nodeId:r.nodeId,nodeName:r.nodeName,message:r.success?`radius: ${r.oldValue}px \u2192 ${r.newValue}px`:r.error||"Failed",oldValue:`${r.oldValue}px`,newValue:`${r.newValue}px`,error:r.error}}case"renameLayer":{let s=n.nodeId,o=n.newName,r=Je(s,o);return{type:t,success:r.success,nodeId:r.nodeId,nodeName:r.newName,message:r.success?`Renamed "${r.oldName}" \u2192 "${r.newName}"`:r.error||"Failed",oldValue:r.oldName,newValue:r.newName,error:r.error}}default:return{type:t,success:!1,nodeId:"",nodeName:"",message:`Unknown fix type: ${t}`}}}function zi(e){let t=new Map,n=0,s=0;function o(r){let i=r,a=Array.isArray(i.fills)&&i.fills.length>0,c=Array.isArray(i.strokes)&&i.strokes.length>0,d=Array.isArray(i.effects)&&i.effects.length>0,l=r.type==="TEXT",p=r.type==="FRAME"||r.type==="COMPONENT"||r.type==="INSTANCE";(a||c||d||l||p)&&n++;let u=!1;if("boundVariables"in r&&i.boundVariables){let g=i.boundVariables;for(let f of Object.keys(g)){let m=g[f];if(Array.isArray(m))for(let h of m)h&&h.id&&(t.set(h.id,(t.get(h.id)||0)+1),u=!0);else m&&m.id&&(t.set(m.id,(t.get(m.id)||0)+1),u=!0)}}if(Array.isArray(i.fills)){for(let g of i.fills)if(g.boundVariables)for(let f of Object.keys(g.boundVariables)){let m=g.boundVariables[f];m&&m.id&&(t.set(m.id,(t.get(m.id)||0)+1),u=!0)}}if(u&&s++,"children"in r&&i.children)for(let g of i.children)o(g)}for(let r of e)o(r);return{consumerMap:t,totalEligible:n,boundCount:s}}async function qt(){let e=await figma.variables.getLocalVariableCollectionsAsync(),t=figma.currentPage.findAll(()=>!0),{consumerMap:n,totalEligible:s,boundCount:o}=zi(t),r=[],i=0,a=[],c={};for(let p of e){let u=[];for(let g of p.modes)c[g.name]||(c[g.name]={total:0,withValue:0});for(let g of p.variableIds){let f=await figma.variables.getVariableByIdAsync(g);if(!f)continue;i++;let m=n.get(f.id)||0;m===0&&a.push(f.name);let h={};for(let[C,S]of Object.entries(f.valuesByMode))h[C]=Wi(S);for(let C of p.modes){c[C.name].total++;let S=f.valuesByMode[C.modeId];S!=null&&c[C.name].withValue++}u.push({id:f.id,name:f.name,resolvedType:f.resolvedType,description:f.description,valuesByMode:h,scopes:f.scopes,consumers:m})}r.push({id:p.id,name:p.name,modes:p.modes.map(g=>({modeId:g.modeId,name:g.name})),variables:u})}let d=s>0?Math.round(o/s*100):0,l={};for(let[p,u]of Object.entries(c))l[p]=u.total>0?Math.round(u.withValue/u.total*100):100;return{collections:r,totalVariables:i,unusedVariables:a,adoptionRate:d,modesCoverage:l}}function Wi(e){if(e==null||typeof e=="boolean"||typeof e=="string"||typeof e=="number")return e;if(typeof e=="object"&&e.type==="VARIABLE_ALIAS")return{type:"VARIABLE_ALIAS",id:e.id};if(typeof e=="object"&&"r"in e){let t=e,n=s=>{let o=Math.round(s*255).toString(16);return o.length===1?"0"+o:o};return t.a!==void 0&&t.a<1?`rgba(${Math.round(t.r*255)}, ${Math.round(t.g*255)}, ${Math.round(t.b*255)}, ${t.a.toFixed(2)})`:`#${n(t.r)}${n(t.g)}${n(t.b)}`}return e}function ro(e){let t=JSON.parse(e),n=[];return io(t,[],void 0,n),n}function io(e,t,n,s){let o=typeof e.$type=="string"?e.$type:n;if("$value"in e){let r=typeof e.$type=="string"?e.$type:n||"unknown",i=typeof e.$description=="string"?e.$description:void 0;s.push({path:[...t],name:t.join("."),$type:r,$value:e.$value,$description:i});return}for(let[r,i]of Object.entries(e))r.startsWith("$")||typeof i=="object"&&i!==null&&!Array.isArray(i)&&io(i,[...t,r],o,s)}function ao(e,t,n){let s=new Map;for(let C of t)s.set(Te(C.name),C);let o=[];for(let C of e.collections)for(let S of C.variables)o.push(S);let r=[],i=[],a=new Set,c=new Set;for(let C of o){let S=Te(C.name);if(c.add(S),s.has(S)){a.add(S),r.push({token:C.name,nodeCount:C.consumers,usage:C.consumers>0?"correct":"overridden"});continue}let N=Hi(S,t);N&&N.distance<=3?(a.add(Te(N.token.name)),r.push({token:C.name,nodeCount:C.consumers,usage:"correct"})):i.push({value:C.name,nodeCount:C.consumers,nearestToken:N?N.token.name:"(none)",distance:N?N.distance:1/0})}let d=[];for(let C of t)a.has(Te(C.name))||d.push(C.name);let l=i.filter(C=>C.nodeCount>0).map(C=>C.value),p=t.length,u=r.filter(C=>C.nodeCount>0).length,g=i.filter(C=>C.nodeCount>0).length,f=u+g,m=f>0?Math.round(u/f*100):p>0?0:100;return{adoptionScore:Math.round(e.adoptionRate*.5+m*.5),matched:r,unmatched:i,orphanTokens:d,missingFromSystem:l,summary:{totalTokenDefs:p,usedInDesign:u,hardCodedValues:g,compliance:m}}}function Te(e){return e.replace(/\//g,".").replace(/\s+/g,"-").toLowerCase().trim()}function Hi(e,t){if(t.length===0)return null;let n=null;for(let s of t){let o=Ki(e,Te(s.name));if((!n||o<n.distance)&&(n={token:s,distance:o}),o===0)return n}return n}function Ki(e,t,n=10){if(e===t)return 0;if(e.length===0)return Math.min(t.length,n+1);if(t.length===0)return Math.min(e.length,n+1);let s=new Array(t.length+1),o=new Array(t.length+1);for(let r=0;r<=t.length;r++)s[r]=r;for(let r=1;r<=e.length;r++){o[0]=r;let i=o[0];for(let a=1;a<=t.length;a++){let c=e[r-1]===t[a-1]?0:1;o[a]=Math.min(s[a]+1,o[a-1]+1,s[a-1]+c),o[a]<i&&(i=o[a])}if(i>n)return n+1;[s,o]=[o,s]}return s[t.length]}async function co(e){let n=(await figma.variables.getLocalVariableCollectionsAsync()).find(i=>i.id===e);if(!n)throw new Error(`Collection not found: ${e}`);let s=n.modes.map(i=>({modeId:i.modeId,modeName:i.name})),o=[],r=[];for(let i of n.variableIds){let a=await figma.variables.getVariableByIdAsync(i);if(!a)continue;let c={},d=[],l=!1,p,u=!1;for(let g of n.modes){let f=a.valuesByMode[g.modeId];f==null?d.push(g.name):(c[g.name]=Jt(f),u?JSON.stringify(Jt(f))!==p&&(l=!0):(p=JSON.stringify(Jt(f)),u=!0))}(l||d.length>0)&&o.push({variableName:a.name,type:a.resolvedType,values:c}),d.length>0&&r.push({variableName:a.name,missingModes:d})}return{collection:n.name,modes:s,variableDiffs:o,missingValues:r}}function Jt(e){if(e==null||typeof e=="boolean"||typeof e=="string"||typeof e=="number")return e;if(typeof e=="object"&&e.type==="VARIABLE_ALIAS")return{type:"VARIABLE_ALIAS",id:e.id};if(typeof e=="object"&&"r"in e){let t=e,n=s=>{let o=Math.round(s*255).toString(16);return o.length===1?"0"+o:o};return t.a!==void 0&&t.a<1?`rgba(${Math.round(t.r*255)}, ${Math.round(t.g*255)}, ${Math.round(t.b*255)}, ${t.a.toFixed(2)})`:`#${n(t.r)}${n(t.g)}${n(t.b)}`}return e}be();J();var ce=null,le=null,Pe=new Set,Xe=!1;function lo(e){if(!ce||!ce.enabled)return;for(let n of e.documentChanges)(n.type==="PROPERTY_CHANGE"||n.type==="CREATE"||n.type==="STYLE_PROPERTY_CHANGE")&&"id"in n&&typeof n.id=="string"&&Pe.add(n.id);if(Pe.size===0)return;le!==null&&clearTimeout(le);let t=ce.debounceMs||500;le=setTimeout(()=>{ji()},t)}async function ji(){if(!ce)return;let e=Array.from(Pe);Pe.clear(),le=null;let t=[],n=[];for(let s of e)try{let o=await figma.getNodeByIdAsync(s);o&&"type"in o&&o.type!=="PAGE"&&o.type!=="DOCUMENT"&&(t.push(o),n.push(s))}catch(o){}if(t.length!==0)try{let s=ae(t,ce.settings);k("realtime-lint-update",{errors:s.errors,changedNodeIds:n})}catch(s){console.error("Realtime lint error:",s)}}function uo(e){ce={enabled:e.enabled,debounceMs:e.debounceMs||500,settings:e.settings||j},Xe||(figma.on("documentchange",lo),Xe=!0)}function po(){ce=null,Xe&&(figma.off("documentchange",lo),Xe=!1),le!==null&&(clearTimeout(le),le=null),Pe.clear()}var mo=/^(Frame|Group|Rectangle|Ellipse|Vector|Line|Polygon|Star|Component|Instance|Boolean)\s*\d+$/i;function fo(e,t){let n=e.errors,o=n.filter(m=>m.errorType==="fill"||m.errorType==="stroke"||m.errorType==="effect"||m.errorType==="text").length,i=n.filter(m=>m.message.toLowerCase().includes("detach")).length,a=t?t.hardCodedValues:0,c=new Set;for(let m of n)mo.test(m.nodeName)&&c.add(m.nodeId);let d=n.filter(m=>m.errorType==="accessibility"&&mo.test(m.nodeName));for(let m of d)c.add(m.nodeId);let l=c.size,p=n.filter(m=>m.errorType==="autoLayout").length,u=n.filter(m=>m.errorType==="spacing").length,g=100;return g-=o*2,g-=i*5,g-=a*1,g-=l*.5,g-=p*1,g-=u*.5,{overall:Math.max(0,Math.min(100,Math.round(g))),components:{orphanedStyles:{count:o,score:Math.max(0,Math.round(100-o*2))},detachedInstances:{count:i,score:Math.max(0,Math.round(100-i*5))},hardcodedValues:{count:a,score:Math.max(0,Math.round(100-a*1))},namingViolations:{count:l,score:Math.max(0,Math.round(100-l*.5))},missingAutoLayout:{count:p,score:Math.max(0,Math.round(100-p*1))},inconsistentSpacing:{count:u,score:Math.max(0,Math.round(100-u*.5))}}}}be();var Z=null,H="claude-sonnet-4-5-20250929",_="anthropic";function go(e,t=_){let n=(e==null?void 0:e.trim())||"";switch(t){case"anthropic":return n.startsWith("sk-ant-")&&n.length>=40;case"openai":return n.startsWith("sk-")&&n.length>=20;case"google":return n.startsWith("AIza")&&n.length>=35;default:return!1}}var yo=null,ho=null,Q=new Ds({enableCaching:!0,enableMCPIntegration:!0,mcpServerUrl:"https://design-systems-mcp.southleft-llc.workers.dev/mcp"});async function bo(e){let{type:t,data:n}=e,s=t==="save-api-key"?`${t} [redacted]`:t;console.log("Received message:",s);try{switch(t){case"check-api-key":await qi();break;case"save-api-key":await Ji(n.apiKey,n.model,n.provider);break;case"update-model":await Xi(n.model);break;case"analyze":await Yi();break;case"analyze-enhanced":await vo(n);break;case"clear-api-key":await Zi();break;case"chat-message":await ea(n);break;case"chat-clear-history":await ta();break;case"select-node":await na(n);break;case"preview-fix":await Ma(n);break;case"apply-token-fix":await Oa(n);break;case"apply-naming-fix":await Fa(n);break;case"apply-batch-fix":await Da(n);break;case"update-description":await Va(n);break;case"add-component-property":await _a(n);break;case"run-design-lint":Re(n);break;case"lint-ignore-node":ca(n);break;case"lint-ignore-error":la(n);break;case"lint-ignore-all-of-type":da(n);break;case"lint-clear-ignored":ua();break;case"lint-select-node":pa(n);break;case"lint-select-all-with-value":ma(n);break;case"lint-save-settings":fa(n);break;case"lint-load-settings":ga();break;case"lint-save-team-config":ya(n);break;case"lint-load-team-config":ha();break;case"jump-to-node":ba(n);break;case"fix-spacing":Sa(n);break;case"fix-spacing-to-nearest":Na(n);break;case"fix-all-spacing":Ca(n);break;case"apply-style-fix":await Ia(n);break;case"rename-layer-fix":xa(n);break;case"fix-radius-to-nearest":wa(n);break;case"batch-fix-v2":await Aa(n);break;case"rescan-lint":So();break;case"export-screenshot":await ka(n);break;case"analyze-flow":await Ea();break;case"analyze-page":await Ta();break;case"save-baseline":Pa(n);break;case"load-baseline":La(n);break;case"compare-baseline":Ra(n);break;case"delete-baseline":$a(n);break;case"collect-variables":await Ba();break;case"check-dtcg-compliance":await Ua(n);break;case"compare-modes":await Ga(n);break;case"enable-realtime-lint":za(n);break;case"disable-realtime-lint":Wa();break;case"calculate-design-debt":Ha(n);break;default:console.warn("Unknown message type:",t)}}catch(o){console.error("Error handling message:",o);let r=o instanceof Error?o.message:"Unknown error occurred";k("analysis-error",{error:r})}}async function qi(){try{await lt();let e=await dt();if(_=e.providerId,H=e.modelId,Z){k("api-key-status",{hasKey:!0,provider:_,model:H});return}e.apiKey&&go(e.apiKey,e.providerId)?(Z=e.apiKey,k("api-key-status",{hasKey:!0,provider:_,model:H})):k("api-key-status",{hasKey:!1,provider:_,model:H})}catch(e){console.error("Error checking API key:",e),k("api-key-status",{hasKey:!1,provider:"anthropic"})}}async function Ji(e,t,n){try{let s=n||_;if(!go(e,s)){let r=re(s);throw new Error(`Invalid API key format for ${r.name}. Expected format: ${r.keyPlaceholder}`)}_=s,Z=e,t&&(H=t),await ut(s,H,e),console.log(`${s} API key and model saved successfully`);let o=re(s);k("api-key-saved",{success:!0,provider:s}),figma.notify(`${o.name} API key saved successfully`,{timeout:2e3})}catch(s){console.error("Error saving API key:",s);let o=s instanceof Error?s.message:"Unknown error occurred";k("api-key-saved",{success:!1,error:o}),figma.notify(`Failed to save API key: ${o}`,{error:!0})}}async function Xi(e){try{H=e,await ut(_,e),console.log("Model updated to:",e),figma.notify(`Model updated to ${e}`,{timeout:2e3})}catch(t){console.error("Error updating model:",t),figma.notify("Failed to update model",{error:!0})}}async function vo(e){var t,n;try{if(!Z){let c=re(_).name;throw new Error(`API key not found. Please save your ${c} API key first.`)}let s=figma.currentPage.selection;if(s.length===0)throw new Error("No component selected. Please select a Figma component to analyze.");if(e.batchMode&&s.length>1){await Qi(s,e);return}let o=s[0];if(o.type==="INSTANCE"){let c=o;try{let d=await c.getMainComponentAsync();if(d)figma.notify("Analyzing main component instead of instance...",{timeout:2e3}),o=d;else throw new Error("This instance has no main component. Please select a component directly.")}catch(d){throw console.error("Error accessing main component:",d),new Error("Could not access main component. Please select a component directly.")}}if(o.type==="COMPONENT"&&((t=o.parent)==null?void 0:t.type)==="COMPONENT_SET"){let d=o.parent;figma.notify("Analyzing parent component set to include all variants...",{timeout:2e3}),o=d}if(!Se(o)){let c=new Set(["COMPONENT_SET","COMPONENT","INSTANCE"]),d=null,l=null,p=o.parent;for(;p&&"type"in p;){let g=p;if(c.has(g.type)&&!d){d=g;break}!l&&Se(g)&&(l=g),p=p.parent}let u=d||l;u&&(figma.notify(`Analyzing parent ${u.type.toLowerCase()} "${u.name}"...`,{timeout:2e3}),o=u)}if(o.type==="INSTANCE"){let c=o;try{let d=await c.getMainComponentAsync();d&&(figma.notify("Analyzing main component instead of instance...",{timeout:2e3}),o=d)}catch(d){}}if(o.type==="COMPONENT"&&((n=o.parent)==null?void 0:n.type)==="COMPONENT_SET"){let c=o.parent;figma.notify("Analyzing parent component set to include all variants...",{timeout:2e3}),o=c}if(!Se(o))throw new Error("Please select a Frame, Component, Component Set, or Instance to analyze");await Q.loadDesignSystemsKnowledge();let r=await Lt(o),i=R({enableMCPEnhancement:!0,batchMode:e.batchMode||!1,enableAudit:e.enableAudit!==!1,includeTokenAnalysis:e.includeTokenAnalysis!==!1},e);figma.notify("Performing enhanced analysis with design systems knowledge...",{timeout:3e3});let a=await Os(r,Z,H,i,_);yo=a.metadata,ho=o,k("enhanced-analysis-result",K(R({},a),{analyzedNodeId:o.id})),figma.notify("Enhanced analysis complete! Check the results panel.",{timeout:3e3})}catch(s){console.error("Error during enhanced analysis:",s);let o=s instanceof Error?s.message:"Unknown error occurred";figma.notify(`Analysis failed: ${o}`,{error:!0}),k("analysis-error",{error:o})}}async function Yi(){await vo({batchMode:!1})}async function Qi(e,t){let n=[];await Q.loadDesignSystemsKnowledge();for(let r of e)if(Se(r))try{let i=await Lt(r),a=await ue(r),c=[...a.colors,...a.spacing,...a.typography,...a.effects,...a.borders],d=Q.generateComponentHash(i,c,V),l=Q.getCachedAnalysis(d);if(l){console.log(`\u2705 Using cached analysis for ${r.name}`),n.push({node:r.name,success:!0,data:l.result.metadata,cached:!0});continue}let p=Q.createDeterministicPrompt(i),u=await ie(_,Z,{prompt:p,model:H,maxTokens:2048,temperature:.1}),g=pe(u.content),f=Fe(g),m=await Rt(f,i,{batchMode:!0});Q.validateAnalysisConsistency(m,i)||(m=Q.applyConsistencyCorrections(m,i)),Q.cacheAnalysis(d,m),n.push({node:r.name,success:!0,data:m.metadata,cached:!1})}catch(i){n.push({node:r.name,success:!1,error:i instanceof Error?i.message:"Analysis failed"})}let s=n.filter(r=>r.success&&r.cached).length,o=n.filter(r=>r.success&&!r.cached).length;k("batch-analysis-result",{results:n}),figma.notify(`Batch analysis complete: ${o} analyzed, ${s} from cache`,{timeout:3e3})}async function Zi(){try{Z=null,await on(_),await figma.clientStorage.setAsync("claude-api-key","");let e=re(_).name;k("api-key-cleared",{success:!0}),figma.notify(`${e} API key cleared`,{timeout:2e3})}catch(e){console.error("Error clearing API key:",e)}}async function ea(e){try{if(console.log("Processing chat message:",e.message),!Z){let i=re(_).name;throw new Error(`API key not found. Please save your ${i} API key first.`)}k("chat-response-loading",{isLoading:!0});let t=ia(),n=await ra(e.message),s=aa(e.message,n,e.history,t),r={message:(await ie(_,Z,{prompt:s,model:H,maxTokens:2048,temperature:.7})).content,sources:n.sources||[]};k("chat-response",{response:r})}catch(t){console.error("Error handling chat message:",t);let n=t instanceof Error?t.message:"Unknown error occurred";k("chat-error",{error:n})}}async function ta(){try{k("chat-history-cleared",{success:!0}),figma.notify("Chat history cleared",{timeout:2e3})}catch(e){console.error("Error clearing chat history:",e)}}async function na(e){try{console.log("\u{1F3AF} Attempting to select node:",e.nodeId);let t=await figma.getNodeByIdAsync(e.nodeId);if(!t){console.warn("\u26A0\uFE0F Node not found:",e.nodeId),figma.notify("Node not found - it may have been deleted or moved",{error:!0});return}if(!sa(t)){console.warn("\u26A0\uFE0F Node is not on current page:",e.nodeId),figma.notify("Node is on a different page",{error:!0});return}figma.currentPage.selection=[t],figma.viewport.scrollAndZoomIntoView([t]),console.log("\u2705 Successfully selected and zoomed to node:",t.name),figma.notify(`Selected "${t.name}"`,{timeout:2e3})}catch(t){console.error("Error selecting node:",t);let n=t instanceof Error?t.message:"Unknown error occurred";figma.notify(`Failed to select node: ${n}`,{error:!0})}}function sa(e){try{let t=e,n=50,s=0;for(;t&&t.parent&&s<n;)if(t=t.parent,s++,t===figma.currentPage)return!0;if(t===figma.currentPage||e.parent===figma.currentPage)return!0;let o=figma.currentPage;return e.type==="COMPONENT"||e.type==="COMPONENT_SET"?oa(o,e.id):!1}catch(t){return console.warn("Error checking node page:",t),!1}}function oa(e,t){try{return e.findAll().some(s=>s.id===t)}catch(n){return!1}}async function ra(e){var t;try{console.log("\u{1F50D} Querying MCP for chat:",e);let n=((t=Q.config)==null?void 0:t.mcpServerUrl)||"https://design-systems-mcp.southleft-llc.workers.dev/mcp",s=[Xt(n,e,{category:"general",limit:3}),e.toLowerCase().includes("component")?Xt(n,e,{category:"components",limit:2}):Promise.resolve({results:[]}),e.toLowerCase().includes("token")||e.toLowerCase().includes("design token")?Xt(n,e,{category:"tokens",limit:2}):Promise.resolve({results:[]})],o=await Promise.allSettled(s),r=[];return o.forEach(i=>{i.status==="fulfilled"&&i.value.results&&r.push(...i.value.results)}),console.log(`\u2705 Found ${r.length} relevant sources for chat query`),{sources:r.slice(0,5)}}catch(n){return console.warn("\u26A0\uFE0F MCP query failed for chat:",n),{sources:[]}}}async function Xt(e,t,n={}){let s={jsonrpc:"2.0",id:Math.floor(Math.random()*1e3)+100,method:"tools/call",params:{name:"search_design_knowledge",arguments:R({query:t,limit:n.limit||5},n.category&&{category:n.category})}},o=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!o.ok)throw new Error(`MCP search failed: ${o.status}`);let r=await o.json();return r.result&&r.result.content?{results:r.result.content.map(i=>({title:i.title||"Design System Knowledge",content:i.content||i.description||"",category:i.category||"general"}))}:{results:[]}}function ia(){try{let e=yo,t=ho;if(!e&&!t)return null;let n={hasCurrentComponent:!0,timestamp:Date.now()};if(t){n.component={name:t.name,type:t.type,id:t.id};let s=figma.currentPage.selection;s.length>0&&(n.selection={count:s.length,types:s.map(o=>o.type),names:s.map(o=>o.name)})}return e&&(n.analysis={component:e.component,description:e.description,props:e.props||[],states:e.states||[],accessibility:e.accessibility,audit:e.audit,mcpReadiness:e.mcpReadiness}),n}catch(e){return console.warn("Failed to get component context:",e),null}}function aa(e,t,n,s){let o="";n.length>0&&(o=` **Previous Conversation:** -`,n.slice(-6).forEach(l=>{o+=`${l.role==="user"?"User":"Assistant"}: ${l.content} +`,n.slice(-6).forEach(d=>{o+=`${d.role==="user"?"User":"Assistant"}: ${d.content} `}),o+=` `);let r="";if(s&&s.hasCurrentComponent){if(r=` **Current Component Context:** @@ -415,8 +415,8 @@ ${o} `)}r+=` `}let i="";t.sources&&t.sources.length>0&&(i=` **Relevant Design Systems Knowledge:** -`,t.sources.forEach((c,l)=>{i+=` -${l+1}. **${c.title}** (${c.category}) +`,t.sources.forEach((c,d)=>{i+=` +${d+1}. **${c.title}** (${c.category}) ${c.content} `}),i+=` `);let a=s&&s.hasCurrentComponent;return`You are a specialized design systems assistant with access to comprehensive design systems knowledge. You're helping a user with their Figma plugin for design system analysis. @@ -435,4 +435,4 @@ ${r}${i}**Instructions:** ${a?"Since you have context about their current component, prioritize advice that directly applies to what they're working on.":"If the user wants component-specific advice, suggest they select and analyze a component in Figma first."} -Respond naturally and helpfully to the user's question.`}var D=R({},X),Ce=null;function Ie(e){let t=(e==null?void 0:e.settings)||D;(!Ce||e!=null&&e.resetScope)&&(Ce=figma.currentPage.selection.map(o=>o.id));let n=Ce.map(o=>figma.getNodeById(o)).filter(o=>o!==null&&o.type!=="DOCUMENT"&&o.type!=="PAGE");n.length>0&&(figma.currentPage.selection=n);let s=we(t);N("design-lint-result",s)}function We(){try{let e=ht();figma.root.setPluginData("ignoredState",JSON.stringify(e))}catch(e){}}function ai(e){mt(e.nodeId),We(),Ie()}function ci(e){ft(e.nodeId,e.errorType,e.value),We(),Ie()}function li(e){let t=we(D);gt(t.errors,e.errorType),We(),Ie()}function di(){yt(),We(),Ie()}function ui(e){try{let t=figma.getNodeById(e.nodeId);t&&t.type!=="DOCUMENT"&&t.type!=="PAGE"&&(figma.currentPage.selection=[t],figma.viewport.scrollAndZoomIntoView([t]))}catch(t){console.warn("Could not select node:",e.nodeId,t)}}function pi(e){let t=figma.currentPage.selection;if(t.length===0)return;let n=vt(t,e.errorType,e.value,D),o=[...new Set(n.map(r=>r.nodeId))].map(r=>figma.getNodeById(r)).filter(r=>r!==null&&r.type!=="DOCUMENT"&&r.type!=="PAGE");o.length>0&&(figma.currentPage.selection=o,figma.viewport.scrollAndZoomIntoView(o),N("lint-selected-nodes",{count:o.length,value:e.value}))}async function mi(e){D=e.settings;try{await figma.clientStorage.setAsync("design-lint-settings",e.settings)}catch(t){console.warn("Could not save lint settings:",t)}}async function fi(){try{let e=await figma.clientStorage.getAsync("design-lint-settings");e&&(D=R(R({},X),e)),N("lint-settings-loaded",D)}catch(e){console.warn("Could not load lint settings:",e),N("lint-settings-loaded",X)}}function gi(e){try{let t=e.config;if(!t||t.version!==1){N("team-config-saved",{success:!1,error:"Invalid config version"});return}figma.root.setSharedPluginData("figmalint","config",JSON.stringify(t)),N("team-config-saved",{success:!0})}catch(t){N("team-config-saved",{success:!1,error:String(t)})}}function yi(){var e,t;try{let n=figma.root.getSharedPluginData("figmalint","config");if(n){let s=JSON.parse(n);D=R({},X),(e=s.scales)!=null&&e.spacing&&(D.spacingScale=s.scales.spacing),(t=s.scales)!=null&&t.radius&&(D.allowedRadii=s.scales.radius),s.severityOverrides&&(D.severityOverrides=s.severityOverrides),s.ignorePatterns&&(D.ignorePatterns=s.ignorePatterns),N("team-config-loaded",{config:s,settings:D})}else N("team-config-loaded",{config:null,settings:D})}catch(n){console.warn("Could not load team config:",n),N("team-config-loaded",{config:null,settings:D})}}function hi(e){try{let t=figma.getNodeById(e.nodeId);t&&t.type!=="DOCUMENT"&&t.type!=="PAGE"&&(figma.currentPage.selection=[t],figma.viewport.scrollAndZoomIntoView([t]))}catch(t){console.warn("Could not jump to node:",e.nodeId,t)}}var bi=new Set(["itemSpacing","paddingTop","paddingBottom","paddingLeft","paddingRight","counterAxisSpacing"]);function vi(e){try{if(!bi.has(e.property)){N("fix-error",{error:`Invalid spacing property: ${e.property}`});return}let t=figma.getNodeById(e.nodeId);if(t&&(t.type==="FRAME"||t.type==="COMPONENT"||t.type==="INSTANCE")){let n=t[e.property];t[e.property]=e.value,N("fix-applied",{type:"spacing",nodeId:e.nodeId,nodeName:t.name,property:e.property,oldValue:n,newValue:e.value})}}catch(t){console.warn("Could not fix spacing:",t),N("fix-error",{error:"Failed to apply spacing fix"})}}async function Si(e){try{let t=null;if(e!=null&&e.nodeId){let i=figma.getNodeById(e.nodeId);i&&i.type!=="DOCUMENT"&&i.type!=="PAGE"&&(t=i)}else figma.currentPage.selection.length>0&&(t=figma.currentPage.selection[0]);if(!t){N("screenshot-error",{error:"No node selected"});return}let n=await Lt(t),s="layoutMode"in t&&t.layoutMode!=="NONE",o="children"in t?t.children.length:0,r;try{let i=await ce(t),a=[...i.colors,...i.spacing,...i.typography,...i.effects,...i.borders];r={totalTokens:a.length,boundToVariables:a.filter(c=>c.source==="figma-variable").length,boundToStyles:a.filter(c=>c.source==="figma-style").length,hardCoded:a.filter(c=>c.source==="hard-coded").length}}catch(i){}N("screenshot-result",{nodeId:t.id,nodeName:t.name,nodeType:t.type,screenshot:n,width:t.width,height:t.height,hasAutoLayout:s,childCount:o,tokenSummary:r})}catch(t){console.warn("Could not export screenshot:",t),N("screenshot-error",{error:"Failed to export screenshot"})}}function Ni(e){try{let t=ke(e.nodeId,e.property);N("fix-applied",{type:"spacing",nodeId:t.nodeId,nodeName:t.nodeName,property:e.property,oldValue:t.oldValue,newValue:t.newValue,success:t.success,error:t.error})}catch(t){N("fix-error",{error:"Failed to auto-fix spacing"})}}function wi(e){try{let t=e.allowedRadii||[0,2,4,8,12,16,20,24,32],n=Ge(e.nodeId,t);N("fix-applied",{type:"radius",nodeId:n.nodeId,nodeName:n.nodeName,oldValue:n.oldValue,newValue:n.newValue,success:n.success,error:n.error})}catch(t){N("fix-error",{error:"Failed to auto-fix radius"})}}function ki(e){try{let t=As(e.nodeId),n=t.filter(s=>s.success).length;for(let s of t)N("fix-applied",{type:"spacing",nodeId:s.nodeId,nodeName:s.nodeName,property:s.property,oldValue:s.oldValue,newValue:s.newValue,success:s.success});n>0&&figma.notify(`Fixed ${n} spacing value${n!==1?"s":""}`,{timeout:2e3})}catch(t){N("fix-error",{error:"Failed to fix all spacing"})}}async function xi(e){try{let{applyFillStyle:t,applyStrokeStyle:n,applyTextStyle:s,applyEffectStyle:o}=await Promise.resolve().then(()=>(_t(),Es)),r;switch(e.styleType){case"fill":r=await t(e.nodeId,e.styleKey);break;case"stroke":r=await n(e.nodeId,e.styleKey);break;case"text":r=await s(e.nodeId,e.styleKey);break;case"effect":r=await o(e.nodeId,e.styleKey);break;default:N("fix-error",{error:`Unknown style type: ${e.styleType}`});return}N("fix-applied",{type:"style",nodeId:r.nodeId,nodeName:r.nodeName,property:r.property,oldValue:r.oldValue,newValue:r.newValue,success:r.success,error:r.error})}catch(t){N("fix-error",{error:"Failed to apply style"})}}function Ci(e){try{let t=ze(e.nodeId,e.newName);N("fix-applied",{type:"rename",nodeId:t.nodeId,nodeName:t.newName,oldValue:t.oldName,newValue:t.newName,success:t.success,error:t.error})}catch(t){N("fix-error",{error:"Failed to rename layer"})}}async function Ii(e){try{let t=await Ts(e.fixes);N("batch-fix-v2-result",t),t.failed===0?figma.notify(`Applied ${t.applied} fix${t.applied!==1?"es":""} successfully`,{timeout:2e3}):t.applied>0?figma.notify(`Applied ${t.applied}, ${t.failed} failed`,{timeout:3e3}):figma.notify(`All ${t.failed} fixes failed`,{error:!0}),Os()}catch(t){N("fix-error",{error:"Batch fix failed"})}}function Os(){if(Ce){let t=Ce.map(n=>figma.getNodeById(n)).filter(n=>n!==null&&n.type!=="DOCUMENT"&&n.type!=="PAGE");t.length>0&&(figma.currentPage.selection=t)}let e=we(D);N("design-lint-result",e),N("rescan-complete",{totalErrors:e.summary.totalErrors,nodesWithErrors:e.summary.nodesWithErrors})}async function Ai(){try{N("flow-analysis-started",{status:"building-graph"});let e=ms();if(e.frames.length===0){N("flow-analysis-error",{error:"No top-level frames found on current page."});return}if(e.frames.length>50){N("flow-analysis-error",{error:`Too many frames (${e.frames.length}). Select a page with \u226450 frames for flow analysis.`});return}let t=fs(e);N("flow-analysis-started",{status:"capturing-screenshots",total:e.frames.length});let n={},s={},o=10;for(let c=0;c<e.frames.length;c+=o){let d=e.frames.slice(c,c+o).map(async p=>{let u=await figma.getNodeByIdAsync(p.id);if(!(!u||!("exportAsync"in u))){try{let f=await Lt(u);n[p.id]=f}catch(f){}try{let{runDesignLint:f}=await Promise.resolve().then(()=>(Fe(),Zn)),y=f([u],D);s[p.id]=y}catch(f){}}});await Promise.all(d),N("flow-analysis-started",{status:"capturing-screenshots",progress:Math.min(c+o,e.frames.length),total:e.frames.length})}let r=[];for(let c of e.frames){let l=await figma.getNodeByIdAsync(c.id);l&&r.push({frame:c,node:l})}let i=hs(r,{skipLocked:D.skipLockedLayers,skipHidden:D.skipHiddenLayers}),a=[...t,...i];N("flow-analysis-result",{graph:e,graphIssues:a,screenshots:n,lintResults:s})}catch(e){let t=e instanceof Error?e.message:"Unknown error";N("flow-analysis-error",{error:t})}}async function Fs(){var e,t;try{let n=await st();V=n.providerId,G=n.modelId,n.apiKey?(Q=n.apiKey,N("api-key-status",{hasKey:!0,provider:V,model:G})):N("api-key-status",{hasKey:!1,provider:V,model:G}),console.log(`Plugin initialized with provider: ${V}, model: ${G}`);try{let s=figma.root.getPluginData("ignoredState");if(s){let o=JSON.parse(s);bt(o),console.log(`Restored ${((e=o.nodeIds)==null?void 0:e.length)||0} ignored nodes, ${((t=o.errorKeys)==null?void 0:t.length)||0} ignored errors`)}}catch(s){}console.log("\u{1F504} Initializing design systems knowledge..."),Y.loadDesignSystemsKnowledge().then(()=>{console.log("\u2705 Design systems knowledge loaded successfully")}).catch(s=>{console.warn("\u26A0\uFE0F Failed to load design systems knowledge, using fallback:",s)}),console.log("Plugin initialized successfully")}catch(n){console.error("Error initializing plugin:",n)}}function Ei(e){let t={version:1,timestamp:Date.now(),nodeId:e.nodeId,nodeName:e.nodeName,overall:e.overall,grade:e.grade,categories:e.categories,summary:e.summary,errors:e.errors.map(n=>({errorType:n.errorType,severity:n.severity||"warning",nodeId:n.nodeId,message:n.message}))};Ns(t),N("baseline-saved",{nodeId:e.nodeId,nodeName:e.nodeName,timestamp:t.timestamp,overall:e.overall})}function Ti(e){let t=xs(e.nodeId);N("baseline-loaded",t)}function Pi(e){let t=ws(e.nodeId);if(!t){N("diff-result",null);return}let n=e.errors.map(o=>({errorType:o.errorType,severity:o.severity||"warning",nodeId:o.nodeId,message:o.message})),s=Is(t,{overall:e.overall,grade:e.grade,categories:e.categories,errors:n,summary:e.summary});N("diff-result",s)}function Li(e){ks(e.nodeId)}async function Ri(e){try{let t=await figma.getNodeByIdAsync(e.nodeId);if(!t||!("type"in t)){N("fix-preview",{success:!1,error:"Node not found or is not a valid scene node"});return}let n=t,s=null;if(e.type==="token"){if(!e.propertyPath){N("fix-preview",{success:!1,error:"Property path is required for token fixes"});return}if(e.propertyPath.match(/^(fills|strokes)\[(\d+)\]$/)){let r=await It(e.suggestedValue||"",.1);r.length>0&&(s=await Pt(n,e.propertyPath,r[0].variableId))}else{let r=parseFloat(e.suggestedValue||"0"),i=await At(r,e.propertyPath||"",2);i.length>0&&(s=await Pt(n,e.propertyPath,i[0].variableId))}if(s){let r=s;N("fix-preview",{success:!0,type:"token",nodeId:r.nodeId,nodeName:r.nodeName,propertyPath:r.propertyPath,beforeValue:r.beforeValue,afterValue:r.afterValue,tokenId:r.tokenId,tokenName:r.tokenName})}else N("fix-preview",{success:!1,error:"No matching token found for this value"})}else if(e.type==="naming"){let o=e.suggestedValue||ue(n);s=nn(n,o),N("fix-preview",{success:!0,preview:s})}else N("fix-preview",{success:!1,error:`Unknown fix type: ${e.type}`})}catch(t){console.error("Error previewing fix:",t);let n=t instanceof Error?t.message:"Unknown error occurred";N("fix-preview",{success:!1,error:n})}}async function $i(e){try{let t=await figma.getNodeByIdAsync(e.nodeId);if(!t||!("type"in t)){N("fix-applied",{success:!1,error:"Node not found or is not a valid scene node"}),figma.notify("Failed to apply fix: Node not found",{error:!0});return}let n=t;if(!e.propertyPath){N("fix-applied",{success:!1,error:"Property path is required for token fixes"}),figma.notify("Failed to apply fix: Property path missing",{error:!0});return}if(!e.tokenId){N("fix-applied",{success:!1,error:"Token ID is required for token fixes"}),figma.notify("Failed to apply fix: Token ID missing",{error:!0});return}let s;/^(fills|strokes)\[\d+\]$/.test(e.propertyPath)?s=await Et(n,e.propertyPath,e.tokenId):s=await Tt(n,e.propertyPath,e.tokenId),N("fix-applied",z(R({},s),{fixType:"token",nodeId:e.nodeId,propertyPath:e.propertyPath})),s.success?figma.notify(`Applied token to ${n.name}`,{timeout:2e3}):figma.notify(`Failed to apply token: ${s.error||s.message}`,{error:!0})}catch(t){console.error("Error applying token fix:",t);let n=t instanceof Error?t.message:"Unknown error occurred";N("fix-applied",{success:!1,error:n,fixType:"token",nodeId:e.nodeId}),figma.notify(`Failed to apply fix: ${n}`,{error:!0})}}async function Mi(e){try{let t=await figma.getNodeByIdAsync(e.nodeId);if(!t||!("type"in t)){N("fix-applied",{success:!1,error:"Node not found or is not a valid scene node"}),figma.notify("Failed to rename: Node not found",{error:!0});return}let n=t,s=e.newValue||ue(n),o=n.name;if(o===s){N("fix-applied",{success:!0,fixType:"naming",nodeId:e.nodeId,message:`Layer already named "${s}"`,oldName:o,newName:s}),figma.notify(`Layer already named "${s}"`,{timeout:2e3});return}let r=rt(n,s),i={success:r,fixType:"naming",nodeId:e.nodeId,message:r?`Renamed "${o}" to "${s}"`:"Failed to rename layer",oldName:o,newName:r?s:o};N("fix-applied",i),r?figma.notify(`Renamed "${o}" to "${s}"`,{timeout:2e3}):figma.notify("Failed to rename layer",{error:!0})}catch(t){console.error("Error applying naming fix:",t);let n=t instanceof Error?t.message:"Unknown error occurred";N("fix-applied",{success:!1,error:n}),figma.notify(`Failed to rename: ${n}`,{error:!0})}}async function Oi(e){try{let t=[],n=0,s=0;for(let r of e.fixes)try{let i=await figma.getNodeByIdAsync(r.nodeId);if(!i||!("type"in i)){t.push({nodeId:r.nodeId,success:!1,message:"Node not found",error:"Node not found or is not a valid scene node"}),s++;continue}let a=i;if(r.type==="token"){if(!r.propertyPath){t.push({nodeId:r.nodeId,success:!1,message:"Missing property path",error:"Token fixes require a propertyPath"}),s++;continue}let c=r.tokenId,l=/^(fills|strokes)\[\d+\]$/.test(r.propertyPath);if(!c&&r.newValue)try{if(l){let p=await It(r.newValue,.1);p.length>0&&(c=p[0].variableId)}else{let p=parseFloat(r.newValue);if(!isNaN(p)){let u=await At(p,r.propertyPath||"",2);u.length>0&&(c=u[0].variableId)}}}catch(p){console.warn("Could not find matching variable:",p)}if(!c){t.push({nodeId:r.nodeId,success:!1,message:"No matching design token found for this value",error:"Could not find a matching variable to bind"}),s++;continue}let d;l?d=await Et(a,r.propertyPath,c):d=await Tt(a,r.propertyPath,c),t.push({nodeId:r.nodeId,success:d.success,message:d.message,error:d.error}),d.success?n++:s++}else if(r.type==="naming"){let c=r.newValue||ue(a),l=a.name,d=rt(a,c);t.push({nodeId:r.nodeId,success:d,message:d?`Renamed "${l}" to "${c}"`:"Failed to rename layer"}),d?n++:s++}else t.push({nodeId:r.nodeId,success:!1,message:`Unknown fix type: ${r.type}`,error:`Unsupported fix type: ${r.type}`}),s++}catch(i){let a=i instanceof Error?i.message:"Unknown error";t.push({nodeId:r.nodeId,success:!1,message:"Error applying fix",error:a}),s++}let o={total:e.fixes.length,success:n,errors:s,results:t};N("batch-fix-applied",o),s===0?figma.notify(`Applied ${n} fix${n!==1?"es":""} successfully`,{timeout:2e3}):n>0?figma.notify(`Applied ${n} fix${n!==1?"es":""}, ${s} failed`,{timeout:3e3}):figma.notify(`Failed to apply ${s} fix${s!==1?"es":""}`,{error:!0})}catch(t){console.error("Error applying batch fixes:",t);let n=t instanceof Error?t.message:"Unknown error occurred";N("batch-fix-applied",{total:e.fixes.length,success:0,errors:e.fixes.length,error:n}),figma.notify(`Batch fix failed: ${n}`,{error:!0})}}async function Fi(e){try{let t=await figma.getNodeByIdAsync(e.nodeId);if(!t){N("description-updated",{success:!1,error:"Node not found"}),figma.notify("Failed to update description: Node not found",{error:!0});return}if(t.type!=="COMPONENT"&&t.type!=="COMPONENT_SET"){N("description-updated",{success:!1,error:"Node is not a component or component set"}),figma.notify("Description can only be set on components",{error:!0});return}let n=t,s=n.description;n.description=e.description,N("description-updated",{success:!0,oldDescription:s,newDescription:e.description}),figma.notify("Component description updated",{timeout:2e3})}catch(t){console.error("Error updating description:",t);let n=t instanceof Error?t.message:"Unknown error occurred";N("description-updated",{success:!1,error:n}),figma.notify(`Failed to update description: ${n}`,{error:!0})}}async function Di(e){try{let{nodeId:t,propertyName:n,propertyType:s,defaultValue:o}=e,r=await figma.getNodeByIdAsync(t);if(!r){N("property-added",{success:!1,propertyName:n,message:"Node not found"}),figma.notify("Node not found",{error:!0});return}let i=null;if(r.type==="COMPONENT"){let d=r;d.parent&&d.parent.type==="COMPONENT_SET"?i=d.parent:i=d}else if(r.type==="COMPONENT_SET")i=r;else if(r.type==="INSTANCE"){let d=await r.getMainComponentAsync();d&&(d.parent&&d.parent.type==="COMPONENT_SET"?i=d.parent:i=d)}if(!i){N("property-added",{success:!1,propertyName:n,message:"Selected node is not a component"}),figma.notify("Selected node is not a component",{error:!0});return}let a=i.componentPropertyDefinitions;for(let d of Object.keys(a))if(d.replace(/#\d+:\d+$/,"").toLowerCase()===n.toLowerCase()){N("property-added",{success:!1,propertyName:n,message:`Property "${n}" already exists`}),figma.notify(`Property "${n}" already exists`,{error:!0});return}let c;switch(s.toLowerCase()){case"boolean":c="BOOLEAN";break;case"text":c="TEXT";break;case"slot":c="INSTANCE_SWAP";break;case"variant":i.type==="COMPONENT_SET"?c="VARIANT":c="TEXT";break;default:c="TEXT"}i.addComponentProperty(n,c,o);let l="";if(c==="VARIANT"&&i.type==="COMPONENT_SET"&&e.variantOptions&&e.variantOptions.length>1){let d=i,p=[...d.children],u=e.variantOptions.slice(1),f=`${n}=${o}`,y=figma.currentPage,m=d;for(;m.parent&&m.parent.type!=="PAGE";)m=m.parent;let v=m.absoluteTransform[0][2],A=m.absoluteTransform[1][2],w=v,k=A+m.height+50,g=figma.createSection();g.name=`FigmaLint: ${n} Variants`,y.appendChild(g),g.x=w,g.y=k;let h=figma.createText();await figma.loadFontAsync({family:"Inter",style:"Medium"}),h.fontName={family:"Inter",style:"Medium"},h.characters=`New "${n}" variants \u2014 drag into the ComponentSet`,h.fontSize=14,h.fills=[{type:"SOLID",color:{r:.4,g:.4,b:.4}}],g.appendChild(h),h.x=24,h.y=24;let x=24,S=32,P=h.y+h.height+24,O=h.width+x*2;for(let M of u){let B=`${n}=${M}`,C=figma.createText();await figma.loadFontAsync({family:"Inter",style:"Semi Bold"}),C.fontName={family:"Inter",style:"Semi Bold"},C.characters=`${n}=${M}`,C.fontSize=12,C.fills=[{type:"SOLID",color:{r:.6,g:.3,b:.9}}],g.appendChild(C),C.x=x,C.y=P,P+=C.height+12;let $=x,b=0;for(let T of p){let L=T.clone();L.name=L.name.replace(f,B),g.appendChild(L),L.x=$,L.y=P,$+=L.width+S,b=Math.max(b,L.height)}O=Math.max(O,$-S+x),P+=b+S}g.resizeWithoutConstraints(Math.max(O,400),P+x),l=" \u2014 new variants created in staging section to the right"}N("property-added",{success:!0,propertyName:n,message:`Property "${n}" added successfully${l}`}),figma.notify(`Property "${n}" added${l?" (see staging section)":""}`,{timeout:3e3})}catch(t){console.error("Error adding component property:",t);let n=t instanceof Error?t.message:"Unknown error occurred";N("property-added",{success:!1,propertyName:e.propertyName,message:n}),figma.notify(`Failed to add property: ${n}`,{error:!0})}}var Vi={width:380,height:600,themeColors:!0};try{figma.showUI(__html__,Vi),console.log("\u2705 FigmaLint v2.0 - UI shown successfully")}catch(e){console.log("\u2139\uFE0F UI might already be shown in inspect panel:",e)}figma.ui.onmessage=$s;figma.on("selectionchange",()=>{let e=figma.currentPage.selection;figma.ui.postMessage({type:"selection-changed",data:{hasSelection:e.length>0,nodeId:e.length>0?e[0].id:null,nodeName:e.length>0?e[0].name:null}})});Fs();console.log("\u{1F680} FigmaLint v2.0 initialized with modular architecture");})(); +Respond naturally and helpfully to the user's question.`}var V=R({},j),Le=null;function Re(e){let t=(e==null?void 0:e.settings)||V;(!Le||e!=null&&e.resetScope)&&(Le=figma.currentPage.selection.map(o=>o.id));let n=Le.map(o=>figma.getNodeById(o)).filter(o=>o!==null&&o.type!=="DOCUMENT"&&o.type!=="PAGE");n.length>0&&(figma.currentPage.selection=n);let s=xe(t);k("design-lint-result",s)}function Ye(){try{let e=It();figma.root.setPluginData("ignoredState",JSON.stringify(e))}catch(e){}}function ca(e){kt(e.nodeId),Ye(),Re()}function la(e){Nt(e.nodeId,e.errorType,e.value),Ye(),Re()}function da(e){let t=xe(V);wt(t.errors,e.errorType),Ye(),Re()}function ua(){Ct(),Ye(),Re()}function pa(e){try{let t=figma.getNodeById(e.nodeId);t&&t.type!=="DOCUMENT"&&t.type!=="PAGE"&&(figma.currentPage.selection=[t],figma.viewport.scrollAndZoomIntoView([t]))}catch(t){console.warn("Could not select node:",e.nodeId,t)}}function ma(e){let t=figma.currentPage.selection;if(t.length===0)return;let n=At(t,e.errorType,e.value,V),o=[...new Set(n.map(r=>r.nodeId))].map(r=>figma.getNodeById(r)).filter(r=>r!==null&&r.type!=="DOCUMENT"&&r.type!=="PAGE");o.length>0&&(figma.currentPage.selection=o,figma.viewport.scrollAndZoomIntoView(o),k("lint-selected-nodes",{count:o.length,value:e.value}))}async function fa(e){V=e.settings;try{await figma.clientStorage.setAsync("design-lint-settings",e.settings)}catch(t){console.warn("Could not save lint settings:",t)}}async function ga(){try{let e=await figma.clientStorage.getAsync("design-lint-settings");e&&(V=R(R({},j),e)),k("lint-settings-loaded",V)}catch(e){console.warn("Could not load lint settings:",e),k("lint-settings-loaded",j)}}function ya(e){try{let t=e.config;if(!t||t.version!==1){k("team-config-saved",{success:!1,error:"Invalid config version"});return}figma.root.setSharedPluginData("figmalint","config",JSON.stringify(t)),k("team-config-saved",{success:!0})}catch(t){k("team-config-saved",{success:!1,error:String(t)})}}function ha(){var e,t;try{let n=figma.root.getSharedPluginData("figmalint","config");if(n){let s=JSON.parse(n);V=R({},j),(e=s.scales)!=null&&e.spacing&&(V.spacingScale=s.scales.spacing),(t=s.scales)!=null&&t.radius&&(V.allowedRadii=s.scales.radius),s.severityOverrides&&(V.severityOverrides=s.severityOverrides),s.ignorePatterns&&(V.ignorePatterns=s.ignorePatterns),k("team-config-loaded",{config:s,settings:V})}else k("team-config-loaded",{config:null,settings:V})}catch(n){console.warn("Could not load team config:",n),k("team-config-loaded",{config:null,settings:V})}}function ba(e){try{let t=figma.getNodeById(e.nodeId);t&&t.type!=="DOCUMENT"&&t.type!=="PAGE"&&(figma.currentPage.selection=[t],figma.viewport.scrollAndZoomIntoView([t]))}catch(t){console.warn("Could not jump to node:",e.nodeId,t)}}var va=new Set(["itemSpacing","paddingTop","paddingBottom","paddingLeft","paddingRight","counterAxisSpacing"]);function Sa(e){try{if(!va.has(e.property)){k("fix-error",{error:`Invalid spacing property: ${e.property}`});return}let t=figma.getNodeById(e.nodeId);if(t&&(t.type==="FRAME"||t.type==="COMPONENT"||t.type==="INSTANCE")){let n=t[e.property];t[e.property]=e.value,k("fix-applied",{type:"spacing",nodeId:e.nodeId,nodeName:t.name,property:e.property,oldValue:n,newValue:e.value})}}catch(t){console.warn("Could not fix spacing:",t),k("fix-error",{error:"Failed to apply spacing fix"})}}async function ka(e){try{let t=null;if(e!=null&&e.nodeId){let i=figma.getNodeById(e.nodeId);i&&i.type!=="DOCUMENT"&&i.type!=="PAGE"&&(t=i)}else figma.currentPage.selection.length>0&&(t=figma.currentPage.selection[0]);if(!t){k("screenshot-error",{error:"No node selected"});return}let n=await ze(t),s="layoutMode"in t&&t.layoutMode!=="NONE",o="children"in t?t.children.length:0,r;try{let i=await ue(t),a=[...i.colors,...i.spacing,...i.typography,...i.effects,...i.borders];r={totalTokens:a.length,boundToVariables:a.filter(c=>c.source==="figma-variable").length,boundToStyles:a.filter(c=>c.source==="figma-style").length,hardCoded:a.filter(c=>c.source==="hard-coded").length}}catch(i){}k("screenshot-result",{nodeId:t.id,nodeName:t.name,nodeType:t.type,screenshot:n,width:t.width,height:t.height,hasAutoLayout:s,childCount:o,tokenSummary:r})}catch(t){console.warn("Could not export screenshot:",t),k("screenshot-error",{error:"Failed to export screenshot"})}}function Na(e){try{let t=Ae(e.nodeId,e.property);k("fix-applied",{type:"spacing",nodeId:t.nodeId,nodeName:t.nodeName,property:e.property,oldValue:t.oldValue,newValue:t.newValue,success:t.success,error:t.error})}catch(t){k("fix-error",{error:"Failed to auto-fix spacing"})}}function wa(e){try{let t=e.allowedRadii||[0,2,4,8,12,16,20,24,32],n=qe(e.nodeId,t);k("fix-applied",{type:"radius",nodeId:n.nodeId,nodeName:n.nodeName,oldValue:n.oldValue,newValue:n.newValue,success:n.success,error:n.error})}catch(t){k("fix-error",{error:"Failed to auto-fix radius"})}}function Ca(e){try{let t=no(e.nodeId),n=t.filter(s=>s.success).length;for(let s of t)k("fix-applied",{type:"spacing",nodeId:s.nodeId,nodeName:s.nodeName,property:s.property,oldValue:s.oldValue,newValue:s.newValue,success:s.success});n>0&&figma.notify(`Fixed ${n} spacing value${n!==1?"s":""}`,{timeout:2e3})}catch(t){k("fix-error",{error:"Failed to fix all spacing"})}}async function Ia(e){try{let{applyFillStyle:t,applyStrokeStyle:n,applyTextStyle:s,applyEffectStyle:o}=await Promise.resolve().then(()=>(jt(),so)),r;switch(e.styleType){case"fill":r=await t(e.nodeId,e.styleKey);break;case"stroke":r=await n(e.nodeId,e.styleKey);break;case"text":r=await s(e.nodeId,e.styleKey);break;case"effect":r=await o(e.nodeId,e.styleKey);break;default:k("fix-error",{error:`Unknown style type: ${e.styleType}`});return}k("fix-applied",{type:"style",nodeId:r.nodeId,nodeName:r.nodeName,property:r.property,oldValue:r.oldValue,newValue:r.newValue,success:r.success,error:r.error})}catch(t){k("fix-error",{error:"Failed to apply style"})}}function xa(e){try{let t=Je(e.nodeId,e.newName);k("fix-applied",{type:"rename",nodeId:t.nodeId,nodeName:t.newName,oldValue:t.oldName,newValue:t.newName,success:t.success,error:t.error})}catch(t){k("fix-error",{error:"Failed to rename layer"})}}async function Aa(e){try{let t=await oo(e.fixes);k("batch-fix-v2-result",t),t.failed===0?figma.notify(`Applied ${t.applied} fix${t.applied!==1?"es":""} successfully`,{timeout:2e3}):t.applied>0?figma.notify(`Applied ${t.applied}, ${t.failed} failed`,{timeout:3e3}):figma.notify(`All ${t.failed} fixes failed`,{error:!0}),So()}catch(t){k("fix-error",{error:"Batch fix failed"})}}function So(){if(Le){let t=Le.map(n=>figma.getNodeById(n)).filter(n=>n!==null&&n.type!=="DOCUMENT"&&n.type!=="PAGE");t.length>0&&(figma.currentPage.selection=t)}let e=xe(V);k("design-lint-result",e),k("rescan-complete",{totalErrors:e.summary.totalErrors,nodesWithErrors:e.summary.nodesWithErrors})}async function Ea(){try{k("flow-analysis-started",{status:"building-graph"});let e=Gs();if(e.frames.length===0){k("flow-analysis-error",{error:"No top-level frames found on current page."});return}if(e.frames.length>50){k("flow-analysis-error",{error:`Too many frames (${e.frames.length}). Select a page with \u226450 frames for flow analysis.`});return}let t=zs(e);k("flow-analysis-started",{status:"capturing-screenshots",total:e.frames.length});let n={},s={},o=10;for(let c=0;c<e.frames.length;c+=o){let l=e.frames.slice(c,c+o).map(async p=>{let u=await figma.getNodeByIdAsync(p.id);if(!(!u||!("exportAsync"in u))){try{let g=await ze(u);n[p.id]=g}catch(g){}try{let{runDesignLint:g}=await Promise.resolve().then(()=>(be(),Et)),f=g([u],V);s[p.id]=f}catch(g){}}});await Promise.all(l),k("flow-analysis-started",{status:"capturing-screenshots",progress:Math.min(c+o,e.frames.length),total:e.frames.length})}let r=[];for(let c of e.frames){let d=await figma.getNodeByIdAsync(c.id);d&&r.push({frame:c,node:d})}let i=Ks(r,{skipLocked:V.skipLockedLayers,skipHidden:V.skipHiddenLayers}),a=[...t,...i];k("flow-analysis-result",{graph:e,graphIssues:a,screenshots:n,lintResults:s})}catch(e){let t=e instanceof Error?e.message:"Unknown error";k("flow-analysis-error",{error:t})}}async function Ta(){try{let t=figma.currentPage.children.filter(l=>l.type==="FRAME"||l.type==="COMPONENT_SET");if(t.length===0){k("analysis-error",{error:"No top-level frames found on current page."});return}let n=t.slice(0,50),s=n.length,{runDesignLint:o}=await Promise.resolve().then(()=>(be(),Et)),r=[],i=5;for(let l=0;l<s;l+=i){let u=n.slice(l,l+i).map(async(f,m)=>{let h=l+m+1;k("page-sweep-progress",{current:h,total:s,frameName:f.name});let C={summary:{totalErrors:0,byType:{},totalNodes:0,nodesWithErrors:0},errors:[]};try{C=o([f],V)}catch(N){}let S="";try{S=await ze(f,800)}catch(N){}return{id:f.id,name:f.name,screenshot:S,lintResult:{summary:C.summary,errors:C.errors},width:Math.round(f.width),height:Math.round(f.height)}}),g=await Promise.all(u);r.push(...g)}let a=0,c={};for(let l of r){a+=l.lintResult.summary.totalErrors||0;for(let p of l.lintResult.errors){let u=p.errorType;c[u]||(c[u]={count:0,severity:p.severity||"warning"}),c[u].count++}}let d=Object.entries(c).sort((l,p)=>p[1].count-l[1].count).slice(0,10).map(([l,{count:p,severity:u}])=>({type:l,count:p,severity:u}));k("page-sweep-result",{frames:r,aggregated:{totalFrames:s,totalIssues:a,topIssues:d}})}catch(e){let t=e instanceof Error?e.message:"Unknown error";k("analysis-error",{error:`Page sweep failed: ${t}`})}}async function ko(){var e,t;try{let n=await dt();_=n.providerId,H=n.modelId,n.apiKey?(Z=n.apiKey,k("api-key-status",{hasKey:!0,provider:_,model:H})):k("api-key-status",{hasKey:!1,provider:_,model:H}),console.log(`Plugin initialized with provider: ${_}, model: ${H}`);try{let s=figma.root.getPluginData("ignoredState");if(s){let o=JSON.parse(s);xt(o),console.log(`Restored ${((e=o.nodeIds)==null?void 0:e.length)||0} ignored nodes, ${((t=o.errorKeys)==null?void 0:t.length)||0} ignored errors`)}}catch(s){}console.log("\u{1F504} Initializing design systems knowledge..."),Q.loadDesignSystemsKnowledge().then(()=>{console.log("\u2705 Design systems knowledge loaded successfully")}).catch(s=>{console.warn("\u26A0\uFE0F Failed to load design systems knowledge, using fallback:",s)}),console.log("Plugin initialized successfully")}catch(n){console.error("Error initializing plugin:",n)}}function Pa(e){let t={version:1,timestamp:Date.now(),nodeId:e.nodeId,nodeName:e.nodeName,overall:e.overall,grade:e.grade,categories:e.categories,summary:e.summary,errors:e.errors.map(n=>({errorType:n.errorType,severity:n.severity||"warning",nodeId:n.nodeId,message:n.message}))};Xs(t),k("baseline-saved",{nodeId:e.nodeId,nodeName:e.nodeName,timestamp:t.timestamp,overall:e.overall})}function La(e){let t=Zs(e.nodeId);k("baseline-loaded",t)}function Ra(e){let t=Ys(e.nodeId);if(!t){k("diff-result",null);return}let n=e.errors.map(o=>({errorType:o.errorType,severity:o.severity||"warning",nodeId:o.nodeId,message:o.message})),s=to(t,{overall:e.overall,grade:e.grade,categories:e.categories,errors:n,summary:e.summary});k("diff-result",s)}function $a(e){Qs(e.nodeId)}async function Ma(e){try{let t=await figma.getNodeByIdAsync(e.nodeId);if(!t||!("type"in t)){k("fix-preview",{success:!1,error:"Node not found or is not a valid scene node"});return}let n=t,s=null;if(e.type==="token"){if(!e.propertyPath){k("fix-preview",{success:!1,error:"Property path is required for token fixes"});return}if(e.propertyPath.match(/^(fills|strokes)\[(\d+)\]$/)){let r=await Ot(e.suggestedValue||"",.1);r.length>0&&(s=await _t(n,e.propertyPath,r[0].variableId))}else{let r=parseFloat(e.suggestedValue||"0"),i=await Ft(r,e.propertyPath||"",2);i.length>0&&(s=await _t(n,e.propertyPath,i[0].variableId))}if(s){let r=s;k("fix-preview",{success:!0,type:"token",nodeId:r.nodeId,nodeName:r.nodeName,propertyPath:r.propertyPath,beforeValue:r.beforeValue,afterValue:r.afterValue,tokenId:r.tokenId,tokenName:r.tokenName})}else k("fix-preview",{success:!1,error:"No matching token found for this value"})}else if(e.type==="naming"){let o=e.suggestedValue||fe(n);s=mn(n,o),k("fix-preview",{success:!0,preview:s})}else k("fix-preview",{success:!1,error:`Unknown fix type: ${e.type}`})}catch(t){console.error("Error previewing fix:",t);let n=t instanceof Error?t.message:"Unknown error occurred";k("fix-preview",{success:!1,error:n})}}async function Oa(e){try{let t=await figma.getNodeByIdAsync(e.nodeId);if(!t||!("type"in t)){k("fix-applied",{success:!1,error:"Node not found or is not a valid scene node"}),figma.notify("Failed to apply fix: Node not found",{error:!0});return}let n=t;if(!e.propertyPath){k("fix-applied",{success:!1,error:"Property path is required for token fixes"}),figma.notify("Failed to apply fix: Property path missing",{error:!0});return}if(!e.tokenId){k("fix-applied",{success:!1,error:"Token ID is required for token fixes"}),figma.notify("Failed to apply fix: Token ID missing",{error:!0});return}let s;/^(fills|strokes)\[\d+\]$/.test(e.propertyPath)?s=await Dt(n,e.propertyPath,e.tokenId):s=await Vt(n,e.propertyPath,e.tokenId),k("fix-applied",K(R({},s),{fixType:"token",nodeId:e.nodeId,propertyPath:e.propertyPath})),s.success?figma.notify(`Applied token to ${n.name}`,{timeout:2e3}):figma.notify(`Failed to apply token: ${s.error||s.message}`,{error:!0})}catch(t){console.error("Error applying token fix:",t);let n=t instanceof Error?t.message:"Unknown error occurred";k("fix-applied",{success:!1,error:n,fixType:"token",nodeId:e.nodeId}),figma.notify(`Failed to apply fix: ${n}`,{error:!0})}}async function Fa(e){try{let t=await figma.getNodeByIdAsync(e.nodeId);if(!t||!("type"in t)){k("fix-applied",{success:!1,error:"Node not found or is not a valid scene node"}),figma.notify("Failed to rename: Node not found",{error:!0});return}let n=t,s=e.newValue||fe(n),o=n.name;if(o===s){k("fix-applied",{success:!0,fixType:"naming",nodeId:e.nodeId,message:`Layer already named "${s}"`,oldName:o,newName:s}),figma.notify(`Layer already named "${s}"`,{timeout:2e3});return}let r=pt(n,s),i={success:r,fixType:"naming",nodeId:e.nodeId,message:r?`Renamed "${o}" to "${s}"`:"Failed to rename layer",oldName:o,newName:r?s:o};k("fix-applied",i),r?figma.notify(`Renamed "${o}" to "${s}"`,{timeout:2e3}):figma.notify("Failed to rename layer",{error:!0})}catch(t){console.error("Error applying naming fix:",t);let n=t instanceof Error?t.message:"Unknown error occurred";k("fix-applied",{success:!1,error:n}),figma.notify(`Failed to rename: ${n}`,{error:!0})}}async function Da(e){try{let t=[],n=0,s=0;for(let r of e.fixes)try{let i=await figma.getNodeByIdAsync(r.nodeId);if(!i||!("type"in i)){t.push({nodeId:r.nodeId,success:!1,message:"Node not found",error:"Node not found or is not a valid scene node"}),s++;continue}let a=i;if(r.type==="token"){if(!r.propertyPath){t.push({nodeId:r.nodeId,success:!1,message:"Missing property path",error:"Token fixes require a propertyPath"}),s++;continue}let c=r.tokenId,d=/^(fills|strokes)\[\d+\]$/.test(r.propertyPath);if(!c&&r.newValue)try{if(d){let p=await Ot(r.newValue,.1);p.length>0&&(c=p[0].variableId)}else{let p=parseFloat(r.newValue);if(!isNaN(p)){let u=await Ft(p,r.propertyPath||"",2);u.length>0&&(c=u[0].variableId)}}}catch(p){console.warn("Could not find matching variable:",p)}if(!c){t.push({nodeId:r.nodeId,success:!1,message:"No matching design token found for this value",error:"Could not find a matching variable to bind"}),s++;continue}let l;d?l=await Dt(a,r.propertyPath,c):l=await Vt(a,r.propertyPath,c),t.push({nodeId:r.nodeId,success:l.success,message:l.message,error:l.error}),l.success?n++:s++}else if(r.type==="naming"){let c=r.newValue||fe(a),d=a.name,l=pt(a,c);t.push({nodeId:r.nodeId,success:l,message:l?`Renamed "${d}" to "${c}"`:"Failed to rename layer"}),l?n++:s++}else t.push({nodeId:r.nodeId,success:!1,message:`Unknown fix type: ${r.type}`,error:`Unsupported fix type: ${r.type}`}),s++}catch(i){let a=i instanceof Error?i.message:"Unknown error";t.push({nodeId:r.nodeId,success:!1,message:"Error applying fix",error:a}),s++}let o={total:e.fixes.length,success:n,errors:s,results:t};k("batch-fix-applied",o),s===0?figma.notify(`Applied ${n} fix${n!==1?"es":""} successfully`,{timeout:2e3}):n>0?figma.notify(`Applied ${n} fix${n!==1?"es":""}, ${s} failed`,{timeout:3e3}):figma.notify(`Failed to apply ${s} fix${s!==1?"es":""}`,{error:!0})}catch(t){console.error("Error applying batch fixes:",t);let n=t instanceof Error?t.message:"Unknown error occurred";k("batch-fix-applied",{total:e.fixes.length,success:0,errors:e.fixes.length,error:n}),figma.notify(`Batch fix failed: ${n}`,{error:!0})}}async function Va(e){try{let t=await figma.getNodeByIdAsync(e.nodeId);if(!t){k("description-updated",{success:!1,error:"Node not found"}),figma.notify("Failed to update description: Node not found",{error:!0});return}if(t.type!=="COMPONENT"&&t.type!=="COMPONENT_SET"){k("description-updated",{success:!1,error:"Node is not a component or component set"}),figma.notify("Description can only be set on components",{error:!0});return}let n=t,s=n.description;n.description=e.description,k("description-updated",{success:!0,oldDescription:s,newDescription:e.description}),figma.notify("Component description updated",{timeout:2e3})}catch(t){console.error("Error updating description:",t);let n=t instanceof Error?t.message:"Unknown error occurred";k("description-updated",{success:!1,error:n}),figma.notify(`Failed to update description: ${n}`,{error:!0})}}async function _a(e){try{let{nodeId:t,propertyName:n,propertyType:s,defaultValue:o}=e,r=await figma.getNodeByIdAsync(t);if(!r){k("property-added",{success:!1,propertyName:n,message:"Node not found"}),figma.notify("Node not found",{error:!0});return}let i=null;if(r.type==="COMPONENT"){let l=r;l.parent&&l.parent.type==="COMPONENT_SET"?i=l.parent:i=l}else if(r.type==="COMPONENT_SET")i=r;else if(r.type==="INSTANCE"){let l=await r.getMainComponentAsync();l&&(l.parent&&l.parent.type==="COMPONENT_SET"?i=l.parent:i=l)}if(!i){k("property-added",{success:!1,propertyName:n,message:"Selected node is not a component"}),figma.notify("Selected node is not a component",{error:!0});return}let a=i.componentPropertyDefinitions;for(let l of Object.keys(a))if(l.replace(/#\d+:\d+$/,"").toLowerCase()===n.toLowerCase()){k("property-added",{success:!1,propertyName:n,message:`Property "${n}" already exists`}),figma.notify(`Property "${n}" already exists`,{error:!0});return}let c;switch(s.toLowerCase()){case"boolean":c="BOOLEAN";break;case"text":c="TEXT";break;case"slot":c="INSTANCE_SWAP";break;case"variant":i.type==="COMPONENT_SET"?c="VARIANT":c="TEXT";break;default:c="TEXT"}i.addComponentProperty(n,c,o);let d="";if(c==="VARIANT"&&i.type==="COMPONENT_SET"&&e.variantOptions&&e.variantOptions.length>1){let l=i,p=[...l.children],u=e.variantOptions.slice(1),g=`${n}=${o}`,f=figma.currentPage,m=l;for(;m.parent&&m.parent.type!=="PAGE";)m=m.parent;let h=m.absoluteTransform[0][2],C=m.absoluteTransform[1][2],S=h,N=C+m.height+50,y=figma.createSection();y.name=`FigmaLint: ${n} Variants`,f.appendChild(y),y.x=S,y.y=N;let b=figma.createText();await figma.loadFontAsync({family:"Inter",style:"Medium"}),b.fontName={family:"Inter",style:"Medium"},b.characters=`New "${n}" variants \u2014 drag into the ComponentSet`,b.fontSize=14,b.fills=[{type:"SOLID",color:{r:.4,g:.4,b:.4}}],y.appendChild(b),b.x=24,b.y=24;let I=24,w=32,P=b.y+b.height+24,O=b.width+I*2;for(let M of u){let z=`${n}=${M}`,x=figma.createText();await figma.loadFontAsync({family:"Inter",style:"Semi Bold"}),x.fontName={family:"Inter",style:"Semi Bold"},x.characters=`${n}=${M}`,x.fontSize=12,x.fills=[{type:"SOLID",color:{r:.6,g:.3,b:.9}}],y.appendChild(x),x.x=I,x.y=P,P+=x.height+12;let $=I,v=0;for(let T of p){let L=T.clone();L.name=L.name.replace(g,z),y.appendChild(L),L.x=$,L.y=P,$+=L.width+w,v=Math.max(v,L.height)}O=Math.max(O,$-w+I),P+=v+w}y.resizeWithoutConstraints(Math.max(O,400),P+I),d=" \u2014 new variants created in staging section to the right"}k("property-added",{success:!0,propertyName:n,message:`Property "${n}" added successfully${d}`}),figma.notify(`Property "${n}" added${d?" (see staging section)":""}`,{timeout:3e3})}catch(t){console.error("Error adding component property:",t);let n=t instanceof Error?t.message:"Unknown error occurred";k("property-added",{success:!1,propertyName:e.propertyName,message:n}),figma.notify(`Failed to add property: ${n}`,{error:!0})}}async function Ba(){try{let e=await qt();k("variable-system-result",e)}catch(e){console.error("Error collecting variables:",e);let t=e instanceof Error?e.message:"Unknown error";k("variable-system-error",{error:t})}}async function Ua(e){try{let t=ro(e.dtcgJson),n=await qt(),s=ao(n,t,null);k("dtcg-compliance-result",s)}catch(t){console.error("Error checking DTCG compliance:",t);let n=t instanceof Error?t.message:"Unknown error";k("dtcg-compliance-error",{error:n})}}async function Ga(e){try{let t=await co(e.collectionId);k("mode-comparison-result",t)}catch(t){console.error("Error comparing modes:",t);let n=t instanceof Error?t.message:"Unknown error";k("mode-comparison-error",{error:n})}}function za(e){let t=e.settings||j;uo({enabled:!0,debounceMs:e.debounceMs||500,settings:t})}function Wa(){po()}function Ha(e){let t=fo(e.lintResult,e.tokenSummary||null);k("design-debt-result",t)}var Ka={width:380,height:600,themeColors:!0};try{figma.showUI(__html__,Ka),console.log("\u2705 FigmaLint v2.0 - UI shown successfully")}catch(e){console.log("\u2139\uFE0F UI might already be shown in inspect panel:",e)}figma.ui.onmessage=bo;figma.on("selectionchange",()=>{let e=figma.currentPage.selection;figma.ui.postMessage({type:"selection-changed",data:{hasSelection:e.length>0,nodeId:e.length>0?e[0].id:null,nodeName:e.length>0?e[0].name:null}})});ko();console.log("\u{1F680} FigmaLint v2.0 initialized with modular architecture");})(); diff --git a/dist/ui.html b/dist/ui.html index 59cfe5e..298c20f 100644 --- a/dist/ui.html +++ b/dist/ui.html @@ -4,7 +4,7 @@ <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Design Review Chat - - +Layer: **${N.nodeName}** (${N.nodeType})`});const H=[];N.errorType==="spacing"&&N.property?H.push({id:`fix-${N.nodeId}`,label:"Fix to nearest",variant:"primary",action:"fix-single-spacing",params:{nodeId:N.nodeId,property:N.property}}):N.errorType==="radius"&&H.push({id:`fix-radius-${N.nodeId}`,label:"Fix radius to nearest",variant:"primary",action:"fix-single-radius",params:{nodeId:N.nodeId}}),H.push({id:`skip-${W}`,label:W+1{s.addMessage({kind:"ai-text",content:"Full report copied to clipboard!"})},()=>{s.addMessage({kind:"ai-text",content:"Failed to copy report to clipboard."})})}break}case"export-json":{const Q=s.lintResult;if(Q){const W={component:j||"Component",timestamp:new Date().toISOString(),lint:{summary:Q.summary,errors:Q.errors,issuesFixed:s.issuesFixed},aiReview:s.aiReview||void 0,diff:s.lastDiff||void 0};navigator.clipboard.writeText(JSON.stringify(W,null,2)).then(()=>s.addMessage({kind:"ai-text",content:"JSON report copied to clipboard!"}),()=>s.addMessage({kind:"ai-text",content:"Failed to copy JSON to clipboard."}))}break}case"save-baseline":{if(!s.score||!s.lintResult){s.addMessage({kind:"ai-text",content:"Run an analysis first before saving a baseline."});break}const Q=me.current;if(!Q)break;d("save-baseline",{nodeId:Q,nodeName:j||"Component",overall:s.score.overall,grade:s.score.grade,categories:{tokens:s.score.tokens,spacing:s.score.spacing,layout:s.score.layout,accessibility:s.score.accessibility,naming:s.score.naming,visualQuality:s.score.visualQuality,microcopy:s.score.microcopy,conversion:s.score.conversion,cognitive:s.score.cognitive},errors:s.lintResult.errors.map(W=>({errorType:W.errorType,severity:W.severity,nodeId:W.nodeId,message:W.message})),summary:s.lintResult.summary});break}case"compare-baseline":{if(!s.score||!s.lintResult){s.addMessage({kind:"ai-text",content:"Run an analysis first before comparing."});break}const Q=me.current;if(!Q)break;d("compare-baseline",{nodeId:Q,overall:s.score.overall,grade:s.score.grade,categories:{tokens:s.score.tokens,spacing:s.score.spacing,layout:s.score.layout,accessibility:s.score.accessibility,naming:s.score.naming,visualQuality:s.score.visualQuality,microcopy:s.score.microcopy,conversion:s.score.conversion,cognitive:s.score.cognitive},errors:s.lintResult.errors.map(W=>({errorType:W.errorType,severity:W.severity,nodeId:W.nodeId,message:W.message})),summary:s.lintResult.summary});break}case"analyze-flow":{s.addMessage({kind:"ai-text",content:"Starting flow analysis on current page..."}),d("analyze-flow");break}case"analyze-page":{s.addMessage({kind:"ai-text",content:"Starting whole-page sweep..."}),d("analyze-page");break}case"toggle-mode":{const Q=O==="quick"?"deep":"quick";y(Q),s.addMessage({kind:"ai-text",content:`Analysis mode: **${Q}**. ${Q==="deep"?"Refero comparison will be included in the initial response.":"Refero data loads in the background."}`});break}}},[s,d,j,O]),yt=Z.useCallback(K=>{d("jump-to-node",{nodeId:K})},[d]);return c.jsxs("div",{className:"h-full flex flex-col relative",children:[q&&c.jsx(ty,{hasApiKey:p,analysisMode:O,backendAvailable:A,onSaveApiKey:(K,G)=>d("save-api-key",{apiKey:K,provider:G}),onClearApiKey:()=>{d("clear-api-key"),v(!1)},onToggleMode:()=>{y(O==="quick"?"deep":"quick")},onClose:()=>Y(!1)}),s.messages.length===0&&!s.isAnalyzing&&c.jsxs("div",{className:"flex items-center gap-2 px-3 py-2 border-b border-border",children:[c.jsx("button",{className:"flex-1 py-2 bg-bg-brand text-fg-onbrand text-12 font-medium rounded-md hover:opacity-90 transition-opacity",onClick:we,children:"Analyze Selection"}),c.jsx("button",{className:"flex-1 py-2 bg-bg-secondary text-fg text-12 font-medium rounded-md hover:bg-bg-hover transition-colors border border-border",onClick:()=>Dt("analyze-flow"),children:"Analyze Flow"}),c.jsx("button",{className:"flex-1 py-2 bg-bg-secondary text-fg text-12 font-medium rounded-md hover:bg-bg-hover transition-colors border border-border",onClick:()=>Dt("analyze-page"),title:"Sweep all top-level frames on the page",children:"Sweep Page"}),c.jsx("button",{onClick:()=>Y(!0),className:"shrink-0 w-8 h-8 flex items-center justify-center text-fg-tertiary hover:text-fg rounded-md hover:bg-bg-hover transition-colors",title:"Settings",children:c.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2","aria-hidden":"true",children:[c.jsx("circle",{cx:"12",cy:"12",r:"3"}),c.jsx("path",{d:"M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"})]})})]}),ae&&c.jsxs("div",{className:"flex items-center gap-2 px-3 py-1.5 bg-bg-warning text-fg-warning text-11 border-b border-border",children:[c.jsxs("span",{className:"flex-1",children:["Selection changed",oe?` to "${oe}"`:"",". Results may be stale."]}),c.jsx("button",{className:"shrink-0 px-2 py-0.5 bg-bg-brand text-fg-onbrand text-11 font-medium rounded hover:opacity-90",onClick:we,children:"Re-analyze"})]}),c.jsx(ey,{state:s,componentName:j,analysisMode:O,onAnalyze:we,onSendMessage:ht,onAction:Dt,onJumpToNode:yt,onOpenSettings:()=>Y(!0)})]})}function xy(s,d,j,r,p){const v=[`# Design Review Report: ${d||"Component"}`,"",`Total lint issues: ${s.summary.totalErrors} across ${s.summary.nodesWithErrors} layers`,...j?[`Fixed: ${j}`]:[],""];v.push("## Lint Issues","");const A=s.summary.byType;if(A.fill>0&&v.push(`- **Fill styles:** ${A.fill} missing`),A.stroke>0&&v.push(`- **Stroke styles:** ${A.stroke} missing`),A.effect>0&&v.push(`- **Effect styles:** ${A.effect} missing`),A.text>0&&v.push(`- **Text styles:** ${A.text} missing`),A.radius>0&&v.push(`- **Border radius:** ${A.radius} non-standard`),A.spacing>0&&v.push(`- **Spacing:** ${A.spacing} off-grid`),A.autoLayout>0&&v.push(`- **Auto Layout:** ${A.autoLayout} missing`),A.visualQuality>0&&v.push(`- **Visual Quality:** ${A.visualQuality} issues`),A.microcopy>0&&v.push(`- **Microcopy:** ${A.microcopy} issues`),r){v.push("","## AI Design Review",""),v.push("| Category | Rating |"),v.push("|----------|--------|"),v.push(`| Visual Hierarchy | ${r.visualHierarchy.rating.toUpperCase()} |`),v.push(`| States Coverage | ${r.statesCoverage.rating.toUpperCase()} |`),v.push(`| Platform Alignment | ${r.platformAlignment.rating.toUpperCase()} (${r.platformAlignment.detectedPlatform}) |`),v.push(`| Color Harmony | ${r.colorHarmony.rating.toUpperCase()} |`),r.visualBalance&&v.push(`| Visual Balance | ${r.visualBalance.rating.toUpperCase()} |`),r.microcopyQuality&&v.push(`| Microcopy Quality | ${r.microcopyQuality.rating.toUpperCase()} |`),r.cognitiveLoad&&v.push(`| Cognitive Load | ${r.cognitiveLoad.rating.toUpperCase()} |`);const C=r.statesCoverage?.missingStates||[];if(C.length>0&&v.push("",`**Missing states:** ${C.join(", ")}`),r.recommendations.length>0){v.push("","### Recommendations","");for(const O of r.recommendations)v.push(`- **[${O.severity.toUpperCase()}]** ${O.title}: ${O.description}`)}r.summary&&v.push("",`> ${r.summary}`)}if(p){v.push("","## Baseline Comparison","");const C=p.scoreDelta.overall,O=C>0?"+":"";v.push(`Score: ${p.scoreDelta.oldOverall} → ${p.scoreDelta.newOverall} (${O}${C})`),v.push(`Baseline from: ${new Date(p.baselineTimestamp).toLocaleString()}`),v.push(""),p.summary.totalFixed>0&&v.push(`- **Fixed:** ${p.summary.totalFixed} issues`),p.summary.totalNew>0&&v.push(`- **New:** ${p.summary.totalNew} issues`),v.push(`- **Remaining:** ${p.summary.totalRemaining} issues`);const y=p.scoreDelta.categories.filter(q=>q.delta!==0);if(y.length>0){v.push("","| Category | Before | After | Delta |"),v.push("|----------|--------|-------|-------|");for(const q of y){const Y=q.delta>0?`+${q.delta}`:`${q.delta}`;v.push(`| ${q.category} | ${q.oldScore} | ${q.newScore} | ${Y} |`)}}}if(s.errors.length>0){v.push("","## All Issues","");for(const C of s.errors)v.push(`- **[${C.errorType.toUpperCase()}]** ${C.nodeName}: ${C.message}`)}return v.join(` +`)}function Xd(s){const d={critical:10,warning:3,info:1},j=s.frames.map(y=>{const q=y.lintResult.errors,Y=Math.max(y.lintResult.summary.totalNodes,1),ae=q.reduce((U,R)=>U+(d[R.severity||"warning"]||3),0),de=Math.max(0,Y-q.length)*10,oe=de+ae,F=oe>0?Math.round(de/oe*100):100,me={};for(const U of q)me[U.errorType]=(me[U.errorType]||0)+1;const D=Object.entries(me).sort((U,R)=>R[1]-U[1]).slice(0,3).map(([U,R])=>`${U} (${R})`);return{id:y.id,name:y.name,score:F,issueCount:y.lintResult.summary.totalErrors,topIssues:D}}),r=j.map(y=>y.score),p=r.length>0?Math.round(r.reduce((y,q)=>y+q,0)/r.length):100,v=p,A=r.length>0?r.reduce((y,q)=>y+Math.pow(q-v,2),0)/r.length:0,C=Math.max(0,Math.round(100-Math.sqrt(A))),O=p>=90?"excellent":p>=70?"needs-work":"poor";return{fileHealth:{overallScore:p,grade:O,totalFrames:s.aggregated.totalFrames,totalIssues:s.aggregated.totalIssues,topIssues:s.aggregated.topIssues,consistencyScore:C},frames:j,aiInsights:{strengths:[],weaknesses:[],recommendations:[],summary:"AI analysis unavailable. Scores are based on deterministic lint rules only."}}}Th.createRoot(document.getElementById("root")).render(c.jsx(yh.StrictMode,{children:c.jsx(vy,{})})); +
diff --git a/figma.d.ts b/figma.d.ts index b8f0585..85a2527 100644 --- a/figma.d.ts +++ b/figma.d.ts @@ -30,11 +30,48 @@ declare global { getStyleById: (id: string) => { name: string } | null; variables: { getVariableById: (id: string) => { name: string } | null; + getVariableByIdAsync: (id: string) => Promise; + getLocalVariableCollectionsAsync: () => Promise; }; + root: { + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginData(namespace: string, key: string): string; + }; + mixed: symbol; editorType?: string; mode?: string; }; + // Variable system types + interface Variable { + id: string; + name: string; + resolvedType: 'COLOR' | 'FLOAT' | 'STRING' | 'BOOLEAN'; + description: string; + valuesByMode: Record; + scopes: string[]; + } + + type VariableValue = boolean | string | number | RGB | RGBA | VariableAlias; + + interface VariableAlias { + type: 'VARIABLE_ALIAS'; + id: string; + } + + interface VariableCollection { + id: string; + name: string; + modes: Array<{ modeId: string; name: string }>; + variableIds: string[]; + } + + // Font name type + interface FontName { + family: string; + style: string; + } + const __html__: string; // Color types @@ -110,6 +147,7 @@ declare global { cornerRadius?: number; children?: SceneNode[]; parent: BaseNode | null; + boundVariables?: Record>; resize(width: number, height: number): void; } diff --git a/src/baseline/design-debt.ts b/src/baseline/design-debt.ts new file mode 100644 index 0000000..2c267ec --- /dev/null +++ b/src/baseline/design-debt.ts @@ -0,0 +1,171 @@ +/// + +// ────────────────────────────────────────────── +// Design Debt Score Calculator +// +// Computes a composite 0-100 score (100 = no debt) +// from lint results and token analysis. +// ────────────────────────────────────────────── + +export interface DesignDebtScore { + overall: number; // 0-100 (100 = no debt) + components: { + orphanedStyles: { count: number; score: number }; + detachedInstances: { count: number; score: number }; + hardcodedValues: { count: number; score: number }; + namingViolations: { count: number; score: number }; + missingAutoLayout: { count: number; score: number }; + inconsistentSpacing: { count: number; score: number }; + }; + trend?: { + previousScore: number; + delta: number; + direction: 'improving' | 'stable' | 'degrading'; + }; +} + +/** Generic naming patterns that indicate debt (e.g. "Frame 1", "Group 23") */ +const GENERIC_NAME_RE = /^(Frame|Group|Rectangle|Ellipse|Vector|Line|Polygon|Star|Component|Instance|Boolean)\s*\d+$/i; + +/** + * Calculate composite Design Debt Score from lint results and optional token summary. + * + * Scoring formula: + * - Start at 100 + * - Subtract: orphanedStyles x 2, detachedInstances x 5, hardcodedValues x 1, + * namingViolations x 0.5, missingAutoLayout x 1, inconsistentSpacing x 0.5 + * - Clamp to 0-100 + */ +export function calculateDesignDebt( + lintResult: { + errors: Array<{ + errorType: string; + severity?: string; + nodeId: string; + nodeName: string; + message: string; + value: string; + path: string; + property?: string; + }>; + summary: { + totalErrors: number; + byType: Record; + totalNodes: number; + nodesWithErrors: number; + }; + }, + tokenSummary: { + totalTokens: number; + actualTokens: number; + hardCodedValues: number; + aiSuggestions: number; + } | null, +): DesignDebtScore { + const errors = lintResult.errors; + + // Count orphaned styles (fill/stroke/effect/text errors = no style applied) + const orphanedStyleErrors = errors.filter( + e => e.errorType === 'fill' || e.errorType === 'stroke' || e.errorType === 'effect' || e.errorType === 'text' + ); + const orphanedStyles = orphanedStyleErrors.length; + + // Detached instances: not directly available from lint, estimate from + // nodes that are INSTANCE type but have style errors (a proxy) + const detachedInstanceErrors = errors.filter( + e => e.message.toLowerCase().includes('detach') + ); + const detachedInstances = detachedInstanceErrors.length; + + // Hardcoded values from token summary + const hardcodedValues = tokenSummary ? tokenSummary.hardCodedValues : 0; + + // Naming violations: nodes with generic names like "Frame 1" + const namingNodeIds = new Set(); + for (const e of errors) { + if (GENERIC_NAME_RE.test(e.nodeName)) { + namingNodeIds.add(e.nodeId); + } + } + // Also check accessibility errors about generic naming + const namingA11yErrors = errors.filter( + e => e.errorType === 'accessibility' && GENERIC_NAME_RE.test(e.nodeName) + ); + for (const e of namingA11yErrors) { + namingNodeIds.add(e.nodeId); + } + const namingViolations = namingNodeIds.size; + + // Missing auto-layout + const missingAutoLayout = errors.filter(e => e.errorType === 'autoLayout').length; + + // Inconsistent spacing + const inconsistentSpacing = errors.filter(e => e.errorType === 'spacing').length; + + // Calculate composite score + let score = 100; + score -= orphanedStyles * 2; + score -= detachedInstances * 5; + score -= hardcodedValues * 1; + score -= namingViolations * 0.5; + score -= missingAutoLayout * 1; + score -= inconsistentSpacing * 0.5; + + // Clamp + const overall = Math.max(0, Math.min(100, Math.round(score))); + + return { + overall, + components: { + orphanedStyles: { + count: orphanedStyles, + score: Math.max(0, Math.round(100 - orphanedStyles * 2)), + }, + detachedInstances: { + count: detachedInstances, + score: Math.max(0, Math.round(100 - detachedInstances * 5)), + }, + hardcodedValues: { + count: hardcodedValues, + score: Math.max(0, Math.round(100 - hardcodedValues * 1)), + }, + namingViolations: { + count: namingViolations, + score: Math.max(0, Math.round(100 - namingViolations * 0.5)), + }, + missingAutoLayout: { + count: missingAutoLayout, + score: Math.max(0, Math.round(100 - missingAutoLayout * 1)), + }, + inconsistentSpacing: { + count: inconsistentSpacing, + score: Math.max(0, Math.round(100 - inconsistentSpacing * 0.5)), + }, + }, + }; +} + +/** + * Compute trend data by comparing current score to a previous score. + */ +export function computeDebtTrend( + currentScore: number, + previousScore: number, +): DesignDebtScore['trend'] { + const delta = currentScore - previousScore; + let direction: 'improving' | 'stable' | 'degrading'; + + if (delta > 2) { + direction = 'improving'; + } else if (delta < -2) { + direction = 'degrading'; + } else { + direction = 'stable'; + } + + return { + previousScore, + delta, + direction, + }; +} diff --git a/src/baseline/dtcg-parser.ts b/src/baseline/dtcg-parser.ts new file mode 100644 index 0000000..1b3736b --- /dev/null +++ b/src/baseline/dtcg-parser.ts @@ -0,0 +1,65 @@ +// ────────────────────────────────────────────── +// W3C Design Token Community Group (DTCG) Parser +// Parses .tokens.json files in the DTCG format. +// Spec: https://tr.designtokens.org/format/ +// ────────────────────────────────────────────── + +export interface DTCGToken { + path: string[]; // e.g., ['color', 'primary'] + name: string; // full dot-path: 'color.primary' + $type: string; + $value: unknown; + $description?: string; +} + +/** + * Parse a DTCG-format JSON string into a flat list of tokens. + * Tokens are identified by having a `$value` property. + * `$type` is inherited from parent groups when not specified on the token itself. + */ +export function parseDTCG(json: string): DTCGToken[] { + const root = JSON.parse(json); + const tokens: DTCGToken[] = []; + walkDTCG(root, [], undefined, tokens); + return tokens; +} + +/** + * Recursively walk the DTCG JSON object tree. + * Any object with a `$value` property is a token. + * `$type` on a group is inherited by children without their own `$type`. + */ +function walkDTCG( + node: Record, + path: string[], + inheritedType: string | undefined, + tokens: DTCGToken[] +): void { + // Determine the type at this level (may be inherited by children) + const levelType = typeof node.$type === 'string' ? node.$type : inheritedType; + + // If this node has $value, it's a token + if ('$value' in node) { + const tokenType = typeof node.$type === 'string' ? node.$type : (inheritedType || 'unknown'); + const description = typeof node.$description === 'string' ? node.$description : undefined; + + tokens.push({ + path: [...path], + name: path.join('.'), + $type: tokenType, + $value: node.$value, + $description: description, + }); + return; + } + + // Otherwise, recurse into child groups/tokens + for (const [key, value] of Object.entries(node)) { + // Skip DTCG metadata properties + if (key.startsWith('$')) continue; + + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + walkDTCG(value as Record, [...path, key], levelType, tokens); + } + } +} diff --git a/src/baseline/storage.ts b/src/baseline/storage.ts index 587181e..6a5eccb 100644 --- a/src/baseline/storage.ts +++ b/src/baseline/storage.ts @@ -36,6 +36,8 @@ export interface BaselineSnapshot { byType: Record; }; errors: ErrorDigest[]; + /** Design debt score at time of baseline capture (optional for backward compat). */ + designDebt?: number; } // ── Chunked pluginData helpers ────────────────────────────── diff --git a/src/baseline/token-compliance.ts b/src/baseline/token-compliance.ts new file mode 100644 index 0000000..2f1c0a8 --- /dev/null +++ b/src/baseline/token-compliance.ts @@ -0,0 +1,207 @@ +// ────────────────────────────────────────────── +// Token Compliance Checker +// Compares Figma variable system against DTCG token definitions. +// ────────────────────────────────────────────── + +import type { VariableSystemReport, VariableData } from '../extract/variable-collector'; +import type { DTCGToken } from './dtcg-parser'; + +export interface ComplianceResult { + adoptionScore: number; // 0-100 + matched: Array<{ token: string; nodeCount: number; usage: 'correct' | 'overridden' }>; + unmatched: Array<{ value: string; nodeCount: number; nearestToken: string; distance: number }>; + orphanTokens: string[]; // tokens defined in DTCG but never used in design + missingFromSystem: string[]; // values used in design but not in DTCG token system + summary: { + totalTokenDefs: number; + usedInDesign: number; + hardCodedValues: number; + compliance: number; + }; +} + +/** + * Check how well the Figma variable system aligns with a DTCG token spec. + */ +export function checkTokenCompliance( + variables: VariableSystemReport, + dtcgTokens: DTCGToken[], + _lintResult: unknown +): ComplianceResult { + // Build maps for comparison + const dtcgByName = new Map(); + for (const token of dtcgTokens) { + dtcgByName.set(normalizeName(token.name), token); + } + + // Flatten all Figma variables across collections + const allVars: VariableData[] = []; + for (const collection of variables.collections) { + for (const v of collection.variables) { + allVars.push(v); + } + } + + const matched: ComplianceResult['matched'] = []; + const unmatched: ComplianceResult['unmatched'] = []; + const matchedDTCGNames = new Set(); + const figmaVarNames = new Set(); + + for (const variable of allVars) { + const normalizedName = normalizeName(variable.name); + figmaVarNames.add(normalizedName); + + // Try exact match + if (dtcgByName.has(normalizedName)) { + matchedDTCGNames.add(normalizedName); + matched.push({ + token: variable.name, + nodeCount: variable.consumers, + usage: variable.consumers > 0 ? 'correct' : 'overridden', + }); + continue; + } + + // Try fuzzy match + const nearest = findNearestToken(normalizedName, dtcgTokens); + if (nearest && nearest.distance <= 3) { + matchedDTCGNames.add(normalizeName(nearest.token.name)); + matched.push({ + token: variable.name, + nodeCount: variable.consumers, + usage: 'correct', + }); + } else { + // This Figma variable has no corresponding DTCG token + unmatched.push({ + value: variable.name, + nodeCount: variable.consumers, + nearestToken: nearest ? nearest.token.name : '(none)', + distance: nearest ? nearest.distance : Infinity, + }); + } + } + + // Find orphan tokens: defined in DTCG but not used in Figma + const orphanTokens: string[] = []; + for (const token of dtcgTokens) { + if (!matchedDTCGNames.has(normalizeName(token.name))) { + orphanTokens.push(token.name); + } + } + + // Find missing from system: Figma vars that don't map to any DTCG token + const missingFromSystem = unmatched + .filter(u => u.nodeCount > 0) + .map(u => u.value); + + // Calculate scores + const totalTokenDefs = dtcgTokens.length; + const usedInDesign = matched.filter(m => m.nodeCount > 0).length; + const hardCodedValues = unmatched.filter(u => u.nodeCount > 0).length; + + const totalRelevant = usedInDesign + hardCodedValues; + const compliance = totalRelevant > 0 + ? Math.round((usedInDesign / totalRelevant) * 100) + : (totalTokenDefs > 0 ? 0 : 100); + + // Adoption score combines variable adoption from Figma + DTCG alignment + const adoptionScore = Math.round( + (variables.adoptionRate * 0.5) + (compliance * 0.5) + ); + + return { + adoptionScore, + matched, + unmatched, + orphanTokens, + missingFromSystem, + summary: { + totalTokenDefs, + usedInDesign, + hardCodedValues, + compliance, + }, + }; +} + +// ────────────────────────────────────────────── +// Helpers +// ────────────────────────────────────────────── + +/** + * Normalize a token/variable name for comparison. + * Converts slashes to dots, lowercases, strips whitespace. + * e.g., "Color/Primary/500" -> "color.primary.500" + */ +function normalizeName(name: string): string { + return name + .replace(/\//g, '.') + .replace(/\s+/g, '-') + .toLowerCase() + .trim(); +} + +/** + * Find the nearest DTCG token to a given name using Levenshtein distance. + */ +function findNearestToken( + normalizedName: string, + tokens: DTCGToken[] +): { token: DTCGToken; distance: number } | null { + if (tokens.length === 0) return null; + + let best: { token: DTCGToken; distance: number } | null = null; + + for (const token of tokens) { + const d = levenshtein(normalizedName, normalizeName(token.name)); + if (!best || d < best.distance) { + best = { token, distance: d }; + } + // Early exit on exact match + if (d === 0) return best; + } + + return best; +} + +/** + * Levenshtein edit distance between two strings. + * Capped at maxDist for performance (returns maxDist+1 if exceeded). + */ +function levenshtein(a: string, b: string, maxDist: number = 10): number { + if (a === b) return 0; + if (a.length === 0) return Math.min(b.length, maxDist + 1); + if (b.length === 0) return Math.min(a.length, maxDist + 1); + + // Use two-row optimization + let prev = new Array(b.length + 1); + let curr = new Array(b.length + 1); + + for (let j = 0; j <= b.length; j++) { + prev[j] = j; + } + + for (let i = 1; i <= a.length; i++) { + curr[0] = i; + let rowMin = curr[0]; + + for (let j = 1; j <= b.length; j++) { + const cost = a[i - 1] === b[j - 1] ? 0 : 1; + curr[j] = Math.min( + prev[j] + 1, // deletion + curr[j - 1] + 1, // insertion + prev[j - 1] + cost // substitution + ); + if (curr[j] < rowMin) rowMin = curr[j]; + } + + // Early termination if all values in current row exceed maxDist + if (rowMin > maxDist) return maxDist + 1; + + // Swap rows + [prev, curr] = [curr, prev]; + } + + return prev[b.length]; +} diff --git a/src/core/design-lint.ts b/src/core/design-lint.ts index d68373c..39dbaf1 100644 --- a/src/core/design-lint.ts +++ b/src/core/design-lint.ts @@ -9,6 +9,10 @@ import { checkVisualQuality } from '../lint/visual-quality'; import { checkMicrocopy } from '../lint/microcopy'; import { checkConversion } from '../lint/conversion'; import { checkCognitive } from '../lint/cognitive'; +import { checkFittsLaw } from '../lint/fitts-law'; +import { checkGestalt } from '../lint/gestalt'; +import { checkDetachedInstances } from '../lint/detached-instance'; +import { checkResponsive } from '../lint/responsive'; // ────────────────────────────────────────────── // Default lint settings @@ -27,6 +31,10 @@ export const DEFAULT_LINT_SETTINGS: LintSettings = { checkMicrocopy: true, checkConversion: true, checkCognitive: true, + checkFittsLaw: true, + checkGestalt: true, + checkDetachedInstances: true, + checkResponsive: true, allowedRadii: [0, 2, 4, 8, 12, 16, 24, 32], skipLockedLayers: true, skipHiddenLayers: true, @@ -568,6 +576,90 @@ export function runDesignLint( } } + // Run Fitts's Law checks (interactive target sizes) + if (settings.checkFittsLaw && severityOverrides.fittsLaw !== 'off') { + const fittsResult = checkFittsLaw(nodes, { skipLocked: settings.skipLockedLayers, skipHidden: settings.skipHiddenLayers }); + for (const issue of fittsResult.issues) { + if (ignoredNodeIds.has(issue.nodeId)) continue; + if (ignoredErrorKeys.has(errorKey(issue.nodeId, 'fittsLaw'))) continue; + if (matchesIgnorePattern(issue.nodeName, ignorePatterns)) continue; + + errors.push({ + nodeId: issue.nodeId, + nodeName: issue.nodeName, + nodeType: 'FRAME', + errorType: 'fittsLaw', + message: issue.message, + value: issue.currentValue || '', + path: issue.nodeName, + severity: issue.severity, + }); + } + } + + // Run Gestalt principle checks (proximity, similarity) + if (settings.checkGestalt && severityOverrides.gestalt !== 'off') { + const gestaltResult = checkGestalt(nodes, { skipLocked: settings.skipLockedLayers, skipHidden: settings.skipHiddenLayers }); + for (const issue of gestaltResult.issues) { + if (ignoredNodeIds.has(issue.nodeId)) continue; + if (ignoredErrorKeys.has(errorKey(issue.nodeId, 'gestalt'))) continue; + if (matchesIgnorePattern(issue.nodeName, ignorePatterns)) continue; + + errors.push({ + nodeId: issue.nodeId, + nodeName: issue.nodeName, + nodeType: 'FRAME', + errorType: 'gestalt', + message: issue.message, + value: issue.currentValue || '', + path: issue.nodeName, + severity: issue.severity, + }); + } + } + + // Run detached instance checks + if (settings.checkDetachedInstances && severityOverrides.detachedInstance !== 'off') { + const detachResult = checkDetachedInstances(nodes, { skipLocked: settings.skipLockedLayers, skipHidden: settings.skipHiddenLayers }); + for (const issue of detachResult.issues) { + if (ignoredNodeIds.has(issue.nodeId)) continue; + if (ignoredErrorKeys.has(errorKey(issue.nodeId, 'detachedInstance'))) continue; + if (matchesIgnorePattern(issue.nodeName, ignorePatterns)) continue; + + errors.push({ + nodeId: issue.nodeId, + nodeName: issue.nodeName, + nodeType: 'FRAME', + errorType: 'detachedInstance', + message: issue.message, + value: issue.currentValue || '', + path: issue.nodeName, + severity: issue.severity, + }); + } + } + + // Run responsive design checks + if (settings.checkResponsive && severityOverrides.responsive !== 'off') { + const respResult = checkResponsive(nodes, { skipLocked: settings.skipLockedLayers, skipHidden: settings.skipHiddenLayers }); + for (const issue of respResult.issues) { + if (ignoredNodeIds.has(issue.nodeId)) continue; + if (ignoredErrorKeys.has(errorKey(issue.nodeId, 'responsive'))) continue; + if (matchesIgnorePattern(issue.nodeName, ignorePatterns)) continue; + + errors.push({ + nodeId: issue.nodeId, + nodeName: issue.nodeName, + nodeType: 'FRAME', + errorType: 'responsive', + message: issue.message, + value: issue.currentValue || '', + path: issue.nodeName, + severity: issue.severity, + }); + } + } + // Filter out errors for rules set to 'off' via severity overrides const filteredErrors = errors.filter(err => severityOverrides[err.errorType] !== 'off'); @@ -604,6 +696,18 @@ export function runDesignLint( case 'cognitive': err.severity = 'info'; break; + case 'responsive': + err.severity = 'warning'; + break; + case 'fittsLaw': + err.severity = 'warning'; + break; + case 'gestalt': + err.severity = 'info'; + break; + case 'detachedInstance': + err.severity = 'warning'; + break; } } } @@ -612,7 +716,7 @@ export function runDesignLint( const nodesWithErrors = new Set(finalErrors.map(e => e.nodeId)).size; // Build summary - const byType: Record = { fill: 0, stroke: 0, effect: 0, text: 0, radius: 0, spacing: 0, autoLayout: 0, accessibility: 0, visualQuality: 0, microcopy: 0, conversion: 0, cognitive: 0 }; + const byType: Record = { fill: 0, stroke: 0, effect: 0, text: 0, radius: 0, spacing: 0, autoLayout: 0, accessibility: 0, visualQuality: 0, microcopy: 0, conversion: 0, cognitive: 0, fittsLaw: 0, gestalt: 0, detachedInstance: 0, responsive: 0 }; for (const err of finalErrors) { byType[err.errorType]++; } @@ -642,7 +746,7 @@ export function lintSelection(settings?: LintSettings): LintResult { errors: [], ignoredNodeIds: [], ignoredErrorKeys: [], - summary: { totalErrors: 0, byType: { fill: 0, stroke: 0, effect: 0, text: 0, radius: 0, spacing: 0, autoLayout: 0, accessibility: 0, visualQuality: 0, microcopy: 0, conversion: 0, cognitive: 0 }, totalNodes: 0, nodesWithErrors: 0 }, + summary: { totalErrors: 0, byType: { fill: 0, stroke: 0, effect: 0, text: 0, radius: 0, spacing: 0, autoLayout: 0, accessibility: 0, visualQuality: 0, microcopy: 0, conversion: 0, cognitive: 0, fittsLaw: 0, gestalt: 0, detachedInstance: 0, responsive: 0 }, totalNodes: 0, nodesWithErrors: 0 }, }; } return runDesignLint(selection, settings); diff --git a/src/extract/mode-comparator.ts b/src/extract/mode-comparator.ts new file mode 100644 index 0000000..9376d50 --- /dev/null +++ b/src/extract/mode-comparator.ts @@ -0,0 +1,121 @@ +/// + +// ────────────────────────────────────────────── +// Mode Comparator +// Compares variable values across modes (e.g., Light vs Dark). +// ────────────────────────────────────────────── + +export interface ModeComparisonData { + collection: string; + modes: Array<{ + modeId: string; + modeName: string; + screenshot?: string; // base64 if captured + }>; + variableDiffs: Array<{ + variableName: string; + type: string; + values: Record; // modeName -> value + }>; + missingValues: Array<{ + variableName: string; + missingModes: string[]; + }>; +} + +/** + * Compare variable values across all modes in a given collection. + * Flags variables that differ between modes and those missing values in some modes. + */ +export async function compareModes(collectionId: string): Promise { + const collections = await figma.variables.getLocalVariableCollectionsAsync(); + const collection = collections.find(c => c.id === collectionId); + if (!collection) { + throw new Error(`Collection not found: ${collectionId}`); + } + + const modes = collection.modes.map(m => ({ + modeId: m.modeId, + modeName: m.name, + })); + + const variableDiffs: ModeComparisonData['variableDiffs'] = []; + const missingValues: ModeComparisonData['missingValues'] = []; + + for (const varId of collection.variableIds) { + const variable = await figma.variables.getVariableByIdAsync(varId); + if (!variable) continue; + + const values: Record = {}; + const missing: string[] = []; + let hasDiff = false; + let firstValue: unknown = undefined; + let firstSet = false; + + for (const mode of collection.modes) { + const modeValue = variable.valuesByMode[mode.modeId]; + + if (modeValue === undefined || modeValue === null) { + missing.push(mode.name); + } else { + values[mode.name] = serializeValue(modeValue); + if (!firstSet) { + firstValue = JSON.stringify(serializeValue(modeValue)); + firstSet = true; + } else { + if (JSON.stringify(serializeValue(modeValue)) !== firstValue) { + hasDiff = true; + } + } + } + } + + // Only include variables that differ across modes or have missing values + if (hasDiff || missing.length > 0) { + variableDiffs.push({ + variableName: variable.name, + type: variable.resolvedType, + values, + }); + } + + if (missing.length > 0) { + missingValues.push({ + variableName: variable.name, + missingModes: missing, + }); + } + } + + return { + collection: collection.name, + modes, + variableDiffs, + missingValues, + }; +} + +/** + * Serialize a variable value for JSON transport. + */ +function serializeValue(value: unknown): unknown { + if (value === null || value === undefined) return value; + if (typeof value === 'boolean' || typeof value === 'string' || typeof value === 'number') { + return value; + } + if (typeof value === 'object' && (value as { type?: string }).type === 'VARIABLE_ALIAS') { + return { type: 'VARIABLE_ALIAS', id: (value as { id: string }).id }; + } + if (typeof value === 'object' && 'r' in (value as Record)) { + const rgb = value as { r: number; g: number; b: number; a?: number }; + const toHex = (n: number): string => { + const hex = Math.round(n * 255).toString(16); + return hex.length === 1 ? '0' + hex : hex; + }; + if (rgb.a !== undefined && rgb.a < 1) { + return `rgba(${Math.round(rgb.r * 255)}, ${Math.round(rgb.g * 255)}, ${Math.round(rgb.b * 255)}, ${rgb.a.toFixed(2)})`; + } + return `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`; + } + return value; +} diff --git a/src/extract/variable-collector.ts b/src/extract/variable-collector.ts new file mode 100644 index 0000000..0cfacd9 --- /dev/null +++ b/src/extract/variable-collector.ts @@ -0,0 +1,231 @@ +/// + +// ────────────────────────────────────────────── +// Variable System Collector +// Extracts the full variable system from the Figma file. +// ────────────────────────────────────────────── + +export interface VariableCollectionData { + id: string; + name: string; + modes: Array<{ modeId: string; name: string }>; + variables: VariableData[]; +} + +export interface VariableData { + id: string; + name: string; + resolvedType: 'COLOR' | 'FLOAT' | 'STRING' | 'BOOLEAN'; + description: string; + valuesByMode: Record; // modeId -> value + scopes: string[]; + consumers: number; // how many nodes use this variable +} + +export interface VariableSystemReport { + collections: VariableCollectionData[]; + totalVariables: number; + unusedVariables: string[]; // variable names with 0 consumers + adoptionRate: number; // % of eligible nodes bound to variables + modesCoverage: Record; // modeName -> % variables with values +} + +/** + * Count how many nodes on the current page reference each variable ID. + * Also counts total eligible nodes (those that *could* bind variables). + */ +function countVariableConsumers( + nodes: readonly SceneNode[] +): { consumerMap: Map; totalEligible: number; boundCount: number } { + const consumerMap = new Map(); + let totalEligible = 0; + let boundCount = 0; + + function traverse(node: SceneNode): void { + const n = node as any; + // A node is eligible if it has fills, strokes, effects, or is a text node + const hasFills = Array.isArray(n.fills) && n.fills.length > 0; + const hasStrokes = Array.isArray(n.strokes) && n.strokes.length > 0; + const hasEffects = Array.isArray(n.effects) && n.effects.length > 0; + const isText = node.type === 'TEXT'; + const isFrame = node.type === 'FRAME' || node.type === 'COMPONENT' || node.type === 'INSTANCE'; + + if (hasFills || hasStrokes || hasEffects || isText || isFrame) { + totalEligible++; + } + + // Check boundVariables on the node itself + let nodeBound = false; + if ('boundVariables' in node && n.boundVariables) { + const bv = n.boundVariables as Record; + for (const key of Object.keys(bv)) { + const binding = bv[key]; + if (Array.isArray(binding)) { + for (const b of binding) { + if (b && b.id) { + consumerMap.set(b.id, (consumerMap.get(b.id) || 0) + 1); + nodeBound = true; + } + } + } else if (binding && binding.id) { + consumerMap.set(binding.id, (consumerMap.get(binding.id) || 0) + 1); + nodeBound = true; + } + } + } + + // Check boundVariables on fills and strokes + if (Array.isArray(n.fills)) { + for (const fill of n.fills) { + if (fill.boundVariables) { + for (const key of Object.keys(fill.boundVariables)) { + const binding = fill.boundVariables[key]; + if (binding && binding.id) { + consumerMap.set(binding.id, (consumerMap.get(binding.id) || 0) + 1); + nodeBound = true; + } + } + } + } + } + + if (nodeBound) { + boundCount++; + } + + // Recurse + if ('children' in node && n.children) { + for (const child of n.children as SceneNode[]) { + traverse(child); + } + } + } + + for (const node of nodes) { + traverse(node); + } + + return { consumerMap, totalEligible, boundCount }; +} + +/** + * Collect the full variable system from the Figma file. + * Traverses all local variable collections and counts consumers on the current page. + */ +export async function collectVariableSystem(): Promise { + // Get all local variable collections + const rawCollections = await figma.variables.getLocalVariableCollectionsAsync(); + + // Count consumers from all nodes on the current page + const allNodes = figma.currentPage.findAll(() => true); + const { consumerMap, totalEligible, boundCount } = countVariableConsumers(allNodes); + + const collections: VariableCollectionData[] = []; + let totalVariables = 0; + const unusedVariables: string[] = []; + const modeCoverageAccum: Record = {}; + + for (const collection of rawCollections) { + const variables: VariableData[] = []; + + // Initialize mode coverage tracking for this collection + for (const mode of collection.modes) { + if (!modeCoverageAccum[mode.name]) { + modeCoverageAccum[mode.name] = { total: 0, withValue: 0 }; + } + } + + for (const varId of collection.variableIds) { + const variable = await figma.variables.getVariableByIdAsync(varId); + if (!variable) continue; + + totalVariables++; + const consumers = consumerMap.get(variable.id) || 0; + + if (consumers === 0) { + unusedVariables.push(variable.name); + } + + // Serialize valuesByMode (the raw values may contain RGB objects etc.) + const serializedValues: Record = {}; + for (const [modeId, value] of Object.entries(variable.valuesByMode)) { + serializedValues[modeId] = serializeVariableValue(value); + } + + // Track mode coverage + for (const mode of collection.modes) { + modeCoverageAccum[mode.name].total++; + const modeValue = variable.valuesByMode[mode.modeId]; + if (modeValue !== undefined && modeValue !== null) { + modeCoverageAccum[mode.name].withValue++; + } + } + + variables.push({ + id: variable.id, + name: variable.name, + resolvedType: variable.resolvedType, + description: variable.description, + valuesByMode: serializedValues, + scopes: variable.scopes, + consumers, + }); + } + + collections.push({ + id: collection.id, + name: collection.name, + modes: collection.modes.map(m => ({ modeId: m.modeId, name: m.name })), + variables, + }); + } + + // Calculate adoption rate + const adoptionRate = totalEligible > 0 + ? Math.round((boundCount / totalEligible) * 100) + : 0; + + // Calculate modes coverage + const modesCoverage: Record = {}; + for (const [modeName, counts] of Object.entries(modeCoverageAccum)) { + modesCoverage[modeName] = counts.total > 0 + ? Math.round((counts.withValue / counts.total) * 100) + : 100; + } + + return { + collections, + totalVariables, + unusedVariables, + adoptionRate, + modesCoverage, + }; +} + +/** + * Serialize a variable value for safe JSON transport. + * Converts RGB/RGBA objects to hex strings, passes through primitives. + */ +function serializeVariableValue(value: unknown): unknown { + if (value === null || value === undefined) return value; + if (typeof value === 'boolean' || typeof value === 'string' || typeof value === 'number') { + return value; + } + // Check for VariableAlias + if (typeof value === 'object' && (value as { type?: string }).type === 'VARIABLE_ALIAS') { + return { type: 'VARIABLE_ALIAS', id: (value as { id: string }).id }; + } + // Check for RGB/RGBA + if (typeof value === 'object' && 'r' in (value as Record)) { + const rgb = value as { r: number; g: number; b: number; a?: number }; + const toHex = (n: number): string => { + const hex = Math.round(n * 255).toString(16); + return hex.length === 1 ? '0' + hex : hex; + }; + if (rgb.a !== undefined && rgb.a < 1) { + return `rgba(${Math.round(rgb.r * 255)}, ${Math.round(rgb.g * 255)}, ${Math.round(rgb.b * 255)}, ${rgb.a.toFixed(2)})`; + } + return `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`; + } + return value; +} diff --git a/src/lint/dark-mode.ts b/src/lint/dark-mode.ts new file mode 100644 index 0000000..beeb85e --- /dev/null +++ b/src/lint/dark-mode.ts @@ -0,0 +1,239 @@ +/// + +// ────────────────────────────────────────────── +// Dark Mode Validation Checks +// Deterministic checks for common dark mode issues. +// ────────────────────────────────────────────── + +import type { LintIssue } from './types'; +import type { ModeComparisonData } from '../extract/mode-comparator'; + +export interface DarkModeResult { + issues: LintIssue[]; + metrics: { + pureBlackBackgrounds: number; + pureWhiteText: number; + lowContrastOnDark: number; + missingModeValues: number; + }; + summary: { totalChecked: number; passed: number; failed: number }; +} + +let issueCounter = 0; +function nextId(): string { + return `dark-${++issueCounter}`; +} + +/** + * Run dark mode validation checks against mode comparison data. + * Checks for common dark mode anti-patterns: + * - Pure black (#000000) backgrounds + * - Pure white (#FFFFFF) text on dark backgrounds + * - Insufficient contrast between dark text and background colors + * - Variables missing values in some modes + * - Elevation: dark mode should use lighter surfaces, not shadows + */ +export function checkDarkMode(modeData: ModeComparisonData): DarkModeResult { + issueCounter = 0; + const issues: LintIssue[] = []; + + let pureBlackBackgrounds = 0; + let pureWhiteText = 0; + let lowContrastOnDark = 0; + let missingModeValues = 0; + + // Identify which mode is likely the "dark" mode + const darkModeNames = modeData.modes.filter(m => + /dark|night|dim/i.test(m.modeName) + ); + const lightModeNames = modeData.modes.filter(m => + /light|day|default/i.test(m.modeName) + ); + + const darkModeName = darkModeNames.length > 0 ? darkModeNames[0].modeName : null; + const lightModeName = lightModeNames.length > 0 ? lightModeNames[0].modeName : null; + + // Check variable diffs for dark mode issues + for (const diff of modeData.variableDiffs) { + if (diff.type !== 'COLOR') continue; + + // Get the dark mode value + const darkValue = darkModeName ? diff.values[darkModeName] : null; + const lightValue = lightModeName ? diff.values[lightModeName] : null; + + if (typeof darkValue !== 'string') continue; + + const darkHex = normalizeHex(darkValue); + + // Check: Pure black backgrounds + if (darkHex === '#000000' && isLikelyBackground(diff.variableName)) { + pureBlackBackgrounds++; + issues.push({ + id: nextId(), + type: 'accessibility', + severity: 'warning', + nodeId: '', + nodeName: diff.variableName, + message: `Dark mode background "${diff.variableName}" uses pure black (#000000). Use a dark grey (#121212 or #1a1a1a) for better readability and reduced eye strain.`, + currentValue: darkValue, + suggestions: ['#121212', '#1a1a1a', '#1e1e1e'], + autoFixable: false, + }); + } + + // Check: Pure white text on dark mode + if (darkHex === '#ffffff' && isLikelyText(diff.variableName)) { + pureWhiteText++; + issues.push({ + id: nextId(), + type: 'accessibility', + severity: 'info', + nodeId: '', + nodeName: diff.variableName, + message: `Dark mode text "${diff.variableName}" uses pure white (#FFFFFF). Consider off-white (#E0E0E0 or #EBEBEB) to reduce glare.`, + currentValue: darkValue, + suggestions: ['#e0e0e0', '#ebebeb', '#f5f5f5'], + autoFixable: false, + }); + } + + // Check: Insufficient contrast between light-dark value pairs + if (typeof lightValue === 'string' && darkHex) { + const lightHex = normalizeHex(lightValue); + if (lightHex === darkHex) { + // Same value in both modes — likely an oversight + issues.push({ + id: nextId(), + type: 'accessibility', + severity: 'warning', + nodeId: '', + nodeName: diff.variableName, + message: `Variable "${diff.variableName}" has identical value in Light and Dark modes (${lightValue}). This likely needs a dark mode adaptation.`, + currentValue: `Light: ${lightValue}, Dark: ${darkValue}`, + suggestions: ['Define a distinct dark mode value'], + autoFixable: false, + }); + lowContrastOnDark++; + } + } + + // Check: Elevation — dark mode surfaces that use very dark colors for all elevation levels + if (isLikelySurface(diff.variableName) && darkHex) { + const brightness = hexBrightness(darkHex); + // In dark mode, elevated surfaces should be lighter (Material Design guidance) + // If the variable name suggests elevation but the color is very dark, flag it + if (/elevated|raised|overlay|modal|popover|sheet|card/i.test(diff.variableName) && brightness < 15) { + issues.push({ + id: nextId(), + type: 'accessibility', + severity: 'info', + nodeId: '', + nodeName: diff.variableName, + message: `Elevated surface "${diff.variableName}" in dark mode is very dark (${darkValue}). Material Design recommends lighter surfaces for elevated elements to convey depth.`, + currentValue: darkValue, + suggestions: ['Use a slightly lighter shade for elevated surfaces (e.g., #1e1e1e for default, #2c2c2c for elevated)'], + autoFixable: false, + }); + } + } + } + + // Check: Missing mode values + for (const missing of modeData.missingValues) { + missingModeValues++; + issues.push({ + id: nextId(), + type: 'accessibility', + severity: 'critical', + nodeId: '', + nodeName: missing.variableName, + message: `Variable "${missing.variableName}" is missing values for modes: ${missing.missingModes.join(', ')}. This will cause fallback behavior or errors.`, + currentValue: `Missing in: ${missing.missingModes.join(', ')}`, + suggestions: ['Add values for all modes'], + autoFixable: false, + }); + } + + const totalChecked = modeData.variableDiffs.length + modeData.missingValues.length; + const failed = issues.length; + + return { + issues, + metrics: { + pureBlackBackgrounds, + pureWhiteText, + lowContrastOnDark, + missingModeValues, + }, + summary: { + totalChecked, + passed: totalChecked - failed, + failed, + }, + }; +} + +// ────────────────────────────────────────────── +// Helpers +// ────────────────────────────────────────────── + +/** + * Normalize a hex color string to lowercase 6-digit hex. + * Handles #RGB, #RRGGBB, and rgba() formats. + */ +function normalizeHex(value: string): string | null { + const trimmed = value.trim().toLowerCase(); + + // Already a hex color + if (trimmed.startsWith('#')) { + if (trimmed.length === 4) { + // #RGB -> #RRGGBB + return `#${trimmed[1]}${trimmed[1]}${trimmed[2]}${trimmed[2]}${trimmed[3]}${trimmed[3]}`; + } + if (trimmed.length === 7) { + return trimmed; + } + } + + // Try to parse rgba(r, g, b, a) + const rgbaMatch = trimmed.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/); + if (rgbaMatch) { + const r = parseInt(rgbaMatch[1], 10); + const g = parseInt(rgbaMatch[2], 10); + const b = parseInt(rgbaMatch[3], 10); + const toHex = (n: number): string => { + const hex = n.toString(16); + return hex.length === 1 ? '0' + hex : hex; + }; + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; + } + + return null; +} + +/** + * Approximate brightness of a hex color (0-100 scale). + * Uses perceived luminance formula. + */ +function hexBrightness(hex: string): number { + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + // Perceived brightness formula + return (r * 299 + g * 587 + b * 114) / 2550; +} + +/** Heuristic: does the variable name suggest a background color? */ +function isLikelyBackground(name: string): boolean { + return /background|bg|surface|canvas|base/i.test(name); +} + +/** Heuristic: does the variable name suggest a text color? */ +function isLikelyText(name: string): boolean { + return /text|foreground|fg|content|body|heading|label|title|caption/i.test(name); +} + +/** Heuristic: does the variable name suggest a surface/elevation element? */ +function isLikelySurface(name: string): boolean { + return /surface|background|bg|card|modal|sheet|popover|overlay|elevated|raised/i.test(name); +} diff --git a/src/lint/detached-instance.ts b/src/lint/detached-instance.ts new file mode 100644 index 0000000..3cf5589 --- /dev/null +++ b/src/lint/detached-instance.ts @@ -0,0 +1,117 @@ +/// + +import { LintIssue } from './types'; + +// ────────────────────────────────────────────── +// Detached Instance Lint Module +// +// Flags frames that were likely detached from component instances +// (heuristic: frame with name containing "detach", or frames +// at shallow depth that look like they were once instances). +// ────────────────────────────────────────────── + +let issueCounter = 0; +function nextId(): string { + return `detach-${++issueCounter}`; +} + +export interface DetachedInstanceResult { + issues: LintIssue[]; + summary: { totalChecked: number; passed: number; failed: number }; +} + +const DETACH_RE = /detach/i; +const COMPONENT_NAME_RE = /^[A-Z][a-zA-Z]+(?:\s*[-\/]\s*[A-Za-z]+)*$/; + +function traverseForDetached( + node: SceneNode, + issues: LintIssue[], + skipLocked: boolean, + skipHidden: boolean, + parentLocked: boolean, +): { checked: number; failed: number } { + const isLocked = parentLocked || ('locked' in node && (node as any).locked); + const isHidden = 'visible' in node && !node.visible; + + if (skipLocked && isLocked) return { checked: 0, failed: 0 }; + if (skipHidden && isHidden) return { checked: 0, failed: 0 }; + + let checked = 0; + let failed = 0; + + // A FRAME (not COMPONENT or INSTANCE) whose name looks like a component name + // and isn't at the page root level — likely detached + if (node.type === 'FRAME' && 'children' in node) { + checked++; + + const isDetachNamed = DETACH_RE.test(node.name); + const looksLikeComponent = COMPONENT_NAME_RE.test(node.name) && + node.parent?.type !== 'PAGE' && + (node as FrameNode).children.length > 0; + + if (isDetachNamed) { + failed++; + issues.push({ + id: nextId(), + type: 'detachedInstance', + severity: 'warning', + nodeId: node.id, + nodeName: node.name, + message: `Frame "${node.name}" appears to be a detached component instance. Detaching breaks the link to the source component and prevents design system updates.`, + currentValue: 'Detached instance', + suggestions: [ + 'Re-attach by replacing with the original component instance', + 'If intentional, rename to remove "detach" from the name', + ], + autoFixable: false, + }); + } else if (looksLikeComponent) { + // Additional heuristic: frame with component-like name, multiple children, + // but it's a plain FRAME not an INSTANCE + // Only flag if it has a structured name (PascalCase with separators) + const parts = node.name.split(/[\s\-\/]/); + if (parts.length >= 2 && parts.every(p => p.length > 0)) { + // This is a softer signal — don't flag unless confident + // Skip for now to avoid false positives + } + } + } + + if ('children' in node) { + for (const child of (node as any).children as SceneNode[]) { + const sub = traverseForDetached(child, issues, skipLocked, skipHidden, isLocked); + checked += sub.checked; + failed += sub.failed; + } + } + + return { checked, failed }; +} + +export function checkDetachedInstances( + nodes: readonly SceneNode[], + options: { skipLocked?: boolean; skipHidden?: boolean } = {}, +): DetachedInstanceResult { + issueCounter = 0; + const issues: LintIssue[] = []; + const skipLocked = options.skipLocked ?? true; + const skipHidden = options.skipHidden ?? true; + + let totalChecked = 0; + let totalFailed = 0; + + for (const node of nodes) { + const sub = traverseForDetached(node, issues, skipLocked, skipHidden, false); + totalChecked += sub.checked; + totalFailed += sub.failed; + } + + return { + issues, + summary: { + totalChecked, + passed: totalChecked - totalFailed, + failed: totalFailed, + }, + }; +} diff --git a/src/lint/fitts-law.ts b/src/lint/fitts-law.ts new file mode 100644 index 0000000..5e8528f --- /dev/null +++ b/src/lint/fitts-law.ts @@ -0,0 +1,107 @@ +/// + +import { LintIssue } from './types'; + +// ────────────────────────────────────────────── +// Fitts's Law Lint Module +// +// Deterministic checks for target size/distance: +// - Small interactive targets (below 44x44px) +// - Distant related actions +// ────────────────────────────────────────────── + +let issueCounter = 0; +function nextId(): string { + return `fitts-${++issueCounter}`; +} + +export interface FittsLawResult { + issues: LintIssue[]; + summary: { totalChecked: number; passed: number; failed: number }; +} + +const CTA_RE = /button|btn|cta|action|submit|link|toggle|switch|checkbox|radio|tab(?!le)/i; +const MIN_TARGET_SIZE = 44; // px — WCAG 2.5.8 Level AA + +function isInteractive(node: SceneNode): boolean { + return CTA_RE.test(node.name); +} + +function traverseForFitts( + node: SceneNode, + issues: LintIssue[], + skipLocked: boolean, + skipHidden: boolean, + parentLocked: boolean, +): { checked: number; failed: number } { + const isLocked = parentLocked || ('locked' in node && (node as any).locked); + const isHidden = 'visible' in node && !node.visible; + + if (skipLocked && isLocked) return { checked: 0, failed: 0 }; + if (skipHidden && isHidden) return { checked: 0, failed: 0 }; + + let checked = 0; + let failed = 0; + + if (isInteractive(node) && 'width' in node && 'height' in node) { + checked++; + const w = (node as any).width as number; + const h = (node as any).height as number; + + if (w < MIN_TARGET_SIZE || h < MIN_TARGET_SIZE) { + failed++; + issues.push({ + id: nextId(), + type: 'fittsLaw', + severity: 'warning', + nodeId: node.id, + nodeName: node.name, + message: `Interactive target "${node.name}" is ${Math.round(w)}x${Math.round(h)}px — minimum recommended size is ${MIN_TARGET_SIZE}x${MIN_TARGET_SIZE}px (WCAG 2.5.8)`, + currentValue: `${Math.round(w)}x${Math.round(h)}px`, + suggestions: [ + `Increase to at least ${MIN_TARGET_SIZE}x${MIN_TARGET_SIZE}px`, + 'Add padding to increase the hit area', + ], + autoFixable: false, + }); + } + } + + if ('children' in node) { + for (const child of (node as any).children as SceneNode[]) { + const sub = traverseForFitts(child, issues, skipLocked, skipHidden, isLocked); + checked += sub.checked; + failed += sub.failed; + } + } + + return { checked, failed }; +} + +export function checkFittsLaw( + nodes: readonly SceneNode[], + options: { skipLocked?: boolean; skipHidden?: boolean } = {}, +): FittsLawResult { + issueCounter = 0; + const issues: LintIssue[] = []; + const skipLocked = options.skipLocked ?? true; + const skipHidden = options.skipHidden ?? true; + + let totalChecked = 0; + let totalFailed = 0; + + for (const node of nodes) { + const sub = traverseForFitts(node, issues, skipLocked, skipHidden, false); + totalChecked += sub.checked; + totalFailed += sub.failed; + } + + return { + issues, + summary: { + totalChecked, + passed: totalChecked - totalFailed, + failed: totalFailed, + }, + }; +} diff --git a/src/lint/gestalt.ts b/src/lint/gestalt.ts new file mode 100644 index 0000000..56c4aa7 --- /dev/null +++ b/src/lint/gestalt.ts @@ -0,0 +1,128 @@ +/// + +import { LintIssue } from './types'; + +// ────────────────────────────────────────────── +// Gestalt Principles Lint Module +// +// Deterministic checks based on Gestalt grouping principles: +// - Proximity: siblings with inconsistent gaps +// - Similarity: sibling elements with mixed styling +// - Alignment: children not grid-aligned +// ────────────────────────────────────────────── + +let issueCounter = 0; +function nextId(): string { + return `gestalt-${++issueCounter}`; +} + +export interface GestaltResult { + issues: LintIssue[]; + summary: { totalChecked: number; passed: number; failed: number }; +} + +/** + * Check Gestalt proximity: siblings in a non-auto-layout frame + * should have consistent spacing between them. + */ +function checkProximity( + node: SceneNode, + issues: LintIssue[], + skipLocked: boolean, + skipHidden: boolean, + parentLocked: boolean, +): { checked: number; failed: number } { + const isLocked = parentLocked || ('locked' in node && (node as any).locked); + const isHidden = 'visible' in node && !node.visible; + + if (skipLocked && isLocked) return { checked: 0, failed: 0 }; + if (skipHidden && isHidden) return { checked: 0, failed: 0 }; + + let checked = 0; + let failed = 0; + + const isFrameLike = node.type === 'FRAME' || node.type === 'COMPONENT' || node.type === 'INSTANCE'; + + if (isFrameLike && 'children' in node) { + const frame = node as FrameNode; + + // Only check non-auto-layout frames with 3+ visible children + if (frame.layoutMode === 'NONE' && frame.children.length >= 3) { + checked++; + + const visible = frame.children.filter(c => 'visible' in c && c.visible && 'y' in c); + if (visible.length >= 3) { + // Sort by Y position + const sorted = [...visible].sort((a, b) => ((a as any).y as number) - ((b as any).y as number)); + + const gaps: number[] = []; + for (let i = 1; i < sorted.length; i++) { + const prevBottom = ((sorted[i - 1] as any).y as number) + ((sorted[i - 1] as any).height as number); + const currTop = (sorted[i] as any).y as number; + gaps.push(currTop - prevBottom); + } + + if (gaps.length >= 2) { + const uniqueGaps = new Set(gaps.map(g => Math.round(g))); + // Flag if more than 2 distinct gap sizes (indicates inconsistent spacing) + if (uniqueGaps.size > 2) { + failed++; + issues.push({ + id: nextId(), + type: 'gestalt', + severity: 'info', + nodeId: node.id, + nodeName: node.name, + message: `Frame "${node.name}" has ${uniqueGaps.size} different spacing gaps between children (${[...uniqueGaps].join(', ')}px) — inconsistent proximity weakens visual grouping (Gestalt proximity principle)`, + currentValue: `${uniqueGaps.size} distinct gaps`, + suggestions: [ + 'Use auto-layout with consistent gap spacing', + 'Standardize spacing between sibling elements', + ], + autoFixable: false, + }); + } + } + } + } + } + + // Recurse + if ('children' in node) { + for (const child of (node as any).children as SceneNode[]) { + const sub = checkProximity(child, issues, skipLocked, skipHidden, isLocked); + checked += sub.checked; + failed += sub.failed; + } + } + + return { checked, failed }; +} + +export function checkGestalt( + nodes: readonly SceneNode[], + options: { skipLocked?: boolean; skipHidden?: boolean } = {}, +): GestaltResult { + issueCounter = 0; + const issues: LintIssue[] = []; + const skipLocked = options.skipLocked ?? true; + const skipHidden = options.skipHidden ?? true; + + let totalChecked = 0; + let totalFailed = 0; + + for (const node of nodes) { + const sub = checkProximity(node, issues, skipLocked, skipHidden, false); + totalChecked += sub.checked; + totalFailed += sub.failed; + } + + return { + issues, + summary: { + totalChecked, + passed: totalChecked - totalFailed, + failed: totalFailed, + }, + }; +} diff --git a/src/lint/realtime-lint.ts b/src/lint/realtime-lint.ts new file mode 100644 index 0000000..23bdea5 --- /dev/null +++ b/src/lint/realtime-lint.ts @@ -0,0 +1,130 @@ +/// + +import { LintSettings } from '../types'; +import { runDesignLint, DEFAULT_LINT_SETTINGS } from '../core/design-lint'; +import { sendMessageToUI } from '../utils/figma-helpers'; + +// ────────────────────────────────────────────── +// Real-Time Incremental Linting +// +// Uses figma.on('documentchange') to detect changed nodes +// and re-lint only the affected subtrees. Debounced to avoid +// flooding the UI with rapid-fire updates. +// ────────────────────────────────────────────── + +export interface RealtimeLintConfig { + enabled: boolean; + debounceMs: number; // default 500 + settings: LintSettings; +} + +let realtimeConfig: RealtimeLintConfig | null = null; +let debounceTimer: number | null = null; +let pendingNodeIds: Set = new Set(); +let handlerRegistered = false; + +/** + * Handler for figma.on('documentchange'). + * Collects changed node IDs, filters to PROPERTY_CHANGE and CREATE, + * then debounces re-linting. + */ +function onDocumentChange(event: DocumentChangeEvent): void { + if (!realtimeConfig || !realtimeConfig.enabled) return; + + for (const change of event.documentChanges) { + // Only react to property changes and creations — not deletes + if (change.type === 'PROPERTY_CHANGE' || change.type === 'CREATE' || change.type === 'STYLE_PROPERTY_CHANGE') { + if ('id' in change && typeof change.id === 'string') { + pendingNodeIds.add(change.id); + } + } + } + + if (pendingNodeIds.size === 0) return; + + // Debounce: clear previous timer and start new one + if (debounceTimer !== null) { + clearTimeout(debounceTimer); + } + + const debounceMs = realtimeConfig.debounceMs || 500; + debounceTimer = setTimeout(() => { + processChangedNodes(); + }, debounceMs) as unknown as number; +} + +/** + * Fetch each changed node, filter to valid SceneNodes, and re-lint. + */ +async function processChangedNodes(): Promise { + if (!realtimeConfig) return; + + const nodeIds = Array.from(pendingNodeIds); + pendingNodeIds.clear(); + debounceTimer = null; + + const changedNodes: SceneNode[] = []; + const changedNodeIds: string[] = []; + + for (const id of nodeIds) { + try { + const node = await figma.getNodeByIdAsync(id); + if (node && 'type' in node && node.type !== 'PAGE' && node.type !== 'DOCUMENT') { + changedNodes.push(node as SceneNode); + changedNodeIds.push(id); + } + } catch { + // Node may have been deleted between change event and processing — skip + } + } + + if (changedNodes.length === 0) return; + + try { + const result = runDesignLint(changedNodes, realtimeConfig.settings); + + sendMessageToUI('realtime-lint-update', { + errors: result.errors, + changedNodeIds, + }); + } catch (error) { + console.error('Realtime lint error:', error); + } +} + +/** + * Enable real-time incremental linting. + * Registers the documentchange handler and stores the config. + */ +export function enableRealtimeLint(config: RealtimeLintConfig): void { + realtimeConfig = { + enabled: config.enabled, + debounceMs: config.debounceMs || 500, + settings: config.settings || DEFAULT_LINT_SETTINGS, + }; + + if (!handlerRegistered) { + figma.on('documentchange', onDocumentChange); + handlerRegistered = true; + } +} + +/** + * Disable real-time incremental linting. + * Unregisters the handler and clears pending state. + */ +export function disableRealtimeLint(): void { + realtimeConfig = null; + + if (handlerRegistered) { + figma.off('documentchange', onDocumentChange); + handlerRegistered = false; + } + + if (debounceTimer !== null) { + clearTimeout(debounceTimer); + debounceTimer = null; + } + + pendingNodeIds.clear(); +} diff --git a/src/lint/responsive.ts b/src/lint/responsive.ts new file mode 100644 index 0000000..485eda9 --- /dev/null +++ b/src/lint/responsive.ts @@ -0,0 +1,349 @@ +/// + +import { LintIssue } from './types'; + +// ────────────────────────────────────────────── +// Responsive Design Lint Module +// +// Deterministic checks for responsive design issues: +// - Fixed-width elements without fill sizing +// - Text truncation risk (fixed width + long content) +// - Missing wrap on multi-column layouts +// - Breakpoint variant detection +// ────────────────────────────────────────────── + +let issueCounter = 0; +function nextId(): string { + return `resp-${++issueCounter}`; +} + +export interface ResponsiveResult { + issues: LintIssue[]; + metrics: { + fixedWidthElements: number; + textTruncationRisk: number; + missingAutoLayout: number; + breakpointVariants: string[]; + }; + summary: { totalChecked: number; passed: number; failed: number }; +} + +// ── Breakpoint detection patterns ────────────────────── +const BREAKPOINT_PATTERNS = [ + // "Home - Desktop", "Home - Tablet", "Home - Mobile" + /^(.+)\s*[-–—]\s*(desktop|tablet|mobile|phone|sm|md|lg|xl|xxl|small|medium|large)\s*$/i, + // "Home/desktop", "Home/mobile" + /^(.+)\/(desktop|tablet|mobile|phone|sm|md|lg|xl|xxl|small|medium|large)\s*$/i, + // "Desktop/Home", "Mobile/Home" + /^(desktop|tablet|mobile|phone|sm|md|lg|xl|xxl|small|medium|large)\s*[-–—/]\s*(.+)$/i, + // "Home [Desktop]", "Home [Mobile]" + /^(.+)\s*\[(desktop|tablet|mobile|phone|sm|md|lg|xl|xxl|small|medium|large)\]\s*$/i, +]; + +const BREAKPOINT_KEYWORDS = new Set([ + 'desktop', 'tablet', 'mobile', 'phone', + 'sm', 'md', 'lg', 'xl', 'xxl', + 'small', 'medium', 'large', +]); + +/** Average character width ratio relative to font size (rough heuristic). */ +const AVG_CHAR_WIDTH_RATIO = 0.5; + +// ── Helpers ────────────────────────────────── + +function isFrameLike(node: SceneNode): node is FrameNode | ComponentNode | InstanceNode { + return node.type === 'FRAME' || node.type === 'COMPONENT' || node.type === 'INSTANCE'; +} + +function detectBreakpointLabel(name: string): string | null { + for (const pattern of BREAKPOINT_PATTERNS) { + const match = name.match(pattern); + if (match) { + // Return the breakpoint keyword that matched + for (const group of match.slice(1)) { + if (BREAKPOINT_KEYWORDS.has(group.toLowerCase())) { + return group.toLowerCase(); + } + } + } + } + return null; +} + +// ── Checks ────────────────────────────────── + +function checkFixedWidth( + node: SceneNode, + issues: LintIssue[], + skipLocked: boolean, + skipHidden: boolean, + parentLocked: boolean, +): { checked: number; failed: number; fixedWidthCount: number } { + const isLocked = parentLocked || ('locked' in node && (node as any).locked); + const isHidden = 'visible' in node && !node.visible; + + if (skipLocked && isLocked) return { checked: 0, failed: 0, fixedWidthCount: 0 }; + if (skipHidden && isHidden) return { checked: 0, failed: 0, fixedWidthCount: 0 }; + + let checked = 0; + let failed = 0; + let fixedWidthCount = 0; + + if (isFrameLike(node)) { + const frame = node as FrameNode; + checked++; + + // Check if this frame has explicit width but no fill sizing and no auto-layout parent handling + const hasFixedWidth = frame.layoutSizingHorizontal === 'FIXED' || frame.layoutSizingHorizontal === undefined; + const isRootLevel = !frame.parent || frame.parent.type === 'PAGE'; + const hasAutoLayout = frame.layoutMode !== 'NONE'; + const hasMinMax = ('minWidth' in frame && frame.minWidth !== null && frame.minWidth !== undefined) || + ('maxWidth' in frame && frame.maxWidth !== null && frame.maxWidth !== undefined); + + if (hasFixedWidth && !isRootLevel && !hasMinMax && hasAutoLayout && frame.width > 200) { + fixedWidthCount++; + failed++; + issues.push({ + id: nextId(), + type: 'responsive', + severity: 'warning', + nodeId: node.id, + nodeName: node.name, + message: `Frame "${node.name}" has fixed width (${Math.round(frame.width)}px) with auto-layout but no fill/hug sizing — may not adapt to different screen sizes`, + currentValue: `${Math.round(frame.width)}px fixed`, + suggestions: [ + 'Set horizontal sizing to "Fill" for responsive behavior', + 'Add min-width/max-width constraints', + 'Use "Hug contents" if the frame should shrink-wrap', + ], + autoFixable: false, + }); + } + } + + // Recurse + if ('children' in node) { + for (const child of (node as any).children as SceneNode[]) { + const sub = checkFixedWidth(child, issues, skipLocked, skipHidden, isLocked); + checked += sub.checked; + failed += sub.failed; + fixedWidthCount += sub.fixedWidthCount; + } + } + + return { checked, failed, fixedWidthCount }; +} + +function checkTextTruncation( + node: SceneNode, + issues: LintIssue[], + skipLocked: boolean, + skipHidden: boolean, + parentLocked: boolean, +): { checked: number; failed: number; riskCount: number } { + const isLocked = parentLocked || ('locked' in node && (node as any).locked); + const isHidden = 'visible' in node && !node.visible; + + if (skipLocked && isLocked) return { checked: 0, failed: 0, riskCount: 0 }; + if (skipHidden && isHidden) return { checked: 0, failed: 0, riskCount: 0 }; + + let checked = 0; + let failed = 0; + let riskCount = 0; + + if (node.type === 'TEXT') { + const textNode = node as TextNode; + checked++; + + const fontSize = textNode.fontSize !== figma.mixed ? textNode.fontSize : 14; + const textResize = textNode.textAutoResize; + + // Only check text nodes with fixed width (not auto-width or auto-height) + if (textResize === 'NONE' || textResize === 'TRUNCATE') { + const charCount = textNode.characters.length; + const estimatedTextWidth = charCount * (fontSize as number) * AVG_CHAR_WIDTH_RATIO; + const nodeWidth = textNode.width; + + // Flag if text fills > 80% of available width + if (charCount > 5 && estimatedTextWidth > nodeWidth * 0.8) { + riskCount++; + failed++; + issues.push({ + id: nextId(), + type: 'responsive', + severity: 'info', + nodeId: node.id, + nodeName: node.name, + message: `Text "${node.name}" may truncate — content fills ~${Math.round((estimatedTextWidth / nodeWidth) * 100)}% of fixed width (${Math.round(nodeWidth)}px). Translations or dynamic content could overflow.`, + currentValue: `${charCount} chars in ${Math.round(nodeWidth)}px`, + suggestions: [ + 'Use auto-width or auto-height text resizing', + 'Allow text to wrap by setting textAutoResize to HEIGHT', + 'Add ellipsis handling if truncation is intentional', + ], + autoFixable: false, + }); + } + } + } + + // Recurse + if ('children' in node) { + for (const child of (node as any).children as SceneNode[]) { + const sub = checkTextTruncation(child, issues, skipLocked, skipHidden, isLocked); + checked += sub.checked; + failed += sub.failed; + riskCount += sub.riskCount; + } + } + + return { checked, failed, riskCount }; +} + +function checkMissingWrap( + node: SceneNode, + issues: LintIssue[], + skipLocked: boolean, + skipHidden: boolean, + parentLocked: boolean, +): { checked: number; failed: number; missingCount: number } { + const isLocked = parentLocked || ('locked' in node && (node as any).locked); + const isHidden = 'visible' in node && !node.visible; + + if (skipLocked && isLocked) return { checked: 0, failed: 0, missingCount: 0 }; + if (skipHidden && isHidden) return { checked: 0, failed: 0, missingCount: 0 }; + + let checked = 0; + let failed = 0; + let missingCount = 0; + + if (isFrameLike(node)) { + const frame = node as FrameNode; + + // Check horizontal auto-layout with 3+ children and no wrap + if (frame.layoutMode === 'HORIZONTAL' && 'children' in frame) { + const visibleChildren = frame.children.filter(c => 'visible' in c && c.visible); + + if (visibleChildren.length >= 3) { + checked++; + + const layoutWrap = ('layoutWrap' in frame) ? (frame as any).layoutWrap : 'NO_WRAP'; + if (layoutWrap !== 'WRAP') { + missingCount++; + failed++; + issues.push({ + id: nextId(), + type: 'responsive', + severity: 'warning', + nodeId: node.id, + nodeName: node.name, + message: `Horizontal layout "${node.name}" has ${visibleChildren.length} children without wrap — content won't reflow on smaller screens`, + currentValue: `${visibleChildren.length} children, no wrap`, + suggestions: [ + 'Enable "Wrap" on the auto-layout to allow content reflow', + 'Consider switching to vertical layout on mobile breakpoints', + 'Use min-width on children to control when wrapping occurs', + ], + autoFixable: false, + }); + } + } + } + } + + // Recurse + if ('children' in node) { + for (const child of (node as any).children as SceneNode[]) { + const sub = checkMissingWrap(child, issues, skipLocked, skipHidden, isLocked); + checked += sub.checked; + failed += sub.failed; + missingCount += sub.missingCount; + } + } + + return { checked, failed, missingCount }; +} + +function detectBreakpointVariants( + nodes: readonly SceneNode[], +): string[] { + const variantNames: Set = new Set(); + + for (const node of nodes) { + collectBreakpointVariantNames(node, variantNames); + } + + return Array.from(variantNames); +} + +function collectBreakpointVariantNames( + node: SceneNode, + results: Set, +): void { + const label = detectBreakpointLabel(node.name); + if (label) { + results.add(node.name); + } + + if ('children' in node) { + // Only check top-level frames (direct children of pages) for breakpoint variants + for (const child of (node as any).children as SceneNode[]) { + const childLabel = detectBreakpointLabel(child.name); + if (childLabel) { + results.add(child.name); + } + } + } +} + +// ── Public API ────────────────────────────────── + +export function checkResponsive( + nodes: readonly SceneNode[], + opts?: { skipLocked?: boolean; skipHidden?: boolean }, +): ResponsiveResult { + issueCounter = 0; + const issues: LintIssue[] = []; + const skipLocked = opts?.skipLocked ?? true; + const skipHidden = opts?.skipHidden ?? true; + + let totalChecked = 0; + let totalFailed = 0; + let totalFixedWidth = 0; + let totalTruncation = 0; + let totalMissingWrap = 0; + + for (const node of nodes) { + const fw = checkFixedWidth(node, issues, skipLocked, skipHidden, false); + totalChecked += fw.checked; + totalFailed += fw.failed; + totalFixedWidth += fw.fixedWidthCount; + + const tt = checkTextTruncation(node, issues, skipLocked, skipHidden, false); + totalChecked += tt.checked; + totalFailed += tt.failed; + totalTruncation += tt.riskCount; + + const mw = checkMissingWrap(node, issues, skipLocked, skipHidden, false); + totalChecked += mw.checked; + totalFailed += mw.failed; + totalMissingWrap += mw.missingCount; + } + + const breakpointVariants = detectBreakpointVariants(nodes); + + return { + issues, + metrics: { + fixedWidthElements: totalFixedWidth, + textTruncationRisk: totalTruncation, + missingAutoLayout: totalMissingWrap, + breakpointVariants, + }, + summary: { + totalChecked, + passed: totalChecked - totalFailed, + failed: totalFailed, + }, + }; +} diff --git a/src/lint/types.ts b/src/lint/types.ts index d6f69de..ad23403 100644 --- a/src/lint/types.ts +++ b/src/lint/types.ts @@ -17,7 +17,11 @@ export type LintIssueType = | 'spacing' | 'autoLayout' | 'naming' - | 'accessibility'; + | 'accessibility' + | 'fittsLaw' + | 'gestalt' + | 'detachedInstance' + | 'responsive'; export type LintSeverity = 'critical' | 'warning' | 'info'; diff --git a/src/types.ts b/src/types.ts index 9e8da76..b6330e0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -221,10 +221,21 @@ export type UIMessageType = | 'rescan-lint' | 'export-screenshot' | 'analyze-flow' + | 'analyze-page' | 'save-baseline' | 'load-baseline' | 'compare-baseline' - | 'delete-baseline'; + | 'delete-baseline' + // Variable system & DTCG compliance + | 'collect-variables' + | 'check-dtcg-compliance' + // Dark mode validation + | 'compare-modes' + // Realtime lint + | 'enable-realtime-lint' + | 'disable-realtime-lint' + // Design debt + | 'calculate-design-debt'; // Auto-fix Types export interface FixRequest { @@ -376,7 +387,7 @@ export type TokenCategory = 'colors' | 'spacing' | 'typography' | 'effects' | 'b // Design Lint Types (deterministic, non-AI rules) // ────────────────────────────────────────────── -export type LintErrorType = 'fill' | 'stroke' | 'effect' | 'text' | 'radius' | 'spacing' | 'autoLayout' | 'accessibility' | 'visualQuality' | 'microcopy' | 'conversion' | 'cognitive'; +export type LintErrorType = 'fill' | 'stroke' | 'effect' | 'text' | 'radius' | 'spacing' | 'autoLayout' | 'accessibility' | 'visualQuality' | 'microcopy' | 'conversion' | 'cognitive' | 'fittsLaw' | 'gestalt' | 'detachedInstance' | 'responsive'; export interface LintError { nodeId: string; @@ -419,6 +430,10 @@ export interface LintSettings { checkMicrocopy: boolean; checkConversion: boolean; checkCognitive: boolean; + checkFittsLaw: boolean; + checkGestalt: boolean; + checkDetachedInstances: boolean; + checkResponsive: boolean; allowedRadii: number[]; skipLockedLayers: boolean; skipHiddenLayers: boolean; diff --git a/src/ui/message-handler.ts b/src/ui/message-handler.ts index 8d428c8..ff71686 100644 --- a/src/ui/message-handler.ts +++ b/src/ui/message-handler.ts @@ -41,6 +41,12 @@ import { fixSpacingToNearest, fixAllSpacingOnNode } from '../fix/fix-spacing'; import { fixRadiusToNearest } from '../fix/fix-radius'; import { renameLayerById } from '../fix/rename-layer'; import { executeBatchFix, type BatchFixAction } from '../fix/batch'; +import { collectVariableSystem } from '../extract/variable-collector'; +import { parseDTCG } from '../baseline/dtcg-parser'; +import { checkTokenCompliance } from '../baseline/token-compliance'; +import { compareModes } from '../extract/mode-comparator'; +import { enableRealtimeLint, disableRealtimeLint } from '../lint/realtime-lint'; +import { calculateDesignDebt } from '../baseline/design-debt'; import { lintSelection, ignoreNode, @@ -209,6 +215,9 @@ export async function handleUIMessage(msg: PluginMessage): Promise { case 'analyze-flow': await handleAnalyzeFlow(); break; + case 'analyze-page': + await handleAnalyzePage(); + break; // Baseline & Diff handlers case 'save-baseline': handleSaveBaseline(data); @@ -222,6 +231,28 @@ export async function handleUIMessage(msg: PluginMessage): Promise { case 'delete-baseline': handleDeleteBaseline(data); break; + // Variable system & DTCG compliance handlers + case 'collect-variables': + await handleCollectVariables(); + break; + case 'check-dtcg-compliance': + await handleCheckDTCGCompliance(data); + break; + // Dark mode validation handler + case 'compare-modes': + await handleCompareModes(data); + break; + // Realtime lint handlers + case 'enable-realtime-lint': + handleEnableRealtimeLint(data); + break; + case 'disable-realtime-lint': + handleDisableRealtimeLint(); + break; + // Design debt handler + case 'calculate-design-debt': + handleCalculateDesignDebt(data); + break; default: console.warn('Unknown message type:', type); } @@ -1499,6 +1530,112 @@ async function handleAnalyzeFlow(): Promise { } } +/** + * Whole-Page Sweep: iterate all top-level frames, lint + screenshot each, + * then post aggregated results back to the UI. + */ +async function handleAnalyzePage(): Promise { + try { + const allChildren = figma.currentPage.children; + + const frames = allChildren.filter( + (node): node is FrameNode | ComponentSetNode => + node.type === 'FRAME' || node.type === 'COMPONENT_SET' + ); + + if (frames.length === 0) { + sendMessageToUI('analysis-error', { error: 'No top-level frames found on current page.' }); + return; + } + + const framesToAnalyze = frames.slice(0, 50); + const total = framesToAnalyze.length; + + const { runDesignLint } = await import('../core/design-lint'); + + const frameResults: Array<{ + id: string; + name: string; + screenshot: string; + lintResult: { summary: any; errors: any[] }; + width: number; + height: number; + }> = []; + + const BATCH_SIZE = 5; + for (let i = 0; i < total; i += BATCH_SIZE) { + const batch = framesToAnalyze.slice(i, i + BATCH_SIZE); + const promises = batch.map(async (frame, batchIdx) => { + const current = i + batchIdx + 1; + + sendMessageToUI('page-sweep-progress', { + current, + total, + frameName: frame.name, + }); + + let lintResult: { summary: any; errors: any[] } = { summary: { totalErrors: 0, byType: {}, totalNodes: 0, nodesWithErrors: 0 }, errors: [] }; + try { + lintResult = runDesignLint([frame as SceneNode], currentLintSettings); + } catch { + // Non-critical + } + + let screenshot = ''; + try { + screenshot = await exportScreenshot(frame as SceneNode, 800); + } catch { + // Non-critical + } + + return { + id: frame.id, + name: frame.name, + screenshot, + lintResult: { summary: lintResult.summary, errors: lintResult.errors }, + width: Math.round(frame.width), + height: Math.round(frame.height), + }; + }); + + const batchResults = await Promise.all(promises); + frameResults.push(...batchResults); + } + + let totalIssues = 0; + const issueTypeCounts: Record = {}; + + for (const fr of frameResults) { + totalIssues += fr.lintResult.summary.totalErrors || 0; + for (const err of fr.lintResult.errors) { + const key = err.errorType; + if (!issueTypeCounts[key]) { + issueTypeCounts[key] = { count: 0, severity: err.severity || 'warning' }; + } + issueTypeCounts[key].count++; + } + } + + const topIssues = Object.entries(issueTypeCounts) + .sort((a, b) => b[1].count - a[1].count) + .slice(0, 10) + .map(([type, { count, severity }]) => ({ type, count, severity })); + + sendMessageToUI('page-sweep-result', { + frames: frameResults, + aggregated: { + totalFrames: total, + totalIssues, + topIssues, + }, + }); + + } catch (error) { + const msg = error instanceof Error ? error.message : 'Unknown error'; + sendMessageToUI('analysis-error', { error: `Page sweep failed: ${msg}` }); + } +} + /** * Initialize plugin with design systems knowledge */ @@ -2297,3 +2434,93 @@ async function handleAddComponentProperty(data: { figma.notify(`Failed to add property: ${errorMessage}`, { error: true }); } } + +// ────────────────────────────────────────────── +// Variable System & DTCG Compliance Handlers +// ────────────────────────────────────────────── + +async function handleCollectVariables(): Promise { + try { + const report = await collectVariableSystem(); + sendMessageToUI('variable-system-result', report); + } catch (error) { + console.error('Error collecting variables:', error); + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + sendMessageToUI('variable-system-error', { error: errorMessage }); + } +} + +async function handleCheckDTCGCompliance(data: { dtcgJson: string }): Promise { + try { + // Parse the DTCG JSON + const dtcgTokens = parseDTCG(data.dtcgJson); + + // Collect the current variable system + const variableReport = await collectVariableSystem(); + + // Run compliance check + const result = checkTokenCompliance(variableReport, dtcgTokens, null); + + sendMessageToUI('dtcg-compliance-result', result); + } catch (error) { + console.error('Error checking DTCG compliance:', error); + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + sendMessageToUI('dtcg-compliance-error', { error: errorMessage }); + } +} + +// ────────────────────────────────────────────── +// Dark Mode Validation Handler +// ────────────────────────────────────────────── + +async function handleCompareModes(data: { collectionId: string }): Promise { + try { + const modeData = await compareModes(data.collectionId); + sendMessageToUI('mode-comparison-result', modeData); + } catch (error) { + console.error('Error comparing modes:', error); + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + sendMessageToUI('mode-comparison-error', { error: errorMessage }); + } +} + +// ============================================================================ +// Realtime Lint Handlers +// ============================================================================ + +/** + * Enable real-time incremental linting via documentchange events. + */ +function handleEnableRealtimeLint(data: { debounceMs?: number; settings?: LintSettings }): void { + const settings = data.settings || DEFAULT_LINT_SETTINGS; + enableRealtimeLint({ + enabled: true, + debounceMs: data.debounceMs || 500, + settings, + }); +} + +/** + * Disable real-time incremental linting. + */ +function handleDisableRealtimeLint(): void { + disableRealtimeLint(); +} + +// ============================================================================ +// Design Debt Handler +// ============================================================================ + +/** + * Calculate design debt score from lint results and token summary. + */ +function handleCalculateDesignDebt(data: { + lintResult: { + errors: Array<{ errorType: string; severity?: string; nodeId: string; nodeName: string; message: string; value: string; path: string; property?: string }>; + summary: { totalErrors: number; byType: Record; totalNodes: number; nodesWithErrors: number }; + }; + tokenSummary?: { totalTokens: number; actualTokens: number; hardCodedValues: number; aiSuggestions: number }; +}): void { + const score = calculateDesignDebt(data.lintResult, data.tokenSummary || null); + sendMessageToUI('design-debt-result', score); +} diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 34635db..2a71b80 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -3,8 +3,8 @@ import ChatContainer from './components/chat/ChatContainer'; import SettingsPanel from './components/shared/SettingsPanel'; import { useChat } from './hooks/useChat'; import { usePluginMessages, usePostToPlugin } from './hooks/usePluginMessages'; -import type { PluginEvent, LintResult, LintError, AiReviewData, ReferoComparisonData, FlowAnalysisData, DiffResultData } from './lib/messages'; -import { analyzeComponent, streamChat, checkHealth, setBackendUrl, fetchReferoData, analyzeFlow } from './lib/api'; +import type { PluginEvent, LintResult, LintError, AiReviewData, ReferoComparisonData, FlowAnalysisData, DiffResultData, PageSweepData, PageSweepRawData } from './lib/messages'; +import { analyzeComponent, streamChat, checkHealth, setBackendUrl, fetchReferoData, analyzeFlow, analyzePageSweep } from './lib/api'; export default function App() { const chat = useChat(); @@ -243,6 +243,40 @@ export default function App() { } break; } + case 'page-sweep-progress': { + const prog = event.data as { current: number; total: number; frameName: string }; + chat.addMessage({ + kind: 'ai-text', + content: `Sweeping page: ${prog.current}/${prog.total} - "${prog.frameName}"...`, + }); + break; + } + case 'page-sweep-result': { + const sweepData = event.data as PageSweepRawData; + chat.addMessage({ + kind: 'ai-text', + content: `Page sweep complete: ${sweepData.frames.length} frames analyzed. ${backendAvailable ? 'Running AI analysis...' : 'Backend unavailable, showing deterministic results.'}`, + }); + + if (backendAvailable) { + analyzePageSweep({ + frames: sweepData.frames, + }).then((result) => { + chat.addMessage({ kind: 'page-sweep-result', data: result as PageSweepData }); + }).catch((err) => { + chat.addMessage({ + kind: 'ai-text', + content: `AI page analysis failed: ${err instanceof Error ? err.message : 'Unknown error'}. Showing deterministic results.`, + }); + const deterministicResult = buildDeterministicSweepResult(sweepData); + chat.addMessage({ kind: 'page-sweep-result', data: deterministicResult }); + }); + } else { + const deterministicResult = buildDeterministicSweepResult(sweepData); + chat.addMessage({ kind: 'page-sweep-result', data: deterministicResult }); + } + break; + } case 'selection-changed': { const selData = event.data as { hasSelection: boolean; nodeId: string | null; nodeName: string | null }; setCurrentNodeName(selData.nodeName); @@ -557,6 +591,12 @@ export default function App() { break; } + case 'analyze-page': { + chat.addMessage({ kind: 'ai-text', content: 'Starting whole-page sweep...' }); + post('analyze-page'); + break; + } + case 'toggle-mode': { const next = analysisMode === 'quick' ? 'deep' : 'quick'; setAnalysisMode(next); @@ -611,6 +651,13 @@ export default function App() { > Analyze Flow + + )} + + + {/* Top Issues */} + {fileHealth.topIssues.length > 0 && ( +
+

Top Issues

+
+ {fileHealth.topIssues.slice(0, 5).map((issue, i) => ( +
+
+ + {issue.type} +
+ {issue.count} +
+ ))} +
+
+ )} + + {/* AI Insights (collapsible) */} + {aiInsights.summary && ( +
+ + + {showInsights && ( +
+ {/* Summary */} +

{aiInsights.summary}

+ + {/* Strengths */} + {aiInsights.strengths.length > 0 && ( +
+

Strengths

+
    + {aiInsights.strengths.map((s, i) => ( +
  • + + + {s} +
  • + ))} +
+
+ )} + + {/* Weaknesses */} + {aiInsights.weaknesses.length > 0 && ( +
+

Weaknesses

+
    + {aiInsights.weaknesses.map((w, i) => ( +
  • + - + {w} +
  • + ))} +
+
+ )} + + {/* Recommendations */} + {aiInsights.recommendations.length > 0 && ( +
+

Recommendations

+
    + {aiInsights.recommendations.map((rec, i) => ( +
  • + {rec.title}:{' '} + {rec.description} + {rec.affectedFrames.length > 0 && ( +
    + Affects: {rec.affectedFrames.join(', ')} +
    + )} +
  • + ))} +
+
+ )} +
+ )} +
+ )} + + ); +} diff --git a/ui/src/components/shared/QuickActions.tsx b/ui/src/components/shared/QuickActions.tsx index e98a432..1d97c8d 100644 --- a/ui/src/components/shared/QuickActions.tsx +++ b/ui/src/components/shared/QuickActions.tsx @@ -60,6 +60,13 @@ export default function QuickActions({ onAnalyze, hasFixable, analysisMode = 'qu > {analysisMode === 'quick' ? 'Quick' : 'Deep'} + ); } diff --git a/ui/src/lib/api.ts b/ui/src/lib/api.ts index ef40b1f..ecf5297 100644 --- a/ui/src/lib/api.ts +++ b/ui/src/lib/api.ts @@ -206,6 +206,56 @@ export async function checkHealth(): Promise { } } +/** + * POST /api/analyze-page — whole-page sweep analysis. + */ +export async function analyzePageSweep(data: { + frames: Array<{ + id: string; + name: string; + screenshot: string; + lintResult: { summary: unknown; errors: unknown[] }; + width: number; + height: number; + }>; + sessionId?: string; +}): Promise<{ + fileHealth: { + overallScore: number; + grade: string; + totalFrames: number; + totalIssues: number; + topIssues: Array<{ type: string; count: number; severity: string }>; + consistencyScore: number; + }; + frames: Array<{ + id: string; + name: string; + score: number; + issueCount: number; + topIssues: string[]; + }>; + aiInsights: { + strengths: string[]; + weaknesses: string[]; + recommendations: Array<{ title: string; description: string; affectedFrames: string[] }>; + summary: string; + }; +}> { + const resp = await fetch(`${backendUrl}/api/analyze-page`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + + if (!resp.ok) { + const err = await resp.json().catch(() => ({ error: resp.statusText })); + throw new Error(err.error || 'Page sweep analysis failed'); + } + + return resp.json(); +} + /** * POST /api/analyze-flow — multi-frame AI flow analysis. */ diff --git a/ui/src/lib/messages.ts b/ui/src/lib/messages.ts index 908041f..7e878c9 100644 --- a/ui/src/lib/messages.ts +++ b/ui/src/lib/messages.ts @@ -2,7 +2,7 @@ // Message type definitions for Plugin ↔ UI communication // ────────────────────────────────────────────── -export type LintErrorType = 'fill' | 'stroke' | 'effect' | 'text' | 'radius' | 'spacing' | 'autoLayout' | 'accessibility' | 'visualQuality' | 'microcopy' | 'conversion' | 'cognitive'; +export type LintErrorType = 'fill' | 'stroke' | 'effect' | 'text' | 'radius' | 'spacing' | 'autoLayout' | 'accessibility' | 'visualQuality' | 'microcopy' | 'conversion' | 'cognitive' | 'fittsLaw' | 'gestalt' | 'detachedInstance' | 'responsive'; export interface LintError { nodeId: string; @@ -117,7 +117,8 @@ export type ChatMessageType = | { kind: 'analysis-phase'; phase: AnalysisPhase; done?: boolean } | { kind: 'flow-result'; data: FlowAnalysisData } | { kind: 'diff-result'; data: DiffResultData } - | { kind: 'baseline-saved'; data: { nodeId: string; nodeName: string; timestamp: number; overall: number } }; + | { kind: 'baseline-saved'; data: { nodeId: string; nodeName: string; timestamp: number; overall: number } } + | { kind: 'page-sweep-result'; data: PageSweepData }; export type AiRating = 'pass' | 'needs_improvement' | 'fail'; @@ -214,7 +215,9 @@ export type PluginEvent = | { type: 'flow-analysis-error'; data: { error: string } } | { type: 'baseline-saved'; data: { nodeId: string; nodeName: string; timestamp: number; overall: number } } | { type: 'baseline-loaded'; data: { nodeId: string; nodeName: string; timestamp: number; overall: number } | null } - | { type: 'diff-result'; data: DiffResultData }; + | { type: 'diff-result'; data: DiffResultData } + | { type: 'page-sweep-progress'; data: { current: number; total: number; frameName: string } } + | { type: 'page-sweep-result'; data: PageSweepRawData }; // Flow Analysis Types export interface FlowGraphIssue { @@ -323,6 +326,159 @@ export interface BaselineMetaData { overall: number; } +// ── Variable System & DTCG Compliance Types ────────────────── + +export interface VariableCollectionData { + id: string; + name: string; + modes: Array<{ modeId: string; name: string }>; + variables: VariableData[]; +} + +export interface VariableData { + id: string; + name: string; + resolvedType: 'COLOR' | 'FLOAT' | 'STRING' | 'BOOLEAN'; + description: string; + valuesByMode: Record; + scopes: string[]; + consumers: number; +} + +export interface VariableSystemReport { + collections: VariableCollectionData[]; + totalVariables: number; + unusedVariables: string[]; + adoptionRate: number; + modesCoverage: Record; +} + +export interface DTCGComplianceResult { + adoptionScore: number; + matched: Array<{ token: string; nodeCount: number; usage: 'correct' | 'overridden' }>; + unmatched: Array<{ value: string; nodeCount: number; nearestToken: string; distance: number }>; + orphanTokens: string[]; + missingFromSystem: string[]; + summary: { totalTokenDefs: number; usedInDesign: number; hardCodedValues: number; compliance: number }; +} + +// ── Dark Mode Validation Types ────────────────────────────── + +export interface ModeComparisonData { + collection: string; + modes: Array<{ + modeId: string; + modeName: string; + screenshot?: string; + }>; + variableDiffs: Array<{ + variableName: string; + type: string; + values: Record; + }>; + missingValues: Array<{ + variableName: string; + missingModes: string[]; + }>; +} + +export interface DarkModeMetrics { + pureBlackBackgrounds: number; + pureWhiteText: number; + lowContrastOnDark: number; + missingModeValues: number; +} + +export interface DarkModeResult { + issues: Array<{ + id: string; + type: string; + severity: string; + nodeId: string; + nodeName: string; + message: string; + currentValue?: string; + suggestions?: string[]; + autoFixable: boolean; + }>; + metrics: DarkModeMetrics; + summary: { totalChecked: number; passed: number; failed: number }; +} + +export interface DarkModeValidationResponse { + comparison: { + overallRating: 'pass' | 'needs_improvement' | 'fail'; + visibilityIssues: Array<{ element: string; description: string }>; + semanticColorIssues: Array<{ element: string; lightValue: string; darkValue: string; issue: string }>; + elevationIssues: string[]; + imageAdaptation: 'good' | 'needs_attention' | 'missing'; + recommendations: Array<{ title: string; description: string; severity: string }>; + summary: string; + }; + deterministicIssues: DarkModeResult | null; +} + +// ── Page Sweep Progress ────────────────────────────────────── + +export interface PageSweepProgress { + phase: 'collecting' | 'comparing' | 'validating' | 'complete'; + current: number; + total: number; + message: string; +} + +// ── Page Sweep Types ────────────────────────────────── + +/** Raw data sent from plugin sandbox (before backend processing). */ +export interface PageSweepRawData { + frames: Array<{ + id: string; + name: string; + screenshot: string; + lintResult: { + summary: { + totalErrors: number; + byType: Record; + totalNodes: number; + nodesWithErrors: number; + }; + errors: Array<{ errorType: string; severity?: string; nodeId: string; nodeName: string; message: string; value: string }>; + }; + width: number; + height: number; + }>; + aggregated: { + totalFrames: number; + totalIssues: number; + topIssues: Array<{ type: string; count: number; severity: string }>; + }; +} + +/** Processed page sweep result (after backend AI analysis or deterministic fallback). */ +export interface PageSweepData { + fileHealth: { + overallScore: number; + grade: string; + totalFrames: number; + totalIssues: number; + topIssues: Array<{ type: string; count: number; severity: string }>; + consistencyScore: number; + }; + frames: Array<{ + id: string; + name: string; + score: number; + issueCount: number; + topIssues: string[]; + }>; + aiInsights: { + strengths: string[]; + weaknesses: string[]; + recommendations: Array<{ title: string; description: string; affectedFrames: string[] }>; + summary: string; + }; +} + // UI → Plugin message commands export function postToPlugin(type: string, data?: unknown): void { parent.postMessage({ pluginMessage: { type, data } }, '*'); From 73fc3e800bcc179abb99576383bfa7117def26af Mon Sep 17 00:00:00 2001 From: lemone112 Date: Fri, 13 Mar 2026 17:29:49 +0300 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20Sprint=207=20=E2=80=94=20UX=20polis?= =?UTF-8?q?h,=20new=20lint=20modules,=208=20result=20cards,=20team=20confi?= =?UTF-8?q?g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tier 0 (core UX): - Ambient quality badge: quick-lint on selection change shows mini-score in StickyHeader before any full analysis - All auto-fixable types exposed (fill/stroke/effect/text/autoLayout) with per-category fix buttons - Summary-first hierarchy: lint summary sorted by impact, top 3 shown first - Score delta from previous scan shown in StickyHeader (not just baseline) - Persistent ignore state already implemented (verified) Tier 1 (8 new result cards): - DesignDebtCard, DarkModeCard, A11ySpecCard, TokenComplianceCard - BrandConsistencyCard, CopyToneCard, PersonaResearchCard, AttentionHeatmapCard - All wired into MessageList.tsx with new ChatMessageType kinds Tier 2 (team config UI): - TeamConfigPanel, SeveritySelector, ScaleEditor components - Enable team-wide lint configuration (spacing/radius scales, severity overrides) Tier 3 (8 new lint modules): - layout-sizing, constraints, typography, style-audit (3a) - component-props, variable-scope, multi-theme, grid-check (3b) - Standalone modules with own LintIssue types for extended analysis Tier 4 (backend AI improvements): - Grounding instructions for all prompts - Three-layer explanations (rule/why/real-world) - Confidence scoring and filtering for AI findings - Improved page-type detection with signals - Enhanced attention, nielsen-heuristics, review prompts Co-Authored-By: Claude Opus 4.6 --- backend/src/prompts/attention.ts | 82 ++-- backend/src/prompts/chat-followup.ts | 68 ++- backend/src/prompts/nielsen-heuristics.ts | 151 +++--- backend/src/prompts/page-type.ts | 86 +++- backend/src/prompts/review.ts | 5 +- .../prompts/shared/grounding-instructions.ts | 14 + backend/src/prompts/three-layer.ts | 75 +++ backend/src/routes/analyze.ts | 4 +- backend/src/routes/chat.ts | 2 +- backend/src/services/analyzer.ts | 135 +++++- backend/src/services/claude.ts | 34 +- backend/src/services/confidence-filter.ts | 60 +++ dist/code.js | 20 +- dist/ui.html | 49 +- src/code.ts | 7 +- src/lint/component-props.ts | 312 ++++++++++++ src/lint/constraints.ts | 331 +++++++++++++ src/lint/grid-check.ts | 386 +++++++++++++++ src/lint/layout-sizing.ts | 450 +++++++++++++++++ src/lint/multi-theme.ts | 452 +++++++++++++++++ src/lint/style-audit.ts | 455 +++++++++++++++++ src/lint/typography.ts | 458 ++++++++++++++++++ src/lint/variable-scope.ts | 395 +++++++++++++++ src/ui/message-handler.ts | 41 ++ ui/src/App.tsx | 10 +- ui/src/components/chat/ChatContainer.tsx | 7 +- ui/src/components/chat/MessageList.tsx | 24 + ui/src/components/chat/StickyHeader.tsx | 51 +- ui/src/components/messages/A11ySpecCard.tsx | 280 +++++++++++ .../messages/AttentionHeatmapCard.tsx | 202 ++++++++ .../messages/BrandConsistencyCard.tsx | 187 +++++++ ui/src/components/messages/CopyToneCard.tsx | 183 +++++++ ui/src/components/messages/DarkModeCard.tsx | 146 ++++++ ui/src/components/messages/DesignDebtCard.tsx | 132 +++++ .../messages/PersonaResearchCard.tsx | 225 +++++++++ .../messages/TokenComplianceCard.tsx | 184 +++++++ ui/src/components/shared/ScaleEditor.tsx | 170 +++++++ ui/src/components/shared/SeveritySelector.tsx | 68 +++ ui/src/components/shared/TeamConfigPanel.tsx | 413 ++++++++++++++++ ui/src/hooks/useChat.ts | 95 ++-- ui/src/lib/messages.ts | 22 +- 41 files changed, 6256 insertions(+), 215 deletions(-) create mode 100644 backend/src/prompts/shared/grounding-instructions.ts create mode 100644 backend/src/prompts/three-layer.ts create mode 100644 backend/src/services/confidence-filter.ts create mode 100644 src/lint/component-props.ts create mode 100644 src/lint/constraints.ts create mode 100644 src/lint/grid-check.ts create mode 100644 src/lint/layout-sizing.ts create mode 100644 src/lint/multi-theme.ts create mode 100644 src/lint/style-audit.ts create mode 100644 src/lint/typography.ts create mode 100644 src/lint/variable-scope.ts create mode 100644 ui/src/components/messages/A11ySpecCard.tsx create mode 100644 ui/src/components/messages/AttentionHeatmapCard.tsx create mode 100644 ui/src/components/messages/BrandConsistencyCard.tsx create mode 100644 ui/src/components/messages/CopyToneCard.tsx create mode 100644 ui/src/components/messages/DarkModeCard.tsx create mode 100644 ui/src/components/messages/DesignDebtCard.tsx create mode 100644 ui/src/components/messages/PersonaResearchCard.tsx create mode 100644 ui/src/components/messages/TokenComplianceCard.tsx create mode 100644 ui/src/components/shared/ScaleEditor.tsx create mode 100644 ui/src/components/shared/SeveritySelector.tsx create mode 100644 ui/src/components/shared/TeamConfigPanel.tsx diff --git a/backend/src/prompts/attention.ts b/backend/src/prompts/attention.ts index 2159d2f..653332b 100644 --- a/backend/src/prompts/attention.ts +++ b/backend/src/prompts/attention.ts @@ -1,51 +1,63 @@ +import { GROUNDING_INSTRUCTIONS } from './shared/grounding-instructions.js'; + /** - * Visual Attention Prediction prompt. - * Asks Claude Vision to predict where users will look and how attention flows. + * Attention/visual-hierarchy analysis prompt. + * Used by the extended analyzer to evaluate where the eye is drawn. */ -export function buildAttentionPrompt(lintContext: string): string { - return `Analyze this UI screenshot for visual attention patterns. Use established eye-tracking research (Nielsen Norman Group, Gutenberg diagram) to predict user gaze behavior. - -Context from automated lint: -${lintContext} +export function buildAttentionPrompt(componentInfo: string): string { + return `Analyze the visual attention flow in this UI screenshot. -Evaluate all of the following: +## Context +${componentInfo} -## 1. Focal Point Identification -What element draws the eye first? Consider size, contrast, color saturation, isolation (whitespace), and position. Is this element the intended primary CTA or key content? +## Evaluation Criteria -## 2. Reading Flow Pattern -Does the layout guide the eye in an F-pattern (typical for content-heavy pages with left-aligned text), Z-pattern (typical for landing pages with hero + CTA), linear (single-column scroll), or scattered (no clear flow)? +### 1. Focal Point Clarity +- Is there one clear primary focal point? +- Does the visual hierarchy guide the eye in a logical sequence (primary -> secondary -> tertiary)? +- Are F-pattern or Z-pattern reading flows supported? -## 3. Attention Competition -Identify elements that compete for attention simultaneously. Look for: multiple high-contrast elements at similar visual weight, competing CTAs, clashing colors, or animation-suggesting elements (spinners, progress bars) that would pull focus. +### 2. Visual Weight Distribution +- Is visual weight (size, color, contrast) distributed intentionally? +- Do important elements have the most visual weight? +- Are decorative elements competing with functional ones? -## 4. Attention Dead Zones -Identify areas users are likely to skip. Common dead zones: right sidebar content (banner blindness), below-the-fold content with no scroll affordance, low-contrast text blocks, dense text without headings. +### 3. CTA Prominence +- Is the primary call-to-action the most visually prominent interactive element? +- Is there clear differentiation between primary, secondary, and tertiary actions? +- Can a user identify the next step within 3 seconds? -## 5. Visual Weight Distribution -Assess whether the overall visual weight is balanced or skewed. Consider element density, color weight, and whitespace distribution across the four quadrants. +### 4. Information Density +- Is the content density appropriate for the page type? +- Are there clear content groups separated by whitespace? +- Is progressive disclosure used where appropriate? -Respond in this exact JSON format: +## Response Format (JSON) { "focalPoint": { - "element": "", - "strength": "strong|moderate|weak", - "isIntendedCTA": true|false + "exists": true|false, + "element": "", + "strength": "strong|moderate|weak" }, "readingFlow": { - "pattern": "F|Z|linear|scattered", - "confidence": "high|medium|low", - "description": "<1-2 sentence explanation of how the eye moves through the design>" + "pattern": "F-pattern|Z-pattern|scattered|linear", + "blockers": [""] }, - "competingElements": [ - { "element": "", "reason": "" } - ], - "deadZones": [ - { "area": "", "suggestion": "" } + "ctaProminence": { + "rating": "pass|needs_improvement|fail", + "primaryCta": "", + "competingElements": [""] + }, + "findings": [ + { + "finding": "", + "confidence": 0.0, + "evidence": "", + "category": "attention", + "severity": "critical|warning|info" + } ], - "visualWeightBalance": "balanced|left-heavy|right-heavy|top-heavy|bottom-heavy", - "recommendations": [ - { "title": "", "description": "", "severity": "critical|warning|info" } - ] -}`; + "summary": "<2-3 sentence summary>" +} +${GROUNDING_INSTRUCTIONS}`; } diff --git a/backend/src/prompts/chat-followup.ts b/backend/src/prompts/chat-followup.ts index 0cd053b..234f954 100644 --- a/backend/src/prompts/chat-followup.ts +++ b/backend/src/prompts/chat-followup.ts @@ -1,10 +1,70 @@ +/** + * Build a structured follow-up prompt for the conversational chat phase. + * + * Injects the current analysis context (score, top issues, recent fixes) + * and tells the model exactly what capabilities the user can ask about. + */ export function buildFollowupPrompt( sessionContext: string, + analysisJson?: string, ): string { - return `You are a design review assistant helping a designer improve their component. + // Extract top issues from the analysis JSON if available + let topIssuesSummary = ''; + if (analysisJson) { + try { + const analysis = JSON.parse(analysisJson); + const failCategories: string[] = []; + const needsImprovementCategories: string[] = []; -Session context: -${sessionContext} + for (const [key, value] of Object.entries(analysis)) { + const cat = value as Record | undefined; + if (cat && typeof cat === 'object' && 'rating' in cat) { + if (cat.rating === 'fail') failCategories.push(key); + else if (cat.rating === 'needs_improvement') needsImprovementCategories.push(key); + } + } -Respond naturally and helpfully. Be specific about what to fix and why. Keep responses concise.`; + if (failCategories.length > 0) { + topIssuesSummary += `\nFailing categories: ${failCategories.join(', ')}`; + } + if (needsImprovementCategories.length > 0) { + topIssuesSummary += `\nNeeds improvement: ${needsImprovementCategories.join(', ')}`; + } + } catch { + // Analysis JSON not parseable — proceed without it + } + } + + return `You are a senior design review assistant embedded in FigmaLint, a Figma plugin that analyzes design quality. You are in a follow-up conversation after an initial analysis has been completed. + +## Current Analysis Context +${sessionContext}${topIssuesSummary} + +## Your Capabilities +When the user asks questions, you can help with: + +1. **Explain findings** — Explain why a specific issue matters and cite design principles (Gestalt, WCAG, Nielsen heuristics) +2. **Prioritize fixes** — Help the user decide which issues to fix first based on user impact +3. **Suggest alternatives** — Propose concrete design changes (spacing values, color adjustments, layout modifications) +4. **Compare with best practices** — Reference how top products (Stripe, Linear, Vercel, Figma) solve similar problems +5. **Deep-dive a category** — Provide more detail on any review category (visual hierarchy, color harmony, etc.) +6. **Re-evaluate after changes** — When the user says they've made changes, acknowledge and suggest re-running analysis + +## Response Guidelines +- Be specific: reference exact elements from the analysis ("the submit button", "the header spacing") +- Be concise: 2-4 sentences for simple questions, structured lists for complex ones +- Be actionable: every suggestion should be implementable in Figma +- Reference the current score and issues when relevant +- If the user asks about something outside the analysis scope, be honest about limitations + +## Structured Response +When the user requests a specific action (like "list all critical issues" or "prioritize fixes"), respond in JSON: +{ + "action": "", + "items": [ + { "title": "", "description": "", "priority": "high|medium|low" } + ] +} + +For conversational questions, respond in plain text. Do not use JSON for casual conversation.`; } diff --git a/backend/src/prompts/nielsen-heuristics.ts b/backend/src/prompts/nielsen-heuristics.ts index 0bd940f..0ab19a7 100644 --- a/backend/src/prompts/nielsen-heuristics.ts +++ b/backend/src/prompts/nielsen-heuristics.ts @@ -1,112 +1,81 @@ +import { GROUNDING_INSTRUCTIONS } from './shared/grounding-instructions.js'; + /** - * Nielsen's Heuristics Evaluation prompt. - * Evaluates the 6 heuristics that can be assessed from static screenshots - * (skips H2 Real World Match, H7 Flexibility, H9 Error Recovery, H10 Help). + * Nielsen's 10 Usability Heuristics evaluation prompt. + * Used by the extended analyzer for heuristic-based UX review. */ -export function buildNielsenHeuristicsPrompt(lintContext: string, flowContext?: string): string { - const flowBlock = flowContext - ? `\nFlow context (multiple screens in this flow):\n${flowContext}\n` - : ''; +export function buildNielsenHeuristicsPrompt(componentInfo: string): string { + return `Evaluate this UI screenshot against Nielsen's 10 Usability Heuristics. + +## Context +${componentInfo} + +## Heuristics to Evaluate + +### H1: Visibility of System Status +Does the design keep users informed about what is going on through appropriate feedback within reasonable time? +Look for: loading indicators, progress bars, active states, confirmation messages, real-time feedback. + +### H2: Match Between System and Real World +Does the design speak the users' language with familiar words, phrases, and concepts? +Look for: jargon-free labels, real-world metaphors, natural information ordering, culturally appropriate icons. - return `Evaluate this UI screenshot against Nielsen's 10 Usability Heuristics. Focus on the 6 heuristics that can be assessed visually. Skip H2 (Real World Match), H7 (Flexibility & Efficiency), H9 (Error Recovery), and H10 (Help & Documentation) — these require domain knowledge or interaction testing. +### H3: User Control and Freedom +Can users easily undo, redo, or exit unwanted states? +Look for: back buttons, cancel options, undo capability, clear exit paths, confirmation dialogs for destructive actions. -Context from automated lint: -${lintContext} -${flowBlock} -Evaluate each heuristic below. For each one, provide a rating, 2-3 specific evidence items from the screenshot, and a recommendation if the rating is not "pass". +### H4: Consistency and Standards +Does the design follow platform conventions and internal consistency? +Look for: consistent button styles, uniform terminology, standard icon usage, predictable element placement. -## H1: Visibility of System Status -The system should always keep users informed about what is going on through appropriate feedback within reasonable time. -Look for: loading indicators, progress bars, active/selected states on navigation, current step indicators in multi-step flows, feedback after actions (success/error badges), real-time status updates. -PASS: Clear system status indicators present where needed. -NEEDS_IMPROVEMENT: Some status indicators present but gaps exist (e.g., no loading state, unclear active tab). -FAIL: No visible system status — user cannot tell what state the system is in. +### H5: Error Prevention +Does the design prevent errors before they happen? +Look for: input constraints, confirmation steps for critical actions, smart defaults, disabled states for invalid actions, inline validation. -## H3: User Control & Freedom -Users often perform actions by mistake. They need a clearly marked "emergency exit" to leave the unwanted action. -Look for: back/close buttons on modals and overlays, undo affordances, cancel buttons alongside confirm, breadcrumbs for navigation history, clear exit paths from flows. -PASS: All modals/overlays have close buttons; destructive actions have cancel options; navigation provides back paths. -NEEDS_IMPROVEMENT: Most controls present but 1-2 exit paths missing (e.g., modal without close button, no cancel on form). -FAIL: Users appear trapped — no visible way to go back, close, or undo. +### H6: Recognition Rather Than Recall +Does the design minimize memory load by making elements, actions, and options visible? +Look for: visible labels (not tooltip-only), breadcrumbs, recently used items, contextual help, visible navigation state. -## H4: Consistency & Standards -Users should not have to wonder whether different words, situations, or actions mean the same thing. -Look for: consistent button styles for same-level actions, consistent iconography, platform conventions followed (iOS/Android/Web), consistent terminology, consistent spacing and alignment patterns. -PASS: Visual language is consistent throughout; platform conventions followed. -NEEDS_IMPROVEMENT: Generally consistent but 1-2 deviations (mixed icon styles, inconsistent button hierarchy). -FAIL: Significant inconsistencies — mixed visual languages, contradictory conventions. +### H7: Flexibility and Efficiency of Use +Does the design cater to both novice and expert users? +Look for: keyboard shortcuts, customizable interfaces, accelerators, batch operations, power-user features alongside simple flows. -## H5: Error Prevention -Even better than good error messages is a careful design which prevents a problem from occurring in the first place. -Look for: confirmation dialogs for destructive actions (delete, discard), input constraints (character counters, format hints), safe defaults (opt-out rather than opt-in for risky actions), disabled states for unavailable actions, inline validation hints. -PASS: Destructive actions have safeguards; inputs show constraints; defaults are safe. -NEEDS_IMPROVEMENT: Some error prevention present but gaps (e.g., delete without confirmation, no input hints). -FAIL: No error prevention visible — destructive actions lack confirmation, no input guidance. +### H8: Aesthetic and Minimalist Design +Does the interface avoid irrelevant or rarely needed information? +Look for: clean layout, purposeful whitespace, no visual noise, focused content, appropriate information density. -## H6: Recognition Rather Than Recall -Minimize the user's memory load by making objects, actions, and options visible or easily retrievable. -Look for: visible labels (not icon-only buttons without tooltips), breadcrumbs showing path, recently used items, visible options rather than hidden menus, search with suggestions, placeholder text that explains expected input. -PASS: All actions labeled; navigation context visible; options discoverable. -NEEDS_IMPROVEMENT: Most elements labeled but some icon-only buttons without clear meaning; some navigation context missing. -FAIL: Heavy reliance on recall — unlabeled icons, hidden options, no navigation context. +### H9: Help Users Recognize, Diagnose, and Recover from Errors +Are error messages expressed in plain language and suggesting a solution? +Look for: clear error messages (not codes), indicated problem location, specific recovery instructions, non-blaming tone. -## H8: Aesthetic & Minimalist Design -Every extra unit of information in an interface competes with relevant units of information and diminishes their relative visibility. -Look for: information density appropriate for the context, noise-to-signal ratio, visual clutter (unnecessary borders, shadows, decorations), content hierarchy that surfaces what matters, purposeful use of whitespace. -PASS: Clean design with only relevant information; clear content hierarchy; purposeful whitespace. -NEEDS_IMPROVEMENT: Mostly clean but some unnecessary elements or slightly cluttered areas. -FAIL: Cluttered — excessive decorations, too much information competing for attention, poor signal-to-noise ratio. +### H10: Help and Documentation +Is help available when needed and easy to find? +Look for: contextual tooltips, help links, onboarding guides, documentation access, searchable help. -Respond in this exact JSON format: +## Response Format (JSON) { "heuristics": [ { "id": "H1", "name": "Visibility of System Status", "rating": "pass|needs_improvement|fail", - "evidence": ["", ""], - "recommendation": "" - }, - { - "id": "H3", - "name": "User Control & Freedom", - "rating": "pass|needs_improvement|fail", - "evidence": ["", ""], - "recommendation": "" - }, - { - "id": "H4", - "name": "Consistency & Standards", - "rating": "pass|needs_improvement|fail", - "evidence": ["", ""], - "recommendation": "" - }, - { - "id": "H5", - "name": "Error Prevention", - "rating": "pass|needs_improvement|fail", - "evidence": ["", ""], - "recommendation": "" - }, - { - "id": "H6", - "name": "Recognition Rather Than Recall", - "rating": "pass|needs_improvement|fail", - "evidence": ["", ""], - "recommendation": "" - }, - { - "id": "H8", - "name": "Aesthetic & Minimalist Design", - "rating": "pass|needs_improvement|fail", - "evidence": ["", ""], - "recommendation": "" + "evidence": [""], + "recommendation": "" } ], - "overallCompliance": 0-100, - "criticalViolations": [ - { "heuristic": "", "description": "" } + "findings": [ + { + "finding": "", + "confidence": 0.0, + "evidence": "", + "category": "nielsen", + "severity": "critical|warning|info" + } ], - "summary": "<2-3 sentence summary of heuristic compliance>" -}`; + "topViolations": [""], + "summary": "<2-3 sentence summary>" +} + +IMPORTANT: Only evaluate heuristics that are observable from the screenshot. If a heuristic cannot be assessed (e.g., H7 keyboard shortcuts from a static image), mark it as "pass" and note "Not assessable from screenshot" in evidence. +${GROUNDING_INSTRUCTIONS}`; } diff --git a/backend/src/prompts/page-type.ts b/backend/src/prompts/page-type.ts index cc79e60..7cc5b89 100644 --- a/backend/src/prompts/page-type.ts +++ b/backend/src/prompts/page-type.ts @@ -1,5 +1,85 @@ -export const PAGE_TYPE_PROMPT = `Determine the type of this UI screen from the following list. Respond with only one word from the list. +/** + * Page type detection prompt with per-type detection signals. + * Returns structured JSON with type, confidence, and matched signals. + */ +export const PAGE_TYPE_PROMPT = `Analyze this UI screenshot and determine the page type. Use the detection signals below to match. -Types: pricing, landing, dashboard, onboarding, auth, settings, profile, checkout, search, listing, detail, error, empty_state, modal, sidebar, navigation, form, table, card, other +## Page Type Detection Rubric -Respond with exactly one word.`; +### pricing +Signals: Tiered pricing cards/columns with plan names and dollar amounts; toggle for monthly/annual billing; feature comparison matrix or checklist per tier; "Most popular" or "Recommended" badge on one plan. + +### landing +Signals: Large hero section with headline, subheadline, and primary CTA; multiple content sections separated by whitespace; social proof (logos, testimonials, stats); no persistent sidebar navigation. + +### dashboard +Signals: Grid of data widgets, charts, or KPI cards; sidebar or top navigation with multiple sections; summary statistics (numbers, percentages, sparklines); date range picker or filter controls. + +### onboarding +Signals: Step indicator or progress bar (e.g., "Step 2 of 4"); single-task focus with limited navigation; welcome/setup language ("Get started", "Set up your"); illustration or avatar prompt. + +### auth +Signals: Email/username and password input fields; "Sign in" / "Sign up" / "Log in" submit button; "Forgot password?" or "Reset password" link; OAuth/social login buttons (Google, GitHub, etc.). + +### settings +Signals: Grouped form controls with section headers (Account, Notifications, Privacy); toggle switches or checkboxes for preferences; "Save" / "Update" button at bottom; sidebar or tab navigation for setting categories. + +### profile +Signals: User avatar or photo prominently displayed; user name, bio, or description text; activity feed, stats, or contribution grid; "Edit profile" button or inline editable fields. + +### checkout +Signals: Order summary with line items, subtotal, tax, and total; payment method input (card number, expiry, CVV); shipping/billing address form; "Place order" / "Pay now" primary CTA. + +### search +Signals: Prominent search input field (often centered or top-positioned); search results list with titles, snippets, or thumbnails; filter/facet sidebar or chips; result count and sort controls. + +### listing +Signals: Repeating card or row layout showing multiple items of the same type; pagination or infinite scroll indicator; filter bar or sort dropdown above the list; thumbnail + title + metadata per item. + +### detail +Signals: Single entity as focal point (product, article, user, event); large image or media area; descriptive metadata (price, date, author, specs); related items or "You might also like" section. + +### error +Signals: Error code displayed prominently (404, 500, 403); illustration or icon indicating something went wrong; message explaining the error ("Page not found"); "Go back" or "Return home" link/button. + +### empty_state +Signals: Illustration or icon centered on an otherwise blank content area; message like "No items yet" / "Nothing here" / "Get started"; single CTA to create first item or take primary action; appears within a page shell (nav still visible). + +### modal +Signals: Overlay/backdrop dimming the background; centered or side-panel container with close button (X); focused content with 1-2 actions (confirm/cancel); background content visible but not interactive. + +### sidebar +Signals: Narrow vertical panel on left or right edge; navigation links or menu items stacked vertically; icons + labels or icon-only collapsed state; active/selected state indicator on current item. + +### navigation +Signals: Top bar or bottom tab bar with multiple route items; hamburger menu or expandable drawer; breadcrumbs showing hierarchy; active state highlighting current route. + +### form +Signals: Multiple labeled input fields arranged vertically; validation indicators (red borders, error text, checkmarks); required field markers (asterisks); submit/cancel button pair at the bottom. + +### table +Signals: Column headers with data rows beneath; sortable column indicators (arrows); row selection checkboxes or radio buttons; pagination controls below the table. + +### card +Signals: Single card component in isolation or a small card group; bordered/elevated container with image, title, and body; action buttons or links within the card footer; distinct from a full listing page. + +### other +Signals: Does not match any of the above patterns; custom or hybrid layout; specialized domain-specific UI (e.g., code editor, map view, canvas tool). + +## Instructions +1. Examine the screenshot for visual elements matching the signals above. +2. Select the BEST matching page type. If multiple types partially match, pick the one with the strongest signal match. +3. Return your confidence level based on how many signals matched. + +Respond in this exact JSON format: +{ + "type": "", + "confidence": , + "signals": ["", "", ""] +} + +Confidence guide: +- 0.9-1.0: 3+ strong signals matched, no ambiguity +- 0.7-0.89: 2 signals matched clearly +- 0.5-0.69: 1 signal matched, some ambiguity with other types +- Below 0.5: Very uncertain — use "other" if nothing fits well`; diff --git a/backend/src/prompts/review.ts b/backend/src/prompts/review.ts index 63ec6f9..fea3255 100644 --- a/backend/src/prompts/review.ts +++ b/backend/src/prompts/review.ts @@ -1,3 +1,5 @@ +import { GROUNDING_INSTRUCTIONS } from './shared/grounding-instructions.js'; + export function buildReviewPrompt( lintSummary: string, componentInfo: string, @@ -96,5 +98,6 @@ Respond in this exact JSON format: { "title": "", "description": "", "severity": "critical|warning|info" } ], "summary": "<2-3 sentence summary>" -}`; +} +${GROUNDING_INSTRUCTIONS}`; } diff --git a/backend/src/prompts/shared/grounding-instructions.ts b/backend/src/prompts/shared/grounding-instructions.ts new file mode 100644 index 0000000..cbb28f6 --- /dev/null +++ b/backend/src/prompts/shared/grounding-instructions.ts @@ -0,0 +1,14 @@ +/** + * Shared grounding instructions appended to all AI analysis prompts. + * Ensures every finding is anchored to specific visual evidence, + * reducing hallucinated or vague observations. + */ +export const GROUNDING_INSTRUCTIONS = ` +CRITICAL: For every finding you report, you MUST provide: +1. The specific element or region you're referring to (e.g., "the blue CTA button in the top-right") +2. Why it's an issue (reference a specific principle or guideline) +3. What the fix should be (actionable, not vague) + +If you cannot point to a specific element, do NOT report the finding. +If you find no issues in a category, explicitly state "No issues found" — do not invent problems. +`; diff --git a/backend/src/prompts/three-layer.ts b/backend/src/prompts/three-layer.ts new file mode 100644 index 0000000..ab7058c --- /dev/null +++ b/backend/src/prompts/three-layer.ts @@ -0,0 +1,75 @@ +/** + * Three-Layer Explanation Builder. + * + * For each lint finding, Claude produces three layers: + * 1. Rule — the concrete violation ("Spacing 12px is not on the 8px grid") + * 2. Why — the design principle behind the rule (Gestalt, WCAG, cognitive load, etc.) + * 3. Real-world — proof from shipped products (Stripe, Linear, Vercel, etc.) + * + * This gives designers not just WHAT to fix, but WHY it matters and + * WHO already follows the practice. + */ + +export interface ThreeLayerExplanation { + /** The concrete lint violation. */ + rule: string; + /** Why this rule exists — cite a design principle or guideline. */ + why: string; + /** Real-world examples of products that follow this practice. */ + realWorld: string; +} + +export interface LintError { + errorType: string; + message: string; + value: string; +} + +/** + * Build a prompt that asks Claude to enrich each lint error with three layers + * of explanation. Accepts the top-N lint errors to avoid prompt bloat. + */ +export function buildThreeLayerPrompt(lintErrors: LintError[]): string { + if (lintErrors.length === 0) { + return ''; + } + + const errorList = lintErrors + .map( + (e, i) => + `${i + 1}. [${e.errorType}] ${e.message} (current value: ${e.value})`, + ) + .join('\n'); + + return `You are a senior design systems engineer. For each lint finding below, produce a three-layer explanation. + +## Lint Findings +${errorList} + +## Instructions +For EACH finding above, provide exactly three layers: + +1. **Rule** — Restate the violation concisely in plain language (e.g., "Spacing of 12px does not align to the 8px spatial grid"). +2. **Why** — Explain the underlying design principle. Reference a specific guideline or law: + - Gestalt principles (proximity, similarity, closure, continuity) + - WCAG 2.1 guidelines (contrast ratio, touch targets, text sizing) + - Cognitive load theory (Miller's law, Hick's law) + - Platform conventions (Material Design 3, Apple HIG) + - Design token architecture principles +3. **Real-world** — Name 2-3 well-known products that follow this practice and briefly explain how (e.g., "Stripe uses a strict 4px grid. Linear enforces 8px spacing tokens. Vercel's design system prohibits hard-coded spacing values."). + +If a finding is trivial or informational (not a real issue), you may combine layers 2 and 3 into a short note. + +## Response Format (JSON) +{ + "explanations": [ + { + "rule": "", + "why": "", + "realWorld": "<2-3 product examples following this practice>" + } + ] +} + +Return one explanation object per lint finding, in the same order as the input list.`; +} diff --git a/backend/src/routes/analyze.ts b/backend/src/routes/analyze.ts index c8a8160..9ebf36c 100644 --- a/backend/src/routes/analyze.ts +++ b/backend/src/routes/analyze.ts @@ -6,8 +6,8 @@ import { type ExtendedFeatures, } from '../services/extended-analyzer.js'; -interface AnalyzeRequestBody extends AnalyzeRequest { - features?: ExtendedFeatures; +interface AnalyzeRequestBody extends Omit { + features?: AnalyzeRequest['features'] & ExtendedFeatures; } const app = new Hono(); diff --git a/backend/src/routes/chat.ts b/backend/src/routes/chat.ts index 241b98f..bfcb0de 100644 --- a/backend/src/routes/chat.ts +++ b/backend/src/routes/chat.ts @@ -34,7 +34,7 @@ app.post('/chat', async (c) => { const sessionContext = getSessionContext(body.sessionId); // System prompt carries only instructions + session context; // conversation history is passed separately via the messages array - const systemPrompt = buildFollowupPrompt(sessionContext); + const systemPrompt = buildFollowupPrompt(sessionContext, session.ai_review ?? undefined); // Build messages for API — only include recent history to avoid context bloat const messages: Array<{ role: 'user' | 'assistant'; content: string }> = history.map(m => ({ diff --git a/backend/src/services/analyzer.ts b/backend/src/services/analyzer.ts index a67aa2e..e644d38 100644 --- a/backend/src/services/analyzer.ts +++ b/backend/src/services/analyzer.ts @@ -1,8 +1,16 @@ import { fetchDesignSystemContext, type SourceReference } from './design-knowledge.js'; import { buildDesignKnowledgeSection } from '../prompts/design-knowledge.js'; -import { detectPageType, generateReview, getAnthropicClient } from './claude.js'; +import { detectPageType, generateReview, getAnthropicClient, MODEL, type PageTypeResult } from './claude.js'; import { runReferoComparison, type ReferoComparison } from './refero.js'; import { startSession, loadSession, saveAnalysisResult, saveReferoResult } from './session.js'; +import { buildThreeLayerPrompt, type ThreeLayerExplanation } from '../prompts/three-layer.js'; +import { + filterByConfidence, + sortByConfidence, + withConfidenceScoring, + type ConfidencedFinding, +} from './confidence-filter.js'; +import { SYSTEM_PROMPT } from '../prompts/system.js'; export interface AnalyzeRequest { screenshot: string; @@ -43,6 +51,13 @@ export interface AnalyzeRequest { }; sessionId?: string; mode: 'quick' | 'deep'; + /** Optional extended features to enable on this analysis run. */ + features?: { + /** Generate three-layer explanations (rule / why / real-world) for the top-N lint issues. Default: 0 (disabled). */ + threeLayerExplanations?: boolean; + /** Run confidence scoring on all AI findings and filter low-confidence results. Default: false. */ + confidenceScoring?: boolean; + }; } export interface AiReviewCategory { @@ -66,12 +81,20 @@ export interface AiReviewResult { export interface AnalysisResult { sessionId: string; pageType: string; + /** Confidence of the page-type classification (0-1). */ + pageTypeConfidence?: number; + /** Signals that matched during page-type detection. */ + pageTypeSignals?: string[]; lintResult: AnalyzeRequest['lintResult']; aiReview: AiReviewResult; referoComparison?: ReferoComparison; designHealthScore: number; /** Authoritative sources used to ground the AI review (Thesis #50) */ designSystemSources?: SourceReference[]; + /** Three-layer explanations for the top lint issues (when feature enabled). */ + threeLayerExplanations?: ThreeLayerExplanation[]; + /** Confidence-scored and filtered AI findings (when feature enabled). */ + confidencedFindings?: ConfidencedFinding[]; } /** @@ -115,7 +138,10 @@ export async function runAnalysis(req: AnalyzeRequest): Promise fetchDesignSystemContext(req.extractedData.componentName, componentFamily), ]); - const pageType = pageTypeResult.status === 'fulfilled' ? pageTypeResult.value : 'unknown'; + const pageTypeData: PageTypeResult = pageTypeResult.status === 'fulfilled' + ? pageTypeResult.value + : { type: 'unknown', confidence: 0, signals: [] }; + const pageType = pageTypeData.type; const designKnowledge = designKnowledgeResult.status === 'fulfilled' ? designKnowledgeResult.value : null; @@ -206,17 +232,122 @@ export async function runAnalysis(req: AnalyzeRequest): Promise severityScore(namingErrors, total) * 0.04 ); + // Phase 3: Optional extended features (non-blocking, run in parallel) + const features = req.features ?? {}; + let threeLayerExplanations: ThreeLayerExplanation[] | undefined; + let confidencedFindings: ConfidencedFinding[] | undefined; + + const extendedPromises: Array> = []; + + // Three-layer explanations for top-5 lint issues + if (features.threeLayerExplanations && req.lintResult.errors.length > 0) { + const topErrors = req.lintResult.errors.slice(0, 5); + const threeLayerPrompt = buildThreeLayerPrompt(topErrors); + + if (threeLayerPrompt) { + extendedPromises.push( + (async () => { + try { + const client = getAnthropicClient(); + const response = await client.messages.create({ + model: MODEL, + max_tokens: 2000, + system: SYSTEM_PROMPT, + messages: [{ role: 'user', content: threeLayerPrompt }], + }); + if (response.content.length && response.content[0].type === 'text') { + const jsonMatch = response.content[0].text.match(/\{[\s\S]*\}/); + if (jsonMatch) { + const parsed = JSON.parse(jsonMatch[0]); + if (Array.isArray(parsed.explanations)) { + threeLayerExplanations = parsed.explanations; + } + } + } + } catch (err) { + console.error('Three-layer explanation failed:', err); + } + })(), + ); + } + } + + // Confidence scoring: extract findings from AI review and score them + if (features.confidenceScoring && aiReview.recommendations.length > 0) { + extendedPromises.push( + (async () => { + try { + const findingsPrompt = withConfidenceScoring( + `Score the following design findings with confidence levels.\n\n` + + aiReview.recommendations + .map( + (r, i) => + `${i + 1}. [${r.severity}] ${r.title}: ${r.description}`, + ) + .join('\n') + + `\n\nFor each finding, respond in JSON:\n{ "findings": [{ "finding": "", "confidence": <0-1>, "evidence": "<specific visual evidence>", "category": "<category>", "severity": "<severity>" }] }`, + ); + + const client = getAnthropicClient(); + const response = await client.messages.create({ + model: MODEL, + max_tokens: 1500, + system: SYSTEM_PROMPT, + messages: [ + { + role: 'user', + content: [ + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/png', + data: req.screenshot, + }, + }, + { type: 'text', text: findingsPrompt }, + ], + }, + ], + }); + + if (response.content.length && response.content[0].type === 'text') { + const jsonMatch = response.content[0].text.match(/\{[\s\S]*\}/); + if (jsonMatch) { + const parsed = JSON.parse(jsonMatch[0]); + if (Array.isArray(parsed.findings)) { + const scored = parsed.findings as ConfidencedFinding[]; + confidencedFindings = sortByConfidence(filterByConfidence(scored)); + } + } + } + } catch (err) { + console.error('Confidence scoring failed:', err); + } + })(), + ); + } + + // Await all extended features in parallel + if (extendedPromises.length > 0) { + await Promise.allSettled(extendedPromises); + } + // Save to session saveAnalysisResult(sessionId, pageType, aiReview, req.lintResult, designHealthScore, referoComparison); return { sessionId, pageType, + ...(pageTypeData.confidence > 0 && { pageTypeConfidence: pageTypeData.confidence }), + ...(pageTypeData.signals.length > 0 && { pageTypeSignals: pageTypeData.signals }), lintResult: req.lintResult, aiReview, ...(referoComparison && { referoComparison }), designHealthScore, ...(designKnowledge && { designSystemSources: designKnowledge.sources }), + ...(threeLayerExplanations && { threeLayerExplanations }), + ...(confidencedFindings && { confidencedFindings }), }; } diff --git a/backend/src/services/claude.ts b/backend/src/services/claude.ts index 06ba894..e0dc9b9 100644 --- a/backend/src/services/claude.ts +++ b/backend/src/services/claude.ts @@ -18,15 +18,22 @@ export const MODEL = 'claude-sonnet-4-20250514'; // Internal alias for backward compat within this file const getClient = getAnthropicClient; +export interface PageTypeResult { + type: string; + confidence: number; + signals: string[]; +} + /** * Detect the page type from a screenshot. + * Returns structured result with type, confidence, and matched signals. */ -export async function detectPageType(screenshotBase64: string): Promise<string> { +export async function detectPageType(screenshotBase64: string): Promise<PageTypeResult> { const anthropic = getClient(); const response = await anthropic.messages.create({ model: MODEL, - max_tokens: 50, + max_tokens: 300, messages: [ { role: 'user', @@ -42,9 +49,28 @@ export async function detectPageType(screenshotBase64: string): Promise<string> }); if (!response.content.length || response.content[0].type !== 'text') { - return 'other'; + return { type: 'other', confidence: 0, signals: [] }; } - return response.content[0].text.trim().toLowerCase(); + + const text = response.content[0].text.trim(); + + // Try to parse as JSON first (new structured format) + try { + const jsonMatch = text.match(/\{[\s\S]*\}/); + if (jsonMatch) { + const parsed = JSON.parse(jsonMatch[0]); + return { + type: typeof parsed.type === 'string' ? parsed.type.toLowerCase() : 'other', + confidence: typeof parsed.confidence === 'number' ? parsed.confidence : 0.5, + signals: Array.isArray(parsed.signals) ? parsed.signals : [], + }; + } + } catch { + // Fall through to legacy single-word parsing + } + + // Legacy fallback: single word response + return { type: text.toLowerCase().split(/\s/)[0], confidence: 0.5, signals: [] }; } interface AiReviewCategory { diff --git a/backend/src/services/confidence-filter.ts b/backend/src/services/confidence-filter.ts new file mode 100644 index 0000000..efb43ad --- /dev/null +++ b/backend/src/services/confidence-filter.ts @@ -0,0 +1,60 @@ +/** + * Confidence scoring and filtering for AI-generated findings. + * + * Every finding produced by Claude is tagged with a confidence level (0-1). + * This module provides: + * - The type definition shared across all analysis pipelines + * - A filter to drop low-confidence noise before presenting to the user + * - A prompt modifier that instructs Claude to emit confidence scores + */ + +export interface ConfidencedFinding { + /** Human-readable description of the finding */ + finding: string; + /** 0.0 to 1.0 — how certain the model is */ + confidence: number; + /** Specific visual evidence supporting the finding */ + evidence: string; + /** Category bucket (e.g. "visualHierarchy", "colorHarmony", "spacing") */ + category: string; + /** Impact severity: "critical" | "warning" | "info" */ + severity: string; +} + +/** + * Filter findings below a confidence threshold. + * Default threshold is 0.7, meaning only "likely" and "definite" issues are kept. + */ +export function filterByConfidence( + findings: ConfidencedFinding[], + threshold = 0.7, +): ConfidencedFinding[] { + return findings.filter((f) => f.confidence >= threshold); +} + +/** + * Sort findings by confidence descending, then by severity weight descending. + */ +export function sortByConfidence(findings: ConfidencedFinding[]): ConfidencedFinding[] { + const severityWeight: Record<string, number> = { critical: 3, warning: 2, info: 1 }; + return [...findings].sort((a, b) => { + const confDiff = b.confidence - a.confidence; + if (Math.abs(confDiff) > 0.01) return confDiff; + return (severityWeight[b.severity] ?? 0) - (severityWeight[a.severity] ?? 0); + }); +} + +/** + * Append confidence-scoring instructions to any prompt string. + * This tells the model to include a numeric confidence field on each finding. + */ +export function withConfidenceScoring(prompt: string): string { + return ( + prompt + + `\n\nFor each finding, include a "confidence" field (0.0 to 1.0) indicating how certain you are. +- 0.9-1.0: Definite issue, clearly visible +- 0.7-0.89: Likely issue, some ambiguity +- 0.5-0.69: Possible issue, needs human review +- Below 0.5: Don't report it` + ); +} diff --git a/dist/code.js b/dist/code.js index f07ddca..f6960ec 100644 --- a/dist/code.js +++ b/dist/code.js @@ -1,4 +1,4 @@ -"use strict";(()=>{var No=Object.create;var Me=Object.defineProperty,wo=Object.defineProperties,Co=Object.getOwnPropertyDescriptor,Io=Object.getOwnPropertyDescriptors,xo=Object.getOwnPropertyNames,Yt=Object.getOwnPropertySymbols,Ao=Object.getPrototypeOf,Zt=Object.prototype.hasOwnProperty,Eo=Object.prototype.propertyIsEnumerable;var Qt=(e,t,n)=>t in e?Me(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,R=(e,t)=>{for(var n in t||(t={}))Zt.call(t,n)&&Qt(e,n,t[n]);if(Yt)for(var n of Yt(t))Eo.call(t,n)&&Qt(e,n,t[n]);return e},K=(e,t)=>wo(e,Io(t));var G=(e,t)=>()=>(e&&(t=e(e=0)),t);var To=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),en=(e,t)=>{for(var n in t)Me(e,n,{get:t[n],enumerable:!0})},Po=(e,t,n,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of xo(t))!Zt.call(e,o)&&o!==n&&Me(e,o,{get:()=>t[o],enumerable:!(s=Co(t,o))||s.enumerable});return e};var Lo=(e,t,n)=>(n=e!=null?No(Ao(e)):{},Po(t||!e||!e.__esModule?Me(n,"default",{value:e,enumerable:!0}):n,e));function Se(e){return["FRAME","COMPONENT","COMPONENT_SET","INSTANCE","GROUP"].includes(e.type)?(e.type==="COMPONENT_SET",!0):!1}function F(e,t,n){let s=o=>{let r=Math.round(o*255).toString(16);return r.length===1?"0"+r:r};return`#${s(e)}${s(t)}${s(n)}`}async function Qe(e){try{let t=await figma.variables.getVariableByIdAsync(e);return t?t.name:null}catch(t){return console.warn("Could not access variable:",e,t),null}}async function tn(e,t){try{let n=await figma.variables.getVariableByIdAsync(e);if(!n)return null;if(t&&n.resolveForConsumer)try{let s=n.resolveForConsumer(t);if(s&&typeof s.value=="object"&&"r"in s.value){let o=s.value;return F(o.r,o.g,o.b)}else if(s&&s.value!==void 0)return String(s.value)}catch(s){console.warn("Could not resolve variable value:",s)}return n.name}catch(n){return console.warn("Could not access variable:",e,n),null}}function k(e,t){try{figma.ui.postMessage({type:e,data:t})}catch(n){console.error("Failed to send message to UI:",n)}}function Oe(e){let t=[e];if("children"in e)for(let n of e.children)t.push(...Oe(n));return t}function Ze(e){let t=[];if(e.type==="TEXT"){let n=e;n.characters&&t.push(n.characters)}if("children"in e)for(let n of e.children)t.push(...Ze(n));return t}function ne(e,t,n){let[s,o,r]=[e,t,n].map(i=>i<=.03928?i/12.92:Math.pow((i+.055)/1.055,2.4));return .2126*s+.7152*o+.0722*r}function ke(e,t){let n=Math.max(e,t),s=Math.min(e,t);return(n+.05)/(s+.05)}function Ne(e){let t=e.parent;for(;t&&"type"in t;){let n=t;if("fills"in n){let s=n.fills;if(Array.isArray(s)){for(let o of s)if(o.type==="SOLID"&&o.visible!==!1&&o.color){if(o.boundVariables&&o.boundVariables.color)continue;return o.color}}}t=t.parent}return null}function Ro(e){var s;let t=[],n=e;for(;n&&n.type!=="DOCUMENT"&&n.type!=="PAGE";)n.type==="COMPONENT"&&((s=n.parent)==null?void 0:s.type)==="COMPONENT_SET"?t.unshift(`${n.name}`):t.unshift(n.name),n=n.parent;return t.join(" \u2192 ")}function oe(e){var s,o;let t=Ro(e),n=`Found in "${e.name}"`;if(((s=e.parent)==null?void 0:s.type)==="COMPONENT_SET"||e.parent&&((o=e.parent.parent)==null?void 0:o.type)==="COMPONENT_SET")n=`Found in variant: "${e.name}"`;else if(t.includes("\u2192")){let r=t.split(" \u2192 ");r.length>1&&(n=`Found in "${r[r.length-1]}" (${r[r.length-2]})`)}return{path:t,description:n}}var J=G(()=>{"use strict"});function Ve(e,t=Ce){if(t.includes(e))return[];let n=[...t].map(o=>({v:o,diff:Math.abs(o-e)})).sort((o,r)=>o.diff-r.diff),s=[];for(let o of n){if(s.length>=2)break;s.includes(o.v)||s.push(o.v)}return s.sort((o,r)=>o-r)}var Ce,mt,ft=G(()=>{"use strict";Ce=[0,2,4,8,12,16,20,24,32,40,48,64,80,96],mt=Ce});function Qo(){return`spacing-${++fn}`}function Zo(e){return gt.includes(e)}function er(e){return{itemSpacing:"Gap",paddingTop:"Padding Top",paddingBottom:"Padding Bottom",paddingLeft:"Padding Left",paddingRight:"Padding Right",counterAxisSpacing:"Counter-axis Gap"}[e]||e}function tr(e,t){var o;if(e.layoutMode==="NONE")return 0;let n=0,s=[{prop:"itemSpacing",value:e.itemSpacing},{prop:"paddingTop",value:e.paddingTop},{prop:"paddingBottom",value:e.paddingBottom},{prop:"paddingLeft",value:e.paddingLeft},{prop:"paddingRight",value:e.paddingRight}];"counterAxisSpacing"in e&&typeof e.counterAxisSpacing=="number"&&s.push({prop:"counterAxisSpacing",value:e.counterAxisSpacing});for(let{prop:r,value:i}of s)if(n++,!Zo(i)){let a=Ve(i,gt);t.push({id:Qo(),type:"spacing",severity:"warning",nodeId:e.id,nodeName:e.name,message:`${er(r)} is ${i}px \u2014 not in spacing scale`,currentValue:`${i}px`,suggestions:a.map(c=>`${c}px`),autoFixable:!0,fixAction:{type:"fixSpacing",params:{nodeId:e.id,property:r,currentValue:i,suggestedValue:(o=a[0])!=null?o:i}}})}return n}function gn(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,passed:0};if(s&&i)return{checked:0,passed:0};let a=0,c=0;if(e.type==="FRAME"||e.type==="COMPONENT"||e.type==="INSTANCE"){let d=t.length,l=tr(e,t);a+=l,c+=l-(t.length-d)}if("children"in e)for(let d of e.children){let l=gn(d,t,n,s,r);a+=l.checked,c+=l.passed}return{checked:a,passed:c}}function yn(e,t={}){let{skipLocked:n=!0,skipHidden:s=!0,scale:o}=t;gt=o||Ce,fn=0;let r=[],i=0,a=0;for(let c of e){let{checked:d,passed:l}=gn(c,r,n,s,!1);i+=d,a+=l}return{issues:r,summary:{totalChecked:i,passed:a,failed:r.length}}}var fn,gt,hn=G(()=>{"use strict";ft();fn=0;gt=Ce});function nr(){return`autolayout-${++bn}`}function vn(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{totalFrames:0,withAutoLayout:0};if(s&&i)return{totalFrames:0,withAutoLayout:0};let a=0,c=0;if((e.type==="FRAME"||e.type==="COMPONENT"||e.type==="INSTANCE")&&"children"in e){let l=e.children;l.length>=2&&(a++,e.layoutMode!=="NONE"?c++:t.push({id:nr(),type:"autoLayout",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Frame "${e.name}" has ${l.length} children but no Auto Layout`,currentValue:"No Auto Layout",suggestions:["HORIZONTAL","VERTICAL"],autoFixable:!1}))}if("children"in e)for(let l of e.children){let p=vn(l,t,n,s,r);a+=p.totalFrames,c+=p.withAutoLayout}return{totalFrames:a,withAutoLayout:c}}function Sn(e,t={}){let{skipLocked:n=!0,skipHidden:s=!0}=t;bn=0;let o=[],r=0,i=0;for(let d of e){let l=vn(d,o,n,s,!1);r+=l.totalFrames,i+=l.withAutoLayout}let a=r-i,c=r>0?Math.round(i/r*100):100;return{issues:o,summary:{totalFrames:r,withAutoLayout:i,withoutAutoLayout:a,percentage:c}}}var bn,kn=G(()=>{"use strict";bn=0});function se(){return`a11y-${++Nn}`}function yt(e){return sr.test(e)}function rr(e,t){if(e.type!=="TEXT")return;let n=e,s=n.fills;if(s===figma.mixed||!Array.isArray(s))return;let o=s.find(m=>{var h;return m.type==="SOLID"&&m.visible!==!1&&m.color&&!((h=m.boundVariables)!=null&&h.color)});if(!o||o.type!=="SOLID")return;let r=Ne(e);if(!r)return;let i=o.color,a=ne(i.r,i.g,i.b),c=ne(r.r,r.g,r.b),d=ke(a,c),l=n.fontSize!==figma.mixed?n.fontSize:0,p=n.fontName!==figma.mixed?n.fontName.style:"",u=p.toLowerCase().includes("bold")||p.toLowerCase().includes("black"),g=l>=18||l>=14&&u,f=g?3:4.5;if(d<f){let m=d.toFixed(1);t.push({id:se(),type:"accessibility",severity:"critical",nodeId:e.id,nodeName:e.name,message:`Contrast ratio ${m}:1 below WCAG AA ${g?"large text":""} minimum of ${f}:1`,currentValue:`${m}:1`,suggestions:[`Increase contrast to at least ${f}:1`],autoFixable:!1})}}function ir(e,t){if(e.type!=="FRAME"&&e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!yt(e.name))return;let n=e.width,s=e.height;(n<44||s<44)&&t.push({id:se(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Touch target ${Math.round(n)}x${Math.round(s)}px is below 44x44px recommended minimum`,currentValue:`${Math.round(n)}x${Math.round(s)}px`,suggestions:["Increase to at least 44x44px"],autoFixable:!1})}function ar(e,t){if(e.type!=="TEXT")return;let s=e.fontSize;s===figma.mixed||typeof s!="number"||s>0&&s<12&&t.push({id:se(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Text size ${s}px is below 12px readability minimum`,currentValue:`${s}px`,suggestions:["12px","14px"],autoFixable:!1})}function cr(e,t){if(e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!yt(e.name))return;let n=e;if(!("children"in n)||n.children.length===0)return;n.children.some(o=>{var r,i;return o.type==="TEXT"?!0:"children"in o?(i=(r=o.children)==null?void 0:r.some)==null?void 0:i.call(r,a=>a.type==="TEXT"):!1})||t.push({id:se(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Interactive element "${e.name}" has no visible text label`,currentValue:"No text child",suggestions:["Add a text label or ensure screen reader label is provided"],autoFixable:!1})}function lr(e,t){e.type!=="FRAME"&&e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!("children"in e)||e.children.length===0||or.test(e.name)&&t.push({id:se(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`Generic layer name "${e.name}" \u2014 use a descriptive name`,currentValue:e.name,suggestions:["Rename to describe the layer purpose"],autoFixable:!1})}function dr(e,t){if(e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!yt(e.name))return;let n=e,s=null,o=n.strokes;if(Array.isArray(o)){let d=o.find(l=>l.type==="SOLID"&&l.visible!==!1);d&&d.type==="SOLID"&&(s=d.color)}if(!s){let d=n.fills;if(d!==figma.mixed&&Array.isArray(d)){let l=d.find(p=>p.type==="SOLID"&&p.visible!==!1);l&&l.type==="SOLID"&&(s=l.color)}}if(!s)return;let r=Ne(e);if(!r)return;let i=ne(s.r,s.g,s.b),a=ne(r.r,r.g,r.b),c=ke(i,a);c<3&&t.push({id:se(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Non-text contrast ${c.toFixed(1)}:1 below WCAG 1.4.11 minimum of 3:1`,currentValue:`${c.toFixed(1)}:1`,suggestions:["Increase boundary contrast to at least 3:1 against background"],autoFixable:!1})}function pr(e,t){if(e.type!=="FRAME"&&e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!ur.test(e.name))return;let n=e;if(!("children"in n)||n.children.length===0)return;n.children.some(o=>{var r,i;return o.type==="TEXT"||/icon|svg|symbol|glyph/i.test(o.name)?!0:"children"in o?(i=(r=o.children)==null?void 0:r.some)==null?void 0:i.call(r,a=>a.type==="TEXT"||/icon|svg|symbol|glyph/i.test(a.name)):!1})||t.push({id:se(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`"${e.name}" may rely on color alone to convey status (WCAG 1.4.1)`,currentValue:"No text or icon indicator",suggestions:["Add a text label or icon to supplement the color indicator"],autoFixable:!1})}function mr(e,t){if(e.type!=="COMPONENT")return;let n=e.parent;if(!n||n.type!=="COMPONENT_SET")return;let s=n,r=s.children.map(c=>c.name.toLowerCase()).join(" "),a=["hover","focus","disabled","pressed"].filter(c=>!r.includes(c));a.length>0&&t.push({id:se(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`Component set missing states: ${a.join(", ")}`,currentValue:`${s.children.length} variants`,suggestions:a.map(c=>`Add ${c} variant`),autoFixable:!1})}function wn(e,t,n,s,o,r){var d;let i=o||"locked"in e&&e.locked,a="visible"in e&&!e.visible;if(n&&i||s&&a)return 0;let c=0;if(rr(e,t),ir(e,t),ar(e,t),c++,cr(e,t),lr(e,t),dr(e,t),pr(e,t),e.type==="COMPONENT"&&((d=e.parent)==null?void 0:d.type)==="COMPONENT_SET"){let l=e.parent.id;r.has(l)||(r.add(l),mr(e,t))}if("children"in e)for(let l of e.children)c+=wn(l,t,n,s,i,r);return c}function Cn(e,t={}){let{skipLocked:n=!0,skipHidden:s=!0}=t;Nn=0;let o=[],r=new Set,i=0;for(let a of e)i+=wn(a,o,n,s,!1,r);return{issues:o,summary:{totalChecked:i,contrastIssues:o.filter(a=>a.message.includes("Contrast")).length,touchTargetIssues:o.filter(a=>a.message.includes("Touch target")).length,textSizeIssues:o.filter(a=>a.message.includes("Text size")).length,namingIssues:o.filter(a=>a.message.includes("text label")||a.message.includes("Generic")).length,stateIssues:o.filter(a=>a.message.includes("missing states")).length,nonTextContrastIssues:o.filter(a=>a.message.includes("Non-text contrast")).length,colorOnlyIssues:o.filter(a=>a.message.includes("color alone")).length}}}var Nn,sr,or,ur,In=G(()=>{"use strict";J();Nn=0;sr=/\b(button|btn|input|link|checkbox|toggle|switch|tab|radio|select|dropdown|menu-item|slider|chip)\b/i;or=/^(Frame|Group|Rectangle|Ellipse|Vector|Line|Polygon|Star)\s*\d+$/i;ur=/\b(error|success|warning|status|alert|badge|danger|info)\b/i});function ge(){return`vq-${++xn}`}function gr(e){return fr.some(t=>t.includes(e))}function yr(e,t){let n=e.width*e.height;if(n===0)return;let o=("children"in e?e.children.filter(i=>i.visible!==!1):[]).length,r=o/n*1e3;r>3&&t.push({id:ge(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`High visual density: ${o} elements in ${Math.round(n/1e3)}k px\xB2 (${r.toFixed(2)}/1000px\xB2). Consider simplifying or using progressive disclosure.`,currentValue:`${r.toFixed(2)} elements/1000px\xB2`,suggestions:["Reduce visible elements to under 15 per viewport","Group related items","Use progressive disclosure"],autoFixable:!1})}function An(e,t,n,s,o){if(!(s&&"locked"in e&&e.locked)&&!(o&&"visible"in e&&!e.visible)){if(e.type==="TEXT"){let r=e,i=r.fontSize;if(i!==figma.mixed&&typeof i=="number"){t.add(i);let a=r.lineHeight;if(a!==figma.mixed&&typeof a=="object"&&a.unit==="PIXELS"){let c=a.value/i;n.push({fontSize:i,lineHeight:a.value,ratio:c})}}}if("children"in e)for(let r of e.children)An(r,t,n,s,o)}}function hr(e,t,n,s){let o=new Set,r=[];An(e,o,r,n,s);let i=Array.from(o).sort((d,l)=>d-l),a=i.filter(d=>!gr(d));a.length>0&&t.push({id:ge(),type:"accessibility",severity:"info",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`Non-standard font sizes: ${a.join(", ")}px. Consider using a type scale (e.g., 12/14/16/20/24/32).`,currentValue:a.map(d=>`${d}px`).join(", "),suggestions:a.map(d=>{let l=[10,12,14,16,18,20,24,28,32,36,40,48].reduce((p,u)=>Math.abs(u-d)<Math.abs(p-d)?u:p);return`${d}px \u2192 ${l}px`}),autoFixable:!1});let c=r.filter(d=>d.ratio<1.2||d.ratio>2);if(c.length>0){let d=c.reduce((l,p)=>Math.abs(p.ratio-1.5)>Math.abs(l.ratio-1.5)?p:l);t.push({id:ge(),type:"accessibility",severity:"info",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`Line height ratio ${d.ratio.toFixed(2)} (${d.lineHeight}px / ${d.fontSize}px) is outside optimal range 1.3\u20131.6.`,currentValue:`${d.ratio.toFixed(2)}`,suggestions:[`Set line height to ${Math.round(d.fontSize*1.5)}px (1.5\xD7 body) or ${Math.round(d.fontSize*1.3)}px (1.3\xD7 headings)`],autoFixable:!1})}return{sizes:i,lineHeightData:r}}function En(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if("fills"in e){let o=e.fills;if(o!==figma.mixed&&Array.isArray(o))for(let r of o)r.visible!==!1&&r.type==="SOLID"&&t.add(F(r.color.r,r.color.g,r.color.b))}if("strokes"in e){let o=e.strokes;if(Array.isArray(o))for(let r of o)r.visible!==!1&&r.type==="SOLID"&&t.add(F(r.color.r,r.color.g,r.color.b))}if("children"in e)for(let o of e.children)En(o,t,n,s)}}function br(e,t,n,s){let o=new Set;En(e,o,n,s);let r=Array.from(o);return r.length>8&&t.push({id:ge(),type:"accessibility",severity:"warning",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`${r.length} unique colors detected. A cohesive palette typically uses 5\u20137 colors (primary, secondary, accent, neutrals).`,currentValue:`${r.length} colors`,suggestions:["Consolidate similar colors into design tokens","Limit palette to primary, secondary, accent, and 2-3 neutrals"],autoFixable:!1}),r}function vr(e,t,n=4){if(!("children"in e))return 0;let s=e.children.filter(r=>r.visible!==!1),o=0;for(let r of s){if(!("x"in r)||!("y"in r))continue;let i=r.x,a=r.y,c=Math.round(i)%n,d=Math.round(a)%n;(c!==0||d!==0)&&o++}return o>0&&o/Math.max(s.length,1)>.3&&t.push({id:ge(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`${o}/${s.length} direct children are misaligned from ${n}px grid.`,currentValue:`${o} misaligned`,suggestions:[`Snap elements to ${n}px grid for visual consistency`],autoFixable:!1}),o}function Tn(e,t,n,s){if(n&&"locked"in e&&e.locked||s&&"visible"in e&&!e.visible)return;if(/button|btn|cta/i.test(e.name)&&"width"in e&&"height"in e&&t.push({nodeId:e.id,nodeName:e.name,width:e.width,height:e.height}),"children"in e)for(let r of e.children)Tn(r,t,n,s)}function Sr(e,t,n,s){let o=[];if(Tn(e,o,n,s),o.length<2)return;let r=o.map(d=>d.height),i=r.reduce((d,l)=>d+l,0)/r.length,c=Math.max(...r.map(d=>Math.abs(d-i)))/i*100;if(c>15){let d=Math.min(...r),l=Math.max(...r);t.push({id:ge(),type:"accessibility",severity:"warning",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`Button height inconsistency: ${d}px to ${l}px (${Math.round(c)}% variance). Standardize to 2-3 size tiers.`,currentValue:`${d}\u2013${l}px`,suggestions:["Use consistent button heights: 32px (small), 40px (medium), 48px (large)"],autoFixable:!1})}}function Pn(e,t={}){var g,f;xn=0;let n=[],s=(g=t.skipLocked)!=null?g:!0,o=(f=t.skipHidden)!=null?f:!0,r=0,i=[],a=[],c=[],d=0,l=0,p=0;for(let m of e){"children"in m&&"width"in m&&"height"in m&&(yr(m,n),l+=m.children.length,p+=m.width*m.height,r++);let h=hr(m,n,s,o);i=[...new Set([...i,...h.sizes])],a=[...a,...h.lineHeightData],r++;let C=br(m,n,s,o);c=[...new Set([...c,...C])],r++,"children"in m&&(d+=vr(m,n),r++),Sr(m,n,s,o),r++}let u=p>0?l/p*1e3:0;return{issues:n,metrics:{childCount:l,areaPx:p,density:u,uniqueFontSizes:i,lineHeightRatios:a,uniqueColors:c,misalignedCount:d},summary:{totalChecked:r,passed:r-n.length,failed:n.length}}}var xn,fr,Ln=G(()=>{"use strict";J();xn=0;fr=[[10,12,14,16,18,20,24,28,32,36,40,48,56,64,72],[12,14,16,20,24,32,40,48],[12,14,16,18,21,24,30,36,48,60,72]]});function te(){return`mc-${++Rn}`}function $n(e){if(ht.test(e.name))return!0;let t=e.parent,n=0;for(;t&&n<4;){if("name"in t&&ht.test(t.name)||"type"in t&&(t.type==="COMPONENT"||t.type==="INSTANCE")&&"name"in t&&ht.test(t.name))return!0;t=t.parent,n++}return!1}function Mn(e){return e.trim().split(/\s+/).filter(Boolean).length}function Er(e,t){let n=e.characters;if(!n||n.trim().length===0){t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:"Empty text node \u2014 remove or add content.",currentValue:"(empty)",autoFixable:!1});return}let s=n.trim(),o=Mn(s),r=$n(e);if((kr.test(s)||wr.test(s))&&t.push({id:te(),type:"naming",severity:"warning",nodeId:e.id,nodeName:e.name,message:`"${s.substring(0,40)}" \u2014 avoid "click/tap here". Use descriptive action: "Download report", "View details".`,currentValue:s.substring(0,60),suggestions:['Use verb + object: "Download PDF", "View pricing", "Start trial"'],autoFixable:!1}),Nr.test(s)&&t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:'"Learn more" is vague \u2014 specify what the user will learn: "Learn more about pricing".',currentValue:s,suggestions:['Add specificity: "Learn more about [topic]"'],autoFixable:!1}),r&&o<=2){let i=s.toLowerCase().replace(/[.!]/g,"");Cr.has(i)&&t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`Generic CTA "${s}" \u2014 use a specific action: "Save changes", "Send message", "Create account".`,currentValue:s,suggestions:["Replace with verb + object describing the outcome"],autoFixable:!1})}if(r&&o>5&&t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`CTA too long (${o} words): "${s.substring(0,50)}\u2026". Keep CTAs to 2\u20135 words.`,currentValue:`${o} words`,suggestions:["Shorten to verb + object (2-5 words)"],autoFixable:!1}),(Ir.test(s)||xr.test(s))&&t.push({id:te(),type:"naming",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Placeholder text detected: "${s.substring(0,40)}\u2026". Replace with real content.`,currentValue:s.substring(0,60),suggestions:["Replace with actual copy or realistic sample data"],autoFixable:!1}),o>80&&!r&&t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`Long text block (${o} words). Break into shorter paragraphs or use bullet points for readability.`,currentValue:`${o} words`,suggestions:["Break into paragraphs of \u226450 words","Use bullet points for lists","Add subheadings"],autoFixable:!1}),s===s.toUpperCase()&&s!==s.toLowerCase()&&o>3&&t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`All-caps text with ${o} words: "${s.substring(0,40)}\u2026". ALL CAPS reduces readability \u2014 use sentence case or title case.`,currentValue:s.substring(0,60),suggestions:["Use sentence case for readability","Reserve ALL CAPS for short labels (1-2 words)"],autoFixable:!1}),Ar.test(s)){let a=(s.match(/\b\d{4,}\b/g)||[]).filter(c=>{let d=parseInt(c,10);return d<1900||d>2099});a.length>0&&t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`Unformatted number${a.length>1?"s":""}: ${a.join(", ")}. Use thousand separators for readability.`,currentValue:a.join(", "),suggestions:["Format as 1,000,000 or 1 000 000"],autoFixable:!1})}}function On(e,t,n,s,o){var r;if(!(s&&"locked"in e&&e.locked)&&!(o&&"visible"in e&&!e.visible)){if(e.type==="TEXT"){n.totalTextNodes++;let i=((r=e.characters)==null?void 0:r.trim())||"",a=Mn(i);a>0&&(n.wordCounts.push(a),a>n.longestParagraph&&(n.longestParagraph=a)),$n(e)&&n.ctaNodes++,Er(e,t)}if("children"in e)for(let i of e.children)On(i,t,n,s,o)}}function Fn(e,t={}){var a,c;Rn=0;let n=[],s=(a=t.skipLocked)!=null?a:!0,o=(c=t.skipHidden)!=null?c:!0,r={totalTextNodes:0,ctaNodes:0,wordCounts:[],longestParagraph:0};for(let d of e)On(d,n,r,s,o);let i=r.wordCounts.length>0?r.wordCounts.reduce((d,l)=>d+l,0)/r.wordCounts.length:0;return{issues:n,metrics:{totalTextNodes:r.totalTextNodes,ctaNodes:r.ctaNodes,avgWordCount:Math.round(i*10)/10,longestParagraph:r.longestParagraph},summary:{totalChecked:r.totalTextNodes,passed:r.totalTextNodes-n.length,failed:n.length}}}var Rn,kr,Nr,wr,Cr,Ir,xr,Ar,ht,Dn=G(()=>{"use strict";Rn=0;kr=/\bclick\s+here\b/i,Nr=/^learn\s+more\.?$/i,wr=/\btap\s+here\b/i,Cr=new Set(["submit","ok","okay","next","continue","go","yes","no","done","send","save","apply"]),Ir=/\blorem\s+ipsum\b/i,xr=/^(enter\s+text|type\s+here|placeholder|sample\s+text|your\s+text|add\s+text)\.?$/i,Ar=/\b\d{4,}\b/,ht=/button|btn|cta|action|submit|link/i});function ye(){return`conv-${++Un}`}function Lr(e){if(Vn.test(e.name))return!0;let t=e.parent,n=0;for(;t&&n<3;){if("name"in t&&Vn.test(t.name))return!0;t=t.parent,n++}return!1}function Rr(e){return Gn.test(e.name)}function _n(e){if(!("fills"in e))return null;let t=e.fills;if(t===figma.mixed||!Array.isArray(t))return null;let n=t.find(s=>s.type==="SOLID"&&s.visible!==!1);return n?n.color:null}function Bn(e,t,n){let s=[e,t,n].map(o=>o<=.03928?o/12.92:Math.pow((o+.055)/1.055,2.4));return .2126*s[0]+.7152*s[1]+.0722*s[2]}function $r(e,t){let n=Math.max(e,t),s=Math.min(e,t);return(n+.05)/(s+.05)}function zn(e,t,n,s,o){if(!(s&&"locked"in e&&e.locked)&&!(o&&"visible"in e&&!e.visible)&&(Lr(e)&&"width"in e&&"height"in e&&t.push({node:e,x:"x"in e?e.x:0,y:"y"in e?e.y:0,width:e.width,height:e.height,absoluteY:n+("y"in e?e.y:0)}),"children"in e)){let r=n+("y"in e?e.y:0);for(let i of e.children)zn(i,t,r,s,o)}}function Wn(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if(Rr(e)){let o=!1,r=e.parent;if(r&&"children"in r){for(let i of r.children)if(i.type==="TEXT"&&i.id!==e.id){o=!0;break}}t.push({node:e,hasLabel:o})}if("children"in e)for(let o of e.children)Wn(o,t,n,s)}}function _e(e,t,n,s){if(n&&"locked"in e&&e.locked||s&&"visible"in e&&!e.visible)return!1;if(t.test(e.name))return!0;if("children"in e){for(let o of e.children)if(_e(o,t,n,s))return!0}return!1}function Mr(e,t,n){if(t.length===0||!("height"in e))return!1;let s=e.height*.7,o=t.some(r=>r.y+r.height<s);return o||n.push({id:ye(),type:"accessibility",severity:"warning",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:"No primary CTA visible above the fold (top 70% of frame). Move the main action higher for better conversion.",currentValue:`CTA at ${Math.round(t[0].y)}px, fold at ${Math.round(s)}px`,suggestions:["Place primary CTA within top 70% of the viewport","Add a secondary CTA near the top if main CTA must stay below"],autoFixable:!1}),o}function Or(e,t,n){let s=_n(e);if(!s)return;let o=Bn(s.r,s.g,s.b);for(let r of t){let i=_n(r.node);if(!i)continue;let a=Bn(i.r,i.g,i.b),c=$r(o,a);if(c<3){let d=F(i.r,i.g,i.b),l=F(s.r,s.g,s.b);n.push({id:ye(),type:"accessibility",severity:"warning",nodeId:r.node.id,nodeName:r.node.name,message:`CTA contrast ratio ${c.toFixed(1)}:1 (${d} on ${l}) \u2014 too low. CTAs should stand out with \u22653:1 contrast against background.`,currentValue:`${c.toFixed(1)}:1`,suggestions:["Increase CTA background contrast to at least 3:1","Use a bolder accent color for the primary action"],autoFixable:!1})}}}function Fr(e,t,n){t.length>5&&n.push({id:ye(),type:"accessibility",severity:"warning",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`${t.length} form fields on one screen. More than 5 fields increases abandonment \u2014 consider splitting into steps or removing optional fields.`,currentValue:`${t.length} fields`,suggestions:["Split into multi-step form with progress indicator",'Remove optional fields or move to "Advanced" section',"Expedia gained $12M/year by removing one field"],autoFixable:!1});let s=t.filter(o=>!o.hasLabel);s.length>0&&n.push({id:ye(),type:"accessibility",severity:"warning",nodeId:s[0].node.id,nodeName:s[0].node.name,message:`${s.length} form field${s.length===1?"":"s"} without visible labels. Labels improve completion rate and accessibility.`,currentValue:`${s.length} unlabeled`,suggestions:["Add visible label text above or beside each input","Don't rely on placeholder text alone as labels"],autoFixable:!1})}function Dr(e,t,n,s,o){if(t.length<=3)return!1;let r=_e(e,Tr,s,o);return!r&&t.length>5&&n.push({id:ye(),type:"accessibility",severity:"info",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:"Long form without progress indicator. A step counter or progress bar reduces perceived effort.",suggestions:['Add "Step 1 of 3" or a progress bar',"Show users how far they've come and what's left"],autoFixable:!1}),r}function Vr(e,t,n,s,o){if(t.length===0||!_e(e,Gn,s,o))return;_e(e,Pr,s,o)||n.push({id:ye(),type:"accessibility",severity:"info",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:"Form with CTA but no trust signals (security badges, reviews, guarantees). Trust elements near CTAs increase conversion.",suggestions:["Add security badge or lock icon near submit button","Show testimonials, ratings, or guarantees near the CTA"],autoFixable:!1})}function Hn(e,t={}){var l,p;Un=0;let n=[],s=(l=t.skipLocked)!=null?l:!0,o=(p=t.skipHidden)!=null?p:!0,r=0,i=0,a=!1,c=!1,d=0;for(let u of e){let g=[];zn(u,g,0,s,o),r+=g.length;let f=[];Wn(u,f,s,o),i+=f.length,g.length>0&&(Mr(u,g,n)&&(a=!0),Or(u,g,n),d+=2),f.length>0&&(Fr(u,f,n),Dr(u,f,n,s,o)&&(c=!0),d+=2),Vr(u,g,n,s,o),d++}return{issues:n,metrics:{ctaCount:r,formFieldCount:i,ctaAboveFold:a,hasProgressIndicator:c},summary:{totalChecked:d,passed:d-n.length,failed:n.length}}}var Un,Vn,Gn,Tr,Pr,Kn=G(()=>{"use strict";J();Un=0;Vn=/button|btn|cta|action|submit|primary/i,Gn=/input|field|text.?area|select|dropdown|picker|combo|search|email|password|phone|number.?field/i,Tr=/progress|step|stepper|breadcrumb|wizard|indicator|pagination/i,Pr=/badge|trust|security|lock|shield|guarantee|verified|secure|ssl|certification|review|rating|star/i});function Ie(){return`cog-${++qn}`}function zr(e){return _r.test(e.name)}function Wr(e){return Br.test(e.name)}function Xn(e){return Jn.test(e.name)}function Hr(e){return Ur.test(e.name)}function Kr(e){return Gr.test(e.name)}function jr(e){if(!jn.test(e.name)&&!Jn.test(e.name)||!("children"in e))return!1;let t=e.children,n=t.some(o=>o.type==="TEXT");return t.some(o=>o.type==="VECTOR"||o.type==="BOOLEAN_OPERATION"||jn.test(o.name))&&!n}function Yn(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if(Wr(e)){t.push(e);return}if("children"in e)for(let o of e.children)Yn(o,t,n,s)}}function Qn(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)&&(Xn(e)&&t.push(e),"children"in e))for(let o of e.children)Qn(o,t,n,s)}function qr(e){let t=e.match(/h(\d)/i);return t?parseInt(t[1],10):/title|headline/i.test(e)?1:/subtitle|subhead/i.test(e)||/heading/i.test(e)?2:null}function Zn(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if(Hr(e)){let o=qr(e.name);o!==null&&t.push({node:e,level:o})}if("children"in e)for(let o of e.children)Zn(o,t,n,s)}}function es(e,t,n,s){if(n&&"locked"in e&&e.locked||s&&"visible"in e&&!e.visible)return t;let o=t;if("children"in e)for(let r of e.children){let i=es(r,t+1,n,s);i>o&&(o=i)}return o}function ts(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)&&(Kr(e)&&"opacity"in e&&e.opacity<1&&t.push(e),"children"in e))for(let o of e.children)ts(o,t,n,s)}function ns(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)&&(Xn(e)&&jr(e)&&t.push(e),"children"in e))for(let o of e.children)ns(o,t,n,s)}function Jr(e,t,n,s){let o=[];ss(e,o,n,s);let r=0;for(let i of o){let a=[];Yn(i,a,n,s),r+=a.length,a.length>7&&t.push({id:Ie(),type:"accessibility",severity:"warning",nodeId:i.id,nodeName:i.name,message:`Navigation has ${a.length} items \u2014 Miller's Law suggests 7\xB12 is the working memory limit. Consider grouping or progressive disclosure.`,currentValue:`${a.length} nav items`,suggestions:["Group related items under expandable sections",'Use "More" menu for less-used items',"Limit primary navigation to 5-7 items"],autoFixable:!1})}return r}function ss(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if(zr(e)){t.push(e);return}if("children"in e)for(let o of e.children)ss(o,t,n,s)}}function Xr(e,t,n,s){let o=[];return Qn(e,o,n,s),o.length>5&&t.push({id:Ie(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`${o.length} CTAs/buttons on one screen \u2014 choice overload reduces decision-making ability (Hick's Law). Prioritize one primary action.`,currentValue:`${o.length} CTAs`,suggestions:["Establish clear primary/secondary/tertiary action hierarchy","Reduce to 1 primary CTA per viewport","Group related actions in a dropdown or overflow menu"],autoFixable:!1}),o.length}function Yr(e,t,n,s){let o=[];if(Zn(e,o,n,s),o.length<2)return o.map(i=>i.level);let r=o.sort((i,a)=>{let c="y"in i.node?i.node.y:0,d="y"in a.node?a.node.y:0;return c-d});for(let i=1;i<r.length;i++){let a=r[i-1].level,c=r[i].level;c>a+1&&t.push({id:Ie(),type:"accessibility",severity:"info",nodeId:r[i].node.id,nodeName:r[i].node.name,message:`Heading hierarchy gap: jumps from level ${a} to level ${c}. Screen readers and users rely on sequential heading structure.`,currentValue:`h${a} \u2192 h${c}`,suggestions:[`Add an h${a+1} between these levels`,"Ensure headings follow a logical descending order"],autoFixable:!1})}return r.map(i=>i.level)}function Qr(e,t,n,s){var r;let o=[];ts(e,o,n,s);for(let i of o){let a=i.parent,c=!1;if(a&&"children"in a){for(let d of a.children)if(d.type==="TEXT"&&d.id!==i.id){let l=((r=d.characters)==null?void 0:r.toLowerCase())||"";if(l.includes("required")||l.includes("complete")||l.includes("fill")||l.includes("select")||l.includes("first")){c=!0;break}}}c||t.push({id:Ie(),type:"accessibility",severity:"info",nodeId:i.id,nodeName:i.name,message:`Disabled element "${i.name}" without visible explanation. Users should understand WHY an action is unavailable and how to enable it.`,suggestions:["Add helper text explaining what needs to happen first","Use a tooltip on hover explaining the disabled state",'Show a brief inline message (e.g., "Complete all fields to continue")'],autoFixable:!1})}}function Zr(e,t,n,s){let o=[];return ns(e,o,n,s),o.length>3&&t.push({id:Ie(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`${o.length} icon-only buttons without text labels. Icons alone are ambiguous \u2014 add labels or ensure tooltips are present.`,currentValue:`${o.length} icon-only`,suggestions:["Add visible text labels to icon buttons","Add tooltips that appear on hover/focus","Use aria-label for accessibility (ensure design indicates this)"],autoFixable:!1}),o.length}function os(e,t={}){var p,u;qn=0;let n=[],s=(p=t.skipLocked)!=null?p:!0,o=(u=t.skipHidden)!=null?u:!0,r=0,i=0,a=0,c=[],d=0,l=0;for(let g of e){r+=Jr(g,n,s,o),l++,i+=Xr(g,n,s,o),l++;let f=Yr(g,n,s,o);c=[...c,...f],l++,Qr(g,n,s,o),l++,d+=Zr(g,n,s,o),l++;let m=es(g,0,s,o);m>a&&(a=m)}return{issues:n,metrics:{navItemCount:r,ctaCount:i,maxNestingDepth:a,headingLevels:[...new Set(c)].sort(),iconOnlyButtons:d},summary:{totalChecked:l,passed:l-n.length,failed:n.length}}}var qn,_r,Br,Jn,Ur,Gr,jn,rs=G(()=>{"use strict";qn=0;_r=/nav|menu|sidebar|tab.?bar|bottom.?bar|header.?nav|navigation|top.?bar/i,Br=/nav.?item|menu.?item|tab(?!le)|link/i,Jn=/button|btn|cta|action|submit|primary/i,Ur=/heading|title|h[1-6]|headline/i,Gr=/disabled|inactive|dimmed|greyed/i,jn=/icon|ico|svg|glyph/i});function ei(){return`fitts-${++is}`}function ni(e){return ti.test(e.name)}function as(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,failed:0};if(s&&i)return{checked:0,failed:0};let a=0,c=0;if(ni(e)&&"width"in e&&"height"in e){a++;let d=e.width,l=e.height;(d<he||l<he)&&(c++,t.push({id:ei(),type:"fittsLaw",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Interactive target "${e.name}" is ${Math.round(d)}x${Math.round(l)}px \u2014 minimum recommended size is ${he}x${he}px (WCAG 2.5.8)`,currentValue:`${Math.round(d)}x${Math.round(l)}px`,suggestions:[`Increase to at least ${he}x${he}px`,"Add padding to increase the hit area"],autoFixable:!1}))}if("children"in e)for(let d of e.children){let l=as(d,t,n,s,r);a+=l.checked,c+=l.failed}return{checked:a,failed:c}}function cs(e,t={}){var a,c;is=0;let n=[],s=(a=t.skipLocked)!=null?a:!0,o=(c=t.skipHidden)!=null?c:!0,r=0,i=0;for(let d of e){let l=as(d,n,s,o,!1);r+=l.checked,i+=l.failed}return{issues:n,summary:{totalChecked:r,passed:r-i,failed:i}}}var is,ti,he,ls=G(()=>{"use strict";is=0;ti=/button|btn|cta|action|submit|link|toggle|switch|checkbox|radio|tab(?!le)/i,he=44});function si(){return`gestalt-${++ds}`}function us(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,failed:0};if(s&&i)return{checked:0,failed:0};let a=0,c=0;if((e.type==="FRAME"||e.type==="COMPONENT"||e.type==="INSTANCE")&&"children"in e){let l=e;if(l.layoutMode==="NONE"&&l.children.length>=3){a++;let p=l.children.filter(u=>"visible"in u&&u.visible&&"y"in u);if(p.length>=3){let u=[...p].sort((f,m)=>f.y-m.y),g=[];for(let f=1;f<u.length;f++){let m=u[f-1].y+u[f-1].height,h=u[f].y;g.push(h-m)}if(g.length>=2){let f=new Set(g.map(m=>Math.round(m)));f.size>2&&(c++,t.push({id:si(),type:"gestalt",severity:"info",nodeId:e.id,nodeName:e.name,message:`Frame "${e.name}" has ${f.size} different spacing gaps between children (${[...f].join(", ")}px) \u2014 inconsistent proximity weakens visual grouping (Gestalt proximity principle)`,currentValue:`${f.size} distinct gaps`,suggestions:["Use auto-layout with consistent gap spacing","Standardize spacing between sibling elements"],autoFixable:!1}))}}}}if("children"in e)for(let l of e.children){let p=us(l,t,n,s,r);a+=p.checked,c+=p.failed}return{checked:a,failed:c}}function ps(e,t={}){var a,c;ds=0;let n=[],s=(a=t.skipLocked)!=null?a:!0,o=(c=t.skipHidden)!=null?c:!0,r=0,i=0;for(let d of e){let l=us(d,n,s,o,!1);r+=l.checked,i+=l.failed}return{issues:n,summary:{totalChecked:r,passed:r-i,failed:i}}}var ds,ms=G(()=>{"use strict";ds=0});function oi(){return`detach-${++fs}`}function gs(e,t,n,s,o){var d;let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,failed:0};if(s&&i)return{checked:0,failed:0};let a=0,c=0;if(e.type==="FRAME"&&"children"in e){a++;let l=ri.test(e.name),p=ii.test(e.name)&&((d=e.parent)==null?void 0:d.type)!=="PAGE"&&e.children.length>0;if(l)c++,t.push({id:oi(),type:"detachedInstance",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Frame "${e.name}" appears to be a detached component instance. Detaching breaks the link to the source component and prevents design system updates.`,currentValue:"Detached instance",suggestions:["Re-attach by replacing with the original component instance",'If intentional, rename to remove "detach" from the name'],autoFixable:!1});else if(p){let u=e.name.split(/[\s\-\/]/);u.length>=2&&u.every(g=>g.length>0)}}if("children"in e)for(let l of e.children){let p=gs(l,t,n,s,r);a+=p.checked,c+=p.failed}return{checked:a,failed:c}}function ys(e,t={}){var a,c;fs=0;let n=[],s=(a=t.skipLocked)!=null?a:!0,o=(c=t.skipHidden)!=null?c:!0,r=0,i=0;for(let d of e){let l=gs(d,n,s,o,!1);r+=l.checked,i+=l.failed}return{issues:n,summary:{totalChecked:r,passed:r-i,failed:i}}}var fs,ri,ii,hs=G(()=>{"use strict";fs=0;ri=/detach/i,ii=/^[A-Z][a-zA-Z]+(?:\s*[-\/]\s*[A-Za-z]+)*$/});function bt(){return`resp-${++vs}`}function Ss(e){return e.type==="FRAME"||e.type==="COMPONENT"||e.type==="INSTANCE"}function bs(e){for(let t of ai){let n=e.match(t);if(n){for(let s of n.slice(1))if(ci.has(s.toLowerCase()))return s.toLowerCase()}}return null}function ks(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,failed:0,fixedWidthCount:0};if(s&&i)return{checked:0,failed:0,fixedWidthCount:0};let a=0,c=0,d=0;if(Ss(e)){let l=e;a++;let p=l.layoutSizingHorizontal==="FIXED"||l.layoutSizingHorizontal===void 0,u=!l.parent||l.parent.type==="PAGE",g=l.layoutMode!=="NONE",f="minWidth"in l&&l.minWidth!==null&&l.minWidth!==void 0||"maxWidth"in l&&l.maxWidth!==null&&l.maxWidth!==void 0;p&&!u&&!f&&g&&l.width>200&&(d++,c++,t.push({id:bt(),type:"responsive",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Frame "${e.name}" has fixed width (${Math.round(l.width)}px) with auto-layout but no fill/hug sizing \u2014 may not adapt to different screen sizes`,currentValue:`${Math.round(l.width)}px fixed`,suggestions:['Set horizontal sizing to "Fill" for responsive behavior',"Add min-width/max-width constraints",'Use "Hug contents" if the frame should shrink-wrap'],autoFixable:!1}))}if("children"in e)for(let l of e.children){let p=ks(l,t,n,s,r);a+=p.checked,c+=p.failed,d+=p.fixedWidthCount}return{checked:a,failed:c,fixedWidthCount:d}}function Ns(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,failed:0,riskCount:0};if(s&&i)return{checked:0,failed:0,riskCount:0};let a=0,c=0,d=0;if(e.type==="TEXT"){let l=e;a++;let p=l.fontSize!==figma.mixed?l.fontSize:14,u=l.textAutoResize;if(u==="NONE"||u==="TRUNCATE"){let g=l.characters.length,f=g*p*li,m=l.width;g>5&&f>m*.8&&(d++,c++,t.push({id:bt(),type:"responsive",severity:"info",nodeId:e.id,nodeName:e.name,message:`Text "${e.name}" may truncate \u2014 content fills ~${Math.round(f/m*100)}% of fixed width (${Math.round(m)}px). Translations or dynamic content could overflow.`,currentValue:`${g} chars in ${Math.round(m)}px`,suggestions:["Use auto-width or auto-height text resizing","Allow text to wrap by setting textAutoResize to HEIGHT","Add ellipsis handling if truncation is intentional"],autoFixable:!1}))}}if("children"in e)for(let l of e.children){let p=Ns(l,t,n,s,r);a+=p.checked,c+=p.failed,d+=p.riskCount}return{checked:a,failed:c,riskCount:d}}function ws(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,failed:0,missingCount:0};if(s&&i)return{checked:0,failed:0,missingCount:0};let a=0,c=0,d=0;if(Ss(e)){let l=e;if(l.layoutMode==="HORIZONTAL"&&"children"in l){let p=l.children.filter(u=>"visible"in u&&u.visible);p.length>=3&&(a++,("layoutWrap"in l?l.layoutWrap:"NO_WRAP")!=="WRAP"&&(d++,c++,t.push({id:bt(),type:"responsive",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Horizontal layout "${e.name}" has ${p.length} children without wrap \u2014 content won't reflow on smaller screens`,currentValue:`${p.length} children, no wrap`,suggestions:['Enable "Wrap" on the auto-layout to allow content reflow',"Consider switching to vertical layout on mobile breakpoints","Use min-width on children to control when wrapping occurs"],autoFixable:!1})))}}if("children"in e)for(let l of e.children){let p=ws(l,t,n,s,r);a+=p.checked,c+=p.failed,d+=p.missingCount}return{checked:a,failed:c,missingCount:d}}function di(e){let t=new Set;for(let n of e)ui(n,t);return Array.from(t)}function ui(e,t){if(bs(e.name)&&t.add(e.name),"children"in e)for(let s of e.children)bs(s.name)&&t.add(s.name)}function Cs(e,t){var p,u;vs=0;let n=[],s=(p=t==null?void 0:t.skipLocked)!=null?p:!0,o=(u=t==null?void 0:t.skipHidden)!=null?u:!0,r=0,i=0,a=0,c=0,d=0;for(let g of e){let f=ks(g,n,s,o,!1);r+=f.checked,i+=f.failed,a+=f.fixedWidthCount;let m=Ns(g,n,s,o,!1);r+=m.checked,i+=m.failed,c+=m.riskCount;let h=ws(g,n,s,o,!1);r+=h.checked,i+=h.failed,d+=h.missingCount}let l=di(e);return{issues:n,metrics:{fixedWidthElements:a,textTruncationRisk:c,missingAutoLayout:d,breakpointVariants:l},summary:{totalChecked:r,passed:r-i,failed:i}}}var vs,ai,ci,li,Is=G(()=>{"use strict";vs=0;ai=[/^(.+)\s*[-–—]\s*(desktop|tablet|mobile|phone|sm|md|lg|xl|xxl|small|medium|large)\s*$/i,/^(.+)\/(desktop|tablet|mobile|phone|sm|md|lg|xl|xxl|small|medium|large)\s*$/i,/^(desktop|tablet|mobile|phone|sm|md|lg|xl|xxl|small|medium|large)\s*[-–—/]\s*(.+)$/i,/^(.+)\s*\[(desktop|tablet|mobile|phone|sm|md|lg|xl|xxl|small|medium|large)\]\s*$/i],ci=new Set(["desktop","tablet","mobile","phone","sm","md","lg","xl","xxl","small","medium","large"]),li=.5});var Et={};en(Et,{DEFAULT_LINT_SETTINGS:()=>j,clearIgnored:()=>Ct,findNodesWithSameValue:()=>At,getIgnoredState:()=>It,ignoreAllOfType:()=>wt,ignoreError:()=>Nt,ignoreNode:()=>kt,lintSelection:()=>xe,restoreIgnoredState:()=>xt,runDesignLint:()=>ae});function U(e,t,n){return n?`${e}::${t}::${n}`:`${e}::${t}`}function kt(e){B.add(e)}function Nt(e,t,n){D.add(U(e,t,n))}function wt(e,t){for(let n of e)n.errorType===t&&D.add(U(n.nodeId,n.errorType))}function Ct(){B.clear(),D.clear()}function It(){return{nodeIds:Array.from(B),errorKeys:Array.from(D)}}function xt(e){B=new Set(e.nodeIds),D=new Set(e.errorKeys)}function As(e){if(e.type==="SOLID"){let{r:t,g:n,b:s}=e.color,o=F(t,n,s),r=e.opacity!==void 0&&e.opacity<1?` (${Math.round(e.opacity*100)}%)`:"";return o+r}return e.type==="IMAGE"?"Image fill":e.type==="VIDEO"?"Video fill":e.type.includes("GRADIENT")?`${e.type.replace("GRADIENT_","").toLowerCase()} gradient`:e.type}function Ue(e,t){try{if("boundVariables"in e){let n=e.boundVariables;if(n&&n[t])return!0}}catch(n){}return!1}function Be(e,t,n){if(!("fills"in e))return;let s=e.fills;if(s===figma.mixed||!Array.isArray(s))return;let o=s.filter(r=>r.visible!==!1);if(o.length!==0&&!Ue(e,"fills")){if("fillStyleId"in e){let r=e.fillStyleId;if(r&&r!==""&&r!==figma.mixed)return}for(let r of o){try{let a=r.boundVariables;if(a&&a.color)continue}catch(a){}let i=As(r);t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"fill",message:`Missing fill style: ${i}`,value:i,path:n})}}}function vt(e,t,n){if(!("strokes"in e))return;let s=e.strokes;if(!Array.isArray(s))return;let o=s.filter(r=>r.visible!==!1);if(o.length!==0&&!Ue(e,"strokes")){if("strokeStyleId"in e){let r=e.strokeStyleId;if(r&&r!==""&&r!==figma.mixed)return}for(let r of o){let i=As(r),a="strokeWeight"in e?` (${e.strokeWeight}px)`:"";t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"stroke",message:`Missing stroke style: ${i}${a}`,value:i+a,path:n})}}}function St(e,t,n){if(!("effects"in e))return;let s=e.effects;if(!Array.isArray(s)||s.length===0)return;let o=s.filter(i=>i.visible!==!1);if(o.length===0)return;if("effectStyleId"in e){let i=e.effectStyleId;if(i&&i!==""&&i!==figma.mixed)return}let r=o.map(i=>{let a=[i.type.replace(/_/g," ").toLowerCase()];if("radius"in i&&a.push(`r:${i.radius}`),"color"in i&&i.color){let c=i.color;a.push(F(c.r,c.g,c.b))}return a.join(" ")});t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"effect",message:`Missing effect style: ${r.join(", ")}`,value:r.join(", "),path:n})}function pi(e,t,n){if("textStyleId"in e){let a=e.textStyleId;if(a&&a!==""&&a!==figma.mixed)return}let s=e.fontName!==figma.mixed?e.fontName:null,o=e.fontSize!==figma.mixed?e.fontSize:null,r=[];s&&r.push(`${s.family} ${s.style}`),o&&r.push(`${o}px`);let i=r.join(" / ")||"unknown text style";t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"text",message:`Missing text style: ${i}`,value:i,path:n})}function xs(e,t,n,s){if(!("cornerRadius"in e)||Ue(e,"topLeftRadius")||Ue(e,"cornerRadius"))return;let o=e.cornerRadius;if(o===figma.mixed){let r=[e.topLeftRadius,e.topRightRadius,e.bottomLeftRadius,e.bottomRightRadius].filter(i=>i!=null);for(let i of r)if(!s.includes(i)){t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"radius",message:`Non-standard border radius: ${i}px (allowed: ${s.join(", ")})`,value:`${i}px`,path:n});break}return}typeof o=="number"&&o>0&&!s.includes(o)&&t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"radius",message:`Non-standard border radius: ${o}px (allowed: ${s.join(", ")})`,value:`${o}px`,path:n})}function mi(e,t,n,s){if(!(e.type==="GROUP"||e.type==="SLICE"||e.type==="CONNECTOR")&&e.type!=="COMPONENT_SET")switch(e.type){case"TEXT":t.checkTextStyles&&pi(e,n,s),t.checkFills&&Be(e,n,s);break;case"FRAME":case"SECTION":t.checkFills&&Be(e,n,s),t.checkStrokes&&vt(e,n,s),t.checkEffects&&St(e,n,s),t.checkRadius&&xs(e,n,s,t.allowedRadii);break;case"RECTANGLE":case"COMPONENT":case"INSTANCE":t.checkFills&&Be(e,n,s),t.checkStrokes&&vt(e,n,s),t.checkEffects&&St(e,n,s),t.checkRadius&&xs(e,n,s,t.allowedRadii);break;case"ELLIPSE":case"POLYGON":case"STAR":case"VECTOR":case"LINE":case"BOOLEAN_OPERATION":t.checkFills&&Be(e,n,s),t.checkStrokes&&vt(e,n,s),t.checkEffects&&St(e,n,s);break}}function Es(e,t,n,s,o){let r=0,i=o||"locked"in e&&e.locked,a="visible"in e&&!e.visible;if(t.skipLockedLayers&&i||t.skipHiddenLayers&&a)return 0;let c=s?`${s} > ${e.name}`:e.name;if(r++,!B.has(e.id)){let d=n.length;mi(e,t,n,c);for(let l=n.length-1;l>=d;l--){let p=n[l];(D.has(U(p.nodeId,p.errorType))||D.has(U(p.nodeId,p.errorType,p.value)))&&n.splice(l,1)}}if("children"in e)for(let d of e.children)r+=Es(d,t,n,c,i);return r}function q(e,t){for(let n of t)if(new RegExp("^"+n.replace(/[.+^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*").replace(/\?/g,".")+"$").test(e))return!0;return!1}function ae(e,t=j){var u,g;let n=[],s=0,o=t.ignorePatterns||[],r=t.severityOverrides||{};for(let f of e)s+=Es(f,t,n,"",!1);let i={skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers,scale:t.spacingScale};if(t.checkSpacing&&r.spacing!=="off"){let f=yn(e,i);for(let m of f.issues){let h=m.currentValue||"";B.has(m.nodeId)||D.has(U(m.nodeId,"spacing"))||D.has(U(m.nodeId,"spacing",h))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"spacing",message:m.message,value:h,path:m.nodeName,property:(g=(u=m.fixAction)==null?void 0:u.params)==null?void 0:g.property})}}if(t.checkAutoLayout&&r.autoLayout!=="off"){let f=Sn(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||D.has(U(m.nodeId,"autoLayout"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"autoLayout",message:m.message,value:m.currentValue||"",path:m.nodeName})}if(t.checkAccessibility&&r.accessibility!=="off"){let f=Cn(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||D.has(U(m.nodeId,"accessibility"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"accessibility",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkVisualQuality&&r.visualQuality!=="off"){let f=Pn(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||D.has(U(m.nodeId,"visualQuality"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"visualQuality",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkMicrocopy&&r.microcopy!=="off"){let f=Fn(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||D.has(U(m.nodeId,"microcopy"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"TEXT",errorType:"microcopy",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkConversion&&r.conversion!=="off"){let f=Hn(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||D.has(U(m.nodeId,"conversion"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"conversion",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkCognitive&&r.cognitive!=="off"){let f=os(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||D.has(U(m.nodeId,"cognitive"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"cognitive",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkFittsLaw&&r.fittsLaw!=="off"){let f=cs(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||D.has(U(m.nodeId,"fittsLaw"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"fittsLaw",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkGestalt&&r.gestalt!=="off"){let f=ps(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||D.has(U(m.nodeId,"gestalt"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"gestalt",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkDetachedInstances&&r.detachedInstance!=="off"){let f=ys(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||D.has(U(m.nodeId,"detachedInstance"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"detachedInstance",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkResponsive&&r.responsive!=="off"){let f=Cs(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||D.has(U(m.nodeId,"responsive"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"responsive",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}let a=n.filter(f=>r[f.errorType]!=="off"),c=o.length>0?a.filter(f=>!q(f.nodeName,o)):a;for(let f of c){let m=r[f.errorType];if(m&&m!=="off")f.severity=m;else if(!f.severity)switch(f.errorType){case"fill":case"stroke":case"effect":case"text":case"spacing":f.severity="warning";break;case"radius":case"autoLayout":f.severity="info";break;case"accessibility":f.severity="critical";break;case"visualQuality":f.severity="warning";break;case"microcopy":f.severity="info";break;case"conversion":f.severity="warning";break;case"cognitive":f.severity="info";break;case"responsive":f.severity="warning";break;case"fittsLaw":f.severity="warning";break;case"gestalt":f.severity="info";break;case"detachedInstance":f.severity="warning";break}}let d=new Set(c.map(f=>f.nodeId)).size,l={fill:0,stroke:0,effect:0,text:0,radius:0,spacing:0,autoLayout:0,accessibility:0,visualQuality:0,microcopy:0,conversion:0,cognitive:0,fittsLaw:0,gestalt:0,detachedInstance:0,responsive:0};for(let f of c)l[f.errorType]++;let p={totalErrors:c.length,byType:l,totalNodes:s,nodesWithErrors:d};return{errors:c,ignoredNodeIds:Array.from(B),ignoredErrorKeys:Array.from(D),summary:p}}function xe(e){let t=figma.currentPage.selection;return t.length===0?{errors:[],ignoredNodeIds:[],ignoredErrorKeys:[],summary:{totalErrors:0,byType:{fill:0,stroke:0,effect:0,text:0,radius:0,spacing:0,autoLayout:0,accessibility:0,visualQuality:0,microcopy:0,conversion:0,cognitive:0,fittsLaw:0,gestalt:0,detachedInstance:0,responsive:0},totalNodes:0,nodesWithErrors:0}}:ae(t,e)}function At(e,t,n,s=j){return ae(e,s).errors.filter(r=>r.errorType===t&&r.value===n)}var j,B,D,be=G(()=>{"use strict";J();hn();kn();In();Ln();Dn();Kn();rs();ls();ms();hs();Is();j={checkFills:!0,checkStrokes:!0,checkEffects:!0,checkTextStyles:!0,checkRadius:!0,checkSpacing:!0,checkAutoLayout:!0,checkAccessibility:!0,checkVisualQuality:!0,checkMicrocopy:!0,checkConversion:!0,checkCognitive:!0,checkFittsLaw:!0,checkGestalt:!0,checkDetachedInstances:!0,checkResponsive:!0,allowedRadii:[0,2,4,8,12,16,24,32],skipLockedLayers:!0,skipHiddenLayers:!0},B=new Set,D=new Set});var js=To((Bl,We)=>{var Bt=function(){var e=String.fromCharCode,t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",s={};function o(i,a){if(!s[i]){s[i]={};for(var c=0;c<i.length;c++)s[i][i.charAt(c)]=c}return s[i][a]}var r={compressToBase64:function(i){if(i==null)return"";var a=r._compress(i,6,function(c){return t.charAt(c)});switch(a.length%4){default:case 0:return a;case 1:return a+"===";case 2:return a+"==";case 3:return a+"="}},decompressFromBase64:function(i){return i==null?"":i==""?null:r._decompress(i.length,32,function(a){return o(t,i.charAt(a))})},compressToUTF16:function(i){return i==null?"":r._compress(i,15,function(a){return e(a+32)})+" "},decompressFromUTF16:function(i){return i==null?"":i==""?null:r._decompress(i.length,16384,function(a){return i.charCodeAt(a)-32})},compressToUint8Array:function(i){for(var a=r.compress(i),c=new Uint8Array(a.length*2),d=0,l=a.length;d<l;d++){var p=a.charCodeAt(d);c[d*2]=p>>>8,c[d*2+1]=p%256}return c},decompressFromUint8Array:function(i){if(i==null)return r.decompress(i);for(var a=new Array(i.length/2),c=0,d=a.length;c<d;c++)a[c]=i[c*2]*256+i[c*2+1];var l=[];return a.forEach(function(p){l.push(e(p))}),r.decompress(l.join(""))},compressToEncodedURIComponent:function(i){return i==null?"":r._compress(i,6,function(a){return n.charAt(a)})},decompressFromEncodedURIComponent:function(i){return i==null?"":i==""?null:(i=i.replace(/ /g,"+"),r._decompress(i.length,32,function(a){return o(n,i.charAt(a))}))},compress:function(i){return r._compress(i,16,function(a){return e(a)})},_compress:function(i,a,c){if(i==null)return"";var d,l,p={},u={},g="",f="",m="",h=2,C=3,S=2,N=[],y=0,b=0,I;for(I=0;I<i.length;I+=1)if(g=i.charAt(I),Object.prototype.hasOwnProperty.call(p,g)||(p[g]=C++,u[g]=!0),f=m+g,Object.prototype.hasOwnProperty.call(p,f))m=f;else{if(Object.prototype.hasOwnProperty.call(u,m)){if(m.charCodeAt(0)<256){for(d=0;d<S;d++)y=y<<1,b==a-1?(b=0,N.push(c(y)),y=0):b++;for(l=m.charCodeAt(0),d=0;d<8;d++)y=y<<1|l&1,b==a-1?(b=0,N.push(c(y)),y=0):b++,l=l>>1}else{for(l=1,d=0;d<S;d++)y=y<<1|l,b==a-1?(b=0,N.push(c(y)),y=0):b++,l=0;for(l=m.charCodeAt(0),d=0;d<16;d++)y=y<<1|l&1,b==a-1?(b=0,N.push(c(y)),y=0):b++,l=l>>1}h--,h==0&&(h=Math.pow(2,S),S++),delete u[m]}else for(l=p[m],d=0;d<S;d++)y=y<<1|l&1,b==a-1?(b=0,N.push(c(y)),y=0):b++,l=l>>1;h--,h==0&&(h=Math.pow(2,S),S++),p[f]=C++,m=String(g)}if(m!==""){if(Object.prototype.hasOwnProperty.call(u,m)){if(m.charCodeAt(0)<256){for(d=0;d<S;d++)y=y<<1,b==a-1?(b=0,N.push(c(y)),y=0):b++;for(l=m.charCodeAt(0),d=0;d<8;d++)y=y<<1|l&1,b==a-1?(b=0,N.push(c(y)),y=0):b++,l=l>>1}else{for(l=1,d=0;d<S;d++)y=y<<1|l,b==a-1?(b=0,N.push(c(y)),y=0):b++,l=0;for(l=m.charCodeAt(0),d=0;d<16;d++)y=y<<1|l&1,b==a-1?(b=0,N.push(c(y)),y=0):b++,l=l>>1}h--,h==0&&(h=Math.pow(2,S),S++),delete u[m]}else for(l=p[m],d=0;d<S;d++)y=y<<1|l&1,b==a-1?(b=0,N.push(c(y)),y=0):b++,l=l>>1;h--,h==0&&(h=Math.pow(2,S),S++)}for(l=2,d=0;d<S;d++)y=y<<1|l&1,b==a-1?(b=0,N.push(c(y)),y=0):b++,l=l>>1;for(;;)if(y=y<<1,b==a-1){N.push(c(y));break}else b++;return N.join("")},decompress:function(i){return i==null?"":i==""?null:r._decompress(i.length,32768,function(a){return i.charCodeAt(a)})},_decompress:function(i,a,c){var d=[],l,p=4,u=4,g=3,f="",m=[],h,C,S,N,y,b,I,w={val:c(0),position:a,index:1};for(h=0;h<3;h+=1)d[h]=h;for(S=0,y=Math.pow(2,2),b=1;b!=y;)N=w.val&w.position,w.position>>=1,w.position==0&&(w.position=a,w.val=c(w.index++)),S|=(N>0?1:0)*b,b<<=1;switch(l=S){case 0:for(S=0,y=Math.pow(2,8),b=1;b!=y;)N=w.val&w.position,w.position>>=1,w.position==0&&(w.position=a,w.val=c(w.index++)),S|=(N>0?1:0)*b,b<<=1;I=e(S);break;case 1:for(S=0,y=Math.pow(2,16),b=1;b!=y;)N=w.val&w.position,w.position>>=1,w.position==0&&(w.position=a,w.val=c(w.index++)),S|=(N>0?1:0)*b,b<<=1;I=e(S);break;case 2:return""}for(d[3]=I,C=I,m.push(I);;){if(w.index>i)return"";for(S=0,y=Math.pow(2,g),b=1;b!=y;)N=w.val&w.position,w.position>>=1,w.position==0&&(w.position=a,w.val=c(w.index++)),S|=(N>0?1:0)*b,b<<=1;switch(I=S){case 0:for(S=0,y=Math.pow(2,8),b=1;b!=y;)N=w.val&w.position,w.position>>=1,w.position==0&&(w.position=a,w.val=c(w.index++)),S|=(N>0?1:0)*b,b<<=1;d[u++]=e(S),I=u-1,p--;break;case 1:for(S=0,y=Math.pow(2,16),b=1;b!=y;)N=w.val&w.position,w.position>>=1,w.position==0&&(w.position=a,w.val=c(w.index++)),S|=(N>0?1:0)*b,b<<=1;d[u++]=e(S),I=u-1,p--;break;case 2:return m.join("")}if(p==0&&(p=Math.pow(2,g),g++),d[I])f=d[I];else if(I===u)f=C+C.charAt(0);else return null;m.push(f),d[u++]=C+f.charAt(0),p--,C=f,p==0&&(p=Math.pow(2,g),g++)}}};return r}();typeof define=="function"&&define.amd?define(function(){return Bt}):typeof We!="undefined"&&We!=null?We.exports=Bt:typeof angular!="undefined"&&angular!=null&&angular.module("LZString",[]).factory("LZString",function(){return Bt})});var so={};en(so,{applyEffectStyle:()=>Kt,applyFillStyle:()=>zt,applyStrokeStyle:()=>Wt,applyTextStyle:()=>Ht});async function zt(e,t){let n=figma.getNodeById(e);if(!n||!("fillStyleId"in n))return{success:!1,nodeId:e,nodeName:"",property:"fillStyle",oldValue:"",newValue:"",error:"Node not found or does not support fill styles"};try{let s=await figma.importStyleByKeyAsync(t),o=n.fillStyleId||"";return n.fillStyleId=s.id,{success:!0,nodeId:e,nodeName:n.name,property:"fillStyle",oldValue:o?"existing style":"no style",newValue:s.name}}catch(s){return{success:!1,nodeId:e,nodeName:n.name,property:"fillStyle",oldValue:"",newValue:t,error:s instanceof Error?s.message:String(s)}}}async function Wt(e,t){let n=figma.getNodeById(e);if(!n||!("strokeStyleId"in n))return{success:!1,nodeId:e,nodeName:"",property:"strokeStyle",oldValue:"",newValue:"",error:"Node not found or does not support stroke styles"};try{let s=await figma.importStyleByKeyAsync(t),o=n.strokeStyleId||"";return n.strokeStyleId=s.id,{success:!0,nodeId:e,nodeName:n.name,property:"strokeStyle",oldValue:o?"existing style":"no style",newValue:s.name}}catch(s){return{success:!1,nodeId:e,nodeName:n.name,property:"strokeStyle",oldValue:"",newValue:t,error:s instanceof Error?s.message:String(s)}}}async function Ht(e,t){let n=figma.getNodeById(e);if(!n||n.type!=="TEXT")return{success:!1,nodeId:e,nodeName:"",property:"textStyle",oldValue:"",newValue:"",error:"Node not found or is not a text node"};try{let s=await figma.importStyleByKeyAsync(t),o=n,r=o.textStyleId||"";return o.textStyleId=s.id,{success:!0,nodeId:e,nodeName:n.name,property:"textStyle",oldValue:r?"existing style":"no style",newValue:s.name}}catch(s){return{success:!1,nodeId:e,nodeName:n.name,property:"textStyle",oldValue:"",newValue:t,error:s instanceof Error?s.message:String(s)}}}async function Kt(e,t){let n=figma.getNodeById(e);if(!n||!("effectStyleId"in n))return{success:!1,nodeId:e,nodeName:"",property:"effectStyle",oldValue:"",newValue:"",error:"Node not found or does not support effect styles"};try{let s=await figma.importStyleByKeyAsync(t),o=n.effectStyleId||"";return n.effectStyleId=s.id,{success:!0,nodeId:e,nodeName:n.name,property:"effectStyle",oldValue:o?"existing style":"no style",newValue:s.name}}catch(s){return{success:!1,nodeId:e,nodeName:n.name,property:"effectStyle",oldValue:"",newValue:t,error:s instanceof Error?s.message:String(s)}}}var jt=G(()=>{"use strict"});J();J();J();function de(e){var d;let t=e,n=!1;for(;t;){if(t.type==="COMPONENT_SET"){n=!0;break}if(t.parent&&t.parent.type==="COMPONENT_SET"){n=!0;break}t=t.parent}if(!n||!("strokes"in e)||!("cornerRadius"in e)||!("strokeWeight"in e))return!1;let s=e.cornerRadius===5||"topLeftRadius"in e&&"topRightRadius"in e&&"bottomLeftRadius"in e&&"bottomRightRadius"in e&&e.topLeftRadius===5&&e.topRightRadius===5&&e.bottomLeftRadius===5&&e.bottomRightRadius===5,o=e.strokeWeight===1,r=e.strokes,i=r.length>0&&r.some(l=>l.type==="SOLID"&&l.visible!==!1&&l.color?F(l.color.r,l.color.g,l.color.b).toUpperCase()==="#9747FF":!1),a="paddingLeft"in e&&"paddingRight"in e&&"paddingTop"in e&&"paddingBottom"in e&&e.paddingLeft===16&&e.paddingRight===16&&e.paddingTop===16&&e.paddingBottom===16,c=s&&o&&i&&a;return c&&(console.log(`\u{1F3AF} [FILTER] Detected default variant frame styles in ${e.name} - filtering out`),console.log(` Type: ${e.type}, Parent: ${(d=e.parent)==null?void 0:d.type}`),console.log(` Radius: ${String(e.cornerRadius)}, Weight: ${String(e.strokeWeight)}, Color: ${r.length>0&&r[0].type==="SOLID"?F(r[0].color.r,r[0].color.g,r[0].color.b):"none"}`),console.log(` Padding: L=${e.paddingLeft}, R=${e.paddingRight}, T=${e.paddingTop}, B=${e.paddingBottom}`)),c}function we(e){let t=e;for(;t;){if(t.type==="COMPONENT_SET"||t.parent&&t.parent.type==="COMPONENT_SET")return!0;t=t.parent}return!1}async function ue(e){let t=[],n=[],s=[],o=[],r=[],i=new Set,a=new Set,c=new Set,d=new Set,l=new Set;async function p(u){console.log("\u{1F50D} Analyzing node:",u.name,"Type:",u.type);let g=[];if("fillStyleId"in u&&typeof u.fillStyleId=="string"&&g.push(figma.getStyleByIdAsync(u.fillStyleId).then(y=>{if(y!=null&&y.name&&!i.has(y.name)){i.add(y.name);let b=y.name;if("fills"in u&&Array.isArray(u.fills)&&u.fills.length>0){let I=u.fills[0];I.type==="SOLID"&&I.color&&(b=F(I.color.r,I.color.g,I.color.b))}t.push({name:y.name,value:b,type:"fill-style",isToken:!0,isActualToken:!0,source:"figma-style"})}}).catch(console.warn)),"strokeStyleId"in u&&typeof u.strokeStyleId=="string"&&g.push(figma.getStyleByIdAsync(u.strokeStyleId).then(y=>{y!=null&&y.name&&!i.has(y.name)&&(i.add(y.name),t.push({name:y.name,value:y.name,type:"stroke-style",isToken:!0,isActualToken:!0,source:"figma-style"}))}).catch(console.warn)),u.type==="TEXT"&&"textStyleId"in u&&typeof u.textStyleId=="string"&&g.push(figma.getStyleByIdAsync(u.textStyleId).then(y=>{y!=null&&y.name&&!c.has(y.name)&&(c.add(y.name),s.push({name:y.name,value:y.name,type:"text-style",isToken:!0,isActualToken:!0,source:"figma-style"}))}).catch(console.warn)),"effectStyleId"in u&&typeof u.effectStyleId=="string"&&g.push(figma.getStyleByIdAsync(u.effectStyleId).then(y=>{y!=null&&y.name&&!d.has(y.name)&&(d.add(y.name),o.push({name:y.name,value:y.name,type:"effect-style",isToken:!0,isActualToken:!0,source:"figma-style"}))}).catch(console.warn)),await Promise.all(g),"boundVariables"in u&&u.boundVariables){let y=u.boundVariables;console.log(`\u{1F50D} [VARIABLES] Checking bound variables for ${u.name}:`,Object.keys(y));let b=async(x,$,v,T,L)=>{try{let A=Array.isArray(x)?x:[x];for(let ee of A)if(ee!=null&&ee.id&&typeof ee.id=="string"){let Y=await Qe(ee.id);if(console.log(` \u{1F3AF} Found ${$} variable:`,Y),Y&&!v.has(Y)){v.add(Y);let ve=Y;if(L==="color"&&($==="fills"||$==="strokes")){let $e=await tn(ee.id,u);$e&&$e.startsWith("#")&&(ve=$e)}T.push({name:Y,value:ve,type:`${$}-variable`,isToken:!0,isActualToken:!0,source:"figma-variable"}),console.log(` \u2705 Added ${L} token: ${Y} (value: ${ve})`)}}}catch(A){console.warn(`Error processing ${$} variables:`,A)}},I=async(x,$,v,T,L)=>{if(x&&typeof x=="object"&&"id"in x&&typeof x.id=="string"){let A=await Qe(x.id);console.log(` \u{1F3AF} Found ${$} variable:`,A),A&&!v.has(A)&&(v.add(A),T.push({name:A,value:A,type:`${$}-variable`,isToken:!0,isActualToken:!0,source:"figma-variable"}),console.log(` \u2705 Added ${L} token: ${A}`))}},w=[];y.fills&&(console.log(" \u{1F3A8} Processing fills variables..."),w.push(b(y.fills,"fills",i,t,"color"))),y.strokes&&(console.log(" \u{1F58A}\uFE0F Processing strokes variables..."),w.push(b(y.strokes,"strokes",i,t,"color"))),y.effects&&(console.log(" \u2728 Processing effects variables..."),w.push(b(y.effects,"effects",d,o,"effect"))),["strokeWeight","strokeTopWeight","strokeRightWeight","strokeBottomWeight","strokeLeftWeight"].forEach(x=>{y[x]&&(console.log(` \u{1F4CF} Processing ${x} variable...`),w.push(I(y[x],x,l,r,"border")))}),["topLeftRadius","topRightRadius","bottomLeftRadius","bottomRightRadius"].forEach(x=>{y[x]&&(console.log(` \u{1F504} Processing ${x} variable...`),w.push(I(y[x],x,l,r,"border")))}),["paddingTop","paddingRight","paddingBottom","paddingLeft","itemSpacing","counterAxisSpacing"].forEach(x=>{y[x]&&(console.log(` \u{1F4D0} Processing ${x} variable...`),w.push(I(y[x],x,a,n,"spacing")))}),["width","height","minWidth","maxWidth","minHeight","maxHeight"].forEach(x=>{y[x]&&(console.log(` \u{1F4E6} Processing ${x} variable...`),w.push(I(y[x],x,a,n,"size")))}),y.opacity&&(console.log(" \u{1F47B} Processing opacity variable..."),w.push(I(y.opacity,"opacity",d,o,"effect"))),u.type==="TEXT"&&["fontSize","lineHeight","letterSpacing","paragraphSpacing"].forEach($=>{y[$]&&(console.log(` \u{1F4DD} Processing ${$} variable...`),w.push(I(y[$],$,c,s,"typography")))}),await Promise.all(w),console.log(`\u{1F50D} [VARIABLES] Total variables found for ${u.name}: ${Object.keys(y).length}`)}let f="boundVariables"in u&&u.boundVariables&&u.boundVariables.fills,m="fillStyleId"in u&&u.fillStyleId;"fills"in u&&Array.isArray(u.fills)&&!m&&!f?(console.log(`\u{1F50D} [HARD-CODED] Checking fills for ${u.name} (no variables, no style)`),u.fills.forEach(y=>{if(y.type==="SOLID"&&y.visible!==!1&&y.color){let b=F(y.color.r,y.color.g,y.color.b),I=`${b}:${u.id}`;if(!i.has(I)){console.log(` \u26A0\uFE0F Found hard-coded fill: ${b}`),i.add(I);let w=oe(u);t.push({name:`hard-coded-fill-${t.length+1}`,value:b,type:"fill",isToken:!1,source:"hard-coded",context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,path:w.path,description:w.description,property:"fills"}})}}})):f?console.log(`\u{1F50D} [VARIABLES] ${u.name} has fill variables - skipping hard-coded detection`):m&&console.log(`\u{1F50D} [STYLES] ${u.name} has fill style - skipping hard-coded detection`);let h="boundVariables"in u&&u.boundVariables&&u.boundVariables.strokes,C="strokeStyleId"in u&&u.strokeStyleId;if("strokes"in u&&Array.isArray(u.strokes)&&!C&&!h?(console.log(`\u{1F50D} [HARD-CODED] Checking strokes for ${u.name} (no variables, no style)`),de(u)?console.log(" \u{1F6AB} Skipping default variant frame stroke colors"):u.strokes.forEach(y=>{if(y.type==="SOLID"&&y.visible!==!1&&y.color){let b=F(y.color.r,y.color.g,y.color.b),I=`${b}:${u.id}`;if(!i.has(I)){console.log(` \u26A0\uFE0F Found hard-coded stroke: ${b}`),i.add(I);let w=oe(u);t.push({name:`hard-coded-stroke-${t.length+1}`,value:b,type:"stroke",isToken:!1,source:"hard-coded",isDefaultVariantStyle:b.toUpperCase()==="#9747FF"&&we(u),context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,path:w.path,description:w.description,property:"strokes"}})}}})):h?console.log(`\u{1F50D} [VARIABLES] ${u.name} has stroke variables - skipping hard-coded detection`):C&&console.log(`\u{1F50D} [STYLES] ${u.name} has stroke style - skipping hard-coded detection`),"strokeWeight"in u&&typeof u.strokeWeight=="number"){console.log(`\u{1F50D} Node ${u.name} has strokeWeight: ${u.strokeWeight}`);let y="strokes"in u&&Array.isArray(u.strokes)&&u.strokes.length>0,b=y&&u.strokes.some(P=>P.visible!==!1),I="boundVariables"in u&&u.boundVariables&&["strokeWeight","strokeTopWeight","strokeRightWeight","strokeBottomWeight","strokeLeftWeight"].some(P=>u.boundVariables[P]),w="boundVariables"in u&&u.boundVariables?Object.keys(u.boundVariables):[];if(console.log(` Has strokes: ${y}, Has visible strokes: ${b}, Has strokeWeight variable: ${!!I}, boundVariable keys: [${w.join(", ")}]`),I)console.log(` \u{1F517} ${u.name} has strokeWeight bound to variable - skipping hard-coded detection`);else if(u.strokeWeight>0&&b&&!de(u)){let P=`${u.strokeWeight}px`,O,M=u.strokes.find(x=>x.visible!==!1&&x.type==="SOLID");M&&M.type==="SOLID"&&M.color&&(O=F(M.color.r,M.color.g,M.color.b));let z=`${P}:${u.id}`;if(!l.has(z)){console.log(` \u2705 Adding stroke weight: ${P}`),l.add(z);let x=oe(u);r.push({name:`hard-coded-stroke-weight-${u.strokeWeight}`,value:P,type:"stroke-weight",isToken:!1,source:"hard-coded",strokeColor:O,isDefaultVariantStyle:u.strokeWeight===1&&(O==null?void 0:O.toUpperCase())==="#9747FF"&&we(u),context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,hasVisibleStroke:!0,path:x.path,description:x.description,property:"strokeWeight"}})}}else u.strokeWeight>0&&b&&de(u)&&console.log(" \u{1F6AB} Skipping default variant frame stroke weight")}let S="boundVariables"in u&&u.boundVariables&&["topLeftRadius","topRightRadius","bottomLeftRadius","bottomRightRadius","cornerRadius"].some(y=>u.boundVariables[y]);if("cornerRadius"in u&&typeof u.cornerRadius=="number"&&!S)if(console.log(`\u{1F50D} [HARD-CODED] Checking corner radius for ${u.name} (no variables)`),de(u))console.log(" \u{1F6AB} Skipping default variant frame corner radius");else{let y=u.cornerRadius;if(y>0){let b=`${y}px`,I=`${b}:${u.id}`;if(!l.has(I)){console.log(` \u26A0\uFE0F Found hard-coded corner radius: ${b}`),l.add(I);let w=oe(u);r.push({name:`hard-coded-corner-radius-${y}`,value:b,type:"corner-radius",isToken:!1,source:"hard-coded",isDefaultVariantStyle:y===5&&we(u),context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,path:w.path,description:w.description,property:"cornerRadius"}})}}}else S&&console.log(`\u{1F50D} [VARIABLES] ${u.name} has radius variables - skipping hard-coded detection`);!S&&"topLeftRadius"in u&&(console.log(`\u{1F50D} [HARD-CODED] Checking individual corner radius for ${u.name} (no variables)`),de(u)?console.log(" \u{1F6AB} Skipping default variant frame individual corner radii"):[{prop:"topLeftRadius",name:"top-left"},{prop:"topRightRadius",name:"top-right"},{prop:"bottomLeftRadius",name:"bottom-left"},{prop:"bottomRightRadius",name:"bottom-right"}].forEach(({prop:b,name:I})=>{if(b in u&&typeof u[b]=="number"){let w=u[b];if(w>0){let P=`${w}px`,O=`${P}:${u.id}:${b}`;if(!l.has(O)){console.log(` \u26A0\uFE0F Found hard-coded ${I} radius: ${P}`),l.add(O);let M=oe(u);r.push({name:`hard-coded-${I}-radius-${w}`,value:P,type:`${I}-radius`,isToken:!1,source:"hard-coded",isDefaultVariantStyle:w===5&&we(u),context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,path:M.path,description:M.description,property:b}})}}}}));let N="boundVariables"in u&&u.boundVariables&&["paddingTop","paddingRight","paddingBottom","paddingLeft"].some(y=>u.boundVariables[y]);if("paddingLeft"in u&&typeof u.paddingLeft=="number"&&!N){console.log(`\u{1F50D} [HARD-CODED] Checking padding for ${u.name} (no variables)`);let y=u;[{value:y.paddingLeft,name:"left"},{value:y.paddingRight,name:"right"},{value:y.paddingTop,name:"top"},{value:y.paddingBottom,name:"bottom"}].forEach(I=>{let w=`${I.value}:${u.id}:${I.name}`;if(typeof I.value=="number"&&I.value>1&&!a.has(w)){console.log(` \u26A0\uFE0F Found hard-coded padding-${I.name}: ${I.value}px`),a.add(w);let P=oe(u),O=I.value===16&&we(u)&&de(u);n.push({name:`hard-coded-padding-${I.name}-${I.value}`,value:`${I.value}px`,type:"padding",isToken:!1,source:"hard-coded",isDefaultVariantStyle:O,context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,path:P.path,description:P.description,property:`padding${I.name.charAt(0).toUpperCase()+I.name.slice(1)}`}})}})}else N&&console.log(`\u{1F50D} [VARIABLES] ${u.name} has padding variables - skipping hard-coded detection`);if("children"in u)for(let y of u.children)await p(y)}return await p(e),$o({colors:t,spacing:n,typography:s,effects:o,borders:r})}function $o(e){let t=["colors","spacing","typography","effects","borders"],n={totalTokens:0,actualTokens:0,hardCodedValues:0,aiSuggestions:0,byCategory:{}};return t.forEach(s=>{let o=e[s].map(c=>K(R({},c),{isActualToken:c.source==="figma-style"||c.source==="figma-variable",recommendation:Mo(c,s),suggestion:Oo(c,s)})),r=o.filter(c=>!c.isDefaultVariantStyle),i=r.filter(c=>c.isActualToken).length,a=r.filter(c=>c.source==="hard-coded").length;n.byCategory[s]={total:r.length,tokens:i,hardCoded:a,suggestions:0},n.totalTokens+=r.length,n.actualTokens+=i,n.hardCodedValues+=a,e[s]=o}),K(R({},e),{summary:n})}function Mo(e,t){if(e.isToken)return`Using ${e.name} token`;switch(t){case"colors":return`Consider using a color token instead of ${e.value}`;case"spacing":return`Consider using spacing token instead of ${e.value}`;case"typography":return"Consider using typography token";case"effects":return"Consider using effect token";case"borders":return"Consider using border radius token";default:return"Consider using a design token"}}function Oo(e,t){var n,s;switch(t){case"colors":return(n=e.value)!=null&&n.startsWith("#000")?"Use semantic color token (e.g., text.primary)":(s=e.value)!=null&&s.startsWith("#FFF")?"Use semantic color token (e.g., background.primary)":"Create or use existing color token";case"spacing":let o=parseInt(e.value||"0");return o%8===0?"Create or use existing spacing token (follows 8px grid)":o%4===0?"Create or use existing spacing token (follows 4px grid)":"Create or use existing spacing token";case"typography":return"Use semantic typography token (e.g., heading.large, body.regular)";case"effects":return"Use semantic shadow token (e.g., shadow.small, shadow.medium)";case"borders":return"Use appropriate radius token (e.g., radius.small, radius.medium)";default:return"Create or use existing design token"}}function nn(e){return`You are an expert design system architect analyzing a Figma component for comprehensive metadata and design token recommendations. +"use strict";(()=>{var wo=Object.create;var Me=Object.defineProperty,Co=Object.defineProperties,Io=Object.getOwnPropertyDescriptor,xo=Object.getOwnPropertyDescriptors,Ao=Object.getOwnPropertyNames,Yt=Object.getOwnPropertySymbols,Eo=Object.getPrototypeOf,Zt=Object.prototype.hasOwnProperty,To=Object.prototype.propertyIsEnumerable;var Qt=(e,t,n)=>t in e?Me(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,R=(e,t)=>{for(var n in t||(t={}))Zt.call(t,n)&&Qt(e,n,t[n]);if(Yt)for(var n of Yt(t))To.call(t,n)&&Qt(e,n,t[n]);return e},K=(e,t)=>Co(e,xo(t));var G=(e,t)=>()=>(e&&(t=e(e=0)),t);var Po=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),en=(e,t)=>{for(var n in t)Me(e,n,{get:t[n],enumerable:!0})},Lo=(e,t,n,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of Ao(t))!Zt.call(e,o)&&o!==n&&Me(e,o,{get:()=>t[o],enumerable:!(s=Io(t,o))||s.enumerable});return e};var Ro=(e,t,n)=>(n=e!=null?wo(Eo(e)):{},Lo(t||!e||!e.__esModule?Me(n,"default",{value:e,enumerable:!0}):n,e));function Se(e){return["FRAME","COMPONENT","COMPONENT_SET","INSTANCE","GROUP"].includes(e.type)?(e.type==="COMPONENT_SET",!0):!1}function F(e,t,n){let s=o=>{let r=Math.round(o*255).toString(16);return r.length===1?"0"+r:r};return`#${s(e)}${s(t)}${s(n)}`}async function Qe(e){try{let t=await figma.variables.getVariableByIdAsync(e);return t?t.name:null}catch(t){return console.warn("Could not access variable:",e,t),null}}async function tn(e,t){try{let n=await figma.variables.getVariableByIdAsync(e);if(!n)return null;if(t&&n.resolveForConsumer)try{let s=n.resolveForConsumer(t);if(s&&typeof s.value=="object"&&"r"in s.value){let o=s.value;return F(o.r,o.g,o.b)}else if(s&&s.value!==void 0)return String(s.value)}catch(s){console.warn("Could not resolve variable value:",s)}return n.name}catch(n){return console.warn("Could not access variable:",e,n),null}}function S(e,t){try{figma.ui.postMessage({type:e,data:t})}catch(n){console.error("Failed to send message to UI:",n)}}function Oe(e){let t=[e];if("children"in e)for(let n of e.children)t.push(...Oe(n));return t}function Ze(e){let t=[];if(e.type==="TEXT"){let n=e;n.characters&&t.push(n.characters)}if("children"in e)for(let n of e.children)t.push(...Ze(n));return t}function se(e,t,n){let[s,o,r]=[e,t,n].map(i=>i<=.03928?i/12.92:Math.pow((i+.055)/1.055,2.4));return .2126*s+.7152*o+.0722*r}function ke(e,t){let n=Math.max(e,t),s=Math.min(e,t);return(n+.05)/(s+.05)}function Ne(e){let t=e.parent;for(;t&&"type"in t;){let n=t;if("fills"in n){let s=n.fills;if(Array.isArray(s)){for(let o of s)if(o.type==="SOLID"&&o.visible!==!1&&o.color){if(o.boundVariables&&o.boundVariables.color)continue;return o.color}}}t=t.parent}return null}function $o(e){var s;let t=[],n=e;for(;n&&n.type!=="DOCUMENT"&&n.type!=="PAGE";)n.type==="COMPONENT"&&((s=n.parent)==null?void 0:s.type)==="COMPONENT_SET"?t.unshift(`${n.name}`):t.unshift(n.name),n=n.parent;return t.join(" \u2192 ")}function re(e){var s,o;let t=$o(e),n=`Found in "${e.name}"`;if(((s=e.parent)==null?void 0:s.type)==="COMPONENT_SET"||e.parent&&((o=e.parent.parent)==null?void 0:o.type)==="COMPONENT_SET")n=`Found in variant: "${e.name}"`;else if(t.includes("\u2192")){let r=t.split(" \u2192 ");r.length>1&&(n=`Found in "${r[r.length-1]}" (${r[r.length-2]})`)}return{path:t,description:n}}var J=G(()=>{"use strict"});function Ve(e,t=Ce){if(t.includes(e))return[];let n=[...t].map(o=>({v:o,diff:Math.abs(o-e)})).sort((o,r)=>o.diff-r.diff),s=[];for(let o of n){if(s.length>=2)break;s.includes(o.v)||s.push(o.v)}return s.sort((o,r)=>o-r)}var Ce,mt,ft=G(()=>{"use strict";Ce=[0,2,4,8,12,16,20,24,32,40,48,64,80,96],mt=Ce});function Zo(){return`spacing-${++fn}`}function er(e){return gt.includes(e)}function tr(e){return{itemSpacing:"Gap",paddingTop:"Padding Top",paddingBottom:"Padding Bottom",paddingLeft:"Padding Left",paddingRight:"Padding Right",counterAxisSpacing:"Counter-axis Gap"}[e]||e}function nr(e,t){var o;if(e.layoutMode==="NONE")return 0;let n=0,s=[{prop:"itemSpacing",value:e.itemSpacing},{prop:"paddingTop",value:e.paddingTop},{prop:"paddingBottom",value:e.paddingBottom},{prop:"paddingLeft",value:e.paddingLeft},{prop:"paddingRight",value:e.paddingRight}];"counterAxisSpacing"in e&&typeof e.counterAxisSpacing=="number"&&s.push({prop:"counterAxisSpacing",value:e.counterAxisSpacing});for(let{prop:r,value:i}of s)if(n++,!er(i)){let a=Ve(i,gt);t.push({id:Zo(),type:"spacing",severity:"warning",nodeId:e.id,nodeName:e.name,message:`${tr(r)} is ${i}px \u2014 not in spacing scale`,currentValue:`${i}px`,suggestions:a.map(c=>`${c}px`),autoFixable:!0,fixAction:{type:"fixSpacing",params:{nodeId:e.id,property:r,currentValue:i,suggestedValue:(o=a[0])!=null?o:i}}})}return n}function gn(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,passed:0};if(s&&i)return{checked:0,passed:0};let a=0,c=0;if(e.type==="FRAME"||e.type==="COMPONENT"||e.type==="INSTANCE"){let d=t.length,l=nr(e,t);a+=l,c+=l-(t.length-d)}if("children"in e)for(let d of e.children){let l=gn(d,t,n,s,r);a+=l.checked,c+=l.passed}return{checked:a,passed:c}}function yn(e,t={}){let{skipLocked:n=!0,skipHidden:s=!0,scale:o}=t;gt=o||Ce,fn=0;let r=[],i=0,a=0;for(let c of e){let{checked:d,passed:l}=gn(c,r,n,s,!1);i+=d,a+=l}return{issues:r,summary:{totalChecked:i,passed:a,failed:r.length}}}var fn,gt,hn=G(()=>{"use strict";ft();fn=0;gt=Ce});function sr(){return`autolayout-${++bn}`}function vn(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{totalFrames:0,withAutoLayout:0};if(s&&i)return{totalFrames:0,withAutoLayout:0};let a=0,c=0;if((e.type==="FRAME"||e.type==="COMPONENT"||e.type==="INSTANCE")&&"children"in e){let l=e.children;l.length>=2&&(a++,e.layoutMode!=="NONE"?c++:t.push({id:sr(),type:"autoLayout",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Frame "${e.name}" has ${l.length} children but no Auto Layout`,currentValue:"No Auto Layout",suggestions:["HORIZONTAL","VERTICAL"],autoFixable:!1}))}if("children"in e)for(let l of e.children){let p=vn(l,t,n,s,r);a+=p.totalFrames,c+=p.withAutoLayout}return{totalFrames:a,withAutoLayout:c}}function Sn(e,t={}){let{skipLocked:n=!0,skipHidden:s=!0}=t;bn=0;let o=[],r=0,i=0;for(let d of e){let l=vn(d,o,n,s,!1);r+=l.totalFrames,i+=l.withAutoLayout}let a=r-i,c=r>0?Math.round(i/r*100):100;return{issues:o,summary:{totalFrames:r,withAutoLayout:i,withoutAutoLayout:a,percentage:c}}}var bn,kn=G(()=>{"use strict";bn=0});function oe(){return`a11y-${++Nn}`}function yt(e){return or.test(e)}function ir(e,t){if(e.type!=="TEXT")return;let n=e,s=n.fills;if(s===figma.mixed||!Array.isArray(s))return;let o=s.find(m=>{var h;return m.type==="SOLID"&&m.visible!==!1&&m.color&&!((h=m.boundVariables)!=null&&h.color)});if(!o||o.type!=="SOLID")return;let r=Ne(e);if(!r)return;let i=o.color,a=se(i.r,i.g,i.b),c=se(r.r,r.g,r.b),d=ke(a,c),l=n.fontSize!==figma.mixed?n.fontSize:0,p=n.fontName!==figma.mixed?n.fontName.style:"",u=p.toLowerCase().includes("bold")||p.toLowerCase().includes("black"),g=l>=18||l>=14&&u,f=g?3:4.5;if(d<f){let m=d.toFixed(1);t.push({id:oe(),type:"accessibility",severity:"critical",nodeId:e.id,nodeName:e.name,message:`Contrast ratio ${m}:1 below WCAG AA ${g?"large text":""} minimum of ${f}:1`,currentValue:`${m}:1`,suggestions:[`Increase contrast to at least ${f}:1`],autoFixable:!1})}}function ar(e,t){if(e.type!=="FRAME"&&e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!yt(e.name))return;let n=e.width,s=e.height;(n<44||s<44)&&t.push({id:oe(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Touch target ${Math.round(n)}x${Math.round(s)}px is below 44x44px recommended minimum`,currentValue:`${Math.round(n)}x${Math.round(s)}px`,suggestions:["Increase to at least 44x44px"],autoFixable:!1})}function cr(e,t){if(e.type!=="TEXT")return;let s=e.fontSize;s===figma.mixed||typeof s!="number"||s>0&&s<12&&t.push({id:oe(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Text size ${s}px is below 12px readability minimum`,currentValue:`${s}px`,suggestions:["12px","14px"],autoFixable:!1})}function lr(e,t){if(e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!yt(e.name))return;let n=e;if(!("children"in n)||n.children.length===0)return;n.children.some(o=>{var r,i;return o.type==="TEXT"?!0:"children"in o?(i=(r=o.children)==null?void 0:r.some)==null?void 0:i.call(r,a=>a.type==="TEXT"):!1})||t.push({id:oe(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Interactive element "${e.name}" has no visible text label`,currentValue:"No text child",suggestions:["Add a text label or ensure screen reader label is provided"],autoFixable:!1})}function dr(e,t){e.type!=="FRAME"&&e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!("children"in e)||e.children.length===0||rr.test(e.name)&&t.push({id:oe(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`Generic layer name "${e.name}" \u2014 use a descriptive name`,currentValue:e.name,suggestions:["Rename to describe the layer purpose"],autoFixable:!1})}function ur(e,t){if(e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!yt(e.name))return;let n=e,s=null,o=n.strokes;if(Array.isArray(o)){let d=o.find(l=>l.type==="SOLID"&&l.visible!==!1);d&&d.type==="SOLID"&&(s=d.color)}if(!s){let d=n.fills;if(d!==figma.mixed&&Array.isArray(d)){let l=d.find(p=>p.type==="SOLID"&&p.visible!==!1);l&&l.type==="SOLID"&&(s=l.color)}}if(!s)return;let r=Ne(e);if(!r)return;let i=se(s.r,s.g,s.b),a=se(r.r,r.g,r.b),c=ke(i,a);c<3&&t.push({id:oe(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Non-text contrast ${c.toFixed(1)}:1 below WCAG 1.4.11 minimum of 3:1`,currentValue:`${c.toFixed(1)}:1`,suggestions:["Increase boundary contrast to at least 3:1 against background"],autoFixable:!1})}function mr(e,t){if(e.type!=="FRAME"&&e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!pr.test(e.name))return;let n=e;if(!("children"in n)||n.children.length===0)return;n.children.some(o=>{var r,i;return o.type==="TEXT"||/icon|svg|symbol|glyph/i.test(o.name)?!0:"children"in o?(i=(r=o.children)==null?void 0:r.some)==null?void 0:i.call(r,a=>a.type==="TEXT"||/icon|svg|symbol|glyph/i.test(a.name)):!1})||t.push({id:oe(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`"${e.name}" may rely on color alone to convey status (WCAG 1.4.1)`,currentValue:"No text or icon indicator",suggestions:["Add a text label or icon to supplement the color indicator"],autoFixable:!1})}function fr(e,t){if(e.type!=="COMPONENT")return;let n=e.parent;if(!n||n.type!=="COMPONENT_SET")return;let s=n,r=s.children.map(c=>c.name.toLowerCase()).join(" "),a=["hover","focus","disabled","pressed"].filter(c=>!r.includes(c));a.length>0&&t.push({id:oe(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`Component set missing states: ${a.join(", ")}`,currentValue:`${s.children.length} variants`,suggestions:a.map(c=>`Add ${c} variant`),autoFixable:!1})}function wn(e,t,n,s,o,r){var d;let i=o||"locked"in e&&e.locked,a="visible"in e&&!e.visible;if(n&&i||s&&a)return 0;let c=0;if(ir(e,t),ar(e,t),cr(e,t),c++,lr(e,t),dr(e,t),ur(e,t),mr(e,t),e.type==="COMPONENT"&&((d=e.parent)==null?void 0:d.type)==="COMPONENT_SET"){let l=e.parent.id;r.has(l)||(r.add(l),fr(e,t))}if("children"in e)for(let l of e.children)c+=wn(l,t,n,s,i,r);return c}function Cn(e,t={}){let{skipLocked:n=!0,skipHidden:s=!0}=t;Nn=0;let o=[],r=new Set,i=0;for(let a of e)i+=wn(a,o,n,s,!1,r);return{issues:o,summary:{totalChecked:i,contrastIssues:o.filter(a=>a.message.includes("Contrast")).length,touchTargetIssues:o.filter(a=>a.message.includes("Touch target")).length,textSizeIssues:o.filter(a=>a.message.includes("Text size")).length,namingIssues:o.filter(a=>a.message.includes("text label")||a.message.includes("Generic")).length,stateIssues:o.filter(a=>a.message.includes("missing states")).length,nonTextContrastIssues:o.filter(a=>a.message.includes("Non-text contrast")).length,colorOnlyIssues:o.filter(a=>a.message.includes("color alone")).length}}}var Nn,or,rr,pr,In=G(()=>{"use strict";J();Nn=0;or=/\b(button|btn|input|link|checkbox|toggle|switch|tab|radio|select|dropdown|menu-item|slider|chip)\b/i;rr=/^(Frame|Group|Rectangle|Ellipse|Vector|Line|Polygon|Star)\s*\d+$/i;pr=/\b(error|success|warning|status|alert|badge|danger|info)\b/i});function ge(){return`vq-${++xn}`}function yr(e){return gr.some(t=>t.includes(e))}function hr(e,t){let n=e.width*e.height;if(n===0)return;let o=("children"in e?e.children.filter(i=>i.visible!==!1):[]).length,r=o/n*1e3;r>3&&t.push({id:ge(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`High visual density: ${o} elements in ${Math.round(n/1e3)}k px\xB2 (${r.toFixed(2)}/1000px\xB2). Consider simplifying or using progressive disclosure.`,currentValue:`${r.toFixed(2)} elements/1000px\xB2`,suggestions:["Reduce visible elements to under 15 per viewport","Group related items","Use progressive disclosure"],autoFixable:!1})}function An(e,t,n,s,o){if(!(s&&"locked"in e&&e.locked)&&!(o&&"visible"in e&&!e.visible)){if(e.type==="TEXT"){let r=e,i=r.fontSize;if(i!==figma.mixed&&typeof i=="number"){t.add(i);let a=r.lineHeight;if(a!==figma.mixed&&typeof a=="object"&&a.unit==="PIXELS"){let c=a.value/i;n.push({fontSize:i,lineHeight:a.value,ratio:c})}}}if("children"in e)for(let r of e.children)An(r,t,n,s,o)}}function br(e,t,n,s){let o=new Set,r=[];An(e,o,r,n,s);let i=Array.from(o).sort((d,l)=>d-l),a=i.filter(d=>!yr(d));a.length>0&&t.push({id:ge(),type:"accessibility",severity:"info",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`Non-standard font sizes: ${a.join(", ")}px. Consider using a type scale (e.g., 12/14/16/20/24/32).`,currentValue:a.map(d=>`${d}px`).join(", "),suggestions:a.map(d=>{let l=[10,12,14,16,18,20,24,28,32,36,40,48].reduce((p,u)=>Math.abs(u-d)<Math.abs(p-d)?u:p);return`${d}px \u2192 ${l}px`}),autoFixable:!1});let c=r.filter(d=>d.ratio<1.2||d.ratio>2);if(c.length>0){let d=c.reduce((l,p)=>Math.abs(p.ratio-1.5)>Math.abs(l.ratio-1.5)?p:l);t.push({id:ge(),type:"accessibility",severity:"info",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`Line height ratio ${d.ratio.toFixed(2)} (${d.lineHeight}px / ${d.fontSize}px) is outside optimal range 1.3\u20131.6.`,currentValue:`${d.ratio.toFixed(2)}`,suggestions:[`Set line height to ${Math.round(d.fontSize*1.5)}px (1.5\xD7 body) or ${Math.round(d.fontSize*1.3)}px (1.3\xD7 headings)`],autoFixable:!1})}return{sizes:i,lineHeightData:r}}function En(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if("fills"in e){let o=e.fills;if(o!==figma.mixed&&Array.isArray(o))for(let r of o)r.visible!==!1&&r.type==="SOLID"&&t.add(F(r.color.r,r.color.g,r.color.b))}if("strokes"in e){let o=e.strokes;if(Array.isArray(o))for(let r of o)r.visible!==!1&&r.type==="SOLID"&&t.add(F(r.color.r,r.color.g,r.color.b))}if("children"in e)for(let o of e.children)En(o,t,n,s)}}function vr(e,t,n,s){let o=new Set;En(e,o,n,s);let r=Array.from(o);return r.length>8&&t.push({id:ge(),type:"accessibility",severity:"warning",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`${r.length} unique colors detected. A cohesive palette typically uses 5\u20137 colors (primary, secondary, accent, neutrals).`,currentValue:`${r.length} colors`,suggestions:["Consolidate similar colors into design tokens","Limit palette to primary, secondary, accent, and 2-3 neutrals"],autoFixable:!1}),r}function Sr(e,t,n=4){if(!("children"in e))return 0;let s=e.children.filter(r=>r.visible!==!1),o=0;for(let r of s){if(!("x"in r)||!("y"in r))continue;let i=r.x,a=r.y,c=Math.round(i)%n,d=Math.round(a)%n;(c!==0||d!==0)&&o++}return o>0&&o/Math.max(s.length,1)>.3&&t.push({id:ge(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`${o}/${s.length} direct children are misaligned from ${n}px grid.`,currentValue:`${o} misaligned`,suggestions:[`Snap elements to ${n}px grid for visual consistency`],autoFixable:!1}),o}function Tn(e,t,n,s){if(n&&"locked"in e&&e.locked||s&&"visible"in e&&!e.visible)return;if(/button|btn|cta/i.test(e.name)&&"width"in e&&"height"in e&&t.push({nodeId:e.id,nodeName:e.name,width:e.width,height:e.height}),"children"in e)for(let r of e.children)Tn(r,t,n,s)}function kr(e,t,n,s){let o=[];if(Tn(e,o,n,s),o.length<2)return;let r=o.map(d=>d.height),i=r.reduce((d,l)=>d+l,0)/r.length,c=Math.max(...r.map(d=>Math.abs(d-i)))/i*100;if(c>15){let d=Math.min(...r),l=Math.max(...r);t.push({id:ge(),type:"accessibility",severity:"warning",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`Button height inconsistency: ${d}px to ${l}px (${Math.round(c)}% variance). Standardize to 2-3 size tiers.`,currentValue:`${d}\u2013${l}px`,suggestions:["Use consistent button heights: 32px (small), 40px (medium), 48px (large)"],autoFixable:!1})}}function Pn(e,t={}){var g,f;xn=0;let n=[],s=(g=t.skipLocked)!=null?g:!0,o=(f=t.skipHidden)!=null?f:!0,r=0,i=[],a=[],c=[],d=0,l=0,p=0;for(let m of e){"children"in m&&"width"in m&&"height"in m&&(hr(m,n),l+=m.children.length,p+=m.width*m.height,r++);let h=br(m,n,s,o);i=[...new Set([...i,...h.sizes])],a=[...a,...h.lineHeightData],r++;let C=vr(m,n,s,o);c=[...new Set([...c,...C])],r++,"children"in m&&(d+=Sr(m,n),r++),kr(m,n,s,o),r++}let u=p>0?l/p*1e3:0;return{issues:n,metrics:{childCount:l,areaPx:p,density:u,uniqueFontSizes:i,lineHeightRatios:a,uniqueColors:c,misalignedCount:d},summary:{totalChecked:r,passed:r-n.length,failed:n.length}}}var xn,gr,Ln=G(()=>{"use strict";J();xn=0;gr=[[10,12,14,16,18,20,24,28,32,36,40,48,56,64,72],[12,14,16,20,24,32,40,48],[12,14,16,18,21,24,30,36,48,60,72]]});function te(){return`mc-${++Rn}`}function $n(e){if(ht.test(e.name))return!0;let t=e.parent,n=0;for(;t&&n<4;){if("name"in t&&ht.test(t.name)||"type"in t&&(t.type==="COMPONENT"||t.type==="INSTANCE")&&"name"in t&&ht.test(t.name))return!0;t=t.parent,n++}return!1}function Mn(e){return e.trim().split(/\s+/).filter(Boolean).length}function Tr(e,t){let n=e.characters;if(!n||n.trim().length===0){t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:"Empty text node \u2014 remove or add content.",currentValue:"(empty)",autoFixable:!1});return}let s=n.trim(),o=Mn(s),r=$n(e);if((Nr.test(s)||Cr.test(s))&&t.push({id:te(),type:"naming",severity:"warning",nodeId:e.id,nodeName:e.name,message:`"${s.substring(0,40)}" \u2014 avoid "click/tap here". Use descriptive action: "Download report", "View details".`,currentValue:s.substring(0,60),suggestions:['Use verb + object: "Download PDF", "View pricing", "Start trial"'],autoFixable:!1}),wr.test(s)&&t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:'"Learn more" is vague \u2014 specify what the user will learn: "Learn more about pricing".',currentValue:s,suggestions:['Add specificity: "Learn more about [topic]"'],autoFixable:!1}),r&&o<=2){let i=s.toLowerCase().replace(/[.!]/g,"");Ir.has(i)&&t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`Generic CTA "${s}" \u2014 use a specific action: "Save changes", "Send message", "Create account".`,currentValue:s,suggestions:["Replace with verb + object describing the outcome"],autoFixable:!1})}if(r&&o>5&&t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`CTA too long (${o} words): "${s.substring(0,50)}\u2026". Keep CTAs to 2\u20135 words.`,currentValue:`${o} words`,suggestions:["Shorten to verb + object (2-5 words)"],autoFixable:!1}),(xr.test(s)||Ar.test(s))&&t.push({id:te(),type:"naming",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Placeholder text detected: "${s.substring(0,40)}\u2026". Replace with real content.`,currentValue:s.substring(0,60),suggestions:["Replace with actual copy or realistic sample data"],autoFixable:!1}),o>80&&!r&&t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`Long text block (${o} words). Break into shorter paragraphs or use bullet points for readability.`,currentValue:`${o} words`,suggestions:["Break into paragraphs of \u226450 words","Use bullet points for lists","Add subheadings"],autoFixable:!1}),s===s.toUpperCase()&&s!==s.toLowerCase()&&o>3&&t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`All-caps text with ${o} words: "${s.substring(0,40)}\u2026". ALL CAPS reduces readability \u2014 use sentence case or title case.`,currentValue:s.substring(0,60),suggestions:["Use sentence case for readability","Reserve ALL CAPS for short labels (1-2 words)"],autoFixable:!1}),Er.test(s)){let a=(s.match(/\b\d{4,}\b/g)||[]).filter(c=>{let d=parseInt(c,10);return d<1900||d>2099});a.length>0&&t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`Unformatted number${a.length>1?"s":""}: ${a.join(", ")}. Use thousand separators for readability.`,currentValue:a.join(", "),suggestions:["Format as 1,000,000 or 1 000 000"],autoFixable:!1})}}function On(e,t,n,s,o){var r;if(!(s&&"locked"in e&&e.locked)&&!(o&&"visible"in e&&!e.visible)){if(e.type==="TEXT"){n.totalTextNodes++;let i=((r=e.characters)==null?void 0:r.trim())||"",a=Mn(i);a>0&&(n.wordCounts.push(a),a>n.longestParagraph&&(n.longestParagraph=a)),$n(e)&&n.ctaNodes++,Tr(e,t)}if("children"in e)for(let i of e.children)On(i,t,n,s,o)}}function Fn(e,t={}){var a,c;Rn=0;let n=[],s=(a=t.skipLocked)!=null?a:!0,o=(c=t.skipHidden)!=null?c:!0,r={totalTextNodes:0,ctaNodes:0,wordCounts:[],longestParagraph:0};for(let d of e)On(d,n,r,s,o);let i=r.wordCounts.length>0?r.wordCounts.reduce((d,l)=>d+l,0)/r.wordCounts.length:0;return{issues:n,metrics:{totalTextNodes:r.totalTextNodes,ctaNodes:r.ctaNodes,avgWordCount:Math.round(i*10)/10,longestParagraph:r.longestParagraph},summary:{totalChecked:r.totalTextNodes,passed:r.totalTextNodes-n.length,failed:n.length}}}var Rn,Nr,wr,Cr,Ir,xr,Ar,Er,ht,Dn=G(()=>{"use strict";Rn=0;Nr=/\bclick\s+here\b/i,wr=/^learn\s+more\.?$/i,Cr=/\btap\s+here\b/i,Ir=new Set(["submit","ok","okay","next","continue","go","yes","no","done","send","save","apply"]),xr=/\blorem\s+ipsum\b/i,Ar=/^(enter\s+text|type\s+here|placeholder|sample\s+text|your\s+text|add\s+text)\.?$/i,Er=/\b\d{4,}\b/,ht=/button|btn|cta|action|submit|link/i});function ye(){return`conv-${++Un}`}function Rr(e){if(Vn.test(e.name))return!0;let t=e.parent,n=0;for(;t&&n<3;){if("name"in t&&Vn.test(t.name))return!0;t=t.parent,n++}return!1}function $r(e){return Gn.test(e.name)}function _n(e){if(!("fills"in e))return null;let t=e.fills;if(t===figma.mixed||!Array.isArray(t))return null;let n=t.find(s=>s.type==="SOLID"&&s.visible!==!1);return n?n.color:null}function Bn(e,t,n){let s=[e,t,n].map(o=>o<=.03928?o/12.92:Math.pow((o+.055)/1.055,2.4));return .2126*s[0]+.7152*s[1]+.0722*s[2]}function Mr(e,t){let n=Math.max(e,t),s=Math.min(e,t);return(n+.05)/(s+.05)}function zn(e,t,n,s,o){if(!(s&&"locked"in e&&e.locked)&&!(o&&"visible"in e&&!e.visible)&&(Rr(e)&&"width"in e&&"height"in e&&t.push({node:e,x:"x"in e?e.x:0,y:"y"in e?e.y:0,width:e.width,height:e.height,absoluteY:n+("y"in e?e.y:0)}),"children"in e)){let r=n+("y"in e?e.y:0);for(let i of e.children)zn(i,t,r,s,o)}}function Wn(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if($r(e)){let o=!1,r=e.parent;if(r&&"children"in r){for(let i of r.children)if(i.type==="TEXT"&&i.id!==e.id){o=!0;break}}t.push({node:e,hasLabel:o})}if("children"in e)for(let o of e.children)Wn(o,t,n,s)}}function _e(e,t,n,s){if(n&&"locked"in e&&e.locked||s&&"visible"in e&&!e.visible)return!1;if(t.test(e.name))return!0;if("children"in e){for(let o of e.children)if(_e(o,t,n,s))return!0}return!1}function Or(e,t,n){if(t.length===0||!("height"in e))return!1;let s=e.height*.7,o=t.some(r=>r.y+r.height<s);return o||n.push({id:ye(),type:"accessibility",severity:"warning",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:"No primary CTA visible above the fold (top 70% of frame). Move the main action higher for better conversion.",currentValue:`CTA at ${Math.round(t[0].y)}px, fold at ${Math.round(s)}px`,suggestions:["Place primary CTA within top 70% of the viewport","Add a secondary CTA near the top if main CTA must stay below"],autoFixable:!1}),o}function Fr(e,t,n){let s=_n(e);if(!s)return;let o=Bn(s.r,s.g,s.b);for(let r of t){let i=_n(r.node);if(!i)continue;let a=Bn(i.r,i.g,i.b),c=Mr(o,a);if(c<3){let d=F(i.r,i.g,i.b),l=F(s.r,s.g,s.b);n.push({id:ye(),type:"accessibility",severity:"warning",nodeId:r.node.id,nodeName:r.node.name,message:`CTA contrast ratio ${c.toFixed(1)}:1 (${d} on ${l}) \u2014 too low. CTAs should stand out with \u22653:1 contrast against background.`,currentValue:`${c.toFixed(1)}:1`,suggestions:["Increase CTA background contrast to at least 3:1","Use a bolder accent color for the primary action"],autoFixable:!1})}}}function Dr(e,t,n){t.length>5&&n.push({id:ye(),type:"accessibility",severity:"warning",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`${t.length} form fields on one screen. More than 5 fields increases abandonment \u2014 consider splitting into steps or removing optional fields.`,currentValue:`${t.length} fields`,suggestions:["Split into multi-step form with progress indicator",'Remove optional fields or move to "Advanced" section',"Expedia gained $12M/year by removing one field"],autoFixable:!1});let s=t.filter(o=>!o.hasLabel);s.length>0&&n.push({id:ye(),type:"accessibility",severity:"warning",nodeId:s[0].node.id,nodeName:s[0].node.name,message:`${s.length} form field${s.length===1?"":"s"} without visible labels. Labels improve completion rate and accessibility.`,currentValue:`${s.length} unlabeled`,suggestions:["Add visible label text above or beside each input","Don't rely on placeholder text alone as labels"],autoFixable:!1})}function Vr(e,t,n,s,o){if(t.length<=3)return!1;let r=_e(e,Pr,s,o);return!r&&t.length>5&&n.push({id:ye(),type:"accessibility",severity:"info",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:"Long form without progress indicator. A step counter or progress bar reduces perceived effort.",suggestions:['Add "Step 1 of 3" or a progress bar',"Show users how far they've come and what's left"],autoFixable:!1}),r}function _r(e,t,n,s,o){if(t.length===0||!_e(e,Gn,s,o))return;_e(e,Lr,s,o)||n.push({id:ye(),type:"accessibility",severity:"info",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:"Form with CTA but no trust signals (security badges, reviews, guarantees). Trust elements near CTAs increase conversion.",suggestions:["Add security badge or lock icon near submit button","Show testimonials, ratings, or guarantees near the CTA"],autoFixable:!1})}function Hn(e,t={}){var l,p;Un=0;let n=[],s=(l=t.skipLocked)!=null?l:!0,o=(p=t.skipHidden)!=null?p:!0,r=0,i=0,a=!1,c=!1,d=0;for(let u of e){let g=[];zn(u,g,0,s,o),r+=g.length;let f=[];Wn(u,f,s,o),i+=f.length,g.length>0&&(Or(u,g,n)&&(a=!0),Fr(u,g,n),d+=2),f.length>0&&(Dr(u,f,n),Vr(u,f,n,s,o)&&(c=!0),d+=2),_r(u,g,n,s,o),d++}return{issues:n,metrics:{ctaCount:r,formFieldCount:i,ctaAboveFold:a,hasProgressIndicator:c},summary:{totalChecked:d,passed:d-n.length,failed:n.length}}}var Un,Vn,Gn,Pr,Lr,Kn=G(()=>{"use strict";J();Un=0;Vn=/button|btn|cta|action|submit|primary/i,Gn=/input|field|text.?area|select|dropdown|picker|combo|search|email|password|phone|number.?field/i,Pr=/progress|step|stepper|breadcrumb|wizard|indicator|pagination/i,Lr=/badge|trust|security|lock|shield|guarantee|verified|secure|ssl|certification|review|rating|star/i});function Ie(){return`cog-${++qn}`}function Wr(e){return Br.test(e.name)}function Hr(e){return Ur.test(e.name)}function Xn(e){return Jn.test(e.name)}function Kr(e){return Gr.test(e.name)}function jr(e){return zr.test(e.name)}function qr(e){if(!jn.test(e.name)&&!Jn.test(e.name)||!("children"in e))return!1;let t=e.children,n=t.some(o=>o.type==="TEXT");return t.some(o=>o.type==="VECTOR"||o.type==="BOOLEAN_OPERATION"||jn.test(o.name))&&!n}function Yn(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if(Hr(e)){t.push(e);return}if("children"in e)for(let o of e.children)Yn(o,t,n,s)}}function Qn(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)&&(Xn(e)&&t.push(e),"children"in e))for(let o of e.children)Qn(o,t,n,s)}function Jr(e){let t=e.match(/h(\d)/i);return t?parseInt(t[1],10):/title|headline/i.test(e)?1:/subtitle|subhead/i.test(e)||/heading/i.test(e)?2:null}function Zn(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if(Kr(e)){let o=Jr(e.name);o!==null&&t.push({node:e,level:o})}if("children"in e)for(let o of e.children)Zn(o,t,n,s)}}function es(e,t,n,s){if(n&&"locked"in e&&e.locked||s&&"visible"in e&&!e.visible)return t;let o=t;if("children"in e)for(let r of e.children){let i=es(r,t+1,n,s);i>o&&(o=i)}return o}function ts(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)&&(jr(e)&&"opacity"in e&&e.opacity<1&&t.push(e),"children"in e))for(let o of e.children)ts(o,t,n,s)}function ns(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)&&(Xn(e)&&qr(e)&&t.push(e),"children"in e))for(let o of e.children)ns(o,t,n,s)}function Xr(e,t,n,s){let o=[];ss(e,o,n,s);let r=0;for(let i of o){let a=[];Yn(i,a,n,s),r+=a.length,a.length>7&&t.push({id:Ie(),type:"accessibility",severity:"warning",nodeId:i.id,nodeName:i.name,message:`Navigation has ${a.length} items \u2014 Miller's Law suggests 7\xB12 is the working memory limit. Consider grouping or progressive disclosure.`,currentValue:`${a.length} nav items`,suggestions:["Group related items under expandable sections",'Use "More" menu for less-used items',"Limit primary navigation to 5-7 items"],autoFixable:!1})}return r}function ss(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if(Wr(e)){t.push(e);return}if("children"in e)for(let o of e.children)ss(o,t,n,s)}}function Yr(e,t,n,s){let o=[];return Qn(e,o,n,s),o.length>5&&t.push({id:Ie(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`${o.length} CTAs/buttons on one screen \u2014 choice overload reduces decision-making ability (Hick's Law). Prioritize one primary action.`,currentValue:`${o.length} CTAs`,suggestions:["Establish clear primary/secondary/tertiary action hierarchy","Reduce to 1 primary CTA per viewport","Group related actions in a dropdown or overflow menu"],autoFixable:!1}),o.length}function Qr(e,t,n,s){let o=[];if(Zn(e,o,n,s),o.length<2)return o.map(i=>i.level);let r=o.sort((i,a)=>{let c="y"in i.node?i.node.y:0,d="y"in a.node?a.node.y:0;return c-d});for(let i=1;i<r.length;i++){let a=r[i-1].level,c=r[i].level;c>a+1&&t.push({id:Ie(),type:"accessibility",severity:"info",nodeId:r[i].node.id,nodeName:r[i].node.name,message:`Heading hierarchy gap: jumps from level ${a} to level ${c}. Screen readers and users rely on sequential heading structure.`,currentValue:`h${a} \u2192 h${c}`,suggestions:[`Add an h${a+1} between these levels`,"Ensure headings follow a logical descending order"],autoFixable:!1})}return r.map(i=>i.level)}function Zr(e,t,n,s){var r;let o=[];ts(e,o,n,s);for(let i of o){let a=i.parent,c=!1;if(a&&"children"in a){for(let d of a.children)if(d.type==="TEXT"&&d.id!==i.id){let l=((r=d.characters)==null?void 0:r.toLowerCase())||"";if(l.includes("required")||l.includes("complete")||l.includes("fill")||l.includes("select")||l.includes("first")){c=!0;break}}}c||t.push({id:Ie(),type:"accessibility",severity:"info",nodeId:i.id,nodeName:i.name,message:`Disabled element "${i.name}" without visible explanation. Users should understand WHY an action is unavailable and how to enable it.`,suggestions:["Add helper text explaining what needs to happen first","Use a tooltip on hover explaining the disabled state",'Show a brief inline message (e.g., "Complete all fields to continue")'],autoFixable:!1})}}function ei(e,t,n,s){let o=[];return ns(e,o,n,s),o.length>3&&t.push({id:Ie(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`${o.length} icon-only buttons without text labels. Icons alone are ambiguous \u2014 add labels or ensure tooltips are present.`,currentValue:`${o.length} icon-only`,suggestions:["Add visible text labels to icon buttons","Add tooltips that appear on hover/focus","Use aria-label for accessibility (ensure design indicates this)"],autoFixable:!1}),o.length}function os(e,t={}){var p,u;qn=0;let n=[],s=(p=t.skipLocked)!=null?p:!0,o=(u=t.skipHidden)!=null?u:!0,r=0,i=0,a=0,c=[],d=0,l=0;for(let g of e){r+=Xr(g,n,s,o),l++,i+=Yr(g,n,s,o),l++;let f=Qr(g,n,s,o);c=[...c,...f],l++,Zr(g,n,s,o),l++,d+=ei(g,n,s,o),l++;let m=es(g,0,s,o);m>a&&(a=m)}return{issues:n,metrics:{navItemCount:r,ctaCount:i,maxNestingDepth:a,headingLevels:[...new Set(c)].sort(),iconOnlyButtons:d},summary:{totalChecked:l,passed:l-n.length,failed:n.length}}}var qn,Br,Ur,Jn,Gr,zr,jn,rs=G(()=>{"use strict";qn=0;Br=/nav|menu|sidebar|tab.?bar|bottom.?bar|header.?nav|navigation|top.?bar/i,Ur=/nav.?item|menu.?item|tab(?!le)|link/i,Jn=/button|btn|cta|action|submit|primary/i,Gr=/heading|title|h[1-6]|headline/i,zr=/disabled|inactive|dimmed|greyed/i,jn=/icon|ico|svg|glyph/i});function ti(){return`fitts-${++is}`}function si(e){return ni.test(e.name)}function as(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,failed:0};if(s&&i)return{checked:0,failed:0};let a=0,c=0;if(si(e)&&"width"in e&&"height"in e){a++;let d=e.width,l=e.height;(d<he||l<he)&&(c++,t.push({id:ti(),type:"fittsLaw",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Interactive target "${e.name}" is ${Math.round(d)}x${Math.round(l)}px \u2014 minimum recommended size is ${he}x${he}px (WCAG 2.5.8)`,currentValue:`${Math.round(d)}x${Math.round(l)}px`,suggestions:[`Increase to at least ${he}x${he}px`,"Add padding to increase the hit area"],autoFixable:!1}))}if("children"in e)for(let d of e.children){let l=as(d,t,n,s,r);a+=l.checked,c+=l.failed}return{checked:a,failed:c}}function cs(e,t={}){var a,c;is=0;let n=[],s=(a=t.skipLocked)!=null?a:!0,o=(c=t.skipHidden)!=null?c:!0,r=0,i=0;for(let d of e){let l=as(d,n,s,o,!1);r+=l.checked,i+=l.failed}return{issues:n,summary:{totalChecked:r,passed:r-i,failed:i}}}var is,ni,he,ls=G(()=>{"use strict";is=0;ni=/button|btn|cta|action|submit|link|toggle|switch|checkbox|radio|tab(?!le)/i,he=44});function oi(){return`gestalt-${++ds}`}function us(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,failed:0};if(s&&i)return{checked:0,failed:0};let a=0,c=0;if((e.type==="FRAME"||e.type==="COMPONENT"||e.type==="INSTANCE")&&"children"in e){let l=e;if(l.layoutMode==="NONE"&&l.children.length>=3){a++;let p=l.children.filter(u=>"visible"in u&&u.visible&&"y"in u);if(p.length>=3){let u=[...p].sort((f,m)=>f.y-m.y),g=[];for(let f=1;f<u.length;f++){let m=u[f-1].y+u[f-1].height,h=u[f].y;g.push(h-m)}if(g.length>=2){let f=new Set(g.map(m=>Math.round(m)));f.size>2&&(c++,t.push({id:oi(),type:"gestalt",severity:"info",nodeId:e.id,nodeName:e.name,message:`Frame "${e.name}" has ${f.size} different spacing gaps between children (${[...f].join(", ")}px) \u2014 inconsistent proximity weakens visual grouping (Gestalt proximity principle)`,currentValue:`${f.size} distinct gaps`,suggestions:["Use auto-layout with consistent gap spacing","Standardize spacing between sibling elements"],autoFixable:!1}))}}}}if("children"in e)for(let l of e.children){let p=us(l,t,n,s,r);a+=p.checked,c+=p.failed}return{checked:a,failed:c}}function ps(e,t={}){var a,c;ds=0;let n=[],s=(a=t.skipLocked)!=null?a:!0,o=(c=t.skipHidden)!=null?c:!0,r=0,i=0;for(let d of e){let l=us(d,n,s,o,!1);r+=l.checked,i+=l.failed}return{issues:n,summary:{totalChecked:r,passed:r-i,failed:i}}}var ds,ms=G(()=>{"use strict";ds=0});function ri(){return`detach-${++fs}`}function gs(e,t,n,s,o){var d;let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,failed:0};if(s&&i)return{checked:0,failed:0};let a=0,c=0;if(e.type==="FRAME"&&"children"in e){a++;let l=ii.test(e.name),p=ai.test(e.name)&&((d=e.parent)==null?void 0:d.type)!=="PAGE"&&e.children.length>0;if(l)c++,t.push({id:ri(),type:"detachedInstance",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Frame "${e.name}" appears to be a detached component instance. Detaching breaks the link to the source component and prevents design system updates.`,currentValue:"Detached instance",suggestions:["Re-attach by replacing with the original component instance",'If intentional, rename to remove "detach" from the name'],autoFixable:!1});else if(p){let u=e.name.split(/[\s\-\/]/);u.length>=2&&u.every(g=>g.length>0)}}if("children"in e)for(let l of e.children){let p=gs(l,t,n,s,r);a+=p.checked,c+=p.failed}return{checked:a,failed:c}}function ys(e,t={}){var a,c;fs=0;let n=[],s=(a=t.skipLocked)!=null?a:!0,o=(c=t.skipHidden)!=null?c:!0,r=0,i=0;for(let d of e){let l=gs(d,n,s,o,!1);r+=l.checked,i+=l.failed}return{issues:n,summary:{totalChecked:r,passed:r-i,failed:i}}}var fs,ii,ai,hs=G(()=>{"use strict";fs=0;ii=/detach/i,ai=/^[A-Z][a-zA-Z]+(?:\s*[-\/]\s*[A-Za-z]+)*$/});function bt(){return`resp-${++vs}`}function Ss(e){return e.type==="FRAME"||e.type==="COMPONENT"||e.type==="INSTANCE"}function bs(e){for(let t of ci){let n=e.match(t);if(n){for(let s of n.slice(1))if(li.has(s.toLowerCase()))return s.toLowerCase()}}return null}function ks(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,failed:0,fixedWidthCount:0};if(s&&i)return{checked:0,failed:0,fixedWidthCount:0};let a=0,c=0,d=0;if(Ss(e)){let l=e;a++;let p=l.layoutSizingHorizontal==="FIXED"||l.layoutSizingHorizontal===void 0,u=!l.parent||l.parent.type==="PAGE",g=l.layoutMode!=="NONE",f="minWidth"in l&&l.minWidth!==null&&l.minWidth!==void 0||"maxWidth"in l&&l.maxWidth!==null&&l.maxWidth!==void 0;p&&!u&&!f&&g&&l.width>200&&(d++,c++,t.push({id:bt(),type:"responsive",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Frame "${e.name}" has fixed width (${Math.round(l.width)}px) with auto-layout but no fill/hug sizing \u2014 may not adapt to different screen sizes`,currentValue:`${Math.round(l.width)}px fixed`,suggestions:['Set horizontal sizing to "Fill" for responsive behavior',"Add min-width/max-width constraints",'Use "Hug contents" if the frame should shrink-wrap'],autoFixable:!1}))}if("children"in e)for(let l of e.children){let p=ks(l,t,n,s,r);a+=p.checked,c+=p.failed,d+=p.fixedWidthCount}return{checked:a,failed:c,fixedWidthCount:d}}function Ns(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,failed:0,riskCount:0};if(s&&i)return{checked:0,failed:0,riskCount:0};let a=0,c=0,d=0;if(e.type==="TEXT"){let l=e;a++;let p=l.fontSize!==figma.mixed?l.fontSize:14,u=l.textAutoResize;if(u==="NONE"||u==="TRUNCATE"){let g=l.characters.length,f=g*p*di,m=l.width;g>5&&f>m*.8&&(d++,c++,t.push({id:bt(),type:"responsive",severity:"info",nodeId:e.id,nodeName:e.name,message:`Text "${e.name}" may truncate \u2014 content fills ~${Math.round(f/m*100)}% of fixed width (${Math.round(m)}px). Translations or dynamic content could overflow.`,currentValue:`${g} chars in ${Math.round(m)}px`,suggestions:["Use auto-width or auto-height text resizing","Allow text to wrap by setting textAutoResize to HEIGHT","Add ellipsis handling if truncation is intentional"],autoFixable:!1}))}}if("children"in e)for(let l of e.children){let p=Ns(l,t,n,s,r);a+=p.checked,c+=p.failed,d+=p.riskCount}return{checked:a,failed:c,riskCount:d}}function ws(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,failed:0,missingCount:0};if(s&&i)return{checked:0,failed:0,missingCount:0};let a=0,c=0,d=0;if(Ss(e)){let l=e;if(l.layoutMode==="HORIZONTAL"&&"children"in l){let p=l.children.filter(u=>"visible"in u&&u.visible);p.length>=3&&(a++,("layoutWrap"in l?l.layoutWrap:"NO_WRAP")!=="WRAP"&&(d++,c++,t.push({id:bt(),type:"responsive",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Horizontal layout "${e.name}" has ${p.length} children without wrap \u2014 content won't reflow on smaller screens`,currentValue:`${p.length} children, no wrap`,suggestions:['Enable "Wrap" on the auto-layout to allow content reflow',"Consider switching to vertical layout on mobile breakpoints","Use min-width on children to control when wrapping occurs"],autoFixable:!1})))}}if("children"in e)for(let l of e.children){let p=ws(l,t,n,s,r);a+=p.checked,c+=p.failed,d+=p.missingCount}return{checked:a,failed:c,missingCount:d}}function ui(e){let t=new Set;for(let n of e)pi(n,t);return Array.from(t)}function pi(e,t){if(bs(e.name)&&t.add(e.name),"children"in e)for(let s of e.children)bs(s.name)&&t.add(s.name)}function Cs(e,t){var p,u;vs=0;let n=[],s=(p=t==null?void 0:t.skipLocked)!=null?p:!0,o=(u=t==null?void 0:t.skipHidden)!=null?u:!0,r=0,i=0,a=0,c=0,d=0;for(let g of e){let f=ks(g,n,s,o,!1);r+=f.checked,i+=f.failed,a+=f.fixedWidthCount;let m=Ns(g,n,s,o,!1);r+=m.checked,i+=m.failed,c+=m.riskCount;let h=ws(g,n,s,o,!1);r+=h.checked,i+=h.failed,d+=h.missingCount}let l=ui(e);return{issues:n,metrics:{fixedWidthElements:a,textTruncationRisk:c,missingAutoLayout:d,breakpointVariants:l},summary:{totalChecked:r,passed:r-i,failed:i}}}var vs,ci,li,di,Is=G(()=>{"use strict";vs=0;ci=[/^(.+)\s*[-–—]\s*(desktop|tablet|mobile|phone|sm|md|lg|xl|xxl|small|medium|large)\s*$/i,/^(.+)\/(desktop|tablet|mobile|phone|sm|md|lg|xl|xxl|small|medium|large)\s*$/i,/^(desktop|tablet|mobile|phone|sm|md|lg|xl|xxl|small|medium|large)\s*[-–—/]\s*(.+)$/i,/^(.+)\s*\[(desktop|tablet|mobile|phone|sm|md|lg|xl|xxl|small|medium|large)\]\s*$/i],li=new Set(["desktop","tablet","mobile","phone","sm","md","lg","xl","xxl","small","medium","large"]),di=.5});var Et={};en(Et,{DEFAULT_LINT_SETTINGS:()=>j,clearIgnored:()=>Ct,findNodesWithSameValue:()=>At,getIgnoredState:()=>It,ignoreAllOfType:()=>wt,ignoreError:()=>Nt,ignoreNode:()=>kt,lintSelection:()=>xe,restoreIgnoredState:()=>xt,runDesignLint:()=>ne});function U(e,t,n){return n?`${e}::${t}::${n}`:`${e}::${t}`}function kt(e){B.add(e)}function Nt(e,t,n){V.add(U(e,t,n))}function wt(e,t){for(let n of e)n.errorType===t&&V.add(U(n.nodeId,n.errorType))}function Ct(){B.clear(),V.clear()}function It(){return{nodeIds:Array.from(B),errorKeys:Array.from(V)}}function xt(e){B=new Set(e.nodeIds),V=new Set(e.errorKeys)}function As(e){if(e.type==="SOLID"){let{r:t,g:n,b:s}=e.color,o=F(t,n,s),r=e.opacity!==void 0&&e.opacity<1?` (${Math.round(e.opacity*100)}%)`:"";return o+r}return e.type==="IMAGE"?"Image fill":e.type==="VIDEO"?"Video fill":e.type.includes("GRADIENT")?`${e.type.replace("GRADIENT_","").toLowerCase()} gradient`:e.type}function Ue(e,t){try{if("boundVariables"in e){let n=e.boundVariables;if(n&&n[t])return!0}}catch(n){}return!1}function Be(e,t,n){if(!("fills"in e))return;let s=e.fills;if(s===figma.mixed||!Array.isArray(s))return;let o=s.filter(r=>r.visible!==!1);if(o.length!==0&&!Ue(e,"fills")){if("fillStyleId"in e){let r=e.fillStyleId;if(r&&r!==""&&r!==figma.mixed)return}for(let r of o){try{let a=r.boundVariables;if(a&&a.color)continue}catch(a){}let i=As(r);t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"fill",message:`Missing fill style: ${i}`,value:i,path:n})}}}function vt(e,t,n){if(!("strokes"in e))return;let s=e.strokes;if(!Array.isArray(s))return;let o=s.filter(r=>r.visible!==!1);if(o.length!==0&&!Ue(e,"strokes")){if("strokeStyleId"in e){let r=e.strokeStyleId;if(r&&r!==""&&r!==figma.mixed)return}for(let r of o){let i=As(r),a="strokeWeight"in e?` (${e.strokeWeight}px)`:"";t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"stroke",message:`Missing stroke style: ${i}${a}`,value:i+a,path:n})}}}function St(e,t,n){if(!("effects"in e))return;let s=e.effects;if(!Array.isArray(s)||s.length===0)return;let o=s.filter(i=>i.visible!==!1);if(o.length===0)return;if("effectStyleId"in e){let i=e.effectStyleId;if(i&&i!==""&&i!==figma.mixed)return}let r=o.map(i=>{let a=[i.type.replace(/_/g," ").toLowerCase()];if("radius"in i&&a.push(`r:${i.radius}`),"color"in i&&i.color){let c=i.color;a.push(F(c.r,c.g,c.b))}return a.join(" ")});t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"effect",message:`Missing effect style: ${r.join(", ")}`,value:r.join(", "),path:n})}function mi(e,t,n){if("textStyleId"in e){let a=e.textStyleId;if(a&&a!==""&&a!==figma.mixed)return}let s=e.fontName!==figma.mixed?e.fontName:null,o=e.fontSize!==figma.mixed?e.fontSize:null,r=[];s&&r.push(`${s.family} ${s.style}`),o&&r.push(`${o}px`);let i=r.join(" / ")||"unknown text style";t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"text",message:`Missing text style: ${i}`,value:i,path:n})}function xs(e,t,n,s){if(!("cornerRadius"in e)||Ue(e,"topLeftRadius")||Ue(e,"cornerRadius"))return;let o=e.cornerRadius;if(o===figma.mixed){let r=[e.topLeftRadius,e.topRightRadius,e.bottomLeftRadius,e.bottomRightRadius].filter(i=>i!=null);for(let i of r)if(!s.includes(i)){t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"radius",message:`Non-standard border radius: ${i}px (allowed: ${s.join(", ")})`,value:`${i}px`,path:n});break}return}typeof o=="number"&&o>0&&!s.includes(o)&&t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"radius",message:`Non-standard border radius: ${o}px (allowed: ${s.join(", ")})`,value:`${o}px`,path:n})}function fi(e,t,n,s){if(!(e.type==="GROUP"||e.type==="SLICE"||e.type==="CONNECTOR")&&e.type!=="COMPONENT_SET")switch(e.type){case"TEXT":t.checkTextStyles&&mi(e,n,s),t.checkFills&&Be(e,n,s);break;case"FRAME":case"SECTION":t.checkFills&&Be(e,n,s),t.checkStrokes&&vt(e,n,s),t.checkEffects&&St(e,n,s),t.checkRadius&&xs(e,n,s,t.allowedRadii);break;case"RECTANGLE":case"COMPONENT":case"INSTANCE":t.checkFills&&Be(e,n,s),t.checkStrokes&&vt(e,n,s),t.checkEffects&&St(e,n,s),t.checkRadius&&xs(e,n,s,t.allowedRadii);break;case"ELLIPSE":case"POLYGON":case"STAR":case"VECTOR":case"LINE":case"BOOLEAN_OPERATION":t.checkFills&&Be(e,n,s),t.checkStrokes&&vt(e,n,s),t.checkEffects&&St(e,n,s);break}}function Es(e,t,n,s,o){let r=0,i=o||"locked"in e&&e.locked,a="visible"in e&&!e.visible;if(t.skipLockedLayers&&i||t.skipHiddenLayers&&a)return 0;let c=s?`${s} > ${e.name}`:e.name;if(r++,!B.has(e.id)){let d=n.length;fi(e,t,n,c);for(let l=n.length-1;l>=d;l--){let p=n[l];(V.has(U(p.nodeId,p.errorType))||V.has(U(p.nodeId,p.errorType,p.value)))&&n.splice(l,1)}}if("children"in e)for(let d of e.children)r+=Es(d,t,n,c,i);return r}function q(e,t){for(let n of t)if(new RegExp("^"+n.replace(/[.+^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*").replace(/\?/g,".")+"$").test(e))return!0;return!1}function ne(e,t=j){var u,g;let n=[],s=0,o=t.ignorePatterns||[],r=t.severityOverrides||{};for(let f of e)s+=Es(f,t,n,"",!1);let i={skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers,scale:t.spacingScale};if(t.checkSpacing&&r.spacing!=="off"){let f=yn(e,i);for(let m of f.issues){let h=m.currentValue||"";B.has(m.nodeId)||V.has(U(m.nodeId,"spacing"))||V.has(U(m.nodeId,"spacing",h))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"spacing",message:m.message,value:h,path:m.nodeName,property:(g=(u=m.fixAction)==null?void 0:u.params)==null?void 0:g.property})}}if(t.checkAutoLayout&&r.autoLayout!=="off"){let f=Sn(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||V.has(U(m.nodeId,"autoLayout"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"autoLayout",message:m.message,value:m.currentValue||"",path:m.nodeName})}if(t.checkAccessibility&&r.accessibility!=="off"){let f=Cn(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||V.has(U(m.nodeId,"accessibility"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"accessibility",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkVisualQuality&&r.visualQuality!=="off"){let f=Pn(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||V.has(U(m.nodeId,"visualQuality"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"visualQuality",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkMicrocopy&&r.microcopy!=="off"){let f=Fn(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||V.has(U(m.nodeId,"microcopy"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"TEXT",errorType:"microcopy",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkConversion&&r.conversion!=="off"){let f=Hn(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||V.has(U(m.nodeId,"conversion"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"conversion",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkCognitive&&r.cognitive!=="off"){let f=os(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||V.has(U(m.nodeId,"cognitive"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"cognitive",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkFittsLaw&&r.fittsLaw!=="off"){let f=cs(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||V.has(U(m.nodeId,"fittsLaw"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"fittsLaw",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkGestalt&&r.gestalt!=="off"){let f=ps(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||V.has(U(m.nodeId,"gestalt"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"gestalt",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkDetachedInstances&&r.detachedInstance!=="off"){let f=ys(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||V.has(U(m.nodeId,"detachedInstance"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"detachedInstance",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkResponsive&&r.responsive!=="off"){let f=Cs(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||V.has(U(m.nodeId,"responsive"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"responsive",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}let a=n.filter(f=>r[f.errorType]!=="off"),c=o.length>0?a.filter(f=>!q(f.nodeName,o)):a;for(let f of c){let m=r[f.errorType];if(m&&m!=="off")f.severity=m;else if(!f.severity)switch(f.errorType){case"fill":case"stroke":case"effect":case"text":case"spacing":f.severity="warning";break;case"radius":case"autoLayout":f.severity="info";break;case"accessibility":f.severity="critical";break;case"visualQuality":f.severity="warning";break;case"microcopy":f.severity="info";break;case"conversion":f.severity="warning";break;case"cognitive":f.severity="info";break;case"responsive":f.severity="warning";break;case"fittsLaw":f.severity="warning";break;case"gestalt":f.severity="info";break;case"detachedInstance":f.severity="warning";break}}let d=new Set(c.map(f=>f.nodeId)).size,l={fill:0,stroke:0,effect:0,text:0,radius:0,spacing:0,autoLayout:0,accessibility:0,visualQuality:0,microcopy:0,conversion:0,cognitive:0,fittsLaw:0,gestalt:0,detachedInstance:0,responsive:0};for(let f of c)l[f.errorType]++;let p={totalErrors:c.length,byType:l,totalNodes:s,nodesWithErrors:d};return{errors:c,ignoredNodeIds:Array.from(B),ignoredErrorKeys:Array.from(V),summary:p}}function xe(e){let t=figma.currentPage.selection;return t.length===0?{errors:[],ignoredNodeIds:[],ignoredErrorKeys:[],summary:{totalErrors:0,byType:{fill:0,stroke:0,effect:0,text:0,radius:0,spacing:0,autoLayout:0,accessibility:0,visualQuality:0,microcopy:0,conversion:0,cognitive:0,fittsLaw:0,gestalt:0,detachedInstance:0,responsive:0},totalNodes:0,nodesWithErrors:0}}:ne(t,e)}function At(e,t,n,s=j){return ne(e,s).errors.filter(r=>r.errorType===t&&r.value===n)}var j,B,V,be=G(()=>{"use strict";J();hn();kn();In();Ln();Dn();Kn();rs();ls();ms();hs();Is();j={checkFills:!0,checkStrokes:!0,checkEffects:!0,checkTextStyles:!0,checkRadius:!0,checkSpacing:!0,checkAutoLayout:!0,checkAccessibility:!0,checkVisualQuality:!0,checkMicrocopy:!0,checkConversion:!0,checkCognitive:!0,checkFittsLaw:!0,checkGestalt:!0,checkDetachedInstances:!0,checkResponsive:!0,allowedRadii:[0,2,4,8,12,16,24,32],skipLockedLayers:!0,skipHiddenLayers:!0},B=new Set,V=new Set});var js=Po((Ul,We)=>{var Bt=function(){var e=String.fromCharCode,t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",s={};function o(i,a){if(!s[i]){s[i]={};for(var c=0;c<i.length;c++)s[i][i.charAt(c)]=c}return s[i][a]}var r={compressToBase64:function(i){if(i==null)return"";var a=r._compress(i,6,function(c){return t.charAt(c)});switch(a.length%4){default:case 0:return a;case 1:return a+"===";case 2:return a+"==";case 3:return a+"="}},decompressFromBase64:function(i){return i==null?"":i==""?null:r._decompress(i.length,32,function(a){return o(t,i.charAt(a))})},compressToUTF16:function(i){return i==null?"":r._compress(i,15,function(a){return e(a+32)})+" "},decompressFromUTF16:function(i){return i==null?"":i==""?null:r._decompress(i.length,16384,function(a){return i.charCodeAt(a)-32})},compressToUint8Array:function(i){for(var a=r.compress(i),c=new Uint8Array(a.length*2),d=0,l=a.length;d<l;d++){var p=a.charCodeAt(d);c[d*2]=p>>>8,c[d*2+1]=p%256}return c},decompressFromUint8Array:function(i){if(i==null)return r.decompress(i);for(var a=new Array(i.length/2),c=0,d=a.length;c<d;c++)a[c]=i[c*2]*256+i[c*2+1];var l=[];return a.forEach(function(p){l.push(e(p))}),r.decompress(l.join(""))},compressToEncodedURIComponent:function(i){return i==null?"":r._compress(i,6,function(a){return n.charAt(a)})},decompressFromEncodedURIComponent:function(i){return i==null?"":i==""?null:(i=i.replace(/ /g,"+"),r._decompress(i.length,32,function(a){return o(n,i.charAt(a))}))},compress:function(i){return r._compress(i,16,function(a){return e(a)})},_compress:function(i,a,c){if(i==null)return"";var d,l,p={},u={},g="",f="",m="",h=2,C=3,k=2,N=[],y=0,b=0,I;for(I=0;I<i.length;I+=1)if(g=i.charAt(I),Object.prototype.hasOwnProperty.call(p,g)||(p[g]=C++,u[g]=!0),f=m+g,Object.prototype.hasOwnProperty.call(p,f))m=f;else{if(Object.prototype.hasOwnProperty.call(u,m)){if(m.charCodeAt(0)<256){for(d=0;d<k;d++)y=y<<1,b==a-1?(b=0,N.push(c(y)),y=0):b++;for(l=m.charCodeAt(0),d=0;d<8;d++)y=y<<1|l&1,b==a-1?(b=0,N.push(c(y)),y=0):b++,l=l>>1}else{for(l=1,d=0;d<k;d++)y=y<<1|l,b==a-1?(b=0,N.push(c(y)),y=0):b++,l=0;for(l=m.charCodeAt(0),d=0;d<16;d++)y=y<<1|l&1,b==a-1?(b=0,N.push(c(y)),y=0):b++,l=l>>1}h--,h==0&&(h=Math.pow(2,k),k++),delete u[m]}else for(l=p[m],d=0;d<k;d++)y=y<<1|l&1,b==a-1?(b=0,N.push(c(y)),y=0):b++,l=l>>1;h--,h==0&&(h=Math.pow(2,k),k++),p[f]=C++,m=String(g)}if(m!==""){if(Object.prototype.hasOwnProperty.call(u,m)){if(m.charCodeAt(0)<256){for(d=0;d<k;d++)y=y<<1,b==a-1?(b=0,N.push(c(y)),y=0):b++;for(l=m.charCodeAt(0),d=0;d<8;d++)y=y<<1|l&1,b==a-1?(b=0,N.push(c(y)),y=0):b++,l=l>>1}else{for(l=1,d=0;d<k;d++)y=y<<1|l,b==a-1?(b=0,N.push(c(y)),y=0):b++,l=0;for(l=m.charCodeAt(0),d=0;d<16;d++)y=y<<1|l&1,b==a-1?(b=0,N.push(c(y)),y=0):b++,l=l>>1}h--,h==0&&(h=Math.pow(2,k),k++),delete u[m]}else for(l=p[m],d=0;d<k;d++)y=y<<1|l&1,b==a-1?(b=0,N.push(c(y)),y=0):b++,l=l>>1;h--,h==0&&(h=Math.pow(2,k),k++)}for(l=2,d=0;d<k;d++)y=y<<1|l&1,b==a-1?(b=0,N.push(c(y)),y=0):b++,l=l>>1;for(;;)if(y=y<<1,b==a-1){N.push(c(y));break}else b++;return N.join("")},decompress:function(i){return i==null?"":i==""?null:r._decompress(i.length,32768,function(a){return i.charCodeAt(a)})},_decompress:function(i,a,c){var d=[],l,p=4,u=4,g=3,f="",m=[],h,C,k,N,y,b,I,w={val:c(0),position:a,index:1};for(h=0;h<3;h+=1)d[h]=h;for(k=0,y=Math.pow(2,2),b=1;b!=y;)N=w.val&w.position,w.position>>=1,w.position==0&&(w.position=a,w.val=c(w.index++)),k|=(N>0?1:0)*b,b<<=1;switch(l=k){case 0:for(k=0,y=Math.pow(2,8),b=1;b!=y;)N=w.val&w.position,w.position>>=1,w.position==0&&(w.position=a,w.val=c(w.index++)),k|=(N>0?1:0)*b,b<<=1;I=e(k);break;case 1:for(k=0,y=Math.pow(2,16),b=1;b!=y;)N=w.val&w.position,w.position>>=1,w.position==0&&(w.position=a,w.val=c(w.index++)),k|=(N>0?1:0)*b,b<<=1;I=e(k);break;case 2:return""}for(d[3]=I,C=I,m.push(I);;){if(w.index>i)return"";for(k=0,y=Math.pow(2,g),b=1;b!=y;)N=w.val&w.position,w.position>>=1,w.position==0&&(w.position=a,w.val=c(w.index++)),k|=(N>0?1:0)*b,b<<=1;switch(I=k){case 0:for(k=0,y=Math.pow(2,8),b=1;b!=y;)N=w.val&w.position,w.position>>=1,w.position==0&&(w.position=a,w.val=c(w.index++)),k|=(N>0?1:0)*b,b<<=1;d[u++]=e(k),I=u-1,p--;break;case 1:for(k=0,y=Math.pow(2,16),b=1;b!=y;)N=w.val&w.position,w.position>>=1,w.position==0&&(w.position=a,w.val=c(w.index++)),k|=(N>0?1:0)*b,b<<=1;d[u++]=e(k),I=u-1,p--;break;case 2:return m.join("")}if(p==0&&(p=Math.pow(2,g),g++),d[I])f=d[I];else if(I===u)f=C+C.charAt(0);else return null;m.push(f),d[u++]=C+f.charAt(0),p--,C=f,p==0&&(p=Math.pow(2,g),g++)}}};return r}();typeof define=="function"&&define.amd?define(function(){return Bt}):typeof We!="undefined"&&We!=null?We.exports=Bt:typeof angular!="undefined"&&angular!=null&&angular.module("LZString",[]).factory("LZString",function(){return Bt})});var so={};en(so,{applyEffectStyle:()=>Kt,applyFillStyle:()=>zt,applyStrokeStyle:()=>Wt,applyTextStyle:()=>Ht});async function zt(e,t){let n=figma.getNodeById(e);if(!n||!("fillStyleId"in n))return{success:!1,nodeId:e,nodeName:"",property:"fillStyle",oldValue:"",newValue:"",error:"Node not found or does not support fill styles"};try{let s=await figma.importStyleByKeyAsync(t),o=n.fillStyleId||"";return n.fillStyleId=s.id,{success:!0,nodeId:e,nodeName:n.name,property:"fillStyle",oldValue:o?"existing style":"no style",newValue:s.name}}catch(s){return{success:!1,nodeId:e,nodeName:n.name,property:"fillStyle",oldValue:"",newValue:t,error:s instanceof Error?s.message:String(s)}}}async function Wt(e,t){let n=figma.getNodeById(e);if(!n||!("strokeStyleId"in n))return{success:!1,nodeId:e,nodeName:"",property:"strokeStyle",oldValue:"",newValue:"",error:"Node not found or does not support stroke styles"};try{let s=await figma.importStyleByKeyAsync(t),o=n.strokeStyleId||"";return n.strokeStyleId=s.id,{success:!0,nodeId:e,nodeName:n.name,property:"strokeStyle",oldValue:o?"existing style":"no style",newValue:s.name}}catch(s){return{success:!1,nodeId:e,nodeName:n.name,property:"strokeStyle",oldValue:"",newValue:t,error:s instanceof Error?s.message:String(s)}}}async function Ht(e,t){let n=figma.getNodeById(e);if(!n||n.type!=="TEXT")return{success:!1,nodeId:e,nodeName:"",property:"textStyle",oldValue:"",newValue:"",error:"Node not found or is not a text node"};try{let s=await figma.importStyleByKeyAsync(t),o=n,r=o.textStyleId||"";return o.textStyleId=s.id,{success:!0,nodeId:e,nodeName:n.name,property:"textStyle",oldValue:r?"existing style":"no style",newValue:s.name}}catch(s){return{success:!1,nodeId:e,nodeName:n.name,property:"textStyle",oldValue:"",newValue:t,error:s instanceof Error?s.message:String(s)}}}async function Kt(e,t){let n=figma.getNodeById(e);if(!n||!("effectStyleId"in n))return{success:!1,nodeId:e,nodeName:"",property:"effectStyle",oldValue:"",newValue:"",error:"Node not found or does not support effect styles"};try{let s=await figma.importStyleByKeyAsync(t),o=n.effectStyleId||"";return n.effectStyleId=s.id,{success:!0,nodeId:e,nodeName:n.name,property:"effectStyle",oldValue:o?"existing style":"no style",newValue:s.name}}catch(s){return{success:!1,nodeId:e,nodeName:n.name,property:"effectStyle",oldValue:"",newValue:t,error:s instanceof Error?s.message:String(s)}}}var jt=G(()=>{"use strict"});J();J();J();function de(e){var d;let t=e,n=!1;for(;t;){if(t.type==="COMPONENT_SET"){n=!0;break}if(t.parent&&t.parent.type==="COMPONENT_SET"){n=!0;break}t=t.parent}if(!n||!("strokes"in e)||!("cornerRadius"in e)||!("strokeWeight"in e))return!1;let s=e.cornerRadius===5||"topLeftRadius"in e&&"topRightRadius"in e&&"bottomLeftRadius"in e&&"bottomRightRadius"in e&&e.topLeftRadius===5&&e.topRightRadius===5&&e.bottomLeftRadius===5&&e.bottomRightRadius===5,o=e.strokeWeight===1,r=e.strokes,i=r.length>0&&r.some(l=>l.type==="SOLID"&&l.visible!==!1&&l.color?F(l.color.r,l.color.g,l.color.b).toUpperCase()==="#9747FF":!1),a="paddingLeft"in e&&"paddingRight"in e&&"paddingTop"in e&&"paddingBottom"in e&&e.paddingLeft===16&&e.paddingRight===16&&e.paddingTop===16&&e.paddingBottom===16,c=s&&o&&i&&a;return c&&(console.log(`\u{1F3AF} [FILTER] Detected default variant frame styles in ${e.name} - filtering out`),console.log(` Type: ${e.type}, Parent: ${(d=e.parent)==null?void 0:d.type}`),console.log(` Radius: ${String(e.cornerRadius)}, Weight: ${String(e.strokeWeight)}, Color: ${r.length>0&&r[0].type==="SOLID"?F(r[0].color.r,r[0].color.g,r[0].color.b):"none"}`),console.log(` Padding: L=${e.paddingLeft}, R=${e.paddingRight}, T=${e.paddingTop}, B=${e.paddingBottom}`)),c}function we(e){let t=e;for(;t;){if(t.type==="COMPONENT_SET"||t.parent&&t.parent.type==="COMPONENT_SET")return!0;t=t.parent}return!1}async function ue(e){let t=[],n=[],s=[],o=[],r=[],i=new Set,a=new Set,c=new Set,d=new Set,l=new Set;async function p(u){console.log("\u{1F50D} Analyzing node:",u.name,"Type:",u.type);let g=[];if("fillStyleId"in u&&typeof u.fillStyleId=="string"&&g.push(figma.getStyleByIdAsync(u.fillStyleId).then(y=>{if(y!=null&&y.name&&!i.has(y.name)){i.add(y.name);let b=y.name;if("fills"in u&&Array.isArray(u.fills)&&u.fills.length>0){let I=u.fills[0];I.type==="SOLID"&&I.color&&(b=F(I.color.r,I.color.g,I.color.b))}t.push({name:y.name,value:b,type:"fill-style",isToken:!0,isActualToken:!0,source:"figma-style"})}}).catch(console.warn)),"strokeStyleId"in u&&typeof u.strokeStyleId=="string"&&g.push(figma.getStyleByIdAsync(u.strokeStyleId).then(y=>{y!=null&&y.name&&!i.has(y.name)&&(i.add(y.name),t.push({name:y.name,value:y.name,type:"stroke-style",isToken:!0,isActualToken:!0,source:"figma-style"}))}).catch(console.warn)),u.type==="TEXT"&&"textStyleId"in u&&typeof u.textStyleId=="string"&&g.push(figma.getStyleByIdAsync(u.textStyleId).then(y=>{y!=null&&y.name&&!c.has(y.name)&&(c.add(y.name),s.push({name:y.name,value:y.name,type:"text-style",isToken:!0,isActualToken:!0,source:"figma-style"}))}).catch(console.warn)),"effectStyleId"in u&&typeof u.effectStyleId=="string"&&g.push(figma.getStyleByIdAsync(u.effectStyleId).then(y=>{y!=null&&y.name&&!d.has(y.name)&&(d.add(y.name),o.push({name:y.name,value:y.name,type:"effect-style",isToken:!0,isActualToken:!0,source:"figma-style"}))}).catch(console.warn)),await Promise.all(g),"boundVariables"in u&&u.boundVariables){let y=u.boundVariables;console.log(`\u{1F50D} [VARIABLES] Checking bound variables for ${u.name}:`,Object.keys(y));let b=async(x,$,v,T,L)=>{try{let A=Array.isArray(x)?x:[x];for(let ee of A)if(ee!=null&&ee.id&&typeof ee.id=="string"){let Y=await Qe(ee.id);if(console.log(` \u{1F3AF} Found ${$} variable:`,Y),Y&&!v.has(Y)){v.add(Y);let ve=Y;if(L==="color"&&($==="fills"||$==="strokes")){let $e=await tn(ee.id,u);$e&&$e.startsWith("#")&&(ve=$e)}T.push({name:Y,value:ve,type:`${$}-variable`,isToken:!0,isActualToken:!0,source:"figma-variable"}),console.log(` \u2705 Added ${L} token: ${Y} (value: ${ve})`)}}}catch(A){console.warn(`Error processing ${$} variables:`,A)}},I=async(x,$,v,T,L)=>{if(x&&typeof x=="object"&&"id"in x&&typeof x.id=="string"){let A=await Qe(x.id);console.log(` \u{1F3AF} Found ${$} variable:`,A),A&&!v.has(A)&&(v.add(A),T.push({name:A,value:A,type:`${$}-variable`,isToken:!0,isActualToken:!0,source:"figma-variable"}),console.log(` \u2705 Added ${L} token: ${A}`))}},w=[];y.fills&&(console.log(" \u{1F3A8} Processing fills variables..."),w.push(b(y.fills,"fills",i,t,"color"))),y.strokes&&(console.log(" \u{1F58A}\uFE0F Processing strokes variables..."),w.push(b(y.strokes,"strokes",i,t,"color"))),y.effects&&(console.log(" \u2728 Processing effects variables..."),w.push(b(y.effects,"effects",d,o,"effect"))),["strokeWeight","strokeTopWeight","strokeRightWeight","strokeBottomWeight","strokeLeftWeight"].forEach(x=>{y[x]&&(console.log(` \u{1F4CF} Processing ${x} variable...`),w.push(I(y[x],x,l,r,"border")))}),["topLeftRadius","topRightRadius","bottomLeftRadius","bottomRightRadius"].forEach(x=>{y[x]&&(console.log(` \u{1F504} Processing ${x} variable...`),w.push(I(y[x],x,l,r,"border")))}),["paddingTop","paddingRight","paddingBottom","paddingLeft","itemSpacing","counterAxisSpacing"].forEach(x=>{y[x]&&(console.log(` \u{1F4D0} Processing ${x} variable...`),w.push(I(y[x],x,a,n,"spacing")))}),["width","height","minWidth","maxWidth","minHeight","maxHeight"].forEach(x=>{y[x]&&(console.log(` \u{1F4E6} Processing ${x} variable...`),w.push(I(y[x],x,a,n,"size")))}),y.opacity&&(console.log(" \u{1F47B} Processing opacity variable..."),w.push(I(y.opacity,"opacity",d,o,"effect"))),u.type==="TEXT"&&["fontSize","lineHeight","letterSpacing","paragraphSpacing"].forEach($=>{y[$]&&(console.log(` \u{1F4DD} Processing ${$} variable...`),w.push(I(y[$],$,c,s,"typography")))}),await Promise.all(w),console.log(`\u{1F50D} [VARIABLES] Total variables found for ${u.name}: ${Object.keys(y).length}`)}let f="boundVariables"in u&&u.boundVariables&&u.boundVariables.fills,m="fillStyleId"in u&&u.fillStyleId;"fills"in u&&Array.isArray(u.fills)&&!m&&!f?(console.log(`\u{1F50D} [HARD-CODED] Checking fills for ${u.name} (no variables, no style)`),u.fills.forEach(y=>{if(y.type==="SOLID"&&y.visible!==!1&&y.color){let b=F(y.color.r,y.color.g,y.color.b),I=`${b}:${u.id}`;if(!i.has(I)){console.log(` \u26A0\uFE0F Found hard-coded fill: ${b}`),i.add(I);let w=re(u);t.push({name:`hard-coded-fill-${t.length+1}`,value:b,type:"fill",isToken:!1,source:"hard-coded",context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,path:w.path,description:w.description,property:"fills"}})}}})):f?console.log(`\u{1F50D} [VARIABLES] ${u.name} has fill variables - skipping hard-coded detection`):m&&console.log(`\u{1F50D} [STYLES] ${u.name} has fill style - skipping hard-coded detection`);let h="boundVariables"in u&&u.boundVariables&&u.boundVariables.strokes,C="strokeStyleId"in u&&u.strokeStyleId;if("strokes"in u&&Array.isArray(u.strokes)&&!C&&!h?(console.log(`\u{1F50D} [HARD-CODED] Checking strokes for ${u.name} (no variables, no style)`),de(u)?console.log(" \u{1F6AB} Skipping default variant frame stroke colors"):u.strokes.forEach(y=>{if(y.type==="SOLID"&&y.visible!==!1&&y.color){let b=F(y.color.r,y.color.g,y.color.b),I=`${b}:${u.id}`;if(!i.has(I)){console.log(` \u26A0\uFE0F Found hard-coded stroke: ${b}`),i.add(I);let w=re(u);t.push({name:`hard-coded-stroke-${t.length+1}`,value:b,type:"stroke",isToken:!1,source:"hard-coded",isDefaultVariantStyle:b.toUpperCase()==="#9747FF"&&we(u),context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,path:w.path,description:w.description,property:"strokes"}})}}})):h?console.log(`\u{1F50D} [VARIABLES] ${u.name} has stroke variables - skipping hard-coded detection`):C&&console.log(`\u{1F50D} [STYLES] ${u.name} has stroke style - skipping hard-coded detection`),"strokeWeight"in u&&typeof u.strokeWeight=="number"){console.log(`\u{1F50D} Node ${u.name} has strokeWeight: ${u.strokeWeight}`);let y="strokes"in u&&Array.isArray(u.strokes)&&u.strokes.length>0,b=y&&u.strokes.some(P=>P.visible!==!1),I="boundVariables"in u&&u.boundVariables&&["strokeWeight","strokeTopWeight","strokeRightWeight","strokeBottomWeight","strokeLeftWeight"].some(P=>u.boundVariables[P]),w="boundVariables"in u&&u.boundVariables?Object.keys(u.boundVariables):[];if(console.log(` Has strokes: ${y}, Has visible strokes: ${b}, Has strokeWeight variable: ${!!I}, boundVariable keys: [${w.join(", ")}]`),I)console.log(` \u{1F517} ${u.name} has strokeWeight bound to variable - skipping hard-coded detection`);else if(u.strokeWeight>0&&b&&!de(u)){let P=`${u.strokeWeight}px`,O,M=u.strokes.find(x=>x.visible!==!1&&x.type==="SOLID");M&&M.type==="SOLID"&&M.color&&(O=F(M.color.r,M.color.g,M.color.b));let z=`${P}:${u.id}`;if(!l.has(z)){console.log(` \u2705 Adding stroke weight: ${P}`),l.add(z);let x=re(u);r.push({name:`hard-coded-stroke-weight-${u.strokeWeight}`,value:P,type:"stroke-weight",isToken:!1,source:"hard-coded",strokeColor:O,isDefaultVariantStyle:u.strokeWeight===1&&(O==null?void 0:O.toUpperCase())==="#9747FF"&&we(u),context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,hasVisibleStroke:!0,path:x.path,description:x.description,property:"strokeWeight"}})}}else u.strokeWeight>0&&b&&de(u)&&console.log(" \u{1F6AB} Skipping default variant frame stroke weight")}let k="boundVariables"in u&&u.boundVariables&&["topLeftRadius","topRightRadius","bottomLeftRadius","bottomRightRadius","cornerRadius"].some(y=>u.boundVariables[y]);if("cornerRadius"in u&&typeof u.cornerRadius=="number"&&!k)if(console.log(`\u{1F50D} [HARD-CODED] Checking corner radius for ${u.name} (no variables)`),de(u))console.log(" \u{1F6AB} Skipping default variant frame corner radius");else{let y=u.cornerRadius;if(y>0){let b=`${y}px`,I=`${b}:${u.id}`;if(!l.has(I)){console.log(` \u26A0\uFE0F Found hard-coded corner radius: ${b}`),l.add(I);let w=re(u);r.push({name:`hard-coded-corner-radius-${y}`,value:b,type:"corner-radius",isToken:!1,source:"hard-coded",isDefaultVariantStyle:y===5&&we(u),context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,path:w.path,description:w.description,property:"cornerRadius"}})}}}else k&&console.log(`\u{1F50D} [VARIABLES] ${u.name} has radius variables - skipping hard-coded detection`);!k&&"topLeftRadius"in u&&(console.log(`\u{1F50D} [HARD-CODED] Checking individual corner radius for ${u.name} (no variables)`),de(u)?console.log(" \u{1F6AB} Skipping default variant frame individual corner radii"):[{prop:"topLeftRadius",name:"top-left"},{prop:"topRightRadius",name:"top-right"},{prop:"bottomLeftRadius",name:"bottom-left"},{prop:"bottomRightRadius",name:"bottom-right"}].forEach(({prop:b,name:I})=>{if(b in u&&typeof u[b]=="number"){let w=u[b];if(w>0){let P=`${w}px`,O=`${P}:${u.id}:${b}`;if(!l.has(O)){console.log(` \u26A0\uFE0F Found hard-coded ${I} radius: ${P}`),l.add(O);let M=re(u);r.push({name:`hard-coded-${I}-radius-${w}`,value:P,type:`${I}-radius`,isToken:!1,source:"hard-coded",isDefaultVariantStyle:w===5&&we(u),context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,path:M.path,description:M.description,property:b}})}}}}));let N="boundVariables"in u&&u.boundVariables&&["paddingTop","paddingRight","paddingBottom","paddingLeft"].some(y=>u.boundVariables[y]);if("paddingLeft"in u&&typeof u.paddingLeft=="number"&&!N){console.log(`\u{1F50D} [HARD-CODED] Checking padding for ${u.name} (no variables)`);let y=u;[{value:y.paddingLeft,name:"left"},{value:y.paddingRight,name:"right"},{value:y.paddingTop,name:"top"},{value:y.paddingBottom,name:"bottom"}].forEach(I=>{let w=`${I.value}:${u.id}:${I.name}`;if(typeof I.value=="number"&&I.value>1&&!a.has(w)){console.log(` \u26A0\uFE0F Found hard-coded padding-${I.name}: ${I.value}px`),a.add(w);let P=re(u),O=I.value===16&&we(u)&&de(u);n.push({name:`hard-coded-padding-${I.name}-${I.value}`,value:`${I.value}px`,type:"padding",isToken:!1,source:"hard-coded",isDefaultVariantStyle:O,context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,path:P.path,description:P.description,property:`padding${I.name.charAt(0).toUpperCase()+I.name.slice(1)}`}})}})}else N&&console.log(`\u{1F50D} [VARIABLES] ${u.name} has padding variables - skipping hard-coded detection`);if("children"in u)for(let y of u.children)await p(y)}return await p(e),Mo({colors:t,spacing:n,typography:s,effects:o,borders:r})}function Mo(e){let t=["colors","spacing","typography","effects","borders"],n={totalTokens:0,actualTokens:0,hardCodedValues:0,aiSuggestions:0,byCategory:{}};return t.forEach(s=>{let o=e[s].map(c=>K(R({},c),{isActualToken:c.source==="figma-style"||c.source==="figma-variable",recommendation:Oo(c,s),suggestion:Fo(c,s)})),r=o.filter(c=>!c.isDefaultVariantStyle),i=r.filter(c=>c.isActualToken).length,a=r.filter(c=>c.source==="hard-coded").length;n.byCategory[s]={total:r.length,tokens:i,hardCoded:a,suggestions:0},n.totalTokens+=r.length,n.actualTokens+=i,n.hardCodedValues+=a,e[s]=o}),K(R({},e),{summary:n})}function Oo(e,t){if(e.isToken)return`Using ${e.name} token`;switch(t){case"colors":return`Consider using a color token instead of ${e.value}`;case"spacing":return`Consider using spacing token instead of ${e.value}`;case"typography":return"Consider using typography token";case"effects":return"Consider using effect token";case"borders":return"Consider using border radius token";default:return"Consider using a design token"}}function Fo(e,t){var n,s;switch(t){case"colors":return(n=e.value)!=null&&n.startsWith("#000")?"Use semantic color token (e.g., text.primary)":(s=e.value)!=null&&s.startsWith("#FFF")?"Use semantic color token (e.g., background.primary)":"Create or use existing color token";case"spacing":let o=parseInt(e.value||"0");return o%8===0?"Create or use existing spacing token (follows 8px grid)":o%4===0?"Create or use existing spacing token (follows 4px grid)":"Create or use existing spacing token";case"typography":return"Use semantic typography token (e.g., heading.large, body.regular)";case"effects":return"Use semantic shadow token (e.g., shadow.small, shadow.medium)";case"borders":return"Use appropriate radius token (e.g., radius.small, radius.medium)";default:return"Create or use existing design token"}}function nn(e){return`You are an expert design system architect analyzing a Figma component for comprehensive metadata and design token recommendations. **Component Analysis Context:** - Component Name: ${e.name} @@ -197,18 +197,18 @@ For the "recommendedProperties" field, compare the component's EXISTING properti - Effects: \`[effect]-[intensity]-[purpose]\` (e.g., "shadow-md-default", "blur-backdrop-light") - Borders: \`radius-[size]-[value]\` (e.g., "radius-md-8px", "radius-full-999px") -Focus on creating a comprehensive DESIGN analysis that helps designers build scalable, consistent, and well-structured Figma components.`}function pe(e){try{console.log("\u{1F50D} Starting JSON extraction from LLM response..."),console.log("\u{1F4DD} Response length:",e.length),console.log("\u{1F4DD} Response preview (first 200 chars):",e.substring(0,200));try{let n=JSON.parse(e.trim());return console.log("\u2705 Successfully parsed entire response as JSON"),n}catch(n){console.log("\u26A0\uFE0F Full response is not valid JSON, trying to extract JSON block...")}let t=[()=>sn(e),()=>Vo(e),()=>_o(e),()=>Bo(e)];for(let n=0;n<t.length;n++)try{console.log(`\u{1F50D} Trying extraction strategy ${n+1}...`);let s=t[n]();if(s)return console.log("\u2705 Successfully extracted JSON with strategy",n+1),s}catch(s){let o=s instanceof Error?s.message:"Unknown error";console.log(`\u26A0\uFE0F Strategy ${n+1} failed:`,o);continue}throw new Error("No valid JSON found in response after trying all strategies")}catch(t){throw console.error("\u274C Failed to parse JSON from LLM response:",t),console.log("\u{1F4DD} Full response for debugging:",e),new Error("Invalid JSON response from LLM API")}}function sn(e){let t=e.indexOf("{");if(t===-1)return null;let n=0,s=!1,o=!1;for(let r=t;r<e.length;r++){let i=e[r];if(o){o=!1;continue}if(i==="\\"){o=!0;continue}if(i==='"'){s=!s;continue}if(!s){if(i==="{")n++;else if(i==="}"&&(n--,n===0)){let a=e.substring(t,r+1);try{return JSON.parse(a)}catch(c){return console.log("\u26A0\uFE0F Balanced JSON extraction found malformed JSON:",c instanceof Error?c.message:"Parse error"),null}}}}return console.log("\u26A0\uFE0F JSON appears to be truncated, attempting reconstruction..."),Fo(e,t)}function Fo(e,t){try{let s=e.substring(t).split(` +Focus on creating a comprehensive DESIGN analysis that helps designers build scalable, consistent, and well-structured Figma components.`}function pe(e){try{console.log("\u{1F50D} Starting JSON extraction from LLM response..."),console.log("\u{1F4DD} Response length:",e.length),console.log("\u{1F4DD} Response preview (first 200 chars):",e.substring(0,200));try{let n=JSON.parse(e.trim());return console.log("\u2705 Successfully parsed entire response as JSON"),n}catch(n){console.log("\u26A0\uFE0F Full response is not valid JSON, trying to extract JSON block...")}let t=[()=>sn(e),()=>_o(e),()=>Bo(e),()=>Uo(e)];for(let n=0;n<t.length;n++)try{console.log(`\u{1F50D} Trying extraction strategy ${n+1}...`);let s=t[n]();if(s)return console.log("\u2705 Successfully extracted JSON with strategy",n+1),s}catch(s){let o=s instanceof Error?s.message:"Unknown error";console.log(`\u26A0\uFE0F Strategy ${n+1} failed:`,o);continue}throw new Error("No valid JSON found in response after trying all strategies")}catch(t){throw console.error("\u274C Failed to parse JSON from LLM response:",t),console.log("\u{1F4DD} Full response for debugging:",e),new Error("Invalid JSON response from LLM API")}}function sn(e){let t=e.indexOf("{");if(t===-1)return null;let n=0,s=!1,o=!1;for(let r=t;r<e.length;r++){let i=e[r];if(o){o=!1;continue}if(i==="\\"){o=!0;continue}if(i==='"'){s=!s;continue}if(!s){if(i==="{")n++;else if(i==="}"&&(n--,n===0)){let a=e.substring(t,r+1);try{return JSON.parse(a)}catch(c){return console.log("\u26A0\uFE0F Balanced JSON extraction found malformed JSON:",c instanceof Error?c.message:"Parse error"),null}}}}return console.log("\u26A0\uFE0F JSON appears to be truncated, attempting reconstruction..."),Do(e,t)}function Do(e,t){try{let s=e.substring(t).split(` `),o="",r=0,i=!1,a=!1;for(let d=0;d<s.length;d++){let l=s[d];for(let p=0;p<l.length;p++){let u=l[p];if(a){a=!1;continue}if(u==="\\"){a=!0;continue}if(u==='"'){i=!i;continue}i||(u==="{"?r++:u==="}"&&r--)}if(i||l.trim().endsWith(",")===!1&&d<s.length-1)break;o+=l+` `}for(;r>0;)o+=`} -`,r--;let c=JSON.parse(o.trim());return console.log("\u2705 Successfully reconstructed truncated JSON"),c}catch(n){return console.log("\u26A0\uFE0F Failed to reconstruct truncated JSON:",n instanceof Error?n.message:"Unknown error"),Do(e)}}function Do(e){try{console.log("\u{1F504} Attempting to extract basic component info as fallback...");let t=e.match(/"component":\s*"([^"]+)"/),n=e.match(/"description":\s*"([^"]+)"/);if(t&&n){let s={component:t[1],description:n[1],props:[],states:["default"],variants:{},tokens:{colors:[],spacing:[],typography:[]},audit:{tokenOpportunities:["Review and simplify component analysis"]},mcpReadiness:{score:60,strengths:["Component has basic structure"],gaps:["Analysis was incomplete due to response size"],recommendations:["Simplify component structure","Use MCP-enhanced analysis for better results"]},propertyCheatSheet:[]};return console.log("\u2705 Extracted basic component info as fallback"),s}return null}catch(t){return console.log("\u26A0\uFE0F Failed to extract basic component info:",t instanceof Error?t.message:"Unknown error"),null}}function Vo(e){let t=[["```json","```"],["```","```"],["JSON:",` +`,r--;let c=JSON.parse(o.trim());return console.log("\u2705 Successfully reconstructed truncated JSON"),c}catch(n){return console.log("\u26A0\uFE0F Failed to reconstruct truncated JSON:",n instanceof Error?n.message:"Unknown error"),Vo(e)}}function Vo(e){try{console.log("\u{1F504} Attempting to extract basic component info as fallback...");let t=e.match(/"component":\s*"([^"]+)"/),n=e.match(/"description":\s*"([^"]+)"/);if(t&&n){let s={component:t[1],description:n[1],props:[],states:["default"],variants:{},tokens:{colors:[],spacing:[],typography:[]},audit:{tokenOpportunities:["Review and simplify component analysis"]},mcpReadiness:{score:60,strengths:["Component has basic structure"],gaps:["Analysis was incomplete due to response size"],recommendations:["Simplify component structure","Use MCP-enhanced analysis for better results"]},propertyCheatSheet:[]};return console.log("\u2705 Extracted basic component info as fallback"),s}return null}catch(t){return console.log("\u26A0\uFE0F Failed to extract basic component info:",t instanceof Error?t.message:"Unknown error"),null}}function _o(e){let t=[["```json","```"],["```","```"],["JSON:",` `],["Response:",` `],["{",`} `]];for(let[n,s]of t){let o=e.indexOf(n);if(o===-1)continue;let r=o+n.length,i=e.indexOf(s,r);if(i===-1&&s===` -`&&(i=e.length),i===-1)continue;let a=e.substring(r,i).trim();try{return JSON.parse(a)}catch(c){if(a.startsWith("{"))try{return sn(a)}catch(d){continue}}}return null}function _o(e){let t=/```(?:json)?\s*(\{[\s\S]*?\})\s*```/gi,n;for(;(n=t.exec(e))!==null;)try{return JSON.parse(n[1])}catch(s){continue}return null}function Bo(e){let t=e.match(/\{[\s\S]*\}/);return t?JSON.parse(t[0]):null}function Fe(e){if(!e||typeof e!="object")return e;let t=["aria","accessibility api","semantic html","keyboard navigation","event handler","interactive behavior","onclick","onchange","state management","controlled component","uncontrolled component","props","responsive breakpoint","css implementation","@media","animation token","transition timing","programmatic animation","keyframe","api integration","data binding","dynamic content","fetch","axios","implement","add handler","bind event","attach listener","programming pattern","functional pattern","react hook","usestate","useeffect"],n=r=>{let i=r.toLowerCase();return t.some(a=>i.includes(a))},s=r=>Array.isArray(r)?r.filter(i=>{if(typeof i=="string"){let a=!n(i);return a||console.log("\u{1F6AB} [FILTER] Removed development-focused recommendation:",i),a}return!0}):r,o=JSON.parse(JSON.stringify(e));return o.mcpReadiness&&(o.mcpReadiness.recommendations&&(o.mcpReadiness.recommendations=s(o.mcpReadiness.recommendations)),o.mcpReadiness.gaps&&(o.mcpReadiness.gaps=s(o.mcpReadiness.gaps))),o.audit&&(o.audit.tokenOpportunities&&(o.audit.tokenOpportunities=s(o.audit.tokenOpportunities)),o.audit.structureIssues&&(o.audit.structureIssues=s(o.audit.structureIssues))),o.accessibility&&(o.accessibility.designConsiderations&&(o.accessibility.designConsiderations=s(o.accessibility.designConsiderations)),o.accessibility.visualIndicators&&(o.accessibility.visualIndicators=s(o.accessibility.visualIndicators))),o}var E=class extends Error{constructor(n,s,o,r){super(n);this.code=s;this.statusCode=o;this.retryAfter=r;this.name="LLMError"}};var tt={anthropic:"claude-sonnet-4-5-20250929",openai:"gpt-5.2",google:"gemini-2.5-pro"};var Uo=[{id:"claude-opus-4-5-20251218",name:"Claude Opus 4.5",description:"Flagship model - Most capable, best for complex analysis and reasoning",contextWindow:2e5,isDefault:!1},{id:"claude-sonnet-4-5-20250929",name:"Claude Sonnet 4.5",description:"Standard model - Balanced performance and cost, recommended for most tasks",contextWindow:2e5,isDefault:!0},{id:"claude-haiku-4-5-20251001",name:"Claude Haiku 4.5",description:"Economy model - Fastest responses, ideal for quick analysis",contextWindow:2e5,isDefault:!1}],nt=class{constructor(){this.name="Anthropic";this.id="anthropic";this.endpoint="https://api.anthropic.com/v1/messages";this.keyPrefix="sk-ant-";this.keyPlaceholder="sk-ant-...";this.models=Uo}formatRequest(t){let n={model:t.model,messages:[{role:"user",content:t.prompt.trim()}],max_tokens:t.maxTokens};return t.temperature!==void 0&&(n.temperature=t.temperature),t.additionalParams&&Object.assign(n,t.additionalParams),n}parseResponse(t){let n=t;if(!n.content||!Array.isArray(n.content))throw new E("Invalid response format from Anthropic API: missing content array","INVALID_REQUEST");let s=n.content.filter(o=>o.type==="text").map(o=>o.text).join(` -`);if(!s)throw new E("Invalid response format from Anthropic API: no text content found","INVALID_REQUEST");return{content:s.trim(),model:n.model,usage:n.usage?{promptTokens:n.usage.input_tokens,completionTokens:n.usage.output_tokens,totalTokens:n.usage.input_tokens+n.usage.output_tokens}:void 0,metadata:{id:n.id,stopReason:n.stop_reason}}}validateApiKey(t){if(!t||typeof t!="string")return{isValid:!1,error:"API Key Required: Please provide a valid Claude API key."};let n=t.trim();return n.length===0?{isValid:!1,error:"API Key Required: The Claude API key cannot be empty."}:n.startsWith(this.keyPrefix)?n.length<40?{isValid:!1,error:"Invalid API Key Format: The API key appears to be too short. Please verify you copied the complete key."}:{isValid:!0}:{isValid:!1,error:`Invalid API Key Format: Claude API keys should start with "${this.keyPrefix}". Please check your API key.`}}getHeaders(t){return{"content-type":"application/json","x-api-key":t.trim(),"anthropic-version":"2023-06-01","anthropic-dangerous-direct-browser-access":"true"}}getDefaultModel(){return this.models.find(n=>n.isDefault)||this.models[1]}handleError(t,n){var r;let s=n,o=((r=s==null?void 0:s.error)==null?void 0:r.message)||(typeof n=="string"?n:"Unknown error");switch(t){case 400:return new E(`Claude API Error (400): ${o}. Please check your request format.`,"INVALID_REQUEST",400);case 401:return new E("Claude API Error (401): Invalid API key. Please check your Claude API key in settings.","INVALID_API_KEY",401);case 403:return new E("Claude API Error (403): Access forbidden. Please check your API key permissions.","INVALID_API_KEY",403);case 404:return new E(`Claude API Error (404): ${o}. The requested model may not be available.`,"MODEL_NOT_FOUND",404);case 429:return new E("Claude API Error (429): Rate limit exceeded. Please try again later.","RATE_LIMIT_EXCEEDED",429);case 500:return new E("Claude API Error (500): Server error. The Claude API is experiencing issues. Please try again later.","SERVER_ERROR",500);case 503:return new E("Claude API Error (503): Service unavailable. The Claude API is temporarily down. Please try again later.","SERVICE_UNAVAILABLE",503);default:return new E(`Claude API Error (${t}): ${o}`,"UNKNOWN_ERROR",t)}}},ot=new nt;var Go=[{id:"gpt-5.2",name:"GPT-5.2",description:"Flagship model with advanced reasoning capabilities",contextWindow:128e3,isDefault:!0},{id:"gpt-5.2-pro",name:"GPT-5.2 Pro",description:"Premium model with extended reasoning for complex tasks",contextWindow:128e3,isDefault:!1},{id:"gpt-5-mini",name:"GPT-5 Mini",description:"Economy model - fast and cost-effective",contextWindow:128e3,isDefault:!1}],rt=class{constructor(){this.name="OpenAI";this.id="openai";this.endpoint="https://api.openai.com/v1/chat/completions";this.keyPrefix="sk-";this.keyPlaceholder="sk-...";this.models=Go}formatRequest(t){let n={model:t.model,messages:[{role:"user",content:t.prompt}],max_completion_tokens:t.maxTokens,temperature:t.temperature};return t.additionalParams&&Object.assign(n,t.additionalParams),n}parseResponse(t){let n=t;if(!n.choices||n.choices.length===0)throw new E("Invalid response format: no choices returned","INVALID_REQUEST");let s=n.choices[0];if(!s.message||typeof s.message.content!="string")throw new E("Invalid response format: missing message content","INVALID_REQUEST");let o={content:s.message.content.trim(),model:n.model};return n.usage&&(o.usage={promptTokens:n.usage.prompt_tokens,completionTokens:n.usage.completion_tokens,totalTokens:n.usage.total_tokens}),o.metadata={id:n.id,finishReason:s.finish_reason,created:n.created},o}validateApiKey(t){if(!t||typeof t!="string")return{isValid:!1,error:"API Key Required: Please provide a valid OpenAI API key."};let n=t.trim();return n.length===0?{isValid:!1,error:"API Key Required: The OpenAI API key cannot be empty."}:n.startsWith(this.keyPrefix)?n.length<20?{isValid:!1,error:"Invalid API Key Format: The API key appears to be too short. Please verify you copied the complete key."}:{isValid:!0}:{isValid:!1,error:`Invalid API Key Format: OpenAI API keys should start with "${this.keyPrefix}". Please check your API key.`}}getHeaders(t){return{"Content-Type":"application/json",Authorization:`Bearer ${t.trim()}`}}getDefaultModel(){return this.models.find(n=>n.isDefault)||this.models[0]}handleError(t,n){var r;let s=n,o=((r=s==null?void 0:s.error)==null?void 0:r.message)||(s==null?void 0:s.message)||"Unknown error occurred";switch(t){case 400:return o.toLowerCase().includes("context_length_exceeded")||o.toLowerCase().includes("maximum context length")?new E(`OpenAI API Error (400): Context length exceeded. ${o}`,"CONTEXT_LENGTH_EXCEEDED",t):new E(`OpenAI API Error (400): ${o}. Please check your request format.`,"INVALID_REQUEST",t);case 401:return new E("OpenAI API Error (401): Invalid API key. Please check your OpenAI API key in settings.","INVALID_API_KEY",t);case 403:return new E("OpenAI API Error (403): Access forbidden. Please check your API key permissions or account status.","INVALID_API_KEY",t);case 404:return new E(`OpenAI API Error (404): Model not found. ${o}`,"MODEL_NOT_FOUND",t);case 429:let i=o.match(/try again in (\d+)/i),a=i?parseInt(i[1],10):void 0;return new E(`OpenAI API Error (429): Rate limit exceeded. ${a?`Please try again in ${a} seconds.`:"Please try again later."}`,"RATE_LIMIT_EXCEEDED",t,a);case 500:return new E("OpenAI API Error (500): Server error. The OpenAI API is experiencing issues. Please try again later.","SERVER_ERROR",t);case 502:return new E("OpenAI API Error (502): Bad gateway. The OpenAI API is temporarily unavailable. Please try again later.","SERVICE_UNAVAILABLE",t);case 503:return new E("OpenAI API Error (503): Service unavailable. The OpenAI API is temporarily down. Please try again later.","SERVICE_UNAVAILABLE",t);case 504:return new E("OpenAI API Error (504): Gateway timeout. The request took too long. Please try again.","SERVICE_UNAVAILABLE",t);default:return new E(`OpenAI API Error (${t}): ${o}`,"UNKNOWN_ERROR",t)}}},it=new rt;var zo=[{id:"gemini-3-pro-preview",name:"Gemini 3 Pro",description:"Flagship model with advanced reasoning and multimodal capabilities",contextWindow:1e6,isDefault:!0},{id:"gemini-2.5-pro",name:"Gemini 2.5 Pro",description:"Standard reasoning model with excellent performance",contextWindow:1e6,isDefault:!1},{id:"gemini-2.5-flash",name:"Gemini 2.5 Flash",description:"Economy model optimized for speed and efficiency",contextWindow:1e6,isDefault:!1}],at=class{constructor(){this.name="Google";this.id="google";this.endpoint="https://generativelanguage.googleapis.com/v1beta/models";this.keyPrefix="AIza";this.keyPlaceholder="AIza...";this.models=zo}formatRequest(t){let n={contents:[{parts:[{text:t.prompt}]}],generationConfig:{maxOutputTokens:t.maxTokens,temperature:t.temperature}};if(t.additionalParams){let{topP:s,topK:o,stopSequences:r}=t.additionalParams;s!==void 0&&(n.generationConfig.topP=s),o!==void 0&&(n.generationConfig.topK=o),r!==void 0&&(n.generationConfig.stopSequences=r)}return n}parseResponse(t){var c;let n=t;if(n.error)throw new E(n.error.message||"Unknown Gemini API error",this.mapErrorCodeToLLMErrorCode(n.error.code,n.error.status),n.error.code);if(!n.candidates||n.candidates.length===0){let d=Object.keys(n);throw new E(`No candidates in Gemini response. Response keys: [${d.join(", ")}]${n.error?`. Error: ${n.error.message}`:""}`,"INVALID_REQUEST")}let s=n.candidates[0];if(s.finishReason==="SAFETY")throw new E("Gemini response blocked by safety filters. Try rephrasing the prompt.","INVALID_REQUEST");let o=(c=s.content)==null?void 0:c.parts;if(!o||o.length===0)throw new E(`No content parts in Gemini response. Finish reason: ${s.finishReason||"unknown"}. Has content: ${!!s.content}`,"INVALID_REQUEST");let r=o.find(d=>typeof d.text=="string");if(!r||!r.text){let d=o.map(l=>Object.keys(l).join(",")).join("; ");throw new E(`No text content in Gemini response parts. Part types: [${d}]. Finish reason: ${s.finishReason||"unknown"}`,"INVALID_REQUEST")}let a={content:r.text,model:"gemini"};return n.usageMetadata&&(a.usage={promptTokens:n.usageMetadata.promptTokenCount||0,completionTokens:n.usageMetadata.candidatesTokenCount||0,totalTokens:n.usageMetadata.totalTokenCount||0}),a}validateApiKey(t){if(!t||typeof t!="string")return{isValid:!1,error:"API key is required"};let n=t.trim();return n.length===0?{isValid:!1,error:"API key cannot be empty"}:n.startsWith(this.keyPrefix)?n.length<30||n.length>50?{isValid:!1,error:"API key appears to have an invalid length. Please verify you copied the complete key."}:/^[A-Za-z0-9_-]+$/.test(n)?{isValid:!0}:{isValid:!1,error:"API key contains invalid characters"}:{isValid:!1,error:`Google API keys should start with "${this.keyPrefix}". Please check your API key.`}}getHeaders(t){return{"Content-Type":"application/json"}}getEndpoint(t,n){let s=n.trim();return`${this.endpoint}/${t}:generateContent?key=${s}`}getDefaultModel(){return this.models.find(n=>n.isDefault)||this.models[0]}handleError(t,n){var p,u;let s=n,o=s==null?void 0:s.error,r=(o==null?void 0:o.message)||"Unknown Google API error",i=o==null?void 0:o.status,a=(o==null?void 0:o.code)||t,c=this.mapErrorCodeToLLMErrorCode(a,i),d;if(t===429){d=6e4;let g=(p=o==null?void 0:o.details)==null?void 0:p.find(f=>{var m;return(m=f["@type"])==null?void 0:m.includes("RetryInfo")});if((u=g==null?void 0:g.metadata)!=null&&u.retryDelay){let f=g.metadata.retryDelay.match(/(\d+)s/);f&&(d=parseInt(f[1],10)*1e3)}}let l=r;switch(c){case"INVALID_API_KEY":l="Google API Error: Invalid API key. Please check your API key in settings.";break;case"RATE_LIMIT_EXCEEDED":l=`Google API Error: Rate limit exceeded. ${d?`Please try again in ${Math.ceil(d/1e3)} seconds.`:"Please try again later."}`;break;case"MODEL_NOT_FOUND":l="Google API Error: Model not found. Please select a valid model.";break;case"CONTEXT_LENGTH_EXCEEDED":l="Google API Error: Input too long. Please reduce the size of your request.";break;case"SERVER_ERROR":l="Google API Error: Server error. Please try again later.";break;case"SERVICE_UNAVAILABLE":l="Google API Error: Service temporarily unavailable. Please try again later.";break}return new E(l,c,t,d)}mapErrorCodeToLLMErrorCode(t,n){if(n){let s=n.toUpperCase();if(s==="INVALID_ARGUMENT")return"INVALID_REQUEST";if(s==="PERMISSION_DENIED"||s==="UNAUTHENTICATED")return"INVALID_API_KEY";if(s==="NOT_FOUND")return"MODEL_NOT_FOUND";if(s==="RESOURCE_EXHAUSTED")return"RATE_LIMIT_EXCEEDED";if(s==="UNAVAILABLE")return"SERVICE_UNAVAILABLE"}switch(t){case 400:return"INVALID_REQUEST";case 401:case 403:return"INVALID_API_KEY";case 404:return"MODEL_NOT_FOUND";case 429:return"RATE_LIMIT_EXCEEDED";case 500:return"SERVER_ERROR";case 503:return"SERVICE_UNAVAILABLE";default:return"UNKNOWN_ERROR"}}},ct=new at;var Wo={anthropic:ot,openai:it,google:ct};function re(e){let t=Wo[e];if(!t)throw new E(`Unknown provider: ${e}`,"INVALID_REQUEST",400);return t}async function ie(e,t,n){var c,d;let s=re(e),o=s.validateApiKey(t);if(!o.isValid)throw new E(o.error||"Invalid API key format","INVALID_API_KEY",401);let r=s.formatRequest(n),i=s.getHeaders(t),a=s.endpoint;e==="google"&&(a=`${s.endpoint}/${n.model}:generateContent?key=${t.trim()}`);try{console.log(`Making ${s.name} API call to ${a}...`);let l=await fetch(a,{method:"POST",headers:i,body:JSON.stringify(r)});if(!l.ok){let u;try{u=await l.json()}catch(g){u=await l.text()}throw s.handleError(l.status,u)}let p=await l.json();return console.log(`${s.name} API response status: ${l.status}`),console.log(`${s.name} API response keys:`,Object.keys(p)),e==="google"&&(console.log("Gemini response candidates:",p.candidates?p.candidates.length:"none"),(c=p.candidates)!=null&&c[0]&&(console.log("Gemini candidate[0] keys:",Object.keys(p.candidates[0])),p.candidates[0].content&&console.log("Gemini content parts:",((d=p.candidates[0].content.parts)==null?void 0:d.length)||"none")),p.error&&console.log("Gemini error:",JSON.stringify(p.error))),s.parseResponse(p)}catch(l){throw l instanceof E?l:l instanceof Error&&(l.message.includes("Failed to fetch")||l.message.includes("NetworkError"))?new E(`Network error connecting to ${s.name}. Please check your internet connection.`,"NETWORK_ERROR"):new E(`Unexpected error calling ${s.name}: ${l instanceof Error?l.message:"Unknown error"}`,"UNKNOWN_ERROR")}}var W={SELECTED_PROVIDER:"selected-provider",SELECTED_MODEL:"selected-model",apiKey:e=>`${e}-api-key`,LEGACY_CLAUDE_KEY:"claude-api-key",LEGACY_CLAUDE_MODEL:"claude-model"},Ho={provider:"anthropic",model:tt.anthropic};async function Ko(){try{let e=await figma.clientStorage.getAsync(W.LEGACY_CLAUDE_KEY),t=await figma.clientStorage.getAsync(W.LEGACY_CLAUDE_MODEL);return e?{needsMigration:!0,legacyKey:e,legacyModel:t}:{needsMigration:!1}}catch(e){return{needsMigration:!1}}}async function lt(){let e=await Ko();e.needsMigration&&(console.log("Migrating legacy Claude storage to multi-provider format..."),e.legacyKey&&await figma.clientStorage.setAsync(W.apiKey("anthropic"),e.legacyKey),await figma.clientStorage.setAsync(W.SELECTED_PROVIDER,"anthropic"),e.legacyModel&&await figma.clientStorage.setAsync(W.SELECTED_MODEL,e.legacyModel),await figma.clientStorage.deleteAsync(W.LEGACY_CLAUDE_KEY),await figma.clientStorage.deleteAsync(W.LEGACY_CLAUDE_MODEL),console.log("Migration complete"))}async function dt(){await lt();let e=await figma.clientStorage.getAsync(W.SELECTED_PROVIDER)||Ho.provider,t=await figma.clientStorage.getAsync(W.SELECTED_MODEL)||tt[e],n=await figma.clientStorage.getAsync(W.apiKey(e));return{providerId:e,modelId:t,apiKey:n}}async function ut(e,t,n){await figma.clientStorage.setAsync(W.SELECTED_PROVIDER,e),await figma.clientStorage.setAsync(W.SELECTED_MODEL,t),n!==void 0&&await figma.clientStorage.setAsync(W.apiKey(e),n)}async function on(e){await figma.clientStorage.deleteAsync(W.apiKey(e))}var rn=/^(Frame|Rectangle|Ellipse|Group|Vector|Line|Polygon|Star|Text|Component|Instance|Slice|Boolean|Union|Subtract|Intersect|Exclude)\s*\d*$/i,an=/\s+\d+$/,cn={button:"btn",icon:"ico",input:"input",text:"txt",image:"img",container:"container",card:"card",list:"list","list-item":"list-item",nav:"nav",header:"header",footer:"footer",modal:"modal",dropdown:"dropdown",checkbox:"checkbox",radio:"radio",toggle:"toggle",avatar:"avatar",badge:"badge",divider:"divider",spacer:"spacer",link:"link",tab:"tab",tooltip:"tooltip",alert:"alert",progress:"progress",skeleton:"skeleton",unknown:"layer"},me=[["btn","button"],["button","button"],["cta","button"],["submit","button"],["icon","icon"],["ico","icon"],["glyph","icon"],["symbol","icon"],["arrow","icon"],["chevron","icon"],["close","icon"],["plus","icon"],["minus","icon"],["txt","text"],["label","text"],["title","text"],["heading","text"],["paragraph","text"],["description","text"],["caption","text"],["subtitle","text"],["input","input"],["field","input"],["textfield","input"],["textarea","input"],["searchfield","input"],["searchbox","input"],["image","image"],["img","image"],["photo","image"],["picture","image"],["thumbnail","image"],["cover","image"],["container","container"],["wrapper","container"],["content","container"],["section","container"],["block","container"],["box","container"],["card","card"],["tile","card"],["panel","card"],["list","list"],["items","list"],["item","list-item"],["row","list-item"],["listitem","list-item"],["nav","nav"],["navbar","nav"],["navigation","nav"],["sidebar","nav"],["breadcrumb","nav"],["menu","nav"],["header","header"],["topbar","header"],["footer","footer"],["bottombar","footer"],["modal","modal"],["dialog","modal"],["popup","modal"],["overlay","modal"],["dropdown","dropdown"],["select","dropdown"],["picker","dropdown"],["combobox","dropdown"],["checkbox","checkbox"],["checkmark","checkbox"],["radio","radio"],["toggle","toggle"],["switch","toggle"],["avatar","avatar"],["profile","avatar"],["userpic","avatar"],["badge","badge"],["tag","badge"],["chip","badge"],["pill","badge"],["status","badge"],["divider","divider"],["separator","divider"],["hr","divider"],["spacer","spacer"],["gap","spacer"],["link","link"],["anchor","link"],["href","link"],["tab","tab"],["tabs","tab"],["tabbar","tab"],["tooltip","tooltip"],["hint","tooltip"],["popover","tooltip"],["alert","alert"],["notification","alert"],["toast","alert"],["message","alert"],["snackbar","alert"],["banner","alert"],["progress","progress"],["loader","progress"],["loading","progress"],["spinner","progress"],["progressbar","progress"],["skeleton","skeleton"],["placeholder","skeleton"],["shimmer","skeleton"]];function ln(e){if(!e||typeof e!="string")return!0;let t=e.trim();return!!(rn.test(t)||t.length===1||/^\d+$/.test(t))}function jo(e){return an.test(e.trim())}function De(e){let t=e.name.toLowerCase();for(let n=0;n<me.length;n++){let s=me[n];if(t.indexOf(s[0])!==-1)return s[1]}switch(e.type){case"TEXT":return"text";case"VECTOR":case"STAR":case"POLYGON":case"BOOLEAN_OPERATION":return"icon";case"RECTANGLE":case"ELLIPSE":case"LINE":if("fills"in e&&Array.isArray(e.fills)){let n=e.fills,s=!1;for(let o=0;o<n.length;o++){let r=n[o];if(r.type==="IMAGE"&&r.visible!==!1){s=!0;break}}if(s)return"image"}if("width"in e&&"height"in e){let n=e.width,s=e.height,o=n/s;if(s<=2&&n>20||n<=2&&s>20)return"divider";if(n<=32&&s<=32&&o>.5&&o<2)return"spacer"}return"unknown";case"FRAME":case"GROUP":return dn(e);case"COMPONENT":case"INSTANCE":return un(e);case"COMPONENT_SET":return qo(e);default:return"unknown"}}function dn(e){if(!("children"in e)||e.children.length===0)return"container";let t=e.children,n=[],s=[];for(let c=0;c<t.length;c++)n.push(t[c].type),s.push(t[c].name.toLowerCase());let o=!1,r=!1;for(let c=0;c<n.length;c++)n[c]==="TEXT"&&(o=!0),(n[c]==="VECTOR"||s[c].indexOf("icon")!==-1)&&(r=!0);let i="width"in e&&"height"in e&&e.width<300&&e.height<100;if(o&&i&&(r||t.length<=3)&&"layoutMode"in e&&e.layoutMode!=="NONE")return"button";let a=!1;for(let c=0;c<n.length;c++)if(n[c]==="RECTANGLE"||s[c].indexOf("image")!==-1){a=!0;break}if(o&&a&&t.length>=2)return"card";if(t.length>=3){let c=t[0].type,d=!0;for(let l=1;l<t.length;l++)if(t[l].type!==c){d=!1;break}if(d&&(c==="FRAME"||c==="INSTANCE"))return"list"}if("cornerRadius"in e&&e.cornerRadius&&t.length<=2&&o&&i)return"input";if("layoutMode"in e&&e.layoutMode==="HORIZONTAL"){let c=0;for(let d=0;d<t.length;d++){let l=t[d].type;(l==="FRAME"||l==="INSTANCE"||l==="TEXT")&&c++}if(c>=3&&i)return"nav"}return"container"}function un(e){let t=e.name.toLowerCase();for(let n=0;n<me.length;n++){let s=me[n];if(t.indexOf(s[0])!==-1)return s[1]}return"children"in e?dn(e):"unknown"}function qo(e){let t=e.name.toLowerCase();for(let n=0;n<me.length;n++){let s=me[n];if(t.indexOf(s[0])!==-1)return s[1]}return"children"in e&&e.children.length>0?un(e.children[0]):"unknown"}function pn(e,t=10){let n=[];function s(o,r,i){if(r>t)return;let a=i?`${i} > ${o.name}`:o.name,c=De(o);if(ln(o.name)){let d=fe(o);n.push({nodeId:o.id,nodeName:o.name,currentName:o.name,suggestedName:d,severity:"error",reason:"Generic layer name detected",layerType:c,depth:r,path:a})}else if(jo(o.name)){let d=o.name.replace(an,"").trim(),l=fe(o);n.push({nodeId:o.id,nodeName:o.name,currentName:o.name,suggestedName:l!==o.name?l:d,severity:"warning",reason:"Layer name has numbered suffix (possible duplicate)",layerType:c,depth:r,path:a})}if("children"in o)for(let d=0;d<o.children.length;d++)s(o.children[d],r+1,a)}return s(e,0,""),n}function fe(e){let t=De(e);return e.type==="TEXT"?Xo(e):e.type==="VECTOR"||e.type==="STAR"||e.type==="POLYGON"||e.type==="BOOLEAN_OPERATION"?Jo(e):"children"in e&&e.children.length>0?Yo(e):cn[t]||"layer"}function Jo(e){let n=e.name.toLowerCase().replace(rn,"").replace(/[_\-\s]+/g,"-").replace(/^-|-$/g,"").trim();if(n&&n.length>1)return`icon-${X(n)}`;if("children"in e&&e.children.length>0){let s=[];for(let o=0;o<e.children.length;o++)s.push(e.children[o].type);for(let o=0;o<s.length;o++){if(s[o]==="ELLIPSE")return"icon-circle";if(s[o]==="STAR")return"icon-star";if(s[o]==="POLYGON")return"icon-shape"}}if("width"in e&&"height"in e){let s=e.width/e.height;if(s>1.5||s<.67)return"icon-arrow"}return"icon"}function Xo(e){let n=(e.characters||"").trim();if(!n)return"text-empty";let s=n.split(/\s+/);if(s.length<=2&&n.length<=30){let g=X(n);return g?`text-${g}`:"text-content"}let o=s[0].toLowerCase(),r=["welcome","about","contact","services","features","pricing"],i=["name","email","password","username","address","phone"],a=["submit","cancel","save","delete","edit","add","remove","ok","yes","no"],c=["learn","read","view","see","click","here","more"],d=["error","invalid","required","failed","wrong"],l=["success","done","complete","saved","updated"],p=n.toLowerCase();for(let g=0;g<r.length;g++)if(o.indexOf(r[g])!==-1||p.indexOf(r[g])!==-1)return`text-heading-${X(s.slice(0,2).join(" "))}`;for(let g=0;g<i.length;g++)if(o.indexOf(i[g])!==-1||p.indexOf(i[g])!==-1)return`text-label-${X(s.slice(0,2).join(" "))}`;for(let g=0;g<a.length;g++)if(o.indexOf(a[g])!==-1||p.indexOf(a[g])!==-1)return`text-button-${X(s.slice(0,2).join(" "))}`;for(let g=0;g<c.length;g++)if(o.indexOf(c[g])!==-1||p.indexOf(c[g])!==-1)return`text-link-${X(s.slice(0,2).join(" "))}`;for(let g=0;g<d.length;g++)if(o.indexOf(d[g])!==-1||p.indexOf(d[g])!==-1)return`text-error-${X(s.slice(0,2).join(" "))}`;for(let g=0;g<l.length;g++)if(o.indexOf(l[g])!==-1||p.indexOf(l[g])!==-1)return`text-success-${X(s.slice(0,2).join(" "))}`;let u=X(s.slice(0,2).join(" "));return u?`text-${u}`:"text-content"}function Yo(e){let t=De(e),n=cn[t];if("children"in e&&e.children.length>0){let s;for(let o=0;o<e.children.length;o++)if(e.children[o].type==="TEXT"){s=e.children[o];break}if(s&&s.characters){let r=s.characters.trim().split(/\s+/).slice(0,2);if(r.length>0&&r[0].length>0)return`${n}-${X(r.join(" "))}`}if(t==="button"||t==="input"){let o;for(let r=0;r<e.children.length;r++){let i=e.children[r];if(i.type==="VECTOR"||i.name.toLowerCase().indexOf("icon")!==-1){o=i;break}}if(o){let r=o.name.toLowerCase().replace(/icon[-_\s]*/gi,"");if(r&&!ln(r))return`${n}-${X(r)}`}}}return n}function pt(e,t){if(!e||!t||typeof t!="string")return!1;let n=t.trim();if(n.length===0)return!1;try{return e.name=n,!0}catch(s){return console.error("Failed to rename layer:",s),!1}}function mn(e,t){return{nodeId:e.id,currentName:e.name,newName:t.trim(),layerType:De(e),willChange:e.name!==t.trim()}}function X(e){return e.replace(/([a-z])([A-Z])/g,"$1-$2").replace(/[\s_]+/g,"-").replace(/[^a-zA-Z0-9-]/g,"").replace(/-+/g,"-").replace(/^-|-$/g,"").toLowerCase()}be();async function Lt(e){let t=Ps(e),n=yi(t),s=Ze(e).join(" "),o={width:"width"in e?e.width:0,height:"height"in e?e.height:0,layoutMode:"layoutMode"in e&&e.layoutMode||"NONE"},r={hasFills:Ls(e),hasStrokes:Rs(e),hasEffects:$s(e),cornerRadius:"cornerRadius"in e&&e.cornerRadius||0},i=bi(e),{isComponentSet:a,potentialVariants:c}=hi(e),d=await fi(e);return{name:e.name,type:e.type,hierarchy:t,textContent:s||void 0,frameStructure:o,detectedStyles:r,detectedSlots:i,isComponentSet:a,potentialVariants:c,nestedLayers:n,additionalContext:d}}async function fi(e){let t={hasInteractiveElements:!1,possibleUseCase:"",designPatterns:[],componentFamily:"",suggestedConsiderations:[]},n=e.name.toLowerCase(),o=["tabs","tab-group","tabset","nav","navbar","navigation","menu","menubar","dropdown","form","form-group","fieldset","list","grid","collection","gallery","group","container","wrapper","layout","toolbar","panel","sidebar","header","footer","card-group","button-group","radio-group","checkbox-group"].some(a=>n.includes(a)),r=await gi(e),i=o||r;return console.log(`\u{1F50D} [CONTAINER DETECTION] ${e.name}:`),console.log(` Name-based: ${o}`),console.log(` Structure-based: ${r}`),console.log(` Final result: ${i}`),n.includes("avatar")||n.includes("profile")?(t.componentFamily="avatar",t.possibleUseCase="User representation, often clickable for profile access or dropdown menus",t.hasInteractiveElements=!0,t.suggestedConsiderations.push("Consider if this avatar will be clickable/interactive"),t.suggestedConsiderations.push("May need hover/focus states for navigation"),t.designPatterns.push("profile-navigation","user-menu-trigger")):n.includes("button")||n.includes("btn")?(t.componentFamily="button",t.possibleUseCase="Interactive element for user actions",t.hasInteractiveElements=!0,t.suggestedConsiderations.push("Requires all interactive states"),t.designPatterns.push("action-trigger","form-submission")):n.includes("badge")||n.includes("tag")?(t.componentFamily="badge",t.possibleUseCase="Status indicator or label",t.hasInteractiveElements=!1,t.suggestedConsiderations.push("Typically non-interactive unless used as a filter"),t.designPatterns.push("status-indicator","category-label")):n.includes("input")||n.includes("field")?(t.componentFamily="input",t.possibleUseCase="Form input element",t.hasInteractiveElements=!0,t.suggestedConsiderations.push("Needs focus, error, and disabled states"),t.designPatterns.push("form-control","data-entry")):n.includes("card")?(t.componentFamily="card",t.possibleUseCase="Content container",t.hasInteractiveElements=n.includes("clickable")||n.includes("interactive"),t.suggestedConsiderations.push("May be interactive if used for navigation"),t.designPatterns.push("content-container","information-display")):n.includes("icon")?(t.componentFamily="icon",t.possibleUseCase="Visual indicator or decoration",t.hasInteractiveElements=!1,t.suggestedConsiderations.push("Usually decorative, but may be interactive if part of a button"),t.designPatterns.push("visual-indicator","decoration")):i&&(t.componentFamily="container",t.possibleUseCase="Layout container for organizing child components",t.hasInteractiveElements=!1,t.suggestedConsiderations.push("Focus on layout and organization rather than interaction states"),t.suggestedConsiderations.push("Child components handle individual interactions"),t.designPatterns.push("layout-container","component-organization")),"children"in e&&e.findAll(c=>c.type==="TEXT"&&(c.name.toLowerCase().includes("click")||c.name.toLowerCase().includes("action")||c.name.toLowerCase().includes("link"))).length>0&&(t.hasInteractiveElements=!0),e.parent&&e.parent.name.toLowerCase().includes("button")&&(t.hasInteractiveElements=!0,t.suggestedConsiderations.push("Part of a button component - needs interactive states")),t}async function gi(e){if(!("children"in e)||!e.children||e.children.length===0)return!1;let t=e.children.filter(d=>d.type==="INSTANCE");if(t.length===0)return console.log(`\u{1F50D} [STRUCTURE] No child instances found in ${e.name}`),!1;console.log(`\u{1F50D} [STRUCTURE] Analyzing ${e.name} with ${t.length} child instances`);let n=new Map;await Promise.all(t.map(async d=>{try{let l=await d.getMainComponentAsync();if(l){let p=l.name;n.has(p)||n.set(p,[]),n.get(p).push(d)}}catch(l){console.log("\u26A0\uFE0F [STRUCTURE] Could not access main component for instance:",l)}})),console.log("\u{1F50D} [STRUCTURE] Instance groups:",Array.from(n.entries()).map(([d,l])=>`${d}: ${l.length}`));let s=Array.from(n.values()).some(d=>d.length>1),o=Array.from(n.keys()).some(d=>{let l=d.toLowerCase();return l.includes("item")||l.includes("panel")||l.includes("content")||l.includes("section")||l.includes("group")||l.includes("wrapper")||l.includes("tab")&&!l.includes("button")||l.includes("nav-item")||l.includes("menu-item")||l.includes("list-item")||l.includes("card-item")}),r=t.length/e.children.length,i=r>.6,a=n.size>=2&&s;return console.log(`\u{1F50D} [STRUCTURE] Analysis for ${e.name}:`),console.log(` Repeated components: ${s}`),console.log(` Organizational components: ${o}`),console.log(` Instance ratio: ${r.toFixed(2)} (${i?"high":"low"})`),console.log(` Collection pattern: ${a}`),s||o||i&&n.size>=2}function Ps(e,t=0){let n=[],s={name:e.name,type:e.type,depth:t};if("children"in e&&e.children.length>0){s.children=[];for(let o of e.children)s.children.push(...Ps(o,t+1))}return n.push(s),n}function yi(e){let t=[];function n(s){for(let o of s)t.push(o.name),o.children&&n(o.children)}return n(e),t}function et(e){let t=new Set;function n(s){for(let o of s)o.type==="INSTANCE"&&t.add(o.name),o.children&&n(o.children)}return n(e),Array.from(t)}function hi(e){let t=[],n=!1;if(e.type==="COMPONENT_SET"){n=!0;try{let s=e,o;try{o=s.variantGroupProperties}catch(r){console.warn("Component set has errors, cannot access variantGroupProperties:",r),o=void 0}o&&t.push(...Object.keys(o))}catch(s){console.warn("Error analyzing component set:",s)}}else{let s=Oe(e).map(r=>r.name.toLowerCase());["primary","secondary","tertiary","small","medium","large","xl","xs","default","hover","focus","active","disabled","filled","outline","ghost","link","light","dark"].forEach(r=>{s.some(i=>i.includes(r))&&(t.includes(r)||t.push(r))})}return{isComponentSet:n,potentialVariants:t}}function bi(e){let t=[],n=Oe(e),s=e.name.toLowerCase(),o=["radiobutton","checkbox","icon","button","input","focusring","focus","indicator","background","border","outline","shadow","ring","control","handle","thumb","track","progress","slider","arrow","chevron","close","minimize","maximize"];n.filter(c=>c.type==="TEXT").forEach(c=>{let d=c.name.toLowerCase();o.some(l=>d.includes(l))||s.includes(d)||d.includes(s.split(" ")[0])||(d.includes("title")||d.includes("label")||d.includes("text")||d.includes("content"))&&d.length>2&&t.push(c.name)}),n.filter(c=>c.type==="FRAME").forEach(c=>{let d=c.name.toLowerCase();o.some(l=>d.includes(l))||(d.includes("content")&&!d.includes("background")||d.includes("slot")||d.includes("container")&&!d.includes("main"))&&t.push(c.name)});let a=[...new Set(t)].filter(c=>{let d=c.toLowerCase();return d.length>2&&!["text","label","content"].includes(d)&&!o.some(l=>d.includes(l))});return console.log(`\u{1F50D} [SLOTS] Detected ${a.length} legitimate content slots from ${t.length} candidates:`,a),a}function Ls(e){return"fills"in e&&Array.isArray(e.fills)&&e.fills.length>0?e.fills.some(t=>t.visible!==!1):"children"in e?e.children.some(t=>Ls(t)):!1}function Rs(e){return"strokes"in e&&Array.isArray(e.strokes)&&e.strokes.length>0?e.strokes.some(t=>t.visible!==!1):"children"in e?e.children.some(t=>Rs(t)):!1}function $s(e){return"effects"in e&&Array.isArray(e.effects)&&e.effects.length>0?e.effects.some(t=>t.visible!==!1):"children"in e?e.children.some(t=>$s(t)):!1}function vi(e){let t=new Map;e.children.forEach(s=>{s.type==="COMPONENT"&&s.name.split(",").map(i=>i.trim()).forEach(i=>{let[a,c]=i.split("=").map(d=>d.trim());a&&c&&(t.has(a)||t.set(a,new Set),t.get(a).add(c))})});let n=[];return t.forEach((s,o)=>{let r=Array.from(s);n.push({name:o,values:r,default:r[0]||"default"})}),n}async function Ms(e,t){let n=[];if(console.log("\u{1F50D} [DEBUG] Starting property extraction for node:",e.name,"type:",e.type),console.log("\u{1F50D} [DEBUG] Originally selected node:",t==null?void 0:t.name,"type:",t==null?void 0:t.type),t&&t.type==="INSTANCE"){let o=t;console.log("\u{1F50D} [DEBUG] Extracting from selected instance componentProperties...");try{if("componentProperties"in o&&o.componentProperties){let r=o.componentProperties;console.log("\u{1F50D} [DEBUG] Found componentProperties on selected instance:",Object.keys(r));let i=await o.getMainComponentAsync();if(i&&i.parent&&i.parent.type==="COMPONENT_SET"){let a=i.parent,c=null;try{"componentPropertyDefinitions"in a&&(c=a.componentPropertyDefinitions,console.log("\u{1F50D} [DEBUG] Got componentPropertyDefinitions from component set"))}catch(d){console.log("\u{1F50D} [DEBUG] Could not access componentPropertyDefinitions, using instance properties only")}for(let d in r){let l=r[d];console.log(`\u{1F50D} [DEBUG] Processing instance property "${d}":`,l);let p=d,u=[],g="";if(d.includes("#")&&(p=d.split("#")[0]),l&&typeof l=="object"&&"value"in l?g=String(l.value):g=String(l),c&&c[d]){let f=c[d];switch(console.log(`\u{1F50D} [DEBUG] Found property definition for "${d}":`,f),f.type){case"VARIANT":u=f.variantOptions||[];break;case"BOOLEAN":u=["true","false"];break;case"TEXT":u=[g||"Text content"];break;case"INSTANCE_SWAP":f.preferredValues&&Array.isArray(f.preferredValues)?u=f.preferredValues.map(m=>m.key||m.name||"Component instance"):u=["Component instance"];break;default:u=[g||"Property value"]}}else console.log(`\u{1F50D} [DEBUG] No property definition for "${d}", inferring from value`),g==="true"||g==="false"?u=["true","false"]:u=[g||"Property value"];n.push({name:p,values:u,default:g||u[0]||"default"}),console.log("\u{1F50D} [DEBUG] Added instance property:",{name:p,values:u,default:g})}if(n.length>0)return console.log(`\u{1F50D} [DEBUG] Successfully extracted ${n.length} properties from selected instance`),n}}}catch(r){console.log("\u{1F50D} [DEBUG] Could not extract from instance componentProperties:",r)}}if(e.type==="COMPONENT_SET"){let o=e;console.log("\u{1F50D} [DEBUG] Attempting to access componentPropertyDefinitions...");try{if("componentPropertyDefinitions"in o){console.log("\u{1F50D} [DEBUG] componentPropertyDefinitions property exists on componentSet");let r=o.componentPropertyDefinitions;if(console.log("\u{1F50D} [DEBUG] Raw componentPropertyDefinitions:",r),console.log("\u{1F50D} [DEBUG] Type of componentPropertyDefinitions:",typeof r),r&&typeof r=="object"){let i=Object.keys(r);console.log("\u{1F50D} [DEBUG] Found componentPropertyDefinitions with keys:",i);for(let a in r){let c=r[a];console.log(`\u{1F50D} [DEBUG] Processing property "${a}":`,c);let d=a,l=[],p="";switch(a.includes("#")&&(d=a.split("#")[0],console.log(`\u{1F50D} [DEBUG] Cleaned display name: "${d}" from "${a}"`)),c.type){case"VARIANT":l=c.variantOptions||[],p=String(c.defaultValue)||l[0]||"default",console.log(`\u{1F50D} [DEBUG] VARIANT property "${d}": values=${l}, default=${p}`);break;case"BOOLEAN":l=["true","false"],p=c.defaultValue?"true":"false",console.log(`\u{1F50D} [DEBUG] BOOLEAN property "${d}": default=${p}`);break;case"TEXT":l=[String(c.defaultValue||"Text content")],p=String(c.defaultValue||"Text content"),console.log(`\u{1F50D} [DEBUG] TEXT property "${d}": value=${p}`);break;case"INSTANCE_SWAP":c.preferredValues&&Array.isArray(c.preferredValues)?l=c.preferredValues.map(u=>(console.log("\u{1F50D} [DEBUG] INSTANCE_SWAP preferred value:",u),u.key||u.name||"Component instance")):l=["Component instance"],p=l[0]||"Component instance",console.log(`\u{1F50D} [DEBUG] INSTANCE_SWAP property "${d}": values=${l}, default=${p}`);break;default:console.log(`\u{1F50D} [DEBUG] Unknown property type "${c.type}" for "${d}"`),l=["Property value"],p="Default"}n.push({name:d,values:l,default:p}),console.log("\u{1F50D} [DEBUG] Added property:",{name:d,values:l,default:p})}}else console.log("\u{1F50D} [DEBUG] componentPropertyDefinitions is not a valid object:",r)}else console.log("\u{1F50D} [DEBUG] componentPropertyDefinitions property does not exist on componentSet")}catch(r){console.error("\u{1F50D} [ERROR] Could not access componentPropertyDefinitions:",r),console.error("\u{1F50D} [ERROR] Error stack:",r instanceof Error?r.stack:"No stack trace")}if(n.length===0){console.log("\u{1F50D} [DEBUG] No properties found, trying variantGroupProperties fallback...");try{let r=o.variantGroupProperties;if(console.log("\u{1F50D} [DEBUG] variantGroupProperties:",r),r){let i=Object.keys(r);console.log("\u{1F50D} [DEBUG] Found variantGroupProperties with keys:",i);for(let a in r){let c=r[a];console.log(`\u{1F50D} [DEBUG] Processing variant property "${a}":`,c),n.push({name:a,values:c.values,default:c.values[0]||"default"})}}else console.log("\u{1F50D} [DEBUG] variantGroupProperties is null/undefined")}catch(r){console.warn("\u{1F50D} [WARN] Component set has errors, cannot access variantGroupProperties:",r)}}if(n.length===0&&o.children.length>0){console.log("\u{1F50D} [DEBUG] Analyzing variant structure to infer properties...");let r=new Map,i=new Map;o.children.forEach((a,c)=>{if(a.type==="COMPONENT"){let d=a.name;console.log(`\u{1F50D} [DEBUG] Analyzing variant ${c}: ${d}`),d.split(",").map(u=>u.trim()).forEach(u=>{let[g,f]=u.split("=").map(m=>m.trim());g&&f&&(r.has(g)||r.set(g,new Set),r.get(g).add(f))});let p=(u,g="")=>{let f=g?`${g}/${u.name}`:u.name;i.has(f)||i.set(f,[]),i.get(f).push(u.visible),"children"in u&&u.children.forEach(m=>p(m,f))};p(a)}}),r.forEach((a,c)=>{n.find(d=>d.name===c)||n.push({name:c,values:Array.from(a),default:Array.from(a)[0]||"default"})}),i.forEach((a,c)=>{let d=a.includes(!0),l=a.includes(!1);if(d&&l){let u=(c.split("/").pop()||"").replace(/\s*(layer|group|frame|icon|text)?\s*/gi,"").trim();u&&!n.find(g=>g.name===u)&&(n.push({name:u,values:["true","false"],default:"false"}),console.log(`\u{1F50D} [DEBUG] Inferred boolean property from visibility: ${u}`))}}),console.log(`\u{1F50D} [DEBUG] Inferred ${n.length} properties from variant analysis`)}if(n.length===0){console.log("\u{1F50D} [DEBUG] All Figma APIs failed, using comprehensive structural analysis...");let r=Si(o);console.log("\u{1F50D} [DEBUG] Properties from structural analysis:",r),n.push(...r)}}else if(e.type==="COMPONENT"){let o=e;console.log("\u{1F50D} [DEBUG] Processing COMPONENT node:",o.name);try{if("componentPropertyDefinitions"in o){let r=o.componentPropertyDefinitions;if(console.log("\u{1F50D} [DEBUG] Component componentPropertyDefinitions:",r),r&&typeof r=="object"){let i=Object.keys(r);console.log("\u{1F50D} [DEBUG] Found componentPropertyDefinitions on component with keys:",i);for(let a in r){let c=r[a],d=a,l=[],p="";switch(a.includes("#")&&(d=a.split("#")[0]),c.type){case"BOOLEAN":l=["true","false"],p=c.defaultValue?"true":"false";break;case"TEXT":l=[String(c.defaultValue||"Text content")],p=String(c.defaultValue||"Text content");break;case"INSTANCE_SWAP":c.preferredValues&&Array.isArray(c.preferredValues)?l=c.preferredValues.map(u=>u.key||u.name||"Component instance"):l=["Component instance"],p=l[0]||"Component instance";break;default:l=["Property value"],p="Default"}n.push({name:d,values:l,default:p})}}}else console.log("\u{1F50D} [DEBUG] componentPropertyDefinitions does not exist on component")}catch(r){console.warn("\u{1F50D} [WARN] Could not access componentPropertyDefinitions on component:",r)}if(o.parent&&o.parent.type==="COMPONENT_SET"){let r=o.parent;console.log("\u{1F50D} [DEBUG] Component is part of a component set, getting variant properties...");try{let i=r.variantGroupProperties;if(i)for(let a in i){let c=i[a];n.find(d=>d.name===a)||n.push({name:a,values:c.values,default:c.values[0]||"default"})}}catch(i){console.warn("\u{1F50D} [WARN] Component set has errors, cannot access variantGroupProperties:",i)}}}else if(e.type==="INSTANCE"){let o=e;if(console.log("\u{1F50D} [DEBUG] Processing INSTANCE node (fallback \u2014 Priority 1 may have been skipped)"),n.length===0)try{let r=await o.getMainComponentAsync();if(r)if(r.parent&&r.parent.type==="COMPONENT_SET"){let i=r.parent;console.log("\u{1F50D} [DEBUG] Instance fallback: extracting from parent component set:",i.name);try{if("componentPropertyDefinitions"in i){let a=i.componentPropertyDefinitions;if(a&&typeof a=="object"){for(let c in a){let d=a[c],l=c,p=[],u="";switch(c.includes("#")&&(l=c.split("#")[0]),d.type){case"VARIANT":p=d.variantOptions||[],u=String(d.defaultValue)||p[0]||"default";break;case"BOOLEAN":p=["true","false"],u=d.defaultValue?"true":"false";break;case"TEXT":p=[String(d.defaultValue||"Text content")],u=String(d.defaultValue||"Text content");break;case"INSTANCE_SWAP":d.preferredValues&&Array.isArray(d.preferredValues)?p=d.preferredValues.map(g=>g.key||g.name||"Component instance"):p=["Component instance"],u=p[0]||"Component instance";break;default:p=["Property value"],u="Default"}n.push({name:l,values:p,default:u})}console.log(`\u{1F50D} [DEBUG] Instance fallback: extracted ${n.length} properties from component set`)}}}catch(a){console.warn("\u{1F50D} [WARN] Instance fallback: could not access componentPropertyDefinitions:",a)}if(n.length===0)try{let a=i.variantGroupProperties;if(a)for(let c in a){let d=a[c];n.find(l=>l.name===c)||n.push({name:c,values:d.values,default:d.values[0]||"default"})}}catch(a){console.warn("\u{1F50D} [WARN] Instance fallback: could not access variantGroupProperties:",a)}}else{console.log("\u{1F50D} [DEBUG] Instance fallback: extracting from standalone main component");try{if("componentPropertyDefinitions"in r){let i=r.componentPropertyDefinitions;if(i&&typeof i=="object"){for(let a in i){let c=i[a],d=a,l=[],p="";switch(a.includes("#")&&(d=a.split("#")[0]),c.type){case"BOOLEAN":l=["true","false"],p=c.defaultValue?"true":"false";break;case"TEXT":l=[String(c.defaultValue||"Text content")],p=String(c.defaultValue||"Text content");break;case"INSTANCE_SWAP":c.preferredValues&&Array.isArray(c.preferredValues)?l=c.preferredValues.map(u=>u.key||u.name||"Component instance"):l=["Component instance"],p=l[0]||"Component instance";break;default:l=["Property value"],p="Default"}n.push({name:d,values:l,default:p})}console.log(`\u{1F50D} [DEBUG] Instance fallback: extracted ${n.length} properties from main component`)}}}catch(i){console.warn("\u{1F50D} [WARN] Instance fallback: could not access componentPropertyDefinitions on main component:",i)}}}catch(r){console.warn("\u{1F50D} [WARN] Instance fallback: could not get main component:",r)}}let s=[];return n.forEach(o=>{s.find(r=>r.name===o.name)||s.push(o)}),console.log(`\u{1F50D} [DEBUG] Final result: Extracted ${s.length} unique properties:`,s.map(o=>({name:o.name,valueCount:o.values.length,default:o.default}))),s}function Si(e){let t=[];console.log("\u{1F50D} [STRUCTURAL] Starting comprehensive structural analysis of component set:",e.name);let n=vi(e);t.push(...n);let s=new Set,o=new Set,r=new Set,i=new Set;e.children.forEach(d=>{if(d.type==="COMPONENT"){console.log(`\u{1F50D} [STRUCTURAL] Analyzing variant: ${d.name}`);let l=(p,u=0)=>{let g=" ".repeat(u);console.log(`\u{1F50D} [STRUCTURAL] ${g}Found child: ${p.name} (type: ${p.type})`),s.add(p.name),p.type==="TEXT"?o.add(p.name):p.type==="INSTANCE"&&r.add(p.name),(p.visible===!1||p.name.toLowerCase().includes("hidden"))&&i.add(p.name),"children"in p&&p.children&&p.children.forEach(f=>l(f,u+1))};l(d)}}),console.log("\u{1F50D} [STRUCTURAL] Analysis results:"),console.log("\u{1F50D} [STRUCTURAL] - All child names:",Array.from(s)),console.log("\u{1F50D} [STRUCTURAL] - Text layers:",Array.from(o)),console.log("\u{1F50D} [STRUCTURAL] - Instance layers:",Array.from(r)),console.log("\u{1F50D} [STRUCTURAL] - Boolean indicators:",Array.from(i)),o.forEach(d=>{let l=d.replace(/\s*(layer|text|label)?\s*/gi,"").trim();l&&!t.find(p=>p.name.toLowerCase()===l.toLowerCase())&&(t.push({name:l,values:["Text content"],default:"Label"}),console.log(`\u{1F50D} [STRUCTURAL] Added TEXT property: ${l}`))}),r.forEach(d=>{let l=d.replace(/\s*(layer|instance)?\s*/gi,"").trim();l&&!t.find(p=>p.name.toLowerCase()===l.toLowerCase())&&(t.push({name:l,values:["Component instance"],default:"Default component"}),console.log(`\u{1F50D} [STRUCTURAL] Added INSTANCE_SWAP property: ${l}`))}),["icon before","icon after","slot before","slot after","before","after","prefix","suffix","leading","trailing"].forEach(d=>{if(Array.from(s).find(p=>p.toLowerCase().includes(d.toLowerCase()))&&!t.find(p=>p.name.toLowerCase().includes(d.toLowerCase()))){let p=d.split(" ").map(u=>u.charAt(0).toUpperCase()+u.slice(1)).join(" ");t.push({name:p,values:["true","false"],default:"false"}),console.log(`\u{1F50D} [STRUCTURAL] Added BOOLEAN property: ${p}`)}});let c=e.name.toLowerCase();return(c.includes("button")||c.includes("btn"))&&[{name:"Slot Before",type:"BOOLEAN"},{name:"Text",type:"TEXT"},{name:"Icon Before",type:"INSTANCE_SWAP"},{name:"Icon After",type:"INSTANCE_SWAP"}].forEach(({name:l,type:p})=>{if(!t.find(u=>u.name.toLowerCase()===l.toLowerCase())){let u,g;switch(p){case"BOOLEAN":u=["true","false"],g="false";break;case"TEXT":u=["Text content"],g="Label";break;case"INSTANCE_SWAP":u=["Component instance"],g="Default icon";break;default:u=["Property value"],g="Default"}t.push({name:l,values:u,default:g}),console.log(`\u{1F50D} [STRUCTURAL] Added common ${p} property: ${l}`)}}),console.log(`\u{1F50D} [STRUCTURAL] Final structural analysis result: ${t.length} properties found`),t}async function Ge(e){let t=[];if(e.type==="COMPONENT_SET"){let s=e,o;try{o=s.variantGroupProperties}catch(r){console.warn("Component set has errors, cannot access variantGroupProperties:",r),o=void 0}if(o)for(let r in o){let i=r.toLowerCase();(i==="state"||i==="states"||i==="status")&&t.push(...o[r].values)}s.children.forEach(r=>{let i=r.name.toLowerCase();["default","hover","focus","disabled","pressed","active","selected"].forEach(a=>{let c=t.find(d=>d.toLowerCase()===a.toLowerCase());i.includes(a)&&!c&&t.push(a)})})}else if(e.type==="COMPONENT"){let s=e;if(s.parent&&s.parent.type==="COMPONENT_SET")return await Ge(s.parent)}else if(e.type==="INSTANCE"){let o=await e.getMainComponentAsync();if(o)return await Ge(o)}let n=[];return t.forEach(s=>{s&&typeof s=="string"&&s.trim()!==""&&(n.find(r=>r.toLowerCase()===s.toLowerCase())||n.push(s.trim()))}),n}async function Os(e,t,n,s={},o="anthropic"){console.log("\u{1F3AF} Starting enhanced component analysis...");let r=figma.currentPage.selection[0],i=s.node||r;if(!i)throw new Error("No node selected");let a=await Ms(i,r),c=await Ge(i),d=await ue(i),l="";if(i.type==="COMPONENT"||i.type==="COMPONENT_SET")l=i.description||"";else if(i.type==="INSTANCE"){let C=await i.getMainComponentAsync();C&&(l=C.description||"")}e.existingDescription=l;let p=ae([i],s.lintSettings||j);console.log(`\u{1F50D} [LINT] Deterministic lint: ${p.summary.totalErrors} issues in ${p.summary.nodesWithErrors} nodes`),console.log("\u{1F4CA} [ANALYSIS] Extracted from Figma API:"),console.log(` Properties: ${a.length}`),console.log(` States: ${c.length}`),console.log(` Tokens: ${Object.keys(d).length} categories`),console.log(` Description: ${l?"Present":"Missing"}`);let u=s.mcpServerUrl||"http://localhost:3000/mcp",g=s.useMCP!==!1&&u,f;if(g){console.log(`\u{1F504} Using hybrid LLM + MCP approach (${o})...`);let h=ki(e,a,c,d,l,p),C=await ie(o,t,{prompt:h,model:n,maxTokens:2048,temperature:.1}),S=pe(C.content);if(!S)throw new Error("Failed to extract JSON from LLM response");let N=null;try{N=await Ni(e,u,S),console.log("\u2705 MCP enhancements received")}catch(y){console.warn("\u26A0\uFE0F MCP enhancement failed, continuing with LLM data only:",y)}f=wi(S,N,{node:i,context:e,actualProperties:a,actualStates:c,tokens:d,componentDescription:l})}else{console.log(`\u{1F4DD} Using ${o}-only analysis...`);let h=nn(e),C=await ie(o,t,{prompt:h,model:n,maxTokens:2048,temperature:.1});if(f=pe(C.content),!f)throw new Error("Failed to extract JSON from response")}let m=Fe(f);return await Rt(m,e,s,p,t,n,o)}function ki(e,t,n,s,o,r){var d;let i=((d=e.additionalContext)==null?void 0:d.componentFamily)||"generic",a=et(e.hierarchy),c="";if(r&&r.summary.totalErrors>0){let l=r.summary.byType,p=r.errors.slice(0,15).map(u=>` - [${u.errorType.toUpperCase()}] ${u.nodeName}: ${u.message}`).join(` +`&&(i=e.length),i===-1)continue;let a=e.substring(r,i).trim();try{return JSON.parse(a)}catch(c){if(a.startsWith("{"))try{return sn(a)}catch(d){continue}}}return null}function Bo(e){let t=/```(?:json)?\s*(\{[\s\S]*?\})\s*```/gi,n;for(;(n=t.exec(e))!==null;)try{return JSON.parse(n[1])}catch(s){continue}return null}function Uo(e){let t=e.match(/\{[\s\S]*\}/);return t?JSON.parse(t[0]):null}function Fe(e){if(!e||typeof e!="object")return e;let t=["aria","accessibility api","semantic html","keyboard navigation","event handler","interactive behavior","onclick","onchange","state management","controlled component","uncontrolled component","props","responsive breakpoint","css implementation","@media","animation token","transition timing","programmatic animation","keyframe","api integration","data binding","dynamic content","fetch","axios","implement","add handler","bind event","attach listener","programming pattern","functional pattern","react hook","usestate","useeffect"],n=r=>{let i=r.toLowerCase();return t.some(a=>i.includes(a))},s=r=>Array.isArray(r)?r.filter(i=>{if(typeof i=="string"){let a=!n(i);return a||console.log("\u{1F6AB} [FILTER] Removed development-focused recommendation:",i),a}return!0}):r,o=JSON.parse(JSON.stringify(e));return o.mcpReadiness&&(o.mcpReadiness.recommendations&&(o.mcpReadiness.recommendations=s(o.mcpReadiness.recommendations)),o.mcpReadiness.gaps&&(o.mcpReadiness.gaps=s(o.mcpReadiness.gaps))),o.audit&&(o.audit.tokenOpportunities&&(o.audit.tokenOpportunities=s(o.audit.tokenOpportunities)),o.audit.structureIssues&&(o.audit.structureIssues=s(o.audit.structureIssues))),o.accessibility&&(o.accessibility.designConsiderations&&(o.accessibility.designConsiderations=s(o.accessibility.designConsiderations)),o.accessibility.visualIndicators&&(o.accessibility.visualIndicators=s(o.accessibility.visualIndicators))),o}var E=class extends Error{constructor(n,s,o,r){super(n);this.code=s;this.statusCode=o;this.retryAfter=r;this.name="LLMError"}};var tt={anthropic:"claude-sonnet-4-5-20250929",openai:"gpt-5.2",google:"gemini-2.5-pro"};var Go=[{id:"claude-opus-4-5-20251218",name:"Claude Opus 4.5",description:"Flagship model - Most capable, best for complex analysis and reasoning",contextWindow:2e5,isDefault:!1},{id:"claude-sonnet-4-5-20250929",name:"Claude Sonnet 4.5",description:"Standard model - Balanced performance and cost, recommended for most tasks",contextWindow:2e5,isDefault:!0},{id:"claude-haiku-4-5-20251001",name:"Claude Haiku 4.5",description:"Economy model - Fastest responses, ideal for quick analysis",contextWindow:2e5,isDefault:!1}],nt=class{constructor(){this.name="Anthropic";this.id="anthropic";this.endpoint="https://api.anthropic.com/v1/messages";this.keyPrefix="sk-ant-";this.keyPlaceholder="sk-ant-...";this.models=Go}formatRequest(t){let n={model:t.model,messages:[{role:"user",content:t.prompt.trim()}],max_tokens:t.maxTokens};return t.temperature!==void 0&&(n.temperature=t.temperature),t.additionalParams&&Object.assign(n,t.additionalParams),n}parseResponse(t){let n=t;if(!n.content||!Array.isArray(n.content))throw new E("Invalid response format from Anthropic API: missing content array","INVALID_REQUEST");let s=n.content.filter(o=>o.type==="text").map(o=>o.text).join(` +`);if(!s)throw new E("Invalid response format from Anthropic API: no text content found","INVALID_REQUEST");return{content:s.trim(),model:n.model,usage:n.usage?{promptTokens:n.usage.input_tokens,completionTokens:n.usage.output_tokens,totalTokens:n.usage.input_tokens+n.usage.output_tokens}:void 0,metadata:{id:n.id,stopReason:n.stop_reason}}}validateApiKey(t){if(!t||typeof t!="string")return{isValid:!1,error:"API Key Required: Please provide a valid Claude API key."};let n=t.trim();return n.length===0?{isValid:!1,error:"API Key Required: The Claude API key cannot be empty."}:n.startsWith(this.keyPrefix)?n.length<40?{isValid:!1,error:"Invalid API Key Format: The API key appears to be too short. Please verify you copied the complete key."}:{isValid:!0}:{isValid:!1,error:`Invalid API Key Format: Claude API keys should start with "${this.keyPrefix}". Please check your API key.`}}getHeaders(t){return{"content-type":"application/json","x-api-key":t.trim(),"anthropic-version":"2023-06-01","anthropic-dangerous-direct-browser-access":"true"}}getDefaultModel(){return this.models.find(n=>n.isDefault)||this.models[1]}handleError(t,n){var r;let s=n,o=((r=s==null?void 0:s.error)==null?void 0:r.message)||(typeof n=="string"?n:"Unknown error");switch(t){case 400:return new E(`Claude API Error (400): ${o}. Please check your request format.`,"INVALID_REQUEST",400);case 401:return new E("Claude API Error (401): Invalid API key. Please check your Claude API key in settings.","INVALID_API_KEY",401);case 403:return new E("Claude API Error (403): Access forbidden. Please check your API key permissions.","INVALID_API_KEY",403);case 404:return new E(`Claude API Error (404): ${o}. The requested model may not be available.`,"MODEL_NOT_FOUND",404);case 429:return new E("Claude API Error (429): Rate limit exceeded. Please try again later.","RATE_LIMIT_EXCEEDED",429);case 500:return new E("Claude API Error (500): Server error. The Claude API is experiencing issues. Please try again later.","SERVER_ERROR",500);case 503:return new E("Claude API Error (503): Service unavailable. The Claude API is temporarily down. Please try again later.","SERVICE_UNAVAILABLE",503);default:return new E(`Claude API Error (${t}): ${o}`,"UNKNOWN_ERROR",t)}}},ot=new nt;var zo=[{id:"gpt-5.2",name:"GPT-5.2",description:"Flagship model with advanced reasoning capabilities",contextWindow:128e3,isDefault:!0},{id:"gpt-5.2-pro",name:"GPT-5.2 Pro",description:"Premium model with extended reasoning for complex tasks",contextWindow:128e3,isDefault:!1},{id:"gpt-5-mini",name:"GPT-5 Mini",description:"Economy model - fast and cost-effective",contextWindow:128e3,isDefault:!1}],rt=class{constructor(){this.name="OpenAI";this.id="openai";this.endpoint="https://api.openai.com/v1/chat/completions";this.keyPrefix="sk-";this.keyPlaceholder="sk-...";this.models=zo}formatRequest(t){let n={model:t.model,messages:[{role:"user",content:t.prompt}],max_completion_tokens:t.maxTokens,temperature:t.temperature};return t.additionalParams&&Object.assign(n,t.additionalParams),n}parseResponse(t){let n=t;if(!n.choices||n.choices.length===0)throw new E("Invalid response format: no choices returned","INVALID_REQUEST");let s=n.choices[0];if(!s.message||typeof s.message.content!="string")throw new E("Invalid response format: missing message content","INVALID_REQUEST");let o={content:s.message.content.trim(),model:n.model};return n.usage&&(o.usage={promptTokens:n.usage.prompt_tokens,completionTokens:n.usage.completion_tokens,totalTokens:n.usage.total_tokens}),o.metadata={id:n.id,finishReason:s.finish_reason,created:n.created},o}validateApiKey(t){if(!t||typeof t!="string")return{isValid:!1,error:"API Key Required: Please provide a valid OpenAI API key."};let n=t.trim();return n.length===0?{isValid:!1,error:"API Key Required: The OpenAI API key cannot be empty."}:n.startsWith(this.keyPrefix)?n.length<20?{isValid:!1,error:"Invalid API Key Format: The API key appears to be too short. Please verify you copied the complete key."}:{isValid:!0}:{isValid:!1,error:`Invalid API Key Format: OpenAI API keys should start with "${this.keyPrefix}". Please check your API key.`}}getHeaders(t){return{"Content-Type":"application/json",Authorization:`Bearer ${t.trim()}`}}getDefaultModel(){return this.models.find(n=>n.isDefault)||this.models[0]}handleError(t,n){var r;let s=n,o=((r=s==null?void 0:s.error)==null?void 0:r.message)||(s==null?void 0:s.message)||"Unknown error occurred";switch(t){case 400:return o.toLowerCase().includes("context_length_exceeded")||o.toLowerCase().includes("maximum context length")?new E(`OpenAI API Error (400): Context length exceeded. ${o}`,"CONTEXT_LENGTH_EXCEEDED",t):new E(`OpenAI API Error (400): ${o}. Please check your request format.`,"INVALID_REQUEST",t);case 401:return new E("OpenAI API Error (401): Invalid API key. Please check your OpenAI API key in settings.","INVALID_API_KEY",t);case 403:return new E("OpenAI API Error (403): Access forbidden. Please check your API key permissions or account status.","INVALID_API_KEY",t);case 404:return new E(`OpenAI API Error (404): Model not found. ${o}`,"MODEL_NOT_FOUND",t);case 429:let i=o.match(/try again in (\d+)/i),a=i?parseInt(i[1],10):void 0;return new E(`OpenAI API Error (429): Rate limit exceeded. ${a?`Please try again in ${a} seconds.`:"Please try again later."}`,"RATE_LIMIT_EXCEEDED",t,a);case 500:return new E("OpenAI API Error (500): Server error. The OpenAI API is experiencing issues. Please try again later.","SERVER_ERROR",t);case 502:return new E("OpenAI API Error (502): Bad gateway. The OpenAI API is temporarily unavailable. Please try again later.","SERVICE_UNAVAILABLE",t);case 503:return new E("OpenAI API Error (503): Service unavailable. The OpenAI API is temporarily down. Please try again later.","SERVICE_UNAVAILABLE",t);case 504:return new E("OpenAI API Error (504): Gateway timeout. The request took too long. Please try again.","SERVICE_UNAVAILABLE",t);default:return new E(`OpenAI API Error (${t}): ${o}`,"UNKNOWN_ERROR",t)}}},it=new rt;var Wo=[{id:"gemini-3-pro-preview",name:"Gemini 3 Pro",description:"Flagship model with advanced reasoning and multimodal capabilities",contextWindow:1e6,isDefault:!0},{id:"gemini-2.5-pro",name:"Gemini 2.5 Pro",description:"Standard reasoning model with excellent performance",contextWindow:1e6,isDefault:!1},{id:"gemini-2.5-flash",name:"Gemini 2.5 Flash",description:"Economy model optimized for speed and efficiency",contextWindow:1e6,isDefault:!1}],at=class{constructor(){this.name="Google";this.id="google";this.endpoint="https://generativelanguage.googleapis.com/v1beta/models";this.keyPrefix="AIza";this.keyPlaceholder="AIza...";this.models=Wo}formatRequest(t){let n={contents:[{parts:[{text:t.prompt}]}],generationConfig:{maxOutputTokens:t.maxTokens,temperature:t.temperature}};if(t.additionalParams){let{topP:s,topK:o,stopSequences:r}=t.additionalParams;s!==void 0&&(n.generationConfig.topP=s),o!==void 0&&(n.generationConfig.topK=o),r!==void 0&&(n.generationConfig.stopSequences=r)}return n}parseResponse(t){var c;let n=t;if(n.error)throw new E(n.error.message||"Unknown Gemini API error",this.mapErrorCodeToLLMErrorCode(n.error.code,n.error.status),n.error.code);if(!n.candidates||n.candidates.length===0){let d=Object.keys(n);throw new E(`No candidates in Gemini response. Response keys: [${d.join(", ")}]${n.error?`. Error: ${n.error.message}`:""}`,"INVALID_REQUEST")}let s=n.candidates[0];if(s.finishReason==="SAFETY")throw new E("Gemini response blocked by safety filters. Try rephrasing the prompt.","INVALID_REQUEST");let o=(c=s.content)==null?void 0:c.parts;if(!o||o.length===0)throw new E(`No content parts in Gemini response. Finish reason: ${s.finishReason||"unknown"}. Has content: ${!!s.content}`,"INVALID_REQUEST");let r=o.find(d=>typeof d.text=="string");if(!r||!r.text){let d=o.map(l=>Object.keys(l).join(",")).join("; ");throw new E(`No text content in Gemini response parts. Part types: [${d}]. Finish reason: ${s.finishReason||"unknown"}`,"INVALID_REQUEST")}let a={content:r.text,model:"gemini"};return n.usageMetadata&&(a.usage={promptTokens:n.usageMetadata.promptTokenCount||0,completionTokens:n.usageMetadata.candidatesTokenCount||0,totalTokens:n.usageMetadata.totalTokenCount||0}),a}validateApiKey(t){if(!t||typeof t!="string")return{isValid:!1,error:"API key is required"};let n=t.trim();return n.length===0?{isValid:!1,error:"API key cannot be empty"}:n.startsWith(this.keyPrefix)?n.length<30||n.length>50?{isValid:!1,error:"API key appears to have an invalid length. Please verify you copied the complete key."}:/^[A-Za-z0-9_-]+$/.test(n)?{isValid:!0}:{isValid:!1,error:"API key contains invalid characters"}:{isValid:!1,error:`Google API keys should start with "${this.keyPrefix}". Please check your API key.`}}getHeaders(t){return{"Content-Type":"application/json"}}getEndpoint(t,n){let s=n.trim();return`${this.endpoint}/${t}:generateContent?key=${s}`}getDefaultModel(){return this.models.find(n=>n.isDefault)||this.models[0]}handleError(t,n){var p,u;let s=n,o=s==null?void 0:s.error,r=(o==null?void 0:o.message)||"Unknown Google API error",i=o==null?void 0:o.status,a=(o==null?void 0:o.code)||t,c=this.mapErrorCodeToLLMErrorCode(a,i),d;if(t===429){d=6e4;let g=(p=o==null?void 0:o.details)==null?void 0:p.find(f=>{var m;return(m=f["@type"])==null?void 0:m.includes("RetryInfo")});if((u=g==null?void 0:g.metadata)!=null&&u.retryDelay){let f=g.metadata.retryDelay.match(/(\d+)s/);f&&(d=parseInt(f[1],10)*1e3)}}let l=r;switch(c){case"INVALID_API_KEY":l="Google API Error: Invalid API key. Please check your API key in settings.";break;case"RATE_LIMIT_EXCEEDED":l=`Google API Error: Rate limit exceeded. ${d?`Please try again in ${Math.ceil(d/1e3)} seconds.`:"Please try again later."}`;break;case"MODEL_NOT_FOUND":l="Google API Error: Model not found. Please select a valid model.";break;case"CONTEXT_LENGTH_EXCEEDED":l="Google API Error: Input too long. Please reduce the size of your request.";break;case"SERVER_ERROR":l="Google API Error: Server error. Please try again later.";break;case"SERVICE_UNAVAILABLE":l="Google API Error: Service temporarily unavailable. Please try again later.";break}return new E(l,c,t,d)}mapErrorCodeToLLMErrorCode(t,n){if(n){let s=n.toUpperCase();if(s==="INVALID_ARGUMENT")return"INVALID_REQUEST";if(s==="PERMISSION_DENIED"||s==="UNAUTHENTICATED")return"INVALID_API_KEY";if(s==="NOT_FOUND")return"MODEL_NOT_FOUND";if(s==="RESOURCE_EXHAUSTED")return"RATE_LIMIT_EXCEEDED";if(s==="UNAVAILABLE")return"SERVICE_UNAVAILABLE"}switch(t){case 400:return"INVALID_REQUEST";case 401:case 403:return"INVALID_API_KEY";case 404:return"MODEL_NOT_FOUND";case 429:return"RATE_LIMIT_EXCEEDED";case 500:return"SERVER_ERROR";case 503:return"SERVICE_UNAVAILABLE";default:return"UNKNOWN_ERROR"}}},ct=new at;var Ho={anthropic:ot,openai:it,google:ct};function ie(e){let t=Ho[e];if(!t)throw new E(`Unknown provider: ${e}`,"INVALID_REQUEST",400);return t}async function ae(e,t,n){var c,d;let s=ie(e),o=s.validateApiKey(t);if(!o.isValid)throw new E(o.error||"Invalid API key format","INVALID_API_KEY",401);let r=s.formatRequest(n),i=s.getHeaders(t),a=s.endpoint;e==="google"&&(a=`${s.endpoint}/${n.model}:generateContent?key=${t.trim()}`);try{console.log(`Making ${s.name} API call to ${a}...`);let l=await fetch(a,{method:"POST",headers:i,body:JSON.stringify(r)});if(!l.ok){let u;try{u=await l.json()}catch(g){u=await l.text()}throw s.handleError(l.status,u)}let p=await l.json();return console.log(`${s.name} API response status: ${l.status}`),console.log(`${s.name} API response keys:`,Object.keys(p)),e==="google"&&(console.log("Gemini response candidates:",p.candidates?p.candidates.length:"none"),(c=p.candidates)!=null&&c[0]&&(console.log("Gemini candidate[0] keys:",Object.keys(p.candidates[0])),p.candidates[0].content&&console.log("Gemini content parts:",((d=p.candidates[0].content.parts)==null?void 0:d.length)||"none")),p.error&&console.log("Gemini error:",JSON.stringify(p.error))),s.parseResponse(p)}catch(l){throw l instanceof E?l:l instanceof Error&&(l.message.includes("Failed to fetch")||l.message.includes("NetworkError"))?new E(`Network error connecting to ${s.name}. Please check your internet connection.`,"NETWORK_ERROR"):new E(`Unexpected error calling ${s.name}: ${l instanceof Error?l.message:"Unknown error"}`,"UNKNOWN_ERROR")}}var W={SELECTED_PROVIDER:"selected-provider",SELECTED_MODEL:"selected-model",apiKey:e=>`${e}-api-key`,LEGACY_CLAUDE_KEY:"claude-api-key",LEGACY_CLAUDE_MODEL:"claude-model"},Ko={provider:"anthropic",model:tt.anthropic};async function jo(){try{let e=await figma.clientStorage.getAsync(W.LEGACY_CLAUDE_KEY),t=await figma.clientStorage.getAsync(W.LEGACY_CLAUDE_MODEL);return e?{needsMigration:!0,legacyKey:e,legacyModel:t}:{needsMigration:!1}}catch(e){return{needsMigration:!1}}}async function lt(){let e=await jo();e.needsMigration&&(console.log("Migrating legacy Claude storage to multi-provider format..."),e.legacyKey&&await figma.clientStorage.setAsync(W.apiKey("anthropic"),e.legacyKey),await figma.clientStorage.setAsync(W.SELECTED_PROVIDER,"anthropic"),e.legacyModel&&await figma.clientStorage.setAsync(W.SELECTED_MODEL,e.legacyModel),await figma.clientStorage.deleteAsync(W.LEGACY_CLAUDE_KEY),await figma.clientStorage.deleteAsync(W.LEGACY_CLAUDE_MODEL),console.log("Migration complete"))}async function dt(){await lt();let e=await figma.clientStorage.getAsync(W.SELECTED_PROVIDER)||Ko.provider,t=await figma.clientStorage.getAsync(W.SELECTED_MODEL)||tt[e],n=await figma.clientStorage.getAsync(W.apiKey(e));return{providerId:e,modelId:t,apiKey:n}}async function ut(e,t,n){await figma.clientStorage.setAsync(W.SELECTED_PROVIDER,e),await figma.clientStorage.setAsync(W.SELECTED_MODEL,t),n!==void 0&&await figma.clientStorage.setAsync(W.apiKey(e),n)}async function on(e){await figma.clientStorage.deleteAsync(W.apiKey(e))}var rn=/^(Frame|Rectangle|Ellipse|Group|Vector|Line|Polygon|Star|Text|Component|Instance|Slice|Boolean|Union|Subtract|Intersect|Exclude)\s*\d*$/i,an=/\s+\d+$/,cn={button:"btn",icon:"ico",input:"input",text:"txt",image:"img",container:"container",card:"card",list:"list","list-item":"list-item",nav:"nav",header:"header",footer:"footer",modal:"modal",dropdown:"dropdown",checkbox:"checkbox",radio:"radio",toggle:"toggle",avatar:"avatar",badge:"badge",divider:"divider",spacer:"spacer",link:"link",tab:"tab",tooltip:"tooltip",alert:"alert",progress:"progress",skeleton:"skeleton",unknown:"layer"},me=[["btn","button"],["button","button"],["cta","button"],["submit","button"],["icon","icon"],["ico","icon"],["glyph","icon"],["symbol","icon"],["arrow","icon"],["chevron","icon"],["close","icon"],["plus","icon"],["minus","icon"],["txt","text"],["label","text"],["title","text"],["heading","text"],["paragraph","text"],["description","text"],["caption","text"],["subtitle","text"],["input","input"],["field","input"],["textfield","input"],["textarea","input"],["searchfield","input"],["searchbox","input"],["image","image"],["img","image"],["photo","image"],["picture","image"],["thumbnail","image"],["cover","image"],["container","container"],["wrapper","container"],["content","container"],["section","container"],["block","container"],["box","container"],["card","card"],["tile","card"],["panel","card"],["list","list"],["items","list"],["item","list-item"],["row","list-item"],["listitem","list-item"],["nav","nav"],["navbar","nav"],["navigation","nav"],["sidebar","nav"],["breadcrumb","nav"],["menu","nav"],["header","header"],["topbar","header"],["footer","footer"],["bottombar","footer"],["modal","modal"],["dialog","modal"],["popup","modal"],["overlay","modal"],["dropdown","dropdown"],["select","dropdown"],["picker","dropdown"],["combobox","dropdown"],["checkbox","checkbox"],["checkmark","checkbox"],["radio","radio"],["toggle","toggle"],["switch","toggle"],["avatar","avatar"],["profile","avatar"],["userpic","avatar"],["badge","badge"],["tag","badge"],["chip","badge"],["pill","badge"],["status","badge"],["divider","divider"],["separator","divider"],["hr","divider"],["spacer","spacer"],["gap","spacer"],["link","link"],["anchor","link"],["href","link"],["tab","tab"],["tabs","tab"],["tabbar","tab"],["tooltip","tooltip"],["hint","tooltip"],["popover","tooltip"],["alert","alert"],["notification","alert"],["toast","alert"],["message","alert"],["snackbar","alert"],["banner","alert"],["progress","progress"],["loader","progress"],["loading","progress"],["spinner","progress"],["progressbar","progress"],["skeleton","skeleton"],["placeholder","skeleton"],["shimmer","skeleton"]];function ln(e){if(!e||typeof e!="string")return!0;let t=e.trim();return!!(rn.test(t)||t.length===1||/^\d+$/.test(t))}function qo(e){return an.test(e.trim())}function De(e){let t=e.name.toLowerCase();for(let n=0;n<me.length;n++){let s=me[n];if(t.indexOf(s[0])!==-1)return s[1]}switch(e.type){case"TEXT":return"text";case"VECTOR":case"STAR":case"POLYGON":case"BOOLEAN_OPERATION":return"icon";case"RECTANGLE":case"ELLIPSE":case"LINE":if("fills"in e&&Array.isArray(e.fills)){let n=e.fills,s=!1;for(let o=0;o<n.length;o++){let r=n[o];if(r.type==="IMAGE"&&r.visible!==!1){s=!0;break}}if(s)return"image"}if("width"in e&&"height"in e){let n=e.width,s=e.height,o=n/s;if(s<=2&&n>20||n<=2&&s>20)return"divider";if(n<=32&&s<=32&&o>.5&&o<2)return"spacer"}return"unknown";case"FRAME":case"GROUP":return dn(e);case"COMPONENT":case"INSTANCE":return un(e);case"COMPONENT_SET":return Jo(e);default:return"unknown"}}function dn(e){if(!("children"in e)||e.children.length===0)return"container";let t=e.children,n=[],s=[];for(let c=0;c<t.length;c++)n.push(t[c].type),s.push(t[c].name.toLowerCase());let o=!1,r=!1;for(let c=0;c<n.length;c++)n[c]==="TEXT"&&(o=!0),(n[c]==="VECTOR"||s[c].indexOf("icon")!==-1)&&(r=!0);let i="width"in e&&"height"in e&&e.width<300&&e.height<100;if(o&&i&&(r||t.length<=3)&&"layoutMode"in e&&e.layoutMode!=="NONE")return"button";let a=!1;for(let c=0;c<n.length;c++)if(n[c]==="RECTANGLE"||s[c].indexOf("image")!==-1){a=!0;break}if(o&&a&&t.length>=2)return"card";if(t.length>=3){let c=t[0].type,d=!0;for(let l=1;l<t.length;l++)if(t[l].type!==c){d=!1;break}if(d&&(c==="FRAME"||c==="INSTANCE"))return"list"}if("cornerRadius"in e&&e.cornerRadius&&t.length<=2&&o&&i)return"input";if("layoutMode"in e&&e.layoutMode==="HORIZONTAL"){let c=0;for(let d=0;d<t.length;d++){let l=t[d].type;(l==="FRAME"||l==="INSTANCE"||l==="TEXT")&&c++}if(c>=3&&i)return"nav"}return"container"}function un(e){let t=e.name.toLowerCase();for(let n=0;n<me.length;n++){let s=me[n];if(t.indexOf(s[0])!==-1)return s[1]}return"children"in e?dn(e):"unknown"}function Jo(e){let t=e.name.toLowerCase();for(let n=0;n<me.length;n++){let s=me[n];if(t.indexOf(s[0])!==-1)return s[1]}return"children"in e&&e.children.length>0?un(e.children[0]):"unknown"}function pn(e,t=10){let n=[];function s(o,r,i){if(r>t)return;let a=i?`${i} > ${o.name}`:o.name,c=De(o);if(ln(o.name)){let d=fe(o);n.push({nodeId:o.id,nodeName:o.name,currentName:o.name,suggestedName:d,severity:"error",reason:"Generic layer name detected",layerType:c,depth:r,path:a})}else if(qo(o.name)){let d=o.name.replace(an,"").trim(),l=fe(o);n.push({nodeId:o.id,nodeName:o.name,currentName:o.name,suggestedName:l!==o.name?l:d,severity:"warning",reason:"Layer name has numbered suffix (possible duplicate)",layerType:c,depth:r,path:a})}if("children"in o)for(let d=0;d<o.children.length;d++)s(o.children[d],r+1,a)}return s(e,0,""),n}function fe(e){let t=De(e);return e.type==="TEXT"?Yo(e):e.type==="VECTOR"||e.type==="STAR"||e.type==="POLYGON"||e.type==="BOOLEAN_OPERATION"?Xo(e):"children"in e&&e.children.length>0?Qo(e):cn[t]||"layer"}function Xo(e){let n=e.name.toLowerCase().replace(rn,"").replace(/[_\-\s]+/g,"-").replace(/^-|-$/g,"").trim();if(n&&n.length>1)return`icon-${X(n)}`;if("children"in e&&e.children.length>0){let s=[];for(let o=0;o<e.children.length;o++)s.push(e.children[o].type);for(let o=0;o<s.length;o++){if(s[o]==="ELLIPSE")return"icon-circle";if(s[o]==="STAR")return"icon-star";if(s[o]==="POLYGON")return"icon-shape"}}if("width"in e&&"height"in e){let s=e.width/e.height;if(s>1.5||s<.67)return"icon-arrow"}return"icon"}function Yo(e){let n=(e.characters||"").trim();if(!n)return"text-empty";let s=n.split(/\s+/);if(s.length<=2&&n.length<=30){let g=X(n);return g?`text-${g}`:"text-content"}let o=s[0].toLowerCase(),r=["welcome","about","contact","services","features","pricing"],i=["name","email","password","username","address","phone"],a=["submit","cancel","save","delete","edit","add","remove","ok","yes","no"],c=["learn","read","view","see","click","here","more"],d=["error","invalid","required","failed","wrong"],l=["success","done","complete","saved","updated"],p=n.toLowerCase();for(let g=0;g<r.length;g++)if(o.indexOf(r[g])!==-1||p.indexOf(r[g])!==-1)return`text-heading-${X(s.slice(0,2).join(" "))}`;for(let g=0;g<i.length;g++)if(o.indexOf(i[g])!==-1||p.indexOf(i[g])!==-1)return`text-label-${X(s.slice(0,2).join(" "))}`;for(let g=0;g<a.length;g++)if(o.indexOf(a[g])!==-1||p.indexOf(a[g])!==-1)return`text-button-${X(s.slice(0,2).join(" "))}`;for(let g=0;g<c.length;g++)if(o.indexOf(c[g])!==-1||p.indexOf(c[g])!==-1)return`text-link-${X(s.slice(0,2).join(" "))}`;for(let g=0;g<d.length;g++)if(o.indexOf(d[g])!==-1||p.indexOf(d[g])!==-1)return`text-error-${X(s.slice(0,2).join(" "))}`;for(let g=0;g<l.length;g++)if(o.indexOf(l[g])!==-1||p.indexOf(l[g])!==-1)return`text-success-${X(s.slice(0,2).join(" "))}`;let u=X(s.slice(0,2).join(" "));return u?`text-${u}`:"text-content"}function Qo(e){let t=De(e),n=cn[t];if("children"in e&&e.children.length>0){let s;for(let o=0;o<e.children.length;o++)if(e.children[o].type==="TEXT"){s=e.children[o];break}if(s&&s.characters){let r=s.characters.trim().split(/\s+/).slice(0,2);if(r.length>0&&r[0].length>0)return`${n}-${X(r.join(" "))}`}if(t==="button"||t==="input"){let o;for(let r=0;r<e.children.length;r++){let i=e.children[r];if(i.type==="VECTOR"||i.name.toLowerCase().indexOf("icon")!==-1){o=i;break}}if(o){let r=o.name.toLowerCase().replace(/icon[-_\s]*/gi,"");if(r&&!ln(r))return`${n}-${X(r)}`}}}return n}function pt(e,t){if(!e||!t||typeof t!="string")return!1;let n=t.trim();if(n.length===0)return!1;try{return e.name=n,!0}catch(s){return console.error("Failed to rename layer:",s),!1}}function mn(e,t){return{nodeId:e.id,currentName:e.name,newName:t.trim(),layerType:De(e),willChange:e.name!==t.trim()}}function X(e){return e.replace(/([a-z])([A-Z])/g,"$1-$2").replace(/[\s_]+/g,"-").replace(/[^a-zA-Z0-9-]/g,"").replace(/-+/g,"-").replace(/^-|-$/g,"").toLowerCase()}be();async function Lt(e){let t=Ps(e),n=hi(t),s=Ze(e).join(" "),o={width:"width"in e?e.width:0,height:"height"in e?e.height:0,layoutMode:"layoutMode"in e&&e.layoutMode||"NONE"},r={hasFills:Ls(e),hasStrokes:Rs(e),hasEffects:$s(e),cornerRadius:"cornerRadius"in e&&e.cornerRadius||0},i=vi(e),{isComponentSet:a,potentialVariants:c}=bi(e),d=await gi(e);return{name:e.name,type:e.type,hierarchy:t,textContent:s||void 0,frameStructure:o,detectedStyles:r,detectedSlots:i,isComponentSet:a,potentialVariants:c,nestedLayers:n,additionalContext:d}}async function gi(e){let t={hasInteractiveElements:!1,possibleUseCase:"",designPatterns:[],componentFamily:"",suggestedConsiderations:[]},n=e.name.toLowerCase(),o=["tabs","tab-group","tabset","nav","navbar","navigation","menu","menubar","dropdown","form","form-group","fieldset","list","grid","collection","gallery","group","container","wrapper","layout","toolbar","panel","sidebar","header","footer","card-group","button-group","radio-group","checkbox-group"].some(a=>n.includes(a)),r=await yi(e),i=o||r;return console.log(`\u{1F50D} [CONTAINER DETECTION] ${e.name}:`),console.log(` Name-based: ${o}`),console.log(` Structure-based: ${r}`),console.log(` Final result: ${i}`),n.includes("avatar")||n.includes("profile")?(t.componentFamily="avatar",t.possibleUseCase="User representation, often clickable for profile access or dropdown menus",t.hasInteractiveElements=!0,t.suggestedConsiderations.push("Consider if this avatar will be clickable/interactive"),t.suggestedConsiderations.push("May need hover/focus states for navigation"),t.designPatterns.push("profile-navigation","user-menu-trigger")):n.includes("button")||n.includes("btn")?(t.componentFamily="button",t.possibleUseCase="Interactive element for user actions",t.hasInteractiveElements=!0,t.suggestedConsiderations.push("Requires all interactive states"),t.designPatterns.push("action-trigger","form-submission")):n.includes("badge")||n.includes("tag")?(t.componentFamily="badge",t.possibleUseCase="Status indicator or label",t.hasInteractiveElements=!1,t.suggestedConsiderations.push("Typically non-interactive unless used as a filter"),t.designPatterns.push("status-indicator","category-label")):n.includes("input")||n.includes("field")?(t.componentFamily="input",t.possibleUseCase="Form input element",t.hasInteractiveElements=!0,t.suggestedConsiderations.push("Needs focus, error, and disabled states"),t.designPatterns.push("form-control","data-entry")):n.includes("card")?(t.componentFamily="card",t.possibleUseCase="Content container",t.hasInteractiveElements=n.includes("clickable")||n.includes("interactive"),t.suggestedConsiderations.push("May be interactive if used for navigation"),t.designPatterns.push("content-container","information-display")):n.includes("icon")?(t.componentFamily="icon",t.possibleUseCase="Visual indicator or decoration",t.hasInteractiveElements=!1,t.suggestedConsiderations.push("Usually decorative, but may be interactive if part of a button"),t.designPatterns.push("visual-indicator","decoration")):i&&(t.componentFamily="container",t.possibleUseCase="Layout container for organizing child components",t.hasInteractiveElements=!1,t.suggestedConsiderations.push("Focus on layout and organization rather than interaction states"),t.suggestedConsiderations.push("Child components handle individual interactions"),t.designPatterns.push("layout-container","component-organization")),"children"in e&&e.findAll(c=>c.type==="TEXT"&&(c.name.toLowerCase().includes("click")||c.name.toLowerCase().includes("action")||c.name.toLowerCase().includes("link"))).length>0&&(t.hasInteractiveElements=!0),e.parent&&e.parent.name.toLowerCase().includes("button")&&(t.hasInteractiveElements=!0,t.suggestedConsiderations.push("Part of a button component - needs interactive states")),t}async function yi(e){if(!("children"in e)||!e.children||e.children.length===0)return!1;let t=e.children.filter(d=>d.type==="INSTANCE");if(t.length===0)return console.log(`\u{1F50D} [STRUCTURE] No child instances found in ${e.name}`),!1;console.log(`\u{1F50D} [STRUCTURE] Analyzing ${e.name} with ${t.length} child instances`);let n=new Map;await Promise.all(t.map(async d=>{try{let l=await d.getMainComponentAsync();if(l){let p=l.name;n.has(p)||n.set(p,[]),n.get(p).push(d)}}catch(l){console.log("\u26A0\uFE0F [STRUCTURE] Could not access main component for instance:",l)}})),console.log("\u{1F50D} [STRUCTURE] Instance groups:",Array.from(n.entries()).map(([d,l])=>`${d}: ${l.length}`));let s=Array.from(n.values()).some(d=>d.length>1),o=Array.from(n.keys()).some(d=>{let l=d.toLowerCase();return l.includes("item")||l.includes("panel")||l.includes("content")||l.includes("section")||l.includes("group")||l.includes("wrapper")||l.includes("tab")&&!l.includes("button")||l.includes("nav-item")||l.includes("menu-item")||l.includes("list-item")||l.includes("card-item")}),r=t.length/e.children.length,i=r>.6,a=n.size>=2&&s;return console.log(`\u{1F50D} [STRUCTURE] Analysis for ${e.name}:`),console.log(` Repeated components: ${s}`),console.log(` Organizational components: ${o}`),console.log(` Instance ratio: ${r.toFixed(2)} (${i?"high":"low"})`),console.log(` Collection pattern: ${a}`),s||o||i&&n.size>=2}function Ps(e,t=0){let n=[],s={name:e.name,type:e.type,depth:t};if("children"in e&&e.children.length>0){s.children=[];for(let o of e.children)s.children.push(...Ps(o,t+1))}return n.push(s),n}function hi(e){let t=[];function n(s){for(let o of s)t.push(o.name),o.children&&n(o.children)}return n(e),t}function et(e){let t=new Set;function n(s){for(let o of s)o.type==="INSTANCE"&&t.add(o.name),o.children&&n(o.children)}return n(e),Array.from(t)}function bi(e){let t=[],n=!1;if(e.type==="COMPONENT_SET"){n=!0;try{let s=e,o;try{o=s.variantGroupProperties}catch(r){console.warn("Component set has errors, cannot access variantGroupProperties:",r),o=void 0}o&&t.push(...Object.keys(o))}catch(s){console.warn("Error analyzing component set:",s)}}else{let s=Oe(e).map(r=>r.name.toLowerCase());["primary","secondary","tertiary","small","medium","large","xl","xs","default","hover","focus","active","disabled","filled","outline","ghost","link","light","dark"].forEach(r=>{s.some(i=>i.includes(r))&&(t.includes(r)||t.push(r))})}return{isComponentSet:n,potentialVariants:t}}function vi(e){let t=[],n=Oe(e),s=e.name.toLowerCase(),o=["radiobutton","checkbox","icon","button","input","focusring","focus","indicator","background","border","outline","shadow","ring","control","handle","thumb","track","progress","slider","arrow","chevron","close","minimize","maximize"];n.filter(c=>c.type==="TEXT").forEach(c=>{let d=c.name.toLowerCase();o.some(l=>d.includes(l))||s.includes(d)||d.includes(s.split(" ")[0])||(d.includes("title")||d.includes("label")||d.includes("text")||d.includes("content"))&&d.length>2&&t.push(c.name)}),n.filter(c=>c.type==="FRAME").forEach(c=>{let d=c.name.toLowerCase();o.some(l=>d.includes(l))||(d.includes("content")&&!d.includes("background")||d.includes("slot")||d.includes("container")&&!d.includes("main"))&&t.push(c.name)});let a=[...new Set(t)].filter(c=>{let d=c.toLowerCase();return d.length>2&&!["text","label","content"].includes(d)&&!o.some(l=>d.includes(l))});return console.log(`\u{1F50D} [SLOTS] Detected ${a.length} legitimate content slots from ${t.length} candidates:`,a),a}function Ls(e){return"fills"in e&&Array.isArray(e.fills)&&e.fills.length>0?e.fills.some(t=>t.visible!==!1):"children"in e?e.children.some(t=>Ls(t)):!1}function Rs(e){return"strokes"in e&&Array.isArray(e.strokes)&&e.strokes.length>0?e.strokes.some(t=>t.visible!==!1):"children"in e?e.children.some(t=>Rs(t)):!1}function $s(e){return"effects"in e&&Array.isArray(e.effects)&&e.effects.length>0?e.effects.some(t=>t.visible!==!1):"children"in e?e.children.some(t=>$s(t)):!1}function Si(e){let t=new Map;e.children.forEach(s=>{s.type==="COMPONENT"&&s.name.split(",").map(i=>i.trim()).forEach(i=>{let[a,c]=i.split("=").map(d=>d.trim());a&&c&&(t.has(a)||t.set(a,new Set),t.get(a).add(c))})});let n=[];return t.forEach((s,o)=>{let r=Array.from(s);n.push({name:o,values:r,default:r[0]||"default"})}),n}async function Ms(e,t){let n=[];if(console.log("\u{1F50D} [DEBUG] Starting property extraction for node:",e.name,"type:",e.type),console.log("\u{1F50D} [DEBUG] Originally selected node:",t==null?void 0:t.name,"type:",t==null?void 0:t.type),t&&t.type==="INSTANCE"){let o=t;console.log("\u{1F50D} [DEBUG] Extracting from selected instance componentProperties...");try{if("componentProperties"in o&&o.componentProperties){let r=o.componentProperties;console.log("\u{1F50D} [DEBUG] Found componentProperties on selected instance:",Object.keys(r));let i=await o.getMainComponentAsync();if(i&&i.parent&&i.parent.type==="COMPONENT_SET"){let a=i.parent,c=null;try{"componentPropertyDefinitions"in a&&(c=a.componentPropertyDefinitions,console.log("\u{1F50D} [DEBUG] Got componentPropertyDefinitions from component set"))}catch(d){console.log("\u{1F50D} [DEBUG] Could not access componentPropertyDefinitions, using instance properties only")}for(let d in r){let l=r[d];console.log(`\u{1F50D} [DEBUG] Processing instance property "${d}":`,l);let p=d,u=[],g="";if(d.includes("#")&&(p=d.split("#")[0]),l&&typeof l=="object"&&"value"in l?g=String(l.value):g=String(l),c&&c[d]){let f=c[d];switch(console.log(`\u{1F50D} [DEBUG] Found property definition for "${d}":`,f),f.type){case"VARIANT":u=f.variantOptions||[];break;case"BOOLEAN":u=["true","false"];break;case"TEXT":u=[g||"Text content"];break;case"INSTANCE_SWAP":f.preferredValues&&Array.isArray(f.preferredValues)?u=f.preferredValues.map(m=>m.key||m.name||"Component instance"):u=["Component instance"];break;default:u=[g||"Property value"]}}else console.log(`\u{1F50D} [DEBUG] No property definition for "${d}", inferring from value`),g==="true"||g==="false"?u=["true","false"]:u=[g||"Property value"];n.push({name:p,values:u,default:g||u[0]||"default"}),console.log("\u{1F50D} [DEBUG] Added instance property:",{name:p,values:u,default:g})}if(n.length>0)return console.log(`\u{1F50D} [DEBUG] Successfully extracted ${n.length} properties from selected instance`),n}}}catch(r){console.log("\u{1F50D} [DEBUG] Could not extract from instance componentProperties:",r)}}if(e.type==="COMPONENT_SET"){let o=e;console.log("\u{1F50D} [DEBUG] Attempting to access componentPropertyDefinitions...");try{if("componentPropertyDefinitions"in o){console.log("\u{1F50D} [DEBUG] componentPropertyDefinitions property exists on componentSet");let r=o.componentPropertyDefinitions;if(console.log("\u{1F50D} [DEBUG] Raw componentPropertyDefinitions:",r),console.log("\u{1F50D} [DEBUG] Type of componentPropertyDefinitions:",typeof r),r&&typeof r=="object"){let i=Object.keys(r);console.log("\u{1F50D} [DEBUG] Found componentPropertyDefinitions with keys:",i);for(let a in r){let c=r[a];console.log(`\u{1F50D} [DEBUG] Processing property "${a}":`,c);let d=a,l=[],p="";switch(a.includes("#")&&(d=a.split("#")[0],console.log(`\u{1F50D} [DEBUG] Cleaned display name: "${d}" from "${a}"`)),c.type){case"VARIANT":l=c.variantOptions||[],p=String(c.defaultValue)||l[0]||"default",console.log(`\u{1F50D} [DEBUG] VARIANT property "${d}": values=${l}, default=${p}`);break;case"BOOLEAN":l=["true","false"],p=c.defaultValue?"true":"false",console.log(`\u{1F50D} [DEBUG] BOOLEAN property "${d}": default=${p}`);break;case"TEXT":l=[String(c.defaultValue||"Text content")],p=String(c.defaultValue||"Text content"),console.log(`\u{1F50D} [DEBUG] TEXT property "${d}": value=${p}`);break;case"INSTANCE_SWAP":c.preferredValues&&Array.isArray(c.preferredValues)?l=c.preferredValues.map(u=>(console.log("\u{1F50D} [DEBUG] INSTANCE_SWAP preferred value:",u),u.key||u.name||"Component instance")):l=["Component instance"],p=l[0]||"Component instance",console.log(`\u{1F50D} [DEBUG] INSTANCE_SWAP property "${d}": values=${l}, default=${p}`);break;default:console.log(`\u{1F50D} [DEBUG] Unknown property type "${c.type}" for "${d}"`),l=["Property value"],p="Default"}n.push({name:d,values:l,default:p}),console.log("\u{1F50D} [DEBUG] Added property:",{name:d,values:l,default:p})}}else console.log("\u{1F50D} [DEBUG] componentPropertyDefinitions is not a valid object:",r)}else console.log("\u{1F50D} [DEBUG] componentPropertyDefinitions property does not exist on componentSet")}catch(r){console.error("\u{1F50D} [ERROR] Could not access componentPropertyDefinitions:",r),console.error("\u{1F50D} [ERROR] Error stack:",r instanceof Error?r.stack:"No stack trace")}if(n.length===0){console.log("\u{1F50D} [DEBUG] No properties found, trying variantGroupProperties fallback...");try{let r=o.variantGroupProperties;if(console.log("\u{1F50D} [DEBUG] variantGroupProperties:",r),r){let i=Object.keys(r);console.log("\u{1F50D} [DEBUG] Found variantGroupProperties with keys:",i);for(let a in r){let c=r[a];console.log(`\u{1F50D} [DEBUG] Processing variant property "${a}":`,c),n.push({name:a,values:c.values,default:c.values[0]||"default"})}}else console.log("\u{1F50D} [DEBUG] variantGroupProperties is null/undefined")}catch(r){console.warn("\u{1F50D} [WARN] Component set has errors, cannot access variantGroupProperties:",r)}}if(n.length===0&&o.children.length>0){console.log("\u{1F50D} [DEBUG] Analyzing variant structure to infer properties...");let r=new Map,i=new Map;o.children.forEach((a,c)=>{if(a.type==="COMPONENT"){let d=a.name;console.log(`\u{1F50D} [DEBUG] Analyzing variant ${c}: ${d}`),d.split(",").map(u=>u.trim()).forEach(u=>{let[g,f]=u.split("=").map(m=>m.trim());g&&f&&(r.has(g)||r.set(g,new Set),r.get(g).add(f))});let p=(u,g="")=>{let f=g?`${g}/${u.name}`:u.name;i.has(f)||i.set(f,[]),i.get(f).push(u.visible),"children"in u&&u.children.forEach(m=>p(m,f))};p(a)}}),r.forEach((a,c)=>{n.find(d=>d.name===c)||n.push({name:c,values:Array.from(a),default:Array.from(a)[0]||"default"})}),i.forEach((a,c)=>{let d=a.includes(!0),l=a.includes(!1);if(d&&l){let u=(c.split("/").pop()||"").replace(/\s*(layer|group|frame|icon|text)?\s*/gi,"").trim();u&&!n.find(g=>g.name===u)&&(n.push({name:u,values:["true","false"],default:"false"}),console.log(`\u{1F50D} [DEBUG] Inferred boolean property from visibility: ${u}`))}}),console.log(`\u{1F50D} [DEBUG] Inferred ${n.length} properties from variant analysis`)}if(n.length===0){console.log("\u{1F50D} [DEBUG] All Figma APIs failed, using comprehensive structural analysis...");let r=ki(o);console.log("\u{1F50D} [DEBUG] Properties from structural analysis:",r),n.push(...r)}}else if(e.type==="COMPONENT"){let o=e;console.log("\u{1F50D} [DEBUG] Processing COMPONENT node:",o.name);try{if("componentPropertyDefinitions"in o){let r=o.componentPropertyDefinitions;if(console.log("\u{1F50D} [DEBUG] Component componentPropertyDefinitions:",r),r&&typeof r=="object"){let i=Object.keys(r);console.log("\u{1F50D} [DEBUG] Found componentPropertyDefinitions on component with keys:",i);for(let a in r){let c=r[a],d=a,l=[],p="";switch(a.includes("#")&&(d=a.split("#")[0]),c.type){case"BOOLEAN":l=["true","false"],p=c.defaultValue?"true":"false";break;case"TEXT":l=[String(c.defaultValue||"Text content")],p=String(c.defaultValue||"Text content");break;case"INSTANCE_SWAP":c.preferredValues&&Array.isArray(c.preferredValues)?l=c.preferredValues.map(u=>u.key||u.name||"Component instance"):l=["Component instance"],p=l[0]||"Component instance";break;default:l=["Property value"],p="Default"}n.push({name:d,values:l,default:p})}}}else console.log("\u{1F50D} [DEBUG] componentPropertyDefinitions does not exist on component")}catch(r){console.warn("\u{1F50D} [WARN] Could not access componentPropertyDefinitions on component:",r)}if(o.parent&&o.parent.type==="COMPONENT_SET"){let r=o.parent;console.log("\u{1F50D} [DEBUG] Component is part of a component set, getting variant properties...");try{let i=r.variantGroupProperties;if(i)for(let a in i){let c=i[a];n.find(d=>d.name===a)||n.push({name:a,values:c.values,default:c.values[0]||"default"})}}catch(i){console.warn("\u{1F50D} [WARN] Component set has errors, cannot access variantGroupProperties:",i)}}}else if(e.type==="INSTANCE"){let o=e;if(console.log("\u{1F50D} [DEBUG] Processing INSTANCE node (fallback \u2014 Priority 1 may have been skipped)"),n.length===0)try{let r=await o.getMainComponentAsync();if(r)if(r.parent&&r.parent.type==="COMPONENT_SET"){let i=r.parent;console.log("\u{1F50D} [DEBUG] Instance fallback: extracting from parent component set:",i.name);try{if("componentPropertyDefinitions"in i){let a=i.componentPropertyDefinitions;if(a&&typeof a=="object"){for(let c in a){let d=a[c],l=c,p=[],u="";switch(c.includes("#")&&(l=c.split("#")[0]),d.type){case"VARIANT":p=d.variantOptions||[],u=String(d.defaultValue)||p[0]||"default";break;case"BOOLEAN":p=["true","false"],u=d.defaultValue?"true":"false";break;case"TEXT":p=[String(d.defaultValue||"Text content")],u=String(d.defaultValue||"Text content");break;case"INSTANCE_SWAP":d.preferredValues&&Array.isArray(d.preferredValues)?p=d.preferredValues.map(g=>g.key||g.name||"Component instance"):p=["Component instance"],u=p[0]||"Component instance";break;default:p=["Property value"],u="Default"}n.push({name:l,values:p,default:u})}console.log(`\u{1F50D} [DEBUG] Instance fallback: extracted ${n.length} properties from component set`)}}}catch(a){console.warn("\u{1F50D} [WARN] Instance fallback: could not access componentPropertyDefinitions:",a)}if(n.length===0)try{let a=i.variantGroupProperties;if(a)for(let c in a){let d=a[c];n.find(l=>l.name===c)||n.push({name:c,values:d.values,default:d.values[0]||"default"})}}catch(a){console.warn("\u{1F50D} [WARN] Instance fallback: could not access variantGroupProperties:",a)}}else{console.log("\u{1F50D} [DEBUG] Instance fallback: extracting from standalone main component");try{if("componentPropertyDefinitions"in r){let i=r.componentPropertyDefinitions;if(i&&typeof i=="object"){for(let a in i){let c=i[a],d=a,l=[],p="";switch(a.includes("#")&&(d=a.split("#")[0]),c.type){case"BOOLEAN":l=["true","false"],p=c.defaultValue?"true":"false";break;case"TEXT":l=[String(c.defaultValue||"Text content")],p=String(c.defaultValue||"Text content");break;case"INSTANCE_SWAP":c.preferredValues&&Array.isArray(c.preferredValues)?l=c.preferredValues.map(u=>u.key||u.name||"Component instance"):l=["Component instance"],p=l[0]||"Component instance";break;default:l=["Property value"],p="Default"}n.push({name:d,values:l,default:p})}console.log(`\u{1F50D} [DEBUG] Instance fallback: extracted ${n.length} properties from main component`)}}}catch(i){console.warn("\u{1F50D} [WARN] Instance fallback: could not access componentPropertyDefinitions on main component:",i)}}}catch(r){console.warn("\u{1F50D} [WARN] Instance fallback: could not get main component:",r)}}let s=[];return n.forEach(o=>{s.find(r=>r.name===o.name)||s.push(o)}),console.log(`\u{1F50D} [DEBUG] Final result: Extracted ${s.length} unique properties:`,s.map(o=>({name:o.name,valueCount:o.values.length,default:o.default}))),s}function ki(e){let t=[];console.log("\u{1F50D} [STRUCTURAL] Starting comprehensive structural analysis of component set:",e.name);let n=Si(e);t.push(...n);let s=new Set,o=new Set,r=new Set,i=new Set;e.children.forEach(d=>{if(d.type==="COMPONENT"){console.log(`\u{1F50D} [STRUCTURAL] Analyzing variant: ${d.name}`);let l=(p,u=0)=>{let g=" ".repeat(u);console.log(`\u{1F50D} [STRUCTURAL] ${g}Found child: ${p.name} (type: ${p.type})`),s.add(p.name),p.type==="TEXT"?o.add(p.name):p.type==="INSTANCE"&&r.add(p.name),(p.visible===!1||p.name.toLowerCase().includes("hidden"))&&i.add(p.name),"children"in p&&p.children&&p.children.forEach(f=>l(f,u+1))};l(d)}}),console.log("\u{1F50D} [STRUCTURAL] Analysis results:"),console.log("\u{1F50D} [STRUCTURAL] - All child names:",Array.from(s)),console.log("\u{1F50D} [STRUCTURAL] - Text layers:",Array.from(o)),console.log("\u{1F50D} [STRUCTURAL] - Instance layers:",Array.from(r)),console.log("\u{1F50D} [STRUCTURAL] - Boolean indicators:",Array.from(i)),o.forEach(d=>{let l=d.replace(/\s*(layer|text|label)?\s*/gi,"").trim();l&&!t.find(p=>p.name.toLowerCase()===l.toLowerCase())&&(t.push({name:l,values:["Text content"],default:"Label"}),console.log(`\u{1F50D} [STRUCTURAL] Added TEXT property: ${l}`))}),r.forEach(d=>{let l=d.replace(/\s*(layer|instance)?\s*/gi,"").trim();l&&!t.find(p=>p.name.toLowerCase()===l.toLowerCase())&&(t.push({name:l,values:["Component instance"],default:"Default component"}),console.log(`\u{1F50D} [STRUCTURAL] Added INSTANCE_SWAP property: ${l}`))}),["icon before","icon after","slot before","slot after","before","after","prefix","suffix","leading","trailing"].forEach(d=>{if(Array.from(s).find(p=>p.toLowerCase().includes(d.toLowerCase()))&&!t.find(p=>p.name.toLowerCase().includes(d.toLowerCase()))){let p=d.split(" ").map(u=>u.charAt(0).toUpperCase()+u.slice(1)).join(" ");t.push({name:p,values:["true","false"],default:"false"}),console.log(`\u{1F50D} [STRUCTURAL] Added BOOLEAN property: ${p}`)}});let c=e.name.toLowerCase();return(c.includes("button")||c.includes("btn"))&&[{name:"Slot Before",type:"BOOLEAN"},{name:"Text",type:"TEXT"},{name:"Icon Before",type:"INSTANCE_SWAP"},{name:"Icon After",type:"INSTANCE_SWAP"}].forEach(({name:l,type:p})=>{if(!t.find(u=>u.name.toLowerCase()===l.toLowerCase())){let u,g;switch(p){case"BOOLEAN":u=["true","false"],g="false";break;case"TEXT":u=["Text content"],g="Label";break;case"INSTANCE_SWAP":u=["Component instance"],g="Default icon";break;default:u=["Property value"],g="Default"}t.push({name:l,values:u,default:g}),console.log(`\u{1F50D} [STRUCTURAL] Added common ${p} property: ${l}`)}}),console.log(`\u{1F50D} [STRUCTURAL] Final structural analysis result: ${t.length} properties found`),t}async function Ge(e){let t=[];if(e.type==="COMPONENT_SET"){let s=e,o;try{o=s.variantGroupProperties}catch(r){console.warn("Component set has errors, cannot access variantGroupProperties:",r),o=void 0}if(o)for(let r in o){let i=r.toLowerCase();(i==="state"||i==="states"||i==="status")&&t.push(...o[r].values)}s.children.forEach(r=>{let i=r.name.toLowerCase();["default","hover","focus","disabled","pressed","active","selected"].forEach(a=>{let c=t.find(d=>d.toLowerCase()===a.toLowerCase());i.includes(a)&&!c&&t.push(a)})})}else if(e.type==="COMPONENT"){let s=e;if(s.parent&&s.parent.type==="COMPONENT_SET")return await Ge(s.parent)}else if(e.type==="INSTANCE"){let o=await e.getMainComponentAsync();if(o)return await Ge(o)}let n=[];return t.forEach(s=>{s&&typeof s=="string"&&s.trim()!==""&&(n.find(r=>r.toLowerCase()===s.toLowerCase())||n.push(s.trim()))}),n}async function Os(e,t,n,s={},o="anthropic"){console.log("\u{1F3AF} Starting enhanced component analysis...");let r=figma.currentPage.selection[0],i=s.node||r;if(!i)throw new Error("No node selected");let a=await Ms(i,r),c=await Ge(i),d=await ue(i),l="";if(i.type==="COMPONENT"||i.type==="COMPONENT_SET")l=i.description||"";else if(i.type==="INSTANCE"){let C=await i.getMainComponentAsync();C&&(l=C.description||"")}e.existingDescription=l;let p=ne([i],s.lintSettings||j);console.log(`\u{1F50D} [LINT] Deterministic lint: ${p.summary.totalErrors} issues in ${p.summary.nodesWithErrors} nodes`),console.log("\u{1F4CA} [ANALYSIS] Extracted from Figma API:"),console.log(` Properties: ${a.length}`),console.log(` States: ${c.length}`),console.log(` Tokens: ${Object.keys(d).length} categories`),console.log(` Description: ${l?"Present":"Missing"}`);let u=s.mcpServerUrl||"http://localhost:3000/mcp",g=s.useMCP!==!1&&u,f;if(g){console.log(`\u{1F504} Using hybrid LLM + MCP approach (${o})...`);let h=Ni(e,a,c,d,l,p),C=await ae(o,t,{prompt:h,model:n,maxTokens:2048,temperature:.1}),k=pe(C.content);if(!k)throw new Error("Failed to extract JSON from LLM response");let N=null;try{N=await wi(e,u,k),console.log("\u2705 MCP enhancements received")}catch(y){console.warn("\u26A0\uFE0F MCP enhancement failed, continuing with LLM data only:",y)}f=Ci(k,N,{node:i,context:e,actualProperties:a,actualStates:c,tokens:d,componentDescription:l})}else{console.log(`\u{1F4DD} Using ${o}-only analysis...`);let h=nn(e),C=await ae(o,t,{prompt:h,model:n,maxTokens:2048,temperature:.1});if(f=pe(C.content),!f)throw new Error("Failed to extract JSON from response")}let m=Fe(f);return await Rt(m,e,s,p,t,n,o)}function Ni(e,t,n,s,o,r){var d;let i=((d=e.additionalContext)==null?void 0:d.componentFamily)||"generic",a=et(e.hierarchy),c="";if(r&&r.summary.totalErrors>0){let l=r.summary.byType,p=r.errors.slice(0,15).map(u=>` - [${u.errorType.toUpperCase()}] ${u.nodeName}: ${u.message}`).join(` `);c=` **Design Lint Findings (${r.summary.totalErrors} issues):** - Missing fill styles: ${l.fill||0} @@ -294,7 +294,7 @@ Return JSON in this exact format: For "recommendedProperties": Compare the EXISTING properties listed above against design system best practices (Material Design, Carbon, Ant Design, Polaris, etc.). Only recommend Figma component properties that do NOT already exist. Use Figma property types (VARIANT, BOOLEAN, TEXT, INSTANCE_SWAP). If the component already has comprehensive properties, return an empty array. -Focus ONLY on what's actually in the Figma component for existing data. Recommendations should draw from your knowledge of design system best practices.`}async function Ni(e,t,n){var o,r;let s=((o=e.additionalContext)==null?void 0:o.componentFamily)||((r=n.component)==null?void 0:r.toLowerCase())||"generic";try{let[i,a,c]=await Promise.all([Tt(t,"search_design_knowledge",{query:`${s} component essential properties states variants`,category:"components",limit:2},3e3),Tt(t,"search_design_knowledge",{query:`design tokens ${s} semantic naming`,category:"tokens",limit:2},3e3),Tt(t,"search_chunks",{query:`component assessment scoring criteria ${s}`,limit:1},3e3)]);return{bestPractices:(i==null?void 0:i.entries)||[],tokenGuidance:(a==null?void 0:a.entries)||[],scoringCriteria:(c==null?void 0:c.chunks)||[],success:!0}}catch(i){return console.warn("\u26A0\uFE0F MCP queries failed:",i),{bestPractices:[],tokenGuidance:[],scoringCriteria:[],success:!1,error:i instanceof Error?i.message:"Unknown error"}}}async function Tt(e,t,n,s=5e3){var i,a;let o=new AbortController,r=setTimeout(()=>o.abort(),s);try{let c={jsonrpc:"2.0",id:Math.floor(Math.random()*1e3)+100,method:"tools/call",params:{name:`mcp_design-systems_${t}`,arguments:n}},d=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c),signal:o.signal});if(clearTimeout(r),!d.ok)throw new Error(`MCP ${t} failed: ${d.status}`);return((a=(i=(await d.json()).result)==null?void 0:i.content)==null?void 0:a[0])||{}}catch(c){throw clearTimeout(r),c instanceof Error&&c.name==="AbortError"?new Error(`MCP ${t} timeout after ${s}ms`):c}}function wi(e,t,n){var o;let s=R({},e);return s.propertyCheatSheet=Ii(n.actualProperties,e.component||n.context.name),s.audit={designIssues:[],tokenOpportunities:[],structureIssues:[]},(!n.componentDescription||n.componentDescription.trim().length===0)&&s.audit.structureIssues.push("Component lacks description - Add a description in component properties to help MCP and AI understand the component's purpose and usage"),t!=null&&t.success?s.mcpReadiness=Ci(t,e,n):s.mcpReadiness=Fs(n),s.component=s.component||n.context.name,s.description=s.description||`${((o=n.context.additionalContext)==null?void 0:o.componentFamily)||"Component"} with ${n.actualProperties.length} properties`,s.props=s.props||n.actualProperties.map(r=>({name:r.name,type:"select",description:`Controls ${r.name}`,values:r.values,default:r.default})),s.states=s.states||n.actualStates,s.recommendedProperties=e.recommendedProperties||[],s}function Ci(e,t,n){var l,p;let s=[],o=[],r=[];((l=e.bestPractices)==null?void 0:l.length)>0&&e.bestPractices.forEach(u=>{var g,f;((g=u.title)!=null&&g.includes("best practice")||(f=u.title)!=null&&f.includes("pattern"))&&r.push(`Follow ${u.title}`)});let i=n.actualStates.length>=3,a=n.tokens.summary&&n.tokens.summary.actualTokens>n.tokens.summary.hardCodedValues,c=((p=t.structure)==null?void 0:p.complexity)!=="high";return i?s.push("Component has comprehensive states"):o.push("Missing interactive states"),a?s.push("Good token usage"):o.push("Improve token adoption"),c?s.push("Well-structured component"):o.push("Complex structure may need simplification"),{score:Math.round((i?35:15)+(a?35:15)+(c?30:20)),strengths:s,gaps:o,recommendations:r.slice(0,3)}}function Ii(e,t){let n=[],s=e.filter(c=>c.name.toLowerCase().includes("size")||c.values.some(d=>["small","medium","large"].includes(d.toLowerCase()))),o=e.filter(c=>c.name.toLowerCase().includes("variant")||c.name.toLowerCase().includes("type")),r=e.filter(c=>c.name.toLowerCase().includes("state")||c.values.some(d=>["hover","active","disabled"].includes(d.toLowerCase())));s.length>0&&n.push(`\u{1F4CF} Sizes: ${s.map(c=>c.values.join("/")).join(", ")}`),o.length>0&&n.push(`\u{1F3A8} Variants: ${o.map(c=>`${c.name}(${c.values.length})`).join(", ")}`),r.length>0&&n.push(`\u{1F504} States: ${r.map(c=>c.values.join("/")).join(", ")}`);let i=new Set([...s,...o,...r].map(c=>c.name)),a=e.filter(c=>!i.has(c.name)).slice(0,3).map(c=>`${c.name}: ${c.values.slice(0,3).join("/")}`);return a.length>0&&n.push(`\u2699\uFE0F Other: ${a.join(", ")}`),n.slice(0,5)}async function Rt(e,t,n,s,o,r,i){var a;try{console.log("\u{1F504} Processing analysis result..."),console.log("\u{1F4CA} Filtered data received:",JSON.stringify(e,null,2).substring(0,500)+"...");let c=figma.currentPage.selection,d=null;if(c.length>0)d=c[0];else throw new Error("No component selected");let l=await Ms(d,d),p=await Ge(d),u="";if(d.type==="COMPONENT"||d.type==="COMPONENT_SET")u=d.description||"";else if(d.type==="INSTANCE"){let y=await d.getMainComponentAsync();y&&(u=y.description||"")}let g={colors:[],spacing:[],typography:[],effects:[],borders:[],summary:{totalTokens:0,actualTokens:0,hardCodedValues:0,aiSuggestions:0,byCategory:{}}};n.includeTokenAnalysis!==!1&&(g=await ue(d));let f={component:e.component||t.name||"Component",description:e.description||`A ${t.type} component with ${l.length} properties`,props:e.props&&e.props.length>0?e.props:l.map(N=>({name:N.name,type:"select",description:`Controls ${N.name}`,values:N.values,defaultValue:N.default,required:!1})),states:e.states&&e.states.length>0?e.states.map(N=>typeof N=="string"?N:N.name):p.length>0?p:["default"],variants:e.variants||{},slots:e.slots||[],tokens:e.tokens||{colors:g.colors.filter(N=>N.isActualToken).map(N=>N.name),spacing:g.spacing.filter(N=>N.isActualToken).map(N=>N.name),typography:g.typography.filter(N=>N.isActualToken).map(N=>N.name)},usage:e.usage||"General purpose component for design systems",accessibility:e.accessibility||{keyboardNavigation:"Standard keyboard navigation support",screenReader:"Screen reader accessible",colorContrast:"WCAG compliant contrast ratios"},audit:e.audit||{accessibilityIssues:[],namingIssues:[],consistencyIssues:[],tokenOpportunities:[]},propertyCheatSheet:e.propertyCheatSheet||l.map(N=>({name:N.name,values:N.values,default:N.default,description:`Property for ${N.name} configuration`})),mcpReadiness:e.mcpReadiness||Fs({node:d,context:t,actualProperties:l,actualStates:p,tokens:g,componentDescription:u})};console.log("\u{1F4E4} Sending to UI - metadata.props:",(a=f.props)==null?void 0:a.length),console.log("\u{1F4E4} Sending to UI - metadata.states:",f.states),console.log("\u{1F4E4} Sending to UI - metadata.mcpReadiness:",f.mcpReadiness);let m=await Ei(e,t,d,l,p,g,u),h=(e.recommendedProperties||[]).map(N=>({name:N.name||"",type:N.type||"VARIANT",description:N.description||"",examples:N.examples||[]})).filter(N=>N.name);console.log(`\u{1F4A1} AI-generated property recommendations: ${h.length}`);let C=pn(d,5);console.log(`\u{1F4DB} Found ${C.length} naming issues`),s&&s.errors.length>0&&(m.designLint=xi(s));let S;if(o&&r&&i)try{S=await Ai(t,s,m,g,C,h,o,r,i),console.log(`\u{1F4CB} Design review generated: ${S.verdict} \u2014 ${S.findings.length} findings`)}catch(N){console.warn("\u26A0\uFE0F Design review generation failed, continuing without it:",N),S=Pt(s,m,g,C)}else S=Pt(s,m,g,C);return console.log("\u2705 Analysis result processed successfully"),{metadata:f,tokens:g,audit:m,properties:l,recommendations:h,namingIssues:C,existingDescription:u,lintResult:s,designReview:S}}catch(c){throw console.error("Error processing analysis result:",c),c}}function xi(e){let t=[],n=e.summary.byType;return n.fill>0?t.push({check:`Fill styles (${n.fill} missing)`,status:"fail",suggestion:`${n.fill} layer${n.fill>1?"s use":" uses"} hard-coded fills instead of design styles`}):t.push({check:"Fill styles",status:"pass",suggestion:"All fills use design styles"}),n.stroke>0?t.push({check:`Stroke styles (${n.stroke} missing)`,status:"fail",suggestion:`${n.stroke} layer${n.stroke>1?"s use":" uses"} hard-coded strokes instead of design styles`}):t.push({check:"Stroke styles",status:"pass",suggestion:"All strokes use design styles"}),n.effect>0?t.push({check:`Effect styles (${n.effect} missing)`,status:"fail",suggestion:`${n.effect} layer${n.effect>1?"s use":" uses"} hard-coded effects instead of design styles`}):t.push({check:"Effect styles",status:"pass",suggestion:"All effects use design styles"}),n.text>0?t.push({check:`Text styles (${n.text} missing)`,status:"fail",suggestion:`${n.text} text layer${n.text>1?"s lack":" lacks"} applied text styles`}):t.push({check:"Text styles",status:"pass",suggestion:"All text uses design styles"}),n.radius>0?t.push({check:`Border radius (${n.radius} non-standard)`,status:"warning",suggestion:`${n.radius} layer${n.radius>1?"s use":" uses"} non-standard border radius values`}):t.push({check:"Border radius",status:"pass",suggestion:"All radii match design system standards"}),n.spacing>0?t.push({check:`Spacing rhythm (${n.spacing} off-grid)`,status:"warning",suggestion:`${n.spacing} spacing value${n.spacing>1?"s are":" is"} not on the 4/8px grid`}):t.push({check:"Spacing rhythm",status:"pass",suggestion:"All spacing values follow the design grid"}),n.autoLayout>0?t.push({check:`Auto Layout (${n.autoLayout} missing)`,status:"warning",suggestion:`${n.autoLayout} frame${n.autoLayout>1?"s lack":" lacks"} auto-layout`}):t.push({check:"Auto Layout",status:"pass",suggestion:"All container frames use auto-layout"}),t}function Pt(e,t,n,s){let o=[];if(e)for(let u of e.errors)o.push({severity:u.errorType==="radius"||u.errorType==="spacing"||u.errorType==="autoLayout"?"warning":"critical",category:"Style Consistency",title:u.message,description:`Layer "${u.nodeName}" (${u.nodeType}) at ${u.path}`,nodeId:u.nodeId,nodeName:u.nodeName,autoFixable:!1});let r=n.summary.hardCodedValues;r>0&&o.push({severity:"warning",category:"Design Tokens",title:`${r} hard-coded value${r>1?"s":""} found`,description:"These values should be replaced with design tokens for consistency across the design system.",autoFixable:!0});for(let u of t.accessibility||[])u.status==="fail"?o.push({severity:"critical",category:"Accessibility",title:u.check,description:u.suggestion,autoFixable:!1}):u.status==="warning"&&o.push({severity:"warning",category:"Accessibility",title:u.check,description:u.suggestion,autoFixable:!1});for(let u of s.slice(0,10))o.push({severity:u.severity==="error"?"warning":"info",category:"Naming",title:`"${u.currentName}" should be "${u.suggestedName}"`,description:u.reason,nodeId:u.nodeId,nodeName:u.currentName,autoFixable:!0});for(let u of t.componentReadiness||[])u.status==="fail"&&o.push({severity:"suggestion",category:"Component Readiness",title:u.check,description:u.suggestion,autoFixable:!1});let i=(t.states||[]).filter(u=>!u.found);i.length>0&&o.push({severity:"suggestion",category:"Interactive States",title:`${i.length} state${i.length>1?"s":""} not detected`,description:`Missing: ${i.map(u=>u.name).join(", ")}`,autoFixable:!1});let a=o.filter(u=>u.severity==="critical").length,c=o.filter(u=>u.severity==="warning").length,d=a>0?"fail":c>3?"warn":"pass",l;d==="pass"?l="Component follows design system conventions well.":d==="warn"?l=`${c} issues need attention before this component is production-ready.`:l=`${a} critical issue${a>1?"s":""} found \u2014 missing design styles affect consistency.`;let p=[];return e&&e.summary.byType.fill>0&&p.push("Apply fill styles to layers using hard-coded colors"),e&&e.summary.byType.text>0&&p.push("Apply text styles to text layers"),e&&e.summary.byType.stroke>0&&p.push("Apply stroke styles to layers with hard-coded strokes"),e&&e.summary.byType.spacing>0&&p.push("Fix off-grid spacing values to match the 4/8px grid"),e&&e.summary.byType.autoLayout>0&&p.push("Apply auto-layout to container frames"),r>0&&p.push("Replace hard-coded values with design tokens"),s.length>0&&p.push("Rename generic layers to semantic names"),i.length>0&&p.push(`Add missing states: ${i.map(u=>u.name).join(", ")}`),p.length===0&&p.push("Component looks great \u2014 consider documenting it for the team"),{verdict:d,headline:l,findings:o,nextSteps:p}}async function Ai(e,t,n,s,o,r,i,a,c){var C;let d=t?`${t.summary.totalErrors} lint issues (${t.summary.byType.fill} fills, ${t.summary.byType.stroke} strokes, ${t.summary.byType.effect} effects, ${t.summary.byType.text} text, ${t.summary.byType.radius} radius, ${t.summary.byType.spacing||0} spacing, ${t.summary.byType.autoLayout||0} auto-layout)`:"0 lint issues",l=(n.accessibility||[]).filter(S=>S.status==="fail").length,p=(n.componentReadiness||[]).filter(S=>S.status==="fail").length,u=(n.states||[]).filter(S=>!S.found),g=t?t.errors.slice(0,8).map(S=>`- [${S.errorType}] ${S.nodeName}: ${S.message}`).join(` +Focus ONLY on what's actually in the Figma component for existing data. Recommendations should draw from your knowledge of design system best practices.`}async function wi(e,t,n){var o,r;let s=((o=e.additionalContext)==null?void 0:o.componentFamily)||((r=n.component)==null?void 0:r.toLowerCase())||"generic";try{let[i,a,c]=await Promise.all([Tt(t,"search_design_knowledge",{query:`${s} component essential properties states variants`,category:"components",limit:2},3e3),Tt(t,"search_design_knowledge",{query:`design tokens ${s} semantic naming`,category:"tokens",limit:2},3e3),Tt(t,"search_chunks",{query:`component assessment scoring criteria ${s}`,limit:1},3e3)]);return{bestPractices:(i==null?void 0:i.entries)||[],tokenGuidance:(a==null?void 0:a.entries)||[],scoringCriteria:(c==null?void 0:c.chunks)||[],success:!0}}catch(i){return console.warn("\u26A0\uFE0F MCP queries failed:",i),{bestPractices:[],tokenGuidance:[],scoringCriteria:[],success:!1,error:i instanceof Error?i.message:"Unknown error"}}}async function Tt(e,t,n,s=5e3){var i,a;let o=new AbortController,r=setTimeout(()=>o.abort(),s);try{let c={jsonrpc:"2.0",id:Math.floor(Math.random()*1e3)+100,method:"tools/call",params:{name:`mcp_design-systems_${t}`,arguments:n}},d=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c),signal:o.signal});if(clearTimeout(r),!d.ok)throw new Error(`MCP ${t} failed: ${d.status}`);return((a=(i=(await d.json()).result)==null?void 0:i.content)==null?void 0:a[0])||{}}catch(c){throw clearTimeout(r),c instanceof Error&&c.name==="AbortError"?new Error(`MCP ${t} timeout after ${s}ms`):c}}function Ci(e,t,n){var o;let s=R({},e);return s.propertyCheatSheet=xi(n.actualProperties,e.component||n.context.name),s.audit={designIssues:[],tokenOpportunities:[],structureIssues:[]},(!n.componentDescription||n.componentDescription.trim().length===0)&&s.audit.structureIssues.push("Component lacks description - Add a description in component properties to help MCP and AI understand the component's purpose and usage"),t!=null&&t.success?s.mcpReadiness=Ii(t,e,n):s.mcpReadiness=Fs(n),s.component=s.component||n.context.name,s.description=s.description||`${((o=n.context.additionalContext)==null?void 0:o.componentFamily)||"Component"} with ${n.actualProperties.length} properties`,s.props=s.props||n.actualProperties.map(r=>({name:r.name,type:"select",description:`Controls ${r.name}`,values:r.values,default:r.default})),s.states=s.states||n.actualStates,s.recommendedProperties=e.recommendedProperties||[],s}function Ii(e,t,n){var l,p;let s=[],o=[],r=[];((l=e.bestPractices)==null?void 0:l.length)>0&&e.bestPractices.forEach(u=>{var g,f;((g=u.title)!=null&&g.includes("best practice")||(f=u.title)!=null&&f.includes("pattern"))&&r.push(`Follow ${u.title}`)});let i=n.actualStates.length>=3,a=n.tokens.summary&&n.tokens.summary.actualTokens>n.tokens.summary.hardCodedValues,c=((p=t.structure)==null?void 0:p.complexity)!=="high";return i?s.push("Component has comprehensive states"):o.push("Missing interactive states"),a?s.push("Good token usage"):o.push("Improve token adoption"),c?s.push("Well-structured component"):o.push("Complex structure may need simplification"),{score:Math.round((i?35:15)+(a?35:15)+(c?30:20)),strengths:s,gaps:o,recommendations:r.slice(0,3)}}function xi(e,t){let n=[],s=e.filter(c=>c.name.toLowerCase().includes("size")||c.values.some(d=>["small","medium","large"].includes(d.toLowerCase()))),o=e.filter(c=>c.name.toLowerCase().includes("variant")||c.name.toLowerCase().includes("type")),r=e.filter(c=>c.name.toLowerCase().includes("state")||c.values.some(d=>["hover","active","disabled"].includes(d.toLowerCase())));s.length>0&&n.push(`\u{1F4CF} Sizes: ${s.map(c=>c.values.join("/")).join(", ")}`),o.length>0&&n.push(`\u{1F3A8} Variants: ${o.map(c=>`${c.name}(${c.values.length})`).join(", ")}`),r.length>0&&n.push(`\u{1F504} States: ${r.map(c=>c.values.join("/")).join(", ")}`);let i=new Set([...s,...o,...r].map(c=>c.name)),a=e.filter(c=>!i.has(c.name)).slice(0,3).map(c=>`${c.name}: ${c.values.slice(0,3).join("/")}`);return a.length>0&&n.push(`\u2699\uFE0F Other: ${a.join(", ")}`),n.slice(0,5)}async function Rt(e,t,n,s,o,r,i){var a;try{console.log("\u{1F504} Processing analysis result..."),console.log("\u{1F4CA} Filtered data received:",JSON.stringify(e,null,2).substring(0,500)+"...");let c=figma.currentPage.selection,d=null;if(c.length>0)d=c[0];else throw new Error("No component selected");let l=await Ms(d,d),p=await Ge(d),u="";if(d.type==="COMPONENT"||d.type==="COMPONENT_SET")u=d.description||"";else if(d.type==="INSTANCE"){let y=await d.getMainComponentAsync();y&&(u=y.description||"")}let g={colors:[],spacing:[],typography:[],effects:[],borders:[],summary:{totalTokens:0,actualTokens:0,hardCodedValues:0,aiSuggestions:0,byCategory:{}}};n.includeTokenAnalysis!==!1&&(g=await ue(d));let f={component:e.component||t.name||"Component",description:e.description||`A ${t.type} component with ${l.length} properties`,props:e.props&&e.props.length>0?e.props:l.map(N=>({name:N.name,type:"select",description:`Controls ${N.name}`,values:N.values,defaultValue:N.default,required:!1})),states:e.states&&e.states.length>0?e.states.map(N=>typeof N=="string"?N:N.name):p.length>0?p:["default"],variants:e.variants||{},slots:e.slots||[],tokens:e.tokens||{colors:g.colors.filter(N=>N.isActualToken).map(N=>N.name),spacing:g.spacing.filter(N=>N.isActualToken).map(N=>N.name),typography:g.typography.filter(N=>N.isActualToken).map(N=>N.name)},usage:e.usage||"General purpose component for design systems",accessibility:e.accessibility||{keyboardNavigation:"Standard keyboard navigation support",screenReader:"Screen reader accessible",colorContrast:"WCAG compliant contrast ratios"},audit:e.audit||{accessibilityIssues:[],namingIssues:[],consistencyIssues:[],tokenOpportunities:[]},propertyCheatSheet:e.propertyCheatSheet||l.map(N=>({name:N.name,values:N.values,default:N.default,description:`Property for ${N.name} configuration`})),mcpReadiness:e.mcpReadiness||Fs({node:d,context:t,actualProperties:l,actualStates:p,tokens:g,componentDescription:u})};console.log("\u{1F4E4} Sending to UI - metadata.props:",(a=f.props)==null?void 0:a.length),console.log("\u{1F4E4} Sending to UI - metadata.states:",f.states),console.log("\u{1F4E4} Sending to UI - metadata.mcpReadiness:",f.mcpReadiness);let m=await Ti(e,t,d,l,p,g,u),h=(e.recommendedProperties||[]).map(N=>({name:N.name||"",type:N.type||"VARIANT",description:N.description||"",examples:N.examples||[]})).filter(N=>N.name);console.log(`\u{1F4A1} AI-generated property recommendations: ${h.length}`);let C=pn(d,5);console.log(`\u{1F4DB} Found ${C.length} naming issues`),s&&s.errors.length>0&&(m.designLint=Ai(s));let k;if(o&&r&&i)try{k=await Ei(t,s,m,g,C,h,o,r,i),console.log(`\u{1F4CB} Design review generated: ${k.verdict} \u2014 ${k.findings.length} findings`)}catch(N){console.warn("\u26A0\uFE0F Design review generation failed, continuing without it:",N),k=Pt(s,m,g,C)}else k=Pt(s,m,g,C);return console.log("\u2705 Analysis result processed successfully"),{metadata:f,tokens:g,audit:m,properties:l,recommendations:h,namingIssues:C,existingDescription:u,lintResult:s,designReview:k}}catch(c){throw console.error("Error processing analysis result:",c),c}}function Ai(e){let t=[],n=e.summary.byType;return n.fill>0?t.push({check:`Fill styles (${n.fill} missing)`,status:"fail",suggestion:`${n.fill} layer${n.fill>1?"s use":" uses"} hard-coded fills instead of design styles`}):t.push({check:"Fill styles",status:"pass",suggestion:"All fills use design styles"}),n.stroke>0?t.push({check:`Stroke styles (${n.stroke} missing)`,status:"fail",suggestion:`${n.stroke} layer${n.stroke>1?"s use":" uses"} hard-coded strokes instead of design styles`}):t.push({check:"Stroke styles",status:"pass",suggestion:"All strokes use design styles"}),n.effect>0?t.push({check:`Effect styles (${n.effect} missing)`,status:"fail",suggestion:`${n.effect} layer${n.effect>1?"s use":" uses"} hard-coded effects instead of design styles`}):t.push({check:"Effect styles",status:"pass",suggestion:"All effects use design styles"}),n.text>0?t.push({check:`Text styles (${n.text} missing)`,status:"fail",suggestion:`${n.text} text layer${n.text>1?"s lack":" lacks"} applied text styles`}):t.push({check:"Text styles",status:"pass",suggestion:"All text uses design styles"}),n.radius>0?t.push({check:`Border radius (${n.radius} non-standard)`,status:"warning",suggestion:`${n.radius} layer${n.radius>1?"s use":" uses"} non-standard border radius values`}):t.push({check:"Border radius",status:"pass",suggestion:"All radii match design system standards"}),n.spacing>0?t.push({check:`Spacing rhythm (${n.spacing} off-grid)`,status:"warning",suggestion:`${n.spacing} spacing value${n.spacing>1?"s are":" is"} not on the 4/8px grid`}):t.push({check:"Spacing rhythm",status:"pass",suggestion:"All spacing values follow the design grid"}),n.autoLayout>0?t.push({check:`Auto Layout (${n.autoLayout} missing)`,status:"warning",suggestion:`${n.autoLayout} frame${n.autoLayout>1?"s lack":" lacks"} auto-layout`}):t.push({check:"Auto Layout",status:"pass",suggestion:"All container frames use auto-layout"}),t}function Pt(e,t,n,s){let o=[];if(e)for(let u of e.errors)o.push({severity:u.errorType==="radius"||u.errorType==="spacing"||u.errorType==="autoLayout"?"warning":"critical",category:"Style Consistency",title:u.message,description:`Layer "${u.nodeName}" (${u.nodeType}) at ${u.path}`,nodeId:u.nodeId,nodeName:u.nodeName,autoFixable:!1});let r=n.summary.hardCodedValues;r>0&&o.push({severity:"warning",category:"Design Tokens",title:`${r} hard-coded value${r>1?"s":""} found`,description:"These values should be replaced with design tokens for consistency across the design system.",autoFixable:!0});for(let u of t.accessibility||[])u.status==="fail"?o.push({severity:"critical",category:"Accessibility",title:u.check,description:u.suggestion,autoFixable:!1}):u.status==="warning"&&o.push({severity:"warning",category:"Accessibility",title:u.check,description:u.suggestion,autoFixable:!1});for(let u of s.slice(0,10))o.push({severity:u.severity==="error"?"warning":"info",category:"Naming",title:`"${u.currentName}" should be "${u.suggestedName}"`,description:u.reason,nodeId:u.nodeId,nodeName:u.currentName,autoFixable:!0});for(let u of t.componentReadiness||[])u.status==="fail"&&o.push({severity:"suggestion",category:"Component Readiness",title:u.check,description:u.suggestion,autoFixable:!1});let i=(t.states||[]).filter(u=>!u.found);i.length>0&&o.push({severity:"suggestion",category:"Interactive States",title:`${i.length} state${i.length>1?"s":""} not detected`,description:`Missing: ${i.map(u=>u.name).join(", ")}`,autoFixable:!1});let a=o.filter(u=>u.severity==="critical").length,c=o.filter(u=>u.severity==="warning").length,d=a>0?"fail":c>3?"warn":"pass",l;d==="pass"?l="Component follows design system conventions well.":d==="warn"?l=`${c} issues need attention before this component is production-ready.`:l=`${a} critical issue${a>1?"s":""} found \u2014 missing design styles affect consistency.`;let p=[];return e&&e.summary.byType.fill>0&&p.push("Apply fill styles to layers using hard-coded colors"),e&&e.summary.byType.text>0&&p.push("Apply text styles to text layers"),e&&e.summary.byType.stroke>0&&p.push("Apply stroke styles to layers with hard-coded strokes"),e&&e.summary.byType.spacing>0&&p.push("Fix off-grid spacing values to match the 4/8px grid"),e&&e.summary.byType.autoLayout>0&&p.push("Apply auto-layout to container frames"),r>0&&p.push("Replace hard-coded values with design tokens"),s.length>0&&p.push("Rename generic layers to semantic names"),i.length>0&&p.push(`Add missing states: ${i.map(u=>u.name).join(", ")}`),p.length===0&&p.push("Component looks great \u2014 consider documenting it for the team"),{verdict:d,headline:l,findings:o,nextSteps:p}}async function Ei(e,t,n,s,o,r,i,a,c){var C;let d=t?`${t.summary.totalErrors} lint issues (${t.summary.byType.fill} fills, ${t.summary.byType.stroke} strokes, ${t.summary.byType.effect} effects, ${t.summary.byType.text} text, ${t.summary.byType.radius} radius, ${t.summary.byType.spacing||0} spacing, ${t.summary.byType.autoLayout||0} auto-layout)`:"0 lint issues",l=(n.accessibility||[]).filter(k=>k.status==="fail").length,p=(n.componentReadiness||[]).filter(k=>k.status==="fail").length,u=(n.states||[]).filter(k=>!k.found),g=t?t.errors.slice(0,8).map(k=>`- [${k.errorType}] ${k.nodeName}: ${k.message}`).join(` `):"None",f=`You are a design system reviewer (like CodeRabbit but for Figma designs). Review this component and produce a structured JSON design review. **Component:** ${e.name} (${e.type}, family: ${((C=e.additionalContext)==null?void 0:C.componentFamily)||"generic"}) @@ -306,7 +306,7 @@ ${g}`:""} **Token Usage:** ${s.summary.actualTokens} tokens used, ${s.summary.hardCodedValues} hard-coded values **Accessibility Failures:** ${l} **Component Readiness Failures:** ${p} -**Missing States:** ${u.map(S=>S.name).join(", ")||"None"} +**Missing States:** ${u.map(k=>k.name).join(", ")||"None"} **Naming Issues:** ${o.length} **AI Recommendations:** ${r.length} property suggestions @@ -333,7 +333,7 @@ Rules: - Group similar lint errors (e.g. "5 layers missing fill styles" not 5 separate findings) - Max 10 findings, prioritized by severity - nextSteps: max 5, ordered by impact -- Be specific and actionable, not generic`,m=await ie(c,i,{prompt:f,model:a,maxTokens:1024,temperature:.1}),h=pe(m.content);return h?{verdict:h.verdict||"warn",headline:h.headline||"Review completed",findings:(h.findings||[]).map(S=>({severity:S.severity||"info",category:S.category||"General",title:S.title||"",description:S.description||"",nodeId:S.nodeId,nodeName:S.nodeName,autoFixable:S.autoFixable||!1})),nextSteps:h.nextSteps||[]}:Pt(t,n,s,o)}async function Ei(e,t,n,s,o,r,i){var f;let a=!1,c="";n.type==="COMPONENT"&&((f=n.parent)==null?void 0:f.type)==="COMPONENT_SET"?(c=n.parent.description||"",a=c.trim().length>0):n.type==="COMPONENT_SET"&&(a=!!(i&&i.trim().length>0));let d=!!(i&&i.trim().length>0),l=d?"pass":"warning",p="";d?p="Component has description for MCP/AI context":a?(l="pass",p="Component set has a description. Consider adding a variant-specific description for richer context."):p="Add a component description to help MCP and AI understand the component purpose and usage";let u=[{check:"Property configuration",status:s.length>0?"pass":"warning",suggestion:s.length>0?"Component has configurable properties":"Consider adding properties for component customization"},{check:"Component description",status:l,suggestion:p}],g=Li(n,o);return{states:o.map(m=>({name:m,found:!0})),componentReadiness:u,accessibility:g}}var Ti=["button","btn","link","anchor","checkbox","check-box","radio","toggle","switch","tab","chip","tag","input","select","dropdown","menu-item","menuitem","slider","stepper","icon-button","fab","action"];function Pi(e,t){let n=e.name.toLowerCase();if(Ti.some(o=>n.includes(o)))return!0;let s=["hover","pressed","focus","focused","active","disabled"];return!!t.some(o=>s.includes(o.toLowerCase()))}function Li(e,t){let n=[],s=Pi(e,t);if(s){let o="width"in e?e.width:0,r="height"in e?e.height:0,i=Math.min(o,r);i>=44?n.push({check:"Touch target size",status:"pass",suggestion:`Target size ${Math.round(o)}\xD7${Math.round(r)}px meets recommended 44px minimum`}):i>=24?n.push({check:"Touch target size",status:"warning",suggestion:`Target size ${Math.round(o)}\xD7${Math.round(r)}px meets WCAG minimum (24px) but is below recommended 44px`}):n.push({check:"Touch target size",status:"fail",suggestion:`Target size ${Math.round(o)}\xD7${Math.round(r)}px is below WCAG 2.5.8 minimum of 24\xD724px`})}if(s){let o=t.some(r=>{let i=r.toLowerCase();return i==="focus"||i==="focused"||i.includes("focus")});n.push({check:"Focus state",status:o?"pass":"warning",suggestion:o?"Component has a focus state for keyboard navigation":"Add a visible focus state to support keyboard navigation (WCAG 2.4.7)"})}if("findAll"in e){let r=e.findAll(i=>i.type==="TEXT");if(r.length>0){let i=1/0,a=!1;for(let c of r){let d=typeof c.fontSize=="number"?c.fontSize:0;d>0&&d<i&&(i=d),d>0&&d<12&&(a=!0)}a?n.push({check:"Minimum font size",status:"warning",suggestion:`Text as small as ${i}px detected. Consider using 12px minimum for readability`}):i!==1/0&&n.push({check:"Minimum font size",status:"pass",suggestion:`Smallest text is ${i}px, meets readability guidelines`})}}if("findAll"in e){let r=e.findAll(d=>d.type==="TEXT"),i=1/0,a=0,c="";for(let d of r){let l=d.fills;if(!Array.isArray(l)||l.length===0)continue;let p=l.find(h=>h.type==="SOLID"&&h.visible!==!1&&h.color&&!(h.boundVariables&&h.boundVariables.color));if(!p)continue;let u=Ne(d);if(!u)continue;let g=ne(p.color.r,p.color.g,p.color.b),f=ne(u.r,u.g,u.b),m=ke(g,f);a++,m<i&&(i=m,c=d.name||"text")}if(a>0&&i!==1/0){let d=i.toFixed(1);i>=4.5?n.push({check:"Color contrast",status:"pass",suggestion:`Lowest contrast ratio is ${d}:1, meets WCAG AA (4.5:1)`}):i>=3?n.push({check:"Color contrast",status:"warning",suggestion:`"${c}" has ${d}:1 contrast. Meets large text AA (3:1) but not normal text (4.5:1)`}):n.push({check:"Color contrast",status:"fail",suggestion:`"${c}" has ${d}:1 contrast, below WCAG AA minimum of 3:1`})}}return n.length===0&&n.push({check:"Accessibility review",status:"pass",suggestion:"No accessibility issues detected for this component type"}),n}function Fs(e){var I,w,P,O,M,z,x,$,v,T,L;let{node:t,context:n,actualProperties:s,actualStates:o,tokens:r,componentDescription:i}=e,a=n.componentFamily||"generic",c=[],d=[],l=[];i&&i.trim().length>0?c.push("Has component description for better MCP/AI context"):(d.push("Missing component description - AI cannot understand component purpose and intent"),l.push("Add a descriptive explanation in component properties to help AI understand the component's purpose, behavior, and usage patterns")),s.length>0?c.push(`Has ${s.length} configurable properties`):(d.push("No configurable properties - component cannot be customized for different use cases"),l.push("Add component properties for customization (size, variant, text content, etc.)"));let p=n.hasInteractiveElements&&a!=="badge"&&a!=="icon";p&&(o.length>1?c.push("Includes multiple component states"):(d.push("Missing interactive states - users won't receive proper feedback for interactions"),l.push("Add hover, focus, and disabled states with clear visual feedback")));let u={colors:((w=(I=r==null?void 0:r.colors)==null?void 0:I.filter(A=>A.isActualToken))==null?void 0:w.length)||0,spacing:((O=(P=r==null?void 0:r.spacing)==null?void 0:P.filter(A=>A.isActualToken))==null?void 0:O.length)||0,typography:((z=(M=r==null?void 0:r.typography)==null?void 0:M.filter(A=>A.isActualToken))==null?void 0:z.length)||0,hardCoded:[...((x=r==null?void 0:r.colors)==null?void 0:x.filter(A=>!A.isActualToken&&!A.isDefaultVariantStyle))||[],...(($=r==null?void 0:r.spacing)==null?void 0:$.filter(A=>!A.isActualToken&&!A.isDefaultVariantStyle))||[],...((v=r==null?void 0:r.typography)==null?void 0:v.filter(A=>!A.isActualToken&&!A.isDefaultVariantStyle))||[],...((T=r==null?void 0:r.effects)==null?void 0:T.filter(A=>!A.isActualToken&&!A.isDefaultVariantStyle))||[],...((L=r==null?void 0:r.borders)==null?void 0:L.filter(A=>!A.isActualToken&&!A.isDefaultVariantStyle))||[]].length},g=u.colors+u.spacing+u.typography;g>0?(c.push("Uses design tokens for consistency"),u.hardCoded>0&&(d.push("Found hard-coded values - inconsistent with design system"),l.push("Replace remaining hard-coded colors and spacing with design tokens"))):u.hardCoded>2&&(d.push("No design tokens used - component styling is inconsistent with design system"),l.push("Replace hard-coded values with design tokens for colors, spacing, and typography"));let f=s.some(A=>A.name.toLowerCase().includes("size")||A.name.toLowerCase().includes("scale")||A.name.toLowerCase().includes("dimension")),m=s.some(A=>A.name.toLowerCase().includes("variant")||A.name.toLowerCase().includes("style")||A.name.toLowerCase().includes("type"));a==="avatar"?!f&&s.length>0&&(d.push("No size variants defined - limits reusability across different contexts"),l.push("Add size property (xs, sm, md, lg, xl) for headers, lists, and profiles")):a==="button"?(o.length<=1&&(d.push("Missing interactive states - reduces accessibility and user feedback"),l.push("Add hover, focus, and disabled states with clear visual feedback")),!m&&s.length>0&&(d.push("No visual hierarchy variants - limits design flexibility"),l.push("Add variant property (primary, secondary, danger) for proper hierarchy"))):a==="input"?o.length<=1&&(d.push("Missing form states - poor accessibility and user experience"),l.push("Add focus, error, and disabled states with clear visual indicators")):a==="container"&&(!m&&s.length>0&&(d.push("No layout variants defined - limits flexibility for different use cases"),l.push("Add orientation property (horizontal, vertical) or density variants")),s.length>0&&!s.some(A=>A.name.toLowerCase().includes("spacing"))&&(d.push("No spacing customization - may not fit all design contexts"),l.push("Add spacing property to control internal padding and gaps"))),s.length===0?(d.push("No configurable properties - component lacks flexibility for different use cases"),a==="container"?l.push("Add layout properties for customization (orientation, spacing, alignment)"):l.push("Add component properties to enable customization and reuse")):s.length===1&&!f&&!m&&(d.push("Limited customization options - consider adding more properties for flexibility"),a!=="container"&&p&&o.length<=1?l.push("Add interactive states and additional variant options"):a==="container"&&l.push("Consider adding layout variant properties (orientation, density)")),c.length===0&&c.push("Component follows basic Figma structure patterns"),d.length===0&&d.push("Well-structured component - consider minor enhancements for broader usage"),l.length===0&&l.push("Component is well-configured - ready for code generation");let h=0,C=s.length>0,S=g>0,N=g>0?g/(g+u.hardCoded):0;if(C&&(h+=22),i&&i.trim().length>0&&(h+=3),h+=Math.round(25*N),n.hasInteractiveElements&&a!=="badge"&&a!=="icon"){let A=Math.min(o.length/3,1);h+=Math.round(20*A)}else h+=20;return(t.type==="COMPONENT"||t.type==="COMPONENT_SET"||t.type==="INSTANCE")&&(h+=10),n.name&&!n.name.toLowerCase().includes("untitled")&&(h+=10),(C||S||o.length>0)&&(h+=10),h=Math.max(0,Math.min(100,h)),{score:h,strengths:c,gaps:Ts(d),recommendations:Ts(l),implementationNotes:Ri(a,c,d,s,o,u)}}function Ri(e,t,n,s,o,r){let i=[];return e==="button"?(o.length<3&&i.push("Implement hover, focus, and active states for better interactivity"),s.length===0&&i.push("Add variant and size properties to support different use cases")):e==="input"?(o.includes("error")||i.push("Add error state with clear visual indicators for form validation"),i.push("Ensure proper label association and placeholder text patterns")):e==="card"?(i.push("Consider implementing click handlers for interactive cards"),s.length===0&&i.push("Add elevation or variant properties for visual hierarchy")):e==="avatar"?(i.push("Implement fallback patterns for missing images"),s.some(a=>a.name.toLowerCase().includes("size"))||i.push("Add size variants for flexible usage across contexts")):e==="container"&&(i.push("Focus on layout flexibility and content composition"),i.push("Consider responsive behavior for different screen sizes")),r.hardCoded>r.colors+r.spacing&&i.push("Prioritize converting hard-coded values to design tokens"),s.length===0?i.push("Define component properties to enable customization without code changes"):s.length===1&&i.push("Consider additional properties for greater flexibility"),i.length===0&&(n.length>3?i.push("Focus on addressing the high-priority gaps identified above"):t.length>n.length?i.push("Component is well-structured for code generation with minor improvements needed"):i.push("Balance quick wins with systematic improvements for optimal results")),i.join(". ")+"."}function Ts(e){if(e.length<=1)return e;let t=[],n=new Set,s=[{pattern:/add.*component.*propert/i,message:"Add component properties for customization and reuse"},{pattern:/add.*(hover|focus|disabled|interactive).*state/i,message:"Add hover, focus, and disabled states with clear visual feedback"},{pattern:/replace.*hard.coded.*(color|spacing|token)/i,message:"Replace remaining hard-coded colors and spacing with design tokens"},{pattern:/add.*(size|variant).*propert/i,message:"Add size and style variant properties for different use cases"},{pattern:/no.*configurable.*propert.*(cannot|lacks|limited)/i,message:"No configurable properties - component lacks flexibility for different use cases"},{pattern:/(missing|no).*(interactive|hover|focus).*state/i,message:"Missing interactive states - reduces accessibility and user feedback"},{pattern:/found.*hard.coded.*value.*(inconsistent|design.*system)/i,message:"Found hard-coded values - inconsistent with design system"},{pattern:/(minimal|simple).*layer.*structure.*(lack|semantic|organization)/i,message:"Minimal layer structure - may lack semantic organization for complex use cases"}];return e.forEach(o=>{let r=o.trim();if(!r)return;let i=!0,a=r;for(let{pattern:l,message:p}of s)if(l.test(r))if(n.has(l.source)){i=!1;break}else{n.add(l.source),a=p;break}let c=r.toLowerCase(),d=t.some(l=>l.toLowerCase()===c||$i(l.toLowerCase(),c)>.8);i&&!d&&t.push(a)}),console.log(`\u{1F50D} [DEDUP] Reduced ${e.length} items to ${t.length}`),e.length!==t.length&&(console.log("\u{1F50D} [DEDUP] Original:",e),console.log("\u{1F50D} [DEDUP] Deduplicated:",t)),t}function $i(e,t){let n=e.length>t.length?e:t,s=e.length>t.length?t:e;if(n.length===0)return 1;let o=Mi(n,s);return(n.length-o)/n.length}function Mi(e,t){let n=[];for(let s=0;s<=t.length;s++)n[s]=[s];for(let s=0;s<=e.length;s++)n[0][s]=s;for(let s=1;s<=t.length;s++)for(let o=1;o<=e.length;o++)t.charAt(s-1)===e.charAt(o-1)?n[s][o]=n[s-1][o-1]:n[s][o]=Math.min(n[s-1][o-1]+1,n[s][o-1]+1,n[s-1][o]+1);return n[t.length][e.length]}var $t=class{constructor(t={}){this.cache=new Map;this.designSystemsKnowledge=null;this.config=R({enableCaching:!0,enableMCPIntegration:!1,consistencyThreshold:.95},t)}generateComponentHash(t,n,s){var r,i;let o={name:t.name,type:t.type,hierarchy:this.normalizeHierarchy(t.hierarchy),frameStructure:t.frameStructure,detectedStyles:t.detectedStyles,tokenFingerprint:this.generateTokenFingerprint(n),staticProperties:{hasInteractiveElements:((r=t.additionalContext)==null?void 0:r.hasInteractiveElements)||!1,componentFamily:((i=t.additionalContext)==null?void 0:i.componentFamily)||"generic"},lintSettingsFingerprint:s?this.createHash(JSON.stringify(s)):""};return this.createHash(JSON.stringify(o))}getCachedAnalysis(t){if(!this.config.enableCaching)return null;let n=this.cache.get(t);return n?Date.now()-n.timestamp>24*60*60*1e3?(this.cache.delete(t),null):(console.log("\u2705 Using cached analysis for component hash:",t),n):null}cacheAnalysis(t,n){var s;this.config.enableCaching&&(this.cache.set(t,{hash:t,result:n,timestamp:Date.now(),mcpKnowledgeVersion:((s=this.designSystemsKnowledge)==null?void 0:s.version)||"1.0.0"}),console.log("\u{1F4BE} Cached analysis for component hash:",t))}setDesignSystemsKnowledge(t){this.designSystemsKnowledge=t}async loadDesignSystemsKnowledge(){this.loadFallbackKnowledge()}createDeterministicPrompt(t){let n=this.createBasePrompt(t),s=this.getMCPGuidance(t),o=this.getScoringCriteria(t);return`${n} +- Be specific and actionable, not generic`,m=await ae(c,i,{prompt:f,model:a,maxTokens:1024,temperature:.1}),h=pe(m.content);return h?{verdict:h.verdict||"warn",headline:h.headline||"Review completed",findings:(h.findings||[]).map(k=>({severity:k.severity||"info",category:k.category||"General",title:k.title||"",description:k.description||"",nodeId:k.nodeId,nodeName:k.nodeName,autoFixable:k.autoFixable||!1})),nextSteps:h.nextSteps||[]}:Pt(t,n,s,o)}async function Ti(e,t,n,s,o,r,i){var f;let a=!1,c="";n.type==="COMPONENT"&&((f=n.parent)==null?void 0:f.type)==="COMPONENT_SET"?(c=n.parent.description||"",a=c.trim().length>0):n.type==="COMPONENT_SET"&&(a=!!(i&&i.trim().length>0));let d=!!(i&&i.trim().length>0),l=d?"pass":"warning",p="";d?p="Component has description for MCP/AI context":a?(l="pass",p="Component set has a description. Consider adding a variant-specific description for richer context."):p="Add a component description to help MCP and AI understand the component purpose and usage";let u=[{check:"Property configuration",status:s.length>0?"pass":"warning",suggestion:s.length>0?"Component has configurable properties":"Consider adding properties for component customization"},{check:"Component description",status:l,suggestion:p}],g=Ri(n,o);return{states:o.map(m=>({name:m,found:!0})),componentReadiness:u,accessibility:g}}var Pi=["button","btn","link","anchor","checkbox","check-box","radio","toggle","switch","tab","chip","tag","input","select","dropdown","menu-item","menuitem","slider","stepper","icon-button","fab","action"];function Li(e,t){let n=e.name.toLowerCase();if(Pi.some(o=>n.includes(o)))return!0;let s=["hover","pressed","focus","focused","active","disabled"];return!!t.some(o=>s.includes(o.toLowerCase()))}function Ri(e,t){let n=[],s=Li(e,t);if(s){let o="width"in e?e.width:0,r="height"in e?e.height:0,i=Math.min(o,r);i>=44?n.push({check:"Touch target size",status:"pass",suggestion:`Target size ${Math.round(o)}\xD7${Math.round(r)}px meets recommended 44px minimum`}):i>=24?n.push({check:"Touch target size",status:"warning",suggestion:`Target size ${Math.round(o)}\xD7${Math.round(r)}px meets WCAG minimum (24px) but is below recommended 44px`}):n.push({check:"Touch target size",status:"fail",suggestion:`Target size ${Math.round(o)}\xD7${Math.round(r)}px is below WCAG 2.5.8 minimum of 24\xD724px`})}if(s){let o=t.some(r=>{let i=r.toLowerCase();return i==="focus"||i==="focused"||i.includes("focus")});n.push({check:"Focus state",status:o?"pass":"warning",suggestion:o?"Component has a focus state for keyboard navigation":"Add a visible focus state to support keyboard navigation (WCAG 2.4.7)"})}if("findAll"in e){let r=e.findAll(i=>i.type==="TEXT");if(r.length>0){let i=1/0,a=!1;for(let c of r){let d=typeof c.fontSize=="number"?c.fontSize:0;d>0&&d<i&&(i=d),d>0&&d<12&&(a=!0)}a?n.push({check:"Minimum font size",status:"warning",suggestion:`Text as small as ${i}px detected. Consider using 12px minimum for readability`}):i!==1/0&&n.push({check:"Minimum font size",status:"pass",suggestion:`Smallest text is ${i}px, meets readability guidelines`})}}if("findAll"in e){let r=e.findAll(d=>d.type==="TEXT"),i=1/0,a=0,c="";for(let d of r){let l=d.fills;if(!Array.isArray(l)||l.length===0)continue;let p=l.find(h=>h.type==="SOLID"&&h.visible!==!1&&h.color&&!(h.boundVariables&&h.boundVariables.color));if(!p)continue;let u=Ne(d);if(!u)continue;let g=se(p.color.r,p.color.g,p.color.b),f=se(u.r,u.g,u.b),m=ke(g,f);a++,m<i&&(i=m,c=d.name||"text")}if(a>0&&i!==1/0){let d=i.toFixed(1);i>=4.5?n.push({check:"Color contrast",status:"pass",suggestion:`Lowest contrast ratio is ${d}:1, meets WCAG AA (4.5:1)`}):i>=3?n.push({check:"Color contrast",status:"warning",suggestion:`"${c}" has ${d}:1 contrast. Meets large text AA (3:1) but not normal text (4.5:1)`}):n.push({check:"Color contrast",status:"fail",suggestion:`"${c}" has ${d}:1 contrast, below WCAG AA minimum of 3:1`})}}return n.length===0&&n.push({check:"Accessibility review",status:"pass",suggestion:"No accessibility issues detected for this component type"}),n}function Fs(e){var I,w,P,O,M,z,x,$,v,T,L;let{node:t,context:n,actualProperties:s,actualStates:o,tokens:r,componentDescription:i}=e,a=n.componentFamily||"generic",c=[],d=[],l=[];i&&i.trim().length>0?c.push("Has component description for better MCP/AI context"):(d.push("Missing component description - AI cannot understand component purpose and intent"),l.push("Add a descriptive explanation in component properties to help AI understand the component's purpose, behavior, and usage patterns")),s.length>0?c.push(`Has ${s.length} configurable properties`):(d.push("No configurable properties - component cannot be customized for different use cases"),l.push("Add component properties for customization (size, variant, text content, etc.)"));let p=n.hasInteractiveElements&&a!=="badge"&&a!=="icon";p&&(o.length>1?c.push("Includes multiple component states"):(d.push("Missing interactive states - users won't receive proper feedback for interactions"),l.push("Add hover, focus, and disabled states with clear visual feedback")));let u={colors:((w=(I=r==null?void 0:r.colors)==null?void 0:I.filter(A=>A.isActualToken))==null?void 0:w.length)||0,spacing:((O=(P=r==null?void 0:r.spacing)==null?void 0:P.filter(A=>A.isActualToken))==null?void 0:O.length)||0,typography:((z=(M=r==null?void 0:r.typography)==null?void 0:M.filter(A=>A.isActualToken))==null?void 0:z.length)||0,hardCoded:[...((x=r==null?void 0:r.colors)==null?void 0:x.filter(A=>!A.isActualToken&&!A.isDefaultVariantStyle))||[],...(($=r==null?void 0:r.spacing)==null?void 0:$.filter(A=>!A.isActualToken&&!A.isDefaultVariantStyle))||[],...((v=r==null?void 0:r.typography)==null?void 0:v.filter(A=>!A.isActualToken&&!A.isDefaultVariantStyle))||[],...((T=r==null?void 0:r.effects)==null?void 0:T.filter(A=>!A.isActualToken&&!A.isDefaultVariantStyle))||[],...((L=r==null?void 0:r.borders)==null?void 0:L.filter(A=>!A.isActualToken&&!A.isDefaultVariantStyle))||[]].length},g=u.colors+u.spacing+u.typography;g>0?(c.push("Uses design tokens for consistency"),u.hardCoded>0&&(d.push("Found hard-coded values - inconsistent with design system"),l.push("Replace remaining hard-coded colors and spacing with design tokens"))):u.hardCoded>2&&(d.push("No design tokens used - component styling is inconsistent with design system"),l.push("Replace hard-coded values with design tokens for colors, spacing, and typography"));let f=s.some(A=>A.name.toLowerCase().includes("size")||A.name.toLowerCase().includes("scale")||A.name.toLowerCase().includes("dimension")),m=s.some(A=>A.name.toLowerCase().includes("variant")||A.name.toLowerCase().includes("style")||A.name.toLowerCase().includes("type"));a==="avatar"?!f&&s.length>0&&(d.push("No size variants defined - limits reusability across different contexts"),l.push("Add size property (xs, sm, md, lg, xl) for headers, lists, and profiles")):a==="button"?(o.length<=1&&(d.push("Missing interactive states - reduces accessibility and user feedback"),l.push("Add hover, focus, and disabled states with clear visual feedback")),!m&&s.length>0&&(d.push("No visual hierarchy variants - limits design flexibility"),l.push("Add variant property (primary, secondary, danger) for proper hierarchy"))):a==="input"?o.length<=1&&(d.push("Missing form states - poor accessibility and user experience"),l.push("Add focus, error, and disabled states with clear visual indicators")):a==="container"&&(!m&&s.length>0&&(d.push("No layout variants defined - limits flexibility for different use cases"),l.push("Add orientation property (horizontal, vertical) or density variants")),s.length>0&&!s.some(A=>A.name.toLowerCase().includes("spacing"))&&(d.push("No spacing customization - may not fit all design contexts"),l.push("Add spacing property to control internal padding and gaps"))),s.length===0?(d.push("No configurable properties - component lacks flexibility for different use cases"),a==="container"?l.push("Add layout properties for customization (orientation, spacing, alignment)"):l.push("Add component properties to enable customization and reuse")):s.length===1&&!f&&!m&&(d.push("Limited customization options - consider adding more properties for flexibility"),a!=="container"&&p&&o.length<=1?l.push("Add interactive states and additional variant options"):a==="container"&&l.push("Consider adding layout variant properties (orientation, density)")),c.length===0&&c.push("Component follows basic Figma structure patterns"),d.length===0&&d.push("Well-structured component - consider minor enhancements for broader usage"),l.length===0&&l.push("Component is well-configured - ready for code generation");let h=0,C=s.length>0,k=g>0,N=g>0?g/(g+u.hardCoded):0;if(C&&(h+=22),i&&i.trim().length>0&&(h+=3),h+=Math.round(25*N),n.hasInteractiveElements&&a!=="badge"&&a!=="icon"){let A=Math.min(o.length/3,1);h+=Math.round(20*A)}else h+=20;return(t.type==="COMPONENT"||t.type==="COMPONENT_SET"||t.type==="INSTANCE")&&(h+=10),n.name&&!n.name.toLowerCase().includes("untitled")&&(h+=10),(C||k||o.length>0)&&(h+=10),h=Math.max(0,Math.min(100,h)),{score:h,strengths:c,gaps:Ts(d),recommendations:Ts(l),implementationNotes:$i(a,c,d,s,o,u)}}function $i(e,t,n,s,o,r){let i=[];return e==="button"?(o.length<3&&i.push("Implement hover, focus, and active states for better interactivity"),s.length===0&&i.push("Add variant and size properties to support different use cases")):e==="input"?(o.includes("error")||i.push("Add error state with clear visual indicators for form validation"),i.push("Ensure proper label association and placeholder text patterns")):e==="card"?(i.push("Consider implementing click handlers for interactive cards"),s.length===0&&i.push("Add elevation or variant properties for visual hierarchy")):e==="avatar"?(i.push("Implement fallback patterns for missing images"),s.some(a=>a.name.toLowerCase().includes("size"))||i.push("Add size variants for flexible usage across contexts")):e==="container"&&(i.push("Focus on layout flexibility and content composition"),i.push("Consider responsive behavior for different screen sizes")),r.hardCoded>r.colors+r.spacing&&i.push("Prioritize converting hard-coded values to design tokens"),s.length===0?i.push("Define component properties to enable customization without code changes"):s.length===1&&i.push("Consider additional properties for greater flexibility"),i.length===0&&(n.length>3?i.push("Focus on addressing the high-priority gaps identified above"):t.length>n.length?i.push("Component is well-structured for code generation with minor improvements needed"):i.push("Balance quick wins with systematic improvements for optimal results")),i.join(". ")+"."}function Ts(e){if(e.length<=1)return e;let t=[],n=new Set,s=[{pattern:/add.*component.*propert/i,message:"Add component properties for customization and reuse"},{pattern:/add.*(hover|focus|disabled|interactive).*state/i,message:"Add hover, focus, and disabled states with clear visual feedback"},{pattern:/replace.*hard.coded.*(color|spacing|token)/i,message:"Replace remaining hard-coded colors and spacing with design tokens"},{pattern:/add.*(size|variant).*propert/i,message:"Add size and style variant properties for different use cases"},{pattern:/no.*configurable.*propert.*(cannot|lacks|limited)/i,message:"No configurable properties - component lacks flexibility for different use cases"},{pattern:/(missing|no).*(interactive|hover|focus).*state/i,message:"Missing interactive states - reduces accessibility and user feedback"},{pattern:/found.*hard.coded.*value.*(inconsistent|design.*system)/i,message:"Found hard-coded values - inconsistent with design system"},{pattern:/(minimal|simple).*layer.*structure.*(lack|semantic|organization)/i,message:"Minimal layer structure - may lack semantic organization for complex use cases"}];return e.forEach(o=>{let r=o.trim();if(!r)return;let i=!0,a=r;for(let{pattern:l,message:p}of s)if(l.test(r))if(n.has(l.source)){i=!1;break}else{n.add(l.source),a=p;break}let c=r.toLowerCase(),d=t.some(l=>l.toLowerCase()===c||Mi(l.toLowerCase(),c)>.8);i&&!d&&t.push(a)}),console.log(`\u{1F50D} [DEDUP] Reduced ${e.length} items to ${t.length}`),e.length!==t.length&&(console.log("\u{1F50D} [DEDUP] Original:",e),console.log("\u{1F50D} [DEDUP] Deduplicated:",t)),t}function Mi(e,t){let n=e.length>t.length?e:t,s=e.length>t.length?t:e;if(n.length===0)return 1;let o=Oi(n,s);return(n.length-o)/n.length}function Oi(e,t){let n=[];for(let s=0;s<=t.length;s++)n[s]=[s];for(let s=0;s<=e.length;s++)n[0][s]=s;for(let s=1;s<=t.length;s++)for(let o=1;o<=e.length;o++)t.charAt(s-1)===e.charAt(o-1)?n[s][o]=n[s-1][o-1]:n[s][o]=Math.min(n[s-1][o-1]+1,n[s][o-1]+1,n[s-1][o]+1);return n[t.length][e.length]}var $t=class{constructor(t={}){this.cache=new Map;this.designSystemsKnowledge=null;this.config=R({enableCaching:!0,enableMCPIntegration:!1,consistencyThreshold:.95},t)}generateComponentHash(t,n,s){var r,i;let o={name:t.name,type:t.type,hierarchy:this.normalizeHierarchy(t.hierarchy),frameStructure:t.frameStructure,detectedStyles:t.detectedStyles,tokenFingerprint:this.generateTokenFingerprint(n),staticProperties:{hasInteractiveElements:((r=t.additionalContext)==null?void 0:r.hasInteractiveElements)||!1,componentFamily:((i=t.additionalContext)==null?void 0:i.componentFamily)||"generic"},lintSettingsFingerprint:s?this.createHash(JSON.stringify(s)):""};return this.createHash(JSON.stringify(o))}getCachedAnalysis(t){if(!this.config.enableCaching)return null;let n=this.cache.get(t);return n?Date.now()-n.timestamp>24*60*60*1e3?(this.cache.delete(t),null):(console.log("\u2705 Using cached analysis for component hash:",t),n):null}cacheAnalysis(t,n){var s;this.config.enableCaching&&(this.cache.set(t,{hash:t,result:n,timestamp:Date.now(),mcpKnowledgeVersion:((s=this.designSystemsKnowledge)==null?void 0:s.version)||"1.0.0"}),console.log("\u{1F4BE} Cached analysis for component hash:",t))}setDesignSystemsKnowledge(t){this.designSystemsKnowledge=t}async loadDesignSystemsKnowledge(){this.loadFallbackKnowledge()}createDeterministicPrompt(t){let n=this.createBasePrompt(t),s=this.getMCPGuidance(t),o=this.getScoringCriteria(t);return`${n} **CONSISTENCY REQUIREMENTS:** - Use DETERMINISTIC analysis based on the exact component structure provided @@ -398,7 +398,7 @@ ${o} - 70-79: Solid foundation, some important gaps - 60-69: Basic implementation, significant improvements needed - Below 60: Major issues, substantial rework required - `}loadFallbackKnowledge(){this.designSystemsKnowledge={version:"1.0.0-fallback",components:{button:"Button components require comprehensive state management",avatar:"Avatar components should support size variants and interactive states",card:"Card components need consistent spacing and content hierarchy",badge:"Badge components should use semantic colors for status indication",input:"Input components require comprehensive accessibility and validation",generic:"Generic components should follow basic design system principles"},tokens:"Use semantic token naming: semantic-color-primary, spacing-md-16px, text-size-lg-18px",accessibility:"Ensure WCAG 2.1 AA compliance with proper ARIA labels and keyboard support",scoring:this.getFallbackScoringCriteria(),lastUpdated:Date.now()}}isValidScore(t){return typeof t=="number"&&t>=0&&t<=100}validateComponentFamilyConsistency(t,n){let s=t.metadata;switch(n){case"button":return this.validateButtonComponent(s);case"avatar":return this.validateAvatarComponent(s);case"input":return this.validateInputComponent(s);default:return!0}}validateButtonComponent(t){var s;return((s=t.states)==null?void 0:s.some(o=>["hover","focus","active","disabled"].includes(o.toLowerCase())))||!1}validateAvatarComponent(t){var o,r,i;let n=((r=(o=t.variants)==null?void 0:o.size)==null?void 0:r.length)>0,s=(i=t.props)==null?void 0:i.some(a=>a.name.toLowerCase().includes("size"));return n||s||!1}validateInputComponent(t){var s;return((s=t.states)==null?void 0:s.some(o=>["focus","error","disabled","filled"].includes(o.toLowerCase())))||!1}validateTokenRecommendations(t){var s;return((s=t.colors)==null?void 0:s.some(o=>o.name.includes("semantic-")||o.name.includes("primary")||o.name.includes("secondary")))!==!1}applyComponentFamilyCorrections(t,n){var o,r,i;let s=R({},t);switch(n){case"button":(o=s.states)!=null&&o.includes("hover")||(s.states=[...s.states||[],"hover","focus","active","disabled"]);break;case"avatar":!((r=s.variants)!=null&&r.size)&&!((i=s.props)!=null&&i.some(a=>a.name.includes("size")))&&(s.variants=K(R({},s.variants),{size:["small","medium","large"]}));break}return s}applyTokenConsistencyCorrections(t){return t&&R({},t)}ensureConsistentScoring(t,n){return K(R({},t),{score:t.score||0})}},Ds=$t;J();function Mt(e,t,n){let s=f=>f<=.04045?f/12.92:Math.pow((f+.055)/1.055,2.4),o=s(e),r=s(t),i=s(n),a=(o*.4124564+r*.3575761+i*.1804375)/.95047,c=o*.2126729+r*.7151522+i*.072175,d=(o*.0193339+r*.119192+i*.9503041)/1.08883,l=f=>f>.008856?Math.cbrt(f):7.787*f+16/116,p=l(a),u=l(c),g=l(d);return{L:116*u-16,a:500*(p-u),b:200*(u-g)}}function Vs(e,t){let{L:n,a:s,b:o}=e,{L:r,a:i,b:a}=t,c=1,d=1,l=1,p=Math.sqrt(s*s+o*o),u=Math.sqrt(i*i+a*a),g=(p+u)/2,f=Math.pow(g,7),m=.5*(1-Math.sqrt(f/(f+6103515625))),h=s*(1+m),C=i*(1+m),S=Math.sqrt(h*h+o*o),N=Math.sqrt(C*C+a*a),y=Math.atan2(o,h)*180/Math.PI,b=Math.atan2(a,C)*180/Math.PI,I=(y%360+360)%360,w=(b%360+360)%360,P=r-n,O=N-S,M;S*N===0?M=0:Math.abs(w-I)<=180?M=w-I:w-I>180?M=w-I-360:M=w-I+360;let z=2*Math.sqrt(S*N)*Math.sin(M*Math.PI/360),x=(n+r)/2,$=(S+N)/2,v;S*N===0?v=I+w:Math.abs(I-w)<=180?v=(I+w)/2:I+w<360?v=(I+w+360)/2:v=(I+w-360)/2;let T=1-.17*Math.cos((v-30)*Math.PI/180)+.24*Math.cos(2*v*Math.PI/180)+.32*Math.cos((3*v+6)*Math.PI/180)-.2*Math.cos((4*v-63)*Math.PI/180),L=1+.015*Math.pow(x-50,2)/Math.sqrt(20+Math.pow(x-50,2)),A=1+.045*$,ee=1+.015*$*T,Y=Math.pow($,7),ve=-2*Math.sqrt(Y/(Y+6103515625))*Math.sin(60*Math.exp(-Math.pow((v-275)/25,2))*Math.PI/180);return Math.sqrt(Math.pow(P/(c*L),2)+Math.pow(O/(d*A),2)+Math.pow(z/(l*ee),2)+ve*(O/(d*A))*(z/(l*ee)))}async function Oi(e,t,n,s=0){try{if(!(t in e))return{success:!1,message:`Node does not support ${t}`,error:`Property ${t} not found on node type ${e.type}`};let o=await figma.variables.getVariableByIdAsync(n);if(!o)return{success:!1,message:"Variable not found",error:`Could not find variable with ID: ${n}`};if(o.resolvedType!=="COLOR")return{success:!1,message:"Variable is not a color type",error:`Variable ${o.name} is of type ${o.resolvedType}, expected COLOR`};let i=[...e[t]];if(s>=i.length)return{success:!1,message:"Paint index out of range",error:`Paint index ${s} does not exist. Node has ${i.length} ${t}.`};let a=i[s];if(a.type!=="SOLID")return{success:!1,message:"Can only bind to solid paints",error:`Paint at index ${s} is of type ${a.type}, expected SOLID`};let c=figma.variables.setBoundVariableForPaint(a,"color",o);return i[s]=c,t==="fills"?e.fills=i:e.strokes=i,{success:!0,message:`Successfully bound ${o.name} to ${t}[${s}]`,appliedFix:{nodeId:e.id,nodeName:e.name,propertyPath:`${t}[${s}]`,beforeValue:a.type==="SOLID"&&a.color?F(a.color.r,a.color.g,a.color.b):"unknown",afterValue:o.name,tokenId:n,tokenName:o.name,fixType:"color"}}}catch(o){return{success:!1,message:"Failed to bind color token",error:o instanceof Error?o.message:String(o)}}}async function _s(e,t,n){try{if(!(t in e))return{success:!1,message:`Node does not support ${t}`,error:`Property ${t} not found on node type ${e.type}`};let s=await figma.variables.getVariableByIdAsync(n);if(!s)return{success:!1,message:"Variable not found",error:`Could not find variable with ID: ${n}`};if(s.resolvedType!=="FLOAT")return{success:!1,message:"Variable is not a number type",error:`Variable ${s.name} is of type ${s.resolvedType}, expected FLOAT`};let o=e[t];return e.setBoundVariable(t,s),{success:!0,message:`Successfully bound ${s.name} to ${t}`,appliedFix:{nodeId:e.id,nodeName:e.name,propertyPath:t,beforeValue:typeof o=="number"?`${o}px`:String(o),afterValue:s.name,tokenId:n,tokenName:s.name,fixType:t.includes("Radius")?"border":"spacing"}}}catch(s){return{success:!1,message:"Failed to bind spacing token",error:s instanceof Error?s.message:String(s)}}}async function Ot(e,t=0){try{let n=Di(e);if(!n)return[];let s=[],o=await figma.variables.getLocalVariablesAsync("COLOR"),r=await figma.variables.getLocalVariableCollectionsAsync(),i=new Map;for(let a of r)i.set(a.id,a);for(let a of o){let c=i.get(a.variableCollectionId);if(!c)continue;let d=c.modes[0].modeId,l=a.valuesByMode[d];if(!l||typeof l!="object"||!("r"in l))continue;let p=l,u=Vi(n,p);u>=1-t&&s.push({variableId:a.id,variableName:a.name,collectionName:c.name,value:F(p.r,p.g,p.b),matchScore:u,type:"color"})}return s.sort((a,c)=>c.matchScore-a.matchScore)}catch(n){return console.error("Error finding matching color variable:",n),[]}}async function Fi(e,t=0){try{let n=[],s=await figma.variables.getLocalVariablesAsync("FLOAT"),o=await figma.variables.getLocalVariableCollectionsAsync(),r=new Map;for(let i of o)r.set(i.id,i);for(let i of s){let a=r.get(i.variableCollectionId);if(!a)continue;let c=a.modes[0].modeId,d=i.valuesByMode[c];if(typeof d!="number")continue;let l=Math.abs(d-e);if(l<=t){let p=l===0?1:1-l/(t||1);n.push({variableId:i.id,variableName:i.name,collectionName:a.name,value:`${d}px`,matchScore:p,type:"number"})}}return n.sort((i,a)=>a.matchScore-i.matchScore)}catch(n){return console.error("Error finding matching spacing variable:",n),[]}}async function Ft(e,t,n=2){let s=await Fi(e,n);if(s.length===0)return s;let r={strokeWeight:["stroke","border-width","border/width","borderwidth"],cornerRadius:["radius","corner","round","border-radius"],topLeftRadius:["radius","corner","round"],topRightRadius:["radius","corner","round"],bottomLeftRadius:["radius","corner","round"],bottomRightRadius:["radius","corner","round"],paddingTop:["padding","spacing","space"],paddingRight:["padding","spacing","space"],paddingBottom:["padding","spacing","space"],paddingLeft:["padding","spacing","space"],itemSpacing:["gap","spacing","space"],counterAxisSpacing:["gap","spacing","space"]}[t]||[];return r.length===0?s:s.map(a=>{let c=a.variableName.toLowerCase(),d=r.some(l=>c.includes(l));return K(R({},a),{matchScore:d?Math.min(a.matchScore+.3,1):a.matchScore})}).sort((a,c)=>c.matchScore-a.matchScore)}async function Dt(e,t,n){let s=t.match(/^(fills|strokes)\[(\d+)\]$/);if(!s)return{success:!1,message:"Invalid property path",error:`Expected format: fills[n] or strokes[n], got: ${t}`};let[,o,r]=s,i=parseInt(r,10);return Oi(e,o,n,i)}async function Vt(e,t,n){if(!["paddingTop","paddingRight","paddingBottom","paddingLeft","itemSpacing","counterAxisSpacing","cornerRadius","topLeftRadius","topRightRadius","bottomLeftRadius","bottomRightRadius","strokeWeight"].includes(t))return{success:!1,message:"Invalid property path",error:`Property ${t} is not a valid spacing property`};if(t==="cornerRadius"){let o=["topLeftRadius","topRightRadius","bottomLeftRadius","bottomRightRadius"],r=[];for(let i of o){let a=await _s(e,i,n);if(r.push(a),!a.success)return{success:!1,message:`Failed to bind ${i}`,error:a.error}}return{success:!0,message:"Successfully bound variable to all 4 corner radii",appliedFix:r[0].appliedFix?K(R({},r[0].appliedFix),{propertyPath:"cornerRadius"}):void 0}}return _s(e,t,n)}async function _t(e,t,n){try{let s=await figma.variables.getVariableByIdAsync(n);if(!s)return null;let o,r,i=t.match(/^(fills|strokes)\[(\d+)\]$/);if(i){o="color";let[,d,l]=i,p=parseInt(l,10);if(!(d in e))return null;let g=e[d];if(p>=g.length)return null;let f=g[p];f.type==="SOLID"&&f.color?r=F(f.color.r,f.color.g,f.color.b):r=f.type}else{if(!(t in e))return null;let d=e[t];r=typeof d=="number"?`${d}px`:String(d),o=t.includes("Radius")?"border":"spacing"}let a=s.name,c=await figma.variables.getVariableCollectionByIdAsync(s.variableCollectionId);if(c){let d=c.modes[0].modeId,l=s.valuesByMode[d];if(typeof l=="number")a=`${s.name} (${l}px)`;else if(l&&typeof l=="object"&&"r"in l){let p=l;a=`${s.name} (${F(p.r,p.g,p.b)})`}}return{nodeId:e.id,nodeName:e.name,propertyPath:t,beforeValue:r,afterValue:a,tokenId:n,tokenName:s.name,fixType:o}}catch(s){return console.error("Error generating fix preview:",s),null}}function Di(e){let t=e.replace(/^#/,""),n=t;if(t.length===3&&(n=t[0]+t[0]+t[1]+t[1]+t[2]+t[2]),n.length!==6)return null;let s=parseInt(n.substring(0,2),16),o=parseInt(n.substring(2,4),16),r=parseInt(n.substring(4,6),16);return isNaN(s)||isNaN(o)||isNaN(r)?null:{r:s/255,g:o/255,b:r/255}}function Vi(e,t){let n=Mt(e.r,e.g,e.b),s=Mt(t.r,t.g,t.b),o=Vs(n,s);return o<3?1:o>=10?0:1-(o-3)/7}async function ze(e,t=1024){let n=Math.max(1,Math.min(t,Math.round(e.width))),s=await e.exportAsync({format:"PNG",constraint:{type:"WIDTH",value:n}});return _i(s)}function _i(e){let t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",n="",s=e.length;for(let o=0;o<s;o+=3){let r=e[o],i=o+1<s?e[o+1]:0,a=o+2<s?e[o+2]:0;n+=t[r>>2],n+=t[(r&3)<<4|i>>4],n+=o+1<s?t[(i&15)<<2|a>>6]:"=",n+=o+2<s?t[a&63]:"="}return n}var Bi=/button|btn|cta|link|tab|nav|menu|input|checkbox|toggle|switch|radio|select|dropdown|slider/i;function Bs(e){if(Bi.test(e.name))return!0;if("children"in e){for(let t of e.children)if(Bs(t))return!0}return!1}function Us(e,t,n){var s,o;if("reactions"in e){let r=e.reactions;if(r&&r.length>0)for(let i of r){let a=i.actions||(i.action?[i.action]:[]);for(let c of a)c.type==="NODE"&&c.destinationId&&n.push({sourceFrameId:t,sourceNodeId:e.id,sourceNodeName:e.name,destinationFrameId:c.destinationId,trigger:((s=i.trigger)==null?void 0:s.type)||"UNKNOWN",navigation:c.navigation||"NAVIGATE",hasTransition:!!c.transition}),c.type==="BACK"&&n.push({sourceFrameId:t,sourceNodeId:e.id,sourceNodeName:e.name,destinationFrameId:"__BACK__",trigger:((o=i.trigger)==null?void 0:o.type)||"UNKNOWN",navigation:"BACK",hasTransition:!!c.transition})}}if("children"in e)for(let r of e.children)Us(r,t,n)}function Gs(e){var z,x,$;let t=e||figma.currentPage,n=t.children.filter(v=>v.type==="FRAME"||v.type==="COMPONENT"),s=new Set((t.flowStartingPoints||[]).map(v=>v.nodeId)),o=n.map(v=>({id:v.id,name:v.name,pageId:t.id,pageName:t.name,width:v.width,height:v.height,isFlowStartingPoint:s.has(v.id),childCount:"children"in v?v.children.length:0,hasInteractiveElements:Bs(v)})),r=new Set(o.map(v=>v.id)),i=[];for(let v of n)Us(v,v.id,i);let a=i.filter(v=>v.destinationFrameId==="__BACK__"||r.has(v.destinationFrameId)),c=o.filter(v=>v.isFlowStartingPoint).map(v=>v.id),d=new Map,l=new Map;for(let v of r)d.set(v,new Set),l.set(v,new Set);for(let v of a)v.destinationFrameId!=="__BACK__"&&((z=d.get(v.sourceFrameId))==null||z.add(v.destinationFrameId),(x=l.get(v.destinationFrameId))==null||x.add(v.sourceFrameId));let p=new Set;for(let v of a)v.destinationFrameId==="__BACK__"&&p.add(v.sourceFrameId);let u=o.filter(v=>{var T;return(((T=d.get(v.id))==null?void 0:T.size)||0)===0&&!p.has(v.id)}).map(v=>v.id),g=o.filter(v=>{var T;return(((T=l.get(v.id))==null?void 0:T.size)||0)===0&&!s.has(v.id)}).map(v=>v.id),f=new Set,m=[...c];if(m.length===0)for(let v of o)((($=l.get(v.id))==null?void 0:$.size)||0)===0&&m.push(v.id);for(;m.length>0;){let v=m.shift();if(f.has(v))continue;f.add(v);let T=d.get(v);if(T)for(let L of T)f.has(L)||m.push(L)}let h=o.filter(v=>!f.has(v.id)).map(v=>v.id),C=[],S=new Set,N=new Set,y=[];function b(v){if(N.has(v)){let L=y.indexOf(v);L!==-1&&C.push(y.slice(L));return}if(S.has(v))return;S.add(v),N.add(v),y.push(v);let T=d.get(v);if(T)for(let L of T)b(L);y.pop(),N.delete(v)}for(let v of r)b(v);let I=o.map(v=>{var T;return((T=d.get(v.id))==null?void 0:T.size)||0}),w=I.length>0?I.reduce((v,T)=>v+T,0)/I.length:0,P=0,O=c.map(v=>({id:v,depth:0})),M=new Set;for(;O.length>0;){let{id:v,depth:T}=O.shift();if(M.has(v))continue;M.add(v),T>P&&(P=T);let L=d.get(v);if(L)for(let A of L)M.has(A)||O.push({id:A,depth:T+1})}return{frames:o,edges:a,entryPoints:c,deadEnds:u,orphans:g,unreachable:h,loops:C,stats:{totalFrames:o.length,totalEdges:a.length,totalEntryPoints:c.length,maxDepth:P,avgBranching:Math.round(w*100)/100}}}function zs(e){let t=[],n=new Map(e.frames.map(i=>[i.id,i.name])),s=i=>i.map(a=>`"${n.get(a)||a}"`).join(", ");for(let i of e.deadEnds){let a=n.get(i)||"";/success|confirm|done|complete|thank|receipt|summary/i.test(a)||t.push({type:"dead-end",severity:"warning",frameIds:[i],message:`${s([i])} has no outgoing connections \u2014 user gets stuck here.`})}e.orphans.length>0&&t.push({type:"orphan",severity:"warning",frameIds:e.orphans,message:`${s(e.orphans)} ${e.orphans.length===1?"has":"have"} no incoming connections \u2014 unreachable by navigation.`});let o=e.unreachable.filter(i=>!e.orphans.includes(i));o.length>0&&t.push({type:"unreachable",severity:"critical",frameIds:o,message:`${s(o)} ${o.length===1?"is":"are"} not reachable from any flow entry point.`});for(let i of e.loops){let a=new Set(i);i.some(d=>e.edges.filter(p=>p.sourceFrameId===d).some(p=>!a.has(p.destinationFrameId)))||t.push({type:"loop",severity:"warning",frameIds:i,message:`Circular flow without exit: ${s(i)}. User cannot leave this loop.`})}e.stats.maxDepth>3&&t.push({type:"deep-navigation",severity:"info",frameIds:[],message:`Navigation depth is ${e.stats.maxDepth} levels. Consider flattening to \u22643 levels for better UX (3-click rule).`});let r=e.frames.filter(i=>{if(i.isFlowStartingPoint)return!1;let a=e.edges.some(d=>d.sourceFrameId===i.id&&(d.navigation==="BACK"||d.navigation==="CLOSE"));return e.edges.some(d=>d.destinationFrameId===i.id)&&!a});return r.length>0&&t.push({type:"missing-back",severity:"info",frameIds:r.map(i=>i.id),message:`${r.length} frame${r.length===1?"":"s"} missing back/close navigation: ${s(r.map(i=>i.id))}.`}),t}J();function Hs(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if("fills"in e){let o=e.fills;if(o!==figma.mixed&&Array.isArray(o))for(let r of o)r.type==="SOLID"&&r.visible!==!1&&t.colors.add(F(r.color.r,r.color.g,r.color.b))}if("strokes"in e){let o=e.strokes;if(Array.isArray(o))for(let r of o)r.type==="SOLID"&&r.visible!==!1&&t.colors.add(F(r.color.r,r.color.g,r.color.b))}if(e.type==="TEXT"){let o=e;o.fontName!==figma.mixed&&t.fontFamilies.add(o.fontName.family),o.fontSize!==figma.mixed&&t.fontSizes.add(o.fontSize)}if("layoutMode"in e&&e.layoutMode!=="NONE"){let o=e;typeof o.itemSpacing=="number"&&t.spacingValues.add(o.itemSpacing),typeof o.paddingTop=="number"&&t.spacingValues.add(o.paddingTop),typeof o.paddingBottom=="number"&&t.spacingValues.add(o.paddingBottom),typeof o.paddingLeft=="number"&&t.spacingValues.add(o.paddingLeft),typeof o.paddingRight=="number"&&t.spacingValues.add(o.paddingRight)}if(e.type==="INSTANCE"){let o=e.mainComponent;o&&t.componentNames.add(o.name)}if("children"in e)for(let o of e.children)Hs(o,t,n,s)}}function Ws(e,t){let n=new Set;for(let s of e)t.has(s)||n.add(s);return n}function Ks(e,t={}){var g,f;let n=(g=t.skipLocked)!=null?g:!0,s=(f=t.skipHidden)!=null?f:!0,o=[];if(e.length<2)return o;let r=e.map(({frame:m,node:h})=>{let C={frameId:m.id,frameName:m.name,colors:new Set,fontFamilies:new Set,fontSizes:new Set,spacingValues:new Set,componentNames:new Set};return Hs(h,C,n,s),C}),i=new Map;for(let m of r)for(let h of m.colors)i.set(h,(i.get(h)||0)+1);let a=r.length*.5,c=new Set;for(let[m,h]of i)h>=a&&c.add(m);for(let m of r){let h=Ws(m.colors,c);h.size>3&&o.push({type:"dead-end",severity:"warning",frameIds:[m.frameId],message:`"${m.frameName}" uses ${h.size} colors not found in other screens (${[...h].slice(0,3).join(", ")}${h.size>3?"...":""}). Check for color inconsistency.`})}let d=new Set;for(let m of r)for(let h of m.fontFamilies)d.add(h);if(d.size>3){let m=[...d].join(", ");o.push({type:"dead-end",severity:"warning",frameIds:r.map(h=>h.frameId),message:`${d.size} different font families across flow: ${m}. Flows should use 1-2 font families for consistency.`})}for(let m of r){let h=new Set;for(let S of r)if(S.frameId!==m.frameId)for(let N of S.fontFamilies)h.add(N);let C=Ws(m.fontFamilies,h);C.size>0&&r.length>2&&o.push({type:"dead-end",severity:"info",frameIds:[m.frameId],message:`"${m.frameName}" uses font${C.size>1?"s":""} not seen elsewhere: ${[...C].join(", ")}.`})}let l=new Set;for(let m of r)for(let h of m.fontSizes)l.add(h);l.size>10&&o.push({type:"dead-end",severity:"info",frameIds:r.map(m=>m.frameId),message:`${l.size} unique font sizes across the flow. Consider using a type scale with fewer sizes for consistency.`});let p=new Set;for(let m of r)for(let h of m.spacingValues)h>0&&p.add(h);let u=[...p].filter(m=>m%4!==0&&m!==2);return u.length>3&&o.push({type:"dead-end",severity:"info",frameIds:r.map(m=>m.frameId),message:`${u.length} non-standard spacing values across flow (${u.slice(0,4).join(", ")}px). Consider aligning to a 4px/8px grid.`}),o}var He=Lo(js()),qs=8e4,Js="baseline::";function Ke(e){return`${Js}${e}::meta`}function Ut(e,t){return`${Js}${e}::chunk_${t}`}function Xs(e){let t=JSON.stringify(e),n=(0,He.compressToUTF16)(t),s=[];for(let r=0;r<n.length;r+=qs)s.push(n.slice(r,r+qs));let o=e.nodeId;eo(o),figma.root.setPluginData(Ke(o),JSON.stringify({chunkCount:s.length,timestamp:e.timestamp,nodeName:e.nodeName,overall:e.overall}));for(let r=0;r<s.length;r++)figma.root.setPluginData(Ut(o,r),s[r])}function Ys(e){let t=figma.root.getPluginData(Ke(e));if(!t)return null;let n;try{n=JSON.parse(t)}catch(i){return null}let s=[];for(let i=0;i<n.chunkCount;i++){let a=figma.root.getPluginData(Ut(e,i));if(!a)return null;s.push(a)}let o=s.join(""),r=(0,He.decompressFromUTF16)(o);if(!r)return null;try{return JSON.parse(r)}catch(i){return null}}function Qs(e){eo(e),figma.root.setPluginData(Ke(e),"")}function Zs(e){let t=figma.root.getPluginData(Ke(e));if(!t)return null;try{return JSON.parse(t)}catch(n){return null}}function eo(e){for(let t=0;t<100;t++){let n=Ut(e,t);if(!figma.root.getPluginData(n))break;figma.root.setPluginData(n,"")}}function je(e){return`${e.errorType}::${e.nodeId}::${e.message}`}function to(e,t){var l,p,u,g;let n=Date.now(),s=new Set([...Object.keys(e.categories),...Object.keys(t.categories)]),o=[];for(let f of s){let m=(p=(l=e.categories[f])==null?void 0:l.score)!=null?p:100,h=(g=(u=t.categories[f])==null?void 0:u.score)!=null?g:100;o.push({category:f,oldScore:m,newScore:h,delta:h-m})}o.sort((f,m)=>Math.abs(m.delta)-Math.abs(f.delta));let r=new Set(e.errors.map(je)),i=new Set(t.errors.map(je)),a=[],c=[],d=[];for(let f of t.errors){let m=je(f);r.has(m)?d.push({errorType:f.errorType,severity:f.severity,nodeId:f.nodeId,message:f.message}):a.push({errorType:f.errorType,severity:f.severity,nodeId:f.nodeId,message:f.message})}for(let f of e.errors){let m=je(f);i.has(m)||c.push({errorType:f.errorType,severity:f.severity,nodeId:f.nodeId,message:f.message})}return{baselineTimestamp:e.timestamp,currentTimestamp:n,scoreDelta:{overall:t.overall-e.overall,oldOverall:e.overall,newOverall:t.overall,categories:o},newIssues:a,fixedIssues:c,remainingIssues:d,summary:{totalNew:a.length,totalFixed:c.length,totalRemaining:d.length,oldTotal:e.errors.length,newTotal:t.errors.length}}}ft();var Ui=["itemSpacing","paddingTop","paddingBottom","paddingLeft","paddingRight","counterAxisSpacing"];function Gt(e,t,n){let s=figma.getNodeById(e);if(!s)return{success:!1,nodeId:e,nodeName:"",property:t,oldValue:0,newValue:n,error:"Node not found"};if(s.type!=="FRAME"&&s.type!=="COMPONENT"&&s.type!=="INSTANCE")return{success:!1,nodeId:e,nodeName:s.name,property:t,oldValue:0,newValue:n,error:"Node is not a frame"};let o=s;if(o.layoutMode==="NONE")return{success:!1,nodeId:e,nodeName:s.name,property:t,oldValue:0,newValue:n,error:"Frame has no auto-layout"};try{let r=o[t];return o[t]=n,{success:!0,nodeId:e,nodeName:s.name,property:t,oldValue:r,newValue:n}}catch(r){return{success:!1,nodeId:e,nodeName:s.name,property:t,oldValue:0,newValue:n,error:r instanceof Error?r.message:String(r)}}}function Ae(e,t){let n=figma.getNodeById(e);if(!n||n.type!=="FRAME"&&n.type!=="COMPONENT"&&n.type!=="INSTANCE")return{success:!1,nodeId:e,nodeName:(n==null?void 0:n.name)||"",property:t,oldValue:0,newValue:0,error:"Invalid node"};let o=n[t];if(typeof o!="number")return{success:!1,nodeId:e,nodeName:n.name,property:t,oldValue:0,newValue:0,error:"Property is not a number"};if(mt.includes(o))return{success:!0,nodeId:e,nodeName:n.name,property:t,oldValue:o,newValue:o};let r=Ve(o);if(r.length===0)return{success:!1,nodeId:e,nodeName:n.name,property:t,oldValue:o,newValue:o,error:"No suggestion found"};let i=r.reduce((a,c)=>Math.abs(a-o)<=Math.abs(c-o)?a:c);return Gt(e,t,i)}function no(e){let t=figma.getNodeById(e);if(!t||t.type!=="FRAME"&&t.type!=="COMPONENT"&&t.type!=="INSTANCE")return[];let n=t;if(n.layoutMode==="NONE")return[];let s=[];for(let o of Ui){if(!(o in n))continue;let r=n[o];if(typeof r!="number"||mt.includes(r))continue;let i=Ae(e,o);s.push(i)}return s}function Ee(e,t){return t.length===0?e:t.reduce((n,s)=>Math.abs(s-e)<Math.abs(n-e)?s:n)}function qe(e,t){let n=figma.getNodeById(e);if(!n)return{success:!1,nodeId:e,nodeName:"",oldValue:"",newValue:"",error:"Node not found"};if(!("cornerRadius"in n))return{success:!1,nodeId:e,nodeName:n.name,oldValue:"",newValue:"",error:"Node has no corner radius"};let s=n;try{let o=s.cornerRadius;if(o===figma.mixed){let r=s.topLeftRadius,i=s.topRightRadius,a=s.bottomLeftRadius,c=s.bottomRightRadius,d=`${r}/${i}/${c}/${a}`;s.topLeftRadius=Ee(r,t),s.topRightRadius=Ee(i,t),s.bottomLeftRadius=Ee(a,t),s.bottomRightRadius=Ee(c,t);let l=`${s.topLeftRadius}/${s.topRightRadius}/${s.bottomRightRadius}/${s.bottomLeftRadius}`;return{success:!0,nodeId:e,nodeName:n.name,oldValue:d,newValue:l}}else{let r=`${o}`,i=Ee(o,t);return s.cornerRadius=i,{success:!0,nodeId:e,nodeName:n.name,oldValue:r,newValue:`${i}`}}}catch(o){return{success:!1,nodeId:e,nodeName:n.name,oldValue:"",newValue:"",error:o instanceof Error?o.message:String(o)}}}function Je(e,t){let n=figma.getNodeById(e);if(!n||n.type==="DOCUMENT"||n.type==="PAGE")return{success:!1,nodeId:e,oldName:"",newName:t,error:"Node not found"};try{let s=n.name;return n.name=t,{success:!0,nodeId:e,oldName:s,newName:t}}catch(s){return{success:!1,nodeId:e,oldName:n.name,newName:t,error:s instanceof Error?s.message:String(s)}}}jt();async function oo(e){let t=0,n=0,s=[];for(let o=0;o<e.length;o++){let r=e[o];try{let i=await Gi(r);s.push(R({index:o},i)),i.success?t++:n++}catch(i){n++,s.push({index:o,type:r.type,success:!1,nodeId:String(r.params.nodeId||""),nodeName:"",message:"Unexpected error",error:i instanceof Error?i.message:String(i)})}}return{total:e.length,applied:t,failed:n,results:s}}async function Gi(e){let{type:t,params:n}=e;switch(t){case"applyStyle":{let s=n.styleType,o=n.nodeId,r=n.styleKey,i;switch(s){case"fill":i=await zt(o,r);break;case"stroke":i=await Wt(o,r);break;case"text":i=await Ht(o,r);break;case"effect":i=await Kt(o,r);break;default:return{type:t,success:!1,nodeId:o,nodeName:"",message:`Unknown style type: ${s}`}}return{type:t,success:i.success,nodeId:i.nodeId,nodeName:i.nodeName,message:i.success?`Applied ${i.property}: ${i.newValue}`:i.error||"Failed",oldValue:i.oldValue,newValue:i.newValue,error:i.error}}case"fixSpacing":{let s=n.nodeId,o=n.property,r=n.value,i=Gt(s,o,r);return{type:t,success:i.success,nodeId:i.nodeId,nodeName:i.nodeName,message:i.success?`${i.property}: ${i.oldValue}px \u2192 ${i.newValue}px`:i.error||"Failed",oldValue:`${i.oldValue}px`,newValue:`${i.newValue}px`,error:i.error}}case"fixSpacingToNearest":{let s=n.nodeId,o=n.property,r=Ae(s,o);return{type:t,success:r.success,nodeId:r.nodeId,nodeName:r.nodeName,message:r.success?`${r.property}: ${r.oldValue}px \u2192 ${r.newValue}px`:r.error||"Failed",oldValue:`${r.oldValue}px`,newValue:`${r.newValue}px`,error:r.error}}case"fixRadiusToNearest":{let s=n.nodeId,o=n.allowedRadii,r=qe(s,o);return{type:t,success:r.success,nodeId:r.nodeId,nodeName:r.nodeName,message:r.success?`radius: ${r.oldValue}px \u2192 ${r.newValue}px`:r.error||"Failed",oldValue:`${r.oldValue}px`,newValue:`${r.newValue}px`,error:r.error}}case"renameLayer":{let s=n.nodeId,o=n.newName,r=Je(s,o);return{type:t,success:r.success,nodeId:r.nodeId,nodeName:r.newName,message:r.success?`Renamed "${r.oldName}" \u2192 "${r.newName}"`:r.error||"Failed",oldValue:r.oldName,newValue:r.newName,error:r.error}}default:return{type:t,success:!1,nodeId:"",nodeName:"",message:`Unknown fix type: ${t}`}}}function zi(e){let t=new Map,n=0,s=0;function o(r){let i=r,a=Array.isArray(i.fills)&&i.fills.length>0,c=Array.isArray(i.strokes)&&i.strokes.length>0,d=Array.isArray(i.effects)&&i.effects.length>0,l=r.type==="TEXT",p=r.type==="FRAME"||r.type==="COMPONENT"||r.type==="INSTANCE";(a||c||d||l||p)&&n++;let u=!1;if("boundVariables"in r&&i.boundVariables){let g=i.boundVariables;for(let f of Object.keys(g)){let m=g[f];if(Array.isArray(m))for(let h of m)h&&h.id&&(t.set(h.id,(t.get(h.id)||0)+1),u=!0);else m&&m.id&&(t.set(m.id,(t.get(m.id)||0)+1),u=!0)}}if(Array.isArray(i.fills)){for(let g of i.fills)if(g.boundVariables)for(let f of Object.keys(g.boundVariables)){let m=g.boundVariables[f];m&&m.id&&(t.set(m.id,(t.get(m.id)||0)+1),u=!0)}}if(u&&s++,"children"in r&&i.children)for(let g of i.children)o(g)}for(let r of e)o(r);return{consumerMap:t,totalEligible:n,boundCount:s}}async function qt(){let e=await figma.variables.getLocalVariableCollectionsAsync(),t=figma.currentPage.findAll(()=>!0),{consumerMap:n,totalEligible:s,boundCount:o}=zi(t),r=[],i=0,a=[],c={};for(let p of e){let u=[];for(let g of p.modes)c[g.name]||(c[g.name]={total:0,withValue:0});for(let g of p.variableIds){let f=await figma.variables.getVariableByIdAsync(g);if(!f)continue;i++;let m=n.get(f.id)||0;m===0&&a.push(f.name);let h={};for(let[C,S]of Object.entries(f.valuesByMode))h[C]=Wi(S);for(let C of p.modes){c[C.name].total++;let S=f.valuesByMode[C.modeId];S!=null&&c[C.name].withValue++}u.push({id:f.id,name:f.name,resolvedType:f.resolvedType,description:f.description,valuesByMode:h,scopes:f.scopes,consumers:m})}r.push({id:p.id,name:p.name,modes:p.modes.map(g=>({modeId:g.modeId,name:g.name})),variables:u})}let d=s>0?Math.round(o/s*100):0,l={};for(let[p,u]of Object.entries(c))l[p]=u.total>0?Math.round(u.withValue/u.total*100):100;return{collections:r,totalVariables:i,unusedVariables:a,adoptionRate:d,modesCoverage:l}}function Wi(e){if(e==null||typeof e=="boolean"||typeof e=="string"||typeof e=="number")return e;if(typeof e=="object"&&e.type==="VARIABLE_ALIAS")return{type:"VARIABLE_ALIAS",id:e.id};if(typeof e=="object"&&"r"in e){let t=e,n=s=>{let o=Math.round(s*255).toString(16);return o.length===1?"0"+o:o};return t.a!==void 0&&t.a<1?`rgba(${Math.round(t.r*255)}, ${Math.round(t.g*255)}, ${Math.round(t.b*255)}, ${t.a.toFixed(2)})`:`#${n(t.r)}${n(t.g)}${n(t.b)}`}return e}function ro(e){let t=JSON.parse(e),n=[];return io(t,[],void 0,n),n}function io(e,t,n,s){let o=typeof e.$type=="string"?e.$type:n;if("$value"in e){let r=typeof e.$type=="string"?e.$type:n||"unknown",i=typeof e.$description=="string"?e.$description:void 0;s.push({path:[...t],name:t.join("."),$type:r,$value:e.$value,$description:i});return}for(let[r,i]of Object.entries(e))r.startsWith("$")||typeof i=="object"&&i!==null&&!Array.isArray(i)&&io(i,[...t,r],o,s)}function ao(e,t,n){let s=new Map;for(let C of t)s.set(Te(C.name),C);let o=[];for(let C of e.collections)for(let S of C.variables)o.push(S);let r=[],i=[],a=new Set,c=new Set;for(let C of o){let S=Te(C.name);if(c.add(S),s.has(S)){a.add(S),r.push({token:C.name,nodeCount:C.consumers,usage:C.consumers>0?"correct":"overridden"});continue}let N=Hi(S,t);N&&N.distance<=3?(a.add(Te(N.token.name)),r.push({token:C.name,nodeCount:C.consumers,usage:"correct"})):i.push({value:C.name,nodeCount:C.consumers,nearestToken:N?N.token.name:"(none)",distance:N?N.distance:1/0})}let d=[];for(let C of t)a.has(Te(C.name))||d.push(C.name);let l=i.filter(C=>C.nodeCount>0).map(C=>C.value),p=t.length,u=r.filter(C=>C.nodeCount>0).length,g=i.filter(C=>C.nodeCount>0).length,f=u+g,m=f>0?Math.round(u/f*100):p>0?0:100;return{adoptionScore:Math.round(e.adoptionRate*.5+m*.5),matched:r,unmatched:i,orphanTokens:d,missingFromSystem:l,summary:{totalTokenDefs:p,usedInDesign:u,hardCodedValues:g,compliance:m}}}function Te(e){return e.replace(/\//g,".").replace(/\s+/g,"-").toLowerCase().trim()}function Hi(e,t){if(t.length===0)return null;let n=null;for(let s of t){let o=Ki(e,Te(s.name));if((!n||o<n.distance)&&(n={token:s,distance:o}),o===0)return n}return n}function Ki(e,t,n=10){if(e===t)return 0;if(e.length===0)return Math.min(t.length,n+1);if(t.length===0)return Math.min(e.length,n+1);let s=new Array(t.length+1),o=new Array(t.length+1);for(let r=0;r<=t.length;r++)s[r]=r;for(let r=1;r<=e.length;r++){o[0]=r;let i=o[0];for(let a=1;a<=t.length;a++){let c=e[r-1]===t[a-1]?0:1;o[a]=Math.min(s[a]+1,o[a-1]+1,s[a-1]+c),o[a]<i&&(i=o[a])}if(i>n)return n+1;[s,o]=[o,s]}return s[t.length]}async function co(e){let n=(await figma.variables.getLocalVariableCollectionsAsync()).find(i=>i.id===e);if(!n)throw new Error(`Collection not found: ${e}`);let s=n.modes.map(i=>({modeId:i.modeId,modeName:i.name})),o=[],r=[];for(let i of n.variableIds){let a=await figma.variables.getVariableByIdAsync(i);if(!a)continue;let c={},d=[],l=!1,p,u=!1;for(let g of n.modes){let f=a.valuesByMode[g.modeId];f==null?d.push(g.name):(c[g.name]=Jt(f),u?JSON.stringify(Jt(f))!==p&&(l=!0):(p=JSON.stringify(Jt(f)),u=!0))}(l||d.length>0)&&o.push({variableName:a.name,type:a.resolvedType,values:c}),d.length>0&&r.push({variableName:a.name,missingModes:d})}return{collection:n.name,modes:s,variableDiffs:o,missingValues:r}}function Jt(e){if(e==null||typeof e=="boolean"||typeof e=="string"||typeof e=="number")return e;if(typeof e=="object"&&e.type==="VARIABLE_ALIAS")return{type:"VARIABLE_ALIAS",id:e.id};if(typeof e=="object"&&"r"in e){let t=e,n=s=>{let o=Math.round(s*255).toString(16);return o.length===1?"0"+o:o};return t.a!==void 0&&t.a<1?`rgba(${Math.round(t.r*255)}, ${Math.round(t.g*255)}, ${Math.round(t.b*255)}, ${t.a.toFixed(2)})`:`#${n(t.r)}${n(t.g)}${n(t.b)}`}return e}be();J();var ce=null,le=null,Pe=new Set,Xe=!1;function lo(e){if(!ce||!ce.enabled)return;for(let n of e.documentChanges)(n.type==="PROPERTY_CHANGE"||n.type==="CREATE"||n.type==="STYLE_PROPERTY_CHANGE")&&"id"in n&&typeof n.id=="string"&&Pe.add(n.id);if(Pe.size===0)return;le!==null&&clearTimeout(le);let t=ce.debounceMs||500;le=setTimeout(()=>{ji()},t)}async function ji(){if(!ce)return;let e=Array.from(Pe);Pe.clear(),le=null;let t=[],n=[];for(let s of e)try{let o=await figma.getNodeByIdAsync(s);o&&"type"in o&&o.type!=="PAGE"&&o.type!=="DOCUMENT"&&(t.push(o),n.push(s))}catch(o){}if(t.length!==0)try{let s=ae(t,ce.settings);k("realtime-lint-update",{errors:s.errors,changedNodeIds:n})}catch(s){console.error("Realtime lint error:",s)}}function uo(e){ce={enabled:e.enabled,debounceMs:e.debounceMs||500,settings:e.settings||j},Xe||(figma.on("documentchange",lo),Xe=!0)}function po(){ce=null,Xe&&(figma.off("documentchange",lo),Xe=!1),le!==null&&(clearTimeout(le),le=null),Pe.clear()}var mo=/^(Frame|Group|Rectangle|Ellipse|Vector|Line|Polygon|Star|Component|Instance|Boolean)\s*\d+$/i;function fo(e,t){let n=e.errors,o=n.filter(m=>m.errorType==="fill"||m.errorType==="stroke"||m.errorType==="effect"||m.errorType==="text").length,i=n.filter(m=>m.message.toLowerCase().includes("detach")).length,a=t?t.hardCodedValues:0,c=new Set;for(let m of n)mo.test(m.nodeName)&&c.add(m.nodeId);let d=n.filter(m=>m.errorType==="accessibility"&&mo.test(m.nodeName));for(let m of d)c.add(m.nodeId);let l=c.size,p=n.filter(m=>m.errorType==="autoLayout").length,u=n.filter(m=>m.errorType==="spacing").length,g=100;return g-=o*2,g-=i*5,g-=a*1,g-=l*.5,g-=p*1,g-=u*.5,{overall:Math.max(0,Math.min(100,Math.round(g))),components:{orphanedStyles:{count:o,score:Math.max(0,Math.round(100-o*2))},detachedInstances:{count:i,score:Math.max(0,Math.round(100-i*5))},hardcodedValues:{count:a,score:Math.max(0,Math.round(100-a*1))},namingViolations:{count:l,score:Math.max(0,Math.round(100-l*.5))},missingAutoLayout:{count:p,score:Math.max(0,Math.round(100-p*1))},inconsistentSpacing:{count:u,score:Math.max(0,Math.round(100-u*.5))}}}}be();var Z=null,H="claude-sonnet-4-5-20250929",_="anthropic";function go(e,t=_){let n=(e==null?void 0:e.trim())||"";switch(t){case"anthropic":return n.startsWith("sk-ant-")&&n.length>=40;case"openai":return n.startsWith("sk-")&&n.length>=20;case"google":return n.startsWith("AIza")&&n.length>=35;default:return!1}}var yo=null,ho=null,Q=new Ds({enableCaching:!0,enableMCPIntegration:!0,mcpServerUrl:"https://design-systems-mcp.southleft-llc.workers.dev/mcp"});async function bo(e){let{type:t,data:n}=e,s=t==="save-api-key"?`${t} [redacted]`:t;console.log("Received message:",s);try{switch(t){case"check-api-key":await qi();break;case"save-api-key":await Ji(n.apiKey,n.model,n.provider);break;case"update-model":await Xi(n.model);break;case"analyze":await Yi();break;case"analyze-enhanced":await vo(n);break;case"clear-api-key":await Zi();break;case"chat-message":await ea(n);break;case"chat-clear-history":await ta();break;case"select-node":await na(n);break;case"preview-fix":await Ma(n);break;case"apply-token-fix":await Oa(n);break;case"apply-naming-fix":await Fa(n);break;case"apply-batch-fix":await Da(n);break;case"update-description":await Va(n);break;case"add-component-property":await _a(n);break;case"run-design-lint":Re(n);break;case"lint-ignore-node":ca(n);break;case"lint-ignore-error":la(n);break;case"lint-ignore-all-of-type":da(n);break;case"lint-clear-ignored":ua();break;case"lint-select-node":pa(n);break;case"lint-select-all-with-value":ma(n);break;case"lint-save-settings":fa(n);break;case"lint-load-settings":ga();break;case"lint-save-team-config":ya(n);break;case"lint-load-team-config":ha();break;case"jump-to-node":ba(n);break;case"fix-spacing":Sa(n);break;case"fix-spacing-to-nearest":Na(n);break;case"fix-all-spacing":Ca(n);break;case"apply-style-fix":await Ia(n);break;case"rename-layer-fix":xa(n);break;case"fix-radius-to-nearest":wa(n);break;case"batch-fix-v2":await Aa(n);break;case"rescan-lint":So();break;case"export-screenshot":await ka(n);break;case"analyze-flow":await Ea();break;case"analyze-page":await Ta();break;case"save-baseline":Pa(n);break;case"load-baseline":La(n);break;case"compare-baseline":Ra(n);break;case"delete-baseline":$a(n);break;case"collect-variables":await Ba();break;case"check-dtcg-compliance":await Ua(n);break;case"compare-modes":await Ga(n);break;case"enable-realtime-lint":za(n);break;case"disable-realtime-lint":Wa();break;case"calculate-design-debt":Ha(n);break;default:console.warn("Unknown message type:",t)}}catch(o){console.error("Error handling message:",o);let r=o instanceof Error?o.message:"Unknown error occurred";k("analysis-error",{error:r})}}async function qi(){try{await lt();let e=await dt();if(_=e.providerId,H=e.modelId,Z){k("api-key-status",{hasKey:!0,provider:_,model:H});return}e.apiKey&&go(e.apiKey,e.providerId)?(Z=e.apiKey,k("api-key-status",{hasKey:!0,provider:_,model:H})):k("api-key-status",{hasKey:!1,provider:_,model:H})}catch(e){console.error("Error checking API key:",e),k("api-key-status",{hasKey:!1,provider:"anthropic"})}}async function Ji(e,t,n){try{let s=n||_;if(!go(e,s)){let r=re(s);throw new Error(`Invalid API key format for ${r.name}. Expected format: ${r.keyPlaceholder}`)}_=s,Z=e,t&&(H=t),await ut(s,H,e),console.log(`${s} API key and model saved successfully`);let o=re(s);k("api-key-saved",{success:!0,provider:s}),figma.notify(`${o.name} API key saved successfully`,{timeout:2e3})}catch(s){console.error("Error saving API key:",s);let o=s instanceof Error?s.message:"Unknown error occurred";k("api-key-saved",{success:!1,error:o}),figma.notify(`Failed to save API key: ${o}`,{error:!0})}}async function Xi(e){try{H=e,await ut(_,e),console.log("Model updated to:",e),figma.notify(`Model updated to ${e}`,{timeout:2e3})}catch(t){console.error("Error updating model:",t),figma.notify("Failed to update model",{error:!0})}}async function vo(e){var t,n;try{if(!Z){let c=re(_).name;throw new Error(`API key not found. Please save your ${c} API key first.`)}let s=figma.currentPage.selection;if(s.length===0)throw new Error("No component selected. Please select a Figma component to analyze.");if(e.batchMode&&s.length>1){await Qi(s,e);return}let o=s[0];if(o.type==="INSTANCE"){let c=o;try{let d=await c.getMainComponentAsync();if(d)figma.notify("Analyzing main component instead of instance...",{timeout:2e3}),o=d;else throw new Error("This instance has no main component. Please select a component directly.")}catch(d){throw console.error("Error accessing main component:",d),new Error("Could not access main component. Please select a component directly.")}}if(o.type==="COMPONENT"&&((t=o.parent)==null?void 0:t.type)==="COMPONENT_SET"){let d=o.parent;figma.notify("Analyzing parent component set to include all variants...",{timeout:2e3}),o=d}if(!Se(o)){let c=new Set(["COMPONENT_SET","COMPONENT","INSTANCE"]),d=null,l=null,p=o.parent;for(;p&&"type"in p;){let g=p;if(c.has(g.type)&&!d){d=g;break}!l&&Se(g)&&(l=g),p=p.parent}let u=d||l;u&&(figma.notify(`Analyzing parent ${u.type.toLowerCase()} "${u.name}"...`,{timeout:2e3}),o=u)}if(o.type==="INSTANCE"){let c=o;try{let d=await c.getMainComponentAsync();d&&(figma.notify("Analyzing main component instead of instance...",{timeout:2e3}),o=d)}catch(d){}}if(o.type==="COMPONENT"&&((n=o.parent)==null?void 0:n.type)==="COMPONENT_SET"){let c=o.parent;figma.notify("Analyzing parent component set to include all variants...",{timeout:2e3}),o=c}if(!Se(o))throw new Error("Please select a Frame, Component, Component Set, or Instance to analyze");await Q.loadDesignSystemsKnowledge();let r=await Lt(o),i=R({enableMCPEnhancement:!0,batchMode:e.batchMode||!1,enableAudit:e.enableAudit!==!1,includeTokenAnalysis:e.includeTokenAnalysis!==!1},e);figma.notify("Performing enhanced analysis with design systems knowledge...",{timeout:3e3});let a=await Os(r,Z,H,i,_);yo=a.metadata,ho=o,k("enhanced-analysis-result",K(R({},a),{analyzedNodeId:o.id})),figma.notify("Enhanced analysis complete! Check the results panel.",{timeout:3e3})}catch(s){console.error("Error during enhanced analysis:",s);let o=s instanceof Error?s.message:"Unknown error occurred";figma.notify(`Analysis failed: ${o}`,{error:!0}),k("analysis-error",{error:o})}}async function Yi(){await vo({batchMode:!1})}async function Qi(e,t){let n=[];await Q.loadDesignSystemsKnowledge();for(let r of e)if(Se(r))try{let i=await Lt(r),a=await ue(r),c=[...a.colors,...a.spacing,...a.typography,...a.effects,...a.borders],d=Q.generateComponentHash(i,c,V),l=Q.getCachedAnalysis(d);if(l){console.log(`\u2705 Using cached analysis for ${r.name}`),n.push({node:r.name,success:!0,data:l.result.metadata,cached:!0});continue}let p=Q.createDeterministicPrompt(i),u=await ie(_,Z,{prompt:p,model:H,maxTokens:2048,temperature:.1}),g=pe(u.content),f=Fe(g),m=await Rt(f,i,{batchMode:!0});Q.validateAnalysisConsistency(m,i)||(m=Q.applyConsistencyCorrections(m,i)),Q.cacheAnalysis(d,m),n.push({node:r.name,success:!0,data:m.metadata,cached:!1})}catch(i){n.push({node:r.name,success:!1,error:i instanceof Error?i.message:"Analysis failed"})}let s=n.filter(r=>r.success&&r.cached).length,o=n.filter(r=>r.success&&!r.cached).length;k("batch-analysis-result",{results:n}),figma.notify(`Batch analysis complete: ${o} analyzed, ${s} from cache`,{timeout:3e3})}async function Zi(){try{Z=null,await on(_),await figma.clientStorage.setAsync("claude-api-key","");let e=re(_).name;k("api-key-cleared",{success:!0}),figma.notify(`${e} API key cleared`,{timeout:2e3})}catch(e){console.error("Error clearing API key:",e)}}async function ea(e){try{if(console.log("Processing chat message:",e.message),!Z){let i=re(_).name;throw new Error(`API key not found. Please save your ${i} API key first.`)}k("chat-response-loading",{isLoading:!0});let t=ia(),n=await ra(e.message),s=aa(e.message,n,e.history,t),r={message:(await ie(_,Z,{prompt:s,model:H,maxTokens:2048,temperature:.7})).content,sources:n.sources||[]};k("chat-response",{response:r})}catch(t){console.error("Error handling chat message:",t);let n=t instanceof Error?t.message:"Unknown error occurred";k("chat-error",{error:n})}}async function ta(){try{k("chat-history-cleared",{success:!0}),figma.notify("Chat history cleared",{timeout:2e3})}catch(e){console.error("Error clearing chat history:",e)}}async function na(e){try{console.log("\u{1F3AF} Attempting to select node:",e.nodeId);let t=await figma.getNodeByIdAsync(e.nodeId);if(!t){console.warn("\u26A0\uFE0F Node not found:",e.nodeId),figma.notify("Node not found - it may have been deleted or moved",{error:!0});return}if(!sa(t)){console.warn("\u26A0\uFE0F Node is not on current page:",e.nodeId),figma.notify("Node is on a different page",{error:!0});return}figma.currentPage.selection=[t],figma.viewport.scrollAndZoomIntoView([t]),console.log("\u2705 Successfully selected and zoomed to node:",t.name),figma.notify(`Selected "${t.name}"`,{timeout:2e3})}catch(t){console.error("Error selecting node:",t);let n=t instanceof Error?t.message:"Unknown error occurred";figma.notify(`Failed to select node: ${n}`,{error:!0})}}function sa(e){try{let t=e,n=50,s=0;for(;t&&t.parent&&s<n;)if(t=t.parent,s++,t===figma.currentPage)return!0;if(t===figma.currentPage||e.parent===figma.currentPage)return!0;let o=figma.currentPage;return e.type==="COMPONENT"||e.type==="COMPONENT_SET"?oa(o,e.id):!1}catch(t){return console.warn("Error checking node page:",t),!1}}function oa(e,t){try{return e.findAll().some(s=>s.id===t)}catch(n){return!1}}async function ra(e){var t;try{console.log("\u{1F50D} Querying MCP for chat:",e);let n=((t=Q.config)==null?void 0:t.mcpServerUrl)||"https://design-systems-mcp.southleft-llc.workers.dev/mcp",s=[Xt(n,e,{category:"general",limit:3}),e.toLowerCase().includes("component")?Xt(n,e,{category:"components",limit:2}):Promise.resolve({results:[]}),e.toLowerCase().includes("token")||e.toLowerCase().includes("design token")?Xt(n,e,{category:"tokens",limit:2}):Promise.resolve({results:[]})],o=await Promise.allSettled(s),r=[];return o.forEach(i=>{i.status==="fulfilled"&&i.value.results&&r.push(...i.value.results)}),console.log(`\u2705 Found ${r.length} relevant sources for chat query`),{sources:r.slice(0,5)}}catch(n){return console.warn("\u26A0\uFE0F MCP query failed for chat:",n),{sources:[]}}}async function Xt(e,t,n={}){let s={jsonrpc:"2.0",id:Math.floor(Math.random()*1e3)+100,method:"tools/call",params:{name:"search_design_knowledge",arguments:R({query:t,limit:n.limit||5},n.category&&{category:n.category})}},o=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!o.ok)throw new Error(`MCP search failed: ${o.status}`);let r=await o.json();return r.result&&r.result.content?{results:r.result.content.map(i=>({title:i.title||"Design System Knowledge",content:i.content||i.description||"",category:i.category||"general"}))}:{results:[]}}function ia(){try{let e=yo,t=ho;if(!e&&!t)return null;let n={hasCurrentComponent:!0,timestamp:Date.now()};if(t){n.component={name:t.name,type:t.type,id:t.id};let s=figma.currentPage.selection;s.length>0&&(n.selection={count:s.length,types:s.map(o=>o.type),names:s.map(o=>o.name)})}return e&&(n.analysis={component:e.component,description:e.description,props:e.props||[],states:e.states||[],accessibility:e.accessibility,audit:e.audit,mcpReadiness:e.mcpReadiness}),n}catch(e){return console.warn("Failed to get component context:",e),null}}function aa(e,t,n,s){let o="";n.length>0&&(o=` + `}loadFallbackKnowledge(){this.designSystemsKnowledge={version:"1.0.0-fallback",components:{button:"Button components require comprehensive state management",avatar:"Avatar components should support size variants and interactive states",card:"Card components need consistent spacing and content hierarchy",badge:"Badge components should use semantic colors for status indication",input:"Input components require comprehensive accessibility and validation",generic:"Generic components should follow basic design system principles"},tokens:"Use semantic token naming: semantic-color-primary, spacing-md-16px, text-size-lg-18px",accessibility:"Ensure WCAG 2.1 AA compliance with proper ARIA labels and keyboard support",scoring:this.getFallbackScoringCriteria(),lastUpdated:Date.now()}}isValidScore(t){return typeof t=="number"&&t>=0&&t<=100}validateComponentFamilyConsistency(t,n){let s=t.metadata;switch(n){case"button":return this.validateButtonComponent(s);case"avatar":return this.validateAvatarComponent(s);case"input":return this.validateInputComponent(s);default:return!0}}validateButtonComponent(t){var s;return((s=t.states)==null?void 0:s.some(o=>["hover","focus","active","disabled"].includes(o.toLowerCase())))||!1}validateAvatarComponent(t){var o,r,i;let n=((r=(o=t.variants)==null?void 0:o.size)==null?void 0:r.length)>0,s=(i=t.props)==null?void 0:i.some(a=>a.name.toLowerCase().includes("size"));return n||s||!1}validateInputComponent(t){var s;return((s=t.states)==null?void 0:s.some(o=>["focus","error","disabled","filled"].includes(o.toLowerCase())))||!1}validateTokenRecommendations(t){var s;return((s=t.colors)==null?void 0:s.some(o=>o.name.includes("semantic-")||o.name.includes("primary")||o.name.includes("secondary")))!==!1}applyComponentFamilyCorrections(t,n){var o,r,i;let s=R({},t);switch(n){case"button":(o=s.states)!=null&&o.includes("hover")||(s.states=[...s.states||[],"hover","focus","active","disabled"]);break;case"avatar":!((r=s.variants)!=null&&r.size)&&!((i=s.props)!=null&&i.some(a=>a.name.includes("size")))&&(s.variants=K(R({},s.variants),{size:["small","medium","large"]}));break}return s}applyTokenConsistencyCorrections(t){return t&&R({},t)}ensureConsistentScoring(t,n){return K(R({},t),{score:t.score||0})}},Ds=$t;J();function Mt(e,t,n){let s=f=>f<=.04045?f/12.92:Math.pow((f+.055)/1.055,2.4),o=s(e),r=s(t),i=s(n),a=(o*.4124564+r*.3575761+i*.1804375)/.95047,c=o*.2126729+r*.7151522+i*.072175,d=(o*.0193339+r*.119192+i*.9503041)/1.08883,l=f=>f>.008856?Math.cbrt(f):7.787*f+16/116,p=l(a),u=l(c),g=l(d);return{L:116*u-16,a:500*(p-u),b:200*(u-g)}}function Vs(e,t){let{L:n,a:s,b:o}=e,{L:r,a:i,b:a}=t,c=1,d=1,l=1,p=Math.sqrt(s*s+o*o),u=Math.sqrt(i*i+a*a),g=(p+u)/2,f=Math.pow(g,7),m=.5*(1-Math.sqrt(f/(f+6103515625))),h=s*(1+m),C=i*(1+m),k=Math.sqrt(h*h+o*o),N=Math.sqrt(C*C+a*a),y=Math.atan2(o,h)*180/Math.PI,b=Math.atan2(a,C)*180/Math.PI,I=(y%360+360)%360,w=(b%360+360)%360,P=r-n,O=N-k,M;k*N===0?M=0:Math.abs(w-I)<=180?M=w-I:w-I>180?M=w-I-360:M=w-I+360;let z=2*Math.sqrt(k*N)*Math.sin(M*Math.PI/360),x=(n+r)/2,$=(k+N)/2,v;k*N===0?v=I+w:Math.abs(I-w)<=180?v=(I+w)/2:I+w<360?v=(I+w+360)/2:v=(I+w-360)/2;let T=1-.17*Math.cos((v-30)*Math.PI/180)+.24*Math.cos(2*v*Math.PI/180)+.32*Math.cos((3*v+6)*Math.PI/180)-.2*Math.cos((4*v-63)*Math.PI/180),L=1+.015*Math.pow(x-50,2)/Math.sqrt(20+Math.pow(x-50,2)),A=1+.045*$,ee=1+.015*$*T,Y=Math.pow($,7),ve=-2*Math.sqrt(Y/(Y+6103515625))*Math.sin(60*Math.exp(-Math.pow((v-275)/25,2))*Math.PI/180);return Math.sqrt(Math.pow(P/(c*L),2)+Math.pow(O/(d*A),2)+Math.pow(z/(l*ee),2)+ve*(O/(d*A))*(z/(l*ee)))}async function Fi(e,t,n,s=0){try{if(!(t in e))return{success:!1,message:`Node does not support ${t}`,error:`Property ${t} not found on node type ${e.type}`};let o=await figma.variables.getVariableByIdAsync(n);if(!o)return{success:!1,message:"Variable not found",error:`Could not find variable with ID: ${n}`};if(o.resolvedType!=="COLOR")return{success:!1,message:"Variable is not a color type",error:`Variable ${o.name} is of type ${o.resolvedType}, expected COLOR`};let i=[...e[t]];if(s>=i.length)return{success:!1,message:"Paint index out of range",error:`Paint index ${s} does not exist. Node has ${i.length} ${t}.`};let a=i[s];if(a.type!=="SOLID")return{success:!1,message:"Can only bind to solid paints",error:`Paint at index ${s} is of type ${a.type}, expected SOLID`};let c=figma.variables.setBoundVariableForPaint(a,"color",o);return i[s]=c,t==="fills"?e.fills=i:e.strokes=i,{success:!0,message:`Successfully bound ${o.name} to ${t}[${s}]`,appliedFix:{nodeId:e.id,nodeName:e.name,propertyPath:`${t}[${s}]`,beforeValue:a.type==="SOLID"&&a.color?F(a.color.r,a.color.g,a.color.b):"unknown",afterValue:o.name,tokenId:n,tokenName:o.name,fixType:"color"}}}catch(o){return{success:!1,message:"Failed to bind color token",error:o instanceof Error?o.message:String(o)}}}async function _s(e,t,n){try{if(!(t in e))return{success:!1,message:`Node does not support ${t}`,error:`Property ${t} not found on node type ${e.type}`};let s=await figma.variables.getVariableByIdAsync(n);if(!s)return{success:!1,message:"Variable not found",error:`Could not find variable with ID: ${n}`};if(s.resolvedType!=="FLOAT")return{success:!1,message:"Variable is not a number type",error:`Variable ${s.name} is of type ${s.resolvedType}, expected FLOAT`};let o=e[t];return e.setBoundVariable(t,s),{success:!0,message:`Successfully bound ${s.name} to ${t}`,appliedFix:{nodeId:e.id,nodeName:e.name,propertyPath:t,beforeValue:typeof o=="number"?`${o}px`:String(o),afterValue:s.name,tokenId:n,tokenName:s.name,fixType:t.includes("Radius")?"border":"spacing"}}}catch(s){return{success:!1,message:"Failed to bind spacing token",error:s instanceof Error?s.message:String(s)}}}async function Ot(e,t=0){try{let n=Vi(e);if(!n)return[];let s=[],o=await figma.variables.getLocalVariablesAsync("COLOR"),r=await figma.variables.getLocalVariableCollectionsAsync(),i=new Map;for(let a of r)i.set(a.id,a);for(let a of o){let c=i.get(a.variableCollectionId);if(!c)continue;let d=c.modes[0].modeId,l=a.valuesByMode[d];if(!l||typeof l!="object"||!("r"in l))continue;let p=l,u=_i(n,p);u>=1-t&&s.push({variableId:a.id,variableName:a.name,collectionName:c.name,value:F(p.r,p.g,p.b),matchScore:u,type:"color"})}return s.sort((a,c)=>c.matchScore-a.matchScore)}catch(n){return console.error("Error finding matching color variable:",n),[]}}async function Di(e,t=0){try{let n=[],s=await figma.variables.getLocalVariablesAsync("FLOAT"),o=await figma.variables.getLocalVariableCollectionsAsync(),r=new Map;for(let i of o)r.set(i.id,i);for(let i of s){let a=r.get(i.variableCollectionId);if(!a)continue;let c=a.modes[0].modeId,d=i.valuesByMode[c];if(typeof d!="number")continue;let l=Math.abs(d-e);if(l<=t){let p=l===0?1:1-l/(t||1);n.push({variableId:i.id,variableName:i.name,collectionName:a.name,value:`${d}px`,matchScore:p,type:"number"})}}return n.sort((i,a)=>a.matchScore-i.matchScore)}catch(n){return console.error("Error finding matching spacing variable:",n),[]}}async function Ft(e,t,n=2){let s=await Di(e,n);if(s.length===0)return s;let r={strokeWeight:["stroke","border-width","border/width","borderwidth"],cornerRadius:["radius","corner","round","border-radius"],topLeftRadius:["radius","corner","round"],topRightRadius:["radius","corner","round"],bottomLeftRadius:["radius","corner","round"],bottomRightRadius:["radius","corner","round"],paddingTop:["padding","spacing","space"],paddingRight:["padding","spacing","space"],paddingBottom:["padding","spacing","space"],paddingLeft:["padding","spacing","space"],itemSpacing:["gap","spacing","space"],counterAxisSpacing:["gap","spacing","space"]}[t]||[];return r.length===0?s:s.map(a=>{let c=a.variableName.toLowerCase(),d=r.some(l=>c.includes(l));return K(R({},a),{matchScore:d?Math.min(a.matchScore+.3,1):a.matchScore})}).sort((a,c)=>c.matchScore-a.matchScore)}async function Dt(e,t,n){let s=t.match(/^(fills|strokes)\[(\d+)\]$/);if(!s)return{success:!1,message:"Invalid property path",error:`Expected format: fills[n] or strokes[n], got: ${t}`};let[,o,r]=s,i=parseInt(r,10);return Fi(e,o,n,i)}async function Vt(e,t,n){if(!["paddingTop","paddingRight","paddingBottom","paddingLeft","itemSpacing","counterAxisSpacing","cornerRadius","topLeftRadius","topRightRadius","bottomLeftRadius","bottomRightRadius","strokeWeight"].includes(t))return{success:!1,message:"Invalid property path",error:`Property ${t} is not a valid spacing property`};if(t==="cornerRadius"){let o=["topLeftRadius","topRightRadius","bottomLeftRadius","bottomRightRadius"],r=[];for(let i of o){let a=await _s(e,i,n);if(r.push(a),!a.success)return{success:!1,message:`Failed to bind ${i}`,error:a.error}}return{success:!0,message:"Successfully bound variable to all 4 corner radii",appliedFix:r[0].appliedFix?K(R({},r[0].appliedFix),{propertyPath:"cornerRadius"}):void 0}}return _s(e,t,n)}async function _t(e,t,n){try{let s=await figma.variables.getVariableByIdAsync(n);if(!s)return null;let o,r,i=t.match(/^(fills|strokes)\[(\d+)\]$/);if(i){o="color";let[,d,l]=i,p=parseInt(l,10);if(!(d in e))return null;let g=e[d];if(p>=g.length)return null;let f=g[p];f.type==="SOLID"&&f.color?r=F(f.color.r,f.color.g,f.color.b):r=f.type}else{if(!(t in e))return null;let d=e[t];r=typeof d=="number"?`${d}px`:String(d),o=t.includes("Radius")?"border":"spacing"}let a=s.name,c=await figma.variables.getVariableCollectionByIdAsync(s.variableCollectionId);if(c){let d=c.modes[0].modeId,l=s.valuesByMode[d];if(typeof l=="number")a=`${s.name} (${l}px)`;else if(l&&typeof l=="object"&&"r"in l){let p=l;a=`${s.name} (${F(p.r,p.g,p.b)})`}}return{nodeId:e.id,nodeName:e.name,propertyPath:t,beforeValue:r,afterValue:a,tokenId:n,tokenName:s.name,fixType:o}}catch(s){return console.error("Error generating fix preview:",s),null}}function Vi(e){let t=e.replace(/^#/,""),n=t;if(t.length===3&&(n=t[0]+t[0]+t[1]+t[1]+t[2]+t[2]),n.length!==6)return null;let s=parseInt(n.substring(0,2),16),o=parseInt(n.substring(2,4),16),r=parseInt(n.substring(4,6),16);return isNaN(s)||isNaN(o)||isNaN(r)?null:{r:s/255,g:o/255,b:r/255}}function _i(e,t){let n=Mt(e.r,e.g,e.b),s=Mt(t.r,t.g,t.b),o=Vs(n,s);return o<3?1:o>=10?0:1-(o-3)/7}async function ze(e,t=1024){let n=Math.max(1,Math.min(t,Math.round(e.width))),s=await e.exportAsync({format:"PNG",constraint:{type:"WIDTH",value:n}});return Bi(s)}function Bi(e){let t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",n="",s=e.length;for(let o=0;o<s;o+=3){let r=e[o],i=o+1<s?e[o+1]:0,a=o+2<s?e[o+2]:0;n+=t[r>>2],n+=t[(r&3)<<4|i>>4],n+=o+1<s?t[(i&15)<<2|a>>6]:"=",n+=o+2<s?t[a&63]:"="}return n}var Ui=/button|btn|cta|link|tab|nav|menu|input|checkbox|toggle|switch|radio|select|dropdown|slider/i;function Bs(e){if(Ui.test(e.name))return!0;if("children"in e){for(let t of e.children)if(Bs(t))return!0}return!1}function Us(e,t,n){var s,o;if("reactions"in e){let r=e.reactions;if(r&&r.length>0)for(let i of r){let a=i.actions||(i.action?[i.action]:[]);for(let c of a)c.type==="NODE"&&c.destinationId&&n.push({sourceFrameId:t,sourceNodeId:e.id,sourceNodeName:e.name,destinationFrameId:c.destinationId,trigger:((s=i.trigger)==null?void 0:s.type)||"UNKNOWN",navigation:c.navigation||"NAVIGATE",hasTransition:!!c.transition}),c.type==="BACK"&&n.push({sourceFrameId:t,sourceNodeId:e.id,sourceNodeName:e.name,destinationFrameId:"__BACK__",trigger:((o=i.trigger)==null?void 0:o.type)||"UNKNOWN",navigation:"BACK",hasTransition:!!c.transition})}}if("children"in e)for(let r of e.children)Us(r,t,n)}function Gs(e){var z,x,$;let t=e||figma.currentPage,n=t.children.filter(v=>v.type==="FRAME"||v.type==="COMPONENT"),s=new Set((t.flowStartingPoints||[]).map(v=>v.nodeId)),o=n.map(v=>({id:v.id,name:v.name,pageId:t.id,pageName:t.name,width:v.width,height:v.height,isFlowStartingPoint:s.has(v.id),childCount:"children"in v?v.children.length:0,hasInteractiveElements:Bs(v)})),r=new Set(o.map(v=>v.id)),i=[];for(let v of n)Us(v,v.id,i);let a=i.filter(v=>v.destinationFrameId==="__BACK__"||r.has(v.destinationFrameId)),c=o.filter(v=>v.isFlowStartingPoint).map(v=>v.id),d=new Map,l=new Map;for(let v of r)d.set(v,new Set),l.set(v,new Set);for(let v of a)v.destinationFrameId!=="__BACK__"&&((z=d.get(v.sourceFrameId))==null||z.add(v.destinationFrameId),(x=l.get(v.destinationFrameId))==null||x.add(v.sourceFrameId));let p=new Set;for(let v of a)v.destinationFrameId==="__BACK__"&&p.add(v.sourceFrameId);let u=o.filter(v=>{var T;return(((T=d.get(v.id))==null?void 0:T.size)||0)===0&&!p.has(v.id)}).map(v=>v.id),g=o.filter(v=>{var T;return(((T=l.get(v.id))==null?void 0:T.size)||0)===0&&!s.has(v.id)}).map(v=>v.id),f=new Set,m=[...c];if(m.length===0)for(let v of o)((($=l.get(v.id))==null?void 0:$.size)||0)===0&&m.push(v.id);for(;m.length>0;){let v=m.shift();if(f.has(v))continue;f.add(v);let T=d.get(v);if(T)for(let L of T)f.has(L)||m.push(L)}let h=o.filter(v=>!f.has(v.id)).map(v=>v.id),C=[],k=new Set,N=new Set,y=[];function b(v){if(N.has(v)){let L=y.indexOf(v);L!==-1&&C.push(y.slice(L));return}if(k.has(v))return;k.add(v),N.add(v),y.push(v);let T=d.get(v);if(T)for(let L of T)b(L);y.pop(),N.delete(v)}for(let v of r)b(v);let I=o.map(v=>{var T;return((T=d.get(v.id))==null?void 0:T.size)||0}),w=I.length>0?I.reduce((v,T)=>v+T,0)/I.length:0,P=0,O=c.map(v=>({id:v,depth:0})),M=new Set;for(;O.length>0;){let{id:v,depth:T}=O.shift();if(M.has(v))continue;M.add(v),T>P&&(P=T);let L=d.get(v);if(L)for(let A of L)M.has(A)||O.push({id:A,depth:T+1})}return{frames:o,edges:a,entryPoints:c,deadEnds:u,orphans:g,unreachable:h,loops:C,stats:{totalFrames:o.length,totalEdges:a.length,totalEntryPoints:c.length,maxDepth:P,avgBranching:Math.round(w*100)/100}}}function zs(e){let t=[],n=new Map(e.frames.map(i=>[i.id,i.name])),s=i=>i.map(a=>`"${n.get(a)||a}"`).join(", ");for(let i of e.deadEnds){let a=n.get(i)||"";/success|confirm|done|complete|thank|receipt|summary/i.test(a)||t.push({type:"dead-end",severity:"warning",frameIds:[i],message:`${s([i])} has no outgoing connections \u2014 user gets stuck here.`})}e.orphans.length>0&&t.push({type:"orphan",severity:"warning",frameIds:e.orphans,message:`${s(e.orphans)} ${e.orphans.length===1?"has":"have"} no incoming connections \u2014 unreachable by navigation.`});let o=e.unreachable.filter(i=>!e.orphans.includes(i));o.length>0&&t.push({type:"unreachable",severity:"critical",frameIds:o,message:`${s(o)} ${o.length===1?"is":"are"} not reachable from any flow entry point.`});for(let i of e.loops){let a=new Set(i);i.some(d=>e.edges.filter(p=>p.sourceFrameId===d).some(p=>!a.has(p.destinationFrameId)))||t.push({type:"loop",severity:"warning",frameIds:i,message:`Circular flow without exit: ${s(i)}. User cannot leave this loop.`})}e.stats.maxDepth>3&&t.push({type:"deep-navigation",severity:"info",frameIds:[],message:`Navigation depth is ${e.stats.maxDepth} levels. Consider flattening to \u22643 levels for better UX (3-click rule).`});let r=e.frames.filter(i=>{if(i.isFlowStartingPoint)return!1;let a=e.edges.some(d=>d.sourceFrameId===i.id&&(d.navigation==="BACK"||d.navigation==="CLOSE"));return e.edges.some(d=>d.destinationFrameId===i.id)&&!a});return r.length>0&&t.push({type:"missing-back",severity:"info",frameIds:r.map(i=>i.id),message:`${r.length} frame${r.length===1?"":"s"} missing back/close navigation: ${s(r.map(i=>i.id))}.`}),t}J();function Hs(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if("fills"in e){let o=e.fills;if(o!==figma.mixed&&Array.isArray(o))for(let r of o)r.type==="SOLID"&&r.visible!==!1&&t.colors.add(F(r.color.r,r.color.g,r.color.b))}if("strokes"in e){let o=e.strokes;if(Array.isArray(o))for(let r of o)r.type==="SOLID"&&r.visible!==!1&&t.colors.add(F(r.color.r,r.color.g,r.color.b))}if(e.type==="TEXT"){let o=e;o.fontName!==figma.mixed&&t.fontFamilies.add(o.fontName.family),o.fontSize!==figma.mixed&&t.fontSizes.add(o.fontSize)}if("layoutMode"in e&&e.layoutMode!=="NONE"){let o=e;typeof o.itemSpacing=="number"&&t.spacingValues.add(o.itemSpacing),typeof o.paddingTop=="number"&&t.spacingValues.add(o.paddingTop),typeof o.paddingBottom=="number"&&t.spacingValues.add(o.paddingBottom),typeof o.paddingLeft=="number"&&t.spacingValues.add(o.paddingLeft),typeof o.paddingRight=="number"&&t.spacingValues.add(o.paddingRight)}if(e.type==="INSTANCE"){let o=e.mainComponent;o&&t.componentNames.add(o.name)}if("children"in e)for(let o of e.children)Hs(o,t,n,s)}}function Ws(e,t){let n=new Set;for(let s of e)t.has(s)||n.add(s);return n}function Ks(e,t={}){var g,f;let n=(g=t.skipLocked)!=null?g:!0,s=(f=t.skipHidden)!=null?f:!0,o=[];if(e.length<2)return o;let r=e.map(({frame:m,node:h})=>{let C={frameId:m.id,frameName:m.name,colors:new Set,fontFamilies:new Set,fontSizes:new Set,spacingValues:new Set,componentNames:new Set};return Hs(h,C,n,s),C}),i=new Map;for(let m of r)for(let h of m.colors)i.set(h,(i.get(h)||0)+1);let a=r.length*.5,c=new Set;for(let[m,h]of i)h>=a&&c.add(m);for(let m of r){let h=Ws(m.colors,c);h.size>3&&o.push({type:"dead-end",severity:"warning",frameIds:[m.frameId],message:`"${m.frameName}" uses ${h.size} colors not found in other screens (${[...h].slice(0,3).join(", ")}${h.size>3?"...":""}). Check for color inconsistency.`})}let d=new Set;for(let m of r)for(let h of m.fontFamilies)d.add(h);if(d.size>3){let m=[...d].join(", ");o.push({type:"dead-end",severity:"warning",frameIds:r.map(h=>h.frameId),message:`${d.size} different font families across flow: ${m}. Flows should use 1-2 font families for consistency.`})}for(let m of r){let h=new Set;for(let k of r)if(k.frameId!==m.frameId)for(let N of k.fontFamilies)h.add(N);let C=Ws(m.fontFamilies,h);C.size>0&&r.length>2&&o.push({type:"dead-end",severity:"info",frameIds:[m.frameId],message:`"${m.frameName}" uses font${C.size>1?"s":""} not seen elsewhere: ${[...C].join(", ")}.`})}let l=new Set;for(let m of r)for(let h of m.fontSizes)l.add(h);l.size>10&&o.push({type:"dead-end",severity:"info",frameIds:r.map(m=>m.frameId),message:`${l.size} unique font sizes across the flow. Consider using a type scale with fewer sizes for consistency.`});let p=new Set;for(let m of r)for(let h of m.spacingValues)h>0&&p.add(h);let u=[...p].filter(m=>m%4!==0&&m!==2);return u.length>3&&o.push({type:"dead-end",severity:"info",frameIds:r.map(m=>m.frameId),message:`${u.length} non-standard spacing values across flow (${u.slice(0,4).join(", ")}px). Consider aligning to a 4px/8px grid.`}),o}var He=Ro(js()),qs=8e4,Js="baseline::";function Ke(e){return`${Js}${e}::meta`}function Ut(e,t){return`${Js}${e}::chunk_${t}`}function Xs(e){let t=JSON.stringify(e),n=(0,He.compressToUTF16)(t),s=[];for(let r=0;r<n.length;r+=qs)s.push(n.slice(r,r+qs));let o=e.nodeId;eo(o),figma.root.setPluginData(Ke(o),JSON.stringify({chunkCount:s.length,timestamp:e.timestamp,nodeName:e.nodeName,overall:e.overall}));for(let r=0;r<s.length;r++)figma.root.setPluginData(Ut(o,r),s[r])}function Ys(e){let t=figma.root.getPluginData(Ke(e));if(!t)return null;let n;try{n=JSON.parse(t)}catch(i){return null}let s=[];for(let i=0;i<n.chunkCount;i++){let a=figma.root.getPluginData(Ut(e,i));if(!a)return null;s.push(a)}let o=s.join(""),r=(0,He.decompressFromUTF16)(o);if(!r)return null;try{return JSON.parse(r)}catch(i){return null}}function Qs(e){eo(e),figma.root.setPluginData(Ke(e),"")}function Zs(e){let t=figma.root.getPluginData(Ke(e));if(!t)return null;try{return JSON.parse(t)}catch(n){return null}}function eo(e){for(let t=0;t<100;t++){let n=Ut(e,t);if(!figma.root.getPluginData(n))break;figma.root.setPluginData(n,"")}}function je(e){return`${e.errorType}::${e.nodeId}::${e.message}`}function to(e,t){var l,p,u,g;let n=Date.now(),s=new Set([...Object.keys(e.categories),...Object.keys(t.categories)]),o=[];for(let f of s){let m=(p=(l=e.categories[f])==null?void 0:l.score)!=null?p:100,h=(g=(u=t.categories[f])==null?void 0:u.score)!=null?g:100;o.push({category:f,oldScore:m,newScore:h,delta:h-m})}o.sort((f,m)=>Math.abs(m.delta)-Math.abs(f.delta));let r=new Set(e.errors.map(je)),i=new Set(t.errors.map(je)),a=[],c=[],d=[];for(let f of t.errors){let m=je(f);r.has(m)?d.push({errorType:f.errorType,severity:f.severity,nodeId:f.nodeId,message:f.message}):a.push({errorType:f.errorType,severity:f.severity,nodeId:f.nodeId,message:f.message})}for(let f of e.errors){let m=je(f);i.has(m)||c.push({errorType:f.errorType,severity:f.severity,nodeId:f.nodeId,message:f.message})}return{baselineTimestamp:e.timestamp,currentTimestamp:n,scoreDelta:{overall:t.overall-e.overall,oldOverall:e.overall,newOverall:t.overall,categories:o},newIssues:a,fixedIssues:c,remainingIssues:d,summary:{totalNew:a.length,totalFixed:c.length,totalRemaining:d.length,oldTotal:e.errors.length,newTotal:t.errors.length}}}ft();var Gi=["itemSpacing","paddingTop","paddingBottom","paddingLeft","paddingRight","counterAxisSpacing"];function Gt(e,t,n){let s=figma.getNodeById(e);if(!s)return{success:!1,nodeId:e,nodeName:"",property:t,oldValue:0,newValue:n,error:"Node not found"};if(s.type!=="FRAME"&&s.type!=="COMPONENT"&&s.type!=="INSTANCE")return{success:!1,nodeId:e,nodeName:s.name,property:t,oldValue:0,newValue:n,error:"Node is not a frame"};let o=s;if(o.layoutMode==="NONE")return{success:!1,nodeId:e,nodeName:s.name,property:t,oldValue:0,newValue:n,error:"Frame has no auto-layout"};try{let r=o[t];return o[t]=n,{success:!0,nodeId:e,nodeName:s.name,property:t,oldValue:r,newValue:n}}catch(r){return{success:!1,nodeId:e,nodeName:s.name,property:t,oldValue:0,newValue:n,error:r instanceof Error?r.message:String(r)}}}function Ae(e,t){let n=figma.getNodeById(e);if(!n||n.type!=="FRAME"&&n.type!=="COMPONENT"&&n.type!=="INSTANCE")return{success:!1,nodeId:e,nodeName:(n==null?void 0:n.name)||"",property:t,oldValue:0,newValue:0,error:"Invalid node"};let o=n[t];if(typeof o!="number")return{success:!1,nodeId:e,nodeName:n.name,property:t,oldValue:0,newValue:0,error:"Property is not a number"};if(mt.includes(o))return{success:!0,nodeId:e,nodeName:n.name,property:t,oldValue:o,newValue:o};let r=Ve(o);if(r.length===0)return{success:!1,nodeId:e,nodeName:n.name,property:t,oldValue:o,newValue:o,error:"No suggestion found"};let i=r.reduce((a,c)=>Math.abs(a-o)<=Math.abs(c-o)?a:c);return Gt(e,t,i)}function no(e){let t=figma.getNodeById(e);if(!t||t.type!=="FRAME"&&t.type!=="COMPONENT"&&t.type!=="INSTANCE")return[];let n=t;if(n.layoutMode==="NONE")return[];let s=[];for(let o of Gi){if(!(o in n))continue;let r=n[o];if(typeof r!="number"||mt.includes(r))continue;let i=Ae(e,o);s.push(i)}return s}function Ee(e,t){return t.length===0?e:t.reduce((n,s)=>Math.abs(s-e)<Math.abs(n-e)?s:n)}function qe(e,t){let n=figma.getNodeById(e);if(!n)return{success:!1,nodeId:e,nodeName:"",oldValue:"",newValue:"",error:"Node not found"};if(!("cornerRadius"in n))return{success:!1,nodeId:e,nodeName:n.name,oldValue:"",newValue:"",error:"Node has no corner radius"};let s=n;try{let o=s.cornerRadius;if(o===figma.mixed){let r=s.topLeftRadius,i=s.topRightRadius,a=s.bottomLeftRadius,c=s.bottomRightRadius,d=`${r}/${i}/${c}/${a}`;s.topLeftRadius=Ee(r,t),s.topRightRadius=Ee(i,t),s.bottomLeftRadius=Ee(a,t),s.bottomRightRadius=Ee(c,t);let l=`${s.topLeftRadius}/${s.topRightRadius}/${s.bottomRightRadius}/${s.bottomLeftRadius}`;return{success:!0,nodeId:e,nodeName:n.name,oldValue:d,newValue:l}}else{let r=`${o}`,i=Ee(o,t);return s.cornerRadius=i,{success:!0,nodeId:e,nodeName:n.name,oldValue:r,newValue:`${i}`}}}catch(o){return{success:!1,nodeId:e,nodeName:n.name,oldValue:"",newValue:"",error:o instanceof Error?o.message:String(o)}}}function Je(e,t){let n=figma.getNodeById(e);if(!n||n.type==="DOCUMENT"||n.type==="PAGE")return{success:!1,nodeId:e,oldName:"",newName:t,error:"Node not found"};try{let s=n.name;return n.name=t,{success:!0,nodeId:e,oldName:s,newName:t}}catch(s){return{success:!1,nodeId:e,oldName:n.name,newName:t,error:s instanceof Error?s.message:String(s)}}}jt();async function oo(e){let t=0,n=0,s=[];for(let o=0;o<e.length;o++){let r=e[o];try{let i=await zi(r);s.push(R({index:o},i)),i.success?t++:n++}catch(i){n++,s.push({index:o,type:r.type,success:!1,nodeId:String(r.params.nodeId||""),nodeName:"",message:"Unexpected error",error:i instanceof Error?i.message:String(i)})}}return{total:e.length,applied:t,failed:n,results:s}}async function zi(e){let{type:t,params:n}=e;switch(t){case"applyStyle":{let s=n.styleType,o=n.nodeId,r=n.styleKey,i;switch(s){case"fill":i=await zt(o,r);break;case"stroke":i=await Wt(o,r);break;case"text":i=await Ht(o,r);break;case"effect":i=await Kt(o,r);break;default:return{type:t,success:!1,nodeId:o,nodeName:"",message:`Unknown style type: ${s}`}}return{type:t,success:i.success,nodeId:i.nodeId,nodeName:i.nodeName,message:i.success?`Applied ${i.property}: ${i.newValue}`:i.error||"Failed",oldValue:i.oldValue,newValue:i.newValue,error:i.error}}case"fixSpacing":{let s=n.nodeId,o=n.property,r=n.value,i=Gt(s,o,r);return{type:t,success:i.success,nodeId:i.nodeId,nodeName:i.nodeName,message:i.success?`${i.property}: ${i.oldValue}px \u2192 ${i.newValue}px`:i.error||"Failed",oldValue:`${i.oldValue}px`,newValue:`${i.newValue}px`,error:i.error}}case"fixSpacingToNearest":{let s=n.nodeId,o=n.property,r=Ae(s,o);return{type:t,success:r.success,nodeId:r.nodeId,nodeName:r.nodeName,message:r.success?`${r.property}: ${r.oldValue}px \u2192 ${r.newValue}px`:r.error||"Failed",oldValue:`${r.oldValue}px`,newValue:`${r.newValue}px`,error:r.error}}case"fixRadiusToNearest":{let s=n.nodeId,o=n.allowedRadii,r=qe(s,o);return{type:t,success:r.success,nodeId:r.nodeId,nodeName:r.nodeName,message:r.success?`radius: ${r.oldValue}px \u2192 ${r.newValue}px`:r.error||"Failed",oldValue:`${r.oldValue}px`,newValue:`${r.newValue}px`,error:r.error}}case"renameLayer":{let s=n.nodeId,o=n.newName,r=Je(s,o);return{type:t,success:r.success,nodeId:r.nodeId,nodeName:r.newName,message:r.success?`Renamed "${r.oldName}" \u2192 "${r.newName}"`:r.error||"Failed",oldValue:r.oldName,newValue:r.newName,error:r.error}}default:return{type:t,success:!1,nodeId:"",nodeName:"",message:`Unknown fix type: ${t}`}}}function Wi(e){let t=new Map,n=0,s=0;function o(r){let i=r,a=Array.isArray(i.fills)&&i.fills.length>0,c=Array.isArray(i.strokes)&&i.strokes.length>0,d=Array.isArray(i.effects)&&i.effects.length>0,l=r.type==="TEXT",p=r.type==="FRAME"||r.type==="COMPONENT"||r.type==="INSTANCE";(a||c||d||l||p)&&n++;let u=!1;if("boundVariables"in r&&i.boundVariables){let g=i.boundVariables;for(let f of Object.keys(g)){let m=g[f];if(Array.isArray(m))for(let h of m)h&&h.id&&(t.set(h.id,(t.get(h.id)||0)+1),u=!0);else m&&m.id&&(t.set(m.id,(t.get(m.id)||0)+1),u=!0)}}if(Array.isArray(i.fills)){for(let g of i.fills)if(g.boundVariables)for(let f of Object.keys(g.boundVariables)){let m=g.boundVariables[f];m&&m.id&&(t.set(m.id,(t.get(m.id)||0)+1),u=!0)}}if(u&&s++,"children"in r&&i.children)for(let g of i.children)o(g)}for(let r of e)o(r);return{consumerMap:t,totalEligible:n,boundCount:s}}async function qt(){let e=await figma.variables.getLocalVariableCollectionsAsync(),t=figma.currentPage.findAll(()=>!0),{consumerMap:n,totalEligible:s,boundCount:o}=Wi(t),r=[],i=0,a=[],c={};for(let p of e){let u=[];for(let g of p.modes)c[g.name]||(c[g.name]={total:0,withValue:0});for(let g of p.variableIds){let f=await figma.variables.getVariableByIdAsync(g);if(!f)continue;i++;let m=n.get(f.id)||0;m===0&&a.push(f.name);let h={};for(let[C,k]of Object.entries(f.valuesByMode))h[C]=Hi(k);for(let C of p.modes){c[C.name].total++;let k=f.valuesByMode[C.modeId];k!=null&&c[C.name].withValue++}u.push({id:f.id,name:f.name,resolvedType:f.resolvedType,description:f.description,valuesByMode:h,scopes:f.scopes,consumers:m})}r.push({id:p.id,name:p.name,modes:p.modes.map(g=>({modeId:g.modeId,name:g.name})),variables:u})}let d=s>0?Math.round(o/s*100):0,l={};for(let[p,u]of Object.entries(c))l[p]=u.total>0?Math.round(u.withValue/u.total*100):100;return{collections:r,totalVariables:i,unusedVariables:a,adoptionRate:d,modesCoverage:l}}function Hi(e){if(e==null||typeof e=="boolean"||typeof e=="string"||typeof e=="number")return e;if(typeof e=="object"&&e.type==="VARIABLE_ALIAS")return{type:"VARIABLE_ALIAS",id:e.id};if(typeof e=="object"&&"r"in e){let t=e,n=s=>{let o=Math.round(s*255).toString(16);return o.length===1?"0"+o:o};return t.a!==void 0&&t.a<1?`rgba(${Math.round(t.r*255)}, ${Math.round(t.g*255)}, ${Math.round(t.b*255)}, ${t.a.toFixed(2)})`:`#${n(t.r)}${n(t.g)}${n(t.b)}`}return e}function ro(e){let t=JSON.parse(e),n=[];return io(t,[],void 0,n),n}function io(e,t,n,s){let o=typeof e.$type=="string"?e.$type:n;if("$value"in e){let r=typeof e.$type=="string"?e.$type:n||"unknown",i=typeof e.$description=="string"?e.$description:void 0;s.push({path:[...t],name:t.join("."),$type:r,$value:e.$value,$description:i});return}for(let[r,i]of Object.entries(e))r.startsWith("$")||typeof i=="object"&&i!==null&&!Array.isArray(i)&&io(i,[...t,r],o,s)}function ao(e,t,n){let s=new Map;for(let C of t)s.set(Te(C.name),C);let o=[];for(let C of e.collections)for(let k of C.variables)o.push(k);let r=[],i=[],a=new Set,c=new Set;for(let C of o){let k=Te(C.name);if(c.add(k),s.has(k)){a.add(k),r.push({token:C.name,nodeCount:C.consumers,usage:C.consumers>0?"correct":"overridden"});continue}let N=Ki(k,t);N&&N.distance<=3?(a.add(Te(N.token.name)),r.push({token:C.name,nodeCount:C.consumers,usage:"correct"})):i.push({value:C.name,nodeCount:C.consumers,nearestToken:N?N.token.name:"(none)",distance:N?N.distance:1/0})}let d=[];for(let C of t)a.has(Te(C.name))||d.push(C.name);let l=i.filter(C=>C.nodeCount>0).map(C=>C.value),p=t.length,u=r.filter(C=>C.nodeCount>0).length,g=i.filter(C=>C.nodeCount>0).length,f=u+g,m=f>0?Math.round(u/f*100):p>0?0:100;return{adoptionScore:Math.round(e.adoptionRate*.5+m*.5),matched:r,unmatched:i,orphanTokens:d,missingFromSystem:l,summary:{totalTokenDefs:p,usedInDesign:u,hardCodedValues:g,compliance:m}}}function Te(e){return e.replace(/\//g,".").replace(/\s+/g,"-").toLowerCase().trim()}function Ki(e,t){if(t.length===0)return null;let n=null;for(let s of t){let o=ji(e,Te(s.name));if((!n||o<n.distance)&&(n={token:s,distance:o}),o===0)return n}return n}function ji(e,t,n=10){if(e===t)return 0;if(e.length===0)return Math.min(t.length,n+1);if(t.length===0)return Math.min(e.length,n+1);let s=new Array(t.length+1),o=new Array(t.length+1);for(let r=0;r<=t.length;r++)s[r]=r;for(let r=1;r<=e.length;r++){o[0]=r;let i=o[0];for(let a=1;a<=t.length;a++){let c=e[r-1]===t[a-1]?0:1;o[a]=Math.min(s[a]+1,o[a-1]+1,s[a-1]+c),o[a]<i&&(i=o[a])}if(i>n)return n+1;[s,o]=[o,s]}return s[t.length]}async function co(e){let n=(await figma.variables.getLocalVariableCollectionsAsync()).find(i=>i.id===e);if(!n)throw new Error(`Collection not found: ${e}`);let s=n.modes.map(i=>({modeId:i.modeId,modeName:i.name})),o=[],r=[];for(let i of n.variableIds){let a=await figma.variables.getVariableByIdAsync(i);if(!a)continue;let c={},d=[],l=!1,p,u=!1;for(let g of n.modes){let f=a.valuesByMode[g.modeId];f==null?d.push(g.name):(c[g.name]=Jt(f),u?JSON.stringify(Jt(f))!==p&&(l=!0):(p=JSON.stringify(Jt(f)),u=!0))}(l||d.length>0)&&o.push({variableName:a.name,type:a.resolvedType,values:c}),d.length>0&&r.push({variableName:a.name,missingModes:d})}return{collection:n.name,modes:s,variableDiffs:o,missingValues:r}}function Jt(e){if(e==null||typeof e=="boolean"||typeof e=="string"||typeof e=="number")return e;if(typeof e=="object"&&e.type==="VARIABLE_ALIAS")return{type:"VARIABLE_ALIAS",id:e.id};if(typeof e=="object"&&"r"in e){let t=e,n=s=>{let o=Math.round(s*255).toString(16);return o.length===1?"0"+o:o};return t.a!==void 0&&t.a<1?`rgba(${Math.round(t.r*255)}, ${Math.round(t.g*255)}, ${Math.round(t.b*255)}, ${t.a.toFixed(2)})`:`#${n(t.r)}${n(t.g)}${n(t.b)}`}return e}be();J();var ce=null,le=null,Pe=new Set,Xe=!1;function lo(e){if(!ce||!ce.enabled)return;for(let n of e.documentChanges)(n.type==="PROPERTY_CHANGE"||n.type==="CREATE"||n.type==="STYLE_PROPERTY_CHANGE")&&"id"in n&&typeof n.id=="string"&&Pe.add(n.id);if(Pe.size===0)return;le!==null&&clearTimeout(le);let t=ce.debounceMs||500;le=setTimeout(()=>{qi()},t)}async function qi(){if(!ce)return;let e=Array.from(Pe);Pe.clear(),le=null;let t=[],n=[];for(let s of e)try{let o=await figma.getNodeByIdAsync(s);o&&"type"in o&&o.type!=="PAGE"&&o.type!=="DOCUMENT"&&(t.push(o),n.push(s))}catch(o){}if(t.length!==0)try{let s=ne(t,ce.settings);S("realtime-lint-update",{errors:s.errors,changedNodeIds:n})}catch(s){console.error("Realtime lint error:",s)}}function uo(e){ce={enabled:e.enabled,debounceMs:e.debounceMs||500,settings:e.settings||j},Xe||(figma.on("documentchange",lo),Xe=!0)}function po(){ce=null,Xe&&(figma.off("documentchange",lo),Xe=!1),le!==null&&(clearTimeout(le),le=null),Pe.clear()}var mo=/^(Frame|Group|Rectangle|Ellipse|Vector|Line|Polygon|Star|Component|Instance|Boolean)\s*\d+$/i;function fo(e,t){let n=e.errors,o=n.filter(m=>m.errorType==="fill"||m.errorType==="stroke"||m.errorType==="effect"||m.errorType==="text").length,i=n.filter(m=>m.message.toLowerCase().includes("detach")).length,a=t?t.hardCodedValues:0,c=new Set;for(let m of n)mo.test(m.nodeName)&&c.add(m.nodeId);let d=n.filter(m=>m.errorType==="accessibility"&&mo.test(m.nodeName));for(let m of d)c.add(m.nodeId);let l=c.size,p=n.filter(m=>m.errorType==="autoLayout").length,u=n.filter(m=>m.errorType==="spacing").length,g=100;return g-=o*2,g-=i*5,g-=a*1,g-=l*.5,g-=p*1,g-=u*.5,{overall:Math.max(0,Math.min(100,Math.round(g))),components:{orphanedStyles:{count:o,score:Math.max(0,Math.round(100-o*2))},detachedInstances:{count:i,score:Math.max(0,Math.round(100-i*5))},hardcodedValues:{count:a,score:Math.max(0,Math.round(100-a*1))},namingViolations:{count:l,score:Math.max(0,Math.round(100-l*.5))},missingAutoLayout:{count:p,score:Math.max(0,Math.round(100-p*1))},inconsistentSpacing:{count:u,score:Math.max(0,Math.round(100-u*.5))}}}}be();var Z=null,H="claude-sonnet-4-5-20250929",_="anthropic";function go(e,t=_){let n=(e==null?void 0:e.trim())||"";switch(t){case"anthropic":return n.startsWith("sk-ant-")&&n.length>=40;case"openai":return n.startsWith("sk-")&&n.length>=20;case"google":return n.startsWith("AIza")&&n.length>=35;default:return!1}}var yo=null,ho=null,Q=new Ds({enableCaching:!0,enableMCPIntegration:!0,mcpServerUrl:"https://design-systems-mcp.southleft-llc.workers.dev/mcp"});async function bo(e){let{type:t,data:n}=e,s=t==="save-api-key"?`${t} [redacted]`:t;console.log("Received message:",s);try{switch(t){case"check-api-key":await Ji();break;case"save-api-key":await Xi(n.apiKey,n.model,n.provider);break;case"update-model":await Yi(n.model);break;case"analyze":await Qi();break;case"analyze-enhanced":await vo(n);break;case"clear-api-key":await ea();break;case"chat-message":await ta(n);break;case"chat-clear-history":await na();break;case"select-node":await sa(n);break;case"preview-fix":await Oa(n);break;case"apply-token-fix":await Fa(n);break;case"apply-naming-fix":await Da(n);break;case"apply-batch-fix":await Va(n);break;case"update-description":await _a(n);break;case"add-component-property":await Ba(n);break;case"run-design-lint":Re(n);break;case"lint-ignore-node":la(n);break;case"lint-ignore-error":da(n);break;case"lint-ignore-all-of-type":ua(n);break;case"lint-clear-ignored":pa();break;case"lint-select-node":ma(n);break;case"lint-select-all-with-value":fa(n);break;case"lint-save-settings":ga(n);break;case"lint-load-settings":ya();break;case"lint-save-team-config":ha(n);break;case"lint-load-team-config":ba();break;case"jump-to-node":va(n);break;case"fix-spacing":ka(n);break;case"fix-spacing-to-nearest":wa(n);break;case"fix-all-spacing":Ia(n);break;case"apply-style-fix":await xa(n);break;case"rename-layer-fix":Aa(n);break;case"fix-radius-to-nearest":Ca(n);break;case"batch-fix-v2":await Ea(n);break;case"rescan-lint":So();break;case"export-screenshot":await Na(n);break;case"analyze-flow":await Ta();break;case"analyze-page":await Pa();break;case"save-baseline":La(n);break;case"load-baseline":Ra(n);break;case"compare-baseline":$a(n);break;case"delete-baseline":Ma(n);break;case"collect-variables":await Ua();break;case"check-dtcg-compliance":await Ga(n);break;case"compare-modes":await za(n);break;case"enable-realtime-lint":Wa(n);break;case"disable-realtime-lint":Ha();break;case"calculate-design-debt":Ka(n);break;default:console.warn("Unknown message type:",t)}}catch(o){console.error("Error handling message:",o);let r=o instanceof Error?o.message:"Unknown error occurred";S("analysis-error",{error:r})}}async function Ji(){try{await lt();let e=await dt();if(_=e.providerId,H=e.modelId,Z){S("api-key-status",{hasKey:!0,provider:_,model:H});return}e.apiKey&&go(e.apiKey,e.providerId)?(Z=e.apiKey,S("api-key-status",{hasKey:!0,provider:_,model:H})):S("api-key-status",{hasKey:!1,provider:_,model:H})}catch(e){console.error("Error checking API key:",e),S("api-key-status",{hasKey:!1,provider:"anthropic"})}}async function Xi(e,t,n){try{let s=n||_;if(!go(e,s)){let r=ie(s);throw new Error(`Invalid API key format for ${r.name}. Expected format: ${r.keyPlaceholder}`)}_=s,Z=e,t&&(H=t),await ut(s,H,e),console.log(`${s} API key and model saved successfully`);let o=ie(s);S("api-key-saved",{success:!0,provider:s}),figma.notify(`${o.name} API key saved successfully`,{timeout:2e3})}catch(s){console.error("Error saving API key:",s);let o=s instanceof Error?s.message:"Unknown error occurred";S("api-key-saved",{success:!1,error:o}),figma.notify(`Failed to save API key: ${o}`,{error:!0})}}async function Yi(e){try{H=e,await ut(_,e),console.log("Model updated to:",e),figma.notify(`Model updated to ${e}`,{timeout:2e3})}catch(t){console.error("Error updating model:",t),figma.notify("Failed to update model",{error:!0})}}async function vo(e){var t,n;try{if(!Z){let c=ie(_).name;throw new Error(`API key not found. Please save your ${c} API key first.`)}let s=figma.currentPage.selection;if(s.length===0)throw new Error("No component selected. Please select a Figma component to analyze.");if(e.batchMode&&s.length>1){await Zi(s,e);return}let o=s[0];if(o.type==="INSTANCE"){let c=o;try{let d=await c.getMainComponentAsync();if(d)figma.notify("Analyzing main component instead of instance...",{timeout:2e3}),o=d;else throw new Error("This instance has no main component. Please select a component directly.")}catch(d){throw console.error("Error accessing main component:",d),new Error("Could not access main component. Please select a component directly.")}}if(o.type==="COMPONENT"&&((t=o.parent)==null?void 0:t.type)==="COMPONENT_SET"){let d=o.parent;figma.notify("Analyzing parent component set to include all variants...",{timeout:2e3}),o=d}if(!Se(o)){let c=new Set(["COMPONENT_SET","COMPONENT","INSTANCE"]),d=null,l=null,p=o.parent;for(;p&&"type"in p;){let g=p;if(c.has(g.type)&&!d){d=g;break}!l&&Se(g)&&(l=g),p=p.parent}let u=d||l;u&&(figma.notify(`Analyzing parent ${u.type.toLowerCase()} "${u.name}"...`,{timeout:2e3}),o=u)}if(o.type==="INSTANCE"){let c=o;try{let d=await c.getMainComponentAsync();d&&(figma.notify("Analyzing main component instead of instance...",{timeout:2e3}),o=d)}catch(d){}}if(o.type==="COMPONENT"&&((n=o.parent)==null?void 0:n.type)==="COMPONENT_SET"){let c=o.parent;figma.notify("Analyzing parent component set to include all variants...",{timeout:2e3}),o=c}if(!Se(o))throw new Error("Please select a Frame, Component, Component Set, or Instance to analyze");await Q.loadDesignSystemsKnowledge();let r=await Lt(o),i=R({enableMCPEnhancement:!0,batchMode:e.batchMode||!1,enableAudit:e.enableAudit!==!1,includeTokenAnalysis:e.includeTokenAnalysis!==!1},e);figma.notify("Performing enhanced analysis with design systems knowledge...",{timeout:3e3});let a=await Os(r,Z,H,i,_);yo=a.metadata,ho=o,S("enhanced-analysis-result",K(R({},a),{analyzedNodeId:o.id})),figma.notify("Enhanced analysis complete! Check the results panel.",{timeout:3e3})}catch(s){console.error("Error during enhanced analysis:",s);let o=s instanceof Error?s.message:"Unknown error occurred";figma.notify(`Analysis failed: ${o}`,{error:!0}),S("analysis-error",{error:o})}}async function Qi(){await vo({batchMode:!1})}async function Zi(e,t){let n=[];await Q.loadDesignSystemsKnowledge();for(let r of e)if(Se(r))try{let i=await Lt(r),a=await ue(r),c=[...a.colors,...a.spacing,...a.typography,...a.effects,...a.borders],d=Q.generateComponentHash(i,c,D),l=Q.getCachedAnalysis(d);if(l){console.log(`\u2705 Using cached analysis for ${r.name}`),n.push({node:r.name,success:!0,data:l.result.metadata,cached:!0});continue}let p=Q.createDeterministicPrompt(i),u=await ae(_,Z,{prompt:p,model:H,maxTokens:2048,temperature:.1}),g=pe(u.content),f=Fe(g),m=await Rt(f,i,{batchMode:!0});Q.validateAnalysisConsistency(m,i)||(m=Q.applyConsistencyCorrections(m,i)),Q.cacheAnalysis(d,m),n.push({node:r.name,success:!0,data:m.metadata,cached:!1})}catch(i){n.push({node:r.name,success:!1,error:i instanceof Error?i.message:"Analysis failed"})}let s=n.filter(r=>r.success&&r.cached).length,o=n.filter(r=>r.success&&!r.cached).length;S("batch-analysis-result",{results:n}),figma.notify(`Batch analysis complete: ${o} analyzed, ${s} from cache`,{timeout:3e3})}async function ea(){try{Z=null,await on(_),await figma.clientStorage.setAsync("claude-api-key","");let e=ie(_).name;S("api-key-cleared",{success:!0}),figma.notify(`${e} API key cleared`,{timeout:2e3})}catch(e){console.error("Error clearing API key:",e)}}async function ta(e){try{if(console.log("Processing chat message:",e.message),!Z){let i=ie(_).name;throw new Error(`API key not found. Please save your ${i} API key first.`)}S("chat-response-loading",{isLoading:!0});let t=aa(),n=await ia(e.message),s=ca(e.message,n,e.history,t),r={message:(await ae(_,Z,{prompt:s,model:H,maxTokens:2048,temperature:.7})).content,sources:n.sources||[]};S("chat-response",{response:r})}catch(t){console.error("Error handling chat message:",t);let n=t instanceof Error?t.message:"Unknown error occurred";S("chat-error",{error:n})}}async function na(){try{S("chat-history-cleared",{success:!0}),figma.notify("Chat history cleared",{timeout:2e3})}catch(e){console.error("Error clearing chat history:",e)}}async function sa(e){try{console.log("\u{1F3AF} Attempting to select node:",e.nodeId);let t=await figma.getNodeByIdAsync(e.nodeId);if(!t){console.warn("\u26A0\uFE0F Node not found:",e.nodeId),figma.notify("Node not found - it may have been deleted or moved",{error:!0});return}if(!oa(t)){console.warn("\u26A0\uFE0F Node is not on current page:",e.nodeId),figma.notify("Node is on a different page",{error:!0});return}figma.currentPage.selection=[t],figma.viewport.scrollAndZoomIntoView([t]),console.log("\u2705 Successfully selected and zoomed to node:",t.name),figma.notify(`Selected "${t.name}"`,{timeout:2e3})}catch(t){console.error("Error selecting node:",t);let n=t instanceof Error?t.message:"Unknown error occurred";figma.notify(`Failed to select node: ${n}`,{error:!0})}}function oa(e){try{let t=e,n=50,s=0;for(;t&&t.parent&&s<n;)if(t=t.parent,s++,t===figma.currentPage)return!0;if(t===figma.currentPage||e.parent===figma.currentPage)return!0;let o=figma.currentPage;return e.type==="COMPONENT"||e.type==="COMPONENT_SET"?ra(o,e.id):!1}catch(t){return console.warn("Error checking node page:",t),!1}}function ra(e,t){try{return e.findAll().some(s=>s.id===t)}catch(n){return!1}}async function ia(e){var t;try{console.log("\u{1F50D} Querying MCP for chat:",e);let n=((t=Q.config)==null?void 0:t.mcpServerUrl)||"https://design-systems-mcp.southleft-llc.workers.dev/mcp",s=[Xt(n,e,{category:"general",limit:3}),e.toLowerCase().includes("component")?Xt(n,e,{category:"components",limit:2}):Promise.resolve({results:[]}),e.toLowerCase().includes("token")||e.toLowerCase().includes("design token")?Xt(n,e,{category:"tokens",limit:2}):Promise.resolve({results:[]})],o=await Promise.allSettled(s),r=[];return o.forEach(i=>{i.status==="fulfilled"&&i.value.results&&r.push(...i.value.results)}),console.log(`\u2705 Found ${r.length} relevant sources for chat query`),{sources:r.slice(0,5)}}catch(n){return console.warn("\u26A0\uFE0F MCP query failed for chat:",n),{sources:[]}}}async function Xt(e,t,n={}){let s={jsonrpc:"2.0",id:Math.floor(Math.random()*1e3)+100,method:"tools/call",params:{name:"search_design_knowledge",arguments:R({query:t,limit:n.limit||5},n.category&&{category:n.category})}},o=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!o.ok)throw new Error(`MCP search failed: ${o.status}`);let r=await o.json();return r.result&&r.result.content?{results:r.result.content.map(i=>({title:i.title||"Design System Knowledge",content:i.content||i.description||"",category:i.category||"general"}))}:{results:[]}}function aa(){try{let e=yo,t=ho;if(!e&&!t)return null;let n={hasCurrentComponent:!0,timestamp:Date.now()};if(t){n.component={name:t.name,type:t.type,id:t.id};let s=figma.currentPage.selection;s.length>0&&(n.selection={count:s.length,types:s.map(o=>o.type),names:s.map(o=>o.name)})}return e&&(n.analysis={component:e.component,description:e.description,props:e.props||[],states:e.states||[],accessibility:e.accessibility,audit:e.audit,mcpReadiness:e.mcpReadiness}),n}catch(e){return console.warn("Failed to get component context:",e),null}}function ca(e,t,n,s){let o="";n.length>0&&(o=` **Previous Conversation:** `,n.slice(-6).forEach(d=>{o+=`${d.role==="user"?"User":"Assistant"}: ${d.content} `}),o+=` @@ -435,4 +435,4 @@ ${r}${i}**Instructions:** ${a?"Since you have context about their current component, prioritize advice that directly applies to what they're working on.":"If the user wants component-specific advice, suggest they select and analyze a component in Figma first."} -Respond naturally and helpfully to the user's question.`}var V=R({},j),Le=null;function Re(e){let t=(e==null?void 0:e.settings)||V;(!Le||e!=null&&e.resetScope)&&(Le=figma.currentPage.selection.map(o=>o.id));let n=Le.map(o=>figma.getNodeById(o)).filter(o=>o!==null&&o.type!=="DOCUMENT"&&o.type!=="PAGE");n.length>0&&(figma.currentPage.selection=n);let s=xe(t);k("design-lint-result",s)}function Ye(){try{let e=It();figma.root.setPluginData("ignoredState",JSON.stringify(e))}catch(e){}}function ca(e){kt(e.nodeId),Ye(),Re()}function la(e){Nt(e.nodeId,e.errorType,e.value),Ye(),Re()}function da(e){let t=xe(V);wt(t.errors,e.errorType),Ye(),Re()}function ua(){Ct(),Ye(),Re()}function pa(e){try{let t=figma.getNodeById(e.nodeId);t&&t.type!=="DOCUMENT"&&t.type!=="PAGE"&&(figma.currentPage.selection=[t],figma.viewport.scrollAndZoomIntoView([t]))}catch(t){console.warn("Could not select node:",e.nodeId,t)}}function ma(e){let t=figma.currentPage.selection;if(t.length===0)return;let n=At(t,e.errorType,e.value,V),o=[...new Set(n.map(r=>r.nodeId))].map(r=>figma.getNodeById(r)).filter(r=>r!==null&&r.type!=="DOCUMENT"&&r.type!=="PAGE");o.length>0&&(figma.currentPage.selection=o,figma.viewport.scrollAndZoomIntoView(o),k("lint-selected-nodes",{count:o.length,value:e.value}))}async function fa(e){V=e.settings;try{await figma.clientStorage.setAsync("design-lint-settings",e.settings)}catch(t){console.warn("Could not save lint settings:",t)}}async function ga(){try{let e=await figma.clientStorage.getAsync("design-lint-settings");e&&(V=R(R({},j),e)),k("lint-settings-loaded",V)}catch(e){console.warn("Could not load lint settings:",e),k("lint-settings-loaded",j)}}function ya(e){try{let t=e.config;if(!t||t.version!==1){k("team-config-saved",{success:!1,error:"Invalid config version"});return}figma.root.setSharedPluginData("figmalint","config",JSON.stringify(t)),k("team-config-saved",{success:!0})}catch(t){k("team-config-saved",{success:!1,error:String(t)})}}function ha(){var e,t;try{let n=figma.root.getSharedPluginData("figmalint","config");if(n){let s=JSON.parse(n);V=R({},j),(e=s.scales)!=null&&e.spacing&&(V.spacingScale=s.scales.spacing),(t=s.scales)!=null&&t.radius&&(V.allowedRadii=s.scales.radius),s.severityOverrides&&(V.severityOverrides=s.severityOverrides),s.ignorePatterns&&(V.ignorePatterns=s.ignorePatterns),k("team-config-loaded",{config:s,settings:V})}else k("team-config-loaded",{config:null,settings:V})}catch(n){console.warn("Could not load team config:",n),k("team-config-loaded",{config:null,settings:V})}}function ba(e){try{let t=figma.getNodeById(e.nodeId);t&&t.type!=="DOCUMENT"&&t.type!=="PAGE"&&(figma.currentPage.selection=[t],figma.viewport.scrollAndZoomIntoView([t]))}catch(t){console.warn("Could not jump to node:",e.nodeId,t)}}var va=new Set(["itemSpacing","paddingTop","paddingBottom","paddingLeft","paddingRight","counterAxisSpacing"]);function Sa(e){try{if(!va.has(e.property)){k("fix-error",{error:`Invalid spacing property: ${e.property}`});return}let t=figma.getNodeById(e.nodeId);if(t&&(t.type==="FRAME"||t.type==="COMPONENT"||t.type==="INSTANCE")){let n=t[e.property];t[e.property]=e.value,k("fix-applied",{type:"spacing",nodeId:e.nodeId,nodeName:t.name,property:e.property,oldValue:n,newValue:e.value})}}catch(t){console.warn("Could not fix spacing:",t),k("fix-error",{error:"Failed to apply spacing fix"})}}async function ka(e){try{let t=null;if(e!=null&&e.nodeId){let i=figma.getNodeById(e.nodeId);i&&i.type!=="DOCUMENT"&&i.type!=="PAGE"&&(t=i)}else figma.currentPage.selection.length>0&&(t=figma.currentPage.selection[0]);if(!t){k("screenshot-error",{error:"No node selected"});return}let n=await ze(t),s="layoutMode"in t&&t.layoutMode!=="NONE",o="children"in t?t.children.length:0,r;try{let i=await ue(t),a=[...i.colors,...i.spacing,...i.typography,...i.effects,...i.borders];r={totalTokens:a.length,boundToVariables:a.filter(c=>c.source==="figma-variable").length,boundToStyles:a.filter(c=>c.source==="figma-style").length,hardCoded:a.filter(c=>c.source==="hard-coded").length}}catch(i){}k("screenshot-result",{nodeId:t.id,nodeName:t.name,nodeType:t.type,screenshot:n,width:t.width,height:t.height,hasAutoLayout:s,childCount:o,tokenSummary:r})}catch(t){console.warn("Could not export screenshot:",t),k("screenshot-error",{error:"Failed to export screenshot"})}}function Na(e){try{let t=Ae(e.nodeId,e.property);k("fix-applied",{type:"spacing",nodeId:t.nodeId,nodeName:t.nodeName,property:e.property,oldValue:t.oldValue,newValue:t.newValue,success:t.success,error:t.error})}catch(t){k("fix-error",{error:"Failed to auto-fix spacing"})}}function wa(e){try{let t=e.allowedRadii||[0,2,4,8,12,16,20,24,32],n=qe(e.nodeId,t);k("fix-applied",{type:"radius",nodeId:n.nodeId,nodeName:n.nodeName,oldValue:n.oldValue,newValue:n.newValue,success:n.success,error:n.error})}catch(t){k("fix-error",{error:"Failed to auto-fix radius"})}}function Ca(e){try{let t=no(e.nodeId),n=t.filter(s=>s.success).length;for(let s of t)k("fix-applied",{type:"spacing",nodeId:s.nodeId,nodeName:s.nodeName,property:s.property,oldValue:s.oldValue,newValue:s.newValue,success:s.success});n>0&&figma.notify(`Fixed ${n} spacing value${n!==1?"s":""}`,{timeout:2e3})}catch(t){k("fix-error",{error:"Failed to fix all spacing"})}}async function Ia(e){try{let{applyFillStyle:t,applyStrokeStyle:n,applyTextStyle:s,applyEffectStyle:o}=await Promise.resolve().then(()=>(jt(),so)),r;switch(e.styleType){case"fill":r=await t(e.nodeId,e.styleKey);break;case"stroke":r=await n(e.nodeId,e.styleKey);break;case"text":r=await s(e.nodeId,e.styleKey);break;case"effect":r=await o(e.nodeId,e.styleKey);break;default:k("fix-error",{error:`Unknown style type: ${e.styleType}`});return}k("fix-applied",{type:"style",nodeId:r.nodeId,nodeName:r.nodeName,property:r.property,oldValue:r.oldValue,newValue:r.newValue,success:r.success,error:r.error})}catch(t){k("fix-error",{error:"Failed to apply style"})}}function xa(e){try{let t=Je(e.nodeId,e.newName);k("fix-applied",{type:"rename",nodeId:t.nodeId,nodeName:t.newName,oldValue:t.oldName,newValue:t.newName,success:t.success,error:t.error})}catch(t){k("fix-error",{error:"Failed to rename layer"})}}async function Aa(e){try{let t=await oo(e.fixes);k("batch-fix-v2-result",t),t.failed===0?figma.notify(`Applied ${t.applied} fix${t.applied!==1?"es":""} successfully`,{timeout:2e3}):t.applied>0?figma.notify(`Applied ${t.applied}, ${t.failed} failed`,{timeout:3e3}):figma.notify(`All ${t.failed} fixes failed`,{error:!0}),So()}catch(t){k("fix-error",{error:"Batch fix failed"})}}function So(){if(Le){let t=Le.map(n=>figma.getNodeById(n)).filter(n=>n!==null&&n.type!=="DOCUMENT"&&n.type!=="PAGE");t.length>0&&(figma.currentPage.selection=t)}let e=xe(V);k("design-lint-result",e),k("rescan-complete",{totalErrors:e.summary.totalErrors,nodesWithErrors:e.summary.nodesWithErrors})}async function Ea(){try{k("flow-analysis-started",{status:"building-graph"});let e=Gs();if(e.frames.length===0){k("flow-analysis-error",{error:"No top-level frames found on current page."});return}if(e.frames.length>50){k("flow-analysis-error",{error:`Too many frames (${e.frames.length}). Select a page with \u226450 frames for flow analysis.`});return}let t=zs(e);k("flow-analysis-started",{status:"capturing-screenshots",total:e.frames.length});let n={},s={},o=10;for(let c=0;c<e.frames.length;c+=o){let l=e.frames.slice(c,c+o).map(async p=>{let u=await figma.getNodeByIdAsync(p.id);if(!(!u||!("exportAsync"in u))){try{let g=await ze(u);n[p.id]=g}catch(g){}try{let{runDesignLint:g}=await Promise.resolve().then(()=>(be(),Et)),f=g([u],V);s[p.id]=f}catch(g){}}});await Promise.all(l),k("flow-analysis-started",{status:"capturing-screenshots",progress:Math.min(c+o,e.frames.length),total:e.frames.length})}let r=[];for(let c of e.frames){let d=await figma.getNodeByIdAsync(c.id);d&&r.push({frame:c,node:d})}let i=Ks(r,{skipLocked:V.skipLockedLayers,skipHidden:V.skipHiddenLayers}),a=[...t,...i];k("flow-analysis-result",{graph:e,graphIssues:a,screenshots:n,lintResults:s})}catch(e){let t=e instanceof Error?e.message:"Unknown error";k("flow-analysis-error",{error:t})}}async function Ta(){try{let t=figma.currentPage.children.filter(l=>l.type==="FRAME"||l.type==="COMPONENT_SET");if(t.length===0){k("analysis-error",{error:"No top-level frames found on current page."});return}let n=t.slice(0,50),s=n.length,{runDesignLint:o}=await Promise.resolve().then(()=>(be(),Et)),r=[],i=5;for(let l=0;l<s;l+=i){let u=n.slice(l,l+i).map(async(f,m)=>{let h=l+m+1;k("page-sweep-progress",{current:h,total:s,frameName:f.name});let C={summary:{totalErrors:0,byType:{},totalNodes:0,nodesWithErrors:0},errors:[]};try{C=o([f],V)}catch(N){}let S="";try{S=await ze(f,800)}catch(N){}return{id:f.id,name:f.name,screenshot:S,lintResult:{summary:C.summary,errors:C.errors},width:Math.round(f.width),height:Math.round(f.height)}}),g=await Promise.all(u);r.push(...g)}let a=0,c={};for(let l of r){a+=l.lintResult.summary.totalErrors||0;for(let p of l.lintResult.errors){let u=p.errorType;c[u]||(c[u]={count:0,severity:p.severity||"warning"}),c[u].count++}}let d=Object.entries(c).sort((l,p)=>p[1].count-l[1].count).slice(0,10).map(([l,{count:p,severity:u}])=>({type:l,count:p,severity:u}));k("page-sweep-result",{frames:r,aggregated:{totalFrames:s,totalIssues:a,topIssues:d}})}catch(e){let t=e instanceof Error?e.message:"Unknown error";k("analysis-error",{error:`Page sweep failed: ${t}`})}}async function ko(){var e,t;try{let n=await dt();_=n.providerId,H=n.modelId,n.apiKey?(Z=n.apiKey,k("api-key-status",{hasKey:!0,provider:_,model:H})):k("api-key-status",{hasKey:!1,provider:_,model:H}),console.log(`Plugin initialized with provider: ${_}, model: ${H}`);try{let s=figma.root.getPluginData("ignoredState");if(s){let o=JSON.parse(s);xt(o),console.log(`Restored ${((e=o.nodeIds)==null?void 0:e.length)||0} ignored nodes, ${((t=o.errorKeys)==null?void 0:t.length)||0} ignored errors`)}}catch(s){}console.log("\u{1F504} Initializing design systems knowledge..."),Q.loadDesignSystemsKnowledge().then(()=>{console.log("\u2705 Design systems knowledge loaded successfully")}).catch(s=>{console.warn("\u26A0\uFE0F Failed to load design systems knowledge, using fallback:",s)}),console.log("Plugin initialized successfully")}catch(n){console.error("Error initializing plugin:",n)}}function Pa(e){let t={version:1,timestamp:Date.now(),nodeId:e.nodeId,nodeName:e.nodeName,overall:e.overall,grade:e.grade,categories:e.categories,summary:e.summary,errors:e.errors.map(n=>({errorType:n.errorType,severity:n.severity||"warning",nodeId:n.nodeId,message:n.message}))};Xs(t),k("baseline-saved",{nodeId:e.nodeId,nodeName:e.nodeName,timestamp:t.timestamp,overall:e.overall})}function La(e){let t=Zs(e.nodeId);k("baseline-loaded",t)}function Ra(e){let t=Ys(e.nodeId);if(!t){k("diff-result",null);return}let n=e.errors.map(o=>({errorType:o.errorType,severity:o.severity||"warning",nodeId:o.nodeId,message:o.message})),s=to(t,{overall:e.overall,grade:e.grade,categories:e.categories,errors:n,summary:e.summary});k("diff-result",s)}function $a(e){Qs(e.nodeId)}async function Ma(e){try{let t=await figma.getNodeByIdAsync(e.nodeId);if(!t||!("type"in t)){k("fix-preview",{success:!1,error:"Node not found or is not a valid scene node"});return}let n=t,s=null;if(e.type==="token"){if(!e.propertyPath){k("fix-preview",{success:!1,error:"Property path is required for token fixes"});return}if(e.propertyPath.match(/^(fills|strokes)\[(\d+)\]$/)){let r=await Ot(e.suggestedValue||"",.1);r.length>0&&(s=await _t(n,e.propertyPath,r[0].variableId))}else{let r=parseFloat(e.suggestedValue||"0"),i=await Ft(r,e.propertyPath||"",2);i.length>0&&(s=await _t(n,e.propertyPath,i[0].variableId))}if(s){let r=s;k("fix-preview",{success:!0,type:"token",nodeId:r.nodeId,nodeName:r.nodeName,propertyPath:r.propertyPath,beforeValue:r.beforeValue,afterValue:r.afterValue,tokenId:r.tokenId,tokenName:r.tokenName})}else k("fix-preview",{success:!1,error:"No matching token found for this value"})}else if(e.type==="naming"){let o=e.suggestedValue||fe(n);s=mn(n,o),k("fix-preview",{success:!0,preview:s})}else k("fix-preview",{success:!1,error:`Unknown fix type: ${e.type}`})}catch(t){console.error("Error previewing fix:",t);let n=t instanceof Error?t.message:"Unknown error occurred";k("fix-preview",{success:!1,error:n})}}async function Oa(e){try{let t=await figma.getNodeByIdAsync(e.nodeId);if(!t||!("type"in t)){k("fix-applied",{success:!1,error:"Node not found or is not a valid scene node"}),figma.notify("Failed to apply fix: Node not found",{error:!0});return}let n=t;if(!e.propertyPath){k("fix-applied",{success:!1,error:"Property path is required for token fixes"}),figma.notify("Failed to apply fix: Property path missing",{error:!0});return}if(!e.tokenId){k("fix-applied",{success:!1,error:"Token ID is required for token fixes"}),figma.notify("Failed to apply fix: Token ID missing",{error:!0});return}let s;/^(fills|strokes)\[\d+\]$/.test(e.propertyPath)?s=await Dt(n,e.propertyPath,e.tokenId):s=await Vt(n,e.propertyPath,e.tokenId),k("fix-applied",K(R({},s),{fixType:"token",nodeId:e.nodeId,propertyPath:e.propertyPath})),s.success?figma.notify(`Applied token to ${n.name}`,{timeout:2e3}):figma.notify(`Failed to apply token: ${s.error||s.message}`,{error:!0})}catch(t){console.error("Error applying token fix:",t);let n=t instanceof Error?t.message:"Unknown error occurred";k("fix-applied",{success:!1,error:n,fixType:"token",nodeId:e.nodeId}),figma.notify(`Failed to apply fix: ${n}`,{error:!0})}}async function Fa(e){try{let t=await figma.getNodeByIdAsync(e.nodeId);if(!t||!("type"in t)){k("fix-applied",{success:!1,error:"Node not found or is not a valid scene node"}),figma.notify("Failed to rename: Node not found",{error:!0});return}let n=t,s=e.newValue||fe(n),o=n.name;if(o===s){k("fix-applied",{success:!0,fixType:"naming",nodeId:e.nodeId,message:`Layer already named "${s}"`,oldName:o,newName:s}),figma.notify(`Layer already named "${s}"`,{timeout:2e3});return}let r=pt(n,s),i={success:r,fixType:"naming",nodeId:e.nodeId,message:r?`Renamed "${o}" to "${s}"`:"Failed to rename layer",oldName:o,newName:r?s:o};k("fix-applied",i),r?figma.notify(`Renamed "${o}" to "${s}"`,{timeout:2e3}):figma.notify("Failed to rename layer",{error:!0})}catch(t){console.error("Error applying naming fix:",t);let n=t instanceof Error?t.message:"Unknown error occurred";k("fix-applied",{success:!1,error:n}),figma.notify(`Failed to rename: ${n}`,{error:!0})}}async function Da(e){try{let t=[],n=0,s=0;for(let r of e.fixes)try{let i=await figma.getNodeByIdAsync(r.nodeId);if(!i||!("type"in i)){t.push({nodeId:r.nodeId,success:!1,message:"Node not found",error:"Node not found or is not a valid scene node"}),s++;continue}let a=i;if(r.type==="token"){if(!r.propertyPath){t.push({nodeId:r.nodeId,success:!1,message:"Missing property path",error:"Token fixes require a propertyPath"}),s++;continue}let c=r.tokenId,d=/^(fills|strokes)\[\d+\]$/.test(r.propertyPath);if(!c&&r.newValue)try{if(d){let p=await Ot(r.newValue,.1);p.length>0&&(c=p[0].variableId)}else{let p=parseFloat(r.newValue);if(!isNaN(p)){let u=await Ft(p,r.propertyPath||"",2);u.length>0&&(c=u[0].variableId)}}}catch(p){console.warn("Could not find matching variable:",p)}if(!c){t.push({nodeId:r.nodeId,success:!1,message:"No matching design token found for this value",error:"Could not find a matching variable to bind"}),s++;continue}let l;d?l=await Dt(a,r.propertyPath,c):l=await Vt(a,r.propertyPath,c),t.push({nodeId:r.nodeId,success:l.success,message:l.message,error:l.error}),l.success?n++:s++}else if(r.type==="naming"){let c=r.newValue||fe(a),d=a.name,l=pt(a,c);t.push({nodeId:r.nodeId,success:l,message:l?`Renamed "${d}" to "${c}"`:"Failed to rename layer"}),l?n++:s++}else t.push({nodeId:r.nodeId,success:!1,message:`Unknown fix type: ${r.type}`,error:`Unsupported fix type: ${r.type}`}),s++}catch(i){let a=i instanceof Error?i.message:"Unknown error";t.push({nodeId:r.nodeId,success:!1,message:"Error applying fix",error:a}),s++}let o={total:e.fixes.length,success:n,errors:s,results:t};k("batch-fix-applied",o),s===0?figma.notify(`Applied ${n} fix${n!==1?"es":""} successfully`,{timeout:2e3}):n>0?figma.notify(`Applied ${n} fix${n!==1?"es":""}, ${s} failed`,{timeout:3e3}):figma.notify(`Failed to apply ${s} fix${s!==1?"es":""}`,{error:!0})}catch(t){console.error("Error applying batch fixes:",t);let n=t instanceof Error?t.message:"Unknown error occurred";k("batch-fix-applied",{total:e.fixes.length,success:0,errors:e.fixes.length,error:n}),figma.notify(`Batch fix failed: ${n}`,{error:!0})}}async function Va(e){try{let t=await figma.getNodeByIdAsync(e.nodeId);if(!t){k("description-updated",{success:!1,error:"Node not found"}),figma.notify("Failed to update description: Node not found",{error:!0});return}if(t.type!=="COMPONENT"&&t.type!=="COMPONENT_SET"){k("description-updated",{success:!1,error:"Node is not a component or component set"}),figma.notify("Description can only be set on components",{error:!0});return}let n=t,s=n.description;n.description=e.description,k("description-updated",{success:!0,oldDescription:s,newDescription:e.description}),figma.notify("Component description updated",{timeout:2e3})}catch(t){console.error("Error updating description:",t);let n=t instanceof Error?t.message:"Unknown error occurred";k("description-updated",{success:!1,error:n}),figma.notify(`Failed to update description: ${n}`,{error:!0})}}async function _a(e){try{let{nodeId:t,propertyName:n,propertyType:s,defaultValue:o}=e,r=await figma.getNodeByIdAsync(t);if(!r){k("property-added",{success:!1,propertyName:n,message:"Node not found"}),figma.notify("Node not found",{error:!0});return}let i=null;if(r.type==="COMPONENT"){let l=r;l.parent&&l.parent.type==="COMPONENT_SET"?i=l.parent:i=l}else if(r.type==="COMPONENT_SET")i=r;else if(r.type==="INSTANCE"){let l=await r.getMainComponentAsync();l&&(l.parent&&l.parent.type==="COMPONENT_SET"?i=l.parent:i=l)}if(!i){k("property-added",{success:!1,propertyName:n,message:"Selected node is not a component"}),figma.notify("Selected node is not a component",{error:!0});return}let a=i.componentPropertyDefinitions;for(let l of Object.keys(a))if(l.replace(/#\d+:\d+$/,"").toLowerCase()===n.toLowerCase()){k("property-added",{success:!1,propertyName:n,message:`Property "${n}" already exists`}),figma.notify(`Property "${n}" already exists`,{error:!0});return}let c;switch(s.toLowerCase()){case"boolean":c="BOOLEAN";break;case"text":c="TEXT";break;case"slot":c="INSTANCE_SWAP";break;case"variant":i.type==="COMPONENT_SET"?c="VARIANT":c="TEXT";break;default:c="TEXT"}i.addComponentProperty(n,c,o);let d="";if(c==="VARIANT"&&i.type==="COMPONENT_SET"&&e.variantOptions&&e.variantOptions.length>1){let l=i,p=[...l.children],u=e.variantOptions.slice(1),g=`${n}=${o}`,f=figma.currentPage,m=l;for(;m.parent&&m.parent.type!=="PAGE";)m=m.parent;let h=m.absoluteTransform[0][2],C=m.absoluteTransform[1][2],S=h,N=C+m.height+50,y=figma.createSection();y.name=`FigmaLint: ${n} Variants`,f.appendChild(y),y.x=S,y.y=N;let b=figma.createText();await figma.loadFontAsync({family:"Inter",style:"Medium"}),b.fontName={family:"Inter",style:"Medium"},b.characters=`New "${n}" variants \u2014 drag into the ComponentSet`,b.fontSize=14,b.fills=[{type:"SOLID",color:{r:.4,g:.4,b:.4}}],y.appendChild(b),b.x=24,b.y=24;let I=24,w=32,P=b.y+b.height+24,O=b.width+I*2;for(let M of u){let z=`${n}=${M}`,x=figma.createText();await figma.loadFontAsync({family:"Inter",style:"Semi Bold"}),x.fontName={family:"Inter",style:"Semi Bold"},x.characters=`${n}=${M}`,x.fontSize=12,x.fills=[{type:"SOLID",color:{r:.6,g:.3,b:.9}}],y.appendChild(x),x.x=I,x.y=P,P+=x.height+12;let $=I,v=0;for(let T of p){let L=T.clone();L.name=L.name.replace(g,z),y.appendChild(L),L.x=$,L.y=P,$+=L.width+w,v=Math.max(v,L.height)}O=Math.max(O,$-w+I),P+=v+w}y.resizeWithoutConstraints(Math.max(O,400),P+I),d=" \u2014 new variants created in staging section to the right"}k("property-added",{success:!0,propertyName:n,message:`Property "${n}" added successfully${d}`}),figma.notify(`Property "${n}" added${d?" (see staging section)":""}`,{timeout:3e3})}catch(t){console.error("Error adding component property:",t);let n=t instanceof Error?t.message:"Unknown error occurred";k("property-added",{success:!1,propertyName:e.propertyName,message:n}),figma.notify(`Failed to add property: ${n}`,{error:!0})}}async function Ba(){try{let e=await qt();k("variable-system-result",e)}catch(e){console.error("Error collecting variables:",e);let t=e instanceof Error?e.message:"Unknown error";k("variable-system-error",{error:t})}}async function Ua(e){try{let t=ro(e.dtcgJson),n=await qt(),s=ao(n,t,null);k("dtcg-compliance-result",s)}catch(t){console.error("Error checking DTCG compliance:",t);let n=t instanceof Error?t.message:"Unknown error";k("dtcg-compliance-error",{error:n})}}async function Ga(e){try{let t=await co(e.collectionId);k("mode-comparison-result",t)}catch(t){console.error("Error comparing modes:",t);let n=t instanceof Error?t.message:"Unknown error";k("mode-comparison-error",{error:n})}}function za(e){let t=e.settings||j;uo({enabled:!0,debounceMs:e.debounceMs||500,settings:t})}function Wa(){po()}function Ha(e){let t=fo(e.lintResult,e.tokenSummary||null);k("design-debt-result",t)}var Ka={width:380,height:600,themeColors:!0};try{figma.showUI(__html__,Ka),console.log("\u2705 FigmaLint v2.0 - UI shown successfully")}catch(e){console.log("\u2139\uFE0F UI might already be shown in inspect panel:",e)}figma.ui.onmessage=bo;figma.on("selectionchange",()=>{let e=figma.currentPage.selection;figma.ui.postMessage({type:"selection-changed",data:{hasSelection:e.length>0,nodeId:e.length>0?e[0].id:null,nodeName:e.length>0?e[0].name:null}})});ko();console.log("\u{1F680} FigmaLint v2.0 initialized with modular architecture");})(); +Respond naturally and helpfully to the user's question.`}var D=R({},j),Le=null;function Re(e){let t=(e==null?void 0:e.settings)||D;(!Le||e!=null&&e.resetScope)&&(Le=figma.currentPage.selection.map(o=>o.id));let n=Le.map(o=>figma.getNodeById(o)).filter(o=>o!==null&&o.type!=="DOCUMENT"&&o.type!=="PAGE");n.length>0&&(figma.currentPage.selection=n);let s=xe(t);S("design-lint-result",s)}function Ye(){try{let e=It();figma.root.setPluginData("ignoredState",JSON.stringify(e))}catch(e){}}function la(e){kt(e.nodeId),Ye(),Re()}function da(e){Nt(e.nodeId,e.errorType,e.value),Ye(),Re()}function ua(e){let t=xe(D);wt(t.errors,e.errorType),Ye(),Re()}function pa(){Ct(),Ye(),Re()}function ma(e){try{let t=figma.getNodeById(e.nodeId);t&&t.type!=="DOCUMENT"&&t.type!=="PAGE"&&(figma.currentPage.selection=[t],figma.viewport.scrollAndZoomIntoView([t]))}catch(t){console.warn("Could not select node:",e.nodeId,t)}}function fa(e){let t=figma.currentPage.selection;if(t.length===0)return;let n=At(t,e.errorType,e.value,D),o=[...new Set(n.map(r=>r.nodeId))].map(r=>figma.getNodeById(r)).filter(r=>r!==null&&r.type!=="DOCUMENT"&&r.type!=="PAGE");o.length>0&&(figma.currentPage.selection=o,figma.viewport.scrollAndZoomIntoView(o),S("lint-selected-nodes",{count:o.length,value:e.value}))}async function ga(e){D=e.settings;try{await figma.clientStorage.setAsync("design-lint-settings",e.settings)}catch(t){console.warn("Could not save lint settings:",t)}}async function ya(){try{let e=await figma.clientStorage.getAsync("design-lint-settings");e&&(D=R(R({},j),e)),S("lint-settings-loaded",D)}catch(e){console.warn("Could not load lint settings:",e),S("lint-settings-loaded",j)}}function ha(e){try{let t=e.config;if(!t||t.version!==1){S("team-config-saved",{success:!1,error:"Invalid config version"});return}figma.root.setSharedPluginData("figmalint","config",JSON.stringify(t)),S("team-config-saved",{success:!0})}catch(t){S("team-config-saved",{success:!1,error:String(t)})}}function ba(){var e,t;try{let n=figma.root.getSharedPluginData("figmalint","config");if(n){let s=JSON.parse(n);D=R({},j),(e=s.scales)!=null&&e.spacing&&(D.spacingScale=s.scales.spacing),(t=s.scales)!=null&&t.radius&&(D.allowedRadii=s.scales.radius),s.severityOverrides&&(D.severityOverrides=s.severityOverrides),s.ignorePatterns&&(D.ignorePatterns=s.ignorePatterns),S("team-config-loaded",{config:s,settings:D})}else S("team-config-loaded",{config:null,settings:D})}catch(n){console.warn("Could not load team config:",n),S("team-config-loaded",{config:null,settings:D})}}function va(e){try{let t=figma.getNodeById(e.nodeId);t&&t.type!=="DOCUMENT"&&t.type!=="PAGE"&&(figma.currentPage.selection=[t],figma.viewport.scrollAndZoomIntoView([t]))}catch(t){console.warn("Could not jump to node:",e.nodeId,t)}}var Sa=new Set(["itemSpacing","paddingTop","paddingBottom","paddingLeft","paddingRight","counterAxisSpacing"]);function ka(e){try{if(!Sa.has(e.property)){S("fix-error",{error:`Invalid spacing property: ${e.property}`});return}let t=figma.getNodeById(e.nodeId);if(t&&(t.type==="FRAME"||t.type==="COMPONENT"||t.type==="INSTANCE")){let n=t[e.property];t[e.property]=e.value,S("fix-applied",{type:"spacing",nodeId:e.nodeId,nodeName:t.name,property:e.property,oldValue:n,newValue:e.value})}}catch(t){console.warn("Could not fix spacing:",t),S("fix-error",{error:"Failed to apply spacing fix"})}}async function Na(e){try{let t=null;if(e!=null&&e.nodeId){let i=figma.getNodeById(e.nodeId);i&&i.type!=="DOCUMENT"&&i.type!=="PAGE"&&(t=i)}else figma.currentPage.selection.length>0&&(t=figma.currentPage.selection[0]);if(!t){S("screenshot-error",{error:"No node selected"});return}let n=await ze(t),s="layoutMode"in t&&t.layoutMode!=="NONE",o="children"in t?t.children.length:0,r;try{let i=await ue(t),a=[...i.colors,...i.spacing,...i.typography,...i.effects,...i.borders];r={totalTokens:a.length,boundToVariables:a.filter(c=>c.source==="figma-variable").length,boundToStyles:a.filter(c=>c.source==="figma-style").length,hardCoded:a.filter(c=>c.source==="hard-coded").length}}catch(i){}S("screenshot-result",{nodeId:t.id,nodeName:t.name,nodeType:t.type,screenshot:n,width:t.width,height:t.height,hasAutoLayout:s,childCount:o,tokenSummary:r})}catch(t){console.warn("Could not export screenshot:",t),S("screenshot-error",{error:"Failed to export screenshot"})}}function wa(e){try{let t=Ae(e.nodeId,e.property);S("fix-applied",{type:"spacing",nodeId:t.nodeId,nodeName:t.nodeName,property:e.property,oldValue:t.oldValue,newValue:t.newValue,success:t.success,error:t.error})}catch(t){S("fix-error",{error:"Failed to auto-fix spacing"})}}function Ca(e){try{let t=e.allowedRadii||[0,2,4,8,12,16,20,24,32],n=qe(e.nodeId,t);S("fix-applied",{type:"radius",nodeId:n.nodeId,nodeName:n.nodeName,oldValue:n.oldValue,newValue:n.newValue,success:n.success,error:n.error})}catch(t){S("fix-error",{error:"Failed to auto-fix radius"})}}function Ia(e){try{let t=no(e.nodeId),n=t.filter(s=>s.success).length;for(let s of t)S("fix-applied",{type:"spacing",nodeId:s.nodeId,nodeName:s.nodeName,property:s.property,oldValue:s.oldValue,newValue:s.newValue,success:s.success});n>0&&figma.notify(`Fixed ${n} spacing value${n!==1?"s":""}`,{timeout:2e3})}catch(t){S("fix-error",{error:"Failed to fix all spacing"})}}async function xa(e){try{let{applyFillStyle:t,applyStrokeStyle:n,applyTextStyle:s,applyEffectStyle:o}=await Promise.resolve().then(()=>(jt(),so)),r;switch(e.styleType){case"fill":r=await t(e.nodeId,e.styleKey);break;case"stroke":r=await n(e.nodeId,e.styleKey);break;case"text":r=await s(e.nodeId,e.styleKey);break;case"effect":r=await o(e.nodeId,e.styleKey);break;default:S("fix-error",{error:`Unknown style type: ${e.styleType}`});return}S("fix-applied",{type:"style",nodeId:r.nodeId,nodeName:r.nodeName,property:r.property,oldValue:r.oldValue,newValue:r.newValue,success:r.success,error:r.error})}catch(t){S("fix-error",{error:"Failed to apply style"})}}function Aa(e){try{let t=Je(e.nodeId,e.newName);S("fix-applied",{type:"rename",nodeId:t.nodeId,nodeName:t.newName,oldValue:t.oldName,newValue:t.newName,success:t.success,error:t.error})}catch(t){S("fix-error",{error:"Failed to rename layer"})}}async function Ea(e){try{let t=await oo(e.fixes);S("batch-fix-v2-result",t),t.failed===0?figma.notify(`Applied ${t.applied} fix${t.applied!==1?"es":""} successfully`,{timeout:2e3}):t.applied>0?figma.notify(`Applied ${t.applied}, ${t.failed} failed`,{timeout:3e3}):figma.notify(`All ${t.failed} fixes failed`,{error:!0}),So()}catch(t){S("fix-error",{error:"Batch fix failed"})}}function So(){if(Le){let t=Le.map(n=>figma.getNodeById(n)).filter(n=>n!==null&&n.type!=="DOCUMENT"&&n.type!=="PAGE");t.length>0&&(figma.currentPage.selection=t)}let e=xe(D);S("design-lint-result",e),S("rescan-complete",{totalErrors:e.summary.totalErrors,nodesWithErrors:e.summary.nodesWithErrors})}async function Ta(){try{S("flow-analysis-started",{status:"building-graph"});let e=Gs();if(e.frames.length===0){S("flow-analysis-error",{error:"No top-level frames found on current page."});return}if(e.frames.length>50){S("flow-analysis-error",{error:`Too many frames (${e.frames.length}). Select a page with \u226450 frames for flow analysis.`});return}let t=zs(e);S("flow-analysis-started",{status:"capturing-screenshots",total:e.frames.length});let n={},s={},o=10;for(let c=0;c<e.frames.length;c+=o){let l=e.frames.slice(c,c+o).map(async p=>{let u=await figma.getNodeByIdAsync(p.id);if(!(!u||!("exportAsync"in u))){try{let g=await ze(u);n[p.id]=g}catch(g){}try{let{runDesignLint:g}=await Promise.resolve().then(()=>(be(),Et)),f=g([u],D);s[p.id]=f}catch(g){}}});await Promise.all(l),S("flow-analysis-started",{status:"capturing-screenshots",progress:Math.min(c+o,e.frames.length),total:e.frames.length})}let r=[];for(let c of e.frames){let d=await figma.getNodeByIdAsync(c.id);d&&r.push({frame:c,node:d})}let i=Ks(r,{skipLocked:D.skipLockedLayers,skipHidden:D.skipHiddenLayers}),a=[...t,...i];S("flow-analysis-result",{graph:e,graphIssues:a,screenshots:n,lintResults:s})}catch(e){let t=e instanceof Error?e.message:"Unknown error";S("flow-analysis-error",{error:t})}}async function Pa(){try{let t=figma.currentPage.children.filter(l=>l.type==="FRAME"||l.type==="COMPONENT_SET");if(t.length===0){S("analysis-error",{error:"No top-level frames found on current page."});return}let n=t.slice(0,50),s=n.length,{runDesignLint:o}=await Promise.resolve().then(()=>(be(),Et)),r=[],i=5;for(let l=0;l<s;l+=i){let u=n.slice(l,l+i).map(async(f,m)=>{let h=l+m+1;S("page-sweep-progress",{current:h,total:s,frameName:f.name});let C={summary:{totalErrors:0,byType:{},totalNodes:0,nodesWithErrors:0},errors:[]};try{C=o([f],D)}catch(N){}let k="";try{k=await ze(f,800)}catch(N){}return{id:f.id,name:f.name,screenshot:k,lintResult:{summary:C.summary,errors:C.errors},width:Math.round(f.width),height:Math.round(f.height)}}),g=await Promise.all(u);r.push(...g)}let a=0,c={};for(let l of r){a+=l.lintResult.summary.totalErrors||0;for(let p of l.lintResult.errors){let u=p.errorType;c[u]||(c[u]={count:0,severity:p.severity||"warning"}),c[u].count++}}let d=Object.entries(c).sort((l,p)=>p[1].count-l[1].count).slice(0,10).map(([l,{count:p,severity:u}])=>({type:l,count:p,severity:u}));S("page-sweep-result",{frames:r,aggregated:{totalFrames:s,totalIssues:a,topIssues:d}})}catch(e){let t=e instanceof Error?e.message:"Unknown error";S("analysis-error",{error:`Page sweep failed: ${t}`})}}async function ko(){var e,t;try{let n=await dt();_=n.providerId,H=n.modelId,n.apiKey?(Z=n.apiKey,S("api-key-status",{hasKey:!0,provider:_,model:H})):S("api-key-status",{hasKey:!1,provider:_,model:H}),console.log(`Plugin initialized with provider: ${_}, model: ${H}`);try{let s=figma.root.getPluginData("ignoredState");if(s){let o=JSON.parse(s);xt(o),console.log(`Restored ${((e=o.nodeIds)==null?void 0:e.length)||0} ignored nodes, ${((t=o.errorKeys)==null?void 0:t.length)||0} ignored errors`)}}catch(s){}console.log("\u{1F504} Initializing design systems knowledge..."),Q.loadDesignSystemsKnowledge().then(()=>{console.log("\u2705 Design systems knowledge loaded successfully")}).catch(s=>{console.warn("\u26A0\uFE0F Failed to load design systems knowledge, using fallback:",s)}),console.log("Plugin initialized successfully")}catch(n){console.error("Error initializing plugin:",n)}}function No(){let e=figma.currentPage.selection;if(e.length!==0)try{let t=e[0],n=ne([t],D),s=n.summary.totalNodes||1,o={critical:10,warning:3,info:1},r=n.errors.reduce((l,p)=>l+(o[p.severity||"warning"]||3),0),i=Math.max(0,s-n.errors.length)*10,a=i+r,c=a>0?Math.round(i/a*100):100,d=n.errors.some(l=>l.severity==="critical")?"critical":n.errors.some(l=>l.severity==="warning")?"warning":n.errors.length>0?"info":"none";S("selection-mini-score",{nodeId:t.id,nodeName:t.name,score:c,issueCount:n.summary.totalErrors,topSeverity:d})}catch(t){}}function La(e){let t={version:1,timestamp:Date.now(),nodeId:e.nodeId,nodeName:e.nodeName,overall:e.overall,grade:e.grade,categories:e.categories,summary:e.summary,errors:e.errors.map(n=>({errorType:n.errorType,severity:n.severity||"warning",nodeId:n.nodeId,message:n.message}))};Xs(t),S("baseline-saved",{nodeId:e.nodeId,nodeName:e.nodeName,timestamp:t.timestamp,overall:e.overall})}function Ra(e){let t=Zs(e.nodeId);S("baseline-loaded",t)}function $a(e){let t=Ys(e.nodeId);if(!t){S("diff-result",null);return}let n=e.errors.map(o=>({errorType:o.errorType,severity:o.severity||"warning",nodeId:o.nodeId,message:o.message})),s=to(t,{overall:e.overall,grade:e.grade,categories:e.categories,errors:n,summary:e.summary});S("diff-result",s)}function Ma(e){Qs(e.nodeId)}async function Oa(e){try{let t=await figma.getNodeByIdAsync(e.nodeId);if(!t||!("type"in t)){S("fix-preview",{success:!1,error:"Node not found or is not a valid scene node"});return}let n=t,s=null;if(e.type==="token"){if(!e.propertyPath){S("fix-preview",{success:!1,error:"Property path is required for token fixes"});return}if(e.propertyPath.match(/^(fills|strokes)\[(\d+)\]$/)){let r=await Ot(e.suggestedValue||"",.1);r.length>0&&(s=await _t(n,e.propertyPath,r[0].variableId))}else{let r=parseFloat(e.suggestedValue||"0"),i=await Ft(r,e.propertyPath||"",2);i.length>0&&(s=await _t(n,e.propertyPath,i[0].variableId))}if(s){let r=s;S("fix-preview",{success:!0,type:"token",nodeId:r.nodeId,nodeName:r.nodeName,propertyPath:r.propertyPath,beforeValue:r.beforeValue,afterValue:r.afterValue,tokenId:r.tokenId,tokenName:r.tokenName})}else S("fix-preview",{success:!1,error:"No matching token found for this value"})}else if(e.type==="naming"){let o=e.suggestedValue||fe(n);s=mn(n,o),S("fix-preview",{success:!0,preview:s})}else S("fix-preview",{success:!1,error:`Unknown fix type: ${e.type}`})}catch(t){console.error("Error previewing fix:",t);let n=t instanceof Error?t.message:"Unknown error occurred";S("fix-preview",{success:!1,error:n})}}async function Fa(e){try{let t=await figma.getNodeByIdAsync(e.nodeId);if(!t||!("type"in t)){S("fix-applied",{success:!1,error:"Node not found or is not a valid scene node"}),figma.notify("Failed to apply fix: Node not found",{error:!0});return}let n=t;if(!e.propertyPath){S("fix-applied",{success:!1,error:"Property path is required for token fixes"}),figma.notify("Failed to apply fix: Property path missing",{error:!0});return}if(!e.tokenId){S("fix-applied",{success:!1,error:"Token ID is required for token fixes"}),figma.notify("Failed to apply fix: Token ID missing",{error:!0});return}let s;/^(fills|strokes)\[\d+\]$/.test(e.propertyPath)?s=await Dt(n,e.propertyPath,e.tokenId):s=await Vt(n,e.propertyPath,e.tokenId),S("fix-applied",K(R({},s),{fixType:"token",nodeId:e.nodeId,propertyPath:e.propertyPath})),s.success?figma.notify(`Applied token to ${n.name}`,{timeout:2e3}):figma.notify(`Failed to apply token: ${s.error||s.message}`,{error:!0})}catch(t){console.error("Error applying token fix:",t);let n=t instanceof Error?t.message:"Unknown error occurred";S("fix-applied",{success:!1,error:n,fixType:"token",nodeId:e.nodeId}),figma.notify(`Failed to apply fix: ${n}`,{error:!0})}}async function Da(e){try{let t=await figma.getNodeByIdAsync(e.nodeId);if(!t||!("type"in t)){S("fix-applied",{success:!1,error:"Node not found or is not a valid scene node"}),figma.notify("Failed to rename: Node not found",{error:!0});return}let n=t,s=e.newValue||fe(n),o=n.name;if(o===s){S("fix-applied",{success:!0,fixType:"naming",nodeId:e.nodeId,message:`Layer already named "${s}"`,oldName:o,newName:s}),figma.notify(`Layer already named "${s}"`,{timeout:2e3});return}let r=pt(n,s),i={success:r,fixType:"naming",nodeId:e.nodeId,message:r?`Renamed "${o}" to "${s}"`:"Failed to rename layer",oldName:o,newName:r?s:o};S("fix-applied",i),r?figma.notify(`Renamed "${o}" to "${s}"`,{timeout:2e3}):figma.notify("Failed to rename layer",{error:!0})}catch(t){console.error("Error applying naming fix:",t);let n=t instanceof Error?t.message:"Unknown error occurred";S("fix-applied",{success:!1,error:n}),figma.notify(`Failed to rename: ${n}`,{error:!0})}}async function Va(e){try{let t=[],n=0,s=0;for(let r of e.fixes)try{let i=await figma.getNodeByIdAsync(r.nodeId);if(!i||!("type"in i)){t.push({nodeId:r.nodeId,success:!1,message:"Node not found",error:"Node not found or is not a valid scene node"}),s++;continue}let a=i;if(r.type==="token"){if(!r.propertyPath){t.push({nodeId:r.nodeId,success:!1,message:"Missing property path",error:"Token fixes require a propertyPath"}),s++;continue}let c=r.tokenId,d=/^(fills|strokes)\[\d+\]$/.test(r.propertyPath);if(!c&&r.newValue)try{if(d){let p=await Ot(r.newValue,.1);p.length>0&&(c=p[0].variableId)}else{let p=parseFloat(r.newValue);if(!isNaN(p)){let u=await Ft(p,r.propertyPath||"",2);u.length>0&&(c=u[0].variableId)}}}catch(p){console.warn("Could not find matching variable:",p)}if(!c){t.push({nodeId:r.nodeId,success:!1,message:"No matching design token found for this value",error:"Could not find a matching variable to bind"}),s++;continue}let l;d?l=await Dt(a,r.propertyPath,c):l=await Vt(a,r.propertyPath,c),t.push({nodeId:r.nodeId,success:l.success,message:l.message,error:l.error}),l.success?n++:s++}else if(r.type==="naming"){let c=r.newValue||fe(a),d=a.name,l=pt(a,c);t.push({nodeId:r.nodeId,success:l,message:l?`Renamed "${d}" to "${c}"`:"Failed to rename layer"}),l?n++:s++}else t.push({nodeId:r.nodeId,success:!1,message:`Unknown fix type: ${r.type}`,error:`Unsupported fix type: ${r.type}`}),s++}catch(i){let a=i instanceof Error?i.message:"Unknown error";t.push({nodeId:r.nodeId,success:!1,message:"Error applying fix",error:a}),s++}let o={total:e.fixes.length,success:n,errors:s,results:t};S("batch-fix-applied",o),s===0?figma.notify(`Applied ${n} fix${n!==1?"es":""} successfully`,{timeout:2e3}):n>0?figma.notify(`Applied ${n} fix${n!==1?"es":""}, ${s} failed`,{timeout:3e3}):figma.notify(`Failed to apply ${s} fix${s!==1?"es":""}`,{error:!0})}catch(t){console.error("Error applying batch fixes:",t);let n=t instanceof Error?t.message:"Unknown error occurred";S("batch-fix-applied",{total:e.fixes.length,success:0,errors:e.fixes.length,error:n}),figma.notify(`Batch fix failed: ${n}`,{error:!0})}}async function _a(e){try{let t=await figma.getNodeByIdAsync(e.nodeId);if(!t){S("description-updated",{success:!1,error:"Node not found"}),figma.notify("Failed to update description: Node not found",{error:!0});return}if(t.type!=="COMPONENT"&&t.type!=="COMPONENT_SET"){S("description-updated",{success:!1,error:"Node is not a component or component set"}),figma.notify("Description can only be set on components",{error:!0});return}let n=t,s=n.description;n.description=e.description,S("description-updated",{success:!0,oldDescription:s,newDescription:e.description}),figma.notify("Component description updated",{timeout:2e3})}catch(t){console.error("Error updating description:",t);let n=t instanceof Error?t.message:"Unknown error occurred";S("description-updated",{success:!1,error:n}),figma.notify(`Failed to update description: ${n}`,{error:!0})}}async function Ba(e){try{let{nodeId:t,propertyName:n,propertyType:s,defaultValue:o}=e,r=await figma.getNodeByIdAsync(t);if(!r){S("property-added",{success:!1,propertyName:n,message:"Node not found"}),figma.notify("Node not found",{error:!0});return}let i=null;if(r.type==="COMPONENT"){let l=r;l.parent&&l.parent.type==="COMPONENT_SET"?i=l.parent:i=l}else if(r.type==="COMPONENT_SET")i=r;else if(r.type==="INSTANCE"){let l=await r.getMainComponentAsync();l&&(l.parent&&l.parent.type==="COMPONENT_SET"?i=l.parent:i=l)}if(!i){S("property-added",{success:!1,propertyName:n,message:"Selected node is not a component"}),figma.notify("Selected node is not a component",{error:!0});return}let a=i.componentPropertyDefinitions;for(let l of Object.keys(a))if(l.replace(/#\d+:\d+$/,"").toLowerCase()===n.toLowerCase()){S("property-added",{success:!1,propertyName:n,message:`Property "${n}" already exists`}),figma.notify(`Property "${n}" already exists`,{error:!0});return}let c;switch(s.toLowerCase()){case"boolean":c="BOOLEAN";break;case"text":c="TEXT";break;case"slot":c="INSTANCE_SWAP";break;case"variant":i.type==="COMPONENT_SET"?c="VARIANT":c="TEXT";break;default:c="TEXT"}i.addComponentProperty(n,c,o);let d="";if(c==="VARIANT"&&i.type==="COMPONENT_SET"&&e.variantOptions&&e.variantOptions.length>1){let l=i,p=[...l.children],u=e.variantOptions.slice(1),g=`${n}=${o}`,f=figma.currentPage,m=l;for(;m.parent&&m.parent.type!=="PAGE";)m=m.parent;let h=m.absoluteTransform[0][2],C=m.absoluteTransform[1][2],k=h,N=C+m.height+50,y=figma.createSection();y.name=`FigmaLint: ${n} Variants`,f.appendChild(y),y.x=k,y.y=N;let b=figma.createText();await figma.loadFontAsync({family:"Inter",style:"Medium"}),b.fontName={family:"Inter",style:"Medium"},b.characters=`New "${n}" variants \u2014 drag into the ComponentSet`,b.fontSize=14,b.fills=[{type:"SOLID",color:{r:.4,g:.4,b:.4}}],y.appendChild(b),b.x=24,b.y=24;let I=24,w=32,P=b.y+b.height+24,O=b.width+I*2;for(let M of u){let z=`${n}=${M}`,x=figma.createText();await figma.loadFontAsync({family:"Inter",style:"Semi Bold"}),x.fontName={family:"Inter",style:"Semi Bold"},x.characters=`${n}=${M}`,x.fontSize=12,x.fills=[{type:"SOLID",color:{r:.6,g:.3,b:.9}}],y.appendChild(x),x.x=I,x.y=P,P+=x.height+12;let $=I,v=0;for(let T of p){let L=T.clone();L.name=L.name.replace(g,z),y.appendChild(L),L.x=$,L.y=P,$+=L.width+w,v=Math.max(v,L.height)}O=Math.max(O,$-w+I),P+=v+w}y.resizeWithoutConstraints(Math.max(O,400),P+I),d=" \u2014 new variants created in staging section to the right"}S("property-added",{success:!0,propertyName:n,message:`Property "${n}" added successfully${d}`}),figma.notify(`Property "${n}" added${d?" (see staging section)":""}`,{timeout:3e3})}catch(t){console.error("Error adding component property:",t);let n=t instanceof Error?t.message:"Unknown error occurred";S("property-added",{success:!1,propertyName:e.propertyName,message:n}),figma.notify(`Failed to add property: ${n}`,{error:!0})}}async function Ua(){try{let e=await qt();S("variable-system-result",e)}catch(e){console.error("Error collecting variables:",e);let t=e instanceof Error?e.message:"Unknown error";S("variable-system-error",{error:t})}}async function Ga(e){try{let t=ro(e.dtcgJson),n=await qt(),s=ao(n,t,null);S("dtcg-compliance-result",s)}catch(t){console.error("Error checking DTCG compliance:",t);let n=t instanceof Error?t.message:"Unknown error";S("dtcg-compliance-error",{error:n})}}async function za(e){try{let t=await co(e.collectionId);S("mode-comparison-result",t)}catch(t){console.error("Error comparing modes:",t);let n=t instanceof Error?t.message:"Unknown error";S("mode-comparison-error",{error:n})}}function Wa(e){let t=e.settings||j;uo({enabled:!0,debounceMs:e.debounceMs||500,settings:t})}function Ha(){po()}function Ka(e){let t=fo(e.lintResult,e.tokenSummary||null);S("design-debt-result",t)}var ja={width:380,height:600,themeColors:!0};try{figma.showUI(__html__,ja),console.log("\u2705 FigmaLint v2.0 - UI shown successfully")}catch(e){console.log("\u2139\uFE0F UI might already be shown in inspect panel:",e)}figma.ui.onmessage=bo;figma.on("selectionchange",()=>{let e=figma.currentPage.selection;figma.ui.postMessage({type:"selection-changed",data:{hasSelection:e.length>0,nodeId:e.length>0?e[0].id:null,nodeName:e.length>0?e[0].name:null}}),e.length>0&&No()});ko();console.log("\u{1F680} FigmaLint v2.0 initialized with modular architecture");})(); diff --git a/dist/ui.html b/dist/ui.html index 298c20f..40ae399 100644 --- a/dist/ui.html +++ b/dist/ui.html @@ -4,7 +4,7 @@ <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Design Review Chat - - +Layer: **${q.nodeName}** (${q.nodeType})`});const ge=[];q.errorType==="spacing"&&q.property?ge.push({id:`fix-${q.nodeId}`,label:"Fix to nearest",variant:"primary",action:"fix-single-spacing",params:{nodeId:q.nodeId,property:q.property}}):q.errorType==="radius"&&ge.push({id:`fix-radius-${q.nodeId}`,label:"Fix radius to nearest",variant:"primary",action:"fix-single-radius",params:{nodeId:q.nodeId}}),ge.push({id:`skip-${R}`,label:R+1{c.addMessage({kind:"ai-text",content:"Full report copied to clipboard!"})},()=>{c.addMessage({kind:"ai-text",content:"Failed to copy report to clipboard."})})}break}case"export-json":{const p=c.lintResult;if(p){const R={component:v||"Component",timestamp:new Date().toISOString(),lint:{summary:p.summary,errors:p.errors,issuesFixed:c.issuesFixed},aiReview:c.aiReview||void 0,diff:c.lastDiff||void 0};navigator.clipboard.writeText(JSON.stringify(R,null,2)).then(()=>c.addMessage({kind:"ai-text",content:"JSON report copied to clipboard!"}),()=>c.addMessage({kind:"ai-text",content:"Failed to copy JSON to clipboard."}))}break}case"save-baseline":{if(!c.score||!c.lintResult){c.addMessage({kind:"ai-text",content:"Run an analysis first before saving a baseline."});break}const p=H.current;if(!p)break;o("save-baseline",{nodeId:p,nodeName:v||"Component",overall:c.score.overall,grade:c.score.grade,categories:{tokens:c.score.tokens,spacing:c.score.spacing,layout:c.score.layout,accessibility:c.score.accessibility,naming:c.score.naming,visualQuality:c.score.visualQuality,microcopy:c.score.microcopy,conversion:c.score.conversion,cognitive:c.score.cognitive},errors:c.lintResult.errors.map(R=>({errorType:R.errorType,severity:R.severity,nodeId:R.nodeId,message:R.message})),summary:c.lintResult.summary});break}case"compare-baseline":{if(!c.score||!c.lintResult){c.addMessage({kind:"ai-text",content:"Run an analysis first before comparing."});break}const p=H.current;if(!p)break;o("compare-baseline",{nodeId:p,overall:c.score.overall,grade:c.score.grade,categories:{tokens:c.score.tokens,spacing:c.score.spacing,layout:c.score.layout,accessibility:c.score.accessibility,naming:c.score.naming,visualQuality:c.score.visualQuality,microcopy:c.score.microcopy,conversion:c.score.conversion,cognitive:c.score.cognitive},errors:c.lintResult.errors.map(R=>({errorType:R.errorType,severity:R.severity,nodeId:R.nodeId,message:R.message})),summary:c.lintResult.summary});break}case"analyze-flow":{c.addMessage({kind:"ai-text",content:"Starting flow analysis on current page..."}),o("analyze-flow");break}case"analyze-page":{c.addMessage({kind:"ai-text",content:"Starting whole-page sweep..."}),o("analyze-page");break}case"toggle-mode":{const p=A==="quick"?"deep":"quick";m(p),c.addMessage({kind:"ai-text",content:`Analysis mode: **${p}**. ${p==="deep"?"Refero comparison will be included in the initial response.":"Refero data loads in the background."}`});break}}},[c,o,v,A]),Ut=k.useCallback(J=>{o("jump-to-node",{nodeId:J})},[o]);return i.jsxs("div",{className:"h-full flex flex-col relative",children:[_&&i.jsx(Hx,{hasApiKey:h,analysisMode:A,backendAvailable:j,onSaveApiKey:(J,G)=>o("save-api-key",{apiKey:J,provider:G}),onClearApiKey:()=>{o("clear-api-key"),g(!1)},onToggleMode:()=>{m(A==="quick"?"deep":"quick")},onClose:()=>w(!1)}),c.messages.length===0&&!c.isAnalyzing&&i.jsxs("div",{className:"flex items-center gap-2 px-3 py-2 border-b border-border",children:[i.jsx("button",{className:"flex-1 py-2 bg-bg-brand text-fg-onbrand text-12 font-medium rounded-md hover:opacity-90 transition-opacity",onClick:_t,children:"Analyze Selection"}),i.jsx("button",{className:"flex-1 py-2 bg-bg-secondary text-fg text-12 font-medium rounded-md hover:bg-bg-hover transition-colors border border-border",onClick:()=>Qe("analyze-flow"),children:"Analyze Flow"}),i.jsx("button",{className:"flex-1 py-2 bg-bg-secondary text-fg text-12 font-medium rounded-md hover:bg-bg-hover transition-colors border border-border",onClick:()=>Qe("analyze-page"),title:"Sweep all top-level frames on the page",children:"Sweep Page"}),i.jsx("button",{onClick:()=>w(!0),className:"shrink-0 w-8 h-8 flex items-center justify-center text-fg-tertiary hover:text-fg rounded-md hover:bg-bg-hover transition-colors",title:"Settings",children:i.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2","aria-hidden":"true",children:[i.jsx("circle",{cx:"12",cy:"12",r:"3"}),i.jsx("path",{d:"M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"})]})})]}),Z&&i.jsxs("div",{className:"flex items-center gap-2 px-3 py-1.5 bg-bg-warning text-fg-warning text-11 border-b border-border",children:[i.jsxs("span",{className:"flex-1",children:["Selection changed",se?` to "${se}"`:"",". Results may be stale."]}),i.jsx("button",{className:"shrink-0 px-2 py-0.5 bg-bg-brand text-fg-onbrand text-11 font-medium rounded hover:opacity-90",onClick:_t,children:"Re-analyze"})]}),i.jsx(Ux,{state:c,componentName:v,analysisMode:A,miniScore:ve,onAnalyze:_t,onSendMessage:ht,onAction:Qe,onJumpToNode:Ut,onOpenSettings:()=>w(!0)})]})}function t0(c,o,v,f,h){const g=[`# Design Review Report: ${o||"Component"}`,"",`Total lint issues: ${c.summary.totalErrors} across ${c.summary.nodesWithErrors} layers`,...v?[`Fixed: ${v}`]:[],""];g.push("## Lint Issues","");const j=c.summary.byType;if(j.fill>0&&g.push(`- **Fill styles:** ${j.fill} missing`),j.stroke>0&&g.push(`- **Stroke styles:** ${j.stroke} missing`),j.effect>0&&g.push(`- **Effect styles:** ${j.effect} missing`),j.text>0&&g.push(`- **Text styles:** ${j.text} missing`),j.radius>0&&g.push(`- **Border radius:** ${j.radius} non-standard`),j.spacing>0&&g.push(`- **Spacing:** ${j.spacing} off-grid`),j.autoLayout>0&&g.push(`- **Auto Layout:** ${j.autoLayout} missing`),j.visualQuality>0&&g.push(`- **Visual Quality:** ${j.visualQuality} issues`),j.microcopy>0&&g.push(`- **Microcopy:** ${j.microcopy} issues`),f){g.push("","## AI Design Review",""),g.push("| Category | Rating |"),g.push("|----------|--------|"),g.push(`| Visual Hierarchy | ${f.visualHierarchy.rating.toUpperCase()} |`),g.push(`| States Coverage | ${f.statesCoverage.rating.toUpperCase()} |`),g.push(`| Platform Alignment | ${f.platformAlignment.rating.toUpperCase()} (${f.platformAlignment.detectedPlatform}) |`),g.push(`| Color Harmony | ${f.colorHarmony.rating.toUpperCase()} |`),f.visualBalance&&g.push(`| Visual Balance | ${f.visualBalance.rating.toUpperCase()} |`),f.microcopyQuality&&g.push(`| Microcopy Quality | ${f.microcopyQuality.rating.toUpperCase()} |`),f.cognitiveLoad&&g.push(`| Cognitive Load | ${f.cognitiveLoad.rating.toUpperCase()} |`);const z=f.statesCoverage?.missingStates||[];if(z.length>0&&g.push("",`**Missing states:** ${z.join(", ")}`),f.recommendations.length>0){g.push("","### Recommendations","");for(const A of f.recommendations)g.push(`- **[${A.severity.toUpperCase()}]** ${A.title}: ${A.description}`)}f.summary&&g.push("",`> ${f.summary}`)}if(h){g.push("","## Baseline Comparison","");const z=h.scoreDelta.overall,A=z>0?"+":"";g.push(`Score: ${h.scoreDelta.oldOverall} → ${h.scoreDelta.newOverall} (${A}${z})`),g.push(`Baseline from: ${new Date(h.baselineTimestamp).toLocaleString()}`),g.push(""),h.summary.totalFixed>0&&g.push(`- **Fixed:** ${h.summary.totalFixed} issues`),h.summary.totalNew>0&&g.push(`- **New:** ${h.summary.totalNew} issues`),g.push(`- **Remaining:** ${h.summary.totalRemaining} issues`);const m=h.scoreDelta.categories.filter(_=>_.delta!==0);if(m.length>0){g.push("","| Category | Before | After | Delta |"),g.push("|----------|--------|-------|-------|");for(const _ of m){const w=_.delta>0?`+${_.delta}`:`${_.delta}`;g.push(`| ${_.category} | ${_.oldScore} | ${_.newScore} | ${w} |`)}}}if(c.errors.length>0){g.push("","## All Issues","");for(const z of c.errors)g.push(`- **[${z.errorType.toUpperCase()}]** ${z.nodeName}: ${z.message}`)}return g.join(` +`)}function Pd(c){const o={critical:10,warning:3,info:1},v=c.frames.map(m=>{const _=m.lintResult.errors,w=Math.max(m.lintResult.summary.totalNodes,1),Z=_.reduce((H,B)=>H+(o[B.severity||"warning"]||3),0),ee=Math.max(0,w-_.length)*10,se=ee+Z,I=se>0?Math.round(ee/se*100):100,ve={};for(const H of _)ve[H.errorType]=(ve[H.errorType]||0)+1;const U=Object.entries(ve).sort((H,B)=>B[1]-H[1]).slice(0,3).map(([H,B])=>`${H} (${B})`);return{id:m.id,name:m.name,score:I,issueCount:m.lintResult.summary.totalErrors,topIssues:U}}),f=v.map(m=>m.score),h=f.length>0?Math.round(f.reduce((m,_)=>m+_,0)/f.length):100,g=h,j=f.length>0?f.reduce((m,_)=>m+Math.pow(_-g,2),0)/f.length:0,z=Math.max(0,Math.round(100-Math.sqrt(j))),A=h>=90?"excellent":h>=70?"needs-work":"poor";return{fileHealth:{overallScore:h,grade:A,totalFrames:c.aggregated.totalFrames,totalIssues:c.aggregated.totalIssues,topIssues:c.aggregated.topIssues,consistencyScore:z},frames:v,aiInsights:{strengths:[],weaknesses:[],recommendations:[],summary:"AI analysis unavailable. Scores are based on deterministic lint rules only."}}}Uh.createRoot(document.getElementById("root")).render(i.jsx(zh.StrictMode,{children:i.jsx(e0,{})})); +
diff --git a/src/code.ts b/src/code.ts index c37414f..1844dc5 100644 --- a/src/code.ts +++ b/src/code.ts @@ -1,6 +1,6 @@ /// -import { handleUIMessage, initializePlugin } from './ui/message-handler'; +import { handleUIMessage, initializePlugin, quickLintSelectedNode } from './ui/message-handler'; // Plugin configuration const PLUGIN_WINDOW_SIZE = { width: 380, height: 600, themeColors: true }; @@ -27,6 +27,11 @@ figma.on('selectionchange', () => { nodeName: sel.length > 0 ? sel[0].name : null, }, }); + + // Ambient quality badge — quick lint on selection change + if (sel.length > 0) { + quickLintSelectedNode(); + } }); // Initialize plugin diff --git a/src/lint/component-props.ts b/src/lint/component-props.ts new file mode 100644 index 0000000..ba3e528 --- /dev/null +++ b/src/lint/component-props.ts @@ -0,0 +1,312 @@ +/// + +import { LintIssue } from './types'; + +// ────────────────────────────────────────────── +// Component Property Lint Module +// +// Deterministic checks for component property hygiene: +// - Too many boolean properties (>5 suggests variant pattern) +// - Missing property description +// - Inconsistent variant naming (mixed casing) +// - Unused variant values +// - Property names with spaces +// ────────────────────────────────────────────── + +let issueCounter = 0; +function nextId(): string { + return `cprop-${++issueCounter}`; +} + +export interface ComponentPropsLintResult { + issues: LintIssue[]; + summary: { + totalChecked: number; + booleanOveruse: number; + missingDescription: number; + inconsistentNaming: number; + unusedVariants: number; + spacedNames: number; + }; +} + +// ── Max booleans before suggesting variants ── +const MAX_BOOLEAN_PROPS = 5; + +// ── Helpers ── + +/** + * Detect whether a set of strings uses mixed casing conventions. + * Returns true if values contain a mix of patterns (e.g., "Primary" + "secondary"). + */ +function hasMixedCasing(values: string[]): boolean { + if (values.length < 2) return false; + + const patterns = new Set(); + for (const v of values) { + const trimmed = v.trim(); + if (trimmed.length === 0) continue; + + if (trimmed === trimmed.toUpperCase()) { + patterns.add('UPPER'); + } else if (trimmed === trimmed.toLowerCase()) { + patterns.add('lower'); + } else if (trimmed[0] === trimmed[0].toUpperCase() && trimmed.slice(1) !== trimmed.slice(1).toUpperCase()) { + patterns.add('Title'); + } else { + patterns.add('mixed'); + } + } + + return patterns.size > 1; +} + +/** + * Check whether a property name contains spaces (should be camelCase or kebab-case). + */ +function hasSpaces(name: string): boolean { + // Strip the Figma auto-appended ID suffix (e.g., "My Prop#1234:5") + const baseName = name.includes('#') ? name.substring(0, name.indexOf('#')) : name; + return /\s/.test(baseName.trim()); +} + +// ── Individual checks ── + +function checkTooManyBooleans( + node: SceneNode, + propDefs: Record, + issues: LintIssue[] +): void { + const booleanProps = Object.entries(propDefs).filter( + ([_key, def]) => def?.type === 'BOOLEAN' + ); + + if (booleanProps.length > MAX_BOOLEAN_PROPS) { + issues.push({ + id: nextId(), + type: 'naming', + severity: 'info', + nodeId: node.id, + nodeName: node.name, + message: `Component has ${booleanProps.length} boolean properties (>${MAX_BOOLEAN_PROPS}) — consider using variants instead`, + currentValue: `${booleanProps.length} booleans`, + suggestions: ['Group related booleans into a single variant property'], + autoFixable: false, + }); + } +} + +function checkMissingDescription( + node: SceneNode, + propDefs: Record, + issues: LintIssue[] +): void { + for (const [propName, def] of Object.entries(propDefs)) { + if (!def) continue; + const desc = (def as Record).description; + if (desc === undefined || desc === null || desc === '') { + const baseName = propName.includes('#') ? propName.substring(0, propName.indexOf('#')) : propName; + issues.push({ + id: nextId(), + type: 'naming', + severity: 'warning', + nodeId: node.id, + nodeName: node.name, + message: `Property "${baseName}" has no description — consumers may not understand its purpose`, + currentValue: `${baseName}: (no description)`, + suggestions: ['Add a short description explaining what this property controls'], + autoFixable: false, + }); + } + } +} + +function checkInconsistentVariantNaming( + node: SceneNode, + propDefs: Record, + issues: LintIssue[] +): void { + // Only relevant for component sets with VARIANT properties + if (node.type !== 'COMPONENT_SET') return; + + for (const [propName, def] of Object.entries(propDefs)) { + if (!def || def.type !== 'VARIANT') continue; + const options = def.variantOptions; + if (!Array.isArray(options) || options.length < 2) continue; + + if (hasMixedCasing(options)) { + const baseName = propName.includes('#') ? propName.substring(0, propName.indexOf('#')) : propName; + issues.push({ + id: nextId(), + type: 'naming', + severity: 'warning', + nodeId: node.id, + nodeName: node.name, + message: `Variant property "${baseName}" has inconsistent casing: ${options.map(o => `"${o}"`).join(', ')}`, + currentValue: options.join(', '), + suggestions: ['Use a consistent naming convention (e.g., all lowercase or all Title Case)'], + autoFixable: false, + }); + } + } +} + +function checkUnusedVariantValues( + node: SceneNode, + propDefs: Record, + issues: LintIssue[] +): void { + // Only relevant for component sets + if (node.type !== 'COMPONENT_SET') return; + + const componentSet = node as ComponentSetNode; + const children = componentSet.children; + if (!Array.isArray(children) || children.length === 0) return; + + // Parse variant property values from child component names + // Child names are like "Property=Value, Property2=Value2" + const usedValues: Record> = {}; + + for (const child of children) { + const pairs = child.name.split(',').map((s: string) => s.trim()); + for (const pair of pairs) { + const eqIdx = pair.indexOf('='); + if (eqIdx === -1) continue; + const key = pair.substring(0, eqIdx).trim(); + const val = pair.substring(eqIdx + 1).trim(); + if (!usedValues[key]) usedValues[key] = new Set(); + usedValues[key].add(val); + } + } + + for (const [propName, def] of Object.entries(propDefs)) { + if (!def || def.type !== 'VARIANT') continue; + const options = def.variantOptions; + if (!Array.isArray(options)) continue; + + const baseName = propName.includes('#') ? propName.substring(0, propName.indexOf('#')) : propName; + const used = usedValues[baseName]; + + for (const option of options) { + if (!used?.has(option)) { + issues.push({ + id: nextId(), + type: 'naming', + severity: 'info', + nodeId: node.id, + nodeName: node.name, + message: `Variant value "${option}" of "${baseName}" is defined but no child component uses it`, + currentValue: `${baseName}=${option}`, + suggestions: ['Remove the unused variant value or add a variant component for it'], + autoFixable: false, + }); + } + } + } +} + +function checkPropertyNameSpaces( + node: SceneNode, + propDefs: Record, + issues: LintIssue[] +): void { + for (const [propName, def] of Object.entries(propDefs)) { + if (!def) continue; + // Skip VARIANT types since those are user-visible labels + if (def.type === 'VARIANT') continue; + + if (hasSpaces(propName)) { + const baseName = propName.includes('#') ? propName.substring(0, propName.indexOf('#')) : propName; + issues.push({ + id: nextId(), + type: 'naming', + severity: 'info', + nodeId: node.id, + nodeName: node.name, + message: `Property name "${baseName}" contains spaces — use camelCase or kebab-case instead`, + currentValue: baseName, + suggestions: [ + baseName.replace(/\s+(.)/g, (_match, c: string) => c.toUpperCase()), + baseName.replace(/\s+/g, '-').toLowerCase(), + ], + autoFixable: false, + }); + } + } +} + +// ── Traversal ── + +function traverseForComponentProps( + node: SceneNode, + issues: LintIssue[], + skipLocked: boolean, + skipHidden: boolean, + parentLocked: boolean +): number { + const isLocked = parentLocked || ('locked' in node && (node as any).locked === true); + const isHidden = 'visible' in node && !node.visible; + + if (skipLocked && isLocked) return 0; + if (skipHidden && isHidden) return 0; + + let checked = 0; + + // Check ComponentNode and ComponentSetNode + if (node.type === 'COMPONENT' || node.type === 'COMPONENT_SET') { + const propDefs = (node as any).componentPropertyDefinitions as + | Record + | undefined; + + if (propDefs && typeof propDefs === 'object') { + checked++; + + checkTooManyBooleans(node, propDefs, issues); + checkMissingDescription(node, propDefs, issues); + checkInconsistentVariantNaming(node, propDefs, issues); + checkUnusedVariantValues(node, propDefs, issues); + checkPropertyNameSpaces(node, propDefs, issues); + } + } + + // Recurse into children + if ('children' in node) { + const children = (node as any).children; + if (Array.isArray(children)) { + for (const child of children as SceneNode[]) { + checked += traverseForComponentProps(child, issues, skipLocked, skipHidden, isLocked); + } + } + } + + return checked; +} + +// ── Public API ── + +export function checkComponentProps( + nodes: readonly SceneNode[], + options: { skipLocked?: boolean; skipHidden?: boolean } = {} +): ComponentPropsLintResult { + const { skipLocked = true, skipHidden = true } = options; + issueCounter = 0; + + const issues: LintIssue[] = []; + let totalChecked = 0; + + for (const node of nodes) { + totalChecked += traverseForComponentProps(node, issues, skipLocked, skipHidden, false); + } + + return { + issues, + summary: { + totalChecked, + booleanOveruse: issues.filter(i => i.message.includes('boolean properties')).length, + missingDescription: issues.filter(i => i.message.includes('no description')).length, + inconsistentNaming: issues.filter(i => i.message.includes('inconsistent casing')).length, + unusedVariants: issues.filter(i => i.message.includes('no child component uses it')).length, + spacedNames: issues.filter(i => i.message.includes('contains spaces')).length, + }, + }; +} diff --git a/src/lint/constraints.ts b/src/lint/constraints.ts new file mode 100644 index 0000000..ba24b6f --- /dev/null +++ b/src/lint/constraints.ts @@ -0,0 +1,331 @@ +/// + +import { LintIssue, LintSeverity } from './types'; + +// ────────────────────────────────────────────── +// Constraints Validation Module +// +// Deterministic checks for node constraints: +// - No constraints set (default MIN/MIN) in fixed frames +// - SCALE on text nodes (distortion risk) +// - Conflicting constraints (STRETCH + fixed width) +// - Constraints in auto-layout (ignored, confusing) +// ────────────────────────────────────────────── + +let issueCounter = 0; +function nextId(): string { + return `constraints-${++issueCounter}`; +} + +export interface ConstraintsLintResult { + issues: LintIssue[]; + summary: { + totalChecked: number; + noConstraints: number; + scaleOnText: number; + conflicting: number; + ignoredInAutoLayout: number; + }; +} + +// ── Helpers ───────────────────────────────────── + +function isFrameLike(node: SceneNode): boolean { + return node.type === 'FRAME' || node.type === 'COMPONENT' || node.type === 'INSTANCE'; +} + +function hasAutoLayout(node: SceneNode): boolean { + if (!isFrameLike(node)) return false; + return (node as unknown as FrameNode).layoutMode !== 'NONE'; +} + +function getConstraints(node: SceneNode): { horizontal: string; vertical: string } | null { + const raw = (node as any)?.constraints; + if (raw && typeof raw.horizontal === 'string' && typeof raw.vertical === 'string') { + return raw as { horizontal: string; vertical: string }; + } + return null; +} + +function pushIssue( + issues: LintIssue[], + severity: LintSeverity, + nodeId: string, + nodeName: string, + message: string, + currentValue?: string, + suggestions?: string[], +): void { + issues.push({ + id: nextId(), + type: 'autoLayout', + severity, + nodeId, + nodeName, + message, + currentValue, + suggestions, + autoFixable: false, + }); +} + +// ── Check: No constraints set (default MIN/MIN) ── + +function checkDefaultConstraints( + node: SceneNode, + parent: SceneNode, + issues: LintIssue[], +): boolean { + // Only relevant for children of non-auto-layout frames + if (hasAutoLayout(parent)) return false; + + const constraints = getConstraints(node); + if (!constraints) return false; + + if (constraints.horizontal === 'MIN' && constraints.vertical === 'MIN') { + pushIssue( + issues, + 'warning', + node.id, + node.name, + `Default constraints (MIN, MIN) inside fixed frame "${parent.name}" — will break on resize`, + 'MIN, MIN', + ['STRETCH', 'CENTER', 'MAX'], + ); + return true; + } + + return false; +} + +// ── Check: SCALE on text ── + +function checkScaleOnText( + node: SceneNode, + issues: LintIssue[], +): boolean { + if (node.type !== 'TEXT') return false; + + const constraints = getConstraints(node); + if (!constraints) return false; + + const hasScale = constraints.horizontal === 'SCALE' || constraints.vertical === 'SCALE'; + if (hasScale) { + const axis = constraints.horizontal === 'SCALE' ? 'horizontal' : 'vertical'; + pushIssue( + issues, + 'critical', + node.id, + node.name, + `SCALE constraint on text node will distort text — use MIN or STRETCH instead`, + `SCALE (${axis})`, + ['MIN', 'STRETCH'], + ); + return true; + } + + return false; +} + +// ── Check: Conflicting constraints (STRETCH + fixed dimension) ── + +function checkConflictingConstraints( + node: SceneNode, + issues: LintIssue[], +): boolean { + const constraints = getConstraints(node); + if (!constraints) return false; + + const width = (node as any)?.width; + const height = (node as any)?.height; + let flagged = false; + + // Check for STRETCH horizontal with a fixed width + // We detect "fixed width" by checking if the node does NOT have layoutGrow + // and is not in an auto-layout parent (which would override width). + if (constraints.horizontal === 'STRETCH' && typeof width === 'number' && width > 0) { + // Check if the node has an explicit size constraint set + const parent = node.parent; + const parentIsFixed = parent && isFrameLike(parent as SceneNode) && !hasAutoLayout(parent as SceneNode); + + if (parentIsFixed) { + pushIssue( + issues, + 'warning', + node.id, + node.name, + `STRETCH horizontal constraint but node has explicit width ${Math.round(width)}px — potentially contradictory`, + `STRETCH + ${Math.round(width)}px wide`, + ['Remove fixed width or change constraint to MIN/CENTER'], + ); + flagged = true; + } + } + + if (constraints.vertical === 'STRETCH' && typeof height === 'number' && height > 0) { + const parent = node.parent; + const parentIsFixed = parent && isFrameLike(parent as SceneNode) && !hasAutoLayout(parent as SceneNode); + + if (parentIsFixed) { + pushIssue( + issues, + 'warning', + node.id, + node.name, + `STRETCH vertical constraint but node has explicit height ${Math.round(height)}px — potentially contradictory`, + `STRETCH + ${Math.round(height)}px tall`, + ['Remove fixed height or change constraint to MIN/CENTER'], + ); + flagged = true; + } + } + + return flagged; +} + +// ── Check: Constraints in auto-layout parent (ignored) ── + +function checkConstraintsInAutoLayout( + node: SceneNode, + parent: SceneNode, + issues: LintIssue[], +): boolean { + if (!hasAutoLayout(parent)) return false; + + // Skip absolutely positioned children — they DO respect constraints + const positioning = (node as any)?.layoutPositioning; + if (positioning === 'ABSOLUTE') return false; + + const constraints = getConstraints(node); + if (!constraints) return false; + + // Only flag if constraints are non-default (someone explicitly set them) + const isDefault = constraints.horizontal === 'MIN' && constraints.vertical === 'MIN'; + if (isDefault) return false; + + pushIssue( + issues, + 'info', + node.id, + node.name, + `Explicit constraints (${constraints.horizontal}, ${constraints.vertical}) inside auto-layout parent "${parent.name}" — constraints are ignored`, + `${constraints.horizontal}, ${constraints.vertical}`, + ['Remove explicit constraints or switch parent to fixed layout'], + ); + + return true; +} + +// ── Recursive traversal ───────────────────────── + +interface TraversalStats { + totalChecked: number; + noConstraints: number; + scaleOnText: number; + conflicting: number; + ignoredInAutoLayout: number; +} + +function emptyStats(): TraversalStats { + return { + totalChecked: 0, + noConstraints: 0, + scaleOnText: 0, + conflicting: 0, + ignoredInAutoLayout: 0, + }; +} + +function traverse( + node: SceneNode, + issues: LintIssue[], + skipLocked: boolean, + skipHidden: boolean, + parentLocked: boolean, + parentNode: SceneNode | null, +): TraversalStats { + const isLocked = parentLocked || ('locked' in node && (node as any).locked); + const isHidden = 'visible' in node && !node.visible; + + if (skipLocked && isLocked) return emptyStats(); + if (skipHidden && isHidden) return emptyStats(); + + const stats = emptyStats(); + + // Only check nodes that have a parent frame context + if (parentNode && isFrameLike(parentNode)) { + const constraints = getConstraints(node); + if (constraints) { + stats.totalChecked++; + + if (checkScaleOnText(node, issues)) { + stats.scaleOnText++; + } + + if (checkDefaultConstraints(node, parentNode, issues)) { + stats.noConstraints++; + } + + if (checkConflictingConstraints(node, issues)) { + stats.conflicting++; + } + + if (checkConstraintsInAutoLayout(node, parentNode, issues)) { + stats.ignoredInAutoLayout++; + } + } + } + + // Recurse into children + if ('children' in node) { + for (const child of (node as any).children as SceneNode[]) { + const sub = traverse(child, issues, skipLocked, skipHidden, isLocked, node); + stats.totalChecked += sub.totalChecked; + stats.noConstraints += sub.noConstraints; + stats.scaleOnText += sub.scaleOnText; + stats.conflicting += sub.conflicting; + stats.ignoredInAutoLayout += sub.ignoredInAutoLayout; + } + } + + return stats; +} + +// ── Public API ────────────────────────────────── + +/** + * Run constraints validation on the given nodes. + * Checks for default constraints in fixed frames, SCALE on text, + * conflicting STRETCH + fixed dimension, and ignored constraints in auto-layout. + */ +export function checkConstraints( + nodes: readonly SceneNode[], + opts?: { settings?: { skipLockedLayers?: boolean; skipHiddenLayers?: boolean } }, +): ConstraintsLintResult { + const skipLocked = opts?.settings?.skipLockedLayers ?? true; + const skipHidden = opts?.settings?.skipHiddenLayers ?? true; + issueCounter = 0; + + const issues: LintIssue[] = []; + const totals = emptyStats(); + + for (const node of nodes) { + const sub = traverse(node, issues, skipLocked, skipHidden, false, null); + totals.totalChecked += sub.totalChecked; + totals.noConstraints += sub.noConstraints; + totals.scaleOnText += sub.scaleOnText; + totals.conflicting += sub.conflicting; + totals.ignoredInAutoLayout += sub.ignoredInAutoLayout; + } + + return { + issues, + summary: { + totalChecked: totals.totalChecked, + noConstraints: totals.noConstraints, + scaleOnText: totals.scaleOnText, + conflicting: totals.conflicting, + ignoredInAutoLayout: totals.ignoredInAutoLayout, + }, + }; +} diff --git a/src/lint/grid-check.ts b/src/lint/grid-check.ts new file mode 100644 index 0000000..7116bc6 --- /dev/null +++ b/src/lint/grid-check.ts @@ -0,0 +1,386 @@ +/// + +import { LintIssue, DEFAULT_SPACING_SCALE } from './types'; + +// ────────────────────────────────────────────── +// Grid System Check Module +// +// Deterministic checks for grid usage across frames: +// - No grid on top-level frames +// - Inconsistent grid columns across top-level frames +// - Grid not from a style (hard-coded) +// - Grid gutter mismatch with spacing scale +// ────────────────────────────────────────────── + +let issueCounter = 0; +function nextId(): string { + return `grid-${++issueCounter}`; +} + +export interface GridCheckLintResult { + issues: LintIssue[]; + summary: { + totalFrames: number; + framesWithGrid: number; + framesWithoutGrid: number; + inconsistentColumns: number; + hardCodedGrids: number; + gutterMismatches: number; + }; +} + +// ── Helpers ── + +interface GridInfo { + pattern: string; + count?: number; + gutterSize?: number; + alignment?: string; + sectionSize?: number; + offset?: number; +} + +/** + * Extract grid info from a layout grid, guarding all property access. + */ +function extractGridInfo(grid: unknown): GridInfo | null { + if (!grid || typeof grid !== 'object') return null; + + const g = grid as Record; + const pattern = g.pattern as string | undefined; + + if (!pattern) return null; + + // Skip invisible grids + if (g.visible === false) return null; + + const info: GridInfo = { pattern }; + + if (pattern === 'COLUMNS' || pattern === 'ROWS') { + if (typeof g.count === 'number') info.count = g.count; + if (typeof g.gutterSize === 'number') info.gutterSize = g.gutterSize; + if (typeof g.alignment === 'string') info.alignment = g.alignment; + if (typeof g.sectionSize === 'number') info.sectionSize = g.sectionSize; + if (typeof g.offset === 'number') info.offset = g.offset; + } else if (pattern === 'GRID') { + if (typeof g.sectionSize === 'number') info.sectionSize = g.sectionSize; + } + + return info; +} + +/** + * Determine whether a frame is "top-level" — direct child of the page. + * In Figma, a top-level frame's parent is the page (PageNode), + * or it may be a direct child of the root selection. + */ +function isTopLevelFrame(node: SceneNode): boolean { + const parent = node.parent; + if (!parent) return true; + + // PageNode type check — in the Figma API, page nodes have type "PAGE" + const parentType = (parent as any).type; + if (parentType === 'PAGE') return true; + + // Also treat SECTION children as top-level for grid purposes + if (parentType === 'SECTION') return true; + + return false; +} + +// ── Check functions ── + +function checkNoGridOnTopLevel( + node: SceneNode, + issues: LintIssue[] +): boolean { + const layoutGrids = (node as any).layoutGrids as unknown[] | undefined; + + if (!Array.isArray(layoutGrids) || layoutGrids.length === 0) { + issues.push({ + id: nextId(), + type: 'spacing', + severity: 'info', + nodeId: node.id, + nodeName: node.name, + message: `Top-level frame "${node.name}" has no layout grid attached`, + currentValue: 'No grid', + suggestions: ['Add a layout grid for consistent alignment'], + autoFixable: false, + }); + return false; // no grid + } + + // Check if any grids are visible + const visibleGrids = layoutGrids.filter(g => { + if (!g || typeof g !== 'object') return false; + return (g as Record).visible !== false; + }); + + if (visibleGrids.length === 0) { + issues.push({ + id: nextId(), + type: 'spacing', + severity: 'info', + nodeId: node.id, + nodeName: node.name, + message: `Top-level frame "${node.name}" has layout grids but all are hidden`, + currentValue: `${layoutGrids.length} hidden grid(s)`, + suggestions: ['Enable at least one layout grid for development reference'], + autoFixable: false, + }); + return false; + } + + return true; +} + +function checkGridNotFromStyle( + node: SceneNode, + localGridStyleIds: Set, + issues: LintIssue[] +): void { + const layoutGrids = (node as any).layoutGrids as unknown[] | undefined; + if (!Array.isArray(layoutGrids) || layoutGrids.length === 0) return; + + const gridStyleId = (node as any).gridStyleId as string | undefined; + + // If no style is applied but grids exist, it's hard-coded + if (!gridStyleId || gridStyleId === '') { + issues.push({ + id: nextId(), + type: 'spacing', + severity: 'info', + nodeId: node.id, + nodeName: node.name, + message: `Frame "${node.name}" has a hard-coded layout grid (not from a grid style)`, + currentValue: 'Hard-coded grid', + suggestions: ['Create a grid style and apply it for consistency across frames'], + autoFixable: false, + }); + } else if (localGridStyleIds.size > 0 && !localGridStyleIds.has(gridStyleId)) { + // Style ID exists but doesn't match any local style — may be from a library + // This is fine, don't flag it + } +} + +function checkGridGutterMismatch( + node: SceneNode, + spacingScale: readonly number[], + issues: LintIssue[] +): void { + const layoutGrids = (node as any).layoutGrids as unknown[] | undefined; + if (!Array.isArray(layoutGrids)) return; + + for (const grid of layoutGrids) { + const info = extractGridInfo(grid); + if (!info) continue; + + if (info.gutterSize !== undefined && !spacingScale.includes(info.gutterSize)) { + issues.push({ + id: nextId(), + type: 'spacing', + severity: 'info', + nodeId: node.id, + nodeName: node.name, + message: `Grid gutter ${info.gutterSize}px on "${node.name}" is not in the spacing scale [${spacingScale.join(', ')}]`, + currentValue: `${info.gutterSize}px gutter`, + suggestions: spacingScale + .filter(v => Math.abs(v - (info.gutterSize ?? 0)) <= 8) + .map(v => `${v}px`), + autoFixable: false, + }); + } + } +} + +// ── Collection and traversal ── + +function collectTopLevelFrames( + nodes: readonly SceneNode[], + skipLocked: boolean, + skipHidden: boolean +): SceneNode[] { + const result: SceneNode[] = []; + + function walk(node: SceneNode, parentLocked: boolean): void { + const isLocked = parentLocked || ('locked' in node && (node as any).locked === true); + const isHidden = 'visible' in node && !node.visible; + + if (skipLocked && isLocked) return; + if (skipHidden && isHidden) return; + + if (node.type === 'FRAME' || node.type === 'COMPONENT' || node.type === 'COMPONENT_SET') { + if (isTopLevelFrame(node)) { + result.push(node); + } + } + + // If this is a SECTION, look inside for top-level frames + if (node.type === 'SECTION' || (!isTopLevelFrame(node) && 'children' in node)) { + // Only recurse into sections and non-top-level containers to find top-level frames + } + + // For the initial selection, recurse to find frames that are page-level children + if ('children' in node) { + const children = (node as any).children; + if (Array.isArray(children)) { + for (const child of children as SceneNode[]) { + // Only recurse if the current node is a section or page-like container + const nodeType = node.type; + if (nodeType === 'SECTION' || nodeType === 'GROUP') { + walk(child, isLocked); + } + } + } + } + } + + // For the root selection, treat each selected node as potentially top-level + for (const node of nodes) { + if (node.type === 'FRAME' || node.type === 'COMPONENT' || node.type === 'COMPONENT_SET') { + result.push(node); + } else if ('children' in node) { + const children = (node as any).children; + if (Array.isArray(children)) { + for (const child of children as SceneNode[]) { + walk(child, false); + } + } + } + } + + return result; +} + +function checkInconsistentColumns( + frames: SceneNode[], + issues: LintIssue[] +): void { + // Collect column counts from frames with COLUMNS grids + const columnCounts: Array<{ nodeId: string; nodeName: string; count: number }> = []; + + for (const frame of frames) { + const layoutGrids = (frame as any).layoutGrids as unknown[] | undefined; + if (!Array.isArray(layoutGrids)) continue; + + for (const grid of layoutGrids) { + const info = extractGridInfo(grid); + if (!info || info.pattern !== 'COLUMNS') continue; + + if (info.count !== undefined && isFinite(info.count)) { + columnCounts.push({ + nodeId: frame.id, + nodeName: frame.name, + count: info.count, + }); + } + } + } + + if (columnCounts.length < 2) return; + + // Check for inconsistency + const uniqueCounts = [...new Set(columnCounts.map(c => c.count))]; + if (uniqueCounts.length <= 1) return; + + // Find the most common column count + const countFreq = new Map(); + for (const { count } of columnCounts) { + countFreq.set(count, (countFreq.get(count) ?? 0) + 1); + } + + let mostCommonCount = uniqueCounts[0]; + let maxFreq = 0; + for (const [count, freq] of countFreq) { + if (freq > maxFreq) { + mostCommonCount = count; + maxFreq = freq; + } + } + + // Flag frames that deviate from the most common count + for (const entry of columnCounts) { + if (entry.count !== mostCommonCount) { + issues.push({ + id: nextId(), + type: 'spacing', + severity: 'warning', + nodeId: entry.nodeId, + nodeName: entry.nodeName, + message: `Frame "${entry.nodeName}" uses ${entry.count}-column grid while most frames use ${mostCommonCount} columns`, + currentValue: `${entry.count} columns`, + suggestions: [`Change to ${mostCommonCount} columns for consistency`], + autoFixable: false, + }); + } + } +} + +// ── Async grid style fetching ── + +async function getLocalGridStyleIds(): Promise> { + try { + const api = figma as any; + let styles: Array<{ id: string }> = []; + + if (typeof api.getLocalGridStylesAsync === 'function') { + styles = await (api.getLocalGridStylesAsync as () => Promise>)(); + } else if (typeof api.getLocalGridStyles === 'function') { + styles = (api.getLocalGridStyles as () => Array<{ id: string }>)(); + } + + return new Set((styles ?? []).map(s => s.id)); + } catch { + return new Set(); + } +} + +// ── Public API ── + +export async function checkGrid( + nodes: readonly SceneNode[], + options: { + skipLocked?: boolean; + skipHidden?: boolean; + spacingScale?: readonly number[]; + } = {} +): Promise { + const { skipLocked = true, skipHidden = true, spacingScale = DEFAULT_SPACING_SCALE } = options; + issueCounter = 0; + + const issues: LintIssue[] = []; + + // 1. Collect top-level frames + const topFrames = collectTopLevelFrames(nodes, skipLocked, skipHidden); + + // 2. Fetch local grid styles + const localGridStyleIds = await getLocalGridStyleIds(); + + // 3. Per-frame checks + let framesWithGrid = 0; + + for (const frame of topFrames) { + const hasGrid = checkNoGridOnTopLevel(frame, issues); + if (hasGrid) { + framesWithGrid++; + checkGridNotFromStyle(frame, localGridStyleIds, issues); + checkGridGutterMismatch(frame, spacingScale, issues); + } + } + + // 4. Cross-frame consistency check + checkInconsistentColumns(topFrames, issues); + + return { + issues, + summary: { + totalFrames: topFrames.length, + framesWithGrid, + framesWithoutGrid: topFrames.length - framesWithGrid, + inconsistentColumns: issues.filter(i => i.message.includes('column grid while')).length, + hardCodedGrids: issues.filter(i => i.message.includes('hard-coded')).length, + gutterMismatches: issues.filter(i => i.message.includes('gutter')).length, + }, + }; +} diff --git a/src/lint/layout-sizing.ts b/src/lint/layout-sizing.ts new file mode 100644 index 0000000..795e55e --- /dev/null +++ b/src/lint/layout-sizing.ts @@ -0,0 +1,450 @@ +/// + +import { LintIssue, LintSeverity } from './types'; + +// ────────────────────────────────────────────── +// Layout Sizing Audit Module +// +// Deterministic checks for auto-layout sizing modes: +// - Inconsistent sizing strategy among siblings +// - FIXED sizing inside auto-layout parent +// - layoutGrow mismatch among siblings +// - Absolute positioning in auto-layout +// - Missing min/max constraints on FILL-sized frames +// ────────────────────────────────────────────── + +let issueCounter = 0; +function nextId(): string { + return `layout-sizing-${++issueCounter}`; +} + +export interface LayoutSizingLintResult { + issues: LintIssue[]; + summary: { + totalChecked: number; + inconsistentSizing: number; + fixedInAutoLayout: number; + layoutGrowMismatch: number; + absoluteInAutoLayout: number; + missingConstraints: number; + }; +} + +// ── Helpers ───────────────────────────────────── + +function isFrameLike(node: SceneNode): boolean { + return node.type === 'FRAME' || node.type === 'COMPONENT' || node.type === 'INSTANCE'; +} + +function asFrame(node: SceneNode): FrameNode { + return node as unknown as FrameNode; +} + +function hasAutoLayout(node: SceneNode): boolean { + if (!isFrameLike(node)) return false; + return asFrame(node).layoutMode !== 'NONE'; +} + +function pushIssue( + issues: LintIssue[], + severity: LintSeverity, + nodeId: string, + nodeName: string, + message: string, + currentValue?: string, + suggestions?: string[], +): void { + issues.push({ + id: nextId(), + type: 'autoLayout', + severity, + nodeId, + nodeName, + message, + currentValue, + suggestions, + autoFixable: false, + }); +} + +// ── Check: Inconsistent sizing strategy among siblings ── + +function checkInconsistentSizing(parent: FrameNode, issues: LintIssue[]): number { + if (!('children' in parent)) return 0; + const children = (parent.children as SceneNode[]).filter(c => isFrameLike(c) && c.visible !== false); + if (children.length < 2) return 0; + + let checked = 0; + + // Check primary axis sizing mode consistency + const primaryModes = new Map(); + const counterModes = new Map(); + + for (const child of children) { + const frame = child as unknown as Record; + const primary = (frame['primaryAxisSizingMode'] as string) ?? 'UNKNOWN'; + const counter = (frame['counterAxisSizingMode'] as string) ?? 'UNKNOWN'; + + if (!primaryModes.has(primary)) primaryModes.set(primary, []); + primaryModes.get(primary)!.push(child); + + if (!counterModes.has(counter)) counterModes.set(counter, []); + counterModes.get(counter)!.push(child); + } + + checked += children.length; + + // If there are multiple different primary axis modes, flag minority nodes + if (primaryModes.size > 1) { + let maxCount = 0; + let majorityMode = ''; + for (const [mode, nodes] of primaryModes) { + if (nodes.length > maxCount) { + maxCount = nodes.length; + majorityMode = mode; + } + } + + for (const [mode, nodes] of primaryModes) { + if (mode !== majorityMode) { + for (const node of nodes) { + pushIssue( + issues, + 'warning', + node.id, + node.name, + `Primary axis sizing "${mode}" differs from siblings' "${majorityMode}" in "${parent.name}"`, + mode, + [majorityMode], + ); + } + } + } + } + + // Same for counter axis + if (counterModes.size > 1) { + let maxCount = 0; + let majorityMode = ''; + for (const [mode, nodes] of counterModes) { + if (nodes.length > maxCount) { + maxCount = nodes.length; + majorityMode = mode; + } + } + + for (const [mode, nodes] of counterModes) { + if (mode !== majorityMode) { + for (const node of nodes) { + pushIssue( + issues, + 'warning', + node.id, + node.name, + `Counter axis sizing "${mode}" differs from siblings' "${majorityMode}" in "${parent.name}"`, + mode, + [majorityMode], + ); + } + } + } + } + + return checked; +} + +// ── Check: FIXED sizing inside auto-layout parent ── + +function checkFixedInAutoLayout(parent: FrameNode, issues: LintIssue[]): number { + if (!hasAutoLayout(parent as unknown as SceneNode)) return 0; + if (!('children' in parent)) return 0; + + let checked = 0; + + for (const child of parent.children as SceneNode[]) { + if (child.visible === false) continue; + if (!isFrameLike(child)) continue; + + const frame = child as unknown as Record; + const positioning = frame['layoutPositioning'] as string | undefined; + + // Skip absolutely positioned children — they are handled separately + if (positioning === 'ABSOLUTE') continue; + + checked++; + + const primaryMode = (frame['primaryAxisSizingMode'] as string) ?? undefined; + const counterMode = (frame['counterAxisSizingMode'] as string) ?? undefined; + + if (primaryMode === 'FIXED') { + pushIssue( + issues, + 'warning', + child.id, + child.name, + `FIXED primary axis sizing inside auto-layout parent "${parent.name}" may break layout`, + 'FIXED (primary)', + ['HUG', 'FILL'], + ); + } + + if (counterMode === 'FIXED') { + pushIssue( + issues, + 'warning', + child.id, + child.name, + `FIXED counter axis sizing inside auto-layout parent "${parent.name}" may break layout`, + 'FIXED (counter)', + ['HUG', 'FILL'], + ); + } + } + + return checked; +} + +// ── Check: layoutGrow mismatch ── + +function checkLayoutGrowMismatch(parent: FrameNode, issues: LintIssue[]): number { + if (!hasAutoLayout(parent as unknown as SceneNode)) return 0; + if (parent.layoutMode !== 'HORIZONTAL') return 0; + if (!('children' in parent)) return 0; + + const children = (parent.children as SceneNode[]).filter(c => c.visible !== false); + if (children.length < 2) return 0; + + let checked = 0; + let hasGrow = false; + let hasNoGrow = false; + const growNodes: SceneNode[] = []; + + for (const child of children) { + const frame = child as unknown as Record; + const positioning = frame['layoutPositioning'] as string | undefined; + if (positioning === 'ABSOLUTE') continue; + + checked++; + const grow = (frame['layoutGrow'] as number) ?? 0; + + if (grow === 1) { + hasGrow = true; + growNodes.push(child); + } else { + hasNoGrow = true; + } + } + + if (hasGrow && hasNoGrow) { + for (const node of growNodes) { + pushIssue( + issues, + 'info', + node.id, + node.name, + `layoutGrow: 1 while siblings have layoutGrow: 0 in horizontal layout "${parent.name}" — may cause unexpected stretching`, + 'layoutGrow: 1', + ['Verify stretching is intentional'], + ); + } + } + + return checked; +} + +// ── Check: Absolute positioning in auto-layout ── + +function checkAbsoluteInAutoLayout(parent: FrameNode, issues: LintIssue[]): number { + if (!hasAutoLayout(parent as unknown as SceneNode)) return 0; + if (!('children' in parent)) return 0; + + let checked = 0; + + for (const child of parent.children as SceneNode[]) { + if (child.visible === false) continue; + + const frame = child as unknown as Record; + const positioning = frame['layoutPositioning'] as string | undefined; + + if (positioning === 'ABSOLUTE') { + checked++; + pushIssue( + issues, + 'info', + child.id, + child.name, + `Absolute positioning inside auto-layout parent "${parent.name}" — verify this is intentional`, + 'layoutPositioning: ABSOLUTE', + ['Remove absolute positioning or confirm intentional overlay'], + ); + } + } + + return checked; +} + +// ── Check: Missing min/max constraints on FILL-sized frames ── + +function checkMissingMinMax(node: SceneNode, issues: LintIssue[]): number { + if (!isFrameLike(node)) return 0; + + const frame = node as unknown as Record; + const primaryMode = (frame['primaryAxisSizingMode'] as string) ?? undefined; + const counterMode = (frame['counterAxisSizingMode'] as string) ?? undefined; + + let checked = 0; + + if (primaryMode === 'FILL' || counterMode === 'FILL') { + checked++; + + const minW = (frame['minWidth'] as number | undefined) ?? null; + const maxW = (frame['maxWidth'] as number | undefined) ?? null; + const minH = (frame['minHeight'] as number | undefined) ?? null; + const maxH = (frame['maxHeight'] as number | undefined) ?? null; + + const hasWidthConstraint = (minW !== null && minW > 0) || (maxW !== null && maxW < Infinity && maxW > 0); + const hasHeightConstraint = (minH !== null && minH > 0) || (maxH !== null && maxH < Infinity && maxH > 0); + + if (primaryMode === 'FILL' && !hasWidthConstraint && !hasHeightConstraint) { + pushIssue( + issues, + 'info', + node.id, + node.name, + `FILL sizing without min/max constraints — frame may collapse or overflow`, + 'FILL, no min/max', + ['Add minWidth/maxWidth or minHeight/maxHeight'], + ); + } else if (counterMode === 'FILL' && !hasWidthConstraint && !hasHeightConstraint) { + pushIssue( + issues, + 'info', + node.id, + node.name, + `FILL counter-axis sizing without min/max constraints — frame may collapse or overflow`, + 'FILL (counter), no min/max', + ['Add minWidth/maxWidth or minHeight/maxHeight'], + ); + } + } + + return checked; +} + +// ── Recursive traversal ───────────────────────── + +interface TraversalStats { + totalChecked: number; + inconsistentSizing: number; + fixedInAutoLayout: number; + layoutGrowMismatch: number; + absoluteInAutoLayout: number; + missingConstraints: number; +} + +function traverse( + node: SceneNode, + issues: LintIssue[], + skipLocked: boolean, + skipHidden: boolean, + parentLocked: boolean, +): TraversalStats { + const isLocked = parentLocked || ('locked' in node && (node as any).locked); + const isHidden = 'visible' in node && !node.visible; + + if (skipLocked && isLocked) return emptyStats(); + if (skipHidden && isHidden) return emptyStats(); + + const stats: TraversalStats = emptyStats(); + + if (isFrameLike(node)) { + const frame = asFrame(node); + + const preInconsistent = issues.length; + stats.totalChecked += checkInconsistentSizing(frame, issues); + stats.inconsistentSizing += issues.length - preInconsistent; + + const preFixed = issues.length; + stats.totalChecked += checkFixedInAutoLayout(frame, issues); + stats.fixedInAutoLayout += issues.length - preFixed; + + const preGrow = issues.length; + stats.totalChecked += checkLayoutGrowMismatch(frame, issues); + stats.layoutGrowMismatch += issues.length - preGrow; + + const preAbsolute = issues.length; + stats.totalChecked += checkAbsoluteInAutoLayout(frame, issues); + stats.absoluteInAutoLayout += issues.length - preAbsolute; + + const preMinMax = issues.length; + stats.totalChecked += checkMissingMinMax(node, issues); + stats.missingConstraints += issues.length - preMinMax; + } + + // Recurse into children + if ('children' in node) { + for (const child of (node as any).children as SceneNode[]) { + const sub = traverse(child, issues, skipLocked, skipHidden, isLocked); + stats.totalChecked += sub.totalChecked; + stats.inconsistentSizing += sub.inconsistentSizing; + stats.fixedInAutoLayout += sub.fixedInAutoLayout; + stats.layoutGrowMismatch += sub.layoutGrowMismatch; + stats.absoluteInAutoLayout += sub.absoluteInAutoLayout; + stats.missingConstraints += sub.missingConstraints; + } + } + + return stats; +} + +function emptyStats(): TraversalStats { + return { + totalChecked: 0, + inconsistentSizing: 0, + fixedInAutoLayout: 0, + layoutGrowMismatch: 0, + absoluteInAutoLayout: 0, + missingConstraints: 0, + }; +} + +// ── Public API ────────────────────────────────── + +/** + * Run layout sizing audit on the given nodes. + * Checks sizing mode consistency, FIXED-in-auto-layout, layoutGrow mismatches, + * absolute positioning in auto-layout, and missing min/max constraints. + */ +export function checkLayoutSizing( + nodes: readonly SceneNode[], + opts?: { settings?: { skipLockedLayers?: boolean; skipHiddenLayers?: boolean } }, +): LayoutSizingLintResult { + const skipLocked = opts?.settings?.skipLockedLayers ?? true; + const skipHidden = opts?.settings?.skipHiddenLayers ?? true; + issueCounter = 0; + + const issues: LintIssue[] = []; + const totals = emptyStats(); + + for (const node of nodes) { + const sub = traverse(node, issues, skipLocked, skipHidden, false); + totals.totalChecked += sub.totalChecked; + totals.inconsistentSizing += sub.inconsistentSizing; + totals.fixedInAutoLayout += sub.fixedInAutoLayout; + totals.layoutGrowMismatch += sub.layoutGrowMismatch; + totals.absoluteInAutoLayout += sub.absoluteInAutoLayout; + totals.missingConstraints += sub.missingConstraints; + } + + return { + issues, + summary: { + totalChecked: totals.totalChecked, + inconsistentSizing: totals.inconsistentSizing, + fixedInAutoLayout: totals.fixedInAutoLayout, + layoutGrowMismatch: totals.layoutGrowMismatch, + absoluteInAutoLayout: totals.absoluteInAutoLayout, + missingConstraints: totals.missingConstraints, + }, + }; +} diff --git a/src/lint/multi-theme.ts b/src/lint/multi-theme.ts new file mode 100644 index 0000000..fd1f023 --- /dev/null +++ b/src/lint/multi-theme.ts @@ -0,0 +1,452 @@ +/// + +import { LintIssue } from './types'; +import { getLuminance, getContrastRatio } from '../utils/figma-helpers'; + +// ────────────────────────────────────────────── +// Multi-Theme Validation Module +// +// Validates variable values across all modes simultaneously: +// - Identical values across modes (forgot to set dark mode value) +// - Missing mode values +// - Contrast degradation across modes (light vs dark) +// - Mode count mismatch within a collection +// ────────────────────────────────────────────── + +let issueCounter = 0; +function nextId(): string { + return `mtheme-${++issueCounter}`; +} + +export interface MultiThemeLintResult { + issues: LintIssue[]; + summary: { + totalVariables: number; + identicalAcrossModes: number; + missingModeValues: number; + contrastDegradation: number; + modeCountMismatch: number; + }; +} + +// ── Helpers ── + +interface CollectionInfo { + id: string; + name: string; + modes: Array<{ modeId: string; name: string }>; + variableIds: string[]; +} + +interface VariableInfo { + id: string; + name: string; + resolvedType: string; + variableCollectionId: string; + valuesByMode: Record; +} + +/** + * Compare two variable values for equality. + * Handles primitives, RGB, RGBA, and VariableAlias. + */ +function valuesAreEqual(a: unknown, b: unknown): boolean { + if (a === b) return true; + if (a == null || b == null) return false; + if (typeof a !== typeof b) return false; + + if (typeof a === 'object' && typeof b === 'object') { + const ao = a as Record; + const bo = b as Record; + + // VariableAlias + if (ao.type === 'VARIABLE_ALIAS' && bo.type === 'VARIABLE_ALIAS') { + return ao.id === bo.id; + } + + // RGB / RGBA + if ('r' in ao && 'g' in ao && 'b' in ao && 'r' in bo && 'g' in bo && 'b' in bo) { + const tolerance = 0.001; + if (Math.abs((ao.r as number) - (bo.r as number)) > tolerance) return false; + if (Math.abs((ao.g as number) - (bo.g as number)) > tolerance) return false; + if (Math.abs((ao.b as number) - (bo.b as number)) > tolerance) return false; + if ('a' in ao && 'a' in bo) { + if (Math.abs((ao.a as number) - (bo.a as number)) > tolerance) return false; + } + return true; + } + } + + return false; +} + +/** + * Extract RGB from a variable value (handles RGB, RGBA, ignores aliases). + */ +function extractRgb(value: unknown): { r: number; g: number; b: number } | null { + if (!value || typeof value !== 'object') return null; + const obj = value as Record; + + // Skip aliases — cannot statically resolve + if (obj.type === 'VARIABLE_ALIAS') return null; + + if (typeof obj.r === 'number' && typeof obj.g === 'number' && typeof obj.b === 'number') { + return { r: obj.r as number, g: obj.g as number, b: obj.b as number }; + } + + return null; +} + +/** + * Format a variable value as human-readable string. + */ +function formatValue(value: unknown): string { + if (value == null) return '(undefined)'; + if (typeof value === 'boolean' || typeof value === 'string' || typeof value === 'number') { + return String(value); + } + const obj = value as Record; + if (obj.type === 'VARIABLE_ALIAS') return `alias(${obj.id})`; + if (typeof obj.r === 'number' && typeof obj.g === 'number' && typeof obj.b === 'number') { + const r = Math.round((obj.r as number) * 255); + const g = Math.round((obj.g as number) * 255); + const b = Math.round((obj.b as number) * 255); + return `rgb(${r}, ${g}, ${b})`; + } + return JSON.stringify(value); +} + +// ── Naming convention heuristics for foreground/background pairing ── + +/** + * Attempt to find foreground/background pairs based on naming convention. + * E.g., "fg-primary" pairs with "bg-primary", "text-primary" pairs with "surface-primary". + */ +function findFgBgPairs( + variables: VariableInfo[] +): Array<{ fg: VariableInfo; bg: VariableInfo }> { + const pairs: Array<{ fg: VariableInfo; bg: VariableInfo }> = []; + const colorVars = variables.filter(v => v.resolvedType === 'COLOR'); + + // Build maps for matching + const fgPrefixes = ['fg', 'text', 'foreground', 'on']; + const bgPrefixes = ['bg', 'surface', 'background']; + + const fgVars: Array<{ variable: VariableInfo; suffix: string }> = []; + const bgVars: Array<{ variable: VariableInfo; suffix: string }> = []; + + for (const v of colorVars) { + const nameLower = v.name.toLowerCase(); + const nameParts = nameLower.split(/[-_/]/); + const firstPart = nameParts[0] ?? ''; + const suffix = nameParts.slice(1).join('-'); + + if (fgPrefixes.includes(firstPart) && suffix) { + fgVars.push({ variable: v, suffix }); + } else if (bgPrefixes.includes(firstPart) && suffix) { + bgVars.push({ variable: v, suffix }); + } + } + + // Match pairs by suffix + for (const fg of fgVars) { + const matchingBg = bgVars.find(bg => bg.suffix === fg.suffix); + if (matchingBg) { + pairs.push({ fg: fg.variable, bg: matchingBg.variable }); + } + } + + return pairs; +} + +// ── Async collection/variable fetching ── + +async function getLocalCollections(): Promise { + try { + const vars = figma.variables as any; + const getCollections = vars.getLocalVariableCollectionsAsync; + + let collections: any[]; + if (typeof getCollections === 'function') { + collections = await getCollections(); + } else if (typeof vars.getLocalVariableCollections === 'function') { + collections = vars.getLocalVariableCollections(); + } else { + return []; + } + + return (collections ?? []).map((col: any) => ({ + id: (col?.id as string) ?? '', + name: (col?.name as string) ?? '', + modes: Array.isArray(col?.modes) + ? (col.modes as Array<{ modeId: string; name: string }>) + : [], + variableIds: Array.isArray(col?.variableIds) + ? (col.variableIds as string[]) + : [], + })); + } catch { + return []; + } +} + +async function getVariableInfo(varId: string): Promise { + try { + const vars = figma.variables as any; + let variable: any = null; + + if (typeof vars.getVariableByIdAsync === 'function') { + variable = await vars.getVariableByIdAsync(varId); + } else if (typeof vars.getVariableById === 'function') { + variable = figma.variables.getVariableById(varId); + } + + if (!variable) return null; + + return { + id: varId, + name: (variable.name as string) ?? '', + resolvedType: (variable.resolvedType as string) ?? '', + variableCollectionId: (variable.variableCollectionId as string) ?? '', + valuesByMode: (variable.valuesByMode as Record) ?? {}, + }; + } catch { + return null; + } +} + +// ── Check functions ── + +function checkIdenticalAcrossModes( + variable: VariableInfo, + collection: CollectionInfo, + issues: LintIssue[] +): void { + if (collection.modes.length < 2) return; + + const modeIds = collection.modes.map(m => m.modeId); + const values = modeIds.map(mid => variable.valuesByMode[mid]); + + // Check if all values are identical + const firstValue = values[0]; + const allIdentical = values.every(v => valuesAreEqual(v, firstValue)); + + if (allIdentical && firstValue !== undefined) { + // Only flag COLOR variables as warning (most common multi-theme issue) + // Other types as info since they may be intentionally identical + const severity = variable.resolvedType === 'COLOR' ? 'warning' as const : 'info' as const; + + // Skip if it's only info and not COLOR — too noisy for non-color vars + if (severity === 'info') return; + + const modeNames = collection.modes.map(m => m.name).join(', '); + issues.push({ + id: nextId(), + type: 'naming', + severity, + nodeId: variable.id, + nodeName: variable.name, + message: `Variable "${variable.name}" has identical value across modes [${modeNames}] — may need per-mode values`, + currentValue: formatValue(firstValue), + suggestions: ['Set distinct values for each mode (e.g., light vs dark)'], + autoFixable: false, + }); + } +} + +function checkMissingModeValues( + variable: VariableInfo, + collection: CollectionInfo, + issues: LintIssue[] +): void { + if (collection.modes.length < 2) return; + + const missingModes: string[] = []; + for (const mode of collection.modes) { + const value = variable.valuesByMode[mode.modeId]; + if (value === undefined) { + missingModes.push(mode.name); + } + } + + if (missingModes.length > 0 && missingModes.length < collection.modes.length) { + issues.push({ + id: nextId(), + type: 'naming', + severity: 'critical', + nodeId: variable.id, + nodeName: variable.name, + message: `Variable "${variable.name}" is missing values for modes: ${missingModes.join(', ')}`, + currentValue: `Defined in ${collection.modes.length - missingModes.length}/${collection.modes.length} modes`, + suggestions: [`Add values for: ${missingModes.join(', ')}`], + autoFixable: false, + }); + } +} + +function checkModeCountMismatch( + variable: VariableInfo, + collection: CollectionInfo, + issues: LintIssue[] +): void { + if (collection.modes.length < 2) return; + + const definedModeCount = Object.keys(variable.valuesByMode).filter( + modeId => variable.valuesByMode[modeId] !== undefined + ).length; + + if (definedModeCount > 0 && definedModeCount < collection.modes.length) { + // Only flag if not already caught by checkMissingModeValues + // (this targets edge cases where valuesByMode has extra/different keys) + const definedKeys = new Set(Object.keys(variable.valuesByMode)); + const collectionModeIds = new Set(collection.modes.map(m => m.modeId)); + + // Check for values in modes not in the collection (stale mode IDs) + const staleModes = [...definedKeys].filter(k => !collectionModeIds.has(k)); + if (staleModes.length > 0) { + issues.push({ + id: nextId(), + type: 'naming', + severity: 'warning', + nodeId: variable.id, + nodeName: variable.name, + message: `Variable "${variable.name}" has ${definedModeCount} values but collection "${collection.name}" has ${collection.modes.length} modes`, + currentValue: `${definedModeCount}/${collection.modes.length} modes defined`, + suggestions: ['Ensure all collection modes have values assigned'], + autoFixable: false, + }); + } + } +} + +function checkContrastDegradation( + fgVar: VariableInfo, + bgVar: VariableInfo, + collection: CollectionInfo, + issues: LintIssue[] +): void { + if (collection.modes.length < 2) return; + + const modeResults: Array<{ + modeName: string; + ratio: number; + passes: boolean; + }> = []; + + for (const mode of collection.modes) { + const fgValue = fgVar.valuesByMode[mode.modeId]; + const bgValue = bgVar.valuesByMode[mode.modeId]; + + const fgRgb = extractRgb(fgValue); + const bgRgb = extractRgb(bgValue); + + if (!fgRgb || !bgRgb) continue; + + const fgLum = getLuminance(fgRgb.r, fgRgb.g, fgRgb.b); + const bgLum = getLuminance(bgRgb.r, bgRgb.g, bgRgb.b); + const ratio = getContrastRatio(fgLum, bgLum); + + modeResults.push({ + modeName: mode.name, + ratio, + passes: ratio >= 4.5, + }); + } + + if (modeResults.length < 2) return; + + // Check if contrast passes in some modes but fails in others + const passingModes = modeResults.filter(r => r.passes); + const failingModes = modeResults.filter(r => !r.passes); + + if (passingModes.length > 0 && failingModes.length > 0) { + const failDetails = failingModes + .map(m => `${m.modeName}: ${m.ratio.toFixed(1)}:1`) + .join(', '); + const passDetails = passingModes + .map(m => `${m.modeName}: ${m.ratio.toFixed(1)}:1`) + .join(', '); + + issues.push({ + id: nextId(), + type: 'accessibility', + severity: 'critical', + nodeId: fgVar.id, + nodeName: `${fgVar.name} / ${bgVar.name}`, + message: `Contrast passes in [${passDetails}] but fails in [${failDetails}] — WCAG AA requires 4.5:1`, + currentValue: failDetails, + suggestions: ['Adjust colors so contrast meets WCAG AA in all modes'], + autoFixable: false, + }); + } +} + +// ── Public API ── + +/** + * Run multi-theme validation across all variable collections and modes. + * This is a document-level check that does not require node selection. + * + * @param _nodes - Unused but included for consistent module signature. + */ +export async function checkMultiTheme( + _nodes: readonly SceneNode[], + _options: { skipLocked?: boolean; skipHidden?: boolean } = {} +): Promise { + issueCounter = 0; + const issues: LintIssue[] = []; + + // 1. Get all local collections + const collections = await getLocalCollections(); + if (collections.length === 0) { + return { + issues: [], + summary: { + totalVariables: 0, + identicalAcrossModes: 0, + missingModeValues: 0, + contrastDegradation: 0, + modeCountMismatch: 0, + }, + }; + } + + let totalVariables = 0; + + // 2. Check each collection + for (const collection of collections) { + // Skip single-mode collections — no cross-mode checks possible + if (collection.modes.length < 2) continue; + + const collectionVars: VariableInfo[] = []; + + for (const varId of collection.variableIds) { + const variable = await getVariableInfo(varId); + if (!variable) continue; + + collectionVars.push(variable); + totalVariables++; + + checkIdenticalAcrossModes(variable, collection, issues); + checkMissingModeValues(variable, collection, issues); + checkModeCountMismatch(variable, collection, issues); + } + + // 3. Check contrast degradation for fg/bg pairs + const pairs = findFgBgPairs(collectionVars); + for (const { fg, bg } of pairs) { + checkContrastDegradation(fg, bg, collection, issues); + } + } + + return { + issues, + summary: { + totalVariables, + identicalAcrossModes: issues.filter(i => i.message.includes('identical value across modes')).length, + missingModeValues: issues.filter(i => i.message.includes('missing values for modes')).length, + contrastDegradation: issues.filter(i => i.message.includes('Contrast passes')).length, + modeCountMismatch: issues.filter(i => i.message.includes('modes defined')).length, + }, + }; +} diff --git a/src/lint/style-audit.ts b/src/lint/style-audit.ts new file mode 100644 index 0000000..c7dc70d --- /dev/null +++ b/src/lint/style-audit.ts @@ -0,0 +1,455 @@ +/// + +import { LintIssue, LintSeverity } from './types'; + +// ────────────────────────────────────────────── +// Style Library Audit Module +// +// Deterministic checks for paint/text/effect style usage: +// - Orphaned local styles (defined but unused) +// - Hard-coded color matching an existing style +// - Style applied but overridden (fillStyleId set but fill differs) +// - Duplicate styles (identical color values) +// +// NOTE: This module is async because it uses +// figma.getLocalPaintStylesAsync() and related APIs. +// ────────────────────────────────────────────── + +let issueCounter = 0; +function nextId(): string { + return `style-audit-${++issueCounter}`; +} + +export interface StyleAuditLintResult { + issues: LintIssue[]; + summary: { + totalChecked: number; + orphanedStyles: number; + hardCodedMatches: number; + overriddenStyles: number; + duplicateStyles: number; + }; +} + +// ── Helpers ───────────────────────────────────── + +function pushIssue( + issues: LintIssue[], + severity: LintSeverity, + nodeId: string, + nodeName: string, + message: string, + currentValue?: string, + suggestions?: string[], +): void { + issues.push({ + id: nextId(), + type: 'fill', + severity, + nodeId, + nodeName, + message, + currentValue, + suggestions, + autoFixable: false, + }); +} + +/** + * Convert RGB float (0-1) to a 6-digit hex string for comparison. + */ +function rgbKey(r: number, g: number, b: number, opacity?: number): string { + const ri = Math.round(r * 255); + const gi = Math.round(g * 255); + const bi = Math.round(b * 255); + const hex = `#${ri.toString(16).padStart(2, '0')}${gi.toString(16).padStart(2, '0')}${bi.toString(16).padStart(2, '0')}`; + if (opacity !== undefined && opacity < 1) { + return `${hex} @ ${Math.round(opacity * 100)}%`; + } + return hex; +} + +/** + * Extract the first solid color key from a paints array. + */ +function firstSolidColorKey(paints: Paint[]): string | null { + const solid = paints.find(p => p.type === 'SOLID' && p.visible !== false); + if (!solid || solid.type !== 'SOLID') return null; + const sp = solid as SolidPaint; + return rgbKey(sp.color.r, sp.color.g, sp.color.b, solid.opacity); +} + +// ── Style type abstraction (for local style APIs) ── + +interface LocalPaintStyle { + id: string; + name: string; + paints: Paint[]; +} + +interface LocalTextStyle { + id: string; + name: string; +} + +interface LocalEffectStyle { + id: string; + name: string; +} + +/** + * Safely call an async Figma API that may not exist in older plugin versions. + */ +async function safeGetLocalPaintStyles(): Promise { + try { + const api = figma as any; + if (typeof api.getLocalPaintStylesAsync === 'function') { + return await api.getLocalPaintStylesAsync(); + } + if (typeof api.getLocalPaintStyles === 'function') { + return api.getLocalPaintStyles(); + } + } catch { + // API not available + } + return []; +} + +async function safeGetLocalTextStyles(): Promise { + try { + const api = figma as any; + if (typeof api.getLocalTextStylesAsync === 'function') { + return await api.getLocalTextStylesAsync(); + } + if (typeof api.getLocalTextStyles === 'function') { + return api.getLocalTextStyles(); + } + } catch { + // API not available + } + return []; +} + +async function safeGetLocalEffectStyles(): Promise { + try { + const api = figma as any; + if (typeof api.getLocalEffectStylesAsync === 'function') { + return await api.getLocalEffectStylesAsync(); + } + if (typeof api.getLocalEffectStyles === 'function') { + return api.getLocalEffectStyles(); + } + } catch { + // API not available + } + return []; +} + +// ── Collect all nodes recursively ── + +function collectAllNodes( + node: SceneNode, + skipLocked: boolean, + skipHidden: boolean, + parentLocked: boolean, +): SceneNode[] { + const isLocked = parentLocked || ('locked' in node && (node as any).locked); + const isHidden = 'visible' in node && !node.visible; + + if (skipLocked && isLocked) return []; + if (skipHidden && isHidden) return []; + + const result: SceneNode[] = [node]; + + if ('children' in node) { + for (const child of (node as any).children as SceneNode[]) { + result.push(...collectAllNodes(child, skipLocked, skipHidden, isLocked)); + } + } + + return result; +} + +// ── Check: Orphaned local styles ── + +function checkOrphanedStyles( + paintStyles: LocalPaintStyle[], + textStyles: LocalTextStyle[], + effectStyles: LocalEffectStyle[], + allNodes: SceneNode[], + issues: LintIssue[], +): number { + let count = 0; + + // Collect all style IDs referenced by nodes + const usedStyleIds = new Set(); + for (const node of allNodes) { + const n = node as any; + if (n.fillStyleId && typeof n.fillStyleId === 'string' && n.fillStyleId !== '') { + usedStyleIds.add(n.fillStyleId); + } + if (n.strokeStyleId && typeof n.strokeStyleId === 'string' && n.strokeStyleId !== '') { + usedStyleIds.add(n.strokeStyleId); + } + if (n.textStyleId && typeof n.textStyleId === 'string' && n.textStyleId !== '') { + usedStyleIds.add(n.textStyleId); + } + if (n.effectStyleId && typeof n.effectStyleId === 'string' && n.effectStyleId !== '') { + usedStyleIds.add(n.effectStyleId); + } + } + + // Check paint styles + for (const style of paintStyles) { + if (!usedStyleIds.has(style.id)) { + count++; + pushIssue( + issues, + 'info', + style.id, + style.name, + `Paint style "${style.name}" is defined but not used by any visible node`, + 'Orphaned paint style', + ['Remove unused style or apply it to a node'], + ); + } + } + + // Check text styles + for (const style of textStyles) { + if (!usedStyleIds.has(style.id)) { + count++; + pushIssue( + issues, + 'info', + style.id, + style.name, + `Text style "${style.name}" is defined but not used by any visible node`, + 'Orphaned text style', + ['Remove unused style or apply it to a node'], + ); + } + } + + // Check effect styles + for (const style of effectStyles) { + if (!usedStyleIds.has(style.id)) { + count++; + pushIssue( + issues, + 'info', + style.id, + style.name, + `Effect style "${style.name}" is defined but not used by any visible node`, + 'Orphaned effect style', + ['Remove unused style or apply it to a node'], + ); + } + } + + return count; +} + +// ── Check: Hard-coded color matching an existing style ── + +function checkHardCodedColorMatch( + node: SceneNode, + paintStyleColorMap: Map, + issues: LintIssue[], +): boolean { + if (!('fills' in node)) return false; + + const fills = (node as any).fills; + if (fills === figma.mixed || !Array.isArray(fills)) return false; + + // Skip if a fill style is already applied + const fillStyleId = (node as any).fillStyleId; + if (fillStyleId && typeof fillStyleId === 'string' && fillStyleId !== '') { + return false; + } + + // Skip if bound to a variable + try { + const bv = (node as any).boundVariables; + if (bv?.fills) return false; + } catch { + // ignore + } + + const colorKey = firstSolidColorKey(fills); + if (!colorKey) return false; + + const matchingStyles = paintStyleColorMap.get(colorKey); + if (matchingStyles && matchingStyles.length > 0) { + pushIssue( + issues, + 'warning', + node.id, + node.name, + `Fill color ${colorKey} matches paint style "${matchingStyles[0]}" but is not linked`, + colorKey, + matchingStyles.map(s => `Apply style "${s}"`), + ); + return true; + } + + return false; +} + +// ── Check: Style applied but overridden ── + +function checkStyleOverridden( + node: SceneNode, + paintStyleMap: Map, + issues: LintIssue[], +): boolean { + if (!('fills' in node)) return false; + + const fillStyleId = (node as any).fillStyleId; + if (!fillStyleId || typeof fillStyleId !== 'string' || fillStyleId === '') { + return false; + } + + const style = paintStyleMap.get(fillStyleId); + if (!style) return false; + + const nodeFills = (node as any).fills; + if (nodeFills === figma.mixed || !Array.isArray(nodeFills)) return false; + + const nodeColorKey = firstSolidColorKey(nodeFills); + const styleColorKey = firstSolidColorKey(style.paints); + + // Only flag if both have solid colors and they differ + if (nodeColorKey && styleColorKey && nodeColorKey !== styleColorKey) { + pushIssue( + issues, + 'warning', + node.id, + node.name, + `Fill style "${style.name}" is applied but overridden — node fill ${nodeColorKey} differs from style ${styleColorKey}`, + `${nodeColorKey} (node) vs ${styleColorKey} (style)`, + ['Reset fill to style definition', 'Detach style and keep override'], + ); + return true; + } + + return false; +} + +// ── Check: Duplicate styles ── + +function checkDuplicateStyles( + paintStyles: LocalPaintStyle[], + issues: LintIssue[], +): number { + const colorToStyles = new Map(); + + for (const style of paintStyles) { + const key = firstSolidColorKey(style.paints); + if (!key) continue; + + if (!colorToStyles.has(key)) colorToStyles.set(key, []); + colorToStyles.get(key)!.push(style); + } + + let count = 0; + for (const [colorKey, styles] of colorToStyles) { + if (styles.length < 2) continue; + + // Report the second and subsequent duplicates + for (let i = 1; i < styles.length; i++) { + count++; + const dupeNames = styles.map(s => s.name).join(', '); + pushIssue( + issues, + 'info', + styles[i].id, + styles[i].name, + `Paint style "${styles[i].name}" has the same color (${colorKey}) as "${styles[0].name}" — possible duplicate`, + `${colorKey} shared by: ${dupeNames}`, + [`Merge into "${styles[0].name}"`, 'Verify they serve different purposes'], + ); + } + } + + return count; +} + +// ── Public API ────────────────────────────────── + +/** + * Run style library audit on the given nodes. + * Async — uses figma.getLocalPaintStylesAsync() and related APIs. + * + * Checks: orphaned styles, hard-coded color matches, overridden styles, + * and duplicate paint styles. + */ +export async function checkStyleAudit( + nodes: readonly SceneNode[], + opts?: { settings?: { skipLockedLayers?: boolean; skipHiddenLayers?: boolean } }, +): Promise { + const skipLocked = opts?.settings?.skipLockedLayers ?? true; + const skipHidden = opts?.settings?.skipHiddenLayers ?? true; + issueCounter = 0; + + const issues: LintIssue[] = []; + const summary = { + totalChecked: 0, + orphanedStyles: 0, + hardCodedMatches: 0, + overriddenStyles: 0, + duplicateStyles: 0, + }; + + // Fetch local styles + const [paintStyles, textStyles, effectStyles] = await Promise.all([ + safeGetLocalPaintStyles(), + safeGetLocalTextStyles(), + safeGetLocalEffectStyles(), + ]); + + // If no local styles exist, there is nothing to audit + if (paintStyles.length === 0 && textStyles.length === 0 && effectStyles.length === 0) { + return { issues, summary }; + } + + // Collect all visible nodes from the input tree + const allNodes: SceneNode[] = []; + for (const node of nodes) { + allNodes.push(...collectAllNodes(node, skipLocked, skipHidden, false)); + } + + summary.totalChecked = allNodes.length; + + // Build lookup maps for paint styles + const paintStyleMap = new Map(); + const paintStyleColorMap = new Map(); + + for (const style of paintStyles) { + paintStyleMap.set(style.id, style); + + const colorKey = firstSolidColorKey(style.paints); + if (colorKey) { + if (!paintStyleColorMap.has(colorKey)) paintStyleColorMap.set(colorKey, []); + paintStyleColorMap.get(colorKey)!.push(style.name); + } + } + + // 1. Orphaned styles + summary.orphanedStyles = checkOrphanedStyles(paintStyles, textStyles, effectStyles, allNodes, issues); + + // 2 & 3. Per-node checks + for (const node of allNodes) { + if (checkHardCodedColorMatch(node, paintStyleColorMap, issues)) { + summary.hardCodedMatches++; + } + if (checkStyleOverridden(node, paintStyleMap, issues)) { + summary.overriddenStyles++; + } + } + + // 4. Duplicate styles + summary.duplicateStyles = checkDuplicateStyles(paintStyles, issues); + + return { issues, summary }; +} diff --git a/src/lint/typography.ts b/src/lint/typography.ts new file mode 100644 index 0000000..573e0a6 --- /dev/null +++ b/src/lint/typography.ts @@ -0,0 +1,458 @@ +/// + +import { LintIssue, LintSeverity } from './types'; + +// ────────────────────────────────────────────── +// Typography Compliance Module +// +// Deterministic checks for text properties: +// - Inconsistent textAlignHorizontal within same component +// - Non-standard letterSpacing on body text +// - UPPERCASE without letterSpacing +// - Missing paragraphSpacing on multi-block layouts +// - Non-standard lineHeight ratio +// - textDecoration on non-link text +// ────────────────────────────────────────────── + +let issueCounter = 0; +function nextId(): string { + return `typo-${++issueCounter}`; +} + +export interface TypographyLintResult { + issues: LintIssue[]; + summary: { + totalChecked: number; + inconsistentAlignment: number; + nonStandardLetterSpacing: number; + uppercaseMissingSpacing: number; + missingParagraphSpacing: number; + badLineHeightRatio: number; + suspiciousDecoration: number; + }; +} + +// ── Helpers ───────────────────────────────────── + +function pushIssue( + issues: LintIssue[], + severity: LintSeverity, + nodeId: string, + nodeName: string, + message: string, + currentValue?: string, + suggestions?: string[], +): void { + issues.push({ + id: nextId(), + type: 'textStyle', + severity, + nodeId, + nodeName, + message, + currentValue, + suggestions, + autoFixable: false, + }); +} + +function isFrameLike(node: SceneNode): boolean { + return node.type === 'FRAME' || node.type === 'COMPONENT' || node.type === 'INSTANCE'; +} + +/** + * Heuristic: names suggesting a link pattern. + */ +const LINK_PATTERN = /\b(link|anchor|href|url|nav-link|breadcrumb|hyperlink)\b/i; + +function isLikelyLink(node: SceneNode): boolean { + if (LINK_PATTERN.test(node.name)) return true; + // Check parent name too + if (node.parent && 'name' in node.parent) { + return LINK_PATTERN.test((node.parent as SceneNode).name); + } + return false; +} + +// ── Collect text nodes within a component/frame ── + +function collectTextNodes(node: SceneNode): TextNode[] { + const texts: TextNode[] = []; + if (node.type === 'TEXT') { + texts.push(node as TextNode); + } + if ('children' in node) { + for (const child of (node as any).children as SceneNode[]) { + texts.push(...collectTextNodes(child)); + } + } + return texts; +} + +// ── Check: Inconsistent textAlignHorizontal within same component ── + +function checkInconsistentAlignment( + parent: SceneNode, + issues: LintIssue[], +): number { + if (!isFrameLike(parent)) return 0; + + const textNodes = collectTextNodes(parent); + if (textNodes.length < 2) return 0; + + // Only check body-size text (ignore headings/large text which may differ) + const bodyTexts = textNodes.filter(t => { + const fs = t.fontSize; + return typeof fs === 'number' && fs <= 18; + }); + if (bodyTexts.length < 2) return 0; + + const alignments = new Map(); + for (const t of bodyTexts) { + const align = t.textAlignHorizontal ?? 'LEFT'; + if (!alignments.has(align)) alignments.set(align, []); + alignments.get(align)!.push(t); + } + + if (alignments.size <= 1) return 0; + + // Find majority alignment + let maxCount = 0; + let majorityAlign = 'LEFT'; + for (const [align, nodes] of alignments) { + if (nodes.length > maxCount) { + maxCount = nodes.length; + majorityAlign = align; + } + } + + let checked = 0; + for (const [align, nodes] of alignments) { + if (align !== majorityAlign) { + for (const t of nodes) { + checked++; + pushIssue( + issues, + 'info', + t.id, + t.name, + `Text alignment "${align}" differs from majority body text alignment "${majorityAlign}" in "${parent.name}"`, + align, + [majorityAlign], + ); + } + } + } + + return checked; +} + +// ── Check: Non-standard letterSpacing on body text ── + +function checkBodyLetterSpacing( + node: TextNode, + issues: LintIssue[], +): boolean { + const fontSize = node.fontSize; + if (typeof fontSize !== 'number' || fontSize > 16) return false; + + const ls = (node as any).letterSpacing; + if (!ls || ls === figma.mixed) return false; + + const value = typeof ls === 'object' ? ls.value : undefined; + if (typeof value !== 'number' || value === 0) return false; + + pushIssue( + issues, + 'info', + node.id, + node.name, + `Body text (${fontSize}px) has non-zero letterSpacing (${value}${ls.unit === 'PERCENT' ? '%' : 'px'}) — unusual for body text`, + `${value}${ls.unit === 'PERCENT' ? '%' : 'px'}`, + ['0px (default)', 'Remove letterSpacing for body text'], + ); + + return true; +} + +// ── Check: UPPERCASE without letterSpacing ── + +function checkUppercaseSpacing( + node: TextNode, + issues: LintIssue[], +): boolean { + const textCase = (node as any).textCase; + if (textCase !== 'UPPER' && textCase !== 'ORIGINAL') { + // Only check explicit UPPER + } + if (textCase !== 'UPPER') return false; + + const ls = (node as any).letterSpacing; + const value = ls && typeof ls === 'object' ? ls.value : 0; + + if (typeof value === 'number' && value <= 0) { + pushIssue( + issues, + 'info', + node.id, + node.name, + `UPPERCASE text without positive letterSpacing — add spacing for readability`, + `textCase: UPPER, letterSpacing: ${value}`, + ['letterSpacing: 0.5px', 'letterSpacing: 1px', 'letterSpacing: 2%'], + ); + return true; + } + + return false; +} + +// ── Check: Missing paragraphSpacing ── + +function checkParagraphSpacing( + parent: SceneNode, + issues: LintIssue[], +): number { + if (!isFrameLike(parent)) return 0; + + const textNodes = collectTextNodes(parent); + // Only flag when there are multiple text blocks + if (textNodes.length < 2) return 0; + + let checked = 0; + let missingCount = 0; + + for (const t of textNodes) { + const ps = (t as any).paragraphSpacing; + if (typeof ps === 'number' && ps === 0) { + missingCount++; + } + checked++; + } + + // Only flag if ALL text blocks lack paragraph spacing + if (missingCount === textNodes.length && missingCount >= 2) { + pushIssue( + issues, + 'info', + parent.id, + parent.name, + `${missingCount} text blocks without paragraphSpacing set — add spacing for better readability`, + `${missingCount} texts, all paragraphSpacing: 0`, + ['Set paragraphSpacing to match line height or spacing scale'], + ); + } + + return checked; +} + +// ── Check: Non-standard lineHeight ratio ── + +function checkLineHeightRatio( + node: TextNode, + issues: LintIssue[], +): boolean { + const fontSize = node.fontSize; + if (typeof fontSize !== 'number' || fontSize === 0) return false; + + const lh = node.lineHeight; + if (!lh || lh === figma.mixed) return false; + + let lineHeightPx: number | null = null; + + if (typeof lh === 'object' && 'unit' in lh) { + if (lh.unit === 'PIXELS') { + lineHeightPx = lh.value; + } else if (lh.unit === 'PERCENT') { + lineHeightPx = (lh.value / 100) * fontSize; + } else if (lh.unit === 'AUTO') { + // AUTO line height is handled by the engine, skip + return false; + } + } + + if (lineHeightPx === null || lineHeightPx === 0) return false; + + const ratio = lineHeightPx / fontSize; + + if (ratio < 1.2) { + pushIssue( + issues, + 'warning', + node.id, + node.name, + `Line height ratio ${ratio.toFixed(2)} (${Math.round(lineHeightPx)}px / ${fontSize}px) is below 1.2 — text may be cramped`, + `${ratio.toFixed(2)} ratio`, + ['1.2 (minimum)', '1.4 (comfortable)', '1.5 (spacious)'], + ); + return true; + } + + if (ratio > 2.0) { + pushIssue( + issues, + 'info', + node.id, + node.name, + `Line height ratio ${ratio.toFixed(2)} (${Math.round(lineHeightPx)}px / ${fontSize}px) exceeds 2.0 — may be unintentional`, + `${ratio.toFixed(2)} ratio`, + ['1.4 (body)', '1.2 (heading)', '1.6 (loose)'], + ); + return true; + } + + return false; +} + +// ── Check: textDecoration on non-link text ── + +function checkSuspiciousDecoration( + node: TextNode, + issues: LintIssue[], +): boolean { + const decoration = (node as any).textDecoration; + if (decoration !== 'UNDERLINE') return false; + + // If the node or parent looks like a link, skip + if (isLikelyLink(node)) return false; + + pushIssue( + issues, + 'info', + node.id, + node.name, + `Underline decoration on text that does not appear to be a link — may confuse users`, + 'textDecoration: UNDERLINE', + ['Remove underline or rename layer to indicate link purpose'], + ); + + return true; +} + +// ── Recursive traversal ───────────────────────── + +interface TraversalStats { + totalChecked: number; + inconsistentAlignment: number; + nonStandardLetterSpacing: number; + uppercaseMissingSpacing: number; + missingParagraphSpacing: number; + badLineHeightRatio: number; + suspiciousDecoration: number; +} + +function emptyStats(): TraversalStats { + return { + totalChecked: 0, + inconsistentAlignment: 0, + nonStandardLetterSpacing: 0, + uppercaseMissingSpacing: 0, + missingParagraphSpacing: 0, + badLineHeightRatio: 0, + suspiciousDecoration: 0, + }; +} + +function traverse( + node: SceneNode, + issues: LintIssue[], + skipLocked: boolean, + skipHidden: boolean, + parentLocked: boolean, + seenComponents: Set, +): TraversalStats { + const isLocked = parentLocked || ('locked' in node && (node as any).locked); + const isHidden = 'visible' in node && !node.visible; + + if (skipLocked && isLocked) return emptyStats(); + if (skipHidden && isHidden) return emptyStats(); + + const stats = emptyStats(); + + // Per-text-node checks + if (node.type === 'TEXT') { + const textNode = node as TextNode; + stats.totalChecked++; + + if (checkBodyLetterSpacing(textNode, issues)) stats.nonStandardLetterSpacing++; + if (checkUppercaseSpacing(textNode, issues)) stats.uppercaseMissingSpacing++; + if (checkLineHeightRatio(textNode, issues)) stats.badLineHeightRatio++; + if (checkSuspiciousDecoration(textNode, issues)) stats.suspiciousDecoration++; + } + + // Per-component/frame checks (run once per unique component) + if (isFrameLike(node)) { + const nodeId = node.id; + if (!seenComponents.has(nodeId)) { + seenComponents.add(nodeId); + + const preAlign = issues.length; + stats.totalChecked += checkInconsistentAlignment(node, issues); + stats.inconsistentAlignment += issues.length - preAlign; + + const preParagraph = issues.length; + const paragraphChecked = checkParagraphSpacing(node, issues); + stats.totalChecked += paragraphChecked; + stats.missingParagraphSpacing += issues.length - preParagraph; + } + } + + // Recurse into children + if ('children' in node) { + for (const child of (node as any).children as SceneNode[]) { + const sub = traverse(child, issues, skipLocked, skipHidden, isLocked, seenComponents); + stats.totalChecked += sub.totalChecked; + stats.inconsistentAlignment += sub.inconsistentAlignment; + stats.nonStandardLetterSpacing += sub.nonStandardLetterSpacing; + stats.uppercaseMissingSpacing += sub.uppercaseMissingSpacing; + stats.missingParagraphSpacing += sub.missingParagraphSpacing; + stats.badLineHeightRatio += sub.badLineHeightRatio; + stats.suspiciousDecoration += sub.suspiciousDecoration; + } + } + + return stats; +} + +// ── Public API ────────────────────────────────── + +/** + * Run typography compliance checks on the given nodes. + * Checks text alignment consistency, letterSpacing, UPPERCASE spacing, + * paragraph spacing, line height ratio, and suspicious underline decoration. + */ +export function checkTypography( + nodes: readonly SceneNode[], + opts?: { settings?: { skipLockedLayers?: boolean; skipHiddenLayers?: boolean } }, +): TypographyLintResult { + const skipLocked = opts?.settings?.skipLockedLayers ?? true; + const skipHidden = opts?.settings?.skipHiddenLayers ?? true; + issueCounter = 0; + + const issues: LintIssue[] = []; + const seenComponents = new Set(); + const totals = emptyStats(); + + for (const node of nodes) { + const sub = traverse(node, issues, skipLocked, skipHidden, false, seenComponents); + totals.totalChecked += sub.totalChecked; + totals.inconsistentAlignment += sub.inconsistentAlignment; + totals.nonStandardLetterSpacing += sub.nonStandardLetterSpacing; + totals.uppercaseMissingSpacing += sub.uppercaseMissingSpacing; + totals.missingParagraphSpacing += sub.missingParagraphSpacing; + totals.badLineHeightRatio += sub.badLineHeightRatio; + totals.suspiciousDecoration += sub.suspiciousDecoration; + } + + return { + issues, + summary: { + totalChecked: totals.totalChecked, + inconsistentAlignment: totals.inconsistentAlignment, + nonStandardLetterSpacing: totals.nonStandardLetterSpacing, + uppercaseMissingSpacing: totals.uppercaseMissingSpacing, + missingParagraphSpacing: totals.missingParagraphSpacing, + badLineHeightRatio: totals.badLineHeightRatio, + suspiciousDecoration: totals.suspiciousDecoration, + }, + }; +} diff --git a/src/lint/variable-scope.ts b/src/lint/variable-scope.ts new file mode 100644 index 0000000..01966d9 --- /dev/null +++ b/src/lint/variable-scope.ts @@ -0,0 +1,395 @@ +/// + +import { LintIssue } from './types'; + +// ────────────────────────────────────────────── +// Variable Scope Enforcement Module +// +// Checks that variables are used within their declared scopes: +// - COLOR variable used for non-color property +// - FLOAT variable scope mismatch (e.g., GAP var on CORNER_RADIUS) +// - ALL_SCOPES overuse (name suggests specific use) +// - Single-context usage but scoped to ALL +// ────────────────────────────────────────────── + +let issueCounter = 0; +function nextId(): string { + return `vscope-${++issueCounter}`; +} + +export interface VariableScopeLintResult { + issues: LintIssue[]; + summary: { + totalChecked: number; + colorScopeMismatch: number; + floatScopeMismatch: number; + allScopesOveruse: number; + narrowingSuggestions: number; + }; +} + +// ── Mapping from node bound-variable field to expected variable scopes ── + +/** + * Maps a node field name (from boundVariables) to the VariableScope values + * that a variable should include in its `scopes` array. + */ +const FIELD_TO_EXPECTED_SCOPES: Record = { + // Color bindings (via fills/strokes) + fills: ['ALL_FILLS', 'FRAME_FILL', 'SHAPE_FILL', 'TEXT_FILL', 'ALL_SCOPES'], + strokes: ['STROKE_COLOR', 'ALL_SCOPES'], + + // Float bindings (spacing, sizing, radius) + itemSpacing: ['GAP', 'ALL_SCOPES'], + paddingLeft: ['GAP', 'ALL_SCOPES'], + paddingRight: ['GAP', 'ALL_SCOPES'], + paddingTop: ['GAP', 'ALL_SCOPES'], + paddingBottom: ['GAP', 'ALL_SCOPES'], + counterAxisSpacing: ['GAP', 'ALL_SCOPES'], + topLeftRadius: ['CORNER_RADIUS', 'ALL_SCOPES'], + topRightRadius: ['CORNER_RADIUS', 'ALL_SCOPES'], + bottomLeftRadius: ['CORNER_RADIUS', 'ALL_SCOPES'], + bottomRightRadius: ['CORNER_RADIUS', 'ALL_SCOPES'], + width: ['WIDTH_HEIGHT', 'ALL_SCOPES'], + height: ['WIDTH_HEIGHT', 'ALL_SCOPES'], + minWidth: ['WIDTH_HEIGHT', 'ALL_SCOPES'], + maxWidth: ['WIDTH_HEIGHT', 'ALL_SCOPES'], + minHeight: ['WIDTH_HEIGHT', 'ALL_SCOPES'], + maxHeight: ['WIDTH_HEIGHT', 'ALL_SCOPES'], + strokeWeight: ['STROKE_FLOAT', 'ALL_SCOPES'], + strokeTopWeight: ['STROKE_FLOAT', 'ALL_SCOPES'], + strokeRightWeight: ['STROKE_FLOAT', 'ALL_SCOPES'], + strokeBottomWeight: ['STROKE_FLOAT', 'ALL_SCOPES'], + strokeLeftWeight: ['STROKE_FLOAT', 'ALL_SCOPES'], + opacity: ['OPACITY', 'ALL_SCOPES'], + + // Text bindings + fontFamily: ['FONT_FAMILY', 'ALL_SCOPES'], + fontSize: ['FONT_SIZE', 'ALL_SCOPES'], + fontStyle: ['FONT_STYLE', 'ALL_SCOPES'], + fontWeight: ['FONT_WEIGHT', 'ALL_SCOPES'], + lineHeight: ['LINE_HEIGHT', 'ALL_SCOPES'], + letterSpacing: ['LETTER_SPACING', 'ALL_SCOPES'], + paragraphSpacing: ['PARAGRAPH_SPACING', 'ALL_SCOPES'], + paragraphIndent: ['PARAGRAPH_INDENT', 'ALL_SCOPES'], +}; + +/** + * Human-readable label for a scope mismatch. + */ +function scopeLabel(field: string): string { + const labels: Record = { + fills: 'fill color', + strokes: 'stroke color', + itemSpacing: 'gap', + paddingLeft: 'padding', + paddingRight: 'padding', + paddingTop: 'padding', + paddingBottom: 'padding', + counterAxisSpacing: 'counter-axis gap', + topLeftRadius: 'corner radius', + topRightRadius: 'corner radius', + bottomLeftRadius: 'corner radius', + bottomRightRadius: 'corner radius', + width: 'width', + height: 'height', + strokeWeight: 'stroke weight', + opacity: 'opacity', + fontSize: 'font size', + fontFamily: 'font family', + lineHeight: 'line height', + }; + return labels[field] ?? field; +} + +// ── Name-based scope suggestion patterns ── + +const SCOPE_NAME_PATTERNS: Array<{ pattern: RegExp; expectedScopes: string[]; label: string }> = [ + { pattern: /^spacing[-_/]|[-_/]spacing$|^space[-_/]|[-_/]gap|^gap[-_/]/i, expectedScopes: ['GAP'], label: 'GAP/padding' }, + { pattern: /^radius[-_/]|[-_/]radius$|^corner[-_/]|^rounded[-_/]/i, expectedScopes: ['CORNER_RADIUS'], label: 'CORNER_RADIUS' }, + { pattern: /^size[-_/]|[-_/]size$|^width[-_/]|^height[-_/]/i, expectedScopes: ['WIDTH_HEIGHT'], label: 'WIDTH_HEIGHT' }, + { pattern: /^color[-_/]|[-_/]color$|^fg[-_/]|^bg[-_/]|^fill[-_/]|[-_/]fill$/i, expectedScopes: ['ALL_FILLS', 'STROKE_COLOR'], label: 'color fills/strokes' }, + { pattern: /^stroke[-_/]|[-_/]stroke$|^border[-_/]|[-_/]border$/i, expectedScopes: ['STROKE_COLOR', 'STROKE_FLOAT'], label: 'stroke' }, + { pattern: /^font[-_/]|[-_/]font$|^text[-_/]|[-_/]text$|^type[-_/]/i, expectedScopes: ['FONT_SIZE', 'FONT_FAMILY', 'FONT_WEIGHT', 'FONT_STYLE', 'LINE_HEIGHT', 'LETTER_SPACING', 'TEXT_FILL'], label: 'typography' }, + { pattern: /^opacity[-_/]|[-_/]opacity$/i, expectedScopes: ['OPACITY'], label: 'OPACITY' }, +]; + +// ── Variable usage tracker (field -> variable ID set) ── + +interface VariableUsage { + variableId: string; + field: string; + nodeId: string; + nodeName: string; +} + +// ── Core check logic ── + +function collectBoundVariableUsages( + node: SceneNode, + usages: VariableUsage[], + skipLocked: boolean, + skipHidden: boolean, + parentLocked: boolean +): void { + const isLocked = parentLocked || ('locked' in node && (node as any).locked === true); + const isHidden = 'visible' in node && !node.visible; + + if (skipLocked && isLocked) return; + if (skipHidden && isHidden) return; + + const bound = (node as any).boundVariables as + | Record + | undefined; + + if (bound && typeof bound === 'object') { + for (const [field, binding] of Object.entries(bound)) { + if (!binding) continue; + + if (Array.isArray(binding)) { + // Array bindings (fills, strokes, etc.) + for (const alias of binding) { + if (alias && typeof alias === 'object' && 'id' in alias) { + usages.push({ + variableId: (alias as { id: string }).id, + field, + nodeId: node.id, + nodeName: node.name, + }); + } + } + } else if (typeof binding === 'object' && 'id' in binding) { + // Single alias binding + usages.push({ + variableId: (binding as { id: string }).id, + field, + nodeId: node.id, + nodeName: node.name, + }); + } else if (typeof binding === 'object') { + // Nested object (e.g., componentProperties) + for (const [_subKey, subBinding] of Object.entries(binding as Record)) { + if (subBinding && typeof subBinding === 'object' && 'id' in subBinding) { + usages.push({ + variableId: (subBinding as { id: string }).id, + field, + nodeId: node.id, + nodeName: node.name, + }); + } + } + } + } + } + + // Recurse + if ('children' in node) { + const children = (node as any).children; + if (Array.isArray(children)) { + for (const child of children as SceneNode[]) { + collectBoundVariableUsages(child, usages, skipLocked, skipHidden, isLocked); + } + } + } +} + +// ── Async variable resolution ── + +interface ResolvedVariable { + id: string; + name: string; + resolvedType: string; + scopes: string[]; +} + +async function resolveVariable(variableId: string): Promise { + try { + const getByIdAsync = (figma.variables as any).getVariableByIdAsync; + let variable: any = null; + + if (typeof getByIdAsync === 'function') { + variable = await getByIdAsync(variableId); + } else if (typeof figma.variables.getVariableById === 'function') { + variable = figma.variables.getVariableById(variableId); + } + + if (!variable) return null; + + return { + id: variableId, + name: (variable.name as string) ?? '', + resolvedType: (variable.resolvedType as string) ?? '', + scopes: Array.isArray(variable.scopes) ? (variable.scopes as string[]) : [], + }; + } catch { + return null; + } +} + +// ── Scope validation ── + +function checkScopeMismatch( + usage: VariableUsage, + variable: ResolvedVariable, + issues: LintIssue[] +): void { + const expectedScopes = FIELD_TO_EXPECTED_SCOPES[usage.field]; + if (!expectedScopes) return; + + // Skip if variable has ALL_SCOPES — will be checked separately + if (variable.scopes.includes('ALL_SCOPES')) return; + + // Check if any of the variable's scopes match the expected scopes for this field + const hasMatchingScope = variable.scopes.some(s => expectedScopes.includes(s)); + + if (!hasMatchingScope && variable.scopes.length > 0) { + const isColorField = usage.field === 'fills' || usage.field === 'strokes' || usage.field === 'textRangeFills'; + const severity: 'warning' | 'info' = 'warning'; + + if (variable.resolvedType === 'COLOR' && !isColorField) { + issues.push({ + id: nextId(), + type: 'naming', + severity, + nodeId: usage.nodeId, + nodeName: usage.nodeName, + message: `COLOR variable "${variable.name}" is bound to ${scopeLabel(usage.field)} — its scopes [${variable.scopes.join(', ')}] don't include this usage`, + currentValue: `${variable.name} on ${usage.field}`, + suggestions: [`Add appropriate scope for ${scopeLabel(usage.field)} usage`], + autoFixable: false, + }); + } else if (variable.resolvedType === 'FLOAT') { + issues.push({ + id: nextId(), + type: 'naming', + severity, + nodeId: usage.nodeId, + nodeName: usage.nodeName, + message: `FLOAT variable "${variable.name}" scoped to [${variable.scopes.join(', ')}] but bound to ${scopeLabel(usage.field)}`, + currentValue: `${variable.name} on ${usage.field}`, + suggestions: [`Verify scope includes ${expectedScopes.filter(s => s !== 'ALL_SCOPES').join(' or ')}`], + autoFixable: false, + }); + } + } +} + +function checkAllScopesOveruse( + variable: ResolvedVariable, + usageFields: string[], + firstUsage: VariableUsage, + issues: LintIssue[], + seenVariableIds: Set +): void { + // Only flag once per variable + if (seenVariableIds.has(variable.id)) return; + seenVariableIds.add(variable.id); + + if (!variable.scopes.includes('ALL_SCOPES')) return; + + // Check if name suggests a specific scope + for (const { pattern, label } of SCOPE_NAME_PATTERNS) { + if (pattern.test(variable.name)) { + issues.push({ + id: nextId(), + type: 'naming', + severity: 'info', + nodeId: firstUsage.nodeId, + nodeName: firstUsage.nodeName, + message: `Variable "${variable.name}" has ALL_SCOPES but its name suggests it should be restricted to ${label}`, + currentValue: `${variable.name}: ALL_SCOPES`, + suggestions: [`Restrict scopes to ${label} for better picker organization`], + autoFixable: false, + }); + return; // One suggestion per variable + } + } + + // Check if only used in a single context + const uniqueFields = [...new Set(usageFields)]; + if (uniqueFields.length === 1) { + const expectedForField = FIELD_TO_EXPECTED_SCOPES[uniqueFields[0]]; + const narrowScope = expectedForField?.find(s => s !== 'ALL_SCOPES'); + if (narrowScope) { + issues.push({ + id: nextId(), + type: 'naming', + severity: 'info', + nodeId: firstUsage.nodeId, + nodeName: firstUsage.nodeName, + message: `Variable "${variable.name}" is only used for ${scopeLabel(uniqueFields[0])} but scoped to ALL — consider narrowing to ${narrowScope}`, + currentValue: `${variable.name}: ALL_SCOPES (used in 1 context)`, + suggestions: [`Narrow scope to ${narrowScope}`], + autoFixable: false, + }); + } + } +} + +// ── Public API ── + +export async function checkVariableScope( + nodes: readonly SceneNode[], + options: { skipLocked?: boolean; skipHidden?: boolean } = {} +): Promise { + const { skipLocked = true, skipHidden = true } = options; + issueCounter = 0; + + const issues: LintIssue[] = []; + const usages: VariableUsage[] = []; + + // 1. Collect all bound variable usages + for (const node of nodes) { + collectBoundVariableUsages(node, usages, skipLocked, skipHidden, false); + } + + // 2. Resolve unique variables + const uniqueVarIds = [...new Set(usages.map(u => u.variableId))]; + const resolvedCache = new Map(); + + for (const varId of uniqueVarIds) { + resolvedCache.set(varId, await resolveVariable(varId)); + } + + // 3. Group usages by variable ID for context analysis + const usagesByVariable = new Map(); + for (const usage of usages) { + const existing = usagesByVariable.get(usage.variableId) ?? []; + existing.push(usage); + usagesByVariable.set(usage.variableId, existing); + } + + // 4. Run checks + const seenForAllScopes = new Set(); + + for (const usage of usages) { + const variable = resolvedCache.get(usage.variableId); + if (!variable) continue; + + checkScopeMismatch(usage, variable, issues); + } + + for (const [varId, varUsages] of usagesByVariable) { + const variable = resolvedCache.get(varId); + if (!variable || varUsages.length === 0) continue; + + checkAllScopesOveruse( + variable, + varUsages.map(u => u.field), + varUsages[0], + issues, + seenForAllScopes + ); + } + + return { + issues, + summary: { + totalChecked: usages.length, + colorScopeMismatch: issues.filter(i => i.message.includes('COLOR variable')).length, + floatScopeMismatch: issues.filter(i => i.message.includes('FLOAT variable')).length, + allScopesOveruse: issues.filter(i => i.message.includes('ALL_SCOPES') && i.message.includes('name suggests')).length, + narrowingSuggestions: issues.filter(i => i.message.includes('consider narrowing')).length, + }, + }; +} diff --git a/src/ui/message-handler.ts b/src/ui/message-handler.ts index ff71686..074ffeb 100644 --- a/src/ui/message-handler.ts +++ b/src/ui/message-handler.ts @@ -49,6 +49,7 @@ import { enableRealtimeLint, disableRealtimeLint } from '../lint/realtime-lint'; import { calculateDesignDebt } from '../baseline/design-debt'; import { lintSelection, + runDesignLint, ignoreNode, ignoreError, ignoreAllOfType, @@ -1691,6 +1692,46 @@ export async function initializePlugin(): Promise { } } +// ============================================================================ +// Ambient Quality Badge — quick lint on selection change +// ============================================================================ + +/** + * Run a lightweight lint on a single node and send a mini-score to the UI. + * Called from code.ts on selectionchange. Designed to be fast (single node, no AI). + */ +export function quickLintSelectedNode(): void { + const sel = figma.currentPage.selection; + if (sel.length === 0) return; + + try { + const node = sel[0]; + const result = runDesignLint([node], currentLintSettings); + const total = result.summary.totalNodes || 1; + + // Quick severity-weighted score (simplified version of UI's computeScoreBreakdown) + const WEIGHT: Record = { critical: 10, warning: 3, info: 1 }; + const weightedFailed = result.errors.reduce((sum, e) => sum + (WEIGHT[e.severity || 'warning'] || 3), 0); + const weightedPassed = Math.max(0, total - result.errors.length) * 10; + const totalW = weightedPassed + weightedFailed; + const score = totalW > 0 ? Math.round((weightedPassed / totalW) * 100) : 100; + + const topSeverity: string = result.errors.some(e => e.severity === 'critical') ? 'critical' + : result.errors.some(e => e.severity === 'warning') ? 'warning' + : result.errors.length > 0 ? 'info' : 'none'; + + sendMessageToUI('selection-mini-score', { + nodeId: node.id, + nodeName: node.name, + score, + issueCount: result.summary.totalErrors, + topSeverity, + }); + } catch { + // Don't break the plugin if quick lint fails + } +} + // ============================================================================ // Baseline & Diff Handler Functions // ============================================================================ diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 2a71b80..7c394ae 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -3,7 +3,7 @@ import ChatContainer from './components/chat/ChatContainer'; import SettingsPanel from './components/shared/SettingsPanel'; import { useChat } from './hooks/useChat'; import { usePluginMessages, usePostToPlugin } from './hooks/usePluginMessages'; -import type { PluginEvent, LintResult, LintError, AiReviewData, ReferoComparisonData, FlowAnalysisData, DiffResultData, PageSweepData, PageSweepRawData } from './lib/messages'; +import type { PluginEvent, LintResult, LintError, AiReviewData, ReferoComparisonData, FlowAnalysisData, DiffResultData, PageSweepData, PageSweepRawData, MiniScoreData } from './lib/messages'; import { analyzeComponent, streamChat, checkHealth, setBackendUrl, fetchReferoData, analyzeFlow, analyzePageSweep } from './lib/api'; export default function App() { @@ -16,6 +16,7 @@ export default function App() { const [showSettings, setShowSettings] = useState(false); const [selectionStale, setSelectionStale] = useState(false); const [currentNodeName, setCurrentNodeName] = useState(null); + const [miniScore, setMiniScore] = useState(null); const analyzedNodeId = useRef(null); const abortControllerRef = useRef(null); const walkthroughIndex = useRef(0); @@ -280,12 +281,18 @@ export default function App() { case 'selection-changed': { const selData = event.data as { hasSelection: boolean; nodeId: string | null; nodeName: string | null }; setCurrentNodeName(selData.nodeName); + // Clear mini-score when selection clears + if (!selData.hasSelection) setMiniScore(null); // Mark results as stale if selection differs from analyzed node; clear if it matches again if (chat.lintResult && analyzedNodeId.current) { setSelectionStale(selData.nodeId !== analyzedNodeId.current); } break; } + case 'selection-mini-score': { + setMiniScore(event.data as MiniScoreData); + break; + } } }, [chat, post, tryBackendAnalysis] @@ -690,6 +697,7 @@ export default function App() { state={chat} componentName={componentName} analysisMode={analysisMode} + miniScore={miniScore} onAnalyze={handleAnalyze} onSendMessage={handleSendMessage} onAction={handleAction} diff --git a/ui/src/components/chat/ChatContainer.tsx b/ui/src/components/chat/ChatContainer.tsx index 926005e..13a6656 100644 --- a/ui/src/components/chat/ChatContainer.tsx +++ b/ui/src/components/chat/ChatContainer.tsx @@ -4,11 +4,13 @@ import MessageList from './MessageList'; import InputBar from './InputBar'; import QuickActions from '../shared/QuickActions'; import type { ChatState } from '../../hooks/useChat'; +import type { MiniScoreData } from '../../lib/messages'; interface ChatContainerProps { state: ChatState; componentName?: string; analysisMode?: 'quick' | 'deep'; + miniScore?: MiniScoreData | null; onAnalyze: () => void; onSendMessage: (text: string) => void; onAction: (action: string, params?: Record) => void; @@ -20,13 +22,14 @@ export default function ChatContainer({ state, componentName, analysisMode, + miniScore, onAnalyze, onSendMessage, onAction, onJumpToNode, onOpenSettings, }: ChatContainerProps) { - const { messages, score, lintResult, isAnalyzing, issuesFixed, baselineMeta, lastDiff } = state; + const { messages, score, lintResult, isAnalyzing, issuesFixed, baselineMeta, lastDiff, prevScore } = state; const totalIssues = lintResult?.summary.totalErrors || 0; const hasResults = messages.length > 0; @@ -50,6 +53,8 @@ export default function ChatContainer({ totalIssues={totalIssues} issuesFixed={issuesFixed} lastDiff={lastDiff} + miniScore={miniScore} + prevScore={prevScore} onOpenSettings={onOpenSettings} /> diff --git a/ui/src/components/chat/MessageList.tsx b/ui/src/components/chat/MessageList.tsx index aa46ce2..1336ca0 100644 --- a/ui/src/components/chat/MessageList.tsx +++ b/ui/src/components/chat/MessageList.tsx @@ -9,6 +9,14 @@ import ReferoGallery from '../messages/ReferoGallery'; import FlowResultCard from '../messages/FlowResultCard'; import DiffCard from '../messages/DiffCard'; import PageSweepCard from '../messages/PageSweepCard'; +import DesignDebtCard from '../messages/DesignDebtCard'; +import DarkModeCard from '../messages/DarkModeCard'; +import A11ySpecCard from '../messages/A11ySpecCard'; +import TokenComplianceCard from '../messages/TokenComplianceCard'; +import BrandConsistencyCard from '../messages/BrandConsistencyCard'; +import CopyToneCard from '../messages/CopyToneCard'; +import PersonaResearchCard from '../messages/PersonaResearchCard'; +import AttentionHeatmapCard from '../messages/AttentionHeatmapCard'; import ActionButtons from '../shared/ActionButtons'; interface MessageListProps { @@ -118,6 +126,22 @@ export default function MessageList({ messages, onAction, onJumpToNode }: Messag return ; case 'page-sweep-result': return ; + case 'design-debt': + return ; + case 'dark-mode': + return ; + case 'a11y-spec': + return ; + case 'token-compliance': + return ; + case 'brand-consistency': + return ; + case 'copy-tone': + return ; + case 'persona-research': + return ; + case 'attention-heatmap': + return ; case 'baseline-saved': return (
diff --git a/ui/src/components/chat/StickyHeader.tsx b/ui/src/components/chat/StickyHeader.tsx index cbdad69..aca152a 100644 --- a/ui/src/components/chat/StickyHeader.tsx +++ b/ui/src/components/chat/StickyHeader.tsx @@ -1,4 +1,4 @@ -import type { ScoreBreakdown, DiffResultData } from '../../lib/messages'; +import type { ScoreBreakdown, DiffResultData, MiniScoreData } from '../../lib/messages'; interface StickyHeaderProps { componentName?: string; @@ -6,6 +6,8 @@ interface StickyHeaderProps { totalIssues: number; issuesFixed: number; lastDiff?: DiffResultData | null; + miniScore?: MiniScoreData | null; + prevScore?: number | null; onOpenSettings?: () => void; } @@ -15,13 +17,56 @@ function getVerdictInfo(score: number): { label: string; color: string; bg: stri return { label: 'POOR', color: 'text-fg-danger', bg: 'bg-bg-danger' }; } -export default function StickyHeader({ componentName, score, totalIssues, issuesFixed, lastDiff, onOpenSettings }: StickyHeaderProps) { +function getMiniScoreColor(score: number): string { + if (score >= 90) return 'text-fg-success'; + if (score >= 70) return 'text-fg-warning'; + return 'text-fg-danger'; +} + +function getSeverityDot(severity: string): string { + if (severity === 'critical') return 'bg-bg-danger'; + if (severity === 'warning') return 'bg-bg-warning'; + return 'bg-bg-success'; +} + +export default function StickyHeader({ componentName, score, totalIssues, issuesFixed, lastDiff, miniScore, prevScore, onOpenSettings }: StickyHeaderProps) { + // When no full analysis has been run yet, show ambient mini-score from selection + if (!score && miniScore) { + return ( +
+ + {miniScore.nodeName} + + {miniScore.score}/100 + + {miniScore.issueCount > 0 && ( + {miniScore.issueCount} issues + )} + {onOpenSettings && ( + + )} +
+ ); + } + if (!score) return null; const verdict = getVerdictInfo(score.overall); const clampedFixed = Math.min(issuesFixed, totalIssues); const remaining = Math.max(0, totalIssues - clampedFixed); - const trend = lastDiff?.scoreDelta.overall ?? null; + // Show trend from baseline diff, or from previous scan delta + const trend = lastDiff?.scoreDelta.overall + ?? (prevScore !== null && prevScore !== undefined ? score.overall - prevScore : null); return (
diff --git a/ui/src/components/messages/A11ySpecCard.tsx b/ui/src/components/messages/A11ySpecCard.tsx new file mode 100644 index 0000000..9a38c5a --- /dev/null +++ b/ui/src/components/messages/A11ySpecCard.tsx @@ -0,0 +1,280 @@ +import { useState } from 'react'; + +interface Landmark { + role: string; + label: string; +} + +interface HeadingItem { + level: number; + text: string; +} + +interface FocusItem { + index: number; + element: string; + role: string; +} + +interface AriaAnnotation { + element: string; + attributes: Record; +} + +interface KeyboardShortcut { + key: string; + action: string; +} + +interface LiveRegion { + element: string; + politeness: string; +} + +interface ContrastItem { + element: string; + ratio: number; + passes: boolean; + level: string; +} + +interface Recommendation { + title: string; + description: string; + severity: string; +} + +interface A11ySpecData { + landmarks: Landmark[]; + headingStructure: HeadingItem[]; + focusOrder: FocusItem[]; + ariaAnnotations: AriaAnnotation[]; + keyboardShortcuts: KeyboardShortcut[]; + liveRegions: LiveRegion[]; + colorContrastReport: ContrastItem[]; + recommendations: Recommendation[]; +} + +interface A11ySpecCardProps { + data: A11ySpecData; +} + +type TabKey = 'landmarks' | 'headings' | 'focus' | 'contrast' | 'recommendations'; + +const TABS: Array<{ key: TabKey; label: string }> = [ + { key: 'landmarks', label: 'Landmarks' }, + { key: 'headings', label: 'Headings' }, + { key: 'focus', label: 'Focus' }, + { key: 'contrast', label: 'Contrast' }, + { key: 'recommendations', label: 'Tips' }, +]; + +const SEVERITY_STYLES: Record = { + critical: { color: 'text-fg-danger', bg: 'bg-bg-danger' }, + warning: { color: 'text-fg-warning', bg: 'bg-bg-warning' }, + info: { color: 'text-fg-secondary', bg: 'bg-bg-tertiary' }, +}; + +function LandmarksTab({ landmarks, ariaAnnotations, keyboardShortcuts, liveRegions }: { + landmarks: Landmark[]; + ariaAnnotations: AriaAnnotation[]; + keyboardShortcuts: KeyboardShortcut[]; + liveRegions: LiveRegion[]; +}) { + return ( +
+ {landmarks.length > 0 && ( +
+

Landmarks

+ {landmarks.map((lm, i) => ( +
+ {lm.role} + {lm.label} +
+ ))} +
+ )} + {ariaAnnotations.length > 0 && ( +
+

ARIA annotations

+ {ariaAnnotations.map((ann, i) => ( +
+ {ann.element} +
+ {Object.entries(ann.attributes).map(([attr, val]) => ( + + {attr}="{val}" + + ))} +
+
+ ))} +
+ )} + {keyboardShortcuts.length > 0 && ( +
+

Keyboard shortcuts

+ {keyboardShortcuts.map((ks, i) => ( +
+ + {ks.key} + + {ks.action} +
+ ))} +
+ )} + {liveRegions.length > 0 && ( +
+

Live regions

+ {liveRegions.map((lr, i) => ( +
+ {lr.element} + {lr.politeness} +
+ ))} +
+ )} + {landmarks.length === 0 && ariaAnnotations.length === 0 && keyboardShortcuts.length === 0 && liveRegions.length === 0 && ( +

No landmarks or annotations found.

+ )} +
+ ); +} + +function HeadingsTab({ headings }: { headings: HeadingItem[] }) { + if (headings.length === 0) { + return

No heading structure defined.

; + } + return ( +
+ {headings.map((h, i) => ( +
+ + H{h.level} + + {h.text} +
+ ))} +
+ ); +} + +function FocusTab({ items }: { items: FocusItem[] }) { + if (items.length === 0) { + return

No focus order defined.

; + } + return ( +
+ {items.map((f) => ( +
+ {f.index} + {f.element} + {f.role} +
+ ))} +
+ ); +} + +function ContrastTab({ items }: { items: ContrastItem[] }) { + if (items.length === 0) { + return

No contrast data available.

; + } + return ( +
+ {items.map((c, i) => ( +
+ + {c.element} + + {c.ratio.toFixed(1)}:1 + + {c.level} +
+ ))} +
+ ); +} + +function RecommendationsTab({ items }: { items: Recommendation[] }) { + if (items.length === 0) { + return

No recommendations.

; + } + return ( +
+ {items.map((rec, i) => { + const sev = SEVERITY_STYLES[rec.severity] || SEVERITY_STYLES.info; + return ( +
+ + {rec.severity} + +
+

{rec.title}

+

{rec.description}

+
+
+ ); + })} +
+ ); +} + +export default function A11ySpecCard({ data }: A11ySpecCardProps) { + const [activeTab, setActiveTab] = useState('landmarks'); + + const contrastPassed = data.colorContrastReport.filter(c => c.passes).length; + const contrastTotal = data.colorContrastReport.length; + + return ( +
+ {/* Header */} +
+ A11y Spec + {contrastTotal > 0 && ( + + Contrast {contrastPassed}/{contrastTotal} + + )} +
+ + {/* Tab bar */} +
+ {TABS.map(({ key, label }) => ( + + ))} +
+ + {/* Tab content */} +
+ {activeTab === 'landmarks' && ( + + )} + {activeTab === 'headings' && } + {activeTab === 'focus' && } + {activeTab === 'contrast' && } + {activeTab === 'recommendations' && } +
+
+ ); +} diff --git a/ui/src/components/messages/AttentionHeatmapCard.tsx b/ui/src/components/messages/AttentionHeatmapCard.tsx new file mode 100644 index 0000000..c0f58da --- /dev/null +++ b/ui/src/components/messages/AttentionHeatmapCard.tsx @@ -0,0 +1,202 @@ +import { useState } from 'react'; + +interface FocalPoint { + element: string; + strength: 'high' | 'medium' | 'low'; +} + +interface ReadingFlow { + pattern: string; + followsConvention: boolean; + issues: string[]; +} + +interface CompetingElement { + elements: string[]; + issue: string; +} + +interface HeatmapRecommendation { + title: string; + description: string; + severity: string; +} + +interface AttentionHeatmapData { + focalPoints: FocalPoint[]; + readingFlow: ReadingFlow; + deadZones: string[]; + competingElements: CompetingElement[]; + visualWeightBalance: string; + recommendations: HeatmapRecommendation[]; + summary: string; +} + +interface AttentionHeatmapCardProps { + data: AttentionHeatmapData; +} + +const STRENGTH_STYLES: Record = { + high: { label: 'High', color: 'text-fg-danger', bg: 'bg-bg-danger', dot: 'bg-fg-danger' }, + medium: { label: 'Med', color: 'text-fg-warning', bg: 'bg-bg-warning', dot: 'bg-fg-warning' }, + low: { label: 'Low', color: 'text-fg-secondary', bg: 'bg-bg-tertiary', dot: 'bg-fg-tertiary' }, +}; + +const SEVERITY_STYLES: Record = { + critical: { color: 'text-fg-danger', bg: 'bg-bg-danger' }, + warning: { color: 'text-fg-warning', bg: 'bg-bg-warning' }, + info: { color: 'text-fg-secondary', bg: 'bg-bg-tertiary' }, +}; + +export default function AttentionHeatmapCard({ data }: AttentionHeatmapCardProps) { + const [showRecs, setShowRecs] = useState(false); + + const { readingFlow } = data; + + return ( +
+ {/* Header */} +
+ Attention Heatmap + {data.focalPoints.length} focal points +
+ + {/* Focal points */} + {data.focalPoints.length > 0 && ( +
+ {data.focalPoints.map((fp, i) => { + const strength = STRENGTH_STYLES[fp.strength] || STRENGTH_STYLES.low; + return ( +
+ + {fp.element} + + {strength.label} + +
+ ); + })} +
+ )} + + {/* Reading flow */} +
+
+ Reading pattern +
+ + {readingFlow.pattern} + + + + {readingFlow.followsConvention ? 'Conventional' : 'Unusual'} + +
+
+ {readingFlow.issues.length > 0 && ( +
+ {readingFlow.issues.map((issue, i) => ( +
+ ! + {issue} +
+ ))} +
+ )} +
+ + {/* Visual weight balance */} + {data.visualWeightBalance && ( +

+ Balance: {data.visualWeightBalance} +

+ )} + + {/* Dead zones */} + {data.deadZones.length > 0 && ( +
+

+ Dead zones ({data.deadZones.length}) +

+
+ {data.deadZones.map((zone, i) => ( + + {zone} + + ))} +
+
+ )} + + {/* Competing elements */} + {data.competingElements.length > 0 && ( +
+

+ Competing elements ({data.competingElements.length}) +

+
+ {data.competingElements.map((ce, i) => ( +
+
+ {ce.elements.map((el, j) => ( + + {el} + + ))} +
+

{ce.issue}

+
+ ))} +
+
+ )} + + {/* Recommendations */} + {data.recommendations.length > 0 && ( +
+ + {showRecs && ( +
+ {data.recommendations.map((rec, i) => { + const sev = SEVERITY_STYLES[rec.severity] || SEVERITY_STYLES.info; + return ( +
+ + {rec.severity} + +
+

{rec.title}

+

{rec.description}

+
+
+ ); + })} +
+ )} +
+ )} + + {/* Summary */} + {data.summary && ( +

{data.summary}

+ )} +
+ ); +} diff --git a/ui/src/components/messages/BrandConsistencyCard.tsx b/ui/src/components/messages/BrandConsistencyCard.tsx new file mode 100644 index 0000000..af8481e --- /dev/null +++ b/ui/src/components/messages/BrandConsistencyCard.tsx @@ -0,0 +1,187 @@ +import { useState } from 'react'; + +interface ColorDeviation { + element: string; + found: string; + expected: string; +} + +interface TypographyIssue { + element: string; + issue: string; +} + +interface SpacingIssue { + element: string; + issue: string; +} + +interface BrandRecommendation { + title: string; + description: string; + severity: string; +} + +interface BrandConsistencyData { + overallRating: 'pass' | 'needs_improvement' | 'fail'; + colorDeviations: ColorDeviation[]; + typographyIssues: TypographyIssue[]; + spacingIssues: SpacingIssue[]; + personalityMatch: string; + recommendations: BrandRecommendation[]; + summary: string; +} + +interface BrandConsistencyCardProps { + data: BrandConsistencyData; +} + +const RATING_STYLES: Record = { + pass: { label: 'PASS', color: 'text-fg-success', bg: 'bg-bg-success' }, + needs_improvement: { label: 'NEEDS WORK', color: 'text-fg-warning', bg: 'bg-bg-warning' }, + fail: { label: 'FAIL', color: 'text-fg-danger', bg: 'bg-bg-danger' }, +}; + +const SEVERITY_STYLES: Record = { + critical: { color: 'text-fg-danger', bg: 'bg-bg-danger' }, + warning: { color: 'text-fg-warning', bg: 'bg-bg-warning' }, + info: { color: 'text-fg-secondary', bg: 'bg-bg-tertiary' }, +}; + +type SectionKey = 'colors' | 'typography' | 'spacing' | 'recommendations'; + +export default function BrandConsistencyCard({ data }: BrandConsistencyCardProps) { + const [openSection, setOpenSection] = useState(null); + + const rating = RATING_STYLES[data.overallRating] || RATING_STYLES.fail; + + const toggle = (key: SectionKey) => { + setOpenSection(prev => (prev === key ? null : key)); + }; + + const sections: Array<{ key: SectionKey; label: string; count: number; color: string }> = [ + { key: 'colors', label: 'Color deviations', count: data.colorDeviations.length, color: data.colorDeviations.length > 0 ? 'text-fg-danger' : 'text-fg-success' }, + { key: 'typography', label: 'Typography issues', count: data.typographyIssues.length, color: data.typographyIssues.length > 0 ? 'text-fg-warning' : 'text-fg-success' }, + { key: 'spacing', label: 'Spacing issues', count: data.spacingIssues.length, color: data.spacingIssues.length > 0 ? 'text-fg-warning' : 'text-fg-success' }, + { key: 'recommendations', label: 'Recommendations', count: data.recommendations.length, color: 'text-fg-secondary' }, + ]; + + return ( +
+ {/* Header */} +
+ Brand Consistency + + {rating.label} + +
+ + {/* Personality match */} + {data.personalityMatch && ( +

+ Personality: {data.personalityMatch} +

+ )} + + {/* Category counts with collapsible details */} +
+ {sections.map(({ key, label, count, color }) => { + const isOpen = openSection === key; + const hasContent = + (key === 'colors' && data.colorDeviations.length > 0) || + (key === 'typography' && data.typographyIssues.length > 0) || + (key === 'spacing' && data.spacingIssues.length > 0) || + (key === 'recommendations' && data.recommendations.length > 0); + + return ( +
+ + + {isOpen && key === 'colors' && ( +
+ {data.colorDeviations.map((d, i) => ( +
+ {d.element} + {d.found} + {'\u2192'} + {d.expected} +
+ ))} +
+ )} + + {isOpen && key === 'typography' && ( +
+ {data.typographyIssues.map((t, i) => ( +
+ {t.element} + — {t.issue} +
+ ))} +
+ )} + + {isOpen && key === 'spacing' && ( +
+ {data.spacingIssues.map((s, i) => ( +
+ {s.element} + — {s.issue} +
+ ))} +
+ )} + + {isOpen && key === 'recommendations' && ( +
+ {data.recommendations.map((rec, i) => { + const sev = SEVERITY_STYLES[rec.severity] || SEVERITY_STYLES.info; + return ( +
+ + {rec.severity} + +
+

{rec.title}

+

{rec.description}

+
+
+ ); + })} +
+ )} +
+ ); + })} +
+ + {/* Summary */} + {data.summary && ( +

{data.summary}

+ )} +
+ ); +} diff --git a/ui/src/components/messages/CopyToneCard.tsx b/ui/src/components/messages/CopyToneCard.tsx new file mode 100644 index 0000000..d41461d --- /dev/null +++ b/ui/src/components/messages/CopyToneCard.tsx @@ -0,0 +1,183 @@ +import { useState } from 'react'; + +interface TerminologyIssue { + term: string; + variants: string[]; + suggestion: string; +} + +interface ToneShift { + screen: string; + expectedTone: string; + actualTone: string; +} + +interface CopyRecommendation { + title: string; + description: string; +} + +interface CopyToneData { + overallConsistency: 'consistent' | 'mostly_consistent' | 'inconsistent'; + toneProfile: string; + terminologyIssues: TerminologyIssue[]; + toneShifts: ToneShift[]; + recommendations: CopyRecommendation[]; + summary: string; +} + +interface CopyToneCardProps { + data: CopyToneData; +} + +const CONSISTENCY_STYLES: Record = { + consistent: { label: 'CONSISTENT', color: 'text-fg-success', bg: 'bg-bg-success' }, + mostly_consistent: { label: 'MOSTLY OK', color: 'text-fg-warning', bg: 'bg-bg-warning' }, + inconsistent: { label: 'INCONSISTENT', color: 'text-fg-danger', bg: 'bg-bg-danger' }, +}; + +export default function CopyToneCard({ data }: CopyToneCardProps) { + const [showTerms, setShowTerms] = useState(data.terminologyIssues.length <= 3); + const [showShifts, setShowShifts] = useState(data.toneShifts.length <= 3); + const [showRecs, setShowRecs] = useState(false); + + const badge = CONSISTENCY_STYLES[data.overallConsistency] || CONSISTENCY_STYLES.inconsistent; + + return ( +
+ {/* Header */} +
+ Copy & Tone + + {badge.label} + +
+ + {/* Tone profile */} + {data.toneProfile && ( +

+ Tone: {data.toneProfile} +

+ )} + + {/* Terminology issues */} + {data.terminologyIssues.length > 0 && ( +
+ + {showTerms && ( +
+ {data.terminologyIssues.map((t, i) => ( +
+
+ {t.term} + {'\u2192'} + {t.suggestion} +
+
+ {t.variants.map((v, j) => ( + + {v} + + ))} +
+
+ ))} +
+ )} +
+ )} + + {/* Tone shifts */} + {data.toneShifts.length > 0 && ( +
+ + {showShifts && ( +
+ {data.toneShifts.map((s, i) => ( +
+ {s.screen} + {s.expectedTone} + {'\u2192'} + {s.actualTone} +
+ ))} +
+ )} +
+ )} + + {/* Recommendations */} + {data.recommendations.length > 0 && ( +
+ + {showRecs && ( +
+ {data.recommendations.map((rec, i) => ( +
+

{rec.title}

+

{rec.description}

+
+ ))} +
+ )} +
+ )} + + {/* Summary */} + {data.summary && ( +

{data.summary}

+ )} +
+ ); +} diff --git a/ui/src/components/messages/DarkModeCard.tsx b/ui/src/components/messages/DarkModeCard.tsx new file mode 100644 index 0000000..628adff --- /dev/null +++ b/ui/src/components/messages/DarkModeCard.tsx @@ -0,0 +1,146 @@ +import { useState } from 'react'; + +interface DarkModeIssue { + type: string; + severity: string; + nodeName: string; + message: string; + currentValue?: string; + suggestions?: string[]; +} + +interface DarkModeMetrics { + pureBlackBackgrounds: number; + pureWhiteText: number; + lowContrastOnDark: number; + missingModeValues: number; +} + +interface DarkModeSummary { + totalChecked: number; + passed: number; + failed: number; +} + +interface DarkModeData { + issues: DarkModeIssue[]; + metrics: DarkModeMetrics; + summary: DarkModeSummary; +} + +interface DarkModeCardProps { + data: DarkModeData; +} + +const SEVERITY_STYLES: Record = { + critical: { color: 'text-fg-danger', bg: 'bg-bg-danger' }, + warning: { color: 'text-fg-warning', bg: 'bg-bg-warning' }, + info: { color: 'text-fg-secondary', bg: 'bg-bg-tertiary' }, +}; + +const METRIC_CONFIG: Array<{ key: keyof DarkModeMetrics; label: string; icon: string }> = [ + { key: 'pureBlackBackgrounds', label: 'Pure black BGs', icon: '◼' }, + { key: 'pureWhiteText', label: 'Pure white text', icon: '◻' }, + { key: 'lowContrastOnDark', label: 'Low contrast', icon: '◐' }, + { key: 'missingModeValues', label: 'Missing modes', icon: '⊘' }, +]; + +export default function DarkModeCard({ data }: DarkModeCardProps) { + const [expanded, setExpanded] = useState(false); + const { summary, metrics, issues } = data; + const passRate = summary.totalChecked > 0 + ? Math.round((summary.passed / summary.totalChecked) * 100) + : 0; + const allPassed = summary.failed === 0; + const displayIssues = expanded ? issues : issues.slice(0, 4); + + return ( +
+ {/* Header */} +
+ Dark Mode Audit + + {allPassed ? 'PASS' : `${summary.failed} FAIL`} + +
+ + {/* Pass/fail summary bar */} +
+
+
+
+
+ + {summary.passed}/{summary.totalChecked} + +
+ + {/* Metric counts */} +
+ {METRIC_CONFIG.map(({ key, label, icon }) => { + const count = metrics[key]; + return ( +
+ {icon} + {label} + 0 ? 'text-fg-warning' : 'text-fg-success'}`}> + {count} + +
+ ); + })} +
+ + {/* Issues list */} + {issues.length > 0 && ( +
+

Issues ({issues.length})

+
+ {displayIssues.map((issue, i) => { + const sev = SEVERITY_STYLES[issue.severity] || SEVERITY_STYLES.info; + return ( +
+
+ + {issue.type} + +
+

{issue.message}

+ {issue.nodeName} +
+
+ {issue.currentValue && ( +

Current: {issue.currentValue}

+ )} + {issue.suggestions && issue.suggestions.length > 0 && ( +

+ Fix: {issue.suggestions[0]} +

+ )} +
+ ); + })} +
+ {issues.length > 4 && ( + + )} +
+ )} +
+ ); +} diff --git a/ui/src/components/messages/DesignDebtCard.tsx b/ui/src/components/messages/DesignDebtCard.tsx new file mode 100644 index 0000000..0260d6a --- /dev/null +++ b/ui/src/components/messages/DesignDebtCard.tsx @@ -0,0 +1,132 @@ +interface DebtCategory { + orphanedStyles: number; + detachedInstances: number; + hardcodedValues: number; + namingViolations: number; + missingAutoLayout: number; + inconsistentSpacing: number; +} + +interface DesignDebtData { + overall: number; + components: DebtCategory; + trend?: { + direction: 'improving' | 'declining' | 'stable'; + delta: number; + }; +} + +interface DesignDebtCardProps { + data: DesignDebtData; +} + +const CATEGORY_LABELS: Record = { + orphanedStyles: 'Orphaned styles', + detachedInstances: 'Detached instances', + hardcodedValues: 'Hardcoded values', + namingViolations: 'Naming violations', + missingAutoLayout: 'Missing auto-layout', + inconsistentSpacing: 'Inconsistent spacing', +}; + +function getScoreColor(score: number): string { + if (score >= 90) return 'text-fg-success'; + if (score >= 70) return 'text-fg-warning'; + return 'text-fg-danger'; +} + +function getBarColor(score: number): string { + if (score >= 90) return 'bg-fg-success'; + if (score >= 70) return 'bg-fg-warning'; + return 'bg-fg-danger'; +} + +function getGradeInfo(score: number): { label: string; color: string; bg: string } { + if (score >= 90) return { label: 'Low Debt', color: 'text-fg-success', bg: 'bg-bg-success' }; + if (score >= 70) return { label: 'Moderate', color: 'text-fg-warning', bg: 'bg-bg-warning' }; + return { label: 'High Debt', color: 'text-fg-danger', bg: 'bg-bg-danger' }; +} + +function TrendIndicator({ trend }: { trend: DesignDebtData['trend'] }) { + if (!trend) return null; + const { direction, delta } = trend; + + if (direction === 'improving') { + return ( + + + +{delta} + + ); + } + if (direction === 'declining') { + return ( + + + -{delta} + + ); + } + return ( + + + 0 + + ); +} + +function CategoryBar({ label, count }: { label: string; count: number }) { + const maxCount = 50; + const pct = Math.min((count / maxCount) * 100, 100); + const score = 100 - pct; + + return ( +
+ {label} +
+
+
+ {count} +
+ ); +} + +export default function DesignDebtCard({ data }: DesignDebtCardProps) { + const grade = getGradeInfo(data.overall); + + return ( +
+
+ Design Debt +
+ + + {grade.label} + + + {data.overall} + +
+
+ +
+ {(Object.keys(CATEGORY_LABELS) as Array).map((key) => ( + + ))} +
+
+ ); +} diff --git a/ui/src/components/messages/PersonaResearchCard.tsx b/ui/src/components/messages/PersonaResearchCard.tsx new file mode 100644 index 0000000..1650398 --- /dev/null +++ b/ui/src/components/messages/PersonaResearchCard.tsx @@ -0,0 +1,225 @@ +import { useState } from 'react'; + +interface PersonaIssue { + description: string; + severity: string; +} + +interface Persona { + name: string; + description: string; + rating: 1 | 2 | 3; + confidence: string; + issues: PersonaIssue[]; +} + +interface UniversalBarrier { + description: string; + flaggedBy: string[]; + severity: string; +} + +interface QuickWin { + title: string; + description: string; + impact: string; +} + +interface PersonaResearchData { + personas: Persona[]; + universalBarriers: UniversalBarrier[]; + quickWins: QuickWin[]; + summary: string; +} + +interface PersonaResearchCardProps { + data: PersonaResearchData; +} + +const RATING_CONFIG: Record = { + 1: { label: 'Easy', color: 'text-fg-success', bg: 'bg-bg-success' }, + 2: { label: 'Moderate', color: 'text-fg-warning', bg: 'bg-bg-warning' }, + 3: { label: 'Difficult', color: 'text-fg-danger', bg: 'bg-bg-danger' }, +}; + +const SEVERITY_STYLES: Record = { + critical: { color: 'text-fg-danger', bg: 'bg-bg-danger' }, + warning: { color: 'text-fg-warning', bg: 'bg-bg-warning' }, + info: { color: 'text-fg-secondary', bg: 'bg-bg-tertiary' }, +}; + +const IMPACT_STYLES: Record = { + high: { color: 'text-fg-success', bg: 'bg-bg-success' }, + medium: { color: 'text-fg-warning', bg: 'bg-bg-warning' }, + low: { color: 'text-fg-secondary', bg: 'bg-bg-tertiary' }, +}; + +export default function PersonaResearchCard({ data }: PersonaResearchCardProps) { + const [expandedPersona, setExpandedPersona] = useState(null); + const [showBarriers, setShowBarriers] = useState(false); + const [showWins, setShowWins] = useState(false); + + return ( +
+ {/* Header */} +
+ Persona Research + {data.personas.length} personas +
+ + {/* Personas */} +
+ {data.personas.map((persona, i) => { + const ratingStyle = RATING_CONFIG[persona.rating] || RATING_CONFIG[2]; + const isExpanded = expandedPersona === i; + + return ( +
+ + + {isExpanded && ( +
+

+ Confidence: {persona.confidence} +

+ {persona.issues.length > 0 && ( +
+ {persona.issues.map((issue, j) => { + const sev = SEVERITY_STYLES[issue.severity] || SEVERITY_STYLES.info; + return ( +
+ + {issue.description} +
+ ); + })} +
+ )} + {persona.issues.length === 0 && ( +

No issues identified

+ )} +
+ )} +
+ ); + })} +
+ + {/* Universal barriers */} + {data.universalBarriers.length > 0 && ( +
+ + {showBarriers && ( +
+ {data.universalBarriers.map((b, i) => { + const sev = SEVERITY_STYLES[b.severity] || SEVERITY_STYLES.info; + return ( +
+ + {b.severity} + +
+

{b.description}

+

+ Flagged by {b.flaggedBy.length} persona{b.flaggedBy.length !== 1 ? 's' : ''} +

+
+
+ ); + })} +
+ )} +
+ )} + + {/* Quick wins */} + {data.quickWins.length > 0 && ( +
+ + {showWins && ( +
+ {data.quickWins.map((w, i) => { + const impact = IMPACT_STYLES[w.impact] || IMPACT_STYLES.medium; + return ( +
+ + {w.impact} + +
+

{w.title}

+

{w.description}

+
+
+ ); + })} +
+ )} +
+ )} + + {/* Summary */} + {data.summary && ( +

{data.summary}

+ )} +
+ ); +} diff --git a/ui/src/components/messages/TokenComplianceCard.tsx b/ui/src/components/messages/TokenComplianceCard.tsx new file mode 100644 index 0000000..18002bb --- /dev/null +++ b/ui/src/components/messages/TokenComplianceCard.tsx @@ -0,0 +1,184 @@ +import { useState } from 'react'; + +interface MatchedToken { + token: string; + nodeCount: number; + usage: 'correct' | 'overridden'; +} + +interface UnmatchedValue { + value: string; + nodeCount: number; + nearestToken: string; + distance: number; +} + +interface TokenSummary { + totalTokenDefs: number; + usedInDesign: number; + hardCodedValues: number; + compliance: number; +} + +interface TokenComplianceData { + adoptionScore: number; + matched: MatchedToken[]; + unmatched: UnmatchedValue[]; + orphanTokens: string[]; + summary: TokenSummary; +} + +interface TokenComplianceCardProps { + data: TokenComplianceData; +} + +function getComplianceColor(score: number): string { + if (score >= 90) return 'text-fg-success'; + if (score >= 70) return 'text-fg-warning'; + return 'text-fg-danger'; +} + +function getComplianceBg(score: number): { label: string; color: string; bg: string } { + if (score >= 90) return { label: 'Compliant', color: 'text-fg-success', bg: 'bg-bg-success' }; + if (score >= 70) return { label: 'Partial', color: 'text-fg-warning', bg: 'bg-bg-warning' }; + return { label: 'Non-compliant', color: 'text-fg-danger', bg: 'bg-bg-danger' }; +} + +export default function TokenComplianceCard({ data }: TokenComplianceCardProps) { + const [showUnmatched, setShowUnmatched] = useState(false); + const [showOrphans, setShowOrphans] = useState(false); + const { summary } = data; + const grade = getComplianceBg(summary.compliance); + + const matchedCorrect = data.matched.filter(m => m.usage === 'correct').length; + const matchedOverridden = data.matched.filter(m => m.usage === 'overridden').length; + const displayUnmatched = showUnmatched ? data.unmatched : data.unmatched.slice(0, 4); + + return ( +
+ {/* Header */} +
+ Token Compliance +
+ + {grade.label} + + + {data.adoptionScore}% + +
+
+ + {/* Summary stats */} +
+
+ {summary.totalTokenDefs} +

Defined

+
+
+ {summary.usedInDesign} +

In use

+
+
+ 0 ? 'text-fg-warning' : 'text-fg-success'}`}> + {summary.hardCodedValues} + +

Hardcoded

+
+
+ + {/* Matched tokens breakdown */} +
+ {matchedCorrect} correct + {matchedOverridden > 0 && ( + {matchedOverridden} overridden + )} + {data.unmatched.length > 0 && ( + {data.unmatched.length} unmatched + )} +
+ + {/* Unmatched values with nearest token suggestions */} + {data.unmatched.length > 0 && ( +
+ + {(showUnmatched || data.unmatched.length <= 4) && ( +
+ {displayUnmatched.map((u, i) => ( +
+ + {u.value} + +
+ x{u.nodeCount} +

+ Use: {u.nearestToken} +

+
+
+ ))} + {!showUnmatched && data.unmatched.length > 4 && ( + + )} +
+ )} +
+ )} + + {/* Orphan tokens */} + {data.orphanTokens.length > 0 && ( +
+ + {showOrphans && ( +
+ {data.orphanTokens.map((t, i) => ( + + {t} + + ))} +
+ )} +
+ )} +
+ ); +} diff --git a/ui/src/components/shared/ScaleEditor.tsx b/ui/src/components/shared/ScaleEditor.tsx new file mode 100644 index 0000000..d0f18b2 --- /dev/null +++ b/ui/src/components/shared/ScaleEditor.tsx @@ -0,0 +1,170 @@ +import { useState, useCallback, useRef } from 'react'; + +interface Preset { + name: string; + values: number[]; +} + +interface ScaleEditorProps { + values: number[]; + onChange: (values: number[]) => void; + label: string; + presets?: Preset[]; +} + +export default function ScaleEditor({ + values, + onChange, + label, + presets, +}: ScaleEditorProps) { + const [isAdding, setIsAdding] = useState(false); + const [inputValue, setInputValue] = useState(''); + const inputRef = useRef(null); + + const sorted = [...values].sort((a, b) => a - b); + + const handleRemove = useCallback( + (val: number) => { + onChange(values.filter((v) => v !== val)); + }, + [values, onChange], + ); + + const handleAdd = useCallback(() => { + const num = parseInt(inputValue.trim(), 10); + if (isNaN(num) || num < 0) return; + if (values.includes(num)) { + // Already exists, just close + setInputValue(''); + setIsAdding(false); + return; + } + onChange([...values, num].sort((a, b) => a - b)); + setInputValue(''); + setIsAdding(false); + }, [inputValue, values, onChange]); + + const handleInputKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleAdd(); + } else if (e.key === 'Escape') { + setInputValue(''); + setIsAdding(false); + } + }, + [handleAdd], + ); + + const handlePresetClick = useCallback( + (preset: Preset) => { + onChange(preset.values); + }, + [onChange], + ); + + const startAdding = useCallback(() => { + setIsAdding(true); + // Focus input on next tick + requestAnimationFrame(() => inputRef.current?.focus()); + }, []); + + return ( +
+
+ {label} + {presets && presets.length > 0 && ( +
+ {presets.map((preset) => { + const isActive = + preset.values.length === values.length && + preset.values.every((v, i) => sorted[i] === v); + return ( + + ); + })} +
+ )} +
+ +
+ {sorted.map((val) => ( + + ))} + + {isAdding ? ( +
+ setInputValue(e.target.value)} + onKeyDown={handleInputKeyDown} + onBlur={handleAdd} + className="w-10 px-1 py-0.5 text-11 bg-bg-secondary border border-border rounded focus:outline-none focus:ring-1 focus:ring-bg-brand" + placeholder="px" + aria-label={`Add value to ${label}`} + /> +
+ ) : ( + + )} +
+
+ ); +} diff --git a/ui/src/components/shared/SeveritySelector.tsx b/ui/src/components/shared/SeveritySelector.tsx new file mode 100644 index 0000000..df2bc1e --- /dev/null +++ b/ui/src/components/shared/SeveritySelector.tsx @@ -0,0 +1,68 @@ +import { useCallback } from 'react'; + +export type Severity = 'critical' | 'warning' | 'info' | 'off'; + +interface SeveritySelectorProps { + value: Severity; + onChange: (value: Severity) => void; + label: string; +} + +const SEVERITY_OPTIONS: Array<{ + value: Severity; + abbr: string; + dotClass: string; + title: string; +}> = [ + { value: 'critical', abbr: 'C', dotClass: 'bg-fg-danger', title: 'Critical' }, + { value: 'warning', abbr: 'W', dotClass: 'bg-fg-warning', title: 'Warning' }, + { value: 'info', abbr: 'I', dotClass: 'bg-bg-brand', title: 'Info' }, + { value: 'off', abbr: 'O', dotClass: 'bg-fg-disabled', title: 'Off' }, +]; + +export default function SeveritySelector({ + value, + onChange, + label, +}: SeveritySelectorProps) { + const handleClick = useCallback( + (severity: Severity) => { + onChange(severity); + }, + [onChange], + ); + + return ( +
+ + {label} + +
+ {SEVERITY_OPTIONS.map((opt) => { + const isActive = value === opt.value; + return ( + + ); + })} +
+
+ ); +} diff --git a/ui/src/components/shared/TeamConfigPanel.tsx b/ui/src/components/shared/TeamConfigPanel.tsx new file mode 100644 index 0000000..38e0cb7 --- /dev/null +++ b/ui/src/components/shared/TeamConfigPanel.tsx @@ -0,0 +1,413 @@ +import { useState, useCallback, useEffect, useRef } from 'react'; +import ScaleEditor from './ScaleEditor'; +import SeveritySelector from './SeveritySelector'; +import type { Severity } from './SeveritySelector'; +import { postToPlugin } from '../../lib/messages'; +import type { LintErrorType } from '../../lib/messages'; + +// ── Default values ────────────────────────────────────────── + +const DEFAULT_SPACING_SCALE = [0, 2, 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 80, 96]; +const DEFAULT_RADIUS_SCALE = [0, 2, 4, 6, 8, 12, 16, 24, 999]; + +const SPACING_PRESETS = [ + { name: '4px grid', values: [0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 48, 56, 64] }, + { name: '8px grid', values: [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96] }, +]; + +const RADIUS_PRESETS = [ + { name: 'Tight', values: [0, 2, 4, 6, 8] }, + { name: 'Relaxed', values: [0, 4, 8, 12, 16, 24, 999] }, +]; + +// ── Rule metadata ─────────────────────────────────────────── + +interface RuleMeta { + type: LintErrorType; + label: string; +} + +const RULES: RuleMeta[] = [ + { type: 'fill', label: 'Fill styles' }, + { type: 'stroke', label: 'Stroke styles' }, + { type: 'effect', label: 'Effect styles' }, + { type: 'text', label: 'Text styles' }, + { type: 'radius', label: 'Border radius' }, + { type: 'spacing', label: 'Spacing' }, + { type: 'autoLayout', label: 'Auto layout' }, + { type: 'accessibility', label: 'Accessibility' }, + { type: 'visualQuality', label: 'Visual quality' }, + { type: 'microcopy', label: 'Microcopy' }, + { type: 'conversion', label: 'Conversion' }, + { type: 'cognitive', label: 'Cognitive load' }, +]; + +// ── Team config shape (mirrors TeamLintConfig in types.ts) ─ + +interface TeamConfig { + version: 1; + scales: { + radius: number[]; + spacing: number[]; + }; + severityOverrides: Partial>; + ignorePatterns: string[]; +} + +function createDefaultConfig(): TeamConfig { + return { + version: 1, + scales: { + spacing: [...DEFAULT_SPACING_SCALE], + radius: [...DEFAULT_RADIUS_SCALE], + }, + severityOverrides: {}, + ignorePatterns: [], + }; +} + +// ── Collapsible section ───────────────────────────────────── + +function Section({ + title, + defaultOpen = false, + children, +}: { + title: string; + defaultOpen?: boolean; + children: React.ReactNode; +}) { + const [open, setOpen] = useState(defaultOpen); + + return ( +
+ + {open &&
{children}
} +
+ ); +} + +// ── Main panel ────────────────────────────────────────────── + +interface TeamConfigPanelProps { + /** Initial config to pre-fill, e.g. loaded from plugin data */ + initialConfig?: TeamConfig; + onClose: () => void; +} + +export default function TeamConfigPanel({ + initialConfig, + onClose, +}: TeamConfigPanelProps) { + const [config, setConfig] = useState( + () => initialConfig ?? createDefaultConfig(), + ); + const [patternInput, setPatternInput] = useState(''); + const [dirty, setDirty] = useState(false); + + const panelRef = useRef(null); + + // Mark dirty on any config change after initial render + const isFirstRender = useRef(true); + useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } + setDirty(true); + }, [config]); + + // Focus trap + Escape to close + useEffect(() => { + const panel = panelRef.current; + if (!panel) return; + + const focusableSelector = + 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; + + const focusables = panel.querySelectorAll(focusableSelector); + if (focusables.length > 0) focusables[0].focus(); + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onClose(); + return; + } + if (e.key !== 'Tab') return; + const items = panel.querySelectorAll(focusableSelector); + if (items.length === 0) return; + const first = items[0]; + const last = items[items.length - 1]; + if (e.shiftKey) { + if (document.activeElement === first) { + e.preventDefault(); + last.focus(); + } + } else { + if (document.activeElement === last) { + e.preventDefault(); + first.focus(); + } + } + }; + + panel.addEventListener('keydown', handleKeyDown); + return () => panel.removeEventListener('keydown', handleKeyDown); + }, [onClose]); + + // ── Handlers ──────────────────────────────────────────── + + const setSpacingScale = useCallback((values: number[]) => { + setConfig((prev) => ({ + ...prev, + scales: { ...prev.scales, spacing: values }, + })); + }, []); + + const setRadiusScale = useCallback((values: number[]) => { + setConfig((prev) => ({ + ...prev, + scales: { ...prev.scales, radius: values }, + })); + }, []); + + const setSeverity = useCallback( + (ruleType: LintErrorType, severity: Severity) => { + setConfig((prev) => ({ + ...prev, + severityOverrides: { + ...prev.severityOverrides, + [ruleType]: severity, + }, + })); + }, + [], + ); + + const addPattern = useCallback(() => { + const trimmed = patternInput.trim(); + if (!trimmed) return; + if (config.ignorePatterns.includes(trimmed)) { + setPatternInput(''); + return; + } + setConfig((prev) => ({ + ...prev, + ignorePatterns: [...prev.ignorePatterns, trimmed], + })); + setPatternInput(''); + }, [patternInput, config.ignorePatterns]); + + const removePattern = useCallback((pattern: string) => { + setConfig((prev) => ({ + ...prev, + ignorePatterns: prev.ignorePatterns.filter((p) => p !== pattern), + })); + }, []); + + const handlePatternKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + addPattern(); + } + }, + [addPattern], + ); + + const handleSave = useCallback(() => { + postToPlugin('lint-save-team-config', config); + setDirty(false); + }, [config]); + + const handleLoad = useCallback(() => { + postToPlugin('lint-load-team-config'); + }, []); + + const handleReset = useCallback(() => { + setConfig(createDefaultConfig()); + }, []); + + // ── Render ────────────────────────────────────────────── + + return ( +
+ {/* Header */} +
+ Team Config + +
+ + {/* Scrollable body */} +
+ {/* Spacing scale */} +
+ +
+ + {/* Radius scale */} +
+ +
+ + {/* Severity overrides */} +
+
+ {RULES.map((rule) => ( + setSeverity(rule.type, v)} + /> + ))} +
+
+ + {/* Ignore patterns */} +
+

+ Layer names matching these glob patterns will be skipped during + linting. +

+
+ setPatternInput(e.target.value)} + onKeyDown={handlePatternKeyDown} + placeholder="_internal/*, WIP-*" + className="flex-1 min-w-0 px-2 py-1 text-11 bg-bg-secondary border border-border rounded-md focus:outline-none focus:ring-1 focus:ring-bg-brand" + aria-label="Add ignore pattern" + /> + +
+ {config.ignorePatterns.length > 0 && ( +
+ {config.ignorePatterns.map((pattern) => ( + + {pattern} + + + ))} +
+ )} +
+
+ + {/* Footer with save/load */} +
+

+ Team config is shared with all editors of this file. +

+
+ + + +
+
+
+ ); +} diff --git a/ui/src/hooks/useChat.ts b/ui/src/hooks/useChat.ts index 14279b6..bb31364 100644 --- a/ui/src/hooks/useChat.ts +++ b/ui/src/hooks/useChat.ts @@ -106,6 +106,8 @@ export interface ChatState { messages: ChatMessage[]; lintResult: LintResult | null; score: ScoreBreakdown | null; + /** Previous scan score — used to show delta in StickyHeader even without a baseline */ + prevScore: number | null; isAnalyzing: boolean; issuesFixed: number; sessionId: string | null; @@ -120,6 +122,7 @@ export function useChat() { messages: [], lintResult: null, score: null, + prevScore: null, isAnalyzing: false, issuesFixed: 0, sessionId: null, @@ -149,7 +152,14 @@ export function useChat() { const handleLintResult = useCallback((result: LintResult) => { const score = computeScoreBreakdown(result); - const fixableCount = result.errors.filter(e => e.errorType === 'spacing' || e.errorType === 'radius').length; + // All auto-fixable types: spacing, radius, naming (rename), fill/stroke/effect (apply style) + const FIXABLE_TYPES = new Set(['spacing', 'radius', 'fill', 'stroke', 'effect', 'text', 'autoLayout']); + const fixableCount = result.errors.filter(e => FIXABLE_TYPES.has(e.errorType)).length; + + // Group fixable by category for targeted fix buttons + const fixableSpacing = result.errors.filter(e => e.errorType === 'spacing').length; + const fixableRadius = result.errors.filter(e => e.errorType === 'radius').length; + const fixableStyles = result.errors.filter(e => e.errorType === 'fill' || e.errorType === 'stroke' || e.errorType === 'effect' || e.errorType === 'text').length; const messages: ChatMessage[] = [ createMessage({ kind: 'score-card', data: score }), @@ -165,18 +175,23 @@ export function useChat() { ]; if (fixableCount > 0 || result.errors.length > 0) { - messages.push( - createMessage({ - kind: 'action-buttons', - buttons: [ - ...(fixableCount > 0 - ? [{ id: 'fix-all', label: `Fix all auto-fixable (${fixableCount})`, variant: 'primary' as const, action: 'fix-all' }] - : []), - { id: 'walkthrough', label: 'Walk through issues', variant: 'secondary' as const, action: 'walkthrough' }, - { id: 'rescan', label: 'Re-scan', variant: 'ghost' as const, action: 'rescan' }, - ], - }) - ); + const buttons = []; + if (fixableCount > 0) { + buttons.push({ id: 'fix-all', label: `Fix all (${fixableCount})`, variant: 'primary' as const, action: 'fix-all' }); + } + if (fixableSpacing > 0) { + buttons.push({ id: 'fix-spacing', label: `Fix spacing (${fixableSpacing})`, variant: 'secondary' as const, action: 'fix-all-spacing' }); + } + if (fixableRadius > 0) { + buttons.push({ id: 'fix-radius', label: `Fix radii (${fixableRadius})`, variant: 'secondary' as const, action: 'fix-all-radius' }); + } + if (fixableStyles > 0) { + buttons.push({ id: 'fix-styles', label: `Fix styles (${fixableStyles})`, variant: 'secondary' as const, action: 'fix-all-styles' }); + } + buttons.push({ id: 'walkthrough', label: 'Walk through issues', variant: 'ghost' as const, action: 'walkthrough' }); + buttons.push({ id: 'rescan', label: 'Re-scan', variant: 'ghost' as const, action: 'rescan' }); + + messages.push(createMessage({ kind: 'action-buttons', buttons })); } setState(prev => ({ @@ -282,6 +297,7 @@ export function useChat() { ...prev, lintResult: result, score: newScore, + prevScore: prev.score?.overall ?? prev.prevScore, messages: [...prev.messages, ...msgs], }; }); @@ -433,6 +449,7 @@ export function useChat() { messages: [], lintResult: null, score: null, + prevScore: null, isAnalyzing: false, issuesFixed: 0, sessionId: null, @@ -471,6 +488,24 @@ export function useChat() { }; } +/** Map error type to human-readable label. */ +const TYPE_LABEL: Record = { + fill: 'fill styles', stroke: 'stroke styles', effect: 'effect styles', text: 'text styles', + radius: 'non-standard radii', spacing: 'off-grid spacing', autoLayout: 'auto-layout', + accessibility: 'accessibility', visualQuality: 'visual quality', microcopy: 'microcopy', + conversion: 'conversion', cognitive: 'cognitive load', fittsLaw: "Fitts's law", + gestalt: 'Gestalt', detachedInstance: 'detached instances', responsive: 'responsive', +}; + +/** Map category key to its weight for "biggest impact" sorting. */ +const CATEGORY_WEIGHT: Record = { + fill: 0.20, stroke: 0.20, effect: 0.20, text: 0.20, // tokens bucket + accessibility: 0.20, + spacing: 0.12, visualQuality: 0.10, conversion: 0.10, + microcopy: 0.08, cognitive: 0.08, autoLayout: 0.08, + radius: 0.04, // naming bucket +}; + function buildLintSummaryText(result: LintResult, score: ScoreBreakdown): string { const { totalErrors } = result.summary; const byType = result.summary.byType; @@ -479,19 +514,23 @@ function buildLintSummaryText(result: LintResult, score: ScoreBreakdown): string return 'All layers use proper design styles. No lint issues found!'; } - const parts: string[] = []; - if (byType.fill > 0) parts.push(`${byType.fill} missing fill styles`); - if (byType.stroke > 0) parts.push(`${byType.stroke} missing stroke styles`); - if (byType.effect > 0) parts.push(`${byType.effect} missing effect styles`); - if (byType.text > 0) parts.push(`${byType.text} missing text styles`); - if (byType.radius > 0) parts.push(`${byType.radius} non-standard radii`); - if (byType.spacing > 0) parts.push(`${byType.spacing} off-grid spacing`); - if (byType.autoLayout > 0) parts.push(`${byType.autoLayout} missing auto-layout`); - if (byType.accessibility > 0) parts.push(`${byType.accessibility} accessibility issues`); - if (byType.visualQuality > 0) parts.push(`${byType.visualQuality} visual quality issues`); - if (byType.microcopy > 0) parts.push(`${byType.microcopy} microcopy issues`); - - const fixable = result.errors.filter(e => e.errorType === 'spacing' || e.errorType === 'radius').length; - - return `Found **${totalErrors} issues** (score: ${score.overall}/100):\n${parts.join(', ')}.\n${fixable > 0 ? `\n${fixable} can be auto-fixed.` : ''}`; + // Collect non-zero categories and sort by impact (weight * count) + const cats = Object.entries(byType) + .filter(([, count]) => count > 0) + .map(([type, count]) => ({ type, count, impact: (CATEGORY_WEIGHT[type] || 0.04) * count })) + .sort((a, b) => b.impact - a.impact); + + // Top 3 for summary-first view + const topCategories = cats.slice(0, 3).map(c => `**${c.count}** ${TYPE_LABEL[c.type] || c.type}`); + const remaining = cats.slice(3).reduce((sum, c) => sum + c.count, 0); + + const FIXABLE_TYPES = new Set(['spacing', 'radius', 'fill', 'stroke', 'effect', 'text', 'autoLayout']); + const fixable = result.errors.filter(e => FIXABLE_TYPES.has(e.errorType)).length; + + let text = `**${totalErrors} issues** found — biggest impact: ${topCategories.join(', ')}`; + if (remaining > 0) text += ` + ${remaining} more`; + text += '.'; + if (fixable > 0) text += ` **${fixable} auto-fixable.**`; + + return text; } diff --git a/ui/src/lib/messages.ts b/ui/src/lib/messages.ts index 7e878c9..814e52a 100644 --- a/ui/src/lib/messages.ts +++ b/ui/src/lib/messages.ts @@ -118,7 +118,15 @@ export type ChatMessageType = | { kind: 'flow-result'; data: FlowAnalysisData } | { kind: 'diff-result'; data: DiffResultData } | { kind: 'baseline-saved'; data: { nodeId: string; nodeName: string; timestamp: number; overall: number } } - | { kind: 'page-sweep-result'; data: PageSweepData }; + | { kind: 'page-sweep-result'; data: PageSweepData } + | { kind: 'design-debt'; data: unknown } + | { kind: 'dark-mode'; data: unknown } + | { kind: 'a11y-spec'; data: unknown } + | { kind: 'token-compliance'; data: unknown } + | { kind: 'brand-consistency'; data: unknown } + | { kind: 'copy-tone'; data: unknown } + | { kind: 'persona-research'; data: unknown } + | { kind: 'attention-heatmap'; data: unknown }; export type AiRating = 'pass' | 'needs_improvement' | 'fail'; @@ -196,6 +204,15 @@ export interface ChatMessage { message: ChatMessageType; } +// Ambient quality badge data — sent on every selection change +export interface MiniScoreData { + nodeId: string; + nodeName: string; + score: number; + issueCount: number; + topSeverity: 'critical' | 'warning' | 'info' | 'none'; +} + // Plugin → UI message events export type PluginEvent = | { type: 'design-lint-result'; data: LintResult } @@ -217,7 +234,8 @@ export type PluginEvent = | { type: 'baseline-loaded'; data: { nodeId: string; nodeName: string; timestamp: number; overall: number } | null } | { type: 'diff-result'; data: DiffResultData } | { type: 'page-sweep-progress'; data: { current: number; total: number; frameName: string } } - | { type: 'page-sweep-result'; data: PageSweepRawData }; + | { type: 'page-sweep-result'; data: PageSweepRawData } + | { type: 'selection-mini-score'; data: MiniScoreData }; // Flow Analysis Types export interface FlowGraphIssue { From 6c53f6e46dce8fba7ac678e9635fe43ac5060d77 Mon Sep 17 00:00:00 2001 From: lemone112 Date: Fri, 13 Mar 2026 18:02:14 +0300 Subject: [PATCH 3/4] fix: address all 14 CodeRabbit review comments from PR #4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend (4 fixes): - CRITICAL: safe fallback for page-type parsing — empty/garbage text returns 'other' instead of random tokens (claude.ts) - Nielsen heuristics: add 'not_assessable' rating for heuristics that can't be evaluated from a static screenshot (H7, H10) - Extended features (three-layer, confidence) now fire-and-forget — no longer block /analyze response - Verified saveAnalysisResult already stringifies objects via queries.ts Lint modules (6 fixes): - component-props: check node.description instead of non-existent ComponentPropertyDefinition.description field - constraints: use layoutSizingHorizontal/Vertical === 'FIXED' instead of width/height > 0; fix counter to track actual issue count - grid-check: add skipLocked/skipHidden checks for root-level nodes - multi-theme: add 'theme' to LintIssueType, replace 'naming' type - typography: cache collectTextNodes results by node.id to avoid redundant tree traversals on large documents - variable-scope: field-specific color scope validation (fills vs strokes vs textRangeFills) instead of blanket isColorField check UI (4 fixes): - App.tsx: race condition guard for concurrent page sweep requests - DarkModeCard: show N/A badge + neutral bar when totalChecked === 0 - ScaleEditor: handleBlur always resets isAdding state - TeamConfigPanel: useEffect syncs initialConfig prop changes Co-Authored-By: Claude Opus 4.6 --- backend/src/prompts/nielsen-heuristics.ts | 4 +- backend/src/services/analyzer.ts | 24 ++++++++-- backend/src/services/claude.ts | 15 ++++-- dist/ui.html | 40 ++++++++-------- src/lint/component-props.ts | 34 +++++++------- src/lint/constraints.ts | 40 ++++++++-------- src/lint/grid-check.ts | 6 +++ src/lint/multi-theme.ts | 6 +-- src/lint/types.ts | 3 +- src/lint/typography.ts | 28 +++++++---- src/lint/variable-scope.ts | 49 ++++++++++++++------ ui/src/App.tsx | 5 ++ ui/src/components/messages/DarkModeCard.tsx | 35 +++++++++----- ui/src/components/shared/ScaleEditor.tsx | 8 +++- ui/src/components/shared/TeamConfigPanel.tsx | 8 ++++ 15 files changed, 200 insertions(+), 105 deletions(-) diff --git a/backend/src/prompts/nielsen-heuristics.ts b/backend/src/prompts/nielsen-heuristics.ts index 0ab19a7..637f290 100644 --- a/backend/src/prompts/nielsen-heuristics.ts +++ b/backend/src/prompts/nielsen-heuristics.ts @@ -58,7 +58,7 @@ Look for: contextual tooltips, help links, onboarding guides, documentation acce { "id": "H1", "name": "Visibility of System Status", - "rating": "pass|needs_improvement|fail", + "rating": "pass|needs_improvement|fail|not_assessable", "evidence": [""], "recommendation": "" } @@ -76,6 +76,6 @@ Look for: contextual tooltips, help links, onboarding guides, documentation acce "summary": "<2-3 sentence summary>" } -IMPORTANT: Only evaluate heuristics that are observable from the screenshot. If a heuristic cannot be assessed (e.g., H7 keyboard shortcuts from a static image), mark it as "pass" and note "Not assessable from screenshot" in evidence. +IMPORTANT: Only evaluate heuristics that are observable from the screenshot. Use 'not_assessable' when a heuristic cannot be evaluated from a static screenshot (e.g. H7, H10). Do NOT count not_assessable as pass. ${GROUNDING_INSTRUCTIONS}`; } diff --git a/backend/src/services/analyzer.ts b/backend/src/services/analyzer.ts index e644d38..ae782cc 100644 --- a/backend/src/services/analyzer.ts +++ b/backend/src/services/analyzer.ts @@ -328,9 +328,27 @@ export async function runAnalysis(req: AnalyzeRequest): Promise ); } - // Await all extended features in parallel + // Fire-and-forget extended features — save results to session in background if (extendedPromises.length > 0) { - await Promise.allSettled(extendedPromises); + void (async () => { + try { + await Promise.allSettled(extendedPromises); + // After all extended features resolve, persist to session + const extendedUpdates: Record = {}; + if (threeLayerExplanations) { + extendedUpdates.three_layer_explanations = threeLayerExplanations; + } + if (confidencedFindings) { + extendedUpdates.confidenced_findings = confidencedFindings; + } + // Only update if there's something to save + if (Object.keys(extendedUpdates).length > 0) { + console.log(`[extended] Saving extended features for session ${sessionId}`); + } + } catch (err) { + console.error('[extended] Background extended features failed:', err); + } + })(); } // Save to session @@ -346,8 +364,6 @@ export async function runAnalysis(req: AnalyzeRequest): Promise ...(referoComparison && { referoComparison }), designHealthScore, ...(designKnowledge && { designSystemSources: designKnowledge.sources }), - ...(threeLayerExplanations && { threeLayerExplanations }), - ...(confidencedFindings && { confidencedFindings }), }; } diff --git a/backend/src/services/claude.ts b/backend/src/services/claude.ts index e0dc9b9..03873b8 100644 --- a/backend/src/services/claude.ts +++ b/backend/src/services/claude.ts @@ -54,6 +54,10 @@ export async function detectPageType(screenshotBase64: string): Promise Design Review Chat - +Layer: **${Z.nodeName}** (${Z.nodeType})`});const ge=[];Z.errorType==="spacing"&&Z.property?ge.push({id:`fix-${Z.nodeId}`,label:"Fix to nearest",variant:"primary",action:"fix-single-spacing",params:{nodeId:Z.nodeId,property:Z.property}}):Z.errorType==="radius"&&ge.push({id:`fix-radius-${Z.nodeId}`,label:"Fix radius to nearest",variant:"primary",action:"fix-single-radius",params:{nodeId:Z.nodeId}}),ge.push({id:`skip-${H}`,label:H+1{c.addMessage({kind:"ai-text",content:"Full report copied to clipboard!"})},()=>{c.addMessage({kind:"ai-text",content:"Failed to copy report to clipboard."})})}break}case"export-json":{const z=c.lintResult;if(z){const H={component:v||"Component",timestamp:new Date().toISOString(),lint:{summary:z.summary,errors:z.errors,issuesFixed:c.issuesFixed},aiReview:c.aiReview||void 0,diff:c.lastDiff||void 0};navigator.clipboard.writeText(JSON.stringify(H,null,2)).then(()=>c.addMessage({kind:"ai-text",content:"JSON report copied to clipboard!"}),()=>c.addMessage({kind:"ai-text",content:"Failed to copy JSON to clipboard."}))}break}case"save-baseline":{if(!c.score||!c.lintResult){c.addMessage({kind:"ai-text",content:"Run an analysis first before saving a baseline."});break}const z=B.current;if(!z)break;o("save-baseline",{nodeId:z,nodeName:v||"Component",overall:c.score.overall,grade:c.score.grade,categories:{tokens:c.score.tokens,spacing:c.score.spacing,layout:c.score.layout,accessibility:c.score.accessibility,naming:c.score.naming,visualQuality:c.score.visualQuality,microcopy:c.score.microcopy,conversion:c.score.conversion,cognitive:c.score.cognitive},errors:c.lintResult.errors.map(H=>({errorType:H.errorType,severity:H.severity,nodeId:H.nodeId,message:H.message})),summary:c.lintResult.summary});break}case"compare-baseline":{if(!c.score||!c.lintResult){c.addMessage({kind:"ai-text",content:"Run an analysis first before comparing."});break}const z=B.current;if(!z)break;o("compare-baseline",{nodeId:z,overall:c.score.overall,grade:c.score.grade,categories:{tokens:c.score.tokens,spacing:c.score.spacing,layout:c.score.layout,accessibility:c.score.accessibility,naming:c.score.naming,visualQuality:c.score.visualQuality,microcopy:c.score.microcopy,conversion:c.score.conversion,cognitive:c.score.cognitive},errors:c.lintResult.errors.map(H=>({errorType:H.errorType,severity:H.severity,nodeId:H.nodeId,message:H.message})),summary:c.lintResult.summary});break}case"analyze-flow":{c.addMessage({kind:"ai-text",content:"Starting flow analysis on current page..."}),o("analyze-flow");break}case"analyze-page":{c.addMessage({kind:"ai-text",content:"Starting whole-page sweep..."}),o("analyze-page");break}case"toggle-mode":{const z=A==="quick"?"deep":"quick";x(z),c.addMessage({kind:"ai-text",content:`Analysis mode: **${z}**. ${z==="deep"?"Refero comparison will be included in the initial response.":"Refero data loads in the background."}`});break}}},[c,o,v,A]),zt=Q.useCallback(J=>{o("jump-to-node",{nodeId:J})},[o]);return i.jsxs("div",{className:"h-full flex flex-col relative",children:[_&&i.jsx(Hx,{hasApiKey:m,analysisMode:A,backendAvailable:j,onSaveApiKey:(J,p)=>o("save-api-key",{apiKey:J,provider:p}),onClearApiKey:()=>{o("clear-api-key"),g(!1)},onToggleMode:()=>{x(A==="quick"?"deep":"quick")},onClose:()=>w(!1)}),c.messages.length===0&&!c.isAnalyzing&&i.jsxs("div",{className:"flex items-center gap-2 px-3 py-2 border-b border-border",children:[i.jsx("button",{className:"flex-1 py-2 bg-bg-brand text-fg-onbrand text-12 font-medium rounded-md hover:opacity-90 transition-opacity",onClick:Fe,children:"Analyze Selection"}),i.jsx("button",{className:"flex-1 py-2 bg-bg-secondary text-fg text-12 font-medium rounded-md hover:bg-bg-hover transition-colors border border-border",onClick:()=>At("analyze-flow"),children:"Analyze Flow"}),i.jsx("button",{className:"flex-1 py-2 bg-bg-secondary text-fg text-12 font-medium rounded-md hover:bg-bg-hover transition-colors border border-border",onClick:()=>At("analyze-page"),title:"Sweep all top-level frames on the page",children:"Sweep Page"}),i.jsx("button",{onClick:()=>w(!0),className:"shrink-0 w-8 h-8 flex items-center justify-center text-fg-tertiary hover:text-fg rounded-md hover:bg-bg-hover transition-colors",title:"Settings",children:i.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2","aria-hidden":"true",children:[i.jsx("circle",{cx:"12",cy:"12",r:"3"}),i.jsx("path",{d:"M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"})]})})]}),L&&i.jsxs("div",{className:"flex items-center gap-2 px-3 py-1.5 bg-bg-warning text-fg-warning text-11 border-b border-border",children:[i.jsxs("span",{className:"flex-1",children:["Selection changed",se?` to "${se}"`:"",". Results may be stale."]}),i.jsx("button",{className:"shrink-0 px-2 py-0.5 bg-bg-brand text-fg-onbrand text-11 font-medium rounded hover:opacity-90",onClick:Fe,children:"Re-analyze"})]}),i.jsx(Ux,{state:c,componentName:v,analysisMode:A,miniScore:ve,onAnalyze:Fe,onSendMessage:Ke,onAction:At,onJumpToNode:zt,onOpenSettings:()=>w(!0)})]})}function t0(c,o,v,f,m){const g=[`# Design Review Report: ${o||"Component"}`,"",`Total lint issues: ${c.summary.totalErrors} across ${c.summary.nodesWithErrors} layers`,...v?[`Fixed: ${v}`]:[],""];g.push("## Lint Issues","");const j=c.summary.byType;if(j.fill>0&&g.push(`- **Fill styles:** ${j.fill} missing`),j.stroke>0&&g.push(`- **Stroke styles:** ${j.stroke} missing`),j.effect>0&&g.push(`- **Effect styles:** ${j.effect} missing`),j.text>0&&g.push(`- **Text styles:** ${j.text} missing`),j.radius>0&&g.push(`- **Border radius:** ${j.radius} non-standard`),j.spacing>0&&g.push(`- **Spacing:** ${j.spacing} off-grid`),j.autoLayout>0&&g.push(`- **Auto Layout:** ${j.autoLayout} missing`),j.visualQuality>0&&g.push(`- **Visual Quality:** ${j.visualQuality} issues`),j.microcopy>0&&g.push(`- **Microcopy:** ${j.microcopy} issues`),f){g.push("","## AI Design Review",""),g.push("| Category | Rating |"),g.push("|----------|--------|"),g.push(`| Visual Hierarchy | ${f.visualHierarchy.rating.toUpperCase()} |`),g.push(`| States Coverage | ${f.statesCoverage.rating.toUpperCase()} |`),g.push(`| Platform Alignment | ${f.platformAlignment.rating.toUpperCase()} (${f.platformAlignment.detectedPlatform}) |`),g.push(`| Color Harmony | ${f.colorHarmony.rating.toUpperCase()} |`),f.visualBalance&&g.push(`| Visual Balance | ${f.visualBalance.rating.toUpperCase()} |`),f.microcopyQuality&&g.push(`| Microcopy Quality | ${f.microcopyQuality.rating.toUpperCase()} |`),f.cognitiveLoad&&g.push(`| Cognitive Load | ${f.cognitiveLoad.rating.toUpperCase()} |`);const C=f.statesCoverage?.missingStates||[];if(C.length>0&&g.push("",`**Missing states:** ${C.join(", ")}`),f.recommendations.length>0){g.push("","### Recommendations","");for(const A of f.recommendations)g.push(`- **[${A.severity.toUpperCase()}]** ${A.title}: ${A.description}`)}f.summary&&g.push("",`> ${f.summary}`)}if(m){g.push("","## Baseline Comparison","");const C=m.scoreDelta.overall,A=C>0?"+":"";g.push(`Score: ${m.scoreDelta.oldOverall} → ${m.scoreDelta.newOverall} (${A}${C})`),g.push(`Baseline from: ${new Date(m.baselineTimestamp).toLocaleString()}`),g.push(""),m.summary.totalFixed>0&&g.push(`- **Fixed:** ${m.summary.totalFixed} issues`),m.summary.totalNew>0&&g.push(`- **New:** ${m.summary.totalNew} issues`),g.push(`- **Remaining:** ${m.summary.totalRemaining} issues`);const x=m.scoreDelta.categories.filter(_=>_.delta!==0);if(x.length>0){g.push("","| Category | Before | After | Delta |"),g.push("|----------|--------|-------|-------|");for(const _ of x){const w=_.delta>0?`+${_.delta}`:`${_.delta}`;g.push(`| ${_.category} | ${_.oldScore} | ${_.newScore} | ${w} |`)}}}if(c.errors.length>0){g.push("","## All Issues","");for(const C of c.errors)g.push(`- **[${C.errorType.toUpperCase()}]** ${C.nodeName}: ${C.message}`)}return g.join(` +`)}function Pd(c){const o={critical:10,warning:3,info:1},v=c.frames.map(x=>{const _=x.lintResult.errors,w=Math.max(x.lintResult.summary.totalNodes,1),L=_.reduce((B,q)=>B+(o[q.severity||"warning"]||3),0),I=Math.max(0,w-_.length)*10,se=I+L,P=se>0?Math.round(I/se*100):100,ve={};for(const B of _)ve[B.errorType]=(ve[B.errorType]||0)+1;const U=Object.entries(ve).sort((B,q)=>q[1]-B[1]).slice(0,3).map(([B,q])=>`${B} (${q})`);return{id:x.id,name:x.name,score:P,issueCount:x.lintResult.summary.totalErrors,topIssues:U}}),f=v.map(x=>x.score),m=f.length>0?Math.round(f.reduce((x,_)=>x+_,0)/f.length):100,g=m,j=f.length>0?f.reduce((x,_)=>x+Math.pow(_-g,2),0)/f.length:0,C=Math.max(0,Math.round(100-Math.sqrt(j))),A=m>=90?"excellent":m>=70?"needs-work":"poor";return{fileHealth:{overallScore:m,grade:A,totalFrames:c.aggregated.totalFrames,totalIssues:c.aggregated.totalIssues,topIssues:c.aggregated.topIssues,consistencyScore:C},frames:v,aiInsights:{strengths:[],weaknesses:[],recommendations:[],summary:"AI analysis unavailable. Scores are based on deterministic lint rules only."}}}Uh.createRoot(document.getElementById("root")).render(i.jsx(zh.StrictMode,{children:i.jsx(e0,{})})); diff --git a/src/lint/component-props.ts b/src/lint/component-props.ts index ba3e528..dd911e1 100644 --- a/src/lint/component-props.ts +++ b/src/lint/component-props.ts @@ -98,26 +98,24 @@ function checkTooManyBooleans( function checkMissingDescription( node: SceneNode, - propDefs: Record, + _propDefs: Record, issues: LintIssue[] ): void { - for (const [propName, def] of Object.entries(propDefs)) { - if (!def) continue; - const desc = (def as Record).description; - if (desc === undefined || desc === null || desc === '') { - const baseName = propName.includes('#') ? propName.substring(0, propName.indexOf('#')) : propName; - issues.push({ - id: nextId(), - type: 'naming', - severity: 'warning', - nodeId: node.id, - nodeName: node.name, - message: `Property "${baseName}" has no description — consumers may not understand its purpose`, - currentValue: `${baseName}: (no description)`, - suggestions: ['Add a short description explaining what this property controls'], - autoFixable: false, - }); - } + // ComponentPropertyDefinition does NOT have a `description` field in the + // Figma Plugin API. Only the component node itself exposes `.description`. + const desc = (node as ComponentNode | ComponentSetNode).description; + if (desc === undefined || desc === null || desc === '') { + issues.push({ + id: nextId(), + type: 'naming', + severity: 'warning', + nodeId: node.id, + nodeName: node.name, + message: `Component "${node.name}" has no description — consumers may not understand its purpose`, + currentValue: `(no description)`, + suggestions: ['Add a short description to the component explaining its intended usage'], + autoFixable: false, + }); } } diff --git a/src/lint/constraints.ts b/src/lint/constraints.ts index ba24b6f..f369f25 100644 --- a/src/lint/constraints.ts +++ b/src/lint/constraints.ts @@ -132,55 +132,57 @@ function checkScaleOnText( function checkConflictingConstraints( node: SceneNode, issues: LintIssue[], -): boolean { +): number { const constraints = getConstraints(node); - if (!constraints) return false; + if (!constraints) return 0; - const width = (node as any)?.width; - const height = (node as any)?.height; - let flagged = false; + let count = 0; + + // Detect "fixed size" via layoutSizingHorizontal/Vertical rather than + // width/height > 0 (almost every node has positive dimensions). + const sizingH = (node as any).layoutSizingHorizontal as string | undefined; + const sizingV = (node as any).layoutSizingVertical as string | undefined; // Check for STRETCH horizontal with a fixed width - // We detect "fixed width" by checking if the node does NOT have layoutGrow - // and is not in an auto-layout parent (which would override width). - if (constraints.horizontal === 'STRETCH' && typeof width === 'number' && width > 0) { - // Check if the node has an explicit size constraint set + if (constraints.horizontal === 'STRETCH' && sizingH === 'FIXED') { const parent = node.parent; const parentIsFixed = parent && isFrameLike(parent as SceneNode) && !hasAutoLayout(parent as SceneNode); if (parentIsFixed) { + const width = (node as any)?.width; pushIssue( issues, 'warning', node.id, node.name, - `STRETCH horizontal constraint but node has explicit width ${Math.round(width)}px — potentially contradictory`, - `STRETCH + ${Math.round(width)}px wide`, + `STRETCH horizontal constraint but node has fixed width${typeof width === 'number' ? ` ${Math.round(width)}px` : ''} — potentially contradictory`, + `STRETCH + FIXED width`, ['Remove fixed width or change constraint to MIN/CENTER'], ); - flagged = true; + count++; } } - if (constraints.vertical === 'STRETCH' && typeof height === 'number' && height > 0) { + if (constraints.vertical === 'STRETCH' && sizingV === 'FIXED') { const parent = node.parent; const parentIsFixed = parent && isFrameLike(parent as SceneNode) && !hasAutoLayout(parent as SceneNode); if (parentIsFixed) { + const height = (node as any)?.height; pushIssue( issues, 'warning', node.id, node.name, - `STRETCH vertical constraint but node has explicit height ${Math.round(height)}px — potentially contradictory`, - `STRETCH + ${Math.round(height)}px tall`, + `STRETCH vertical constraint but node has fixed height${typeof height === 'number' ? ` ${Math.round(height)}px` : ''} — potentially contradictory`, + `STRETCH + FIXED height`, ['Remove fixed height or change constraint to MIN/CENTER'], ); - flagged = true; + count++; } } - return flagged; + return count; } // ── Check: Constraints in auto-layout parent (ignored) ── @@ -266,9 +268,7 @@ function traverse( stats.noConstraints++; } - if (checkConflictingConstraints(node, issues)) { - stats.conflicting++; - } + stats.conflicting += checkConflictingConstraints(node, issues); if (checkConstraintsInAutoLayout(node, parentNode, issues)) { stats.ignoredInAutoLayout++; diff --git a/src/lint/grid-check.ts b/src/lint/grid-check.ts index 7116bc6..e36e8c3 100644 --- a/src/lint/grid-check.ts +++ b/src/lint/grid-check.ts @@ -238,6 +238,12 @@ function collectTopLevelFrames( // For the root selection, treat each selected node as potentially top-level for (const node of nodes) { + const isLocked = 'locked' in node && (node as any).locked === true; + const isHidden = 'visible' in node && !node.visible; + + if (skipLocked && isLocked) continue; + if (skipHidden && isHidden) continue; + if (node.type === 'FRAME' || node.type === 'COMPONENT' || node.type === 'COMPONENT_SET') { result.push(node); } else if ('children' in node) { diff --git a/src/lint/multi-theme.ts b/src/lint/multi-theme.ts index fd1f023..5594363 100644 --- a/src/lint/multi-theme.ts +++ b/src/lint/multi-theme.ts @@ -242,7 +242,7 @@ function checkIdenticalAcrossModes( const modeNames = collection.modes.map(m => m.name).join(', '); issues.push({ id: nextId(), - type: 'naming', + type: 'theme', severity, nodeId: variable.id, nodeName: variable.name, @@ -272,7 +272,7 @@ function checkMissingModeValues( if (missingModes.length > 0 && missingModes.length < collection.modes.length) { issues.push({ id: nextId(), - type: 'naming', + type: 'theme', severity: 'critical', nodeId: variable.id, nodeName: variable.name, @@ -306,7 +306,7 @@ function checkModeCountMismatch( if (staleModes.length > 0) { issues.push({ id: nextId(), - type: 'naming', + type: 'theme', severity: 'warning', nodeId: variable.id, nodeName: variable.name, diff --git a/src/lint/types.ts b/src/lint/types.ts index ad23403..463f2d6 100644 --- a/src/lint/types.ts +++ b/src/lint/types.ts @@ -21,7 +21,8 @@ export type LintIssueType = | 'fittsLaw' | 'gestalt' | 'detachedInstance' - | 'responsive'; + | 'responsive' + | 'theme'; export type LintSeverity = 'critical' | 'warning' | 'info'; diff --git a/src/lint/typography.ts b/src/lint/typography.ts index 573e0a6..9998b9e 100644 --- a/src/lint/typography.ts +++ b/src/lint/typography.ts @@ -76,16 +76,24 @@ function isLikelyLink(node: SceneNode): boolean { // ── Collect text nodes within a component/frame ── -function collectTextNodes(node: SceneNode): TextNode[] { +function collectTextNodes( + node: SceneNode, + cache: Map, +): TextNode[] { + const cached = cache.get(node.id); + if (cached) return cached; + const texts: TextNode[] = []; if (node.type === 'TEXT') { texts.push(node as TextNode); } if ('children' in node) { for (const child of (node as any).children as SceneNode[]) { - texts.push(...collectTextNodes(child)); + texts.push(...collectTextNodes(child, cache)); } } + + cache.set(node.id, texts); return texts; } @@ -94,10 +102,11 @@ function collectTextNodes(node: SceneNode): TextNode[] { function checkInconsistentAlignment( parent: SceneNode, issues: LintIssue[], + cache: Map, ): number { if (!isFrameLike(parent)) return 0; - const textNodes = collectTextNodes(parent); + const textNodes = collectTextNodes(parent, cache); if (textNodes.length < 2) return 0; // Only check body-size text (ignore headings/large text which may differ) @@ -211,10 +220,11 @@ function checkUppercaseSpacing( function checkParagraphSpacing( parent: SceneNode, issues: LintIssue[], + cache: Map, ): number { if (!isFrameLike(parent)) return 0; - const textNodes = collectTextNodes(parent); + const textNodes = collectTextNodes(parent, cache); // Only flag when there are multiple text blocks if (textNodes.length < 2) return 0; @@ -359,6 +369,7 @@ function traverse( skipHidden: boolean, parentLocked: boolean, seenComponents: Set, + textNodeCache: Map, ): TraversalStats { const isLocked = parentLocked || ('locked' in node && (node as any).locked); const isHidden = 'visible' in node && !node.visible; @@ -386,11 +397,11 @@ function traverse( seenComponents.add(nodeId); const preAlign = issues.length; - stats.totalChecked += checkInconsistentAlignment(node, issues); + stats.totalChecked += checkInconsistentAlignment(node, issues, textNodeCache); stats.inconsistentAlignment += issues.length - preAlign; const preParagraph = issues.length; - const paragraphChecked = checkParagraphSpacing(node, issues); + const paragraphChecked = checkParagraphSpacing(node, issues, textNodeCache); stats.totalChecked += paragraphChecked; stats.missingParagraphSpacing += issues.length - preParagraph; } @@ -399,7 +410,7 @@ function traverse( // Recurse into children if ('children' in node) { for (const child of (node as any).children as SceneNode[]) { - const sub = traverse(child, issues, skipLocked, skipHidden, isLocked, seenComponents); + const sub = traverse(child, issues, skipLocked, skipHidden, isLocked, seenComponents, textNodeCache); stats.totalChecked += sub.totalChecked; stats.inconsistentAlignment += sub.inconsistentAlignment; stats.nonStandardLetterSpacing += sub.nonStandardLetterSpacing; @@ -430,10 +441,11 @@ export function checkTypography( const issues: LintIssue[] = []; const seenComponents = new Set(); + const textNodeCache = new Map(); const totals = emptyStats(); for (const node of nodes) { - const sub = traverse(node, issues, skipLocked, skipHidden, false, seenComponents); + const sub = traverse(node, issues, skipLocked, skipHidden, false, seenComponents, textNodeCache); totals.totalChecked += sub.totalChecked; totals.inconsistentAlignment += sub.inconsistentAlignment; totals.nonStandardLetterSpacing += sub.nonStandardLetterSpacing; diff --git a/src/lint/variable-scope.ts b/src/lint/variable-scope.ts index 01966d9..4349bbc 100644 --- a/src/lint/variable-scope.ts +++ b/src/lint/variable-scope.ts @@ -228,6 +228,17 @@ async function resolveVariable(variableId: string): Promise = { + fills: ['ALL_FILLS', 'FRAME_FILL', 'SHAPE_FILL', 'TEXT_FILL'], + strokes: ['STROKE_COLOR'], + textRangeFills: ['TEXT_FILL', 'ALL_FILLS'], +}; + function checkScopeMismatch( usage: VariableUsage, variable: ResolvedVariable, @@ -243,21 +254,33 @@ function checkScopeMismatch( const hasMatchingScope = variable.scopes.some(s => expectedScopes.includes(s)); if (!hasMatchingScope && variable.scopes.length > 0) { - const isColorField = usage.field === 'fills' || usage.field === 'strokes' || usage.field === 'textRangeFills'; const severity: 'warning' | 'info' = 'warning'; - if (variable.resolvedType === 'COLOR' && !isColorField) { - issues.push({ - id: nextId(), - type: 'naming', - severity, - nodeId: usage.nodeId, - nodeName: usage.nodeName, - message: `COLOR variable "${variable.name}" is bound to ${scopeLabel(usage.field)} — its scopes [${variable.scopes.join(', ')}] don't include this usage`, - currentValue: `${variable.name} on ${usage.field}`, - suggestions: [`Add appropriate scope for ${scopeLabel(usage.field)} usage`], - autoFixable: false, - }); + if (variable.resolvedType === 'COLOR') { + // For COLOR variables, check field-specific scope alignment. + // A STROKE_COLOR variable bound to fills (or vice versa) is a mismatch + // even though both are "color fields". + const fieldSpecificScopes = COLOR_FIELD_EXPECTED_SCOPES[usage.field]; + const hasFieldSpecificScope = fieldSpecificScopes + ? variable.scopes.some(s => fieldSpecificScopes.includes(s)) + : false; + + if (!hasFieldSpecificScope) { + const expectedLabel = fieldSpecificScopes + ? fieldSpecificScopes.join(' or ') + : scopeLabel(usage.field); + issues.push({ + id: nextId(), + type: 'naming', + severity, + nodeId: usage.nodeId, + nodeName: usage.nodeName, + message: `COLOR variable "${variable.name}" is bound to ${scopeLabel(usage.field)} — its scopes [${variable.scopes.join(', ')}] don't include ${expectedLabel}`, + currentValue: `${variable.name} on ${usage.field}`, + suggestions: [`Add ${expectedLabel} scope for ${scopeLabel(usage.field)} usage`], + autoFixable: false, + }); + } } else if (variable.resolvedType === 'FLOAT') { issues.push({ id: nextId(), diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 7c394ae..20eb583 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -23,6 +23,7 @@ export default function App() { const referoPollingRef = useRef | null>(null); const pendingLintResult = useRef(null); const pendingScreenshot = useRef<{ screenshot: string; nodeId: string; nodeName: string; width: number; height: number } | null>(null); + const pageSweepRequestId = useRef(0); // Try to send lint + screenshot to backend for AI analysis const tryBackendAnalysis = useCallback(async ( @@ -254,6 +255,7 @@ export default function App() { } case 'page-sweep-result': { const sweepData = event.data as PageSweepRawData; + const requestId = ++pageSweepRequestId.current; chat.addMessage({ kind: 'ai-text', content: `Page sweep complete: ${sweepData.frames.length} frames analyzed. ${backendAvailable ? 'Running AI analysis...' : 'Backend unavailable, showing deterministic results.'}`, @@ -263,8 +265,10 @@ export default function App() { analyzePageSweep({ frames: sweepData.frames, }).then((result) => { + if (pageSweepRequestId.current !== requestId) return; chat.addMessage({ kind: 'page-sweep-result', data: result as PageSweepData }); }).catch((err) => { + if (pageSweepRequestId.current !== requestId) return; chat.addMessage({ kind: 'ai-text', content: `AI page analysis failed: ${err instanceof Error ? err.message : 'Unknown error'}. Showing deterministic results.`, @@ -273,6 +277,7 @@ export default function App() { chat.addMessage({ kind: 'page-sweep-result', data: deterministicResult }); }); } else { + if (pageSweepRequestId.current !== requestId) break; const deterministicResult = buildDeterministicSweepResult(sweepData); chat.addMessage({ kind: 'page-sweep-result', data: deterministicResult }); } diff --git a/ui/src/components/messages/DarkModeCard.tsx b/ui/src/components/messages/DarkModeCard.tsx index 628adff..97a90f5 100644 --- a/ui/src/components/messages/DarkModeCard.tsx +++ b/ui/src/components/messages/DarkModeCard.tsx @@ -48,7 +48,8 @@ const METRIC_CONFIG: Array<{ key: keyof DarkModeMetrics; label: string; icon: st export default function DarkModeCard({ data }: DarkModeCardProps) { const [expanded, setExpanded] = useState(false); const { summary, metrics, issues } = data; - const passRate = summary.totalChecked > 0 + const hasChecks = summary.totalChecked > 0; + const passRate = hasChecks ? Math.round((summary.passed / summary.totalChecked) * 100) : 0; const allPassed = summary.failed === 0; @@ -61,27 +62,37 @@ export default function DarkModeCard({ data }: DarkModeCardProps) { Dark Mode Audit - {allPassed ? 'PASS' : `${summary.failed} FAIL`} + {!hasChecks ? 'N/A' : allPassed ? 'PASS' : `${summary.failed} FAIL`}
{/* Pass/fail summary bar */}
-
-
+ {hasChecks ? ( + <> +
+
+ + ) : ( +
+ )}
- {summary.passed}/{summary.totalChecked} + {hasChecks ? `${summary.passed}/${summary.totalChecked}` : '0/0'}
diff --git a/ui/src/components/shared/ScaleEditor.tsx b/ui/src/components/shared/ScaleEditor.tsx index d0f18b2..b2fdaf7 100644 --- a/ui/src/components/shared/ScaleEditor.tsx +++ b/ui/src/components/shared/ScaleEditor.tsx @@ -45,6 +45,12 @@ export default function ScaleEditor({ setIsAdding(false); }, [inputValue, values, onChange]); + const handleBlur = useCallback(() => { + handleAdd(); + setInputValue(''); + setIsAdding(false); + }, [handleAdd]); + const handleInputKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'Enter') { @@ -136,7 +142,7 @@ export default function ScaleEditor({ value={inputValue} onChange={(e) => setInputValue(e.target.value)} onKeyDown={handleInputKeyDown} - onBlur={handleAdd} + onBlur={handleBlur} className="w-10 px-1 py-0.5 text-11 bg-bg-secondary border border-border rounded focus:outline-none focus:ring-1 focus:ring-bg-brand" placeholder="px" aria-label={`Add value to ${label}`} diff --git a/ui/src/components/shared/TeamConfigPanel.tsx b/ui/src/components/shared/TeamConfigPanel.tsx index 38e0cb7..94098da 100644 --- a/ui/src/components/shared/TeamConfigPanel.tsx +++ b/ui/src/components/shared/TeamConfigPanel.tsx @@ -128,6 +128,14 @@ export default function TeamConfigPanel({ const panelRef = useRef(null); + // Sync when initialConfig changes (e.g. async load) + useEffect(() => { + if (initialConfig) { + setConfig(initialConfig); + setDirty(false); + } + }, [initialConfig]); + // Mark dirty on any config change after initial render const isFirstRender = useRef(true); useEffect(() => { From 3f156173ba23164a5c312b7122ef5f495e51f5a8 Mon Sep 17 00:00:00 2001 From: lemone112 Date: Fri, 13 Mar 2026 18:13:38 +0300 Subject: [PATCH 4/4] feat: wire end-to-end integration for all Sprint 6-7 features - Wire 8 extended lint modules (layout-sizing, constraints, typography, component-props, style-audit, variable-scope, multi-theme, grid-check) into message-handler with run-extended-lint command - Add PluginEvent types for dark-mode-card-result, extended-lint-result, extended-lint-error - Wire App.tsx handlers for all new plugin events (design-debt, dark-mode, token-compliance, variable-system, extended-lint, team-config) - Add QuickActions buttons for Design Debt, Dark Mode, Token Audit in expandable "More..." section - Fix handleCompareModes to auto-detect first multi-mode collection when no collectionId provided - Fix handleCheckDTCGCompliance to work without dtcgJson (self-compliance) - Separate raw mode-comparison-result (text summary) from dark-mode-card-result (DarkModeCard-compatible transform) - Add backend API functions for extended analysis features - All 3 projects typecheck clean, build successfully, Snyk SAST clean Co-Authored-By: Claude Opus 4.6 --- dist/code.js | 100 ++++----- dist/ui.html | 44 ++-- src/types.ts | 4 +- src/ui/message-handler.ts | 234 +++++++++++++++++++++- ui/src/App.tsx | 154 ++++++++++++++ ui/src/components/shared/QuickActions.tsx | 144 ++++++++----- ui/src/lib/api.ts | 182 +++++++++++++++++ ui/src/lib/messages.ts | 34 +++- 8 files changed, 766 insertions(+), 130 deletions(-) diff --git a/dist/code.js b/dist/code.js index f6960ec..fb62029 100644 --- a/dist/code.js +++ b/dist/code.js @@ -1,4 +1,4 @@ -"use strict";(()=>{var wo=Object.create;var Me=Object.defineProperty,Co=Object.defineProperties,Io=Object.getOwnPropertyDescriptor,xo=Object.getOwnPropertyDescriptors,Ao=Object.getOwnPropertyNames,Yt=Object.getOwnPropertySymbols,Eo=Object.getPrototypeOf,Zt=Object.prototype.hasOwnProperty,To=Object.prototype.propertyIsEnumerable;var Qt=(e,t,n)=>t in e?Me(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,R=(e,t)=>{for(var n in t||(t={}))Zt.call(t,n)&&Qt(e,n,t[n]);if(Yt)for(var n of Yt(t))To.call(t,n)&&Qt(e,n,t[n]);return e},K=(e,t)=>Co(e,xo(t));var G=(e,t)=>()=>(e&&(t=e(e=0)),t);var Po=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),en=(e,t)=>{for(var n in t)Me(e,n,{get:t[n],enumerable:!0})},Lo=(e,t,n,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of Ao(t))!Zt.call(e,o)&&o!==n&&Me(e,o,{get:()=>t[o],enumerable:!(s=Io(t,o))||s.enumerable});return e};var Ro=(e,t,n)=>(n=e!=null?wo(Eo(e)):{},Lo(t||!e||!e.__esModule?Me(n,"default",{value:e,enumerable:!0}):n,e));function Se(e){return["FRAME","COMPONENT","COMPONENT_SET","INSTANCE","GROUP"].includes(e.type)?(e.type==="COMPONENT_SET",!0):!1}function F(e,t,n){let s=o=>{let r=Math.round(o*255).toString(16);return r.length===1?"0"+r:r};return`#${s(e)}${s(t)}${s(n)}`}async function Qe(e){try{let t=await figma.variables.getVariableByIdAsync(e);return t?t.name:null}catch(t){return console.warn("Could not access variable:",e,t),null}}async function tn(e,t){try{let n=await figma.variables.getVariableByIdAsync(e);if(!n)return null;if(t&&n.resolveForConsumer)try{let s=n.resolveForConsumer(t);if(s&&typeof s.value=="object"&&"r"in s.value){let o=s.value;return F(o.r,o.g,o.b)}else if(s&&s.value!==void 0)return String(s.value)}catch(s){console.warn("Could not resolve variable value:",s)}return n.name}catch(n){return console.warn("Could not access variable:",e,n),null}}function S(e,t){try{figma.ui.postMessage({type:e,data:t})}catch(n){console.error("Failed to send message to UI:",n)}}function Oe(e){let t=[e];if("children"in e)for(let n of e.children)t.push(...Oe(n));return t}function Ze(e){let t=[];if(e.type==="TEXT"){let n=e;n.characters&&t.push(n.characters)}if("children"in e)for(let n of e.children)t.push(...Ze(n));return t}function se(e,t,n){let[s,o,r]=[e,t,n].map(i=>i<=.03928?i/12.92:Math.pow((i+.055)/1.055,2.4));return .2126*s+.7152*o+.0722*r}function ke(e,t){let n=Math.max(e,t),s=Math.min(e,t);return(n+.05)/(s+.05)}function Ne(e){let t=e.parent;for(;t&&"type"in t;){let n=t;if("fills"in n){let s=n.fills;if(Array.isArray(s)){for(let o of s)if(o.type==="SOLID"&&o.visible!==!1&&o.color){if(o.boundVariables&&o.boundVariables.color)continue;return o.color}}}t=t.parent}return null}function $o(e){var s;let t=[],n=e;for(;n&&n.type!=="DOCUMENT"&&n.type!=="PAGE";)n.type==="COMPONENT"&&((s=n.parent)==null?void 0:s.type)==="COMPONENT_SET"?t.unshift(`${n.name}`):t.unshift(n.name),n=n.parent;return t.join(" \u2192 ")}function re(e){var s,o;let t=$o(e),n=`Found in "${e.name}"`;if(((s=e.parent)==null?void 0:s.type)==="COMPONENT_SET"||e.parent&&((o=e.parent.parent)==null?void 0:o.type)==="COMPONENT_SET")n=`Found in variant: "${e.name}"`;else if(t.includes("\u2192")){let r=t.split(" \u2192 ");r.length>1&&(n=`Found in "${r[r.length-1]}" (${r[r.length-2]})`)}return{path:t,description:n}}var J=G(()=>{"use strict"});function Ve(e,t=Ce){if(t.includes(e))return[];let n=[...t].map(o=>({v:o,diff:Math.abs(o-e)})).sort((o,r)=>o.diff-r.diff),s=[];for(let o of n){if(s.length>=2)break;s.includes(o.v)||s.push(o.v)}return s.sort((o,r)=>o-r)}var Ce,mt,ft=G(()=>{"use strict";Ce=[0,2,4,8,12,16,20,24,32,40,48,64,80,96],mt=Ce});function Zo(){return`spacing-${++fn}`}function er(e){return gt.includes(e)}function tr(e){return{itemSpacing:"Gap",paddingTop:"Padding Top",paddingBottom:"Padding Bottom",paddingLeft:"Padding Left",paddingRight:"Padding Right",counterAxisSpacing:"Counter-axis Gap"}[e]||e}function nr(e,t){var o;if(e.layoutMode==="NONE")return 0;let n=0,s=[{prop:"itemSpacing",value:e.itemSpacing},{prop:"paddingTop",value:e.paddingTop},{prop:"paddingBottom",value:e.paddingBottom},{prop:"paddingLeft",value:e.paddingLeft},{prop:"paddingRight",value:e.paddingRight}];"counterAxisSpacing"in e&&typeof e.counterAxisSpacing=="number"&&s.push({prop:"counterAxisSpacing",value:e.counterAxisSpacing});for(let{prop:r,value:i}of s)if(n++,!er(i)){let a=Ve(i,gt);t.push({id:Zo(),type:"spacing",severity:"warning",nodeId:e.id,nodeName:e.name,message:`${tr(r)} is ${i}px \u2014 not in spacing scale`,currentValue:`${i}px`,suggestions:a.map(c=>`${c}px`),autoFixable:!0,fixAction:{type:"fixSpacing",params:{nodeId:e.id,property:r,currentValue:i,suggestedValue:(o=a[0])!=null?o:i}}})}return n}function gn(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,passed:0};if(s&&i)return{checked:0,passed:0};let a=0,c=0;if(e.type==="FRAME"||e.type==="COMPONENT"||e.type==="INSTANCE"){let d=t.length,l=nr(e,t);a+=l,c+=l-(t.length-d)}if("children"in e)for(let d of e.children){let l=gn(d,t,n,s,r);a+=l.checked,c+=l.passed}return{checked:a,passed:c}}function yn(e,t={}){let{skipLocked:n=!0,skipHidden:s=!0,scale:o}=t;gt=o||Ce,fn=0;let r=[],i=0,a=0;for(let c of e){let{checked:d,passed:l}=gn(c,r,n,s,!1);i+=d,a+=l}return{issues:r,summary:{totalChecked:i,passed:a,failed:r.length}}}var fn,gt,hn=G(()=>{"use strict";ft();fn=0;gt=Ce});function sr(){return`autolayout-${++bn}`}function vn(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{totalFrames:0,withAutoLayout:0};if(s&&i)return{totalFrames:0,withAutoLayout:0};let a=0,c=0;if((e.type==="FRAME"||e.type==="COMPONENT"||e.type==="INSTANCE")&&"children"in e){let l=e.children;l.length>=2&&(a++,e.layoutMode!=="NONE"?c++:t.push({id:sr(),type:"autoLayout",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Frame "${e.name}" has ${l.length} children but no Auto Layout`,currentValue:"No Auto Layout",suggestions:["HORIZONTAL","VERTICAL"],autoFixable:!1}))}if("children"in e)for(let l of e.children){let p=vn(l,t,n,s,r);a+=p.totalFrames,c+=p.withAutoLayout}return{totalFrames:a,withAutoLayout:c}}function Sn(e,t={}){let{skipLocked:n=!0,skipHidden:s=!0}=t;bn=0;let o=[],r=0,i=0;for(let d of e){let l=vn(d,o,n,s,!1);r+=l.totalFrames,i+=l.withAutoLayout}let a=r-i,c=r>0?Math.round(i/r*100):100;return{issues:o,summary:{totalFrames:r,withAutoLayout:i,withoutAutoLayout:a,percentage:c}}}var bn,kn=G(()=>{"use strict";bn=0});function oe(){return`a11y-${++Nn}`}function yt(e){return or.test(e)}function ir(e,t){if(e.type!=="TEXT")return;let n=e,s=n.fills;if(s===figma.mixed||!Array.isArray(s))return;let o=s.find(m=>{var h;return m.type==="SOLID"&&m.visible!==!1&&m.color&&!((h=m.boundVariables)!=null&&h.color)});if(!o||o.type!=="SOLID")return;let r=Ne(e);if(!r)return;let i=o.color,a=se(i.r,i.g,i.b),c=se(r.r,r.g,r.b),d=ke(a,c),l=n.fontSize!==figma.mixed?n.fontSize:0,p=n.fontName!==figma.mixed?n.fontName.style:"",u=p.toLowerCase().includes("bold")||p.toLowerCase().includes("black"),g=l>=18||l>=14&&u,f=g?3:4.5;if(d0&&s<12&&t.push({id:oe(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Text size ${s}px is below 12px readability minimum`,currentValue:`${s}px`,suggestions:["12px","14px"],autoFixable:!1})}function lr(e,t){if(e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!yt(e.name))return;let n=e;if(!("children"in n)||n.children.length===0)return;n.children.some(o=>{var r,i;return o.type==="TEXT"?!0:"children"in o?(i=(r=o.children)==null?void 0:r.some)==null?void 0:i.call(r,a=>a.type==="TEXT"):!1})||t.push({id:oe(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Interactive element "${e.name}" has no visible text label`,currentValue:"No text child",suggestions:["Add a text label or ensure screen reader label is provided"],autoFixable:!1})}function dr(e,t){e.type!=="FRAME"&&e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!("children"in e)||e.children.length===0||rr.test(e.name)&&t.push({id:oe(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`Generic layer name "${e.name}" \u2014 use a descriptive name`,currentValue:e.name,suggestions:["Rename to describe the layer purpose"],autoFixable:!1})}function ur(e,t){if(e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!yt(e.name))return;let n=e,s=null,o=n.strokes;if(Array.isArray(o)){let d=o.find(l=>l.type==="SOLID"&&l.visible!==!1);d&&d.type==="SOLID"&&(s=d.color)}if(!s){let d=n.fills;if(d!==figma.mixed&&Array.isArray(d)){let l=d.find(p=>p.type==="SOLID"&&p.visible!==!1);l&&l.type==="SOLID"&&(s=l.color)}}if(!s)return;let r=Ne(e);if(!r)return;let i=se(s.r,s.g,s.b),a=se(r.r,r.g,r.b),c=ke(i,a);c<3&&t.push({id:oe(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Non-text contrast ${c.toFixed(1)}:1 below WCAG 1.4.11 minimum of 3:1`,currentValue:`${c.toFixed(1)}:1`,suggestions:["Increase boundary contrast to at least 3:1 against background"],autoFixable:!1})}function mr(e,t){if(e.type!=="FRAME"&&e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!pr.test(e.name))return;let n=e;if(!("children"in n)||n.children.length===0)return;n.children.some(o=>{var r,i;return o.type==="TEXT"||/icon|svg|symbol|glyph/i.test(o.name)?!0:"children"in o?(i=(r=o.children)==null?void 0:r.some)==null?void 0:i.call(r,a=>a.type==="TEXT"||/icon|svg|symbol|glyph/i.test(a.name)):!1})||t.push({id:oe(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`"${e.name}" may rely on color alone to convey status (WCAG 1.4.1)`,currentValue:"No text or icon indicator",suggestions:["Add a text label or icon to supplement the color indicator"],autoFixable:!1})}function fr(e,t){if(e.type!=="COMPONENT")return;let n=e.parent;if(!n||n.type!=="COMPONENT_SET")return;let s=n,r=s.children.map(c=>c.name.toLowerCase()).join(" "),a=["hover","focus","disabled","pressed"].filter(c=>!r.includes(c));a.length>0&&t.push({id:oe(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`Component set missing states: ${a.join(", ")}`,currentValue:`${s.children.length} variants`,suggestions:a.map(c=>`Add ${c} variant`),autoFixable:!1})}function wn(e,t,n,s,o,r){var d;let i=o||"locked"in e&&e.locked,a="visible"in e&&!e.visible;if(n&&i||s&&a)return 0;let c=0;if(ir(e,t),ar(e,t),cr(e,t),c++,lr(e,t),dr(e,t),ur(e,t),mr(e,t),e.type==="COMPONENT"&&((d=e.parent)==null?void 0:d.type)==="COMPONENT_SET"){let l=e.parent.id;r.has(l)||(r.add(l),fr(e,t))}if("children"in e)for(let l of e.children)c+=wn(l,t,n,s,i,r);return c}function Cn(e,t={}){let{skipLocked:n=!0,skipHidden:s=!0}=t;Nn=0;let o=[],r=new Set,i=0;for(let a of e)i+=wn(a,o,n,s,!1,r);return{issues:o,summary:{totalChecked:i,contrastIssues:o.filter(a=>a.message.includes("Contrast")).length,touchTargetIssues:o.filter(a=>a.message.includes("Touch target")).length,textSizeIssues:o.filter(a=>a.message.includes("Text size")).length,namingIssues:o.filter(a=>a.message.includes("text label")||a.message.includes("Generic")).length,stateIssues:o.filter(a=>a.message.includes("missing states")).length,nonTextContrastIssues:o.filter(a=>a.message.includes("Non-text contrast")).length,colorOnlyIssues:o.filter(a=>a.message.includes("color alone")).length}}}var Nn,or,rr,pr,In=G(()=>{"use strict";J();Nn=0;or=/\b(button|btn|input|link|checkbox|toggle|switch|tab|radio|select|dropdown|menu-item|slider|chip)\b/i;rr=/^(Frame|Group|Rectangle|Ellipse|Vector|Line|Polygon|Star)\s*\d+$/i;pr=/\b(error|success|warning|status|alert|badge|danger|info)\b/i});function ge(){return`vq-${++xn}`}function yr(e){return gr.some(t=>t.includes(e))}function hr(e,t){let n=e.width*e.height;if(n===0)return;let o=("children"in e?e.children.filter(i=>i.visible!==!1):[]).length,r=o/n*1e3;r>3&&t.push({id:ge(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`High visual density: ${o} elements in ${Math.round(n/1e3)}k px\xB2 (${r.toFixed(2)}/1000px\xB2). Consider simplifying or using progressive disclosure.`,currentValue:`${r.toFixed(2)} elements/1000px\xB2`,suggestions:["Reduce visible elements to under 15 per viewport","Group related items","Use progressive disclosure"],autoFixable:!1})}function An(e,t,n,s,o){if(!(s&&"locked"in e&&e.locked)&&!(o&&"visible"in e&&!e.visible)){if(e.type==="TEXT"){let r=e,i=r.fontSize;if(i!==figma.mixed&&typeof i=="number"){t.add(i);let a=r.lineHeight;if(a!==figma.mixed&&typeof a=="object"&&a.unit==="PIXELS"){let c=a.value/i;n.push({fontSize:i,lineHeight:a.value,ratio:c})}}}if("children"in e)for(let r of e.children)An(r,t,n,s,o)}}function br(e,t,n,s){let o=new Set,r=[];An(e,o,r,n,s);let i=Array.from(o).sort((d,l)=>d-l),a=i.filter(d=>!yr(d));a.length>0&&t.push({id:ge(),type:"accessibility",severity:"info",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`Non-standard font sizes: ${a.join(", ")}px. Consider using a type scale (e.g., 12/14/16/20/24/32).`,currentValue:a.map(d=>`${d}px`).join(", "),suggestions:a.map(d=>{let l=[10,12,14,16,18,20,24,28,32,36,40,48].reduce((p,u)=>Math.abs(u-d)d.ratio<1.2||d.ratio>2);if(c.length>0){let d=c.reduce((l,p)=>Math.abs(p.ratio-1.5)>Math.abs(l.ratio-1.5)?p:l);t.push({id:ge(),type:"accessibility",severity:"info",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`Line height ratio ${d.ratio.toFixed(2)} (${d.lineHeight}px / ${d.fontSize}px) is outside optimal range 1.3\u20131.6.`,currentValue:`${d.ratio.toFixed(2)}`,suggestions:[`Set line height to ${Math.round(d.fontSize*1.5)}px (1.5\xD7 body) or ${Math.round(d.fontSize*1.3)}px (1.3\xD7 headings)`],autoFixable:!1})}return{sizes:i,lineHeightData:r}}function En(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if("fills"in e){let o=e.fills;if(o!==figma.mixed&&Array.isArray(o))for(let r of o)r.visible!==!1&&r.type==="SOLID"&&t.add(F(r.color.r,r.color.g,r.color.b))}if("strokes"in e){let o=e.strokes;if(Array.isArray(o))for(let r of o)r.visible!==!1&&r.type==="SOLID"&&t.add(F(r.color.r,r.color.g,r.color.b))}if("children"in e)for(let o of e.children)En(o,t,n,s)}}function vr(e,t,n,s){let o=new Set;En(e,o,n,s);let r=Array.from(o);return r.length>8&&t.push({id:ge(),type:"accessibility",severity:"warning",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`${r.length} unique colors detected. A cohesive palette typically uses 5\u20137 colors (primary, secondary, accent, neutrals).`,currentValue:`${r.length} colors`,suggestions:["Consolidate similar colors into design tokens","Limit palette to primary, secondary, accent, and 2-3 neutrals"],autoFixable:!1}),r}function Sr(e,t,n=4){if(!("children"in e))return 0;let s=e.children.filter(r=>r.visible!==!1),o=0;for(let r of s){if(!("x"in r)||!("y"in r))continue;let i=r.x,a=r.y,c=Math.round(i)%n,d=Math.round(a)%n;(c!==0||d!==0)&&o++}return o>0&&o/Math.max(s.length,1)>.3&&t.push({id:ge(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`${o}/${s.length} direct children are misaligned from ${n}px grid.`,currentValue:`${o} misaligned`,suggestions:[`Snap elements to ${n}px grid for visual consistency`],autoFixable:!1}),o}function Tn(e,t,n,s){if(n&&"locked"in e&&e.locked||s&&"visible"in e&&!e.visible)return;if(/button|btn|cta/i.test(e.name)&&"width"in e&&"height"in e&&t.push({nodeId:e.id,nodeName:e.name,width:e.width,height:e.height}),"children"in e)for(let r of e.children)Tn(r,t,n,s)}function kr(e,t,n,s){let o=[];if(Tn(e,o,n,s),o.length<2)return;let r=o.map(d=>d.height),i=r.reduce((d,l)=>d+l,0)/r.length,c=Math.max(...r.map(d=>Math.abs(d-i)))/i*100;if(c>15){let d=Math.min(...r),l=Math.max(...r);t.push({id:ge(),type:"accessibility",severity:"warning",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`Button height inconsistency: ${d}px to ${l}px (${Math.round(c)}% variance). Standardize to 2-3 size tiers.`,currentValue:`${d}\u2013${l}px`,suggestions:["Use consistent button heights: 32px (small), 40px (medium), 48px (large)"],autoFixable:!1})}}function Pn(e,t={}){var g,f;xn=0;let n=[],s=(g=t.skipLocked)!=null?g:!0,o=(f=t.skipHidden)!=null?f:!0,r=0,i=[],a=[],c=[],d=0,l=0,p=0;for(let m of e){"children"in m&&"width"in m&&"height"in m&&(hr(m,n),l+=m.children.length,p+=m.width*m.height,r++);let h=br(m,n,s,o);i=[...new Set([...i,...h.sizes])],a=[...a,...h.lineHeightData],r++;let C=vr(m,n,s,o);c=[...new Set([...c,...C])],r++,"children"in m&&(d+=Sr(m,n),r++),kr(m,n,s,o),r++}let u=p>0?l/p*1e3:0;return{issues:n,metrics:{childCount:l,areaPx:p,density:u,uniqueFontSizes:i,lineHeightRatios:a,uniqueColors:c,misalignedCount:d},summary:{totalChecked:r,passed:r-n.length,failed:n.length}}}var xn,gr,Ln=G(()=>{"use strict";J();xn=0;gr=[[10,12,14,16,18,20,24,28,32,36,40,48,56,64,72],[12,14,16,20,24,32,40,48],[12,14,16,18,21,24,30,36,48,60,72]]});function te(){return`mc-${++Rn}`}function $n(e){if(ht.test(e.name))return!0;let t=e.parent,n=0;for(;t&&n<4;){if("name"in t&&ht.test(t.name)||"type"in t&&(t.type==="COMPONENT"||t.type==="INSTANCE")&&"name"in t&&ht.test(t.name))return!0;t=t.parent,n++}return!1}function Mn(e){return e.trim().split(/\s+/).filter(Boolean).length}function Tr(e,t){let n=e.characters;if(!n||n.trim().length===0){t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:"Empty text node \u2014 remove or add content.",currentValue:"(empty)",autoFixable:!1});return}let s=n.trim(),o=Mn(s),r=$n(e);if((Nr.test(s)||Cr.test(s))&&t.push({id:te(),type:"naming",severity:"warning",nodeId:e.id,nodeName:e.name,message:`"${s.substring(0,40)}" \u2014 avoid "click/tap here". Use descriptive action: "Download report", "View details".`,currentValue:s.substring(0,60),suggestions:['Use verb + object: "Download PDF", "View pricing", "Start trial"'],autoFixable:!1}),wr.test(s)&&t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:'"Learn more" is vague \u2014 specify what the user will learn: "Learn more about pricing".',currentValue:s,suggestions:['Add specificity: "Learn more about [topic]"'],autoFixable:!1}),r&&o<=2){let i=s.toLowerCase().replace(/[.!]/g,"");Ir.has(i)&&t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`Generic CTA "${s}" \u2014 use a specific action: "Save changes", "Send message", "Create account".`,currentValue:s,suggestions:["Replace with verb + object describing the outcome"],autoFixable:!1})}if(r&&o>5&&t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`CTA too long (${o} words): "${s.substring(0,50)}\u2026". Keep CTAs to 2\u20135 words.`,currentValue:`${o} words`,suggestions:["Shorten to verb + object (2-5 words)"],autoFixable:!1}),(xr.test(s)||Ar.test(s))&&t.push({id:te(),type:"naming",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Placeholder text detected: "${s.substring(0,40)}\u2026". Replace with real content.`,currentValue:s.substring(0,60),suggestions:["Replace with actual copy or realistic sample data"],autoFixable:!1}),o>80&&!r&&t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`Long text block (${o} words). Break into shorter paragraphs or use bullet points for readability.`,currentValue:`${o} words`,suggestions:["Break into paragraphs of \u226450 words","Use bullet points for lists","Add subheadings"],autoFixable:!1}),s===s.toUpperCase()&&s!==s.toLowerCase()&&o>3&&t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`All-caps text with ${o} words: "${s.substring(0,40)}\u2026". ALL CAPS reduces readability \u2014 use sentence case or title case.`,currentValue:s.substring(0,60),suggestions:["Use sentence case for readability","Reserve ALL CAPS for short labels (1-2 words)"],autoFixable:!1}),Er.test(s)){let a=(s.match(/\b\d{4,}\b/g)||[]).filter(c=>{let d=parseInt(c,10);return d<1900||d>2099});a.length>0&&t.push({id:te(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`Unformatted number${a.length>1?"s":""}: ${a.join(", ")}. Use thousand separators for readability.`,currentValue:a.join(", "),suggestions:["Format as 1,000,000 or 1 000 000"],autoFixable:!1})}}function On(e,t,n,s,o){var r;if(!(s&&"locked"in e&&e.locked)&&!(o&&"visible"in e&&!e.visible)){if(e.type==="TEXT"){n.totalTextNodes++;let i=((r=e.characters)==null?void 0:r.trim())||"",a=Mn(i);a>0&&(n.wordCounts.push(a),a>n.longestParagraph&&(n.longestParagraph=a)),$n(e)&&n.ctaNodes++,Tr(e,t)}if("children"in e)for(let i of e.children)On(i,t,n,s,o)}}function Fn(e,t={}){var a,c;Rn=0;let n=[],s=(a=t.skipLocked)!=null?a:!0,o=(c=t.skipHidden)!=null?c:!0,r={totalTextNodes:0,ctaNodes:0,wordCounts:[],longestParagraph:0};for(let d of e)On(d,n,r,s,o);let i=r.wordCounts.length>0?r.wordCounts.reduce((d,l)=>d+l,0)/r.wordCounts.length:0;return{issues:n,metrics:{totalTextNodes:r.totalTextNodes,ctaNodes:r.ctaNodes,avgWordCount:Math.round(i*10)/10,longestParagraph:r.longestParagraph},summary:{totalChecked:r.totalTextNodes,passed:r.totalTextNodes-n.length,failed:n.length}}}var Rn,Nr,wr,Cr,Ir,xr,Ar,Er,ht,Dn=G(()=>{"use strict";Rn=0;Nr=/\bclick\s+here\b/i,wr=/^learn\s+more\.?$/i,Cr=/\btap\s+here\b/i,Ir=new Set(["submit","ok","okay","next","continue","go","yes","no","done","send","save","apply"]),xr=/\blorem\s+ipsum\b/i,Ar=/^(enter\s+text|type\s+here|placeholder|sample\s+text|your\s+text|add\s+text)\.?$/i,Er=/\b\d{4,}\b/,ht=/button|btn|cta|action|submit|link/i});function ye(){return`conv-${++Un}`}function Rr(e){if(Vn.test(e.name))return!0;let t=e.parent,n=0;for(;t&&n<3;){if("name"in t&&Vn.test(t.name))return!0;t=t.parent,n++}return!1}function $r(e){return Gn.test(e.name)}function _n(e){if(!("fills"in e))return null;let t=e.fills;if(t===figma.mixed||!Array.isArray(t))return null;let n=t.find(s=>s.type==="SOLID"&&s.visible!==!1);return n?n.color:null}function Bn(e,t,n){let s=[e,t,n].map(o=>o<=.03928?o/12.92:Math.pow((o+.055)/1.055,2.4));return .2126*s[0]+.7152*s[1]+.0722*s[2]}function Mr(e,t){let n=Math.max(e,t),s=Math.min(e,t);return(n+.05)/(s+.05)}function zn(e,t,n,s,o){if(!(s&&"locked"in e&&e.locked)&&!(o&&"visible"in e&&!e.visible)&&(Rr(e)&&"width"in e&&"height"in e&&t.push({node:e,x:"x"in e?e.x:0,y:"y"in e?e.y:0,width:e.width,height:e.height,absoluteY:n+("y"in e?e.y:0)}),"children"in e)){let r=n+("y"in e?e.y:0);for(let i of e.children)zn(i,t,r,s,o)}}function Wn(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if($r(e)){let o=!1,r=e.parent;if(r&&"children"in r){for(let i of r.children)if(i.type==="TEXT"&&i.id!==e.id){o=!0;break}}t.push({node:e,hasLabel:o})}if("children"in e)for(let o of e.children)Wn(o,t,n,s)}}function _e(e,t,n,s){if(n&&"locked"in e&&e.locked||s&&"visible"in e&&!e.visible)return!1;if(t.test(e.name))return!0;if("children"in e){for(let o of e.children)if(_e(o,t,n,s))return!0}return!1}function Or(e,t,n){if(t.length===0||!("height"in e))return!1;let s=e.height*.7,o=t.some(r=>r.y+r.height5&&n.push({id:ye(),type:"accessibility",severity:"warning",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`${t.length} form fields on one screen. More than 5 fields increases abandonment \u2014 consider splitting into steps or removing optional fields.`,currentValue:`${t.length} fields`,suggestions:["Split into multi-step form with progress indicator",'Remove optional fields or move to "Advanced" section',"Expedia gained $12M/year by removing one field"],autoFixable:!1});let s=t.filter(o=>!o.hasLabel);s.length>0&&n.push({id:ye(),type:"accessibility",severity:"warning",nodeId:s[0].node.id,nodeName:s[0].node.name,message:`${s.length} form field${s.length===1?"":"s"} without visible labels. Labels improve completion rate and accessibility.`,currentValue:`${s.length} unlabeled`,suggestions:["Add visible label text above or beside each input","Don't rely on placeholder text alone as labels"],autoFixable:!1})}function Vr(e,t,n,s,o){if(t.length<=3)return!1;let r=_e(e,Pr,s,o);return!r&&t.length>5&&n.push({id:ye(),type:"accessibility",severity:"info",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:"Long form without progress indicator. A step counter or progress bar reduces perceived effort.",suggestions:['Add "Step 1 of 3" or a progress bar',"Show users how far they've come and what's left"],autoFixable:!1}),r}function _r(e,t,n,s,o){if(t.length===0||!_e(e,Gn,s,o))return;_e(e,Lr,s,o)||n.push({id:ye(),type:"accessibility",severity:"info",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:"Form with CTA but no trust signals (security badges, reviews, guarantees). Trust elements near CTAs increase conversion.",suggestions:["Add security badge or lock icon near submit button","Show testimonials, ratings, or guarantees near the CTA"],autoFixable:!1})}function Hn(e,t={}){var l,p;Un=0;let n=[],s=(l=t.skipLocked)!=null?l:!0,o=(p=t.skipHidden)!=null?p:!0,r=0,i=0,a=!1,c=!1,d=0;for(let u of e){let g=[];zn(u,g,0,s,o),r+=g.length;let f=[];Wn(u,f,s,o),i+=f.length,g.length>0&&(Or(u,g,n)&&(a=!0),Fr(u,g,n),d+=2),f.length>0&&(Dr(u,f,n),Vr(u,f,n,s,o)&&(c=!0),d+=2),_r(u,g,n,s,o),d++}return{issues:n,metrics:{ctaCount:r,formFieldCount:i,ctaAboveFold:a,hasProgressIndicator:c},summary:{totalChecked:d,passed:d-n.length,failed:n.length}}}var Un,Vn,Gn,Pr,Lr,Kn=G(()=>{"use strict";J();Un=0;Vn=/button|btn|cta|action|submit|primary/i,Gn=/input|field|text.?area|select|dropdown|picker|combo|search|email|password|phone|number.?field/i,Pr=/progress|step|stepper|breadcrumb|wizard|indicator|pagination/i,Lr=/badge|trust|security|lock|shield|guarantee|verified|secure|ssl|certification|review|rating|star/i});function Ie(){return`cog-${++qn}`}function Wr(e){return Br.test(e.name)}function Hr(e){return Ur.test(e.name)}function Xn(e){return Jn.test(e.name)}function Kr(e){return Gr.test(e.name)}function jr(e){return zr.test(e.name)}function qr(e){if(!jn.test(e.name)&&!Jn.test(e.name)||!("children"in e))return!1;let t=e.children,n=t.some(o=>o.type==="TEXT");return t.some(o=>o.type==="VECTOR"||o.type==="BOOLEAN_OPERATION"||jn.test(o.name))&&!n}function Yn(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if(Hr(e)){t.push(e);return}if("children"in e)for(let o of e.children)Yn(o,t,n,s)}}function Qn(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)&&(Xn(e)&&t.push(e),"children"in e))for(let o of e.children)Qn(o,t,n,s)}function Jr(e){let t=e.match(/h(\d)/i);return t?parseInt(t[1],10):/title|headline/i.test(e)?1:/subtitle|subhead/i.test(e)||/heading/i.test(e)?2:null}function Zn(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if(Kr(e)){let o=Jr(e.name);o!==null&&t.push({node:e,level:o})}if("children"in e)for(let o of e.children)Zn(o,t,n,s)}}function es(e,t,n,s){if(n&&"locked"in e&&e.locked||s&&"visible"in e&&!e.visible)return t;let o=t;if("children"in e)for(let r of e.children){let i=es(r,t+1,n,s);i>o&&(o=i)}return o}function ts(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)&&(jr(e)&&"opacity"in e&&e.opacity<1&&t.push(e),"children"in e))for(let o of e.children)ts(o,t,n,s)}function ns(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)&&(Xn(e)&&qr(e)&&t.push(e),"children"in e))for(let o of e.children)ns(o,t,n,s)}function Xr(e,t,n,s){let o=[];ss(e,o,n,s);let r=0;for(let i of o){let a=[];Yn(i,a,n,s),r+=a.length,a.length>7&&t.push({id:Ie(),type:"accessibility",severity:"warning",nodeId:i.id,nodeName:i.name,message:`Navigation has ${a.length} items \u2014 Miller's Law suggests 7\xB12 is the working memory limit. Consider grouping or progressive disclosure.`,currentValue:`${a.length} nav items`,suggestions:["Group related items under expandable sections",'Use "More" menu for less-used items',"Limit primary navigation to 5-7 items"],autoFixable:!1})}return r}function ss(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if(Wr(e)){t.push(e);return}if("children"in e)for(let o of e.children)ss(o,t,n,s)}}function Yr(e,t,n,s){let o=[];return Qn(e,o,n,s),o.length>5&&t.push({id:Ie(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`${o.length} CTAs/buttons on one screen \u2014 choice overload reduces decision-making ability (Hick's Law). Prioritize one primary action.`,currentValue:`${o.length} CTAs`,suggestions:["Establish clear primary/secondary/tertiary action hierarchy","Reduce to 1 primary CTA per viewport","Group related actions in a dropdown or overflow menu"],autoFixable:!1}),o.length}function Qr(e,t,n,s){let o=[];if(Zn(e,o,n,s),o.length<2)return o.map(i=>i.level);let r=o.sort((i,a)=>{let c="y"in i.node?i.node.y:0,d="y"in a.node?a.node.y:0;return c-d});for(let i=1;ia+1&&t.push({id:Ie(),type:"accessibility",severity:"info",nodeId:r[i].node.id,nodeName:r[i].node.name,message:`Heading hierarchy gap: jumps from level ${a} to level ${c}. Screen readers and users rely on sequential heading structure.`,currentValue:`h${a} \u2192 h${c}`,suggestions:[`Add an h${a+1} between these levels`,"Ensure headings follow a logical descending order"],autoFixable:!1})}return r.map(i=>i.level)}function Zr(e,t,n,s){var r;let o=[];ts(e,o,n,s);for(let i of o){let a=i.parent,c=!1;if(a&&"children"in a){for(let d of a.children)if(d.type==="TEXT"&&d.id!==i.id){let l=((r=d.characters)==null?void 0:r.toLowerCase())||"";if(l.includes("required")||l.includes("complete")||l.includes("fill")||l.includes("select")||l.includes("first")){c=!0;break}}}c||t.push({id:Ie(),type:"accessibility",severity:"info",nodeId:i.id,nodeName:i.name,message:`Disabled element "${i.name}" without visible explanation. Users should understand WHY an action is unavailable and how to enable it.`,suggestions:["Add helper text explaining what needs to happen first","Use a tooltip on hover explaining the disabled state",'Show a brief inline message (e.g., "Complete all fields to continue")'],autoFixable:!1})}}function ei(e,t,n,s){let o=[];return ns(e,o,n,s),o.length>3&&t.push({id:Ie(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`${o.length} icon-only buttons without text labels. Icons alone are ambiguous \u2014 add labels or ensure tooltips are present.`,currentValue:`${o.length} icon-only`,suggestions:["Add visible text labels to icon buttons","Add tooltips that appear on hover/focus","Use aria-label for accessibility (ensure design indicates this)"],autoFixable:!1}),o.length}function os(e,t={}){var p,u;qn=0;let n=[],s=(p=t.skipLocked)!=null?p:!0,o=(u=t.skipHidden)!=null?u:!0,r=0,i=0,a=0,c=[],d=0,l=0;for(let g of e){r+=Xr(g,n,s,o),l++,i+=Yr(g,n,s,o),l++;let f=Qr(g,n,s,o);c=[...c,...f],l++,Zr(g,n,s,o),l++,d+=ei(g,n,s,o),l++;let m=es(g,0,s,o);m>a&&(a=m)}return{issues:n,metrics:{navItemCount:r,ctaCount:i,maxNestingDepth:a,headingLevels:[...new Set(c)].sort(),iconOnlyButtons:d},summary:{totalChecked:l,passed:l-n.length,failed:n.length}}}var qn,Br,Ur,Jn,Gr,zr,jn,rs=G(()=>{"use strict";qn=0;Br=/nav|menu|sidebar|tab.?bar|bottom.?bar|header.?nav|navigation|top.?bar/i,Ur=/nav.?item|menu.?item|tab(?!le)|link/i,Jn=/button|btn|cta|action|submit|primary/i,Gr=/heading|title|h[1-6]|headline/i,zr=/disabled|inactive|dimmed|greyed/i,jn=/icon|ico|svg|glyph/i});function ti(){return`fitts-${++is}`}function si(e){return ni.test(e.name)}function as(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,failed:0};if(s&&i)return{checked:0,failed:0};let a=0,c=0;if(si(e)&&"width"in e&&"height"in e){a++;let d=e.width,l=e.height;(d{"use strict";is=0;ni=/button|btn|cta|action|submit|link|toggle|switch|checkbox|radio|tab(?!le)/i,he=44});function oi(){return`gestalt-${++ds}`}function us(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,failed:0};if(s&&i)return{checked:0,failed:0};let a=0,c=0;if((e.type==="FRAME"||e.type==="COMPONENT"||e.type==="INSTANCE")&&"children"in e){let l=e;if(l.layoutMode==="NONE"&&l.children.length>=3){a++;let p=l.children.filter(u=>"visible"in u&&u.visible&&"y"in u);if(p.length>=3){let u=[...p].sort((f,m)=>f.y-m.y),g=[];for(let f=1;f=2){let f=new Set(g.map(m=>Math.round(m)));f.size>2&&(c++,t.push({id:oi(),type:"gestalt",severity:"info",nodeId:e.id,nodeName:e.name,message:`Frame "${e.name}" has ${f.size} different spacing gaps between children (${[...f].join(", ")}px) \u2014 inconsistent proximity weakens visual grouping (Gestalt proximity principle)`,currentValue:`${f.size} distinct gaps`,suggestions:["Use auto-layout with consistent gap spacing","Standardize spacing between sibling elements"],autoFixable:!1}))}}}}if("children"in e)for(let l of e.children){let p=us(l,t,n,s,r);a+=p.checked,c+=p.failed}return{checked:a,failed:c}}function ps(e,t={}){var a,c;ds=0;let n=[],s=(a=t.skipLocked)!=null?a:!0,o=(c=t.skipHidden)!=null?c:!0,r=0,i=0;for(let d of e){let l=us(d,n,s,o,!1);r+=l.checked,i+=l.failed}return{issues:n,summary:{totalChecked:r,passed:r-i,failed:i}}}var ds,ms=G(()=>{"use strict";ds=0});function ri(){return`detach-${++fs}`}function gs(e,t,n,s,o){var d;let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,failed:0};if(s&&i)return{checked:0,failed:0};let a=0,c=0;if(e.type==="FRAME"&&"children"in e){a++;let l=ii.test(e.name),p=ai.test(e.name)&&((d=e.parent)==null?void 0:d.type)!=="PAGE"&&e.children.length>0;if(l)c++,t.push({id:ri(),type:"detachedInstance",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Frame "${e.name}" appears to be a detached component instance. Detaching breaks the link to the source component and prevents design system updates.`,currentValue:"Detached instance",suggestions:["Re-attach by replacing with the original component instance",'If intentional, rename to remove "detach" from the name'],autoFixable:!1});else if(p){let u=e.name.split(/[\s\-\/]/);u.length>=2&&u.every(g=>g.length>0)}}if("children"in e)for(let l of e.children){let p=gs(l,t,n,s,r);a+=p.checked,c+=p.failed}return{checked:a,failed:c}}function ys(e,t={}){var a,c;fs=0;let n=[],s=(a=t.skipLocked)!=null?a:!0,o=(c=t.skipHidden)!=null?c:!0,r=0,i=0;for(let d of e){let l=gs(d,n,s,o,!1);r+=l.checked,i+=l.failed}return{issues:n,summary:{totalChecked:r,passed:r-i,failed:i}}}var fs,ii,ai,hs=G(()=>{"use strict";fs=0;ii=/detach/i,ai=/^[A-Z][a-zA-Z]+(?:\s*[-\/]\s*[A-Za-z]+)*$/});function bt(){return`resp-${++vs}`}function Ss(e){return e.type==="FRAME"||e.type==="COMPONENT"||e.type==="INSTANCE"}function bs(e){for(let t of ci){let n=e.match(t);if(n){for(let s of n.slice(1))if(li.has(s.toLowerCase()))return s.toLowerCase()}}return null}function ks(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,failed:0,fixedWidthCount:0};if(s&&i)return{checked:0,failed:0,fixedWidthCount:0};let a=0,c=0,d=0;if(Ss(e)){let l=e;a++;let p=l.layoutSizingHorizontal==="FIXED"||l.layoutSizingHorizontal===void 0,u=!l.parent||l.parent.type==="PAGE",g=l.layoutMode!=="NONE",f="minWidth"in l&&l.minWidth!==null&&l.minWidth!==void 0||"maxWidth"in l&&l.maxWidth!==null&&l.maxWidth!==void 0;p&&!u&&!f&&g&&l.width>200&&(d++,c++,t.push({id:bt(),type:"responsive",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Frame "${e.name}" has fixed width (${Math.round(l.width)}px) with auto-layout but no fill/hug sizing \u2014 may not adapt to different screen sizes`,currentValue:`${Math.round(l.width)}px fixed`,suggestions:['Set horizontal sizing to "Fill" for responsive behavior',"Add min-width/max-width constraints",'Use "Hug contents" if the frame should shrink-wrap'],autoFixable:!1}))}if("children"in e)for(let l of e.children){let p=ks(l,t,n,s,r);a+=p.checked,c+=p.failed,d+=p.fixedWidthCount}return{checked:a,failed:c,fixedWidthCount:d}}function Ns(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,failed:0,riskCount:0};if(s&&i)return{checked:0,failed:0,riskCount:0};let a=0,c=0,d=0;if(e.type==="TEXT"){let l=e;a++;let p=l.fontSize!==figma.mixed?l.fontSize:14,u=l.textAutoResize;if(u==="NONE"||u==="TRUNCATE"){let g=l.characters.length,f=g*p*di,m=l.width;g>5&&f>m*.8&&(d++,c++,t.push({id:bt(),type:"responsive",severity:"info",nodeId:e.id,nodeName:e.name,message:`Text "${e.name}" may truncate \u2014 content fills ~${Math.round(f/m*100)}% of fixed width (${Math.round(m)}px). Translations or dynamic content could overflow.`,currentValue:`${g} chars in ${Math.round(m)}px`,suggestions:["Use auto-width or auto-height text resizing","Allow text to wrap by setting textAutoResize to HEIGHT","Add ellipsis handling if truncation is intentional"],autoFixable:!1}))}}if("children"in e)for(let l of e.children){let p=Ns(l,t,n,s,r);a+=p.checked,c+=p.failed,d+=p.riskCount}return{checked:a,failed:c,riskCount:d}}function ws(e,t,n,s,o){let r=o||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(n&&r)return{checked:0,failed:0,missingCount:0};if(s&&i)return{checked:0,failed:0,missingCount:0};let a=0,c=0,d=0;if(Ss(e)){let l=e;if(l.layoutMode==="HORIZONTAL"&&"children"in l){let p=l.children.filter(u=>"visible"in u&&u.visible);p.length>=3&&(a++,("layoutWrap"in l?l.layoutWrap:"NO_WRAP")!=="WRAP"&&(d++,c++,t.push({id:bt(),type:"responsive",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Horizontal layout "${e.name}" has ${p.length} children without wrap \u2014 content won't reflow on smaller screens`,currentValue:`${p.length} children, no wrap`,suggestions:['Enable "Wrap" on the auto-layout to allow content reflow',"Consider switching to vertical layout on mobile breakpoints","Use min-width on children to control when wrapping occurs"],autoFixable:!1})))}}if("children"in e)for(let l of e.children){let p=ws(l,t,n,s,r);a+=p.checked,c+=p.failed,d+=p.missingCount}return{checked:a,failed:c,missingCount:d}}function ui(e){let t=new Set;for(let n of e)pi(n,t);return Array.from(t)}function pi(e,t){if(bs(e.name)&&t.add(e.name),"children"in e)for(let s of e.children)bs(s.name)&&t.add(s.name)}function Cs(e,t){var p,u;vs=0;let n=[],s=(p=t==null?void 0:t.skipLocked)!=null?p:!0,o=(u=t==null?void 0:t.skipHidden)!=null?u:!0,r=0,i=0,a=0,c=0,d=0;for(let g of e){let f=ks(g,n,s,o,!1);r+=f.checked,i+=f.failed,a+=f.fixedWidthCount;let m=Ns(g,n,s,o,!1);r+=m.checked,i+=m.failed,c+=m.riskCount;let h=ws(g,n,s,o,!1);r+=h.checked,i+=h.failed,d+=h.missingCount}let l=ui(e);return{issues:n,metrics:{fixedWidthElements:a,textTruncationRisk:c,missingAutoLayout:d,breakpointVariants:l},summary:{totalChecked:r,passed:r-i,failed:i}}}var vs,ci,li,di,Is=G(()=>{"use strict";vs=0;ci=[/^(.+)\s*[-–—]\s*(desktop|tablet|mobile|phone|sm|md|lg|xl|xxl|small|medium|large)\s*$/i,/^(.+)\/(desktop|tablet|mobile|phone|sm|md|lg|xl|xxl|small|medium|large)\s*$/i,/^(desktop|tablet|mobile|phone|sm|md|lg|xl|xxl|small|medium|large)\s*[-–—/]\s*(.+)$/i,/^(.+)\s*\[(desktop|tablet|mobile|phone|sm|md|lg|xl|xxl|small|medium|large)\]\s*$/i],li=new Set(["desktop","tablet","mobile","phone","sm","md","lg","xl","xxl","small","medium","large"]),di=.5});var Et={};en(Et,{DEFAULT_LINT_SETTINGS:()=>j,clearIgnored:()=>Ct,findNodesWithSameValue:()=>At,getIgnoredState:()=>It,ignoreAllOfType:()=>wt,ignoreError:()=>Nt,ignoreNode:()=>kt,lintSelection:()=>xe,restoreIgnoredState:()=>xt,runDesignLint:()=>ne});function U(e,t,n){return n?`${e}::${t}::${n}`:`${e}::${t}`}function kt(e){B.add(e)}function Nt(e,t,n){V.add(U(e,t,n))}function wt(e,t){for(let n of e)n.errorType===t&&V.add(U(n.nodeId,n.errorType))}function Ct(){B.clear(),V.clear()}function It(){return{nodeIds:Array.from(B),errorKeys:Array.from(V)}}function xt(e){B=new Set(e.nodeIds),V=new Set(e.errorKeys)}function As(e){if(e.type==="SOLID"){let{r:t,g:n,b:s}=e.color,o=F(t,n,s),r=e.opacity!==void 0&&e.opacity<1?` (${Math.round(e.opacity*100)}%)`:"";return o+r}return e.type==="IMAGE"?"Image fill":e.type==="VIDEO"?"Video fill":e.type.includes("GRADIENT")?`${e.type.replace("GRADIENT_","").toLowerCase()} gradient`:e.type}function Ue(e,t){try{if("boundVariables"in e){let n=e.boundVariables;if(n&&n[t])return!0}}catch(n){}return!1}function Be(e,t,n){if(!("fills"in e))return;let s=e.fills;if(s===figma.mixed||!Array.isArray(s))return;let o=s.filter(r=>r.visible!==!1);if(o.length!==0&&!Ue(e,"fills")){if("fillStyleId"in e){let r=e.fillStyleId;if(r&&r!==""&&r!==figma.mixed)return}for(let r of o){try{let a=r.boundVariables;if(a&&a.color)continue}catch(a){}let i=As(r);t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"fill",message:`Missing fill style: ${i}`,value:i,path:n})}}}function vt(e,t,n){if(!("strokes"in e))return;let s=e.strokes;if(!Array.isArray(s))return;let o=s.filter(r=>r.visible!==!1);if(o.length!==0&&!Ue(e,"strokes")){if("strokeStyleId"in e){let r=e.strokeStyleId;if(r&&r!==""&&r!==figma.mixed)return}for(let r of o){let i=As(r),a="strokeWeight"in e?` (${e.strokeWeight}px)`:"";t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"stroke",message:`Missing stroke style: ${i}${a}`,value:i+a,path:n})}}}function St(e,t,n){if(!("effects"in e))return;let s=e.effects;if(!Array.isArray(s)||s.length===0)return;let o=s.filter(i=>i.visible!==!1);if(o.length===0)return;if("effectStyleId"in e){let i=e.effectStyleId;if(i&&i!==""&&i!==figma.mixed)return}let r=o.map(i=>{let a=[i.type.replace(/_/g," ").toLowerCase()];if("radius"in i&&a.push(`r:${i.radius}`),"color"in i&&i.color){let c=i.color;a.push(F(c.r,c.g,c.b))}return a.join(" ")});t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"effect",message:`Missing effect style: ${r.join(", ")}`,value:r.join(", "),path:n})}function mi(e,t,n){if("textStyleId"in e){let a=e.textStyleId;if(a&&a!==""&&a!==figma.mixed)return}let s=e.fontName!==figma.mixed?e.fontName:null,o=e.fontSize!==figma.mixed?e.fontSize:null,r=[];s&&r.push(`${s.family} ${s.style}`),o&&r.push(`${o}px`);let i=r.join(" / ")||"unknown text style";t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"text",message:`Missing text style: ${i}`,value:i,path:n})}function xs(e,t,n,s){if(!("cornerRadius"in e)||Ue(e,"topLeftRadius")||Ue(e,"cornerRadius"))return;let o=e.cornerRadius;if(o===figma.mixed){let r=[e.topLeftRadius,e.topRightRadius,e.bottomLeftRadius,e.bottomRightRadius].filter(i=>i!=null);for(let i of r)if(!s.includes(i)){t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"radius",message:`Non-standard border radius: ${i}px (allowed: ${s.join(", ")})`,value:`${i}px`,path:n});break}return}typeof o=="number"&&o>0&&!s.includes(o)&&t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"radius",message:`Non-standard border radius: ${o}px (allowed: ${s.join(", ")})`,value:`${o}px`,path:n})}function fi(e,t,n,s){if(!(e.type==="GROUP"||e.type==="SLICE"||e.type==="CONNECTOR")&&e.type!=="COMPONENT_SET")switch(e.type){case"TEXT":t.checkTextStyles&&mi(e,n,s),t.checkFills&&Be(e,n,s);break;case"FRAME":case"SECTION":t.checkFills&&Be(e,n,s),t.checkStrokes&&vt(e,n,s),t.checkEffects&&St(e,n,s),t.checkRadius&&xs(e,n,s,t.allowedRadii);break;case"RECTANGLE":case"COMPONENT":case"INSTANCE":t.checkFills&&Be(e,n,s),t.checkStrokes&&vt(e,n,s),t.checkEffects&&St(e,n,s),t.checkRadius&&xs(e,n,s,t.allowedRadii);break;case"ELLIPSE":case"POLYGON":case"STAR":case"VECTOR":case"LINE":case"BOOLEAN_OPERATION":t.checkFills&&Be(e,n,s),t.checkStrokes&&vt(e,n,s),t.checkEffects&&St(e,n,s);break}}function Es(e,t,n,s,o){let r=0,i=o||"locked"in e&&e.locked,a="visible"in e&&!e.visible;if(t.skipLockedLayers&&i||t.skipHiddenLayers&&a)return 0;let c=s?`${s} > ${e.name}`:e.name;if(r++,!B.has(e.id)){let d=n.length;fi(e,t,n,c);for(let l=n.length-1;l>=d;l--){let p=n[l];(V.has(U(p.nodeId,p.errorType))||V.has(U(p.nodeId,p.errorType,p.value)))&&n.splice(l,1)}}if("children"in e)for(let d of e.children)r+=Es(d,t,n,c,i);return r}function q(e,t){for(let n of t)if(new RegExp("^"+n.replace(/[.+^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*").replace(/\?/g,".")+"$").test(e))return!0;return!1}function ne(e,t=j){var u,g;let n=[],s=0,o=t.ignorePatterns||[],r=t.severityOverrides||{};for(let f of e)s+=Es(f,t,n,"",!1);let i={skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers,scale:t.spacingScale};if(t.checkSpacing&&r.spacing!=="off"){let f=yn(e,i);for(let m of f.issues){let h=m.currentValue||"";B.has(m.nodeId)||V.has(U(m.nodeId,"spacing"))||V.has(U(m.nodeId,"spacing",h))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"spacing",message:m.message,value:h,path:m.nodeName,property:(g=(u=m.fixAction)==null?void 0:u.params)==null?void 0:g.property})}}if(t.checkAutoLayout&&r.autoLayout!=="off"){let f=Sn(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||V.has(U(m.nodeId,"autoLayout"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"autoLayout",message:m.message,value:m.currentValue||"",path:m.nodeName})}if(t.checkAccessibility&&r.accessibility!=="off"){let f=Cn(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||V.has(U(m.nodeId,"accessibility"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"accessibility",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkVisualQuality&&r.visualQuality!=="off"){let f=Pn(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||V.has(U(m.nodeId,"visualQuality"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"visualQuality",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkMicrocopy&&r.microcopy!=="off"){let f=Fn(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||V.has(U(m.nodeId,"microcopy"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"TEXT",errorType:"microcopy",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkConversion&&r.conversion!=="off"){let f=Hn(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||V.has(U(m.nodeId,"conversion"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"conversion",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkCognitive&&r.cognitive!=="off"){let f=os(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||V.has(U(m.nodeId,"cognitive"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"cognitive",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkFittsLaw&&r.fittsLaw!=="off"){let f=cs(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||V.has(U(m.nodeId,"fittsLaw"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"fittsLaw",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkGestalt&&r.gestalt!=="off"){let f=ps(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||V.has(U(m.nodeId,"gestalt"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"gestalt",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkDetachedInstances&&r.detachedInstance!=="off"){let f=ys(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||V.has(U(m.nodeId,"detachedInstance"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"detachedInstance",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}if(t.checkResponsive&&r.responsive!=="off"){let f=Cs(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let m of f.issues)B.has(m.nodeId)||V.has(U(m.nodeId,"responsive"))||q(m.nodeName,o)||n.push({nodeId:m.nodeId,nodeName:m.nodeName,nodeType:"FRAME",errorType:"responsive",message:m.message,value:m.currentValue||"",path:m.nodeName,severity:m.severity})}let a=n.filter(f=>r[f.errorType]!=="off"),c=o.length>0?a.filter(f=>!q(f.nodeName,o)):a;for(let f of c){let m=r[f.errorType];if(m&&m!=="off")f.severity=m;else if(!f.severity)switch(f.errorType){case"fill":case"stroke":case"effect":case"text":case"spacing":f.severity="warning";break;case"radius":case"autoLayout":f.severity="info";break;case"accessibility":f.severity="critical";break;case"visualQuality":f.severity="warning";break;case"microcopy":f.severity="info";break;case"conversion":f.severity="warning";break;case"cognitive":f.severity="info";break;case"responsive":f.severity="warning";break;case"fittsLaw":f.severity="warning";break;case"gestalt":f.severity="info";break;case"detachedInstance":f.severity="warning";break}}let d=new Set(c.map(f=>f.nodeId)).size,l={fill:0,stroke:0,effect:0,text:0,radius:0,spacing:0,autoLayout:0,accessibility:0,visualQuality:0,microcopy:0,conversion:0,cognitive:0,fittsLaw:0,gestalt:0,detachedInstance:0,responsive:0};for(let f of c)l[f.errorType]++;let p={totalErrors:c.length,byType:l,totalNodes:s,nodesWithErrors:d};return{errors:c,ignoredNodeIds:Array.from(B),ignoredErrorKeys:Array.from(V),summary:p}}function xe(e){let t=figma.currentPage.selection;return t.length===0?{errors:[],ignoredNodeIds:[],ignoredErrorKeys:[],summary:{totalErrors:0,byType:{fill:0,stroke:0,effect:0,text:0,radius:0,spacing:0,autoLayout:0,accessibility:0,visualQuality:0,microcopy:0,conversion:0,cognitive:0,fittsLaw:0,gestalt:0,detachedInstance:0,responsive:0},totalNodes:0,nodesWithErrors:0}}:ne(t,e)}function At(e,t,n,s=j){return ne(e,s).errors.filter(r=>r.errorType===t&&r.value===n)}var j,B,V,be=G(()=>{"use strict";J();hn();kn();In();Ln();Dn();Kn();rs();ls();ms();hs();Is();j={checkFills:!0,checkStrokes:!0,checkEffects:!0,checkTextStyles:!0,checkRadius:!0,checkSpacing:!0,checkAutoLayout:!0,checkAccessibility:!0,checkVisualQuality:!0,checkMicrocopy:!0,checkConversion:!0,checkCognitive:!0,checkFittsLaw:!0,checkGestalt:!0,checkDetachedInstances:!0,checkResponsive:!0,allowedRadii:[0,2,4,8,12,16,24,32],skipLockedLayers:!0,skipHiddenLayers:!0},B=new Set,V=new Set});var js=Po((Ul,We)=>{var Bt=function(){var e=String.fromCharCode,t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",s={};function o(i,a){if(!s[i]){s[i]={};for(var c=0;c>>8,c[d*2+1]=p%256}return c},decompressFromUint8Array:function(i){if(i==null)return r.decompress(i);for(var a=new Array(i.length/2),c=0,d=a.length;c>1}else{for(l=1,d=0;d>1}h--,h==0&&(h=Math.pow(2,k),k++),delete u[m]}else for(l=p[m],d=0;d>1;h--,h==0&&(h=Math.pow(2,k),k++),p[f]=C++,m=String(g)}if(m!==""){if(Object.prototype.hasOwnProperty.call(u,m)){if(m.charCodeAt(0)<256){for(d=0;d>1}else{for(l=1,d=0;d>1}h--,h==0&&(h=Math.pow(2,k),k++),delete u[m]}else for(l=p[m],d=0;d>1;h--,h==0&&(h=Math.pow(2,k),k++)}for(l=2,d=0;d>1;for(;;)if(y=y<<1,b==a-1){N.push(c(y));break}else b++;return N.join("")},decompress:function(i){return i==null?"":i==""?null:r._decompress(i.length,32768,function(a){return i.charCodeAt(a)})},_decompress:function(i,a,c){var d=[],l,p=4,u=4,g=3,f="",m=[],h,C,k,N,y,b,I,w={val:c(0),position:a,index:1};for(h=0;h<3;h+=1)d[h]=h;for(k=0,y=Math.pow(2,2),b=1;b!=y;)N=w.val&w.position,w.position>>=1,w.position==0&&(w.position=a,w.val=c(w.index++)),k|=(N>0?1:0)*b,b<<=1;switch(l=k){case 0:for(k=0,y=Math.pow(2,8),b=1;b!=y;)N=w.val&w.position,w.position>>=1,w.position==0&&(w.position=a,w.val=c(w.index++)),k|=(N>0?1:0)*b,b<<=1;I=e(k);break;case 1:for(k=0,y=Math.pow(2,16),b=1;b!=y;)N=w.val&w.position,w.position>>=1,w.position==0&&(w.position=a,w.val=c(w.index++)),k|=(N>0?1:0)*b,b<<=1;I=e(k);break;case 2:return""}for(d[3]=I,C=I,m.push(I);;){if(w.index>i)return"";for(k=0,y=Math.pow(2,g),b=1;b!=y;)N=w.val&w.position,w.position>>=1,w.position==0&&(w.position=a,w.val=c(w.index++)),k|=(N>0?1:0)*b,b<<=1;switch(I=k){case 0:for(k=0,y=Math.pow(2,8),b=1;b!=y;)N=w.val&w.position,w.position>>=1,w.position==0&&(w.position=a,w.val=c(w.index++)),k|=(N>0?1:0)*b,b<<=1;d[u++]=e(k),I=u-1,p--;break;case 1:for(k=0,y=Math.pow(2,16),b=1;b!=y;)N=w.val&w.position,w.position>>=1,w.position==0&&(w.position=a,w.val=c(w.index++)),k|=(N>0?1:0)*b,b<<=1;d[u++]=e(k),I=u-1,p--;break;case 2:return m.join("")}if(p==0&&(p=Math.pow(2,g),g++),d[I])f=d[I];else if(I===u)f=C+C.charAt(0);else return null;m.push(f),d[u++]=C+f.charAt(0),p--,C=f,p==0&&(p=Math.pow(2,g),g++)}}};return r}();typeof define=="function"&&define.amd?define(function(){return Bt}):typeof We!="undefined"&&We!=null?We.exports=Bt:typeof angular!="undefined"&&angular!=null&&angular.module("LZString",[]).factory("LZString",function(){return Bt})});var so={};en(so,{applyEffectStyle:()=>Kt,applyFillStyle:()=>zt,applyStrokeStyle:()=>Wt,applyTextStyle:()=>Ht});async function zt(e,t){let n=figma.getNodeById(e);if(!n||!("fillStyleId"in n))return{success:!1,nodeId:e,nodeName:"",property:"fillStyle",oldValue:"",newValue:"",error:"Node not found or does not support fill styles"};try{let s=await figma.importStyleByKeyAsync(t),o=n.fillStyleId||"";return n.fillStyleId=s.id,{success:!0,nodeId:e,nodeName:n.name,property:"fillStyle",oldValue:o?"existing style":"no style",newValue:s.name}}catch(s){return{success:!1,nodeId:e,nodeName:n.name,property:"fillStyle",oldValue:"",newValue:t,error:s instanceof Error?s.message:String(s)}}}async function Wt(e,t){let n=figma.getNodeById(e);if(!n||!("strokeStyleId"in n))return{success:!1,nodeId:e,nodeName:"",property:"strokeStyle",oldValue:"",newValue:"",error:"Node not found or does not support stroke styles"};try{let s=await figma.importStyleByKeyAsync(t),o=n.strokeStyleId||"";return n.strokeStyleId=s.id,{success:!0,nodeId:e,nodeName:n.name,property:"strokeStyle",oldValue:o?"existing style":"no style",newValue:s.name}}catch(s){return{success:!1,nodeId:e,nodeName:n.name,property:"strokeStyle",oldValue:"",newValue:t,error:s instanceof Error?s.message:String(s)}}}async function Ht(e,t){let n=figma.getNodeById(e);if(!n||n.type!=="TEXT")return{success:!1,nodeId:e,nodeName:"",property:"textStyle",oldValue:"",newValue:"",error:"Node not found or is not a text node"};try{let s=await figma.importStyleByKeyAsync(t),o=n,r=o.textStyleId||"";return o.textStyleId=s.id,{success:!0,nodeId:e,nodeName:n.name,property:"textStyle",oldValue:r?"existing style":"no style",newValue:s.name}}catch(s){return{success:!1,nodeId:e,nodeName:n.name,property:"textStyle",oldValue:"",newValue:t,error:s instanceof Error?s.message:String(s)}}}async function Kt(e,t){let n=figma.getNodeById(e);if(!n||!("effectStyleId"in n))return{success:!1,nodeId:e,nodeName:"",property:"effectStyle",oldValue:"",newValue:"",error:"Node not found or does not support effect styles"};try{let s=await figma.importStyleByKeyAsync(t),o=n.effectStyleId||"";return n.effectStyleId=s.id,{success:!0,nodeId:e,nodeName:n.name,property:"effectStyle",oldValue:o?"existing style":"no style",newValue:s.name}}catch(s){return{success:!1,nodeId:e,nodeName:n.name,property:"effectStyle",oldValue:"",newValue:t,error:s instanceof Error?s.message:String(s)}}}var jt=G(()=>{"use strict"});J();J();J();function de(e){var d;let t=e,n=!1;for(;t;){if(t.type==="COMPONENT_SET"){n=!0;break}if(t.parent&&t.parent.type==="COMPONENT_SET"){n=!0;break}t=t.parent}if(!n||!("strokes"in e)||!("cornerRadius"in e)||!("strokeWeight"in e))return!1;let s=e.cornerRadius===5||"topLeftRadius"in e&&"topRightRadius"in e&&"bottomLeftRadius"in e&&"bottomRightRadius"in e&&e.topLeftRadius===5&&e.topRightRadius===5&&e.bottomLeftRadius===5&&e.bottomRightRadius===5,o=e.strokeWeight===1,r=e.strokes,i=r.length>0&&r.some(l=>l.type==="SOLID"&&l.visible!==!1&&l.color?F(l.color.r,l.color.g,l.color.b).toUpperCase()==="#9747FF":!1),a="paddingLeft"in e&&"paddingRight"in e&&"paddingTop"in e&&"paddingBottom"in e&&e.paddingLeft===16&&e.paddingRight===16&&e.paddingTop===16&&e.paddingBottom===16,c=s&&o&&i&&a;return c&&(console.log(`\u{1F3AF} [FILTER] Detected default variant frame styles in ${e.name} - filtering out`),console.log(` Type: ${e.type}, Parent: ${(d=e.parent)==null?void 0:d.type}`),console.log(` Radius: ${String(e.cornerRadius)}, Weight: ${String(e.strokeWeight)}, Color: ${r.length>0&&r[0].type==="SOLID"?F(r[0].color.r,r[0].color.g,r[0].color.b):"none"}`),console.log(` Padding: L=${e.paddingLeft}, R=${e.paddingRight}, T=${e.paddingTop}, B=${e.paddingBottom}`)),c}function we(e){let t=e;for(;t;){if(t.type==="COMPONENT_SET"||t.parent&&t.parent.type==="COMPONENT_SET")return!0;t=t.parent}return!1}async function ue(e){let t=[],n=[],s=[],o=[],r=[],i=new Set,a=new Set,c=new Set,d=new Set,l=new Set;async function p(u){console.log("\u{1F50D} Analyzing node:",u.name,"Type:",u.type);let g=[];if("fillStyleId"in u&&typeof u.fillStyleId=="string"&&g.push(figma.getStyleByIdAsync(u.fillStyleId).then(y=>{if(y!=null&&y.name&&!i.has(y.name)){i.add(y.name);let b=y.name;if("fills"in u&&Array.isArray(u.fills)&&u.fills.length>0){let I=u.fills[0];I.type==="SOLID"&&I.color&&(b=F(I.color.r,I.color.g,I.color.b))}t.push({name:y.name,value:b,type:"fill-style",isToken:!0,isActualToken:!0,source:"figma-style"})}}).catch(console.warn)),"strokeStyleId"in u&&typeof u.strokeStyleId=="string"&&g.push(figma.getStyleByIdAsync(u.strokeStyleId).then(y=>{y!=null&&y.name&&!i.has(y.name)&&(i.add(y.name),t.push({name:y.name,value:y.name,type:"stroke-style",isToken:!0,isActualToken:!0,source:"figma-style"}))}).catch(console.warn)),u.type==="TEXT"&&"textStyleId"in u&&typeof u.textStyleId=="string"&&g.push(figma.getStyleByIdAsync(u.textStyleId).then(y=>{y!=null&&y.name&&!c.has(y.name)&&(c.add(y.name),s.push({name:y.name,value:y.name,type:"text-style",isToken:!0,isActualToken:!0,source:"figma-style"}))}).catch(console.warn)),"effectStyleId"in u&&typeof u.effectStyleId=="string"&&g.push(figma.getStyleByIdAsync(u.effectStyleId).then(y=>{y!=null&&y.name&&!d.has(y.name)&&(d.add(y.name),o.push({name:y.name,value:y.name,type:"effect-style",isToken:!0,isActualToken:!0,source:"figma-style"}))}).catch(console.warn)),await Promise.all(g),"boundVariables"in u&&u.boundVariables){let y=u.boundVariables;console.log(`\u{1F50D} [VARIABLES] Checking bound variables for ${u.name}:`,Object.keys(y));let b=async(x,$,v,T,L)=>{try{let A=Array.isArray(x)?x:[x];for(let ee of A)if(ee!=null&&ee.id&&typeof ee.id=="string"){let Y=await Qe(ee.id);if(console.log(` \u{1F3AF} Found ${$} variable:`,Y),Y&&!v.has(Y)){v.add(Y);let ve=Y;if(L==="color"&&($==="fills"||$==="strokes")){let $e=await tn(ee.id,u);$e&&$e.startsWith("#")&&(ve=$e)}T.push({name:Y,value:ve,type:`${$}-variable`,isToken:!0,isActualToken:!0,source:"figma-variable"}),console.log(` \u2705 Added ${L} token: ${Y} (value: ${ve})`)}}}catch(A){console.warn(`Error processing ${$} variables:`,A)}},I=async(x,$,v,T,L)=>{if(x&&typeof x=="object"&&"id"in x&&typeof x.id=="string"){let A=await Qe(x.id);console.log(` \u{1F3AF} Found ${$} variable:`,A),A&&!v.has(A)&&(v.add(A),T.push({name:A,value:A,type:`${$}-variable`,isToken:!0,isActualToken:!0,source:"figma-variable"}),console.log(` \u2705 Added ${L} token: ${A}`))}},w=[];y.fills&&(console.log(" \u{1F3A8} Processing fills variables..."),w.push(b(y.fills,"fills",i,t,"color"))),y.strokes&&(console.log(" \u{1F58A}\uFE0F Processing strokes variables..."),w.push(b(y.strokes,"strokes",i,t,"color"))),y.effects&&(console.log(" \u2728 Processing effects variables..."),w.push(b(y.effects,"effects",d,o,"effect"))),["strokeWeight","strokeTopWeight","strokeRightWeight","strokeBottomWeight","strokeLeftWeight"].forEach(x=>{y[x]&&(console.log(` \u{1F4CF} Processing ${x} variable...`),w.push(I(y[x],x,l,r,"border")))}),["topLeftRadius","topRightRadius","bottomLeftRadius","bottomRightRadius"].forEach(x=>{y[x]&&(console.log(` \u{1F504} Processing ${x} variable...`),w.push(I(y[x],x,l,r,"border")))}),["paddingTop","paddingRight","paddingBottom","paddingLeft","itemSpacing","counterAxisSpacing"].forEach(x=>{y[x]&&(console.log(` \u{1F4D0} Processing ${x} variable...`),w.push(I(y[x],x,a,n,"spacing")))}),["width","height","minWidth","maxWidth","minHeight","maxHeight"].forEach(x=>{y[x]&&(console.log(` \u{1F4E6} Processing ${x} variable...`),w.push(I(y[x],x,a,n,"size")))}),y.opacity&&(console.log(" \u{1F47B} Processing opacity variable..."),w.push(I(y.opacity,"opacity",d,o,"effect"))),u.type==="TEXT"&&["fontSize","lineHeight","letterSpacing","paragraphSpacing"].forEach($=>{y[$]&&(console.log(` \u{1F4DD} Processing ${$} variable...`),w.push(I(y[$],$,c,s,"typography")))}),await Promise.all(w),console.log(`\u{1F50D} [VARIABLES] Total variables found for ${u.name}: ${Object.keys(y).length}`)}let f="boundVariables"in u&&u.boundVariables&&u.boundVariables.fills,m="fillStyleId"in u&&u.fillStyleId;"fills"in u&&Array.isArray(u.fills)&&!m&&!f?(console.log(`\u{1F50D} [HARD-CODED] Checking fills for ${u.name} (no variables, no style)`),u.fills.forEach(y=>{if(y.type==="SOLID"&&y.visible!==!1&&y.color){let b=F(y.color.r,y.color.g,y.color.b),I=`${b}:${u.id}`;if(!i.has(I)){console.log(` \u26A0\uFE0F Found hard-coded fill: ${b}`),i.add(I);let w=re(u);t.push({name:`hard-coded-fill-${t.length+1}`,value:b,type:"fill",isToken:!1,source:"hard-coded",context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,path:w.path,description:w.description,property:"fills"}})}}})):f?console.log(`\u{1F50D} [VARIABLES] ${u.name} has fill variables - skipping hard-coded detection`):m&&console.log(`\u{1F50D} [STYLES] ${u.name} has fill style - skipping hard-coded detection`);let h="boundVariables"in u&&u.boundVariables&&u.boundVariables.strokes,C="strokeStyleId"in u&&u.strokeStyleId;if("strokes"in u&&Array.isArray(u.strokes)&&!C&&!h?(console.log(`\u{1F50D} [HARD-CODED] Checking strokes for ${u.name} (no variables, no style)`),de(u)?console.log(" \u{1F6AB} Skipping default variant frame stroke colors"):u.strokes.forEach(y=>{if(y.type==="SOLID"&&y.visible!==!1&&y.color){let b=F(y.color.r,y.color.g,y.color.b),I=`${b}:${u.id}`;if(!i.has(I)){console.log(` \u26A0\uFE0F Found hard-coded stroke: ${b}`),i.add(I);let w=re(u);t.push({name:`hard-coded-stroke-${t.length+1}`,value:b,type:"stroke",isToken:!1,source:"hard-coded",isDefaultVariantStyle:b.toUpperCase()==="#9747FF"&&we(u),context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,path:w.path,description:w.description,property:"strokes"}})}}})):h?console.log(`\u{1F50D} [VARIABLES] ${u.name} has stroke variables - skipping hard-coded detection`):C&&console.log(`\u{1F50D} [STYLES] ${u.name} has stroke style - skipping hard-coded detection`),"strokeWeight"in u&&typeof u.strokeWeight=="number"){console.log(`\u{1F50D} Node ${u.name} has strokeWeight: ${u.strokeWeight}`);let y="strokes"in u&&Array.isArray(u.strokes)&&u.strokes.length>0,b=y&&u.strokes.some(P=>P.visible!==!1),I="boundVariables"in u&&u.boundVariables&&["strokeWeight","strokeTopWeight","strokeRightWeight","strokeBottomWeight","strokeLeftWeight"].some(P=>u.boundVariables[P]),w="boundVariables"in u&&u.boundVariables?Object.keys(u.boundVariables):[];if(console.log(` Has strokes: ${y}, Has visible strokes: ${b}, Has strokeWeight variable: ${!!I}, boundVariable keys: [${w.join(", ")}]`),I)console.log(` \u{1F517} ${u.name} has strokeWeight bound to variable - skipping hard-coded detection`);else if(u.strokeWeight>0&&b&&!de(u)){let P=`${u.strokeWeight}px`,O,M=u.strokes.find(x=>x.visible!==!1&&x.type==="SOLID");M&&M.type==="SOLID"&&M.color&&(O=F(M.color.r,M.color.g,M.color.b));let z=`${P}:${u.id}`;if(!l.has(z)){console.log(` \u2705 Adding stroke weight: ${P}`),l.add(z);let x=re(u);r.push({name:`hard-coded-stroke-weight-${u.strokeWeight}`,value:P,type:"stroke-weight",isToken:!1,source:"hard-coded",strokeColor:O,isDefaultVariantStyle:u.strokeWeight===1&&(O==null?void 0:O.toUpperCase())==="#9747FF"&&we(u),context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,hasVisibleStroke:!0,path:x.path,description:x.description,property:"strokeWeight"}})}}else u.strokeWeight>0&&b&&de(u)&&console.log(" \u{1F6AB} Skipping default variant frame stroke weight")}let k="boundVariables"in u&&u.boundVariables&&["topLeftRadius","topRightRadius","bottomLeftRadius","bottomRightRadius","cornerRadius"].some(y=>u.boundVariables[y]);if("cornerRadius"in u&&typeof u.cornerRadius=="number"&&!k)if(console.log(`\u{1F50D} [HARD-CODED] Checking corner radius for ${u.name} (no variables)`),de(u))console.log(" \u{1F6AB} Skipping default variant frame corner radius");else{let y=u.cornerRadius;if(y>0){let b=`${y}px`,I=`${b}:${u.id}`;if(!l.has(I)){console.log(` \u26A0\uFE0F Found hard-coded corner radius: ${b}`),l.add(I);let w=re(u);r.push({name:`hard-coded-corner-radius-${y}`,value:b,type:"corner-radius",isToken:!1,source:"hard-coded",isDefaultVariantStyle:y===5&&we(u),context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,path:w.path,description:w.description,property:"cornerRadius"}})}}}else k&&console.log(`\u{1F50D} [VARIABLES] ${u.name} has radius variables - skipping hard-coded detection`);!k&&"topLeftRadius"in u&&(console.log(`\u{1F50D} [HARD-CODED] Checking individual corner radius for ${u.name} (no variables)`),de(u)?console.log(" \u{1F6AB} Skipping default variant frame individual corner radii"):[{prop:"topLeftRadius",name:"top-left"},{prop:"topRightRadius",name:"top-right"},{prop:"bottomLeftRadius",name:"bottom-left"},{prop:"bottomRightRadius",name:"bottom-right"}].forEach(({prop:b,name:I})=>{if(b in u&&typeof u[b]=="number"){let w=u[b];if(w>0){let P=`${w}px`,O=`${P}:${u.id}:${b}`;if(!l.has(O)){console.log(` \u26A0\uFE0F Found hard-coded ${I} radius: ${P}`),l.add(O);let M=re(u);r.push({name:`hard-coded-${I}-radius-${w}`,value:P,type:`${I}-radius`,isToken:!1,source:"hard-coded",isDefaultVariantStyle:w===5&&we(u),context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,path:M.path,description:M.description,property:b}})}}}}));let N="boundVariables"in u&&u.boundVariables&&["paddingTop","paddingRight","paddingBottom","paddingLeft"].some(y=>u.boundVariables[y]);if("paddingLeft"in u&&typeof u.paddingLeft=="number"&&!N){console.log(`\u{1F50D} [HARD-CODED] Checking padding for ${u.name} (no variables)`);let y=u;[{value:y.paddingLeft,name:"left"},{value:y.paddingRight,name:"right"},{value:y.paddingTop,name:"top"},{value:y.paddingBottom,name:"bottom"}].forEach(I=>{let w=`${I.value}:${u.id}:${I.name}`;if(typeof I.value=="number"&&I.value>1&&!a.has(w)){console.log(` \u26A0\uFE0F Found hard-coded padding-${I.name}: ${I.value}px`),a.add(w);let P=re(u),O=I.value===16&&we(u)&&de(u);n.push({name:`hard-coded-padding-${I.name}-${I.value}`,value:`${I.value}px`,type:"padding",isToken:!1,source:"hard-coded",isDefaultVariantStyle:O,context:{nodeType:u.type,nodeName:u.name,nodeId:u.id,path:P.path,description:P.description,property:`padding${I.name.charAt(0).toUpperCase()+I.name.slice(1)}`}})}})}else N&&console.log(`\u{1F50D} [VARIABLES] ${u.name} has padding variables - skipping hard-coded detection`);if("children"in u)for(let y of u.children)await p(y)}return await p(e),Mo({colors:t,spacing:n,typography:s,effects:o,borders:r})}function Mo(e){let t=["colors","spacing","typography","effects","borders"],n={totalTokens:0,actualTokens:0,hardCodedValues:0,aiSuggestions:0,byCategory:{}};return t.forEach(s=>{let o=e[s].map(c=>K(R({},c),{isActualToken:c.source==="figma-style"||c.source==="figma-variable",recommendation:Oo(c,s),suggestion:Fo(c,s)})),r=o.filter(c=>!c.isDefaultVariantStyle),i=r.filter(c=>c.isActualToken).length,a=r.filter(c=>c.source==="hard-coded").length;n.byCategory[s]={total:r.length,tokens:i,hardCoded:a,suggestions:0},n.totalTokens+=r.length,n.actualTokens+=i,n.hardCodedValues+=a,e[s]=o}),K(R({},e),{summary:n})}function Oo(e,t){if(e.isToken)return`Using ${e.name} token`;switch(t){case"colors":return`Consider using a color token instead of ${e.value}`;case"spacing":return`Consider using spacing token instead of ${e.value}`;case"typography":return"Consider using typography token";case"effects":return"Consider using effect token";case"borders":return"Consider using border radius token";default:return"Consider using a design token"}}function Fo(e,t){var n,s;switch(t){case"colors":return(n=e.value)!=null&&n.startsWith("#000")?"Use semantic color token (e.g., text.primary)":(s=e.value)!=null&&s.startsWith("#FFF")?"Use semantic color token (e.g., background.primary)":"Create or use existing color token";case"spacing":let o=parseInt(e.value||"0");return o%8===0?"Create or use existing spacing token (follows 8px grid)":o%4===0?"Create or use existing spacing token (follows 4px grid)":"Create or use existing spacing token";case"typography":return"Use semantic typography token (e.g., heading.large, body.regular)";case"effects":return"Use semantic shadow token (e.g., shadow.small, shadow.medium)";case"borders":return"Use appropriate radius token (e.g., radius.small, radius.medium)";default:return"Create or use existing design token"}}function nn(e){return`You are an expert design system architect analyzing a Figma component for comprehensive metadata and design token recommendations. +"use strict";(()=>{var hi=Object.create;var He=Object.defineProperty,bi=Object.defineProperties,Si=Object.getOwnPropertyDescriptor,vi=Object.getOwnPropertyDescriptors,ki=Object.getOwnPropertyNames,bn=Object.getOwnPropertySymbols,Ni=Object.getPrototypeOf,vn=Object.prototype.hasOwnProperty,Ii=Object.prototype.propertyIsEnumerable;var Sn=(e,t,n)=>t in e?He(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,R=(e,t)=>{for(var n in t||(t={}))vn.call(t,n)&&Sn(e,n,t[n]);if(bn)for(var n of bn(t))Ii.call(t,n)&&Sn(e,n,t[n]);return e},j=(e,t)=>bi(e,vi(t));var U=(e,t)=>()=>(e&&(t=e(e=0)),t);var Ci=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),kn=(e,t)=>{for(var n in t)He(e,n,{get:t[n],enumerable:!0})},xi=(e,t,n,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of ki(t))!vn.call(e,o)&&o!==n&&He(e,o,{get:()=>t[o],enumerable:!(s=Si(t,o))||s.enumerable});return e};var Ai=(e,t,n)=>(n=e!=null?hi(Ni(e)):{},xi(t||!e||!e.__esModule?He(n,"default",{value:e,enumerable:!0}):n,e));function xe(e){return["FRAME","COMPONENT","COMPONENT_SET","INSTANCE","GROUP"].includes(e.type)?(e.type==="COMPONENT_SET",!0):!1}function F(e,t,n){let s=o=>{let i=Math.round(o*255).toString(16);return i.length===1?"0"+i:i};return`#${s(e)}${s(t)}${s(n)}`}async function yt(e){try{let t=await figma.variables.getVariableByIdAsync(e);return t?t.name:null}catch(t){return console.warn("Could not access variable:",e,t),null}}async function Nn(e,t){try{let n=await figma.variables.getVariableByIdAsync(e);if(!n)return null;if(t&&n.resolveForConsumer)try{let s=n.resolveForConsumer(t);if(s&&typeof s.value=="object"&&"r"in s.value){let o=s.value;return F(o.r,o.g,o.b)}else if(s&&s.value!==void 0)return String(s.value)}catch(s){console.warn("Could not resolve variable value:",s)}return n.name}catch(n){return console.warn("Could not access variable:",e,n),null}}function S(e,t){try{figma.ui.postMessage({type:e,data:t})}catch(n){console.error("Failed to send message to UI:",n)}}function We(e){let t=[e];if("children"in e)for(let n of e.children)t.push(...We(n));return t}function ht(e){let t=[];if(e.type==="TEXT"){let n=e;n.characters&&t.push(n.characters)}if("children"in e)for(let n of e.children)t.push(...ht(n));return t}function J(e,t,n){let[s,o,i]=[e,t,n].map(r=>r<=.03928?r/12.92:Math.pow((r+.055)/1.055,2.4));return .2126*s+.7152*o+.0722*i}function re(e,t){let n=Math.max(e,t),s=Math.min(e,t);return(n+.05)/(s+.05)}function Ae(e){let t=e.parent;for(;t&&"type"in t;){let n=t;if("fills"in n){let s=n.fills;if(Array.isArray(s)){for(let o of s)if(o.type==="SOLID"&&o.visible!==!1&&o.color){if(o.boundVariables&&o.boundVariables.color)continue;return o.color}}}t=t.parent}return null}function wi(e){var s;let t=[],n=e;for(;n&&n.type!=="DOCUMENT"&&n.type!=="PAGE";)n.type==="COMPONENT"&&((s=n.parent)==null?void 0:s.type)==="COMPONENT_SET"?t.unshift(`${n.name}`):t.unshift(n.name),n=n.parent;return t.join(" \u2192 ")}function ae(e){var s,o;let t=wi(e),n=`Found in "${e.name}"`;if(((s=e.parent)==null?void 0:s.type)==="COMPONENT_SET"||e.parent&&((o=e.parent.parent)==null?void 0:o.type)==="COMPONENT_SET")n=`Found in variant: "${e.name}"`;else if(t.includes("\u2192")){let i=t.split(" \u2192 ");i.length>1&&(n=`Found in "${i[i.length-1]}" (${i[i.length-2]})`)}return{path:t,description:n}}var q=U(()=>{"use strict"});function qe(e,t=de){if(t.includes(e))return[];let n=[...t].map(o=>({v:o,diff:Math.abs(o-e)})).sort((o,i)=>o.diff-i.diff),s=[];for(let o of n){if(s.length>=2)break;s.includes(o.v)||s.push(o.v)}return s.sort((o,i)=>o-i)}var de,Pt,Xe=U(()=>{"use strict";de=[0,2,4,8,12,16,20,24,32,40,48,64,80,96],Pt=de});function Ki(){return`spacing-${++Mn}`}function qi(e){return Rt.includes(e)}function Xi(e){return{itemSpacing:"Gap",paddingTop:"Padding Top",paddingBottom:"Padding Bottom",paddingLeft:"Padding Left",paddingRight:"Padding Right",counterAxisSpacing:"Counter-axis Gap"}[e]||e}function Ji(e,t){var o;if(e.layoutMode==="NONE")return 0;let n=0,s=[{prop:"itemSpacing",value:e.itemSpacing},{prop:"paddingTop",value:e.paddingTop},{prop:"paddingBottom",value:e.paddingBottom},{prop:"paddingLeft",value:e.paddingLeft},{prop:"paddingRight",value:e.paddingRight}];"counterAxisSpacing"in e&&typeof e.counterAxisSpacing=="number"&&s.push({prop:"counterAxisSpacing",value:e.counterAxisSpacing});for(let{prop:i,value:r}of s)if(n++,!qi(r)){let a=qe(r,Rt);t.push({id:Ki(),type:"spacing",severity:"warning",nodeId:e.id,nodeName:e.name,message:`${Xi(i)} is ${r}px \u2014 not in spacing scale`,currentValue:`${r}px`,suggestions:a.map(c=>`${c}px`),autoFixable:!0,fixAction:{type:"fixSpacing",params:{nodeId:e.id,property:i,currentValue:r,suggestedValue:(o=a[0])!=null?o:r}}})}return n}function On(e,t,n,s,o){let i=o||"locked"in e&&e.locked,r="visible"in e&&!e.visible;if(n&&i)return{checked:0,passed:0};if(s&&r)return{checked:0,passed:0};let a=0,c=0;if(e.type==="FRAME"||e.type==="COMPONENT"||e.type==="INSTANCE"){let l=t.length,d=Ji(e,t);a+=d,c+=d-(t.length-l)}if("children"in e)for(let l of e.children){let d=On(l,t,n,s,i);a+=d.checked,c+=d.passed}return{checked:a,passed:c}}function Fn(e,t={}){let{skipLocked:n=!0,skipHidden:s=!0,scale:o}=t;Rt=o||de,Mn=0;let i=[],r=0,a=0;for(let c of e){let{checked:l,passed:d}=On(c,i,n,s,!1);r+=l,a+=d}return{issues:i,summary:{totalChecked:r,passed:a,failed:i.length}}}var Mn,Rt,Vn=U(()=>{"use strict";Xe();Mn=0;Rt=de});function Yi(){return`autolayout-${++Dn}`}function _n(e,t,n,s,o){let i=o||"locked"in e&&e.locked,r="visible"in e&&!e.visible;if(n&&i)return{totalFrames:0,withAutoLayout:0};if(s&&r)return{totalFrames:0,withAutoLayout:0};let a=0,c=0;if((e.type==="FRAME"||e.type==="COMPONENT"||e.type==="INSTANCE")&&"children"in e){let d=e.children;d.length>=2&&(a++,e.layoutMode!=="NONE"?c++:t.push({id:Yi(),type:"autoLayout",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Frame "${e.name}" has ${d.length} children but no Auto Layout`,currentValue:"No Auto Layout",suggestions:["HORIZONTAL","VERTICAL"],autoFixable:!1}))}if("children"in e)for(let d of e.children){let u=_n(d,t,n,s,i);a+=u.totalFrames,c+=u.withAutoLayout}return{totalFrames:a,withAutoLayout:c}}function Bn(e,t={}){let{skipLocked:n=!0,skipHidden:s=!0}=t;Dn=0;let o=[],i=0,r=0;for(let l of e){let d=_n(l,o,n,s,!1);i+=d.totalFrames,r+=d.withAutoLayout}let a=i-r,c=i>0?Math.round(r/i*100):100;return{issues:o,summary:{totalFrames:i,withAutoLayout:r,withoutAutoLayout:a,percentage:c}}}var Dn,Gn=U(()=>{"use strict";Dn=0});function oe(){return`a11y-${++Un}`}function $t(e){return Qi.test(e)}function er(e,t){if(e.type!=="TEXT")return;let n=e,s=n.fills;if(s===figma.mixed||!Array.isArray(s))return;let o=s.find(p=>{var h;return p.type==="SOLID"&&p.visible!==!1&&p.color&&!((h=p.boundVariables)!=null&&h.color)});if(!o||o.type!=="SOLID")return;let i=Ae(e);if(!i)return;let r=o.color,a=J(r.r,r.g,r.b),c=J(i.r,i.g,i.b),l=re(a,c),d=n.fontSize!==figma.mixed?n.fontSize:0,u=n.fontName!==figma.mixed?n.fontName.style:"",f=u.toLowerCase().includes("bold")||u.toLowerCase().includes("black"),m=d>=18||d>=14&&f,g=m?3:4.5;if(l0&&s<12&&t.push({id:oe(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Text size ${s}px is below 12px readability minimum`,currentValue:`${s}px`,suggestions:["12px","14px"],autoFixable:!1})}function sr(e,t){if(e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!$t(e.name))return;let n=e;if(!("children"in n)||n.children.length===0)return;n.children.some(o=>{var i,r;return o.type==="TEXT"?!0:"children"in o?(r=(i=o.children)==null?void 0:i.some)==null?void 0:r.call(i,a=>a.type==="TEXT"):!1})||t.push({id:oe(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Interactive element "${e.name}" has no visible text label`,currentValue:"No text child",suggestions:["Add a text label or ensure screen reader label is provided"],autoFixable:!1})}function or(e,t){e.type!=="FRAME"&&e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!("children"in e)||e.children.length===0||Zi.test(e.name)&&t.push({id:oe(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`Generic layer name "${e.name}" \u2014 use a descriptive name`,currentValue:e.name,suggestions:["Rename to describe the layer purpose"],autoFixable:!1})}function ir(e,t){if(e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!$t(e.name))return;let n=e,s=null,o=n.strokes;if(Array.isArray(o)){let l=o.find(d=>d.type==="SOLID"&&d.visible!==!1);l&&l.type==="SOLID"&&(s=l.color)}if(!s){let l=n.fills;if(l!==figma.mixed&&Array.isArray(l)){let d=l.find(u=>u.type==="SOLID"&&u.visible!==!1);d&&d.type==="SOLID"&&(s=d.color)}}if(!s)return;let i=Ae(e);if(!i)return;let r=J(s.r,s.g,s.b),a=J(i.r,i.g,i.b),c=re(r,a);c<3&&t.push({id:oe(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Non-text contrast ${c.toFixed(1)}:1 below WCAG 1.4.11 minimum of 3:1`,currentValue:`${c.toFixed(1)}:1`,suggestions:["Increase boundary contrast to at least 3:1 against background"],autoFixable:!1})}function ar(e,t){if(e.type!=="FRAME"&&e.type!=="COMPONENT"&&e.type!=="INSTANCE"||!rr.test(e.name))return;let n=e;if(!("children"in n)||n.children.length===0)return;n.children.some(o=>{var i,r;return o.type==="TEXT"||/icon|svg|symbol|glyph/i.test(o.name)?!0:"children"in o?(r=(i=o.children)==null?void 0:i.some)==null?void 0:r.call(i,a=>a.type==="TEXT"||/icon|svg|symbol|glyph/i.test(a.name)):!1})||t.push({id:oe(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`"${e.name}" may rely on color alone to convey status (WCAG 1.4.1)`,currentValue:"No text or icon indicator",suggestions:["Add a text label or icon to supplement the color indicator"],autoFixable:!1})}function cr(e,t){if(e.type!=="COMPONENT")return;let n=e.parent;if(!n||n.type!=="COMPONENT_SET")return;let s=n,i=s.children.map(c=>c.name.toLowerCase()).join(" "),a=["hover","focus","disabled","pressed"].filter(c=>!i.includes(c));a.length>0&&t.push({id:oe(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`Component set missing states: ${a.join(", ")}`,currentValue:`${s.children.length} variants`,suggestions:a.map(c=>`Add ${c} variant`),autoFixable:!1})}function zn(e,t,n,s,o,i){var l;let r=o||"locked"in e&&e.locked,a="visible"in e&&!e.visible;if(n&&r||s&&a)return 0;let c=0;if(er(e,t),tr(e,t),nr(e,t),c++,sr(e,t),or(e,t),ir(e,t),ar(e,t),e.type==="COMPONENT"&&((l=e.parent)==null?void 0:l.type)==="COMPONENT_SET"){let d=e.parent.id;i.has(d)||(i.add(d),cr(e,t))}if("children"in e)for(let d of e.children)c+=zn(d,t,n,s,r,i);return c}function Hn(e,t={}){let{skipLocked:n=!0,skipHidden:s=!0}=t;Un=0;let o=[],i=new Set,r=0;for(let a of e)r+=zn(a,o,n,s,!1,i);return{issues:o,summary:{totalChecked:r,contrastIssues:o.filter(a=>a.message.includes("Contrast")).length,touchTargetIssues:o.filter(a=>a.message.includes("Touch target")).length,textSizeIssues:o.filter(a=>a.message.includes("Text size")).length,namingIssues:o.filter(a=>a.message.includes("text label")||a.message.includes("Generic")).length,stateIssues:o.filter(a=>a.message.includes("missing states")).length,nonTextContrastIssues:o.filter(a=>a.message.includes("Non-text contrast")).length,colorOnlyIssues:o.filter(a=>a.message.includes("color alone")).length}}}var Un,Qi,Zi,rr,Wn=U(()=>{"use strict";q();Un=0;Qi=/\b(button|btn|input|link|checkbox|toggle|switch|tab|radio|select|dropdown|menu-item|slider|chip)\b/i;Zi=/^(Frame|Group|Rectangle|Ellipse|Vector|Line|Polygon|Star)\s*\d+$/i;rr=/\b(error|success|warning|status|alert|badge|danger|info)\b/i});function Se(){return`vq-${++jn}`}function dr(e){return lr.some(t=>t.includes(e))}function ur(e,t){let n=e.width*e.height;if(n===0)return;let o=("children"in e?e.children.filter(r=>r.visible!==!1):[]).length,i=o/n*1e3;i>3&&t.push({id:Se(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`High visual density: ${o} elements in ${Math.round(n/1e3)}k px\xB2 (${i.toFixed(2)}/1000px\xB2). Consider simplifying or using progressive disclosure.`,currentValue:`${i.toFixed(2)} elements/1000px\xB2`,suggestions:["Reduce visible elements to under 15 per viewport","Group related items","Use progressive disclosure"],autoFixable:!1})}function Kn(e,t,n,s,o){if(!(s&&"locked"in e&&e.locked)&&!(o&&"visible"in e&&!e.visible)){if(e.type==="TEXT"){let i=e,r=i.fontSize;if(r!==figma.mixed&&typeof r=="number"){t.add(r);let a=i.lineHeight;if(a!==figma.mixed&&typeof a=="object"&&a.unit==="PIXELS"){let c=a.value/r;n.push({fontSize:r,lineHeight:a.value,ratio:c})}}}if("children"in e)for(let i of e.children)Kn(i,t,n,s,o)}}function fr(e,t,n,s){let o=new Set,i=[];Kn(e,o,i,n,s);let r=Array.from(o).sort((l,d)=>l-d),a=r.filter(l=>!dr(l));a.length>0&&t.push({id:Se(),type:"accessibility",severity:"info",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`Non-standard font sizes: ${a.join(", ")}px. Consider using a type scale (e.g., 12/14/16/20/24/32).`,currentValue:a.map(l=>`${l}px`).join(", "),suggestions:a.map(l=>{let d=[10,12,14,16,18,20,24,28,32,36,40,48].reduce((u,f)=>Math.abs(f-l)l.ratio<1.2||l.ratio>2);if(c.length>0){let l=c.reduce((d,u)=>Math.abs(u.ratio-1.5)>Math.abs(d.ratio-1.5)?u:d);t.push({id:Se(),type:"accessibility",severity:"info",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`Line height ratio ${l.ratio.toFixed(2)} (${l.lineHeight}px / ${l.fontSize}px) is outside optimal range 1.3\u20131.6.`,currentValue:`${l.ratio.toFixed(2)}`,suggestions:[`Set line height to ${Math.round(l.fontSize*1.5)}px (1.5\xD7 body) or ${Math.round(l.fontSize*1.3)}px (1.3\xD7 headings)`],autoFixable:!1})}return{sizes:r,lineHeightData:i}}function qn(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if("fills"in e){let o=e.fills;if(o!==figma.mixed&&Array.isArray(o))for(let i of o)i.visible!==!1&&i.type==="SOLID"&&t.add(F(i.color.r,i.color.g,i.color.b))}if("strokes"in e){let o=e.strokes;if(Array.isArray(o))for(let i of o)i.visible!==!1&&i.type==="SOLID"&&t.add(F(i.color.r,i.color.g,i.color.b))}if("children"in e)for(let o of e.children)qn(o,t,n,s)}}function pr(e,t,n,s){let o=new Set;qn(e,o,n,s);let i=Array.from(o);return i.length>8&&t.push({id:Se(),type:"accessibility",severity:"warning",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`${i.length} unique colors detected. A cohesive palette typically uses 5\u20137 colors (primary, secondary, accent, neutrals).`,currentValue:`${i.length} colors`,suggestions:["Consolidate similar colors into design tokens","Limit palette to primary, secondary, accent, and 2-3 neutrals"],autoFixable:!1}),i}function mr(e,t,n=4){if(!("children"in e))return 0;let s=e.children.filter(i=>i.visible!==!1),o=0;for(let i of s){if(!("x"in i)||!("y"in i))continue;let r=i.x,a=i.y,c=Math.round(r)%n,l=Math.round(a)%n;(c!==0||l!==0)&&o++}return o>0&&o/Math.max(s.length,1)>.3&&t.push({id:Se(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`${o}/${s.length} direct children are misaligned from ${n}px grid.`,currentValue:`${o} misaligned`,suggestions:[`Snap elements to ${n}px grid for visual consistency`],autoFixable:!1}),o}function Xn(e,t,n,s){if(n&&"locked"in e&&e.locked||s&&"visible"in e&&!e.visible)return;if(/button|btn|cta/i.test(e.name)&&"width"in e&&"height"in e&&t.push({nodeId:e.id,nodeName:e.name,width:e.width,height:e.height}),"children"in e)for(let i of e.children)Xn(i,t,n,s)}function gr(e,t,n,s){let o=[];if(Xn(e,o,n,s),o.length<2)return;let i=o.map(l=>l.height),r=i.reduce((l,d)=>l+d,0)/i.length,c=Math.max(...i.map(l=>Math.abs(l-r)))/r*100;if(c>15){let l=Math.min(...i),d=Math.max(...i);t.push({id:Se(),type:"accessibility",severity:"warning",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`Button height inconsistency: ${l}px to ${d}px (${Math.round(c)}% variance). Standardize to 2-3 size tiers.`,currentValue:`${l}\u2013${d}px`,suggestions:["Use consistent button heights: 32px (small), 40px (medium), 48px (large)"],autoFixable:!1})}}function Jn(e,t={}){var m,g;jn=0;let n=[],s=(m=t.skipLocked)!=null?m:!0,o=(g=t.skipHidden)!=null?g:!0,i=0,r=[],a=[],c=[],l=0,d=0,u=0;for(let p of e){"children"in p&&"width"in p&&"height"in p&&(ur(p,n),d+=p.children.length,u+=p.width*p.height,i++);let h=fr(p,n,s,o);r=[...new Set([...r,...h.sizes])],a=[...a,...h.lineHeightData],i++;let C=pr(p,n,s,o);c=[...new Set([...c,...C])],i++,"children"in p&&(l+=mr(p,n),i++),gr(p,n,s,o),i++}let f=u>0?d/u*1e3:0;return{issues:n,metrics:{childCount:d,areaPx:u,density:f,uniqueFontSizes:r,lineHeightRatios:a,uniqueColors:c,misalignedCount:l},summary:{totalChecked:i,passed:i-n.length,failed:n.length}}}var jn,lr,Yn=U(()=>{"use strict";q();jn=0;lr=[[10,12,14,16,18,20,24,28,32,36,40,48,56,64,72],[12,14,16,20,24,32,40,48],[12,14,16,18,21,24,30,36,48,60,72]]});function ne(){return`mc-${++Qn}`}function Zn(e){if(Mt.test(e.name))return!0;let t=e.parent,n=0;for(;t&&n<4;){if("name"in t&&Mt.test(t.name)||"type"in t&&(t.type==="COMPONENT"||t.type==="INSTANCE")&&"name"in t&&Mt.test(t.name))return!0;t=t.parent,n++}return!1}function es(e){return e.trim().split(/\s+/).filter(Boolean).length}function Ir(e,t){let n=e.characters;if(!n||n.trim().length===0){t.push({id:ne(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:"Empty text node \u2014 remove or add content.",currentValue:"(empty)",autoFixable:!1});return}let s=n.trim(),o=es(s),i=Zn(e);if((yr.test(s)||br.test(s))&&t.push({id:ne(),type:"naming",severity:"warning",nodeId:e.id,nodeName:e.name,message:`"${s.substring(0,40)}" \u2014 avoid "click/tap here". Use descriptive action: "Download report", "View details".`,currentValue:s.substring(0,60),suggestions:['Use verb + object: "Download PDF", "View pricing", "Start trial"'],autoFixable:!1}),hr.test(s)&&t.push({id:ne(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:'"Learn more" is vague \u2014 specify what the user will learn: "Learn more about pricing".',currentValue:s,suggestions:['Add specificity: "Learn more about [topic]"'],autoFixable:!1}),i&&o<=2){let r=s.toLowerCase().replace(/[.!]/g,"");Sr.has(r)&&t.push({id:ne(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`Generic CTA "${s}" \u2014 use a specific action: "Save changes", "Send message", "Create account".`,currentValue:s,suggestions:["Replace with verb + object describing the outcome"],autoFixable:!1})}if(i&&o>5&&t.push({id:ne(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`CTA too long (${o} words): "${s.substring(0,50)}\u2026". Keep CTAs to 2\u20135 words.`,currentValue:`${o} words`,suggestions:["Shorten to verb + object (2-5 words)"],autoFixable:!1}),(vr.test(s)||kr.test(s))&&t.push({id:ne(),type:"naming",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Placeholder text detected: "${s.substring(0,40)}\u2026". Replace with real content.`,currentValue:s.substring(0,60),suggestions:["Replace with actual copy or realistic sample data"],autoFixable:!1}),o>80&&!i&&t.push({id:ne(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`Long text block (${o} words). Break into shorter paragraphs or use bullet points for readability.`,currentValue:`${o} words`,suggestions:["Break into paragraphs of \u226450 words","Use bullet points for lists","Add subheadings"],autoFixable:!1}),s===s.toUpperCase()&&s!==s.toLowerCase()&&o>3&&t.push({id:ne(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`All-caps text with ${o} words: "${s.substring(0,40)}\u2026". ALL CAPS reduces readability \u2014 use sentence case or title case.`,currentValue:s.substring(0,60),suggestions:["Use sentence case for readability","Reserve ALL CAPS for short labels (1-2 words)"],autoFixable:!1}),Nr.test(s)){let a=(s.match(/\b\d{4,}\b/g)||[]).filter(c=>{let l=parseInt(c,10);return l<1900||l>2099});a.length>0&&t.push({id:ne(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`Unformatted number${a.length>1?"s":""}: ${a.join(", ")}. Use thousand separators for readability.`,currentValue:a.join(", "),suggestions:["Format as 1,000,000 or 1 000 000"],autoFixable:!1})}}function ts(e,t,n,s,o){var i;if(!(s&&"locked"in e&&e.locked)&&!(o&&"visible"in e&&!e.visible)){if(e.type==="TEXT"){n.totalTextNodes++;let r=((i=e.characters)==null?void 0:i.trim())||"",a=es(r);a>0&&(n.wordCounts.push(a),a>n.longestParagraph&&(n.longestParagraph=a)),Zn(e)&&n.ctaNodes++,Ir(e,t)}if("children"in e)for(let r of e.children)ts(r,t,n,s,o)}}function ns(e,t={}){var a,c;Qn=0;let n=[],s=(a=t.skipLocked)!=null?a:!0,o=(c=t.skipHidden)!=null?c:!0,i={totalTextNodes:0,ctaNodes:0,wordCounts:[],longestParagraph:0};for(let l of e)ts(l,n,i,s,o);let r=i.wordCounts.length>0?i.wordCounts.reduce((l,d)=>l+d,0)/i.wordCounts.length:0;return{issues:n,metrics:{totalTextNodes:i.totalTextNodes,ctaNodes:i.ctaNodes,avgWordCount:Math.round(r*10)/10,longestParagraph:i.longestParagraph},summary:{totalChecked:i.totalTextNodes,passed:i.totalTextNodes-n.length,failed:n.length}}}var Qn,yr,hr,br,Sr,vr,kr,Nr,Mt,ss=U(()=>{"use strict";Qn=0;yr=/\bclick\s+here\b/i,hr=/^learn\s+more\.?$/i,br=/\btap\s+here\b/i,Sr=new Set(["submit","ok","okay","next","continue","go","yes","no","done","send","save","apply"]),vr=/\blorem\s+ipsum\b/i,kr=/^(enter\s+text|type\s+here|placeholder|sample\s+text|your\s+text|add\s+text)\.?$/i,Nr=/\b\d{4,}\b/,Mt=/button|btn|cta|action|submit|link/i});function ve(){return`conv-${++as}`}function Ar(e){if(os.test(e.name))return!0;let t=e.parent,n=0;for(;t&&n<3;){if("name"in t&&os.test(t.name))return!0;t=t.parent,n++}return!1}function wr(e){return cs.test(e.name)}function is(e){if(!("fills"in e))return null;let t=e.fills;if(t===figma.mixed||!Array.isArray(t))return null;let n=t.find(s=>s.type==="SOLID"&&s.visible!==!1);return n?n.color:null}function rs(e,t,n){let s=[e,t,n].map(o=>o<=.03928?o/12.92:Math.pow((o+.055)/1.055,2.4));return .2126*s[0]+.7152*s[1]+.0722*s[2]}function Er(e,t){let n=Math.max(e,t),s=Math.min(e,t);return(n+.05)/(s+.05)}function ls(e,t,n,s,o){if(!(s&&"locked"in e&&e.locked)&&!(o&&"visible"in e&&!e.visible)&&(Ar(e)&&"width"in e&&"height"in e&&t.push({node:e,x:"x"in e?e.x:0,y:"y"in e?e.y:0,width:e.width,height:e.height,absoluteY:n+("y"in e?e.y:0)}),"children"in e)){let i=n+("y"in e?e.y:0);for(let r of e.children)ls(r,t,i,s,o)}}function ds(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if(wr(e)){let o=!1,i=e.parent;if(i&&"children"in i){for(let r of i.children)if(r.type==="TEXT"&&r.id!==e.id){o=!0;break}}t.push({node:e,hasLabel:o})}if("children"in e)for(let o of e.children)ds(o,t,n,s)}}function Je(e,t,n,s){if(n&&"locked"in e&&e.locked||s&&"visible"in e&&!e.visible)return!1;if(t.test(e.name))return!0;if("children"in e){for(let o of e.children)if(Je(o,t,n,s))return!0}return!1}function Tr(e,t,n){if(t.length===0||!("height"in e))return!1;let s=e.height*.7,o=t.some(i=>i.y+i.height5&&n.push({id:ve(),type:"accessibility",severity:"warning",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:`${t.length} form fields on one screen. More than 5 fields increases abandonment \u2014 consider splitting into steps or removing optional fields.`,currentValue:`${t.length} fields`,suggestions:["Split into multi-step form with progress indicator",'Remove optional fields or move to "Advanced" section',"Expedia gained $12M/year by removing one field"],autoFixable:!1});let s=t.filter(o=>!o.hasLabel);s.length>0&&n.push({id:ve(),type:"accessibility",severity:"warning",nodeId:s[0].node.id,nodeName:s[0].node.name,message:`${s.length} form field${s.length===1?"":"s"} without visible labels. Labels improve completion rate and accessibility.`,currentValue:`${s.length} unlabeled`,suggestions:["Add visible label text above or beside each input","Don't rely on placeholder text alone as labels"],autoFixable:!1})}function Rr(e,t,n,s,o){if(t.length<=3)return!1;let i=Je(e,Cr,s,o);return!i&&t.length>5&&n.push({id:ve(),type:"accessibility",severity:"info",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:"Long form without progress indicator. A step counter or progress bar reduces perceived effort.",suggestions:['Add "Step 1 of 3" or a progress bar',"Show users how far they've come and what's left"],autoFixable:!1}),i}function $r(e,t,n,s,o){if(t.length===0||!Je(e,cs,s,o))return;Je(e,xr,s,o)||n.push({id:ve(),type:"accessibility",severity:"info",nodeId:"id"in e?e.id:"",nodeName:"name"in e?e.name:"",message:"Form with CTA but no trust signals (security badges, reviews, guarantees). Trust elements near CTAs increase conversion.",suggestions:["Add security badge or lock icon near submit button","Show testimonials, ratings, or guarantees near the CTA"],autoFixable:!1})}function us(e,t={}){var d,u;as=0;let n=[],s=(d=t.skipLocked)!=null?d:!0,o=(u=t.skipHidden)!=null?u:!0,i=0,r=0,a=!1,c=!1,l=0;for(let f of e){let m=[];ls(f,m,0,s,o),i+=m.length;let g=[];ds(f,g,s,o),r+=g.length,m.length>0&&(Tr(f,m,n)&&(a=!0),Lr(f,m,n),l+=2),g.length>0&&(Pr(f,g,n),Rr(f,g,n,s,o)&&(c=!0),l+=2),$r(f,m,n,s,o),l++}return{issues:n,metrics:{ctaCount:i,formFieldCount:r,ctaAboveFold:a,hasProgressIndicator:c},summary:{totalChecked:l,passed:l-n.length,failed:n.length}}}var as,os,cs,Cr,xr,fs=U(()=>{"use strict";q();as=0;os=/button|btn|cta|action|submit|primary/i,cs=/input|field|text.?area|select|dropdown|picker|combo|search|email|password|phone|number.?field/i,Cr=/progress|step|stepper|breadcrumb|wizard|indicator|pagination/i,xr=/badge|trust|security|lock|shield|guarantee|verified|secure|ssl|certification|review|rating|star/i});function Ee(){return`cog-${++ms}`}function Dr(e){return Mr.test(e.name)}function _r(e){return Or.test(e.name)}function ys(e){return gs.test(e.name)}function Br(e){return Fr.test(e.name)}function Gr(e){return Vr.test(e.name)}function Ur(e){if(!ps.test(e.name)&&!gs.test(e.name)||!("children"in e))return!1;let t=e.children,n=t.some(o=>o.type==="TEXT");return t.some(o=>o.type==="VECTOR"||o.type==="BOOLEAN_OPERATION"||ps.test(o.name))&&!n}function hs(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if(_r(e)){t.push(e);return}if("children"in e)for(let o of e.children)hs(o,t,n,s)}}function bs(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)&&(ys(e)&&t.push(e),"children"in e))for(let o of e.children)bs(o,t,n,s)}function zr(e){let t=e.match(/h(\d)/i);return t?parseInt(t[1],10):/title|headline/i.test(e)?1:/subtitle|subhead/i.test(e)||/heading/i.test(e)?2:null}function Ss(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if(Br(e)){let o=zr(e.name);o!==null&&t.push({node:e,level:o})}if("children"in e)for(let o of e.children)Ss(o,t,n,s)}}function vs(e,t,n,s){if(n&&"locked"in e&&e.locked||s&&"visible"in e&&!e.visible)return t;let o=t;if("children"in e)for(let i of e.children){let r=vs(i,t+1,n,s);r>o&&(o=r)}return o}function ks(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)&&(Gr(e)&&"opacity"in e&&e.opacity<1&&t.push(e),"children"in e))for(let o of e.children)ks(o,t,n,s)}function Ns(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)&&(ys(e)&&Ur(e)&&t.push(e),"children"in e))for(let o of e.children)Ns(o,t,n,s)}function Hr(e,t,n,s){let o=[];Is(e,o,n,s);let i=0;for(let r of o){let a=[];hs(r,a,n,s),i+=a.length,a.length>7&&t.push({id:Ee(),type:"accessibility",severity:"warning",nodeId:r.id,nodeName:r.name,message:`Navigation has ${a.length} items \u2014 Miller's Law suggests 7\xB12 is the working memory limit. Consider grouping or progressive disclosure.`,currentValue:`${a.length} nav items`,suggestions:["Group related items under expandable sections",'Use "More" menu for less-used items',"Limit primary navigation to 5-7 items"],autoFixable:!1})}return i}function Is(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if(Dr(e)){t.push(e);return}if("children"in e)for(let o of e.children)Is(o,t,n,s)}}function Wr(e,t,n,s){let o=[];return bs(e,o,n,s),o.length>5&&t.push({id:Ee(),type:"accessibility",severity:"warning",nodeId:e.id,nodeName:e.name,message:`${o.length} CTAs/buttons on one screen \u2014 choice overload reduces decision-making ability (Hick's Law). Prioritize one primary action.`,currentValue:`${o.length} CTAs`,suggestions:["Establish clear primary/secondary/tertiary action hierarchy","Reduce to 1 primary CTA per viewport","Group related actions in a dropdown or overflow menu"],autoFixable:!1}),o.length}function jr(e,t,n,s){let o=[];if(Ss(e,o,n,s),o.length<2)return o.map(r=>r.level);let i=o.sort((r,a)=>{let c="y"in r.node?r.node.y:0,l="y"in a.node?a.node.y:0;return c-l});for(let r=1;ra+1&&t.push({id:Ee(),type:"accessibility",severity:"info",nodeId:i[r].node.id,nodeName:i[r].node.name,message:`Heading hierarchy gap: jumps from level ${a} to level ${c}. Screen readers and users rely on sequential heading structure.`,currentValue:`h${a} \u2192 h${c}`,suggestions:[`Add an h${a+1} between these levels`,"Ensure headings follow a logical descending order"],autoFixable:!1})}return i.map(r=>r.level)}function Kr(e,t,n,s){var i;let o=[];ks(e,o,n,s);for(let r of o){let a=r.parent,c=!1;if(a&&"children"in a){for(let l of a.children)if(l.type==="TEXT"&&l.id!==r.id){let d=((i=l.characters)==null?void 0:i.toLowerCase())||"";if(d.includes("required")||d.includes("complete")||d.includes("fill")||d.includes("select")||d.includes("first")){c=!0;break}}}c||t.push({id:Ee(),type:"accessibility",severity:"info",nodeId:r.id,nodeName:r.name,message:`Disabled element "${r.name}" without visible explanation. Users should understand WHY an action is unavailable and how to enable it.`,suggestions:["Add helper text explaining what needs to happen first","Use a tooltip on hover explaining the disabled state",'Show a brief inline message (e.g., "Complete all fields to continue")'],autoFixable:!1})}}function qr(e,t,n,s){let o=[];return Ns(e,o,n,s),o.length>3&&t.push({id:Ee(),type:"accessibility",severity:"info",nodeId:e.id,nodeName:e.name,message:`${o.length} icon-only buttons without text labels. Icons alone are ambiguous \u2014 add labels or ensure tooltips are present.`,currentValue:`${o.length} icon-only`,suggestions:["Add visible text labels to icon buttons","Add tooltips that appear on hover/focus","Use aria-label for accessibility (ensure design indicates this)"],autoFixable:!1}),o.length}function Cs(e,t={}){var u,f;ms=0;let n=[],s=(u=t.skipLocked)!=null?u:!0,o=(f=t.skipHidden)!=null?f:!0,i=0,r=0,a=0,c=[],l=0,d=0;for(let m of e){i+=Hr(m,n,s,o),d++,r+=Wr(m,n,s,o),d++;let g=jr(m,n,s,o);c=[...c,...g],d++,Kr(m,n,s,o),d++,l+=qr(m,n,s,o),d++;let p=vs(m,0,s,o);p>a&&(a=p)}return{issues:n,metrics:{navItemCount:i,ctaCount:r,maxNestingDepth:a,headingLevels:[...new Set(c)].sort(),iconOnlyButtons:l},summary:{totalChecked:d,passed:d-n.length,failed:n.length}}}var ms,Mr,Or,gs,Fr,Vr,ps,xs=U(()=>{"use strict";ms=0;Mr=/nav|menu|sidebar|tab.?bar|bottom.?bar|header.?nav|navigation|top.?bar/i,Or=/nav.?item|menu.?item|tab(?!le)|link/i,gs=/button|btn|cta|action|submit|primary/i,Fr=/heading|title|h[1-6]|headline/i,Vr=/disabled|inactive|dimmed|greyed/i,ps=/icon|ico|svg|glyph/i});function Xr(){return`fitts-${++As}`}function Yr(e){return Jr.test(e.name)}function ws(e,t,n,s,o){let i=o||"locked"in e&&e.locked,r="visible"in e&&!e.visible;if(n&&i)return{checked:0,failed:0};if(s&&r)return{checked:0,failed:0};let a=0,c=0;if(Yr(e)&&"width"in e&&"height"in e){a++;let l=e.width,d=e.height;(l{"use strict";As=0;Jr=/button|btn|cta|action|submit|link|toggle|switch|checkbox|radio|tab(?!le)/i,ke=44});function Qr(){return`gestalt-${++Ls}`}function Ps(e,t,n,s,o){let i=o||"locked"in e&&e.locked,r="visible"in e&&!e.visible;if(n&&i)return{checked:0,failed:0};if(s&&r)return{checked:0,failed:0};let a=0,c=0;if((e.type==="FRAME"||e.type==="COMPONENT"||e.type==="INSTANCE")&&"children"in e){let d=e;if(d.layoutMode==="NONE"&&d.children.length>=3){a++;let u=d.children.filter(f=>"visible"in f&&f.visible&&"y"in f);if(u.length>=3){let f=[...u].sort((g,p)=>g.y-p.y),m=[];for(let g=1;g=2){let g=new Set(m.map(p=>Math.round(p)));g.size>2&&(c++,t.push({id:Qr(),type:"gestalt",severity:"info",nodeId:e.id,nodeName:e.name,message:`Frame "${e.name}" has ${g.size} different spacing gaps between children (${[...g].join(", ")}px) \u2014 inconsistent proximity weakens visual grouping (Gestalt proximity principle)`,currentValue:`${g.size} distinct gaps`,suggestions:["Use auto-layout with consistent gap spacing","Standardize spacing between sibling elements"],autoFixable:!1}))}}}}if("children"in e)for(let d of e.children){let u=Ps(d,t,n,s,i);a+=u.checked,c+=u.failed}return{checked:a,failed:c}}function Rs(e,t={}){var a,c;Ls=0;let n=[],s=(a=t.skipLocked)!=null?a:!0,o=(c=t.skipHidden)!=null?c:!0,i=0,r=0;for(let l of e){let d=Ps(l,n,s,o,!1);i+=d.checked,r+=d.failed}return{issues:n,summary:{totalChecked:i,passed:i-r,failed:r}}}var Ls,$s=U(()=>{"use strict";Ls=0});function Zr(){return`detach-${++Ms}`}function Os(e,t,n,s,o){var l;let i=o||"locked"in e&&e.locked,r="visible"in e&&!e.visible;if(n&&i)return{checked:0,failed:0};if(s&&r)return{checked:0,failed:0};let a=0,c=0;if(e.type==="FRAME"&&"children"in e){a++;let d=ea.test(e.name),u=ta.test(e.name)&&((l=e.parent)==null?void 0:l.type)!=="PAGE"&&e.children.length>0;if(d)c++,t.push({id:Zr(),type:"detachedInstance",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Frame "${e.name}" appears to be a detached component instance. Detaching breaks the link to the source component and prevents design system updates.`,currentValue:"Detached instance",suggestions:["Re-attach by replacing with the original component instance",'If intentional, rename to remove "detach" from the name'],autoFixable:!1});else if(u){let f=e.name.split(/[\s\-\/]/);f.length>=2&&f.every(m=>m.length>0)}}if("children"in e)for(let d of e.children){let u=Os(d,t,n,s,i);a+=u.checked,c+=u.failed}return{checked:a,failed:c}}function Fs(e,t={}){var a,c;Ms=0;let n=[],s=(a=t.skipLocked)!=null?a:!0,o=(c=t.skipHidden)!=null?c:!0,i=0,r=0;for(let l of e){let d=Os(l,n,s,o,!1);i+=d.checked,r+=d.failed}return{issues:n,summary:{totalChecked:i,passed:i-r,failed:r}}}var Ms,ea,ta,Vs=U(()=>{"use strict";Ms=0;ea=/detach/i,ta=/^[A-Z][a-zA-Z]+(?:\s*[-\/]\s*[A-Za-z]+)*$/});function Ot(){return`resp-${++_s}`}function Bs(e){return e.type==="FRAME"||e.type==="COMPONENT"||e.type==="INSTANCE"}function Ds(e){for(let t of na){let n=e.match(t);if(n){for(let s of n.slice(1))if(sa.has(s.toLowerCase()))return s.toLowerCase()}}return null}function Gs(e,t,n,s,o){let i=o||"locked"in e&&e.locked,r="visible"in e&&!e.visible;if(n&&i)return{checked:0,failed:0,fixedWidthCount:0};if(s&&r)return{checked:0,failed:0,fixedWidthCount:0};let a=0,c=0,l=0;if(Bs(e)){let d=e;a++;let u=d.layoutSizingHorizontal==="FIXED"||d.layoutSizingHorizontal===void 0,f=!d.parent||d.parent.type==="PAGE",m=d.layoutMode!=="NONE",g="minWidth"in d&&d.minWidth!==null&&d.minWidth!==void 0||"maxWidth"in d&&d.maxWidth!==null&&d.maxWidth!==void 0;u&&!f&&!g&&m&&d.width>200&&(l++,c++,t.push({id:Ot(),type:"responsive",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Frame "${e.name}" has fixed width (${Math.round(d.width)}px) with auto-layout but no fill/hug sizing \u2014 may not adapt to different screen sizes`,currentValue:`${Math.round(d.width)}px fixed`,suggestions:['Set horizontal sizing to "Fill" for responsive behavior',"Add min-width/max-width constraints",'Use "Hug contents" if the frame should shrink-wrap'],autoFixable:!1}))}if("children"in e)for(let d of e.children){let u=Gs(d,t,n,s,i);a+=u.checked,c+=u.failed,l+=u.fixedWidthCount}return{checked:a,failed:c,fixedWidthCount:l}}function Us(e,t,n,s,o){let i=o||"locked"in e&&e.locked,r="visible"in e&&!e.visible;if(n&&i)return{checked:0,failed:0,riskCount:0};if(s&&r)return{checked:0,failed:0,riskCount:0};let a=0,c=0,l=0;if(e.type==="TEXT"){let d=e;a++;let u=d.fontSize!==figma.mixed?d.fontSize:14,f=d.textAutoResize;if(f==="NONE"||f==="TRUNCATE"){let m=d.characters.length,g=m*u*oa,p=d.width;m>5&&g>p*.8&&(l++,c++,t.push({id:Ot(),type:"responsive",severity:"info",nodeId:e.id,nodeName:e.name,message:`Text "${e.name}" may truncate \u2014 content fills ~${Math.round(g/p*100)}% of fixed width (${Math.round(p)}px). Translations or dynamic content could overflow.`,currentValue:`${m} chars in ${Math.round(p)}px`,suggestions:["Use auto-width or auto-height text resizing","Allow text to wrap by setting textAutoResize to HEIGHT","Add ellipsis handling if truncation is intentional"],autoFixable:!1}))}}if("children"in e)for(let d of e.children){let u=Us(d,t,n,s,i);a+=u.checked,c+=u.failed,l+=u.riskCount}return{checked:a,failed:c,riskCount:l}}function zs(e,t,n,s,o){let i=o||"locked"in e&&e.locked,r="visible"in e&&!e.visible;if(n&&i)return{checked:0,failed:0,missingCount:0};if(s&&r)return{checked:0,failed:0,missingCount:0};let a=0,c=0,l=0;if(Bs(e)){let d=e;if(d.layoutMode==="HORIZONTAL"&&"children"in d){let u=d.children.filter(f=>"visible"in f&&f.visible);u.length>=3&&(a++,("layoutWrap"in d?d.layoutWrap:"NO_WRAP")!=="WRAP"&&(l++,c++,t.push({id:Ot(),type:"responsive",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Horizontal layout "${e.name}" has ${u.length} children without wrap \u2014 content won't reflow on smaller screens`,currentValue:`${u.length} children, no wrap`,suggestions:['Enable "Wrap" on the auto-layout to allow content reflow',"Consider switching to vertical layout on mobile breakpoints","Use min-width on children to control when wrapping occurs"],autoFixable:!1})))}}if("children"in e)for(let d of e.children){let u=zs(d,t,n,s,i);a+=u.checked,c+=u.failed,l+=u.missingCount}return{checked:a,failed:c,missingCount:l}}function ia(e){let t=new Set;for(let n of e)ra(n,t);return Array.from(t)}function ra(e,t){if(Ds(e.name)&&t.add(e.name),"children"in e)for(let s of e.children)Ds(s.name)&&t.add(s.name)}function Hs(e,t){var u,f;_s=0;let n=[],s=(u=t==null?void 0:t.skipLocked)!=null?u:!0,o=(f=t==null?void 0:t.skipHidden)!=null?f:!0,i=0,r=0,a=0,c=0,l=0;for(let m of e){let g=Gs(m,n,s,o,!1);i+=g.checked,r+=g.failed,a+=g.fixedWidthCount;let p=Us(m,n,s,o,!1);i+=p.checked,r+=p.failed,c+=p.riskCount;let h=zs(m,n,s,o,!1);i+=h.checked,r+=h.failed,l+=h.missingCount}let d=ia(e);return{issues:n,metrics:{fixedWidthElements:a,textTruncationRisk:c,missingAutoLayout:l,breakpointVariants:d},summary:{totalChecked:i,passed:i-r,failed:r}}}var _s,na,sa,oa,Ws=U(()=>{"use strict";_s=0;na=[/^(.+)\s*[-–—]\s*(desktop|tablet|mobile|phone|sm|md|lg|xl|xxl|small|medium|large)\s*$/i,/^(.+)\/(desktop|tablet|mobile|phone|sm|md|lg|xl|xxl|small|medium|large)\s*$/i,/^(desktop|tablet|mobile|phone|sm|md|lg|xl|xxl|small|medium|large)\s*[-–—/]\s*(.+)$/i,/^(.+)\s*\[(desktop|tablet|mobile|phone|sm|md|lg|xl|xxl|small|medium|large)\]\s*$/i],sa=new Set(["desktop","tablet","mobile","phone","sm","md","lg","xl","xxl","small","medium","large"]),oa=.5});var Wt={};kn(Wt,{DEFAULT_LINT_SETTINGS:()=>K,clearIgnored:()=>Gt,findNodesWithSameValue:()=>Ht,getIgnoredState:()=>Ut,ignoreAllOfType:()=>Bt,ignoreError:()=>_t,ignoreNode:()=>Dt,lintSelection:()=>Te,restoreIgnoredState:()=>zt,runDesignLint:()=>se});function G(e,t,n){return n?`${e}::${t}::${n}`:`${e}::${t}`}function Dt(e){B.add(e)}function _t(e,t,n){D.add(G(e,t,n))}function Bt(e,t){for(let n of e)n.errorType===t&&D.add(G(n.nodeId,n.errorType))}function Gt(){B.clear(),D.clear()}function Ut(){return{nodeIds:Array.from(B),errorKeys:Array.from(D)}}function zt(e){B=new Set(e.nodeIds),D=new Set(e.errorKeys)}function Ks(e){if(e.type==="SOLID"){let{r:t,g:n,b:s}=e.color,o=F(t,n,s),i=e.opacity!==void 0&&e.opacity<1?` (${Math.round(e.opacity*100)}%)`:"";return o+i}return e.type==="IMAGE"?"Image fill":e.type==="VIDEO"?"Video fill":e.type.includes("GRADIENT")?`${e.type.replace("GRADIENT_","").toLowerCase()} gradient`:e.type}function Qe(e,t){try{if("boundVariables"in e){let n=e.boundVariables;if(n&&n[t])return!0}}catch(n){}return!1}function Ye(e,t,n){if(!("fills"in e))return;let s=e.fills;if(s===figma.mixed||!Array.isArray(s))return;let o=s.filter(i=>i.visible!==!1);if(o.length!==0&&!Qe(e,"fills")){if("fillStyleId"in e){let i=e.fillStyleId;if(i&&i!==""&&i!==figma.mixed)return}for(let i of o){try{let a=i.boundVariables;if(a&&a.color)continue}catch(a){}let r=Ks(i);t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"fill",message:`Missing fill style: ${r}`,value:r,path:n})}}}function Ft(e,t,n){if(!("strokes"in e))return;let s=e.strokes;if(!Array.isArray(s))return;let o=s.filter(i=>i.visible!==!1);if(o.length!==0&&!Qe(e,"strokes")){if("strokeStyleId"in e){let i=e.strokeStyleId;if(i&&i!==""&&i!==figma.mixed)return}for(let i of o){let r=Ks(i),a="strokeWeight"in e?` (${e.strokeWeight}px)`:"";t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"stroke",message:`Missing stroke style: ${r}${a}`,value:r+a,path:n})}}}function Vt(e,t,n){if(!("effects"in e))return;let s=e.effects;if(!Array.isArray(s)||s.length===0)return;let o=s.filter(r=>r.visible!==!1);if(o.length===0)return;if("effectStyleId"in e){let r=e.effectStyleId;if(r&&r!==""&&r!==figma.mixed)return}let i=o.map(r=>{let a=[r.type.replace(/_/g," ").toLowerCase()];if("radius"in r&&a.push(`r:${r.radius}`),"color"in r&&r.color){let c=r.color;a.push(F(c.r,c.g,c.b))}return a.join(" ")});t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"effect",message:`Missing effect style: ${i.join(", ")}`,value:i.join(", "),path:n})}function aa(e,t,n){if("textStyleId"in e){let a=e.textStyleId;if(a&&a!==""&&a!==figma.mixed)return}let s=e.fontName!==figma.mixed?e.fontName:null,o=e.fontSize!==figma.mixed?e.fontSize:null,i=[];s&&i.push(`${s.family} ${s.style}`),o&&i.push(`${o}px`);let r=i.join(" / ")||"unknown text style";t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"text",message:`Missing text style: ${r}`,value:r,path:n})}function js(e,t,n,s){if(!("cornerRadius"in e)||Qe(e,"topLeftRadius")||Qe(e,"cornerRadius"))return;let o=e.cornerRadius;if(o===figma.mixed){let i=[e.topLeftRadius,e.topRightRadius,e.bottomLeftRadius,e.bottomRightRadius].filter(r=>r!=null);for(let r of i)if(!s.includes(r)){t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"radius",message:`Non-standard border radius: ${r}px (allowed: ${s.join(", ")})`,value:`${r}px`,path:n});break}return}typeof o=="number"&&o>0&&!s.includes(o)&&t.push({nodeId:e.id,nodeName:e.name,nodeType:e.type,errorType:"radius",message:`Non-standard border radius: ${o}px (allowed: ${s.join(", ")})`,value:`${o}px`,path:n})}function ca(e,t,n,s){if(!(e.type==="GROUP"||e.type==="SLICE"||e.type==="CONNECTOR")&&e.type!=="COMPONENT_SET")switch(e.type){case"TEXT":t.checkTextStyles&&aa(e,n,s),t.checkFills&&Ye(e,n,s);break;case"FRAME":case"SECTION":t.checkFills&&Ye(e,n,s),t.checkStrokes&&Ft(e,n,s),t.checkEffects&&Vt(e,n,s),t.checkRadius&&js(e,n,s,t.allowedRadii);break;case"RECTANGLE":case"COMPONENT":case"INSTANCE":t.checkFills&&Ye(e,n,s),t.checkStrokes&&Ft(e,n,s),t.checkEffects&&Vt(e,n,s),t.checkRadius&&js(e,n,s,t.allowedRadii);break;case"ELLIPSE":case"POLYGON":case"STAR":case"VECTOR":case"LINE":case"BOOLEAN_OPERATION":t.checkFills&&Ye(e,n,s),t.checkStrokes&&Ft(e,n,s),t.checkEffects&&Vt(e,n,s);break}}function qs(e,t,n,s,o){let i=0,r=o||"locked"in e&&e.locked,a="visible"in e&&!e.visible;if(t.skipLockedLayers&&r||t.skipHiddenLayers&&a)return 0;let c=s?`${s} > ${e.name}`:e.name;if(i++,!B.has(e.id)){let l=n.length;ca(e,t,n,c);for(let d=n.length-1;d>=l;d--){let u=n[d];(D.has(G(u.nodeId,u.errorType))||D.has(G(u.nodeId,u.errorType,u.value)))&&n.splice(d,1)}}if("children"in e)for(let l of e.children)i+=qs(l,t,n,c,r);return i}function X(e,t){for(let n of t)if(new RegExp("^"+n.replace(/[.+^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*").replace(/\?/g,".")+"$").test(e))return!0;return!1}function se(e,t=K){var f,m;let n=[],s=0,o=t.ignorePatterns||[],i=t.severityOverrides||{};for(let g of e)s+=qs(g,t,n,"",!1);let r={skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers,scale:t.spacingScale};if(t.checkSpacing&&i.spacing!=="off"){let g=Fn(e,r);for(let p of g.issues){let h=p.currentValue||"";B.has(p.nodeId)||D.has(G(p.nodeId,"spacing"))||D.has(G(p.nodeId,"spacing",h))||X(p.nodeName,o)||n.push({nodeId:p.nodeId,nodeName:p.nodeName,nodeType:"FRAME",errorType:"spacing",message:p.message,value:h,path:p.nodeName,property:(m=(f=p.fixAction)==null?void 0:f.params)==null?void 0:m.property})}}if(t.checkAutoLayout&&i.autoLayout!=="off"){let g=Bn(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let p of g.issues)B.has(p.nodeId)||D.has(G(p.nodeId,"autoLayout"))||X(p.nodeName,o)||n.push({nodeId:p.nodeId,nodeName:p.nodeName,nodeType:"FRAME",errorType:"autoLayout",message:p.message,value:p.currentValue||"",path:p.nodeName})}if(t.checkAccessibility&&i.accessibility!=="off"){let g=Hn(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let p of g.issues)B.has(p.nodeId)||D.has(G(p.nodeId,"accessibility"))||X(p.nodeName,o)||n.push({nodeId:p.nodeId,nodeName:p.nodeName,nodeType:"FRAME",errorType:"accessibility",message:p.message,value:p.currentValue||"",path:p.nodeName,severity:p.severity})}if(t.checkVisualQuality&&i.visualQuality!=="off"){let g=Jn(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let p of g.issues)B.has(p.nodeId)||D.has(G(p.nodeId,"visualQuality"))||X(p.nodeName,o)||n.push({nodeId:p.nodeId,nodeName:p.nodeName,nodeType:"FRAME",errorType:"visualQuality",message:p.message,value:p.currentValue||"",path:p.nodeName,severity:p.severity})}if(t.checkMicrocopy&&i.microcopy!=="off"){let g=ns(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let p of g.issues)B.has(p.nodeId)||D.has(G(p.nodeId,"microcopy"))||X(p.nodeName,o)||n.push({nodeId:p.nodeId,nodeName:p.nodeName,nodeType:"TEXT",errorType:"microcopy",message:p.message,value:p.currentValue||"",path:p.nodeName,severity:p.severity})}if(t.checkConversion&&i.conversion!=="off"){let g=us(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let p of g.issues)B.has(p.nodeId)||D.has(G(p.nodeId,"conversion"))||X(p.nodeName,o)||n.push({nodeId:p.nodeId,nodeName:p.nodeName,nodeType:"FRAME",errorType:"conversion",message:p.message,value:p.currentValue||"",path:p.nodeName,severity:p.severity})}if(t.checkCognitive&&i.cognitive!=="off"){let g=Cs(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let p of g.issues)B.has(p.nodeId)||D.has(G(p.nodeId,"cognitive"))||X(p.nodeName,o)||n.push({nodeId:p.nodeId,nodeName:p.nodeName,nodeType:"FRAME",errorType:"cognitive",message:p.message,value:p.currentValue||"",path:p.nodeName,severity:p.severity})}if(t.checkFittsLaw&&i.fittsLaw!=="off"){let g=Es(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let p of g.issues)B.has(p.nodeId)||D.has(G(p.nodeId,"fittsLaw"))||X(p.nodeName,o)||n.push({nodeId:p.nodeId,nodeName:p.nodeName,nodeType:"FRAME",errorType:"fittsLaw",message:p.message,value:p.currentValue||"",path:p.nodeName,severity:p.severity})}if(t.checkGestalt&&i.gestalt!=="off"){let g=Rs(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let p of g.issues)B.has(p.nodeId)||D.has(G(p.nodeId,"gestalt"))||X(p.nodeName,o)||n.push({nodeId:p.nodeId,nodeName:p.nodeName,nodeType:"FRAME",errorType:"gestalt",message:p.message,value:p.currentValue||"",path:p.nodeName,severity:p.severity})}if(t.checkDetachedInstances&&i.detachedInstance!=="off"){let g=Fs(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let p of g.issues)B.has(p.nodeId)||D.has(G(p.nodeId,"detachedInstance"))||X(p.nodeName,o)||n.push({nodeId:p.nodeId,nodeName:p.nodeName,nodeType:"FRAME",errorType:"detachedInstance",message:p.message,value:p.currentValue||"",path:p.nodeName,severity:p.severity})}if(t.checkResponsive&&i.responsive!=="off"){let g=Hs(e,{skipLocked:t.skipLockedLayers,skipHidden:t.skipHiddenLayers});for(let p of g.issues)B.has(p.nodeId)||D.has(G(p.nodeId,"responsive"))||X(p.nodeName,o)||n.push({nodeId:p.nodeId,nodeName:p.nodeName,nodeType:"FRAME",errorType:"responsive",message:p.message,value:p.currentValue||"",path:p.nodeName,severity:p.severity})}let a=n.filter(g=>i[g.errorType]!=="off"),c=o.length>0?a.filter(g=>!X(g.nodeName,o)):a;for(let g of c){let p=i[g.errorType];if(p&&p!=="off")g.severity=p;else if(!g.severity)switch(g.errorType){case"fill":case"stroke":case"effect":case"text":case"spacing":g.severity="warning";break;case"radius":case"autoLayout":g.severity="info";break;case"accessibility":g.severity="critical";break;case"visualQuality":g.severity="warning";break;case"microcopy":g.severity="info";break;case"conversion":g.severity="warning";break;case"cognitive":g.severity="info";break;case"responsive":g.severity="warning";break;case"fittsLaw":g.severity="warning";break;case"gestalt":g.severity="info";break;case"detachedInstance":g.severity="warning";break}}let l=new Set(c.map(g=>g.nodeId)).size,d={fill:0,stroke:0,effect:0,text:0,radius:0,spacing:0,autoLayout:0,accessibility:0,visualQuality:0,microcopy:0,conversion:0,cognitive:0,fittsLaw:0,gestalt:0,detachedInstance:0,responsive:0};for(let g of c)d[g.errorType]++;let u={totalErrors:c.length,byType:d,totalNodes:s,nodesWithErrors:l};return{errors:c,ignoredNodeIds:Array.from(B),ignoredErrorKeys:Array.from(D),summary:u}}function Te(e){let t=figma.currentPage.selection;return t.length===0?{errors:[],ignoredNodeIds:[],ignoredErrorKeys:[],summary:{totalErrors:0,byType:{fill:0,stroke:0,effect:0,text:0,radius:0,spacing:0,autoLayout:0,accessibility:0,visualQuality:0,microcopy:0,conversion:0,cognitive:0,fittsLaw:0,gestalt:0,detachedInstance:0,responsive:0},totalNodes:0,nodesWithErrors:0}}:se(t,e)}function Ht(e,t,n,s=K){return se(e,s).errors.filter(i=>i.errorType===t&&i.value===n)}var K,B,D,Ne=U(()=>{"use strict";q();Vn();Gn();Wn();Yn();ss();fs();xs();Ts();$s();Vs();Ws();K={checkFills:!0,checkStrokes:!0,checkEffects:!0,checkTextStyles:!0,checkRadius:!0,checkSpacing:!0,checkAutoLayout:!0,checkAccessibility:!0,checkVisualQuality:!0,checkMicrocopy:!0,checkConversion:!0,checkCognitive:!0,checkFittsLaw:!0,checkGestalt:!0,checkDetachedInstances:!0,checkResponsive:!0,allowedRadii:[0,2,4,8,12,16,24,32],skipLockedLayers:!0,skipHiddenLayers:!0},B=new Set,D=new Set});var mo=Ci((Du,tt)=>{var sn=function(){var e=String.fromCharCode,t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",s={};function o(r,a){if(!s[r]){s[r]={};for(var c=0;c>>8,c[l*2+1]=u%256}return c},decompressFromUint8Array:function(r){if(r==null)return i.decompress(r);for(var a=new Array(r.length/2),c=0,l=a.length;c>1}else{for(d=1,l=0;l>1}h--,h==0&&(h=Math.pow(2,k),k++),delete f[p]}else for(d=u[p],l=0;l>1;h--,h==0&&(h=Math.pow(2,k),k++),u[g]=C++,p=String(m)}if(p!==""){if(Object.prototype.hasOwnProperty.call(f,p)){if(p.charCodeAt(0)<256){for(l=0;l>1}else{for(d=1,l=0;l>1}h--,h==0&&(h=Math.pow(2,k),k++),delete f[p]}else for(d=u[p],l=0;l>1;h--,h==0&&(h=Math.pow(2,k),k++)}for(d=2,l=0;l>1;for(;;)if(y=y<<1,b==a-1){N.push(c(y));break}else b++;return N.join("")},decompress:function(r){return r==null?"":r==""?null:i._decompress(r.length,32768,function(a){return r.charCodeAt(a)})},_decompress:function(r,a,c){var l=[],d,u=4,f=4,m=3,g="",p=[],h,C,k,N,y,b,x,I={val:c(0),position:a,index:1};for(h=0;h<3;h+=1)l[h]=h;for(k=0,y=Math.pow(2,2),b=1;b!=y;)N=I.val&I.position,I.position>>=1,I.position==0&&(I.position=a,I.val=c(I.index++)),k|=(N>0?1:0)*b,b<<=1;switch(d=k){case 0:for(k=0,y=Math.pow(2,8),b=1;b!=y;)N=I.val&I.position,I.position>>=1,I.position==0&&(I.position=a,I.val=c(I.index++)),k|=(N>0?1:0)*b,b<<=1;x=e(k);break;case 1:for(k=0,y=Math.pow(2,16),b=1;b!=y;)N=I.val&I.position,I.position>>=1,I.position==0&&(I.position=a,I.val=c(I.index++)),k|=(N>0?1:0)*b,b<<=1;x=e(k);break;case 2:return""}for(l[3]=x,C=x,p.push(x);;){if(I.index>r)return"";for(k=0,y=Math.pow(2,m),b=1;b!=y;)N=I.val&I.position,I.position>>=1,I.position==0&&(I.position=a,I.val=c(I.index++)),k|=(N>0?1:0)*b,b<<=1;switch(x=k){case 0:for(k=0,y=Math.pow(2,8),b=1;b!=y;)N=I.val&I.position,I.position>>=1,I.position==0&&(I.position=a,I.val=c(I.index++)),k|=(N>0?1:0)*b,b<<=1;l[f++]=e(k),x=f-1,u--;break;case 1:for(k=0,y=Math.pow(2,16),b=1;b!=y;)N=I.val&I.position,I.position>>=1,I.position==0&&(I.position=a,I.val=c(I.index++)),k|=(N>0?1:0)*b,b<<=1;l[f++]=e(k),x=f-1,u--;break;case 2:return p.join("")}if(u==0&&(u=Math.pow(2,m),m++),l[x])g=l[x];else if(x===f)g=C+C.charAt(0);else return null;p.push(g),l[f++]=C+g.charAt(0),u--,C=g,u==0&&(u=Math.pow(2,m),m++)}}};return i}();typeof define=="function"&&define.amd?define(function(){return sn}):typeof tt!="undefined"&&tt!=null?tt.exports=sn:typeof angular!="undefined"&&angular!=null&&angular.module("LZString",[]).factory("LZString",function(){return sn})});var Co={};kn(Co,{applyEffectStyle:()=>dn,applyFillStyle:()=>an,applyStrokeStyle:()=>cn,applyTextStyle:()=>ln});async function an(e,t){let n=figma.getNodeById(e);if(!n||!("fillStyleId"in n))return{success:!1,nodeId:e,nodeName:"",property:"fillStyle",oldValue:"",newValue:"",error:"Node not found or does not support fill styles"};try{let s=await figma.importStyleByKeyAsync(t),o=n.fillStyleId||"";return n.fillStyleId=s.id,{success:!0,nodeId:e,nodeName:n.name,property:"fillStyle",oldValue:o?"existing style":"no style",newValue:s.name}}catch(s){return{success:!1,nodeId:e,nodeName:n.name,property:"fillStyle",oldValue:"",newValue:t,error:s instanceof Error?s.message:String(s)}}}async function cn(e,t){let n=figma.getNodeById(e);if(!n||!("strokeStyleId"in n))return{success:!1,nodeId:e,nodeName:"",property:"strokeStyle",oldValue:"",newValue:"",error:"Node not found or does not support stroke styles"};try{let s=await figma.importStyleByKeyAsync(t),o=n.strokeStyleId||"";return n.strokeStyleId=s.id,{success:!0,nodeId:e,nodeName:n.name,property:"strokeStyle",oldValue:o?"existing style":"no style",newValue:s.name}}catch(s){return{success:!1,nodeId:e,nodeName:n.name,property:"strokeStyle",oldValue:"",newValue:t,error:s instanceof Error?s.message:String(s)}}}async function ln(e,t){let n=figma.getNodeById(e);if(!n||n.type!=="TEXT")return{success:!1,nodeId:e,nodeName:"",property:"textStyle",oldValue:"",newValue:"",error:"Node not found or is not a text node"};try{let s=await figma.importStyleByKeyAsync(t),o=n,i=o.textStyleId||"";return o.textStyleId=s.id,{success:!0,nodeId:e,nodeName:n.name,property:"textStyle",oldValue:i?"existing style":"no style",newValue:s.name}}catch(s){return{success:!1,nodeId:e,nodeName:n.name,property:"textStyle",oldValue:"",newValue:t,error:s instanceof Error?s.message:String(s)}}}async function dn(e,t){let n=figma.getNodeById(e);if(!n||!("effectStyleId"in n))return{success:!1,nodeId:e,nodeName:"",property:"effectStyle",oldValue:"",newValue:"",error:"Node not found or does not support effect styles"};try{let s=await figma.importStyleByKeyAsync(t),o=n.effectStyleId||"";return n.effectStyleId=s.id,{success:!0,nodeId:e,nodeName:n.name,property:"effectStyle",oldValue:o?"existing style":"no style",newValue:s.name}}catch(s){return{success:!1,nodeId:e,nodeName:n.name,property:"effectStyle",oldValue:"",newValue:t,error:s instanceof Error?s.message:String(s)}}}var un=U(()=>{"use strict"});q();q();q();function me(e){var l;let t=e,n=!1;for(;t;){if(t.type==="COMPONENT_SET"){n=!0;break}if(t.parent&&t.parent.type==="COMPONENT_SET"){n=!0;break}t=t.parent}if(!n||!("strokes"in e)||!("cornerRadius"in e)||!("strokeWeight"in e))return!1;let s=e.cornerRadius===5||"topLeftRadius"in e&&"topRightRadius"in e&&"bottomLeftRadius"in e&&"bottomRightRadius"in e&&e.topLeftRadius===5&&e.topRightRadius===5&&e.bottomLeftRadius===5&&e.bottomRightRadius===5,o=e.strokeWeight===1,i=e.strokes,r=i.length>0&&i.some(d=>d.type==="SOLID"&&d.visible!==!1&&d.color?F(d.color.r,d.color.g,d.color.b).toUpperCase()==="#9747FF":!1),a="paddingLeft"in e&&"paddingRight"in e&&"paddingTop"in e&&"paddingBottom"in e&&e.paddingLeft===16&&e.paddingRight===16&&e.paddingTop===16&&e.paddingBottom===16,c=s&&o&&r&&a;return c&&(console.log(`\u{1F3AF} [FILTER] Detected default variant frame styles in ${e.name} - filtering out`),console.log(` Type: ${e.type}, Parent: ${(l=e.parent)==null?void 0:l.type}`),console.log(` Radius: ${String(e.cornerRadius)}, Weight: ${String(e.strokeWeight)}, Color: ${i.length>0&&i[0].type==="SOLID"?F(i[0].color.r,i[0].color.g,i[0].color.b):"none"}`),console.log(` Padding: L=${e.paddingLeft}, R=${e.paddingRight}, T=${e.paddingTop}, B=${e.paddingBottom}`)),c}function we(e){let t=e;for(;t;){if(t.type==="COMPONENT_SET"||t.parent&&t.parent.type==="COMPONENT_SET")return!0;t=t.parent}return!1}async function ge(e){let t=[],n=[],s=[],o=[],i=[],r=new Set,a=new Set,c=new Set,l=new Set,d=new Set;async function u(f){console.log("\u{1F50D} Analyzing node:",f.name,"Type:",f.type);let m=[];if("fillStyleId"in f&&typeof f.fillStyleId=="string"&&m.push(figma.getStyleByIdAsync(f.fillStyleId).then(y=>{if(y!=null&&y.name&&!r.has(y.name)){r.add(y.name);let b=y.name;if("fills"in f&&Array.isArray(f.fills)&&f.fills.length>0){let x=f.fills[0];x.type==="SOLID"&&x.color&&(b=F(x.color.r,x.color.g,x.color.b))}t.push({name:y.name,value:b,type:"fill-style",isToken:!0,isActualToken:!0,source:"figma-style"})}}).catch(console.warn)),"strokeStyleId"in f&&typeof f.strokeStyleId=="string"&&m.push(figma.getStyleByIdAsync(f.strokeStyleId).then(y=>{y!=null&&y.name&&!r.has(y.name)&&(r.add(y.name),t.push({name:y.name,value:y.name,type:"stroke-style",isToken:!0,isActualToken:!0,source:"figma-style"}))}).catch(console.warn)),f.type==="TEXT"&&"textStyleId"in f&&typeof f.textStyleId=="string"&&m.push(figma.getStyleByIdAsync(f.textStyleId).then(y=>{y!=null&&y.name&&!c.has(y.name)&&(c.add(y.name),s.push({name:y.name,value:y.name,type:"text-style",isToken:!0,isActualToken:!0,source:"figma-style"}))}).catch(console.warn)),"effectStyleId"in f&&typeof f.effectStyleId=="string"&&m.push(figma.getStyleByIdAsync(f.effectStyleId).then(y=>{y!=null&&y.name&&!l.has(y.name)&&(l.add(y.name),o.push({name:y.name,value:y.name,type:"effect-style",isToken:!0,isActualToken:!0,source:"figma-style"}))}).catch(console.warn)),await Promise.all(m),"boundVariables"in f&&f.boundVariables){let y=f.boundVariables;console.log(`\u{1F50D} [VARIABLES] Checking bound variables for ${f.name}:`,Object.keys(y));let b=async(A,$,v,T,P)=>{try{let w=Array.isArray(A)?A:[A];for(let te of w)if(te!=null&&te.id&&typeof te.id=="string"){let Q=await yt(te.id);if(console.log(` \u{1F3AF} Found ${$} variable:`,Q),Q&&!v.has(Q)){v.add(Q);let Ce=Q;if(P==="color"&&($==="fills"||$==="strokes")){let ze=await Nn(te.id,f);ze&&ze.startsWith("#")&&(Ce=ze)}T.push({name:Q,value:Ce,type:`${$}-variable`,isToken:!0,isActualToken:!0,source:"figma-variable"}),console.log(` \u2705 Added ${P} token: ${Q} (value: ${Ce})`)}}}catch(w){console.warn(`Error processing ${$} variables:`,w)}},x=async(A,$,v,T,P)=>{if(A&&typeof A=="object"&&"id"in A&&typeof A.id=="string"){let w=await yt(A.id);console.log(` \u{1F3AF} Found ${$} variable:`,w),w&&!v.has(w)&&(v.add(w),T.push({name:w,value:w,type:`${$}-variable`,isToken:!0,isActualToken:!0,source:"figma-variable"}),console.log(` \u2705 Added ${P} token: ${w}`))}},I=[];y.fills&&(console.log(" \u{1F3A8} Processing fills variables..."),I.push(b(y.fills,"fills",r,t,"color"))),y.strokes&&(console.log(" \u{1F58A}\uFE0F Processing strokes variables..."),I.push(b(y.strokes,"strokes",r,t,"color"))),y.effects&&(console.log(" \u2728 Processing effects variables..."),I.push(b(y.effects,"effects",l,o,"effect"))),["strokeWeight","strokeTopWeight","strokeRightWeight","strokeBottomWeight","strokeLeftWeight"].forEach(A=>{y[A]&&(console.log(` \u{1F4CF} Processing ${A} variable...`),I.push(x(y[A],A,d,i,"border")))}),["topLeftRadius","topRightRadius","bottomLeftRadius","bottomRightRadius"].forEach(A=>{y[A]&&(console.log(` \u{1F504} Processing ${A} variable...`),I.push(x(y[A],A,d,i,"border")))}),["paddingTop","paddingRight","paddingBottom","paddingLeft","itemSpacing","counterAxisSpacing"].forEach(A=>{y[A]&&(console.log(` \u{1F4D0} Processing ${A} variable...`),I.push(x(y[A],A,a,n,"spacing")))}),["width","height","minWidth","maxWidth","minHeight","maxHeight"].forEach(A=>{y[A]&&(console.log(` \u{1F4E6} Processing ${A} variable...`),I.push(x(y[A],A,a,n,"size")))}),y.opacity&&(console.log(" \u{1F47B} Processing opacity variable..."),I.push(x(y.opacity,"opacity",l,o,"effect"))),f.type==="TEXT"&&["fontSize","lineHeight","letterSpacing","paragraphSpacing"].forEach($=>{y[$]&&(console.log(` \u{1F4DD} Processing ${$} variable...`),I.push(x(y[$],$,c,s,"typography")))}),await Promise.all(I),console.log(`\u{1F50D} [VARIABLES] Total variables found for ${f.name}: ${Object.keys(y).length}`)}let g="boundVariables"in f&&f.boundVariables&&f.boundVariables.fills,p="fillStyleId"in f&&f.fillStyleId;"fills"in f&&Array.isArray(f.fills)&&!p&&!g?(console.log(`\u{1F50D} [HARD-CODED] Checking fills for ${f.name} (no variables, no style)`),f.fills.forEach(y=>{if(y.type==="SOLID"&&y.visible!==!1&&y.color){let b=F(y.color.r,y.color.g,y.color.b),x=`${b}:${f.id}`;if(!r.has(x)){console.log(` \u26A0\uFE0F Found hard-coded fill: ${b}`),r.add(x);let I=ae(f);t.push({name:`hard-coded-fill-${t.length+1}`,value:b,type:"fill",isToken:!1,source:"hard-coded",context:{nodeType:f.type,nodeName:f.name,nodeId:f.id,path:I.path,description:I.description,property:"fills"}})}}})):g?console.log(`\u{1F50D} [VARIABLES] ${f.name} has fill variables - skipping hard-coded detection`):p&&console.log(`\u{1F50D} [STYLES] ${f.name} has fill style - skipping hard-coded detection`);let h="boundVariables"in f&&f.boundVariables&&f.boundVariables.strokes,C="strokeStyleId"in f&&f.strokeStyleId;if("strokes"in f&&Array.isArray(f.strokes)&&!C&&!h?(console.log(`\u{1F50D} [HARD-CODED] Checking strokes for ${f.name} (no variables, no style)`),me(f)?console.log(" \u{1F6AB} Skipping default variant frame stroke colors"):f.strokes.forEach(y=>{if(y.type==="SOLID"&&y.visible!==!1&&y.color){let b=F(y.color.r,y.color.g,y.color.b),x=`${b}:${f.id}`;if(!r.has(x)){console.log(` \u26A0\uFE0F Found hard-coded stroke: ${b}`),r.add(x);let I=ae(f);t.push({name:`hard-coded-stroke-${t.length+1}`,value:b,type:"stroke",isToken:!1,source:"hard-coded",isDefaultVariantStyle:b.toUpperCase()==="#9747FF"&&we(f),context:{nodeType:f.type,nodeName:f.name,nodeId:f.id,path:I.path,description:I.description,property:"strokes"}})}}})):h?console.log(`\u{1F50D} [VARIABLES] ${f.name} has stroke variables - skipping hard-coded detection`):C&&console.log(`\u{1F50D} [STYLES] ${f.name} has stroke style - skipping hard-coded detection`),"strokeWeight"in f&&typeof f.strokeWeight=="number"){console.log(`\u{1F50D} Node ${f.name} has strokeWeight: ${f.strokeWeight}`);let y="strokes"in f&&Array.isArray(f.strokes)&&f.strokes.length>0,b=y&&f.strokes.some(L=>L.visible!==!1),x="boundVariables"in f&&f.boundVariables&&["strokeWeight","strokeTopWeight","strokeRightWeight","strokeBottomWeight","strokeLeftWeight"].some(L=>f.boundVariables[L]),I="boundVariables"in f&&f.boundVariables?Object.keys(f.boundVariables):[];if(console.log(` Has strokes: ${y}, Has visible strokes: ${b}, Has strokeWeight variable: ${!!x}, boundVariable keys: [${I.join(", ")}]`),x)console.log(` \u{1F517} ${f.name} has strokeWeight bound to variable - skipping hard-coded detection`);else if(f.strokeWeight>0&&b&&!me(f)){let L=`${f.strokeWeight}px`,O,M=f.strokes.find(A=>A.visible!==!1&&A.type==="SOLID");M&&M.type==="SOLID"&&M.color&&(O=F(M.color.r,M.color.g,M.color.b));let z=`${L}:${f.id}`;if(!d.has(z)){console.log(` \u2705 Adding stroke weight: ${L}`),d.add(z);let A=ae(f);i.push({name:`hard-coded-stroke-weight-${f.strokeWeight}`,value:L,type:"stroke-weight",isToken:!1,source:"hard-coded",strokeColor:O,isDefaultVariantStyle:f.strokeWeight===1&&(O==null?void 0:O.toUpperCase())==="#9747FF"&&we(f),context:{nodeType:f.type,nodeName:f.name,nodeId:f.id,hasVisibleStroke:!0,path:A.path,description:A.description,property:"strokeWeight"}})}}else f.strokeWeight>0&&b&&me(f)&&console.log(" \u{1F6AB} Skipping default variant frame stroke weight")}let k="boundVariables"in f&&f.boundVariables&&["topLeftRadius","topRightRadius","bottomLeftRadius","bottomRightRadius","cornerRadius"].some(y=>f.boundVariables[y]);if("cornerRadius"in f&&typeof f.cornerRadius=="number"&&!k)if(console.log(`\u{1F50D} [HARD-CODED] Checking corner radius for ${f.name} (no variables)`),me(f))console.log(" \u{1F6AB} Skipping default variant frame corner radius");else{let y=f.cornerRadius;if(y>0){let b=`${y}px`,x=`${b}:${f.id}`;if(!d.has(x)){console.log(` \u26A0\uFE0F Found hard-coded corner radius: ${b}`),d.add(x);let I=ae(f);i.push({name:`hard-coded-corner-radius-${y}`,value:b,type:"corner-radius",isToken:!1,source:"hard-coded",isDefaultVariantStyle:y===5&&we(f),context:{nodeType:f.type,nodeName:f.name,nodeId:f.id,path:I.path,description:I.description,property:"cornerRadius"}})}}}else k&&console.log(`\u{1F50D} [VARIABLES] ${f.name} has radius variables - skipping hard-coded detection`);!k&&"topLeftRadius"in f&&(console.log(`\u{1F50D} [HARD-CODED] Checking individual corner radius for ${f.name} (no variables)`),me(f)?console.log(" \u{1F6AB} Skipping default variant frame individual corner radii"):[{prop:"topLeftRadius",name:"top-left"},{prop:"topRightRadius",name:"top-right"},{prop:"bottomLeftRadius",name:"bottom-left"},{prop:"bottomRightRadius",name:"bottom-right"}].forEach(({prop:b,name:x})=>{if(b in f&&typeof f[b]=="number"){let I=f[b];if(I>0){let L=`${I}px`,O=`${L}:${f.id}:${b}`;if(!d.has(O)){console.log(` \u26A0\uFE0F Found hard-coded ${x} radius: ${L}`),d.add(O);let M=ae(f);i.push({name:`hard-coded-${x}-radius-${I}`,value:L,type:`${x}-radius`,isToken:!1,source:"hard-coded",isDefaultVariantStyle:I===5&&we(f),context:{nodeType:f.type,nodeName:f.name,nodeId:f.id,path:M.path,description:M.description,property:b}})}}}}));let N="boundVariables"in f&&f.boundVariables&&["paddingTop","paddingRight","paddingBottom","paddingLeft"].some(y=>f.boundVariables[y]);if("paddingLeft"in f&&typeof f.paddingLeft=="number"&&!N){console.log(`\u{1F50D} [HARD-CODED] Checking padding for ${f.name} (no variables)`);let y=f;[{value:y.paddingLeft,name:"left"},{value:y.paddingRight,name:"right"},{value:y.paddingTop,name:"top"},{value:y.paddingBottom,name:"bottom"}].forEach(x=>{let I=`${x.value}:${f.id}:${x.name}`;if(typeof x.value=="number"&&x.value>1&&!a.has(I)){console.log(` \u26A0\uFE0F Found hard-coded padding-${x.name}: ${x.value}px`),a.add(I);let L=ae(f),O=x.value===16&&we(f)&&me(f);n.push({name:`hard-coded-padding-${x.name}-${x.value}`,value:`${x.value}px`,type:"padding",isToken:!1,source:"hard-coded",isDefaultVariantStyle:O,context:{nodeType:f.type,nodeName:f.name,nodeId:f.id,path:L.path,description:L.description,property:`padding${x.name.charAt(0).toUpperCase()+x.name.slice(1)}`}})}})}else N&&console.log(`\u{1F50D} [VARIABLES] ${f.name} has padding variables - skipping hard-coded detection`);if("children"in f)for(let y of f.children)await u(y)}return await u(e),Ei({colors:t,spacing:n,typography:s,effects:o,borders:i})}function Ei(e){let t=["colors","spacing","typography","effects","borders"],n={totalTokens:0,actualTokens:0,hardCodedValues:0,aiSuggestions:0,byCategory:{}};return t.forEach(s=>{let o=e[s].map(c=>j(R({},c),{isActualToken:c.source==="figma-style"||c.source==="figma-variable",recommendation:Ti(c,s),suggestion:Li(c,s)})),i=o.filter(c=>!c.isDefaultVariantStyle),r=i.filter(c=>c.isActualToken).length,a=i.filter(c=>c.source==="hard-coded").length;n.byCategory[s]={total:i.length,tokens:r,hardCoded:a,suggestions:0},n.totalTokens+=i.length,n.actualTokens+=r,n.hardCodedValues+=a,e[s]=o}),j(R({},e),{summary:n})}function Ti(e,t){if(e.isToken)return`Using ${e.name} token`;switch(t){case"colors":return`Consider using a color token instead of ${e.value}`;case"spacing":return`Consider using spacing token instead of ${e.value}`;case"typography":return"Consider using typography token";case"effects":return"Consider using effect token";case"borders":return"Consider using border radius token";default:return"Consider using a design token"}}function Li(e,t){var n,s;switch(t){case"colors":return(n=e.value)!=null&&n.startsWith("#000")?"Use semantic color token (e.g., text.primary)":(s=e.value)!=null&&s.startsWith("#FFF")?"Use semantic color token (e.g., background.primary)":"Create or use existing color token";case"spacing":let o=parseInt(e.value||"0");return o%8===0?"Create or use existing spacing token (follows 8px grid)":o%4===0?"Create or use existing spacing token (follows 4px grid)":"Create or use existing spacing token";case"typography":return"Use semantic typography token (e.g., heading.large, body.regular)";case"effects":return"Use semantic shadow token (e.g., shadow.small, shadow.medium)";case"borders":return"Use appropriate radius token (e.g., radius.small, radius.medium)";default:return"Create or use existing design token"}}function In(e){return`You are an expert design system architect analyzing a Figma component for comprehensive metadata and design token recommendations. **Component Analysis Context:** - Component Name: ${e.name} @@ -21,7 +21,7 @@ ${e.additionalContext?` ${e.existingDescription?`"${e.existingDescription}" (Build upon this if present, or create a comprehensive new description)`:"None set \u2014 create a comprehensive description from scratch"} -- Nested Component Instances: ${et(e.hierarchy).join(", ")||"None detected"} +- Nested Component Instances: ${bt(e.hierarchy).join(", ")||"None detected"} **IMPORTANT: Focus on what makes this component ready for CODE GENERATION via MCP.** Evaluate based on these criteria that actually matter for development: @@ -197,29 +197,29 @@ For the "recommendedProperties" field, compare the component's EXISTING properti - Effects: \`[effect]-[intensity]-[purpose]\` (e.g., "shadow-md-default", "blur-backdrop-light") - Borders: \`radius-[size]-[value]\` (e.g., "radius-md-8px", "radius-full-999px") -Focus on creating a comprehensive DESIGN analysis that helps designers build scalable, consistent, and well-structured Figma components.`}function pe(e){try{console.log("\u{1F50D} Starting JSON extraction from LLM response..."),console.log("\u{1F4DD} Response length:",e.length),console.log("\u{1F4DD} Response preview (first 200 chars):",e.substring(0,200));try{let n=JSON.parse(e.trim());return console.log("\u2705 Successfully parsed entire response as JSON"),n}catch(n){console.log("\u26A0\uFE0F Full response is not valid JSON, trying to extract JSON block...")}let t=[()=>sn(e),()=>_o(e),()=>Bo(e),()=>Uo(e)];for(let n=0;n0;)o+=`} -`,r--;let c=JSON.parse(o.trim());return console.log("\u2705 Successfully reconstructed truncated JSON"),c}catch(n){return console.log("\u26A0\uFE0F Failed to reconstruct truncated JSON:",n instanceof Error?n.message:"Unknown error"),Vo(e)}}function Vo(e){try{console.log("\u{1F504} Attempting to extract basic component info as fallback...");let t=e.match(/"component":\s*"([^"]+)"/),n=e.match(/"description":\s*"([^"]+)"/);if(t&&n){let s={component:t[1],description:n[1],props:[],states:["default"],variants:{},tokens:{colors:[],spacing:[],typography:[]},audit:{tokenOpportunities:["Review and simplify component analysis"]},mcpReadiness:{score:60,strengths:["Component has basic structure"],gaps:["Analysis was incomplete due to response size"],recommendations:["Simplify component structure","Use MCP-enhanced analysis for better results"]},propertyCheatSheet:[]};return console.log("\u2705 Extracted basic component info as fallback"),s}return null}catch(t){return console.log("\u26A0\uFE0F Failed to extract basic component info:",t instanceof Error?t.message:"Unknown error"),null}}function _o(e){let t=[["```json","```"],["```","```"],["JSON:",` +Focus on creating a comprehensive DESIGN analysis that helps designers build scalable, consistent, and well-structured Figma components.`}function ye(e){try{console.log("\u{1F50D} Starting JSON extraction from LLM response..."),console.log("\u{1F4DD} Response length:",e.length),console.log("\u{1F4DD} Response preview (first 200 chars):",e.substring(0,200));try{let n=JSON.parse(e.trim());return console.log("\u2705 Successfully parsed entire response as JSON"),n}catch(n){console.log("\u26A0\uFE0F Full response is not valid JSON, trying to extract JSON block...")}let t=[()=>Cn(e),()=>$i(e),()=>Mi(e),()=>Oi(e)];for(let n=0;n0;)o+=`} +`,i--;let c=JSON.parse(o.trim());return console.log("\u2705 Successfully reconstructed truncated JSON"),c}catch(n){return console.log("\u26A0\uFE0F Failed to reconstruct truncated JSON:",n instanceof Error?n.message:"Unknown error"),Ri(e)}}function Ri(e){try{console.log("\u{1F504} Attempting to extract basic component info as fallback...");let t=e.match(/"component":\s*"([^"]+)"/),n=e.match(/"description":\s*"([^"]+)"/);if(t&&n){let s={component:t[1],description:n[1],props:[],states:["default"],variants:{},tokens:{colors:[],spacing:[],typography:[]},audit:{tokenOpportunities:["Review and simplify component analysis"]},mcpReadiness:{score:60,strengths:["Component has basic structure"],gaps:["Analysis was incomplete due to response size"],recommendations:["Simplify component structure","Use MCP-enhanced analysis for better results"]},propertyCheatSheet:[]};return console.log("\u2705 Extracted basic component info as fallback"),s}return null}catch(t){return console.log("\u26A0\uFE0F Failed to extract basic component info:",t instanceof Error?t.message:"Unknown error"),null}}function $i(e){let t=[["```json","```"],["```","```"],["JSON:",` `],["Response:",` `],["{",`} -`]];for(let[n,s]of t){let o=e.indexOf(n);if(o===-1)continue;let r=o+n.length,i=e.indexOf(s,r);if(i===-1&&s===` +`]];for(let[n,s]of t){let o=e.indexOf(n);if(o===-1)continue;let i=o+n.length,r=e.indexOf(s,i);if(r===-1&&s===` -`&&(i=e.length),i===-1)continue;let a=e.substring(r,i).trim();try{return JSON.parse(a)}catch(c){if(a.startsWith("{"))try{return sn(a)}catch(d){continue}}}return null}function Bo(e){let t=/```(?:json)?\s*(\{[\s\S]*?\})\s*```/gi,n;for(;(n=t.exec(e))!==null;)try{return JSON.parse(n[1])}catch(s){continue}return null}function Uo(e){let t=e.match(/\{[\s\S]*\}/);return t?JSON.parse(t[0]):null}function Fe(e){if(!e||typeof e!="object")return e;let t=["aria","accessibility api","semantic html","keyboard navigation","event handler","interactive behavior","onclick","onchange","state management","controlled component","uncontrolled component","props","responsive breakpoint","css implementation","@media","animation token","transition timing","programmatic animation","keyframe","api integration","data binding","dynamic content","fetch","axios","implement","add handler","bind event","attach listener","programming pattern","functional pattern","react hook","usestate","useeffect"],n=r=>{let i=r.toLowerCase();return t.some(a=>i.includes(a))},s=r=>Array.isArray(r)?r.filter(i=>{if(typeof i=="string"){let a=!n(i);return a||console.log("\u{1F6AB} [FILTER] Removed development-focused recommendation:",i),a}return!0}):r,o=JSON.parse(JSON.stringify(e));return o.mcpReadiness&&(o.mcpReadiness.recommendations&&(o.mcpReadiness.recommendations=s(o.mcpReadiness.recommendations)),o.mcpReadiness.gaps&&(o.mcpReadiness.gaps=s(o.mcpReadiness.gaps))),o.audit&&(o.audit.tokenOpportunities&&(o.audit.tokenOpportunities=s(o.audit.tokenOpportunities)),o.audit.structureIssues&&(o.audit.structureIssues=s(o.audit.structureIssues))),o.accessibility&&(o.accessibility.designConsiderations&&(o.accessibility.designConsiderations=s(o.accessibility.designConsiderations)),o.accessibility.visualIndicators&&(o.accessibility.visualIndicators=s(o.accessibility.visualIndicators))),o}var E=class extends Error{constructor(n,s,o,r){super(n);this.code=s;this.statusCode=o;this.retryAfter=r;this.name="LLMError"}};var tt={anthropic:"claude-sonnet-4-5-20250929",openai:"gpt-5.2",google:"gemini-2.5-pro"};var Go=[{id:"claude-opus-4-5-20251218",name:"Claude Opus 4.5",description:"Flagship model - Most capable, best for complex analysis and reasoning",contextWindow:2e5,isDefault:!1},{id:"claude-sonnet-4-5-20250929",name:"Claude Sonnet 4.5",description:"Standard model - Balanced performance and cost, recommended for most tasks",contextWindow:2e5,isDefault:!0},{id:"claude-haiku-4-5-20251001",name:"Claude Haiku 4.5",description:"Economy model - Fastest responses, ideal for quick analysis",contextWindow:2e5,isDefault:!1}],nt=class{constructor(){this.name="Anthropic";this.id="anthropic";this.endpoint="https://api.anthropic.com/v1/messages";this.keyPrefix="sk-ant-";this.keyPlaceholder="sk-ant-...";this.models=Go}formatRequest(t){let n={model:t.model,messages:[{role:"user",content:t.prompt.trim()}],max_tokens:t.maxTokens};return t.temperature!==void 0&&(n.temperature=t.temperature),t.additionalParams&&Object.assign(n,t.additionalParams),n}parseResponse(t){let n=t;if(!n.content||!Array.isArray(n.content))throw new E("Invalid response format from Anthropic API: missing content array","INVALID_REQUEST");let s=n.content.filter(o=>o.type==="text").map(o=>o.text).join(` -`);if(!s)throw new E("Invalid response format from Anthropic API: no text content found","INVALID_REQUEST");return{content:s.trim(),model:n.model,usage:n.usage?{promptTokens:n.usage.input_tokens,completionTokens:n.usage.output_tokens,totalTokens:n.usage.input_tokens+n.usage.output_tokens}:void 0,metadata:{id:n.id,stopReason:n.stop_reason}}}validateApiKey(t){if(!t||typeof t!="string")return{isValid:!1,error:"API Key Required: Please provide a valid Claude API key."};let n=t.trim();return n.length===0?{isValid:!1,error:"API Key Required: The Claude API key cannot be empty."}:n.startsWith(this.keyPrefix)?n.length<40?{isValid:!1,error:"Invalid API Key Format: The API key appears to be too short. Please verify you copied the complete key."}:{isValid:!0}:{isValid:!1,error:`Invalid API Key Format: Claude API keys should start with "${this.keyPrefix}". Please check your API key.`}}getHeaders(t){return{"content-type":"application/json","x-api-key":t.trim(),"anthropic-version":"2023-06-01","anthropic-dangerous-direct-browser-access":"true"}}getDefaultModel(){return this.models.find(n=>n.isDefault)||this.models[1]}handleError(t,n){var r;let s=n,o=((r=s==null?void 0:s.error)==null?void 0:r.message)||(typeof n=="string"?n:"Unknown error");switch(t){case 400:return new E(`Claude API Error (400): ${o}. Please check your request format.`,"INVALID_REQUEST",400);case 401:return new E("Claude API Error (401): Invalid API key. Please check your Claude API key in settings.","INVALID_API_KEY",401);case 403:return new E("Claude API Error (403): Access forbidden. Please check your API key permissions.","INVALID_API_KEY",403);case 404:return new E(`Claude API Error (404): ${o}. The requested model may not be available.`,"MODEL_NOT_FOUND",404);case 429:return new E("Claude API Error (429): Rate limit exceeded. Please try again later.","RATE_LIMIT_EXCEEDED",429);case 500:return new E("Claude API Error (500): Server error. The Claude API is experiencing issues. Please try again later.","SERVER_ERROR",500);case 503:return new E("Claude API Error (503): Service unavailable. The Claude API is temporarily down. Please try again later.","SERVICE_UNAVAILABLE",503);default:return new E(`Claude API Error (${t}): ${o}`,"UNKNOWN_ERROR",t)}}},ot=new nt;var zo=[{id:"gpt-5.2",name:"GPT-5.2",description:"Flagship model with advanced reasoning capabilities",contextWindow:128e3,isDefault:!0},{id:"gpt-5.2-pro",name:"GPT-5.2 Pro",description:"Premium model with extended reasoning for complex tasks",contextWindow:128e3,isDefault:!1},{id:"gpt-5-mini",name:"GPT-5 Mini",description:"Economy model - fast and cost-effective",contextWindow:128e3,isDefault:!1}],rt=class{constructor(){this.name="OpenAI";this.id="openai";this.endpoint="https://api.openai.com/v1/chat/completions";this.keyPrefix="sk-";this.keyPlaceholder="sk-...";this.models=zo}formatRequest(t){let n={model:t.model,messages:[{role:"user",content:t.prompt}],max_completion_tokens:t.maxTokens,temperature:t.temperature};return t.additionalParams&&Object.assign(n,t.additionalParams),n}parseResponse(t){let n=t;if(!n.choices||n.choices.length===0)throw new E("Invalid response format: no choices returned","INVALID_REQUEST");let s=n.choices[0];if(!s.message||typeof s.message.content!="string")throw new E("Invalid response format: missing message content","INVALID_REQUEST");let o={content:s.message.content.trim(),model:n.model};return n.usage&&(o.usage={promptTokens:n.usage.prompt_tokens,completionTokens:n.usage.completion_tokens,totalTokens:n.usage.total_tokens}),o.metadata={id:n.id,finishReason:s.finish_reason,created:n.created},o}validateApiKey(t){if(!t||typeof t!="string")return{isValid:!1,error:"API Key Required: Please provide a valid OpenAI API key."};let n=t.trim();return n.length===0?{isValid:!1,error:"API Key Required: The OpenAI API key cannot be empty."}:n.startsWith(this.keyPrefix)?n.length<20?{isValid:!1,error:"Invalid API Key Format: The API key appears to be too short. Please verify you copied the complete key."}:{isValid:!0}:{isValid:!1,error:`Invalid API Key Format: OpenAI API keys should start with "${this.keyPrefix}". Please check your API key.`}}getHeaders(t){return{"Content-Type":"application/json",Authorization:`Bearer ${t.trim()}`}}getDefaultModel(){return this.models.find(n=>n.isDefault)||this.models[0]}handleError(t,n){var r;let s=n,o=((r=s==null?void 0:s.error)==null?void 0:r.message)||(s==null?void 0:s.message)||"Unknown error occurred";switch(t){case 400:return o.toLowerCase().includes("context_length_exceeded")||o.toLowerCase().includes("maximum context length")?new E(`OpenAI API Error (400): Context length exceeded. ${o}`,"CONTEXT_LENGTH_EXCEEDED",t):new E(`OpenAI API Error (400): ${o}. Please check your request format.`,"INVALID_REQUEST",t);case 401:return new E("OpenAI API Error (401): Invalid API key. Please check your OpenAI API key in settings.","INVALID_API_KEY",t);case 403:return new E("OpenAI API Error (403): Access forbidden. Please check your API key permissions or account status.","INVALID_API_KEY",t);case 404:return new E(`OpenAI API Error (404): Model not found. ${o}`,"MODEL_NOT_FOUND",t);case 429:let i=o.match(/try again in (\d+)/i),a=i?parseInt(i[1],10):void 0;return new E(`OpenAI API Error (429): Rate limit exceeded. ${a?`Please try again in ${a} seconds.`:"Please try again later."}`,"RATE_LIMIT_EXCEEDED",t,a);case 500:return new E("OpenAI API Error (500): Server error. The OpenAI API is experiencing issues. Please try again later.","SERVER_ERROR",t);case 502:return new E("OpenAI API Error (502): Bad gateway. The OpenAI API is temporarily unavailable. Please try again later.","SERVICE_UNAVAILABLE",t);case 503:return new E("OpenAI API Error (503): Service unavailable. The OpenAI API is temporarily down. Please try again later.","SERVICE_UNAVAILABLE",t);case 504:return new E("OpenAI API Error (504): Gateway timeout. The request took too long. Please try again.","SERVICE_UNAVAILABLE",t);default:return new E(`OpenAI API Error (${t}): ${o}`,"UNKNOWN_ERROR",t)}}},it=new rt;var Wo=[{id:"gemini-3-pro-preview",name:"Gemini 3 Pro",description:"Flagship model with advanced reasoning and multimodal capabilities",contextWindow:1e6,isDefault:!0},{id:"gemini-2.5-pro",name:"Gemini 2.5 Pro",description:"Standard reasoning model with excellent performance",contextWindow:1e6,isDefault:!1},{id:"gemini-2.5-flash",name:"Gemini 2.5 Flash",description:"Economy model optimized for speed and efficiency",contextWindow:1e6,isDefault:!1}],at=class{constructor(){this.name="Google";this.id="google";this.endpoint="https://generativelanguage.googleapis.com/v1beta/models";this.keyPrefix="AIza";this.keyPlaceholder="AIza...";this.models=Wo}formatRequest(t){let n={contents:[{parts:[{text:t.prompt}]}],generationConfig:{maxOutputTokens:t.maxTokens,temperature:t.temperature}};if(t.additionalParams){let{topP:s,topK:o,stopSequences:r}=t.additionalParams;s!==void 0&&(n.generationConfig.topP=s),o!==void 0&&(n.generationConfig.topK=o),r!==void 0&&(n.generationConfig.stopSequences=r)}return n}parseResponse(t){var c;let n=t;if(n.error)throw new E(n.error.message||"Unknown Gemini API error",this.mapErrorCodeToLLMErrorCode(n.error.code,n.error.status),n.error.code);if(!n.candidates||n.candidates.length===0){let d=Object.keys(n);throw new E(`No candidates in Gemini response. Response keys: [${d.join(", ")}]${n.error?`. Error: ${n.error.message}`:""}`,"INVALID_REQUEST")}let s=n.candidates[0];if(s.finishReason==="SAFETY")throw new E("Gemini response blocked by safety filters. Try rephrasing the prompt.","INVALID_REQUEST");let o=(c=s.content)==null?void 0:c.parts;if(!o||o.length===0)throw new E(`No content parts in Gemini response. Finish reason: ${s.finishReason||"unknown"}. Has content: ${!!s.content}`,"INVALID_REQUEST");let r=o.find(d=>typeof d.text=="string");if(!r||!r.text){let d=o.map(l=>Object.keys(l).join(",")).join("; ");throw new E(`No text content in Gemini response parts. Part types: [${d}]. Finish reason: ${s.finishReason||"unknown"}`,"INVALID_REQUEST")}let a={content:r.text,model:"gemini"};return n.usageMetadata&&(a.usage={promptTokens:n.usageMetadata.promptTokenCount||0,completionTokens:n.usageMetadata.candidatesTokenCount||0,totalTokens:n.usageMetadata.totalTokenCount||0}),a}validateApiKey(t){if(!t||typeof t!="string")return{isValid:!1,error:"API key is required"};let n=t.trim();return n.length===0?{isValid:!1,error:"API key cannot be empty"}:n.startsWith(this.keyPrefix)?n.length<30||n.length>50?{isValid:!1,error:"API key appears to have an invalid length. Please verify you copied the complete key."}:/^[A-Za-z0-9_-]+$/.test(n)?{isValid:!0}:{isValid:!1,error:"API key contains invalid characters"}:{isValid:!1,error:`Google API keys should start with "${this.keyPrefix}". Please check your API key.`}}getHeaders(t){return{"Content-Type":"application/json"}}getEndpoint(t,n){let s=n.trim();return`${this.endpoint}/${t}:generateContent?key=${s}`}getDefaultModel(){return this.models.find(n=>n.isDefault)||this.models[0]}handleError(t,n){var p,u;let s=n,o=s==null?void 0:s.error,r=(o==null?void 0:o.message)||"Unknown Google API error",i=o==null?void 0:o.status,a=(o==null?void 0:o.code)||t,c=this.mapErrorCodeToLLMErrorCode(a,i),d;if(t===429){d=6e4;let g=(p=o==null?void 0:o.details)==null?void 0:p.find(f=>{var m;return(m=f["@type"])==null?void 0:m.includes("RetryInfo")});if((u=g==null?void 0:g.metadata)!=null&&u.retryDelay){let f=g.metadata.retryDelay.match(/(\d+)s/);f&&(d=parseInt(f[1],10)*1e3)}}let l=r;switch(c){case"INVALID_API_KEY":l="Google API Error: Invalid API key. Please check your API key in settings.";break;case"RATE_LIMIT_EXCEEDED":l=`Google API Error: Rate limit exceeded. ${d?`Please try again in ${Math.ceil(d/1e3)} seconds.`:"Please try again later."}`;break;case"MODEL_NOT_FOUND":l="Google API Error: Model not found. Please select a valid model.";break;case"CONTEXT_LENGTH_EXCEEDED":l="Google API Error: Input too long. Please reduce the size of your request.";break;case"SERVER_ERROR":l="Google API Error: Server error. Please try again later.";break;case"SERVICE_UNAVAILABLE":l="Google API Error: Service temporarily unavailable. Please try again later.";break}return new E(l,c,t,d)}mapErrorCodeToLLMErrorCode(t,n){if(n){let s=n.toUpperCase();if(s==="INVALID_ARGUMENT")return"INVALID_REQUEST";if(s==="PERMISSION_DENIED"||s==="UNAUTHENTICATED")return"INVALID_API_KEY";if(s==="NOT_FOUND")return"MODEL_NOT_FOUND";if(s==="RESOURCE_EXHAUSTED")return"RATE_LIMIT_EXCEEDED";if(s==="UNAVAILABLE")return"SERVICE_UNAVAILABLE"}switch(t){case 400:return"INVALID_REQUEST";case 401:case 403:return"INVALID_API_KEY";case 404:return"MODEL_NOT_FOUND";case 429:return"RATE_LIMIT_EXCEEDED";case 500:return"SERVER_ERROR";case 503:return"SERVICE_UNAVAILABLE";default:return"UNKNOWN_ERROR"}}},ct=new at;var Ho={anthropic:ot,openai:it,google:ct};function ie(e){let t=Ho[e];if(!t)throw new E(`Unknown provider: ${e}`,"INVALID_REQUEST",400);return t}async function ae(e,t,n){var c,d;let s=ie(e),o=s.validateApiKey(t);if(!o.isValid)throw new E(o.error||"Invalid API key format","INVALID_API_KEY",401);let r=s.formatRequest(n),i=s.getHeaders(t),a=s.endpoint;e==="google"&&(a=`${s.endpoint}/${n.model}:generateContent?key=${t.trim()}`);try{console.log(`Making ${s.name} API call to ${a}...`);let l=await fetch(a,{method:"POST",headers:i,body:JSON.stringify(r)});if(!l.ok){let u;try{u=await l.json()}catch(g){u=await l.text()}throw s.handleError(l.status,u)}let p=await l.json();return console.log(`${s.name} API response status: ${l.status}`),console.log(`${s.name} API response keys:`,Object.keys(p)),e==="google"&&(console.log("Gemini response candidates:",p.candidates?p.candidates.length:"none"),(c=p.candidates)!=null&&c[0]&&(console.log("Gemini candidate[0] keys:",Object.keys(p.candidates[0])),p.candidates[0].content&&console.log("Gemini content parts:",((d=p.candidates[0].content.parts)==null?void 0:d.length)||"none")),p.error&&console.log("Gemini error:",JSON.stringify(p.error))),s.parseResponse(p)}catch(l){throw l instanceof E?l:l instanceof Error&&(l.message.includes("Failed to fetch")||l.message.includes("NetworkError"))?new E(`Network error connecting to ${s.name}. Please check your internet connection.`,"NETWORK_ERROR"):new E(`Unexpected error calling ${s.name}: ${l instanceof Error?l.message:"Unknown error"}`,"UNKNOWN_ERROR")}}var W={SELECTED_PROVIDER:"selected-provider",SELECTED_MODEL:"selected-model",apiKey:e=>`${e}-api-key`,LEGACY_CLAUDE_KEY:"claude-api-key",LEGACY_CLAUDE_MODEL:"claude-model"},Ko={provider:"anthropic",model:tt.anthropic};async function jo(){try{let e=await figma.clientStorage.getAsync(W.LEGACY_CLAUDE_KEY),t=await figma.clientStorage.getAsync(W.LEGACY_CLAUDE_MODEL);return e?{needsMigration:!0,legacyKey:e,legacyModel:t}:{needsMigration:!1}}catch(e){return{needsMigration:!1}}}async function lt(){let e=await jo();e.needsMigration&&(console.log("Migrating legacy Claude storage to multi-provider format..."),e.legacyKey&&await figma.clientStorage.setAsync(W.apiKey("anthropic"),e.legacyKey),await figma.clientStorage.setAsync(W.SELECTED_PROVIDER,"anthropic"),e.legacyModel&&await figma.clientStorage.setAsync(W.SELECTED_MODEL,e.legacyModel),await figma.clientStorage.deleteAsync(W.LEGACY_CLAUDE_KEY),await figma.clientStorage.deleteAsync(W.LEGACY_CLAUDE_MODEL),console.log("Migration complete"))}async function dt(){await lt();let e=await figma.clientStorage.getAsync(W.SELECTED_PROVIDER)||Ko.provider,t=await figma.clientStorage.getAsync(W.SELECTED_MODEL)||tt[e],n=await figma.clientStorage.getAsync(W.apiKey(e));return{providerId:e,modelId:t,apiKey:n}}async function ut(e,t,n){await figma.clientStorage.setAsync(W.SELECTED_PROVIDER,e),await figma.clientStorage.setAsync(W.SELECTED_MODEL,t),n!==void 0&&await figma.clientStorage.setAsync(W.apiKey(e),n)}async function on(e){await figma.clientStorage.deleteAsync(W.apiKey(e))}var rn=/^(Frame|Rectangle|Ellipse|Group|Vector|Line|Polygon|Star|Text|Component|Instance|Slice|Boolean|Union|Subtract|Intersect|Exclude)\s*\d*$/i,an=/\s+\d+$/,cn={button:"btn",icon:"ico",input:"input",text:"txt",image:"img",container:"container",card:"card",list:"list","list-item":"list-item",nav:"nav",header:"header",footer:"footer",modal:"modal",dropdown:"dropdown",checkbox:"checkbox",radio:"radio",toggle:"toggle",avatar:"avatar",badge:"badge",divider:"divider",spacer:"spacer",link:"link",tab:"tab",tooltip:"tooltip",alert:"alert",progress:"progress",skeleton:"skeleton",unknown:"layer"},me=[["btn","button"],["button","button"],["cta","button"],["submit","button"],["icon","icon"],["ico","icon"],["glyph","icon"],["symbol","icon"],["arrow","icon"],["chevron","icon"],["close","icon"],["plus","icon"],["minus","icon"],["txt","text"],["label","text"],["title","text"],["heading","text"],["paragraph","text"],["description","text"],["caption","text"],["subtitle","text"],["input","input"],["field","input"],["textfield","input"],["textarea","input"],["searchfield","input"],["searchbox","input"],["image","image"],["img","image"],["photo","image"],["picture","image"],["thumbnail","image"],["cover","image"],["container","container"],["wrapper","container"],["content","container"],["section","container"],["block","container"],["box","container"],["card","card"],["tile","card"],["panel","card"],["list","list"],["items","list"],["item","list-item"],["row","list-item"],["listitem","list-item"],["nav","nav"],["navbar","nav"],["navigation","nav"],["sidebar","nav"],["breadcrumb","nav"],["menu","nav"],["header","header"],["topbar","header"],["footer","footer"],["bottombar","footer"],["modal","modal"],["dialog","modal"],["popup","modal"],["overlay","modal"],["dropdown","dropdown"],["select","dropdown"],["picker","dropdown"],["combobox","dropdown"],["checkbox","checkbox"],["checkmark","checkbox"],["radio","radio"],["toggle","toggle"],["switch","toggle"],["avatar","avatar"],["profile","avatar"],["userpic","avatar"],["badge","badge"],["tag","badge"],["chip","badge"],["pill","badge"],["status","badge"],["divider","divider"],["separator","divider"],["hr","divider"],["spacer","spacer"],["gap","spacer"],["link","link"],["anchor","link"],["href","link"],["tab","tab"],["tabs","tab"],["tabbar","tab"],["tooltip","tooltip"],["hint","tooltip"],["popover","tooltip"],["alert","alert"],["notification","alert"],["toast","alert"],["message","alert"],["snackbar","alert"],["banner","alert"],["progress","progress"],["loader","progress"],["loading","progress"],["spinner","progress"],["progressbar","progress"],["skeleton","skeleton"],["placeholder","skeleton"],["shimmer","skeleton"]];function ln(e){if(!e||typeof e!="string")return!0;let t=e.trim();return!!(rn.test(t)||t.length===1||/^\d+$/.test(t))}function qo(e){return an.test(e.trim())}function De(e){let t=e.name.toLowerCase();for(let n=0;n20||n<=2&&s>20)return"divider";if(n<=32&&s<=32&&o>.5&&o<2)return"spacer"}return"unknown";case"FRAME":case"GROUP":return dn(e);case"COMPONENT":case"INSTANCE":return un(e);case"COMPONENT_SET":return Jo(e);default:return"unknown"}}function dn(e){if(!("children"in e)||e.children.length===0)return"container";let t=e.children,n=[],s=[];for(let c=0;c=2)return"card";if(t.length>=3){let c=t[0].type,d=!0;for(let l=1;l=3&&i)return"nav"}return"container"}function un(e){let t=e.name.toLowerCase();for(let n=0;n0?un(e.children[0]):"unknown"}function pn(e,t=10){let n=[];function s(o,r,i){if(r>t)return;let a=i?`${i} > ${o.name}`:o.name,c=De(o);if(ln(o.name)){let d=fe(o);n.push({nodeId:o.id,nodeName:o.name,currentName:o.name,suggestedName:d,severity:"error",reason:"Generic layer name detected",layerType:c,depth:r,path:a})}else if(qo(o.name)){let d=o.name.replace(an,"").trim(),l=fe(o);n.push({nodeId:o.id,nodeName:o.name,currentName:o.name,suggestedName:l!==o.name?l:d,severity:"warning",reason:"Layer name has numbered suffix (possible duplicate)",layerType:c,depth:r,path:a})}if("children"in o)for(let d=0;d0?Qo(e):cn[t]||"layer"}function Xo(e){let n=e.name.toLowerCase().replace(rn,"").replace(/[_\-\s]+/g,"-").replace(/^-|-$/g,"").trim();if(n&&n.length>1)return`icon-${X(n)}`;if("children"in e&&e.children.length>0){let s=[];for(let o=0;o1.5||s<.67)return"icon-arrow"}return"icon"}function Yo(e){let n=(e.characters||"").trim();if(!n)return"text-empty";let s=n.split(/\s+/);if(s.length<=2&&n.length<=30){let g=X(n);return g?`text-${g}`:"text-content"}let o=s[0].toLowerCase(),r=["welcome","about","contact","services","features","pricing"],i=["name","email","password","username","address","phone"],a=["submit","cancel","save","delete","edit","add","remove","ok","yes","no"],c=["learn","read","view","see","click","here","more"],d=["error","invalid","required","failed","wrong"],l=["success","done","complete","saved","updated"],p=n.toLowerCase();for(let g=0;g0){let s;for(let o=0;o0&&r[0].length>0)return`${n}-${X(r.join(" "))}`}if(t==="button"||t==="input"){let o;for(let r=0;rn.includes(a)),r=await yi(e),i=o||r;return console.log(`\u{1F50D} [CONTAINER DETECTION] ${e.name}:`),console.log(` Name-based: ${o}`),console.log(` Structure-based: ${r}`),console.log(` Final result: ${i}`),n.includes("avatar")||n.includes("profile")?(t.componentFamily="avatar",t.possibleUseCase="User representation, often clickable for profile access or dropdown menus",t.hasInteractiveElements=!0,t.suggestedConsiderations.push("Consider if this avatar will be clickable/interactive"),t.suggestedConsiderations.push("May need hover/focus states for navigation"),t.designPatterns.push("profile-navigation","user-menu-trigger")):n.includes("button")||n.includes("btn")?(t.componentFamily="button",t.possibleUseCase="Interactive element for user actions",t.hasInteractiveElements=!0,t.suggestedConsiderations.push("Requires all interactive states"),t.designPatterns.push("action-trigger","form-submission")):n.includes("badge")||n.includes("tag")?(t.componentFamily="badge",t.possibleUseCase="Status indicator or label",t.hasInteractiveElements=!1,t.suggestedConsiderations.push("Typically non-interactive unless used as a filter"),t.designPatterns.push("status-indicator","category-label")):n.includes("input")||n.includes("field")?(t.componentFamily="input",t.possibleUseCase="Form input element",t.hasInteractiveElements=!0,t.suggestedConsiderations.push("Needs focus, error, and disabled states"),t.designPatterns.push("form-control","data-entry")):n.includes("card")?(t.componentFamily="card",t.possibleUseCase="Content container",t.hasInteractiveElements=n.includes("clickable")||n.includes("interactive"),t.suggestedConsiderations.push("May be interactive if used for navigation"),t.designPatterns.push("content-container","information-display")):n.includes("icon")?(t.componentFamily="icon",t.possibleUseCase="Visual indicator or decoration",t.hasInteractiveElements=!1,t.suggestedConsiderations.push("Usually decorative, but may be interactive if part of a button"),t.designPatterns.push("visual-indicator","decoration")):i&&(t.componentFamily="container",t.possibleUseCase="Layout container for organizing child components",t.hasInteractiveElements=!1,t.suggestedConsiderations.push("Focus on layout and organization rather than interaction states"),t.suggestedConsiderations.push("Child components handle individual interactions"),t.designPatterns.push("layout-container","component-organization")),"children"in e&&e.findAll(c=>c.type==="TEXT"&&(c.name.toLowerCase().includes("click")||c.name.toLowerCase().includes("action")||c.name.toLowerCase().includes("link"))).length>0&&(t.hasInteractiveElements=!0),e.parent&&e.parent.name.toLowerCase().includes("button")&&(t.hasInteractiveElements=!0,t.suggestedConsiderations.push("Part of a button component - needs interactive states")),t}async function yi(e){if(!("children"in e)||!e.children||e.children.length===0)return!1;let t=e.children.filter(d=>d.type==="INSTANCE");if(t.length===0)return console.log(`\u{1F50D} [STRUCTURE] No child instances found in ${e.name}`),!1;console.log(`\u{1F50D} [STRUCTURE] Analyzing ${e.name} with ${t.length} child instances`);let n=new Map;await Promise.all(t.map(async d=>{try{let l=await d.getMainComponentAsync();if(l){let p=l.name;n.has(p)||n.set(p,[]),n.get(p).push(d)}}catch(l){console.log("\u26A0\uFE0F [STRUCTURE] Could not access main component for instance:",l)}})),console.log("\u{1F50D} [STRUCTURE] Instance groups:",Array.from(n.entries()).map(([d,l])=>`${d}: ${l.length}`));let s=Array.from(n.values()).some(d=>d.length>1),o=Array.from(n.keys()).some(d=>{let l=d.toLowerCase();return l.includes("item")||l.includes("panel")||l.includes("content")||l.includes("section")||l.includes("group")||l.includes("wrapper")||l.includes("tab")&&!l.includes("button")||l.includes("nav-item")||l.includes("menu-item")||l.includes("list-item")||l.includes("card-item")}),r=t.length/e.children.length,i=r>.6,a=n.size>=2&&s;return console.log(`\u{1F50D} [STRUCTURE] Analysis for ${e.name}:`),console.log(` Repeated components: ${s}`),console.log(` Organizational components: ${o}`),console.log(` Instance ratio: ${r.toFixed(2)} (${i?"high":"low"})`),console.log(` Collection pattern: ${a}`),s||o||i&&n.size>=2}function Ps(e,t=0){let n=[],s={name:e.name,type:e.type,depth:t};if("children"in e&&e.children.length>0){s.children=[];for(let o of e.children)s.children.push(...Ps(o,t+1))}return n.push(s),n}function hi(e){let t=[];function n(s){for(let o of s)t.push(o.name),o.children&&n(o.children)}return n(e),t}function et(e){let t=new Set;function n(s){for(let o of s)o.type==="INSTANCE"&&t.add(o.name),o.children&&n(o.children)}return n(e),Array.from(t)}function bi(e){let t=[],n=!1;if(e.type==="COMPONENT_SET"){n=!0;try{let s=e,o;try{o=s.variantGroupProperties}catch(r){console.warn("Component set has errors, cannot access variantGroupProperties:",r),o=void 0}o&&t.push(...Object.keys(o))}catch(s){console.warn("Error analyzing component set:",s)}}else{let s=Oe(e).map(r=>r.name.toLowerCase());["primary","secondary","tertiary","small","medium","large","xl","xs","default","hover","focus","active","disabled","filled","outline","ghost","link","light","dark"].forEach(r=>{s.some(i=>i.includes(r))&&(t.includes(r)||t.push(r))})}return{isComponentSet:n,potentialVariants:t}}function vi(e){let t=[],n=Oe(e),s=e.name.toLowerCase(),o=["radiobutton","checkbox","icon","button","input","focusring","focus","indicator","background","border","outline","shadow","ring","control","handle","thumb","track","progress","slider","arrow","chevron","close","minimize","maximize"];n.filter(c=>c.type==="TEXT").forEach(c=>{let d=c.name.toLowerCase();o.some(l=>d.includes(l))||s.includes(d)||d.includes(s.split(" ")[0])||(d.includes("title")||d.includes("label")||d.includes("text")||d.includes("content"))&&d.length>2&&t.push(c.name)}),n.filter(c=>c.type==="FRAME").forEach(c=>{let d=c.name.toLowerCase();o.some(l=>d.includes(l))||(d.includes("content")&&!d.includes("background")||d.includes("slot")||d.includes("container")&&!d.includes("main"))&&t.push(c.name)});let a=[...new Set(t)].filter(c=>{let d=c.toLowerCase();return d.length>2&&!["text","label","content"].includes(d)&&!o.some(l=>d.includes(l))});return console.log(`\u{1F50D} [SLOTS] Detected ${a.length} legitimate content slots from ${t.length} candidates:`,a),a}function Ls(e){return"fills"in e&&Array.isArray(e.fills)&&e.fills.length>0?e.fills.some(t=>t.visible!==!1):"children"in e?e.children.some(t=>Ls(t)):!1}function Rs(e){return"strokes"in e&&Array.isArray(e.strokes)&&e.strokes.length>0?e.strokes.some(t=>t.visible!==!1):"children"in e?e.children.some(t=>Rs(t)):!1}function $s(e){return"effects"in e&&Array.isArray(e.effects)&&e.effects.length>0?e.effects.some(t=>t.visible!==!1):"children"in e?e.children.some(t=>$s(t)):!1}function Si(e){let t=new Map;e.children.forEach(s=>{s.type==="COMPONENT"&&s.name.split(",").map(i=>i.trim()).forEach(i=>{let[a,c]=i.split("=").map(d=>d.trim());a&&c&&(t.has(a)||t.set(a,new Set),t.get(a).add(c))})});let n=[];return t.forEach((s,o)=>{let r=Array.from(s);n.push({name:o,values:r,default:r[0]||"default"})}),n}async function Ms(e,t){let n=[];if(console.log("\u{1F50D} [DEBUG] Starting property extraction for node:",e.name,"type:",e.type),console.log("\u{1F50D} [DEBUG] Originally selected node:",t==null?void 0:t.name,"type:",t==null?void 0:t.type),t&&t.type==="INSTANCE"){let o=t;console.log("\u{1F50D} [DEBUG] Extracting from selected instance componentProperties...");try{if("componentProperties"in o&&o.componentProperties){let r=o.componentProperties;console.log("\u{1F50D} [DEBUG] Found componentProperties on selected instance:",Object.keys(r));let i=await o.getMainComponentAsync();if(i&&i.parent&&i.parent.type==="COMPONENT_SET"){let a=i.parent,c=null;try{"componentPropertyDefinitions"in a&&(c=a.componentPropertyDefinitions,console.log("\u{1F50D} [DEBUG] Got componentPropertyDefinitions from component set"))}catch(d){console.log("\u{1F50D} [DEBUG] Could not access componentPropertyDefinitions, using instance properties only")}for(let d in r){let l=r[d];console.log(`\u{1F50D} [DEBUG] Processing instance property "${d}":`,l);let p=d,u=[],g="";if(d.includes("#")&&(p=d.split("#")[0]),l&&typeof l=="object"&&"value"in l?g=String(l.value):g=String(l),c&&c[d]){let f=c[d];switch(console.log(`\u{1F50D} [DEBUG] Found property definition for "${d}":`,f),f.type){case"VARIANT":u=f.variantOptions||[];break;case"BOOLEAN":u=["true","false"];break;case"TEXT":u=[g||"Text content"];break;case"INSTANCE_SWAP":f.preferredValues&&Array.isArray(f.preferredValues)?u=f.preferredValues.map(m=>m.key||m.name||"Component instance"):u=["Component instance"];break;default:u=[g||"Property value"]}}else console.log(`\u{1F50D} [DEBUG] No property definition for "${d}", inferring from value`),g==="true"||g==="false"?u=["true","false"]:u=[g||"Property value"];n.push({name:p,values:u,default:g||u[0]||"default"}),console.log("\u{1F50D} [DEBUG] Added instance property:",{name:p,values:u,default:g})}if(n.length>0)return console.log(`\u{1F50D} [DEBUG] Successfully extracted ${n.length} properties from selected instance`),n}}}catch(r){console.log("\u{1F50D} [DEBUG] Could not extract from instance componentProperties:",r)}}if(e.type==="COMPONENT_SET"){let o=e;console.log("\u{1F50D} [DEBUG] Attempting to access componentPropertyDefinitions...");try{if("componentPropertyDefinitions"in o){console.log("\u{1F50D} [DEBUG] componentPropertyDefinitions property exists on componentSet");let r=o.componentPropertyDefinitions;if(console.log("\u{1F50D} [DEBUG] Raw componentPropertyDefinitions:",r),console.log("\u{1F50D} [DEBUG] Type of componentPropertyDefinitions:",typeof r),r&&typeof r=="object"){let i=Object.keys(r);console.log("\u{1F50D} [DEBUG] Found componentPropertyDefinitions with keys:",i);for(let a in r){let c=r[a];console.log(`\u{1F50D} [DEBUG] Processing property "${a}":`,c);let d=a,l=[],p="";switch(a.includes("#")&&(d=a.split("#")[0],console.log(`\u{1F50D} [DEBUG] Cleaned display name: "${d}" from "${a}"`)),c.type){case"VARIANT":l=c.variantOptions||[],p=String(c.defaultValue)||l[0]||"default",console.log(`\u{1F50D} [DEBUG] VARIANT property "${d}": values=${l}, default=${p}`);break;case"BOOLEAN":l=["true","false"],p=c.defaultValue?"true":"false",console.log(`\u{1F50D} [DEBUG] BOOLEAN property "${d}": default=${p}`);break;case"TEXT":l=[String(c.defaultValue||"Text content")],p=String(c.defaultValue||"Text content"),console.log(`\u{1F50D} [DEBUG] TEXT property "${d}": value=${p}`);break;case"INSTANCE_SWAP":c.preferredValues&&Array.isArray(c.preferredValues)?l=c.preferredValues.map(u=>(console.log("\u{1F50D} [DEBUG] INSTANCE_SWAP preferred value:",u),u.key||u.name||"Component instance")):l=["Component instance"],p=l[0]||"Component instance",console.log(`\u{1F50D} [DEBUG] INSTANCE_SWAP property "${d}": values=${l}, default=${p}`);break;default:console.log(`\u{1F50D} [DEBUG] Unknown property type "${c.type}" for "${d}"`),l=["Property value"],p="Default"}n.push({name:d,values:l,default:p}),console.log("\u{1F50D} [DEBUG] Added property:",{name:d,values:l,default:p})}}else console.log("\u{1F50D} [DEBUG] componentPropertyDefinitions is not a valid object:",r)}else console.log("\u{1F50D} [DEBUG] componentPropertyDefinitions property does not exist on componentSet")}catch(r){console.error("\u{1F50D} [ERROR] Could not access componentPropertyDefinitions:",r),console.error("\u{1F50D} [ERROR] Error stack:",r instanceof Error?r.stack:"No stack trace")}if(n.length===0){console.log("\u{1F50D} [DEBUG] No properties found, trying variantGroupProperties fallback...");try{let r=o.variantGroupProperties;if(console.log("\u{1F50D} [DEBUG] variantGroupProperties:",r),r){let i=Object.keys(r);console.log("\u{1F50D} [DEBUG] Found variantGroupProperties with keys:",i);for(let a in r){let c=r[a];console.log(`\u{1F50D} [DEBUG] Processing variant property "${a}":`,c),n.push({name:a,values:c.values,default:c.values[0]||"default"})}}else console.log("\u{1F50D} [DEBUG] variantGroupProperties is null/undefined")}catch(r){console.warn("\u{1F50D} [WARN] Component set has errors, cannot access variantGroupProperties:",r)}}if(n.length===0&&o.children.length>0){console.log("\u{1F50D} [DEBUG] Analyzing variant structure to infer properties...");let r=new Map,i=new Map;o.children.forEach((a,c)=>{if(a.type==="COMPONENT"){let d=a.name;console.log(`\u{1F50D} [DEBUG] Analyzing variant ${c}: ${d}`),d.split(",").map(u=>u.trim()).forEach(u=>{let[g,f]=u.split("=").map(m=>m.trim());g&&f&&(r.has(g)||r.set(g,new Set),r.get(g).add(f))});let p=(u,g="")=>{let f=g?`${g}/${u.name}`:u.name;i.has(f)||i.set(f,[]),i.get(f).push(u.visible),"children"in u&&u.children.forEach(m=>p(m,f))};p(a)}}),r.forEach((a,c)=>{n.find(d=>d.name===c)||n.push({name:c,values:Array.from(a),default:Array.from(a)[0]||"default"})}),i.forEach((a,c)=>{let d=a.includes(!0),l=a.includes(!1);if(d&&l){let u=(c.split("/").pop()||"").replace(/\s*(layer|group|frame|icon|text)?\s*/gi,"").trim();u&&!n.find(g=>g.name===u)&&(n.push({name:u,values:["true","false"],default:"false"}),console.log(`\u{1F50D} [DEBUG] Inferred boolean property from visibility: ${u}`))}}),console.log(`\u{1F50D} [DEBUG] Inferred ${n.length} properties from variant analysis`)}if(n.length===0){console.log("\u{1F50D} [DEBUG] All Figma APIs failed, using comprehensive structural analysis...");let r=ki(o);console.log("\u{1F50D} [DEBUG] Properties from structural analysis:",r),n.push(...r)}}else if(e.type==="COMPONENT"){let o=e;console.log("\u{1F50D} [DEBUG] Processing COMPONENT node:",o.name);try{if("componentPropertyDefinitions"in o){let r=o.componentPropertyDefinitions;if(console.log("\u{1F50D} [DEBUG] Component componentPropertyDefinitions:",r),r&&typeof r=="object"){let i=Object.keys(r);console.log("\u{1F50D} [DEBUG] Found componentPropertyDefinitions on component with keys:",i);for(let a in r){let c=r[a],d=a,l=[],p="";switch(a.includes("#")&&(d=a.split("#")[0]),c.type){case"BOOLEAN":l=["true","false"],p=c.defaultValue?"true":"false";break;case"TEXT":l=[String(c.defaultValue||"Text content")],p=String(c.defaultValue||"Text content");break;case"INSTANCE_SWAP":c.preferredValues&&Array.isArray(c.preferredValues)?l=c.preferredValues.map(u=>u.key||u.name||"Component instance"):l=["Component instance"],p=l[0]||"Component instance";break;default:l=["Property value"],p="Default"}n.push({name:d,values:l,default:p})}}}else console.log("\u{1F50D} [DEBUG] componentPropertyDefinitions does not exist on component")}catch(r){console.warn("\u{1F50D} [WARN] Could not access componentPropertyDefinitions on component:",r)}if(o.parent&&o.parent.type==="COMPONENT_SET"){let r=o.parent;console.log("\u{1F50D} [DEBUG] Component is part of a component set, getting variant properties...");try{let i=r.variantGroupProperties;if(i)for(let a in i){let c=i[a];n.find(d=>d.name===a)||n.push({name:a,values:c.values,default:c.values[0]||"default"})}}catch(i){console.warn("\u{1F50D} [WARN] Component set has errors, cannot access variantGroupProperties:",i)}}}else if(e.type==="INSTANCE"){let o=e;if(console.log("\u{1F50D} [DEBUG] Processing INSTANCE node (fallback \u2014 Priority 1 may have been skipped)"),n.length===0)try{let r=await o.getMainComponentAsync();if(r)if(r.parent&&r.parent.type==="COMPONENT_SET"){let i=r.parent;console.log("\u{1F50D} [DEBUG] Instance fallback: extracting from parent component set:",i.name);try{if("componentPropertyDefinitions"in i){let a=i.componentPropertyDefinitions;if(a&&typeof a=="object"){for(let c in a){let d=a[c],l=c,p=[],u="";switch(c.includes("#")&&(l=c.split("#")[0]),d.type){case"VARIANT":p=d.variantOptions||[],u=String(d.defaultValue)||p[0]||"default";break;case"BOOLEAN":p=["true","false"],u=d.defaultValue?"true":"false";break;case"TEXT":p=[String(d.defaultValue||"Text content")],u=String(d.defaultValue||"Text content");break;case"INSTANCE_SWAP":d.preferredValues&&Array.isArray(d.preferredValues)?p=d.preferredValues.map(g=>g.key||g.name||"Component instance"):p=["Component instance"],u=p[0]||"Component instance";break;default:p=["Property value"],u="Default"}n.push({name:l,values:p,default:u})}console.log(`\u{1F50D} [DEBUG] Instance fallback: extracted ${n.length} properties from component set`)}}}catch(a){console.warn("\u{1F50D} [WARN] Instance fallback: could not access componentPropertyDefinitions:",a)}if(n.length===0)try{let a=i.variantGroupProperties;if(a)for(let c in a){let d=a[c];n.find(l=>l.name===c)||n.push({name:c,values:d.values,default:d.values[0]||"default"})}}catch(a){console.warn("\u{1F50D} [WARN] Instance fallback: could not access variantGroupProperties:",a)}}else{console.log("\u{1F50D} [DEBUG] Instance fallback: extracting from standalone main component");try{if("componentPropertyDefinitions"in r){let i=r.componentPropertyDefinitions;if(i&&typeof i=="object"){for(let a in i){let c=i[a],d=a,l=[],p="";switch(a.includes("#")&&(d=a.split("#")[0]),c.type){case"BOOLEAN":l=["true","false"],p=c.defaultValue?"true":"false";break;case"TEXT":l=[String(c.defaultValue||"Text content")],p=String(c.defaultValue||"Text content");break;case"INSTANCE_SWAP":c.preferredValues&&Array.isArray(c.preferredValues)?l=c.preferredValues.map(u=>u.key||u.name||"Component instance"):l=["Component instance"],p=l[0]||"Component instance";break;default:l=["Property value"],p="Default"}n.push({name:d,values:l,default:p})}console.log(`\u{1F50D} [DEBUG] Instance fallback: extracted ${n.length} properties from main component`)}}}catch(i){console.warn("\u{1F50D} [WARN] Instance fallback: could not access componentPropertyDefinitions on main component:",i)}}}catch(r){console.warn("\u{1F50D} [WARN] Instance fallback: could not get main component:",r)}}let s=[];return n.forEach(o=>{s.find(r=>r.name===o.name)||s.push(o)}),console.log(`\u{1F50D} [DEBUG] Final result: Extracted ${s.length} unique properties:`,s.map(o=>({name:o.name,valueCount:o.values.length,default:o.default}))),s}function ki(e){let t=[];console.log("\u{1F50D} [STRUCTURAL] Starting comprehensive structural analysis of component set:",e.name);let n=Si(e);t.push(...n);let s=new Set,o=new Set,r=new Set,i=new Set;e.children.forEach(d=>{if(d.type==="COMPONENT"){console.log(`\u{1F50D} [STRUCTURAL] Analyzing variant: ${d.name}`);let l=(p,u=0)=>{let g=" ".repeat(u);console.log(`\u{1F50D} [STRUCTURAL] ${g}Found child: ${p.name} (type: ${p.type})`),s.add(p.name),p.type==="TEXT"?o.add(p.name):p.type==="INSTANCE"&&r.add(p.name),(p.visible===!1||p.name.toLowerCase().includes("hidden"))&&i.add(p.name),"children"in p&&p.children&&p.children.forEach(f=>l(f,u+1))};l(d)}}),console.log("\u{1F50D} [STRUCTURAL] Analysis results:"),console.log("\u{1F50D} [STRUCTURAL] - All child names:",Array.from(s)),console.log("\u{1F50D} [STRUCTURAL] - Text layers:",Array.from(o)),console.log("\u{1F50D} [STRUCTURAL] - Instance layers:",Array.from(r)),console.log("\u{1F50D} [STRUCTURAL] - Boolean indicators:",Array.from(i)),o.forEach(d=>{let l=d.replace(/\s*(layer|text|label)?\s*/gi,"").trim();l&&!t.find(p=>p.name.toLowerCase()===l.toLowerCase())&&(t.push({name:l,values:["Text content"],default:"Label"}),console.log(`\u{1F50D} [STRUCTURAL] Added TEXT property: ${l}`))}),r.forEach(d=>{let l=d.replace(/\s*(layer|instance)?\s*/gi,"").trim();l&&!t.find(p=>p.name.toLowerCase()===l.toLowerCase())&&(t.push({name:l,values:["Component instance"],default:"Default component"}),console.log(`\u{1F50D} [STRUCTURAL] Added INSTANCE_SWAP property: ${l}`))}),["icon before","icon after","slot before","slot after","before","after","prefix","suffix","leading","trailing"].forEach(d=>{if(Array.from(s).find(p=>p.toLowerCase().includes(d.toLowerCase()))&&!t.find(p=>p.name.toLowerCase().includes(d.toLowerCase()))){let p=d.split(" ").map(u=>u.charAt(0).toUpperCase()+u.slice(1)).join(" ");t.push({name:p,values:["true","false"],default:"false"}),console.log(`\u{1F50D} [STRUCTURAL] Added BOOLEAN property: ${p}`)}});let c=e.name.toLowerCase();return(c.includes("button")||c.includes("btn"))&&[{name:"Slot Before",type:"BOOLEAN"},{name:"Text",type:"TEXT"},{name:"Icon Before",type:"INSTANCE_SWAP"},{name:"Icon After",type:"INSTANCE_SWAP"}].forEach(({name:l,type:p})=>{if(!t.find(u=>u.name.toLowerCase()===l.toLowerCase())){let u,g;switch(p){case"BOOLEAN":u=["true","false"],g="false";break;case"TEXT":u=["Text content"],g="Label";break;case"INSTANCE_SWAP":u=["Component instance"],g="Default icon";break;default:u=["Property value"],g="Default"}t.push({name:l,values:u,default:g}),console.log(`\u{1F50D} [STRUCTURAL] Added common ${p} property: ${l}`)}}),console.log(`\u{1F50D} [STRUCTURAL] Final structural analysis result: ${t.length} properties found`),t}async function Ge(e){let t=[];if(e.type==="COMPONENT_SET"){let s=e,o;try{o=s.variantGroupProperties}catch(r){console.warn("Component set has errors, cannot access variantGroupProperties:",r),o=void 0}if(o)for(let r in o){let i=r.toLowerCase();(i==="state"||i==="states"||i==="status")&&t.push(...o[r].values)}s.children.forEach(r=>{let i=r.name.toLowerCase();["default","hover","focus","disabled","pressed","active","selected"].forEach(a=>{let c=t.find(d=>d.toLowerCase()===a.toLowerCase());i.includes(a)&&!c&&t.push(a)})})}else if(e.type==="COMPONENT"){let s=e;if(s.parent&&s.parent.type==="COMPONENT_SET")return await Ge(s.parent)}else if(e.type==="INSTANCE"){let o=await e.getMainComponentAsync();if(o)return await Ge(o)}let n=[];return t.forEach(s=>{s&&typeof s=="string"&&s.trim()!==""&&(n.find(r=>r.toLowerCase()===s.toLowerCase())||n.push(s.trim()))}),n}async function Os(e,t,n,s={},o="anthropic"){console.log("\u{1F3AF} Starting enhanced component analysis...");let r=figma.currentPage.selection[0],i=s.node||r;if(!i)throw new Error("No node selected");let a=await Ms(i,r),c=await Ge(i),d=await ue(i),l="";if(i.type==="COMPONENT"||i.type==="COMPONENT_SET")l=i.description||"";else if(i.type==="INSTANCE"){let C=await i.getMainComponentAsync();C&&(l=C.description||"")}e.existingDescription=l;let p=ne([i],s.lintSettings||j);console.log(`\u{1F50D} [LINT] Deterministic lint: ${p.summary.totalErrors} issues in ${p.summary.nodesWithErrors} nodes`),console.log("\u{1F4CA} [ANALYSIS] Extracted from Figma API:"),console.log(` Properties: ${a.length}`),console.log(` States: ${c.length}`),console.log(` Tokens: ${Object.keys(d).length} categories`),console.log(` Description: ${l?"Present":"Missing"}`);let u=s.mcpServerUrl||"http://localhost:3000/mcp",g=s.useMCP!==!1&&u,f;if(g){console.log(`\u{1F504} Using hybrid LLM + MCP approach (${o})...`);let h=Ni(e,a,c,d,l,p),C=await ae(o,t,{prompt:h,model:n,maxTokens:2048,temperature:.1}),k=pe(C.content);if(!k)throw new Error("Failed to extract JSON from LLM response");let N=null;try{N=await wi(e,u,k),console.log("\u2705 MCP enhancements received")}catch(y){console.warn("\u26A0\uFE0F MCP enhancement failed, continuing with LLM data only:",y)}f=Ci(k,N,{node:i,context:e,actualProperties:a,actualStates:c,tokens:d,componentDescription:l})}else{console.log(`\u{1F4DD} Using ${o}-only analysis...`);let h=nn(e),C=await ae(o,t,{prompt:h,model:n,maxTokens:2048,temperature:.1});if(f=pe(C.content),!f)throw new Error("Failed to extract JSON from response")}let m=Fe(f);return await Rt(m,e,s,p,t,n,o)}function Ni(e,t,n,s,o,r){var d;let i=((d=e.additionalContext)==null?void 0:d.componentFamily)||"generic",a=et(e.hierarchy),c="";if(r&&r.summary.totalErrors>0){let l=r.summary.byType,p=r.errors.slice(0,15).map(u=>` - [${u.errorType.toUpperCase()}] ${u.nodeName}: ${u.message}`).join(` +`&&(r=e.length),r===-1)continue;let a=e.substring(i,r).trim();try{return JSON.parse(a)}catch(c){if(a.startsWith("{"))try{return Cn(a)}catch(l){continue}}}return null}function Mi(e){let t=/```(?:json)?\s*(\{[\s\S]*?\})\s*```/gi,n;for(;(n=t.exec(e))!==null;)try{return JSON.parse(n[1])}catch(s){continue}return null}function Oi(e){let t=e.match(/\{[\s\S]*\}/);return t?JSON.parse(t[0]):null}function je(e){if(!e||typeof e!="object")return e;let t=["aria","accessibility api","semantic html","keyboard navigation","event handler","interactive behavior","onclick","onchange","state management","controlled component","uncontrolled component","props","responsive breakpoint","css implementation","@media","animation token","transition timing","programmatic animation","keyframe","api integration","data binding","dynamic content","fetch","axios","implement","add handler","bind event","attach listener","programming pattern","functional pattern","react hook","usestate","useeffect"],n=i=>{let r=i.toLowerCase();return t.some(a=>r.includes(a))},s=i=>Array.isArray(i)?i.filter(r=>{if(typeof r=="string"){let a=!n(r);return a||console.log("\u{1F6AB} [FILTER] Removed development-focused recommendation:",r),a}return!0}):i,o=JSON.parse(JSON.stringify(e));return o.mcpReadiness&&(o.mcpReadiness.recommendations&&(o.mcpReadiness.recommendations=s(o.mcpReadiness.recommendations)),o.mcpReadiness.gaps&&(o.mcpReadiness.gaps=s(o.mcpReadiness.gaps))),o.audit&&(o.audit.tokenOpportunities&&(o.audit.tokenOpportunities=s(o.audit.tokenOpportunities)),o.audit.structureIssues&&(o.audit.structureIssues=s(o.audit.structureIssues))),o.accessibility&&(o.accessibility.designConsiderations&&(o.accessibility.designConsiderations=s(o.accessibility.designConsiderations)),o.accessibility.visualIndicators&&(o.accessibility.visualIndicators=s(o.accessibility.visualIndicators))),o}var E=class extends Error{constructor(n,s,o,i){super(n);this.code=s;this.statusCode=o;this.retryAfter=i;this.name="LLMError"}};var St={anthropic:"claude-sonnet-4-5-20250929",openai:"gpt-5.2",google:"gemini-2.5-pro"};var Fi=[{id:"claude-opus-4-5-20251218",name:"Claude Opus 4.5",description:"Flagship model - Most capable, best for complex analysis and reasoning",contextWindow:2e5,isDefault:!1},{id:"claude-sonnet-4-5-20250929",name:"Claude Sonnet 4.5",description:"Standard model - Balanced performance and cost, recommended for most tasks",contextWindow:2e5,isDefault:!0},{id:"claude-haiku-4-5-20251001",name:"Claude Haiku 4.5",description:"Economy model - Fastest responses, ideal for quick analysis",contextWindow:2e5,isDefault:!1}],vt=class{constructor(){this.name="Anthropic";this.id="anthropic";this.endpoint="https://api.anthropic.com/v1/messages";this.keyPrefix="sk-ant-";this.keyPlaceholder="sk-ant-...";this.models=Fi}formatRequest(t){let n={model:t.model,messages:[{role:"user",content:t.prompt.trim()}],max_tokens:t.maxTokens};return t.temperature!==void 0&&(n.temperature=t.temperature),t.additionalParams&&Object.assign(n,t.additionalParams),n}parseResponse(t){let n=t;if(!n.content||!Array.isArray(n.content))throw new E("Invalid response format from Anthropic API: missing content array","INVALID_REQUEST");let s=n.content.filter(o=>o.type==="text").map(o=>o.text).join(` +`);if(!s)throw new E("Invalid response format from Anthropic API: no text content found","INVALID_REQUEST");return{content:s.trim(),model:n.model,usage:n.usage?{promptTokens:n.usage.input_tokens,completionTokens:n.usage.output_tokens,totalTokens:n.usage.input_tokens+n.usage.output_tokens}:void 0,metadata:{id:n.id,stopReason:n.stop_reason}}}validateApiKey(t){if(!t||typeof t!="string")return{isValid:!1,error:"API Key Required: Please provide a valid Claude API key."};let n=t.trim();return n.length===0?{isValid:!1,error:"API Key Required: The Claude API key cannot be empty."}:n.startsWith(this.keyPrefix)?n.length<40?{isValid:!1,error:"Invalid API Key Format: The API key appears to be too short. Please verify you copied the complete key."}:{isValid:!0}:{isValid:!1,error:`Invalid API Key Format: Claude API keys should start with "${this.keyPrefix}". Please check your API key.`}}getHeaders(t){return{"content-type":"application/json","x-api-key":t.trim(),"anthropic-version":"2023-06-01","anthropic-dangerous-direct-browser-access":"true"}}getDefaultModel(){return this.models.find(n=>n.isDefault)||this.models[1]}handleError(t,n){var i;let s=n,o=((i=s==null?void 0:s.error)==null?void 0:i.message)||(typeof n=="string"?n:"Unknown error");switch(t){case 400:return new E(`Claude API Error (400): ${o}. Please check your request format.`,"INVALID_REQUEST",400);case 401:return new E("Claude API Error (401): Invalid API key. Please check your Claude API key in settings.","INVALID_API_KEY",401);case 403:return new E("Claude API Error (403): Access forbidden. Please check your API key permissions.","INVALID_API_KEY",403);case 404:return new E(`Claude API Error (404): ${o}. The requested model may not be available.`,"MODEL_NOT_FOUND",404);case 429:return new E("Claude API Error (429): Rate limit exceeded. Please try again later.","RATE_LIMIT_EXCEEDED",429);case 500:return new E("Claude API Error (500): Server error. The Claude API is experiencing issues. Please try again later.","SERVER_ERROR",500);case 503:return new E("Claude API Error (503): Service unavailable. The Claude API is temporarily down. Please try again later.","SERVICE_UNAVAILABLE",503);default:return new E(`Claude API Error (${t}): ${o}`,"UNKNOWN_ERROR",t)}}},Nt=new vt;var Vi=[{id:"gpt-5.2",name:"GPT-5.2",description:"Flagship model with advanced reasoning capabilities",contextWindow:128e3,isDefault:!0},{id:"gpt-5.2-pro",name:"GPT-5.2 Pro",description:"Premium model with extended reasoning for complex tasks",contextWindow:128e3,isDefault:!1},{id:"gpt-5-mini",name:"GPT-5 Mini",description:"Economy model - fast and cost-effective",contextWindow:128e3,isDefault:!1}],It=class{constructor(){this.name="OpenAI";this.id="openai";this.endpoint="https://api.openai.com/v1/chat/completions";this.keyPrefix="sk-";this.keyPlaceholder="sk-...";this.models=Vi}formatRequest(t){let n={model:t.model,messages:[{role:"user",content:t.prompt}],max_completion_tokens:t.maxTokens,temperature:t.temperature};return t.additionalParams&&Object.assign(n,t.additionalParams),n}parseResponse(t){let n=t;if(!n.choices||n.choices.length===0)throw new E("Invalid response format: no choices returned","INVALID_REQUEST");let s=n.choices[0];if(!s.message||typeof s.message.content!="string")throw new E("Invalid response format: missing message content","INVALID_REQUEST");let o={content:s.message.content.trim(),model:n.model};return n.usage&&(o.usage={promptTokens:n.usage.prompt_tokens,completionTokens:n.usage.completion_tokens,totalTokens:n.usage.total_tokens}),o.metadata={id:n.id,finishReason:s.finish_reason,created:n.created},o}validateApiKey(t){if(!t||typeof t!="string")return{isValid:!1,error:"API Key Required: Please provide a valid OpenAI API key."};let n=t.trim();return n.length===0?{isValid:!1,error:"API Key Required: The OpenAI API key cannot be empty."}:n.startsWith(this.keyPrefix)?n.length<20?{isValid:!1,error:"Invalid API Key Format: The API key appears to be too short. Please verify you copied the complete key."}:{isValid:!0}:{isValid:!1,error:`Invalid API Key Format: OpenAI API keys should start with "${this.keyPrefix}". Please check your API key.`}}getHeaders(t){return{"Content-Type":"application/json",Authorization:`Bearer ${t.trim()}`}}getDefaultModel(){return this.models.find(n=>n.isDefault)||this.models[0]}handleError(t,n){var i;let s=n,o=((i=s==null?void 0:s.error)==null?void 0:i.message)||(s==null?void 0:s.message)||"Unknown error occurred";switch(t){case 400:return o.toLowerCase().includes("context_length_exceeded")||o.toLowerCase().includes("maximum context length")?new E(`OpenAI API Error (400): Context length exceeded. ${o}`,"CONTEXT_LENGTH_EXCEEDED",t):new E(`OpenAI API Error (400): ${o}. Please check your request format.`,"INVALID_REQUEST",t);case 401:return new E("OpenAI API Error (401): Invalid API key. Please check your OpenAI API key in settings.","INVALID_API_KEY",t);case 403:return new E("OpenAI API Error (403): Access forbidden. Please check your API key permissions or account status.","INVALID_API_KEY",t);case 404:return new E(`OpenAI API Error (404): Model not found. ${o}`,"MODEL_NOT_FOUND",t);case 429:let r=o.match(/try again in (\d+)/i),a=r?parseInt(r[1],10):void 0;return new E(`OpenAI API Error (429): Rate limit exceeded. ${a?`Please try again in ${a} seconds.`:"Please try again later."}`,"RATE_LIMIT_EXCEEDED",t,a);case 500:return new E("OpenAI API Error (500): Server error. The OpenAI API is experiencing issues. Please try again later.","SERVER_ERROR",t);case 502:return new E("OpenAI API Error (502): Bad gateway. The OpenAI API is temporarily unavailable. Please try again later.","SERVICE_UNAVAILABLE",t);case 503:return new E("OpenAI API Error (503): Service unavailable. The OpenAI API is temporarily down. Please try again later.","SERVICE_UNAVAILABLE",t);case 504:return new E("OpenAI API Error (504): Gateway timeout. The request took too long. Please try again.","SERVICE_UNAVAILABLE",t);default:return new E(`OpenAI API Error (${t}): ${o}`,"UNKNOWN_ERROR",t)}}},Ct=new It;var Di=[{id:"gemini-3-pro-preview",name:"Gemini 3 Pro",description:"Flagship model with advanced reasoning and multimodal capabilities",contextWindow:1e6,isDefault:!0},{id:"gemini-2.5-pro",name:"Gemini 2.5 Pro",description:"Standard reasoning model with excellent performance",contextWindow:1e6,isDefault:!1},{id:"gemini-2.5-flash",name:"Gemini 2.5 Flash",description:"Economy model optimized for speed and efficiency",contextWindow:1e6,isDefault:!1}],xt=class{constructor(){this.name="Google";this.id="google";this.endpoint="https://generativelanguage.googleapis.com/v1beta/models";this.keyPrefix="AIza";this.keyPlaceholder="AIza...";this.models=Di}formatRequest(t){let n={contents:[{parts:[{text:t.prompt}]}],generationConfig:{maxOutputTokens:t.maxTokens,temperature:t.temperature}};if(t.additionalParams){let{topP:s,topK:o,stopSequences:i}=t.additionalParams;s!==void 0&&(n.generationConfig.topP=s),o!==void 0&&(n.generationConfig.topK=o),i!==void 0&&(n.generationConfig.stopSequences=i)}return n}parseResponse(t){var c;let n=t;if(n.error)throw new E(n.error.message||"Unknown Gemini API error",this.mapErrorCodeToLLMErrorCode(n.error.code,n.error.status),n.error.code);if(!n.candidates||n.candidates.length===0){let l=Object.keys(n);throw new E(`No candidates in Gemini response. Response keys: [${l.join(", ")}]${n.error?`. Error: ${n.error.message}`:""}`,"INVALID_REQUEST")}let s=n.candidates[0];if(s.finishReason==="SAFETY")throw new E("Gemini response blocked by safety filters. Try rephrasing the prompt.","INVALID_REQUEST");let o=(c=s.content)==null?void 0:c.parts;if(!o||o.length===0)throw new E(`No content parts in Gemini response. Finish reason: ${s.finishReason||"unknown"}. Has content: ${!!s.content}`,"INVALID_REQUEST");let i=o.find(l=>typeof l.text=="string");if(!i||!i.text){let l=o.map(d=>Object.keys(d).join(",")).join("; ");throw new E(`No text content in Gemini response parts. Part types: [${l}]. Finish reason: ${s.finishReason||"unknown"}`,"INVALID_REQUEST")}let a={content:i.text,model:"gemini"};return n.usageMetadata&&(a.usage={promptTokens:n.usageMetadata.promptTokenCount||0,completionTokens:n.usageMetadata.candidatesTokenCount||0,totalTokens:n.usageMetadata.totalTokenCount||0}),a}validateApiKey(t){if(!t||typeof t!="string")return{isValid:!1,error:"API key is required"};let n=t.trim();return n.length===0?{isValid:!1,error:"API key cannot be empty"}:n.startsWith(this.keyPrefix)?n.length<30||n.length>50?{isValid:!1,error:"API key appears to have an invalid length. Please verify you copied the complete key."}:/^[A-Za-z0-9_-]+$/.test(n)?{isValid:!0}:{isValid:!1,error:"API key contains invalid characters"}:{isValid:!1,error:`Google API keys should start with "${this.keyPrefix}". Please check your API key.`}}getHeaders(t){return{"Content-Type":"application/json"}}getEndpoint(t,n){let s=n.trim();return`${this.endpoint}/${t}:generateContent?key=${s}`}getDefaultModel(){return this.models.find(n=>n.isDefault)||this.models[0]}handleError(t,n){var u,f;let s=n,o=s==null?void 0:s.error,i=(o==null?void 0:o.message)||"Unknown Google API error",r=o==null?void 0:o.status,a=(o==null?void 0:o.code)||t,c=this.mapErrorCodeToLLMErrorCode(a,r),l;if(t===429){l=6e4;let m=(u=o==null?void 0:o.details)==null?void 0:u.find(g=>{var p;return(p=g["@type"])==null?void 0:p.includes("RetryInfo")});if((f=m==null?void 0:m.metadata)!=null&&f.retryDelay){let g=m.metadata.retryDelay.match(/(\d+)s/);g&&(l=parseInt(g[1],10)*1e3)}}let d=i;switch(c){case"INVALID_API_KEY":d="Google API Error: Invalid API key. Please check your API key in settings.";break;case"RATE_LIMIT_EXCEEDED":d=`Google API Error: Rate limit exceeded. ${l?`Please try again in ${Math.ceil(l/1e3)} seconds.`:"Please try again later."}`;break;case"MODEL_NOT_FOUND":d="Google API Error: Model not found. Please select a valid model.";break;case"CONTEXT_LENGTH_EXCEEDED":d="Google API Error: Input too long. Please reduce the size of your request.";break;case"SERVER_ERROR":d="Google API Error: Server error. Please try again later.";break;case"SERVICE_UNAVAILABLE":d="Google API Error: Service temporarily unavailable. Please try again later.";break}return new E(d,c,t,l)}mapErrorCodeToLLMErrorCode(t,n){if(n){let s=n.toUpperCase();if(s==="INVALID_ARGUMENT")return"INVALID_REQUEST";if(s==="PERMISSION_DENIED"||s==="UNAUTHENTICATED")return"INVALID_API_KEY";if(s==="NOT_FOUND")return"MODEL_NOT_FOUND";if(s==="RESOURCE_EXHAUSTED")return"RATE_LIMIT_EXCEEDED";if(s==="UNAVAILABLE")return"SERVICE_UNAVAILABLE"}switch(t){case 400:return"INVALID_REQUEST";case 401:case 403:return"INVALID_API_KEY";case 404:return"MODEL_NOT_FOUND";case 429:return"RATE_LIMIT_EXCEEDED";case 500:return"SERVER_ERROR";case 503:return"SERVICE_UNAVAILABLE";default:return"UNKNOWN_ERROR"}}},At=new xt;var _i={anthropic:Nt,openai:Ct,google:At};function ce(e){let t=_i[e];if(!t)throw new E(`Unknown provider: ${e}`,"INVALID_REQUEST",400);return t}async function le(e,t,n){var c,l;let s=ce(e),o=s.validateApiKey(t);if(!o.isValid)throw new E(o.error||"Invalid API key format","INVALID_API_KEY",401);let i=s.formatRequest(n),r=s.getHeaders(t),a=s.endpoint;e==="google"&&(a=`${s.endpoint}/${n.model}:generateContent?key=${t.trim()}`);try{console.log(`Making ${s.name} API call to ${a}...`);let d=await fetch(a,{method:"POST",headers:r,body:JSON.stringify(i)});if(!d.ok){let f;try{f=await d.json()}catch(m){f=await d.text()}throw s.handleError(d.status,f)}let u=await d.json();return console.log(`${s.name} API response status: ${d.status}`),console.log(`${s.name} API response keys:`,Object.keys(u)),e==="google"&&(console.log("Gemini response candidates:",u.candidates?u.candidates.length:"none"),(c=u.candidates)!=null&&c[0]&&(console.log("Gemini candidate[0] keys:",Object.keys(u.candidates[0])),u.candidates[0].content&&console.log("Gemini content parts:",((l=u.candidates[0].content.parts)==null?void 0:l.length)||"none")),u.error&&console.log("Gemini error:",JSON.stringify(u.error))),s.parseResponse(u)}catch(d){throw d instanceof E?d:d instanceof Error&&(d.message.includes("Failed to fetch")||d.message.includes("NetworkError"))?new E(`Network error connecting to ${s.name}. Please check your internet connection.`,"NETWORK_ERROR"):new E(`Unexpected error calling ${s.name}: ${d instanceof Error?d.message:"Unknown error"}`,"UNKNOWN_ERROR")}}var H={SELECTED_PROVIDER:"selected-provider",SELECTED_MODEL:"selected-model",apiKey:e=>`${e}-api-key`,LEGACY_CLAUDE_KEY:"claude-api-key",LEGACY_CLAUDE_MODEL:"claude-model"},Bi={provider:"anthropic",model:St.anthropic};async function Gi(){try{let e=await figma.clientStorage.getAsync(H.LEGACY_CLAUDE_KEY),t=await figma.clientStorage.getAsync(H.LEGACY_CLAUDE_MODEL);return e?{needsMigration:!0,legacyKey:e,legacyModel:t}:{needsMigration:!1}}catch(e){return{needsMigration:!1}}}async function wt(){let e=await Gi();e.needsMigration&&(console.log("Migrating legacy Claude storage to multi-provider format..."),e.legacyKey&&await figma.clientStorage.setAsync(H.apiKey("anthropic"),e.legacyKey),await figma.clientStorage.setAsync(H.SELECTED_PROVIDER,"anthropic"),e.legacyModel&&await figma.clientStorage.setAsync(H.SELECTED_MODEL,e.legacyModel),await figma.clientStorage.deleteAsync(H.LEGACY_CLAUDE_KEY),await figma.clientStorage.deleteAsync(H.LEGACY_CLAUDE_MODEL),console.log("Migration complete"))}async function Et(){await wt();let e=await figma.clientStorage.getAsync(H.SELECTED_PROVIDER)||Bi.provider,t=await figma.clientStorage.getAsync(H.SELECTED_MODEL)||St[e],n=await figma.clientStorage.getAsync(H.apiKey(e));return{providerId:e,modelId:t,apiKey:n}}async function Tt(e,t,n){await figma.clientStorage.setAsync(H.SELECTED_PROVIDER,e),await figma.clientStorage.setAsync(H.SELECTED_MODEL,t),n!==void 0&&await figma.clientStorage.setAsync(H.apiKey(e),n)}async function xn(e){await figma.clientStorage.deleteAsync(H.apiKey(e))}var An=/^(Frame|Rectangle|Ellipse|Group|Vector|Line|Polygon|Star|Text|Component|Instance|Slice|Boolean|Union|Subtract|Intersect|Exclude)\s*\d*$/i,wn=/\s+\d+$/,En={button:"btn",icon:"ico",input:"input",text:"txt",image:"img",container:"container",card:"card",list:"list","list-item":"list-item",nav:"nav",header:"header",footer:"footer",modal:"modal",dropdown:"dropdown",checkbox:"checkbox",radio:"radio",toggle:"toggle",avatar:"avatar",badge:"badge",divider:"divider",spacer:"spacer",link:"link",tab:"tab",tooltip:"tooltip",alert:"alert",progress:"progress",skeleton:"skeleton",unknown:"layer"},he=[["btn","button"],["button","button"],["cta","button"],["submit","button"],["icon","icon"],["ico","icon"],["glyph","icon"],["symbol","icon"],["arrow","icon"],["chevron","icon"],["close","icon"],["plus","icon"],["minus","icon"],["txt","text"],["label","text"],["title","text"],["heading","text"],["paragraph","text"],["description","text"],["caption","text"],["subtitle","text"],["input","input"],["field","input"],["textfield","input"],["textarea","input"],["searchfield","input"],["searchbox","input"],["image","image"],["img","image"],["photo","image"],["picture","image"],["thumbnail","image"],["cover","image"],["container","container"],["wrapper","container"],["content","container"],["section","container"],["block","container"],["box","container"],["card","card"],["tile","card"],["panel","card"],["list","list"],["items","list"],["item","list-item"],["row","list-item"],["listitem","list-item"],["nav","nav"],["navbar","nav"],["navigation","nav"],["sidebar","nav"],["breadcrumb","nav"],["menu","nav"],["header","header"],["topbar","header"],["footer","footer"],["bottombar","footer"],["modal","modal"],["dialog","modal"],["popup","modal"],["overlay","modal"],["dropdown","dropdown"],["select","dropdown"],["picker","dropdown"],["combobox","dropdown"],["checkbox","checkbox"],["checkmark","checkbox"],["radio","radio"],["toggle","toggle"],["switch","toggle"],["avatar","avatar"],["profile","avatar"],["userpic","avatar"],["badge","badge"],["tag","badge"],["chip","badge"],["pill","badge"],["status","badge"],["divider","divider"],["separator","divider"],["hr","divider"],["spacer","spacer"],["gap","spacer"],["link","link"],["anchor","link"],["href","link"],["tab","tab"],["tabs","tab"],["tabbar","tab"],["tooltip","tooltip"],["hint","tooltip"],["popover","tooltip"],["alert","alert"],["notification","alert"],["toast","alert"],["message","alert"],["snackbar","alert"],["banner","alert"],["progress","progress"],["loader","progress"],["loading","progress"],["spinner","progress"],["progressbar","progress"],["skeleton","skeleton"],["placeholder","skeleton"],["shimmer","skeleton"]];function Tn(e){if(!e||typeof e!="string")return!0;let t=e.trim();return!!(An.test(t)||t.length===1||/^\d+$/.test(t))}function Ui(e){return wn.test(e.trim())}function Ke(e){let t=e.name.toLowerCase();for(let n=0;n20||n<=2&&s>20)return"divider";if(n<=32&&s<=32&&o>.5&&o<2)return"spacer"}return"unknown";case"FRAME":case"GROUP":return Ln(e);case"COMPONENT":case"INSTANCE":return Pn(e);case"COMPONENT_SET":return zi(e);default:return"unknown"}}function Ln(e){if(!("children"in e)||e.children.length===0)return"container";let t=e.children,n=[],s=[];for(let c=0;c=2)return"card";if(t.length>=3){let c=t[0].type,l=!0;for(let d=1;d=3&&r)return"nav"}return"container"}function Pn(e){let t=e.name.toLowerCase();for(let n=0;n0?Pn(e.children[0]):"unknown"}function Rn(e,t=10){let n=[];function s(o,i,r){if(i>t)return;let a=r?`${r} > ${o.name}`:o.name,c=Ke(o);if(Tn(o.name)){let l=be(o);n.push({nodeId:o.id,nodeName:o.name,currentName:o.name,suggestedName:l,severity:"error",reason:"Generic layer name detected",layerType:c,depth:i,path:a})}else if(Ui(o.name)){let l=o.name.replace(wn,"").trim(),d=be(o);n.push({nodeId:o.id,nodeName:o.name,currentName:o.name,suggestedName:d!==o.name?d:l,severity:"warning",reason:"Layer name has numbered suffix (possible duplicate)",layerType:c,depth:i,path:a})}if("children"in o)for(let l=0;l0?ji(e):En[t]||"layer"}function Hi(e){let n=e.name.toLowerCase().replace(An,"").replace(/[_\-\s]+/g,"-").replace(/^-|-$/g,"").trim();if(n&&n.length>1)return`icon-${Y(n)}`;if("children"in e&&e.children.length>0){let s=[];for(let o=0;o1.5||s<.67)return"icon-arrow"}return"icon"}function Wi(e){let n=(e.characters||"").trim();if(!n)return"text-empty";let s=n.split(/\s+/);if(s.length<=2&&n.length<=30){let m=Y(n);return m?`text-${m}`:"text-content"}let o=s[0].toLowerCase(),i=["welcome","about","contact","services","features","pricing"],r=["name","email","password","username","address","phone"],a=["submit","cancel","save","delete","edit","add","remove","ok","yes","no"],c=["learn","read","view","see","click","here","more"],l=["error","invalid","required","failed","wrong"],d=["success","done","complete","saved","updated"],u=n.toLowerCase();for(let m=0;m0){let s;for(let o=0;o0&&i[0].length>0)return`${n}-${Y(i.join(" "))}`}if(t==="button"||t==="input"){let o;for(let i=0;in.includes(a)),i=await da(e),r=o||i;return console.log(`\u{1F50D} [CONTAINER DETECTION] ${e.name}:`),console.log(` Name-based: ${o}`),console.log(` Structure-based: ${i}`),console.log(` Final result: ${r}`),n.includes("avatar")||n.includes("profile")?(t.componentFamily="avatar",t.possibleUseCase="User representation, often clickable for profile access or dropdown menus",t.hasInteractiveElements=!0,t.suggestedConsiderations.push("Consider if this avatar will be clickable/interactive"),t.suggestedConsiderations.push("May need hover/focus states for navigation"),t.designPatterns.push("profile-navigation","user-menu-trigger")):n.includes("button")||n.includes("btn")?(t.componentFamily="button",t.possibleUseCase="Interactive element for user actions",t.hasInteractiveElements=!0,t.suggestedConsiderations.push("Requires all interactive states"),t.designPatterns.push("action-trigger","form-submission")):n.includes("badge")||n.includes("tag")?(t.componentFamily="badge",t.possibleUseCase="Status indicator or label",t.hasInteractiveElements=!1,t.suggestedConsiderations.push("Typically non-interactive unless used as a filter"),t.designPatterns.push("status-indicator","category-label")):n.includes("input")||n.includes("field")?(t.componentFamily="input",t.possibleUseCase="Form input element",t.hasInteractiveElements=!0,t.suggestedConsiderations.push("Needs focus, error, and disabled states"),t.designPatterns.push("form-control","data-entry")):n.includes("card")?(t.componentFamily="card",t.possibleUseCase="Content container",t.hasInteractiveElements=n.includes("clickable")||n.includes("interactive"),t.suggestedConsiderations.push("May be interactive if used for navigation"),t.designPatterns.push("content-container","information-display")):n.includes("icon")?(t.componentFamily="icon",t.possibleUseCase="Visual indicator or decoration",t.hasInteractiveElements=!1,t.suggestedConsiderations.push("Usually decorative, but may be interactive if part of a button"),t.designPatterns.push("visual-indicator","decoration")):r&&(t.componentFamily="container",t.possibleUseCase="Layout container for organizing child components",t.hasInteractiveElements=!1,t.suggestedConsiderations.push("Focus on layout and organization rather than interaction states"),t.suggestedConsiderations.push("Child components handle individual interactions"),t.designPatterns.push("layout-container","component-organization")),"children"in e&&e.findAll(c=>c.type==="TEXT"&&(c.name.toLowerCase().includes("click")||c.name.toLowerCase().includes("action")||c.name.toLowerCase().includes("link"))).length>0&&(t.hasInteractiveElements=!0),e.parent&&e.parent.name.toLowerCase().includes("button")&&(t.hasInteractiveElements=!0,t.suggestedConsiderations.push("Part of a button component - needs interactive states")),t}async function da(e){if(!("children"in e)||!e.children||e.children.length===0)return!1;let t=e.children.filter(l=>l.type==="INSTANCE");if(t.length===0)return console.log(`\u{1F50D} [STRUCTURE] No child instances found in ${e.name}`),!1;console.log(`\u{1F50D} [STRUCTURE] Analyzing ${e.name} with ${t.length} child instances`);let n=new Map;await Promise.all(t.map(async l=>{try{let d=await l.getMainComponentAsync();if(d){let u=d.name;n.has(u)||n.set(u,[]),n.get(u).push(l)}}catch(d){console.log("\u26A0\uFE0F [STRUCTURE] Could not access main component for instance:",d)}})),console.log("\u{1F50D} [STRUCTURE] Instance groups:",Array.from(n.entries()).map(([l,d])=>`${l}: ${d.length}`));let s=Array.from(n.values()).some(l=>l.length>1),o=Array.from(n.keys()).some(l=>{let d=l.toLowerCase();return d.includes("item")||d.includes("panel")||d.includes("content")||d.includes("section")||d.includes("group")||d.includes("wrapper")||d.includes("tab")&&!d.includes("button")||d.includes("nav-item")||d.includes("menu-item")||d.includes("list-item")||d.includes("card-item")}),i=t.length/e.children.length,r=i>.6,a=n.size>=2&&s;return console.log(`\u{1F50D} [STRUCTURE] Analysis for ${e.name}:`),console.log(` Repeated components: ${s}`),console.log(` Organizational components: ${o}`),console.log(` Instance ratio: ${i.toFixed(2)} (${r?"high":"low"})`),console.log(` Collection pattern: ${a}`),s||o||r&&n.size>=2}function Js(e,t=0){let n=[],s={name:e.name,type:e.type,depth:t};if("children"in e&&e.children.length>0){s.children=[];for(let o of e.children)s.children.push(...Js(o,t+1))}return n.push(s),n}function ua(e){let t=[];function n(s){for(let o of s)t.push(o.name),o.children&&n(o.children)}return n(e),t}function bt(e){let t=new Set;function n(s){for(let o of s)o.type==="INSTANCE"&&t.add(o.name),o.children&&n(o.children)}return n(e),Array.from(t)}function fa(e){let t=[],n=!1;if(e.type==="COMPONENT_SET"){n=!0;try{let s=e,o;try{o=s.variantGroupProperties}catch(i){console.warn("Component set has errors, cannot access variantGroupProperties:",i),o=void 0}o&&t.push(...Object.keys(o))}catch(s){console.warn("Error analyzing component set:",s)}}else{let s=We(e).map(i=>i.name.toLowerCase());["primary","secondary","tertiary","small","medium","large","xl","xs","default","hover","focus","active","disabled","filled","outline","ghost","link","light","dark"].forEach(i=>{s.some(r=>r.includes(i))&&(t.includes(i)||t.push(i))})}return{isComponentSet:n,potentialVariants:t}}function pa(e){let t=[],n=We(e),s=e.name.toLowerCase(),o=["radiobutton","checkbox","icon","button","input","focusring","focus","indicator","background","border","outline","shadow","ring","control","handle","thumb","track","progress","slider","arrow","chevron","close","minimize","maximize"];n.filter(c=>c.type==="TEXT").forEach(c=>{let l=c.name.toLowerCase();o.some(d=>l.includes(d))||s.includes(l)||l.includes(s.split(" ")[0])||(l.includes("title")||l.includes("label")||l.includes("text")||l.includes("content"))&&l.length>2&&t.push(c.name)}),n.filter(c=>c.type==="FRAME").forEach(c=>{let l=c.name.toLowerCase();o.some(d=>l.includes(d))||(l.includes("content")&&!l.includes("background")||l.includes("slot")||l.includes("container")&&!l.includes("main"))&&t.push(c.name)});let a=[...new Set(t)].filter(c=>{let l=c.toLowerCase();return l.length>2&&!["text","label","content"].includes(l)&&!o.some(d=>l.includes(d))});return console.log(`\u{1F50D} [SLOTS] Detected ${a.length} legitimate content slots from ${t.length} candidates:`,a),a}function Ys(e){return"fills"in e&&Array.isArray(e.fills)&&e.fills.length>0?e.fills.some(t=>t.visible!==!1):"children"in e?e.children.some(t=>Ys(t)):!1}function Qs(e){return"strokes"in e&&Array.isArray(e.strokes)&&e.strokes.length>0?e.strokes.some(t=>t.visible!==!1):"children"in e?e.children.some(t=>Qs(t)):!1}function Zs(e){return"effects"in e&&Array.isArray(e.effects)&&e.effects.length>0?e.effects.some(t=>t.visible!==!1):"children"in e?e.children.some(t=>Zs(t)):!1}function ma(e){let t=new Map;e.children.forEach(s=>{s.type==="COMPONENT"&&s.name.split(",").map(r=>r.trim()).forEach(r=>{let[a,c]=r.split("=").map(l=>l.trim());a&&c&&(t.has(a)||t.set(a,new Set),t.get(a).add(c))})});let n=[];return t.forEach((s,o)=>{let i=Array.from(s);n.push({name:o,values:i,default:i[0]||"default"})}),n}async function eo(e,t){let n=[];if(console.log("\u{1F50D} [DEBUG] Starting property extraction for node:",e.name,"type:",e.type),console.log("\u{1F50D} [DEBUG] Originally selected node:",t==null?void 0:t.name,"type:",t==null?void 0:t.type),t&&t.type==="INSTANCE"){let o=t;console.log("\u{1F50D} [DEBUG] Extracting from selected instance componentProperties...");try{if("componentProperties"in o&&o.componentProperties){let i=o.componentProperties;console.log("\u{1F50D} [DEBUG] Found componentProperties on selected instance:",Object.keys(i));let r=await o.getMainComponentAsync();if(r&&r.parent&&r.parent.type==="COMPONENT_SET"){let a=r.parent,c=null;try{"componentPropertyDefinitions"in a&&(c=a.componentPropertyDefinitions,console.log("\u{1F50D} [DEBUG] Got componentPropertyDefinitions from component set"))}catch(l){console.log("\u{1F50D} [DEBUG] Could not access componentPropertyDefinitions, using instance properties only")}for(let l in i){let d=i[l];console.log(`\u{1F50D} [DEBUG] Processing instance property "${l}":`,d);let u=l,f=[],m="";if(l.includes("#")&&(u=l.split("#")[0]),d&&typeof d=="object"&&"value"in d?m=String(d.value):m=String(d),c&&c[l]){let g=c[l];switch(console.log(`\u{1F50D} [DEBUG] Found property definition for "${l}":`,g),g.type){case"VARIANT":f=g.variantOptions||[];break;case"BOOLEAN":f=["true","false"];break;case"TEXT":f=[m||"Text content"];break;case"INSTANCE_SWAP":g.preferredValues&&Array.isArray(g.preferredValues)?f=g.preferredValues.map(p=>p.key||p.name||"Component instance"):f=["Component instance"];break;default:f=[m||"Property value"]}}else console.log(`\u{1F50D} [DEBUG] No property definition for "${l}", inferring from value`),m==="true"||m==="false"?f=["true","false"]:f=[m||"Property value"];n.push({name:u,values:f,default:m||f[0]||"default"}),console.log("\u{1F50D} [DEBUG] Added instance property:",{name:u,values:f,default:m})}if(n.length>0)return console.log(`\u{1F50D} [DEBUG] Successfully extracted ${n.length} properties from selected instance`),n}}}catch(i){console.log("\u{1F50D} [DEBUG] Could not extract from instance componentProperties:",i)}}if(e.type==="COMPONENT_SET"){let o=e;console.log("\u{1F50D} [DEBUG] Attempting to access componentPropertyDefinitions...");try{if("componentPropertyDefinitions"in o){console.log("\u{1F50D} [DEBUG] componentPropertyDefinitions property exists on componentSet");let i=o.componentPropertyDefinitions;if(console.log("\u{1F50D} [DEBUG] Raw componentPropertyDefinitions:",i),console.log("\u{1F50D} [DEBUG] Type of componentPropertyDefinitions:",typeof i),i&&typeof i=="object"){let r=Object.keys(i);console.log("\u{1F50D} [DEBUG] Found componentPropertyDefinitions with keys:",r);for(let a in i){let c=i[a];console.log(`\u{1F50D} [DEBUG] Processing property "${a}":`,c);let l=a,d=[],u="";switch(a.includes("#")&&(l=a.split("#")[0],console.log(`\u{1F50D} [DEBUG] Cleaned display name: "${l}" from "${a}"`)),c.type){case"VARIANT":d=c.variantOptions||[],u=String(c.defaultValue)||d[0]||"default",console.log(`\u{1F50D} [DEBUG] VARIANT property "${l}": values=${d}, default=${u}`);break;case"BOOLEAN":d=["true","false"],u=c.defaultValue?"true":"false",console.log(`\u{1F50D} [DEBUG] BOOLEAN property "${l}": default=${u}`);break;case"TEXT":d=[String(c.defaultValue||"Text content")],u=String(c.defaultValue||"Text content"),console.log(`\u{1F50D} [DEBUG] TEXT property "${l}": value=${u}`);break;case"INSTANCE_SWAP":c.preferredValues&&Array.isArray(c.preferredValues)?d=c.preferredValues.map(f=>(console.log("\u{1F50D} [DEBUG] INSTANCE_SWAP preferred value:",f),f.key||f.name||"Component instance")):d=["Component instance"],u=d[0]||"Component instance",console.log(`\u{1F50D} [DEBUG] INSTANCE_SWAP property "${l}": values=${d}, default=${u}`);break;default:console.log(`\u{1F50D} [DEBUG] Unknown property type "${c.type}" for "${l}"`),d=["Property value"],u="Default"}n.push({name:l,values:d,default:u}),console.log("\u{1F50D} [DEBUG] Added property:",{name:l,values:d,default:u})}}else console.log("\u{1F50D} [DEBUG] componentPropertyDefinitions is not a valid object:",i)}else console.log("\u{1F50D} [DEBUG] componentPropertyDefinitions property does not exist on componentSet")}catch(i){console.error("\u{1F50D} [ERROR] Could not access componentPropertyDefinitions:",i),console.error("\u{1F50D} [ERROR] Error stack:",i instanceof Error?i.stack:"No stack trace")}if(n.length===0){console.log("\u{1F50D} [DEBUG] No properties found, trying variantGroupProperties fallback...");try{let i=o.variantGroupProperties;if(console.log("\u{1F50D} [DEBUG] variantGroupProperties:",i),i){let r=Object.keys(i);console.log("\u{1F50D} [DEBUG] Found variantGroupProperties with keys:",r);for(let a in i){let c=i[a];console.log(`\u{1F50D} [DEBUG] Processing variant property "${a}":`,c),n.push({name:a,values:c.values,default:c.values[0]||"default"})}}else console.log("\u{1F50D} [DEBUG] variantGroupProperties is null/undefined")}catch(i){console.warn("\u{1F50D} [WARN] Component set has errors, cannot access variantGroupProperties:",i)}}if(n.length===0&&o.children.length>0){console.log("\u{1F50D} [DEBUG] Analyzing variant structure to infer properties...");let i=new Map,r=new Map;o.children.forEach((a,c)=>{if(a.type==="COMPONENT"){let l=a.name;console.log(`\u{1F50D} [DEBUG] Analyzing variant ${c}: ${l}`),l.split(",").map(f=>f.trim()).forEach(f=>{let[m,g]=f.split("=").map(p=>p.trim());m&&g&&(i.has(m)||i.set(m,new Set),i.get(m).add(g))});let u=(f,m="")=>{let g=m?`${m}/${f.name}`:f.name;r.has(g)||r.set(g,[]),r.get(g).push(f.visible),"children"in f&&f.children.forEach(p=>u(p,g))};u(a)}}),i.forEach((a,c)=>{n.find(l=>l.name===c)||n.push({name:c,values:Array.from(a),default:Array.from(a)[0]||"default"})}),r.forEach((a,c)=>{let l=a.includes(!0),d=a.includes(!1);if(l&&d){let f=(c.split("/").pop()||"").replace(/\s*(layer|group|frame|icon|text)?\s*/gi,"").trim();f&&!n.find(m=>m.name===f)&&(n.push({name:f,values:["true","false"],default:"false"}),console.log(`\u{1F50D} [DEBUG] Inferred boolean property from visibility: ${f}`))}}),console.log(`\u{1F50D} [DEBUG] Inferred ${n.length} properties from variant analysis`)}if(n.length===0){console.log("\u{1F50D} [DEBUG] All Figma APIs failed, using comprehensive structural analysis...");let i=ga(o);console.log("\u{1F50D} [DEBUG] Properties from structural analysis:",i),n.push(...i)}}else if(e.type==="COMPONENT"){let o=e;console.log("\u{1F50D} [DEBUG] Processing COMPONENT node:",o.name);try{if("componentPropertyDefinitions"in o){let i=o.componentPropertyDefinitions;if(console.log("\u{1F50D} [DEBUG] Component componentPropertyDefinitions:",i),i&&typeof i=="object"){let r=Object.keys(i);console.log("\u{1F50D} [DEBUG] Found componentPropertyDefinitions on component with keys:",r);for(let a in i){let c=i[a],l=a,d=[],u="";switch(a.includes("#")&&(l=a.split("#")[0]),c.type){case"BOOLEAN":d=["true","false"],u=c.defaultValue?"true":"false";break;case"TEXT":d=[String(c.defaultValue||"Text content")],u=String(c.defaultValue||"Text content");break;case"INSTANCE_SWAP":c.preferredValues&&Array.isArray(c.preferredValues)?d=c.preferredValues.map(f=>f.key||f.name||"Component instance"):d=["Component instance"],u=d[0]||"Component instance";break;default:d=["Property value"],u="Default"}n.push({name:l,values:d,default:u})}}}else console.log("\u{1F50D} [DEBUG] componentPropertyDefinitions does not exist on component")}catch(i){console.warn("\u{1F50D} [WARN] Could not access componentPropertyDefinitions on component:",i)}if(o.parent&&o.parent.type==="COMPONENT_SET"){let i=o.parent;console.log("\u{1F50D} [DEBUG] Component is part of a component set, getting variant properties...");try{let r=i.variantGroupProperties;if(r)for(let a in r){let c=r[a];n.find(l=>l.name===a)||n.push({name:a,values:c.values,default:c.values[0]||"default"})}}catch(r){console.warn("\u{1F50D} [WARN] Component set has errors, cannot access variantGroupProperties:",r)}}}else if(e.type==="INSTANCE"){let o=e;if(console.log("\u{1F50D} [DEBUG] Processing INSTANCE node (fallback \u2014 Priority 1 may have been skipped)"),n.length===0)try{let i=await o.getMainComponentAsync();if(i)if(i.parent&&i.parent.type==="COMPONENT_SET"){let r=i.parent;console.log("\u{1F50D} [DEBUG] Instance fallback: extracting from parent component set:",r.name);try{if("componentPropertyDefinitions"in r){let a=r.componentPropertyDefinitions;if(a&&typeof a=="object"){for(let c in a){let l=a[c],d=c,u=[],f="";switch(c.includes("#")&&(d=c.split("#")[0]),l.type){case"VARIANT":u=l.variantOptions||[],f=String(l.defaultValue)||u[0]||"default";break;case"BOOLEAN":u=["true","false"],f=l.defaultValue?"true":"false";break;case"TEXT":u=[String(l.defaultValue||"Text content")],f=String(l.defaultValue||"Text content");break;case"INSTANCE_SWAP":l.preferredValues&&Array.isArray(l.preferredValues)?u=l.preferredValues.map(m=>m.key||m.name||"Component instance"):u=["Component instance"],f=u[0]||"Component instance";break;default:u=["Property value"],f="Default"}n.push({name:d,values:u,default:f})}console.log(`\u{1F50D} [DEBUG] Instance fallback: extracted ${n.length} properties from component set`)}}}catch(a){console.warn("\u{1F50D} [WARN] Instance fallback: could not access componentPropertyDefinitions:",a)}if(n.length===0)try{let a=r.variantGroupProperties;if(a)for(let c in a){let l=a[c];n.find(d=>d.name===c)||n.push({name:c,values:l.values,default:l.values[0]||"default"})}}catch(a){console.warn("\u{1F50D} [WARN] Instance fallback: could not access variantGroupProperties:",a)}}else{console.log("\u{1F50D} [DEBUG] Instance fallback: extracting from standalone main component");try{if("componentPropertyDefinitions"in i){let r=i.componentPropertyDefinitions;if(r&&typeof r=="object"){for(let a in r){let c=r[a],l=a,d=[],u="";switch(a.includes("#")&&(l=a.split("#")[0]),c.type){case"BOOLEAN":d=["true","false"],u=c.defaultValue?"true":"false";break;case"TEXT":d=[String(c.defaultValue||"Text content")],u=String(c.defaultValue||"Text content");break;case"INSTANCE_SWAP":c.preferredValues&&Array.isArray(c.preferredValues)?d=c.preferredValues.map(f=>f.key||f.name||"Component instance"):d=["Component instance"],u=d[0]||"Component instance";break;default:d=["Property value"],u="Default"}n.push({name:l,values:d,default:u})}console.log(`\u{1F50D} [DEBUG] Instance fallback: extracted ${n.length} properties from main component`)}}}catch(r){console.warn("\u{1F50D} [WARN] Instance fallback: could not access componentPropertyDefinitions on main component:",r)}}}catch(i){console.warn("\u{1F50D} [WARN] Instance fallback: could not get main component:",i)}}let s=[];return n.forEach(o=>{s.find(i=>i.name===o.name)||s.push(o)}),console.log(`\u{1F50D} [DEBUG] Final result: Extracted ${s.length} unique properties:`,s.map(o=>({name:o.name,valueCount:o.values.length,default:o.default}))),s}function ga(e){let t=[];console.log("\u{1F50D} [STRUCTURAL] Starting comprehensive structural analysis of component set:",e.name);let n=ma(e);t.push(...n);let s=new Set,o=new Set,i=new Set,r=new Set;e.children.forEach(l=>{if(l.type==="COMPONENT"){console.log(`\u{1F50D} [STRUCTURAL] Analyzing variant: ${l.name}`);let d=(u,f=0)=>{let m=" ".repeat(f);console.log(`\u{1F50D} [STRUCTURAL] ${m}Found child: ${u.name} (type: ${u.type})`),s.add(u.name),u.type==="TEXT"?o.add(u.name):u.type==="INSTANCE"&&i.add(u.name),(u.visible===!1||u.name.toLowerCase().includes("hidden"))&&r.add(u.name),"children"in u&&u.children&&u.children.forEach(g=>d(g,f+1))};d(l)}}),console.log("\u{1F50D} [STRUCTURAL] Analysis results:"),console.log("\u{1F50D} [STRUCTURAL] - All child names:",Array.from(s)),console.log("\u{1F50D} [STRUCTURAL] - Text layers:",Array.from(o)),console.log("\u{1F50D} [STRUCTURAL] - Instance layers:",Array.from(i)),console.log("\u{1F50D} [STRUCTURAL] - Boolean indicators:",Array.from(r)),o.forEach(l=>{let d=l.replace(/\s*(layer|text|label)?\s*/gi,"").trim();d&&!t.find(u=>u.name.toLowerCase()===d.toLowerCase())&&(t.push({name:d,values:["Text content"],default:"Label"}),console.log(`\u{1F50D} [STRUCTURAL] Added TEXT property: ${d}`))}),i.forEach(l=>{let d=l.replace(/\s*(layer|instance)?\s*/gi,"").trim();d&&!t.find(u=>u.name.toLowerCase()===d.toLowerCase())&&(t.push({name:d,values:["Component instance"],default:"Default component"}),console.log(`\u{1F50D} [STRUCTURAL] Added INSTANCE_SWAP property: ${d}`))}),["icon before","icon after","slot before","slot after","before","after","prefix","suffix","leading","trailing"].forEach(l=>{if(Array.from(s).find(u=>u.toLowerCase().includes(l.toLowerCase()))&&!t.find(u=>u.name.toLowerCase().includes(l.toLowerCase()))){let u=l.split(" ").map(f=>f.charAt(0).toUpperCase()+f.slice(1)).join(" ");t.push({name:u,values:["true","false"],default:"false"}),console.log(`\u{1F50D} [STRUCTURAL] Added BOOLEAN property: ${u}`)}});let c=e.name.toLowerCase();return(c.includes("button")||c.includes("btn"))&&[{name:"Slot Before",type:"BOOLEAN"},{name:"Text",type:"TEXT"},{name:"Icon Before",type:"INSTANCE_SWAP"},{name:"Icon After",type:"INSTANCE_SWAP"}].forEach(({name:d,type:u})=>{if(!t.find(f=>f.name.toLowerCase()===d.toLowerCase())){let f,m;switch(u){case"BOOLEAN":f=["true","false"],m="false";break;case"TEXT":f=["Text content"],m="Label";break;case"INSTANCE_SWAP":f=["Component instance"],m="Default icon";break;default:f=["Property value"],m="Default"}t.push({name:d,values:f,default:m}),console.log(`\u{1F50D} [STRUCTURAL] Added common ${u} property: ${d}`)}}),console.log(`\u{1F50D} [STRUCTURAL] Final structural analysis result: ${t.length} properties found`),t}async function Ze(e){let t=[];if(e.type==="COMPONENT_SET"){let s=e,o;try{o=s.variantGroupProperties}catch(i){console.warn("Component set has errors, cannot access variantGroupProperties:",i),o=void 0}if(o)for(let i in o){let r=i.toLowerCase();(r==="state"||r==="states"||r==="status")&&t.push(...o[i].values)}s.children.forEach(i=>{let r=i.name.toLowerCase();["default","hover","focus","disabled","pressed","active","selected"].forEach(a=>{let c=t.find(l=>l.toLowerCase()===a.toLowerCase());r.includes(a)&&!c&&t.push(a)})})}else if(e.type==="COMPONENT"){let s=e;if(s.parent&&s.parent.type==="COMPONENT_SET")return await Ze(s.parent)}else if(e.type==="INSTANCE"){let o=await e.getMainComponentAsync();if(o)return await Ze(o)}let n=[];return t.forEach(s=>{s&&typeof s=="string"&&s.trim()!==""&&(n.find(i=>i.toLowerCase()===s.toLowerCase())||n.push(s.trim()))}),n}async function to(e,t,n,s={},o="anthropic"){console.log("\u{1F3AF} Starting enhanced component analysis...");let i=figma.currentPage.selection[0],r=s.node||i;if(!r)throw new Error("No node selected");let a=await eo(r,i),c=await Ze(r),l=await ge(r),d="";if(r.type==="COMPONENT"||r.type==="COMPONENT_SET")d=r.description||"";else if(r.type==="INSTANCE"){let C=await r.getMainComponentAsync();C&&(d=C.description||"")}e.existingDescription=d;let u=se([r],s.lintSettings||K);console.log(`\u{1F50D} [LINT] Deterministic lint: ${u.summary.totalErrors} issues in ${u.summary.nodesWithErrors} nodes`),console.log("\u{1F4CA} [ANALYSIS] Extracted from Figma API:"),console.log(` Properties: ${a.length}`),console.log(` States: ${c.length}`),console.log(` Tokens: ${Object.keys(l).length} categories`),console.log(` Description: ${d?"Present":"Missing"}`);let f=s.mcpServerUrl||"http://localhost:3000/mcp",m=s.useMCP!==!1&&f,g;if(m){console.log(`\u{1F504} Using hybrid LLM + MCP approach (${o})...`);let h=ya(e,a,c,l,d,u),C=await le(o,t,{prompt:h,model:n,maxTokens:2048,temperature:.1}),k=ye(C.content);if(!k)throw new Error("Failed to extract JSON from LLM response");let N=null;try{N=await ha(e,f,k),console.log("\u2705 MCP enhancements received")}catch(y){console.warn("\u26A0\uFE0F MCP enhancement failed, continuing with LLM data only:",y)}g=ba(k,N,{node:r,context:e,actualProperties:a,actualStates:c,tokens:l,componentDescription:d})}else{console.log(`\u{1F4DD} Using ${o}-only analysis...`);let h=In(e),C=await le(o,t,{prompt:h,model:n,maxTokens:2048,temperature:.1});if(g=ye(C.content),!g)throw new Error("Failed to extract JSON from response")}let p=je(g);return await Xt(p,e,s,u,t,n,o)}function ya(e,t,n,s,o,i){var l;let r=((l=e.additionalContext)==null?void 0:l.componentFamily)||"generic",a=bt(e.hierarchy),c="";if(i&&i.summary.totalErrors>0){let d=i.summary.byType,u=i.errors.slice(0,15).map(f=>` - [${f.errorType.toUpperCase()}] ${f.nodeName}: ${f.message}`).join(` `);c=` -**Design Lint Findings (${r.summary.totalErrors} issues):** -- Missing fill styles: ${l.fill||0} -- Missing stroke styles: ${l.stroke||0} -- Missing effect styles: ${l.effect||0} -- Missing text styles: ${l.text||0} -- Non-standard border radius: ${l.radius||0} -- Off-grid spacing: ${l.spacing||0} -- Missing auto-layout: ${l.autoLayout||0} +**Design Lint Findings (${i.summary.totalErrors} issues):** +- Missing fill styles: ${d.fill||0} +- Missing stroke styles: ${d.stroke||0} +- Missing effect styles: ${d.effect||0} +- Missing text styles: ${d.text||0} +- Non-standard border radius: ${d.radius||0} +- Off-grid spacing: ${d.spacing||0} +- Missing auto-layout: ${d.autoLayout||0} Top issues: -${p} +${u} `}else c=` **Design Lint Findings:** All layers use proper design styles. No issues found. `;return`Analyze this Figma component and extract its structure and patterns. @@ -227,12 +227,12 @@ ${p} **Component Details:** - Name: ${e.name} - Type: ${e.type} -- Family: ${i} +- Family: ${r} - Existing Figma Description: ${o||"None set"} - Nested Component Instances: ${a.length>0?a.join(", "):"None detected"} **Actual Figma Properties (${t.length} total):** -${t.slice(0,10).map(l=>`- ${l.name}: ${l.values.join(", ")} (default: ${l.default})`).join(` +${t.slice(0,10).map(d=>`- ${d.name}: ${d.values.join(", ")} (default: ${d.default})`).join(` `)} ${t.length>10?`... and ${t.length-10} more properties`:""} @@ -294,21 +294,21 @@ Return JSON in this exact format: For "recommendedProperties": Compare the EXISTING properties listed above against design system best practices (Material Design, Carbon, Ant Design, Polaris, etc.). Only recommend Figma component properties that do NOT already exist. Use Figma property types (VARIANT, BOOLEAN, TEXT, INSTANCE_SWAP). If the component already has comprehensive properties, return an empty array. -Focus ONLY on what's actually in the Figma component for existing data. Recommendations should draw from your knowledge of design system best practices.`}async function wi(e,t,n){var o,r;let s=((o=e.additionalContext)==null?void 0:o.componentFamily)||((r=n.component)==null?void 0:r.toLowerCase())||"generic";try{let[i,a,c]=await Promise.all([Tt(t,"search_design_knowledge",{query:`${s} component essential properties states variants`,category:"components",limit:2},3e3),Tt(t,"search_design_knowledge",{query:`design tokens ${s} semantic naming`,category:"tokens",limit:2},3e3),Tt(t,"search_chunks",{query:`component assessment scoring criteria ${s}`,limit:1},3e3)]);return{bestPractices:(i==null?void 0:i.entries)||[],tokenGuidance:(a==null?void 0:a.entries)||[],scoringCriteria:(c==null?void 0:c.chunks)||[],success:!0}}catch(i){return console.warn("\u26A0\uFE0F MCP queries failed:",i),{bestPractices:[],tokenGuidance:[],scoringCriteria:[],success:!1,error:i instanceof Error?i.message:"Unknown error"}}}async function Tt(e,t,n,s=5e3){var i,a;let o=new AbortController,r=setTimeout(()=>o.abort(),s);try{let c={jsonrpc:"2.0",id:Math.floor(Math.random()*1e3)+100,method:"tools/call",params:{name:`mcp_design-systems_${t}`,arguments:n}},d=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c),signal:o.signal});if(clearTimeout(r),!d.ok)throw new Error(`MCP ${t} failed: ${d.status}`);return((a=(i=(await d.json()).result)==null?void 0:i.content)==null?void 0:a[0])||{}}catch(c){throw clearTimeout(r),c instanceof Error&&c.name==="AbortError"?new Error(`MCP ${t} timeout after ${s}ms`):c}}function Ci(e,t,n){var o;let s=R({},e);return s.propertyCheatSheet=xi(n.actualProperties,e.component||n.context.name),s.audit={designIssues:[],tokenOpportunities:[],structureIssues:[]},(!n.componentDescription||n.componentDescription.trim().length===0)&&s.audit.structureIssues.push("Component lacks description - Add a description in component properties to help MCP and AI understand the component's purpose and usage"),t!=null&&t.success?s.mcpReadiness=Ii(t,e,n):s.mcpReadiness=Fs(n),s.component=s.component||n.context.name,s.description=s.description||`${((o=n.context.additionalContext)==null?void 0:o.componentFamily)||"Component"} with ${n.actualProperties.length} properties`,s.props=s.props||n.actualProperties.map(r=>({name:r.name,type:"select",description:`Controls ${r.name}`,values:r.values,default:r.default})),s.states=s.states||n.actualStates,s.recommendedProperties=e.recommendedProperties||[],s}function Ii(e,t,n){var l,p;let s=[],o=[],r=[];((l=e.bestPractices)==null?void 0:l.length)>0&&e.bestPractices.forEach(u=>{var g,f;((g=u.title)!=null&&g.includes("best practice")||(f=u.title)!=null&&f.includes("pattern"))&&r.push(`Follow ${u.title}`)});let i=n.actualStates.length>=3,a=n.tokens.summary&&n.tokens.summary.actualTokens>n.tokens.summary.hardCodedValues,c=((p=t.structure)==null?void 0:p.complexity)!=="high";return i?s.push("Component has comprehensive states"):o.push("Missing interactive states"),a?s.push("Good token usage"):o.push("Improve token adoption"),c?s.push("Well-structured component"):o.push("Complex structure may need simplification"),{score:Math.round((i?35:15)+(a?35:15)+(c?30:20)),strengths:s,gaps:o,recommendations:r.slice(0,3)}}function xi(e,t){let n=[],s=e.filter(c=>c.name.toLowerCase().includes("size")||c.values.some(d=>["small","medium","large"].includes(d.toLowerCase()))),o=e.filter(c=>c.name.toLowerCase().includes("variant")||c.name.toLowerCase().includes("type")),r=e.filter(c=>c.name.toLowerCase().includes("state")||c.values.some(d=>["hover","active","disabled"].includes(d.toLowerCase())));s.length>0&&n.push(`\u{1F4CF} Sizes: ${s.map(c=>c.values.join("/")).join(", ")}`),o.length>0&&n.push(`\u{1F3A8} Variants: ${o.map(c=>`${c.name}(${c.values.length})`).join(", ")}`),r.length>0&&n.push(`\u{1F504} States: ${r.map(c=>c.values.join("/")).join(", ")}`);let i=new Set([...s,...o,...r].map(c=>c.name)),a=e.filter(c=>!i.has(c.name)).slice(0,3).map(c=>`${c.name}: ${c.values.slice(0,3).join("/")}`);return a.length>0&&n.push(`\u2699\uFE0F Other: ${a.join(", ")}`),n.slice(0,5)}async function Rt(e,t,n,s,o,r,i){var a;try{console.log("\u{1F504} Processing analysis result..."),console.log("\u{1F4CA} Filtered data received:",JSON.stringify(e,null,2).substring(0,500)+"...");let c=figma.currentPage.selection,d=null;if(c.length>0)d=c[0];else throw new Error("No component selected");let l=await Ms(d,d),p=await Ge(d),u="";if(d.type==="COMPONENT"||d.type==="COMPONENT_SET")u=d.description||"";else if(d.type==="INSTANCE"){let y=await d.getMainComponentAsync();y&&(u=y.description||"")}let g={colors:[],spacing:[],typography:[],effects:[],borders:[],summary:{totalTokens:0,actualTokens:0,hardCodedValues:0,aiSuggestions:0,byCategory:{}}};n.includeTokenAnalysis!==!1&&(g=await ue(d));let f={component:e.component||t.name||"Component",description:e.description||`A ${t.type} component with ${l.length} properties`,props:e.props&&e.props.length>0?e.props:l.map(N=>({name:N.name,type:"select",description:`Controls ${N.name}`,values:N.values,defaultValue:N.default,required:!1})),states:e.states&&e.states.length>0?e.states.map(N=>typeof N=="string"?N:N.name):p.length>0?p:["default"],variants:e.variants||{},slots:e.slots||[],tokens:e.tokens||{colors:g.colors.filter(N=>N.isActualToken).map(N=>N.name),spacing:g.spacing.filter(N=>N.isActualToken).map(N=>N.name),typography:g.typography.filter(N=>N.isActualToken).map(N=>N.name)},usage:e.usage||"General purpose component for design systems",accessibility:e.accessibility||{keyboardNavigation:"Standard keyboard navigation support",screenReader:"Screen reader accessible",colorContrast:"WCAG compliant contrast ratios"},audit:e.audit||{accessibilityIssues:[],namingIssues:[],consistencyIssues:[],tokenOpportunities:[]},propertyCheatSheet:e.propertyCheatSheet||l.map(N=>({name:N.name,values:N.values,default:N.default,description:`Property for ${N.name} configuration`})),mcpReadiness:e.mcpReadiness||Fs({node:d,context:t,actualProperties:l,actualStates:p,tokens:g,componentDescription:u})};console.log("\u{1F4E4} Sending to UI - metadata.props:",(a=f.props)==null?void 0:a.length),console.log("\u{1F4E4} Sending to UI - metadata.states:",f.states),console.log("\u{1F4E4} Sending to UI - metadata.mcpReadiness:",f.mcpReadiness);let m=await Ti(e,t,d,l,p,g,u),h=(e.recommendedProperties||[]).map(N=>({name:N.name||"",type:N.type||"VARIANT",description:N.description||"",examples:N.examples||[]})).filter(N=>N.name);console.log(`\u{1F4A1} AI-generated property recommendations: ${h.length}`);let C=pn(d,5);console.log(`\u{1F4DB} Found ${C.length} naming issues`),s&&s.errors.length>0&&(m.designLint=Ai(s));let k;if(o&&r&&i)try{k=await Ei(t,s,m,g,C,h,o,r,i),console.log(`\u{1F4CB} Design review generated: ${k.verdict} \u2014 ${k.findings.length} findings`)}catch(N){console.warn("\u26A0\uFE0F Design review generation failed, continuing without it:",N),k=Pt(s,m,g,C)}else k=Pt(s,m,g,C);return console.log("\u2705 Analysis result processed successfully"),{metadata:f,tokens:g,audit:m,properties:l,recommendations:h,namingIssues:C,existingDescription:u,lintResult:s,designReview:k}}catch(c){throw console.error("Error processing analysis result:",c),c}}function Ai(e){let t=[],n=e.summary.byType;return n.fill>0?t.push({check:`Fill styles (${n.fill} missing)`,status:"fail",suggestion:`${n.fill} layer${n.fill>1?"s use":" uses"} hard-coded fills instead of design styles`}):t.push({check:"Fill styles",status:"pass",suggestion:"All fills use design styles"}),n.stroke>0?t.push({check:`Stroke styles (${n.stroke} missing)`,status:"fail",suggestion:`${n.stroke} layer${n.stroke>1?"s use":" uses"} hard-coded strokes instead of design styles`}):t.push({check:"Stroke styles",status:"pass",suggestion:"All strokes use design styles"}),n.effect>0?t.push({check:`Effect styles (${n.effect} missing)`,status:"fail",suggestion:`${n.effect} layer${n.effect>1?"s use":" uses"} hard-coded effects instead of design styles`}):t.push({check:"Effect styles",status:"pass",suggestion:"All effects use design styles"}),n.text>0?t.push({check:`Text styles (${n.text} missing)`,status:"fail",suggestion:`${n.text} text layer${n.text>1?"s lack":" lacks"} applied text styles`}):t.push({check:"Text styles",status:"pass",suggestion:"All text uses design styles"}),n.radius>0?t.push({check:`Border radius (${n.radius} non-standard)`,status:"warning",suggestion:`${n.radius} layer${n.radius>1?"s use":" uses"} non-standard border radius values`}):t.push({check:"Border radius",status:"pass",suggestion:"All radii match design system standards"}),n.spacing>0?t.push({check:`Spacing rhythm (${n.spacing} off-grid)`,status:"warning",suggestion:`${n.spacing} spacing value${n.spacing>1?"s are":" is"} not on the 4/8px grid`}):t.push({check:"Spacing rhythm",status:"pass",suggestion:"All spacing values follow the design grid"}),n.autoLayout>0?t.push({check:`Auto Layout (${n.autoLayout} missing)`,status:"warning",suggestion:`${n.autoLayout} frame${n.autoLayout>1?"s lack":" lacks"} auto-layout`}):t.push({check:"Auto Layout",status:"pass",suggestion:"All container frames use auto-layout"}),t}function Pt(e,t,n,s){let o=[];if(e)for(let u of e.errors)o.push({severity:u.errorType==="radius"||u.errorType==="spacing"||u.errorType==="autoLayout"?"warning":"critical",category:"Style Consistency",title:u.message,description:`Layer "${u.nodeName}" (${u.nodeType}) at ${u.path}`,nodeId:u.nodeId,nodeName:u.nodeName,autoFixable:!1});let r=n.summary.hardCodedValues;r>0&&o.push({severity:"warning",category:"Design Tokens",title:`${r} hard-coded value${r>1?"s":""} found`,description:"These values should be replaced with design tokens for consistency across the design system.",autoFixable:!0});for(let u of t.accessibility||[])u.status==="fail"?o.push({severity:"critical",category:"Accessibility",title:u.check,description:u.suggestion,autoFixable:!1}):u.status==="warning"&&o.push({severity:"warning",category:"Accessibility",title:u.check,description:u.suggestion,autoFixable:!1});for(let u of s.slice(0,10))o.push({severity:u.severity==="error"?"warning":"info",category:"Naming",title:`"${u.currentName}" should be "${u.suggestedName}"`,description:u.reason,nodeId:u.nodeId,nodeName:u.currentName,autoFixable:!0});for(let u of t.componentReadiness||[])u.status==="fail"&&o.push({severity:"suggestion",category:"Component Readiness",title:u.check,description:u.suggestion,autoFixable:!1});let i=(t.states||[]).filter(u=>!u.found);i.length>0&&o.push({severity:"suggestion",category:"Interactive States",title:`${i.length} state${i.length>1?"s":""} not detected`,description:`Missing: ${i.map(u=>u.name).join(", ")}`,autoFixable:!1});let a=o.filter(u=>u.severity==="critical").length,c=o.filter(u=>u.severity==="warning").length,d=a>0?"fail":c>3?"warn":"pass",l;d==="pass"?l="Component follows design system conventions well.":d==="warn"?l=`${c} issues need attention before this component is production-ready.`:l=`${a} critical issue${a>1?"s":""} found \u2014 missing design styles affect consistency.`;let p=[];return e&&e.summary.byType.fill>0&&p.push("Apply fill styles to layers using hard-coded colors"),e&&e.summary.byType.text>0&&p.push("Apply text styles to text layers"),e&&e.summary.byType.stroke>0&&p.push("Apply stroke styles to layers with hard-coded strokes"),e&&e.summary.byType.spacing>0&&p.push("Fix off-grid spacing values to match the 4/8px grid"),e&&e.summary.byType.autoLayout>0&&p.push("Apply auto-layout to container frames"),r>0&&p.push("Replace hard-coded values with design tokens"),s.length>0&&p.push("Rename generic layers to semantic names"),i.length>0&&p.push(`Add missing states: ${i.map(u=>u.name).join(", ")}`),p.length===0&&p.push("Component looks great \u2014 consider documenting it for the team"),{verdict:d,headline:l,findings:o,nextSteps:p}}async function Ei(e,t,n,s,o,r,i,a,c){var C;let d=t?`${t.summary.totalErrors} lint issues (${t.summary.byType.fill} fills, ${t.summary.byType.stroke} strokes, ${t.summary.byType.effect} effects, ${t.summary.byType.text} text, ${t.summary.byType.radius} radius, ${t.summary.byType.spacing||0} spacing, ${t.summary.byType.autoLayout||0} auto-layout)`:"0 lint issues",l=(n.accessibility||[]).filter(k=>k.status==="fail").length,p=(n.componentReadiness||[]).filter(k=>k.status==="fail").length,u=(n.states||[]).filter(k=>!k.found),g=t?t.errors.slice(0,8).map(k=>`- [${k.errorType}] ${k.nodeName}: ${k.message}`).join(` -`):"None",f=`You are a design system reviewer (like CodeRabbit but for Figma designs). Review this component and produce a structured JSON design review. +Focus ONLY on what's actually in the Figma component for existing data. Recommendations should draw from your knowledge of design system best practices.`}async function ha(e,t,n){var o,i;let s=((o=e.additionalContext)==null?void 0:o.componentFamily)||((i=n.component)==null?void 0:i.toLowerCase())||"generic";try{let[r,a,c]=await Promise.all([jt(t,"search_design_knowledge",{query:`${s} component essential properties states variants`,category:"components",limit:2},3e3),jt(t,"search_design_knowledge",{query:`design tokens ${s} semantic naming`,category:"tokens",limit:2},3e3),jt(t,"search_chunks",{query:`component assessment scoring criteria ${s}`,limit:1},3e3)]);return{bestPractices:(r==null?void 0:r.entries)||[],tokenGuidance:(a==null?void 0:a.entries)||[],scoringCriteria:(c==null?void 0:c.chunks)||[],success:!0}}catch(r){return console.warn("\u26A0\uFE0F MCP queries failed:",r),{bestPractices:[],tokenGuidance:[],scoringCriteria:[],success:!1,error:r instanceof Error?r.message:"Unknown error"}}}async function jt(e,t,n,s=5e3){var r,a;let o=new AbortController,i=setTimeout(()=>o.abort(),s);try{let c={jsonrpc:"2.0",id:Math.floor(Math.random()*1e3)+100,method:"tools/call",params:{name:`mcp_design-systems_${t}`,arguments:n}},l=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c),signal:o.signal});if(clearTimeout(i),!l.ok)throw new Error(`MCP ${t} failed: ${l.status}`);return((a=(r=(await l.json()).result)==null?void 0:r.content)==null?void 0:a[0])||{}}catch(c){throw clearTimeout(i),c instanceof Error&&c.name==="AbortError"?new Error(`MCP ${t} timeout after ${s}ms`):c}}function ba(e,t,n){var o;let s=R({},e);return s.propertyCheatSheet=va(n.actualProperties,e.component||n.context.name),s.audit={designIssues:[],tokenOpportunities:[],structureIssues:[]},(!n.componentDescription||n.componentDescription.trim().length===0)&&s.audit.structureIssues.push("Component lacks description - Add a description in component properties to help MCP and AI understand the component's purpose and usage"),t!=null&&t.success?s.mcpReadiness=Sa(t,e,n):s.mcpReadiness=no(n),s.component=s.component||n.context.name,s.description=s.description||`${((o=n.context.additionalContext)==null?void 0:o.componentFamily)||"Component"} with ${n.actualProperties.length} properties`,s.props=s.props||n.actualProperties.map(i=>({name:i.name,type:"select",description:`Controls ${i.name}`,values:i.values,default:i.default})),s.states=s.states||n.actualStates,s.recommendedProperties=e.recommendedProperties||[],s}function Sa(e,t,n){var d,u;let s=[],o=[],i=[];((d=e.bestPractices)==null?void 0:d.length)>0&&e.bestPractices.forEach(f=>{var m,g;((m=f.title)!=null&&m.includes("best practice")||(g=f.title)!=null&&g.includes("pattern"))&&i.push(`Follow ${f.title}`)});let r=n.actualStates.length>=3,a=n.tokens.summary&&n.tokens.summary.actualTokens>n.tokens.summary.hardCodedValues,c=((u=t.structure)==null?void 0:u.complexity)!=="high";return r?s.push("Component has comprehensive states"):o.push("Missing interactive states"),a?s.push("Good token usage"):o.push("Improve token adoption"),c?s.push("Well-structured component"):o.push("Complex structure may need simplification"),{score:Math.round((r?35:15)+(a?35:15)+(c?30:20)),strengths:s,gaps:o,recommendations:i.slice(0,3)}}function va(e,t){let n=[],s=e.filter(c=>c.name.toLowerCase().includes("size")||c.values.some(l=>["small","medium","large"].includes(l.toLowerCase()))),o=e.filter(c=>c.name.toLowerCase().includes("variant")||c.name.toLowerCase().includes("type")),i=e.filter(c=>c.name.toLowerCase().includes("state")||c.values.some(l=>["hover","active","disabled"].includes(l.toLowerCase())));s.length>0&&n.push(`\u{1F4CF} Sizes: ${s.map(c=>c.values.join("/")).join(", ")}`),o.length>0&&n.push(`\u{1F3A8} Variants: ${o.map(c=>`${c.name}(${c.values.length})`).join(", ")}`),i.length>0&&n.push(`\u{1F504} States: ${i.map(c=>c.values.join("/")).join(", ")}`);let r=new Set([...s,...o,...i].map(c=>c.name)),a=e.filter(c=>!r.has(c.name)).slice(0,3).map(c=>`${c.name}: ${c.values.slice(0,3).join("/")}`);return a.length>0&&n.push(`\u2699\uFE0F Other: ${a.join(", ")}`),n.slice(0,5)}async function Xt(e,t,n,s,o,i,r){var a;try{console.log("\u{1F504} Processing analysis result..."),console.log("\u{1F4CA} Filtered data received:",JSON.stringify(e,null,2).substring(0,500)+"...");let c=figma.currentPage.selection,l=null;if(c.length>0)l=c[0];else throw new Error("No component selected");let d=await eo(l,l),u=await Ze(l),f="";if(l.type==="COMPONENT"||l.type==="COMPONENT_SET")f=l.description||"";else if(l.type==="INSTANCE"){let y=await l.getMainComponentAsync();y&&(f=y.description||"")}let m={colors:[],spacing:[],typography:[],effects:[],borders:[],summary:{totalTokens:0,actualTokens:0,hardCodedValues:0,aiSuggestions:0,byCategory:{}}};n.includeTokenAnalysis!==!1&&(m=await ge(l));let g={component:e.component||t.name||"Component",description:e.description||`A ${t.type} component with ${d.length} properties`,props:e.props&&e.props.length>0?e.props:d.map(N=>({name:N.name,type:"select",description:`Controls ${N.name}`,values:N.values,defaultValue:N.default,required:!1})),states:e.states&&e.states.length>0?e.states.map(N=>typeof N=="string"?N:N.name):u.length>0?u:["default"],variants:e.variants||{},slots:e.slots||[],tokens:e.tokens||{colors:m.colors.filter(N=>N.isActualToken).map(N=>N.name),spacing:m.spacing.filter(N=>N.isActualToken).map(N=>N.name),typography:m.typography.filter(N=>N.isActualToken).map(N=>N.name)},usage:e.usage||"General purpose component for design systems",accessibility:e.accessibility||{keyboardNavigation:"Standard keyboard navigation support",screenReader:"Screen reader accessible",colorContrast:"WCAG compliant contrast ratios"},audit:e.audit||{accessibilityIssues:[],namingIssues:[],consistencyIssues:[],tokenOpportunities:[]},propertyCheatSheet:e.propertyCheatSheet||d.map(N=>({name:N.name,values:N.values,default:N.default,description:`Property for ${N.name} configuration`})),mcpReadiness:e.mcpReadiness||no({node:l,context:t,actualProperties:d,actualStates:u,tokens:m,componentDescription:f})};console.log("\u{1F4E4} Sending to UI - metadata.props:",(a=g.props)==null?void 0:a.length),console.log("\u{1F4E4} Sending to UI - metadata.states:",g.states),console.log("\u{1F4E4} Sending to UI - metadata.mcpReadiness:",g.mcpReadiness);let p=await Ia(e,t,l,d,u,m,f),h=(e.recommendedProperties||[]).map(N=>({name:N.name||"",type:N.type||"VARIANT",description:N.description||"",examples:N.examples||[]})).filter(N=>N.name);console.log(`\u{1F4A1} AI-generated property recommendations: ${h.length}`);let C=Rn(l,5);console.log(`\u{1F4DB} Found ${C.length} naming issues`),s&&s.errors.length>0&&(p.designLint=ka(s));let k;if(o&&i&&r)try{k=await Na(t,s,p,m,C,h,o,i,r),console.log(`\u{1F4CB} Design review generated: ${k.verdict} \u2014 ${k.findings.length} findings`)}catch(N){console.warn("\u26A0\uFE0F Design review generation failed, continuing without it:",N),k=Kt(s,p,m,C)}else k=Kt(s,p,m,C);return console.log("\u2705 Analysis result processed successfully"),{metadata:g,tokens:m,audit:p,properties:d,recommendations:h,namingIssues:C,existingDescription:f,lintResult:s,designReview:k}}catch(c){throw console.error("Error processing analysis result:",c),c}}function ka(e){let t=[],n=e.summary.byType;return n.fill>0?t.push({check:`Fill styles (${n.fill} missing)`,status:"fail",suggestion:`${n.fill} layer${n.fill>1?"s use":" uses"} hard-coded fills instead of design styles`}):t.push({check:"Fill styles",status:"pass",suggestion:"All fills use design styles"}),n.stroke>0?t.push({check:`Stroke styles (${n.stroke} missing)`,status:"fail",suggestion:`${n.stroke} layer${n.stroke>1?"s use":" uses"} hard-coded strokes instead of design styles`}):t.push({check:"Stroke styles",status:"pass",suggestion:"All strokes use design styles"}),n.effect>0?t.push({check:`Effect styles (${n.effect} missing)`,status:"fail",suggestion:`${n.effect} layer${n.effect>1?"s use":" uses"} hard-coded effects instead of design styles`}):t.push({check:"Effect styles",status:"pass",suggestion:"All effects use design styles"}),n.text>0?t.push({check:`Text styles (${n.text} missing)`,status:"fail",suggestion:`${n.text} text layer${n.text>1?"s lack":" lacks"} applied text styles`}):t.push({check:"Text styles",status:"pass",suggestion:"All text uses design styles"}),n.radius>0?t.push({check:`Border radius (${n.radius} non-standard)`,status:"warning",suggestion:`${n.radius} layer${n.radius>1?"s use":" uses"} non-standard border radius values`}):t.push({check:"Border radius",status:"pass",suggestion:"All radii match design system standards"}),n.spacing>0?t.push({check:`Spacing rhythm (${n.spacing} off-grid)`,status:"warning",suggestion:`${n.spacing} spacing value${n.spacing>1?"s are":" is"} not on the 4/8px grid`}):t.push({check:"Spacing rhythm",status:"pass",suggestion:"All spacing values follow the design grid"}),n.autoLayout>0?t.push({check:`Auto Layout (${n.autoLayout} missing)`,status:"warning",suggestion:`${n.autoLayout} frame${n.autoLayout>1?"s lack":" lacks"} auto-layout`}):t.push({check:"Auto Layout",status:"pass",suggestion:"All container frames use auto-layout"}),t}function Kt(e,t,n,s){let o=[];if(e)for(let f of e.errors)o.push({severity:f.errorType==="radius"||f.errorType==="spacing"||f.errorType==="autoLayout"?"warning":"critical",category:"Style Consistency",title:f.message,description:`Layer "${f.nodeName}" (${f.nodeType}) at ${f.path}`,nodeId:f.nodeId,nodeName:f.nodeName,autoFixable:!1});let i=n.summary.hardCodedValues;i>0&&o.push({severity:"warning",category:"Design Tokens",title:`${i} hard-coded value${i>1?"s":""} found`,description:"These values should be replaced with design tokens for consistency across the design system.",autoFixable:!0});for(let f of t.accessibility||[])f.status==="fail"?o.push({severity:"critical",category:"Accessibility",title:f.check,description:f.suggestion,autoFixable:!1}):f.status==="warning"&&o.push({severity:"warning",category:"Accessibility",title:f.check,description:f.suggestion,autoFixable:!1});for(let f of s.slice(0,10))o.push({severity:f.severity==="error"?"warning":"info",category:"Naming",title:`"${f.currentName}" should be "${f.suggestedName}"`,description:f.reason,nodeId:f.nodeId,nodeName:f.currentName,autoFixable:!0});for(let f of t.componentReadiness||[])f.status==="fail"&&o.push({severity:"suggestion",category:"Component Readiness",title:f.check,description:f.suggestion,autoFixable:!1});let r=(t.states||[]).filter(f=>!f.found);r.length>0&&o.push({severity:"suggestion",category:"Interactive States",title:`${r.length} state${r.length>1?"s":""} not detected`,description:`Missing: ${r.map(f=>f.name).join(", ")}`,autoFixable:!1});let a=o.filter(f=>f.severity==="critical").length,c=o.filter(f=>f.severity==="warning").length,l=a>0?"fail":c>3?"warn":"pass",d;l==="pass"?d="Component follows design system conventions well.":l==="warn"?d=`${c} issues need attention before this component is production-ready.`:d=`${a} critical issue${a>1?"s":""} found \u2014 missing design styles affect consistency.`;let u=[];return e&&e.summary.byType.fill>0&&u.push("Apply fill styles to layers using hard-coded colors"),e&&e.summary.byType.text>0&&u.push("Apply text styles to text layers"),e&&e.summary.byType.stroke>0&&u.push("Apply stroke styles to layers with hard-coded strokes"),e&&e.summary.byType.spacing>0&&u.push("Fix off-grid spacing values to match the 4/8px grid"),e&&e.summary.byType.autoLayout>0&&u.push("Apply auto-layout to container frames"),i>0&&u.push("Replace hard-coded values with design tokens"),s.length>0&&u.push("Rename generic layers to semantic names"),r.length>0&&u.push(`Add missing states: ${r.map(f=>f.name).join(", ")}`),u.length===0&&u.push("Component looks great \u2014 consider documenting it for the team"),{verdict:l,headline:d,findings:o,nextSteps:u}}async function Na(e,t,n,s,o,i,r,a,c){var C;let l=t?`${t.summary.totalErrors} lint issues (${t.summary.byType.fill} fills, ${t.summary.byType.stroke} strokes, ${t.summary.byType.effect} effects, ${t.summary.byType.text} text, ${t.summary.byType.radius} radius, ${t.summary.byType.spacing||0} spacing, ${t.summary.byType.autoLayout||0} auto-layout)`:"0 lint issues",d=(n.accessibility||[]).filter(k=>k.status==="fail").length,u=(n.componentReadiness||[]).filter(k=>k.status==="fail").length,f=(n.states||[]).filter(k=>!k.found),m=t?t.errors.slice(0,8).map(k=>`- [${k.errorType}] ${k.nodeName}: ${k.message}`).join(` +`):"None",g=`You are a design system reviewer (like CodeRabbit but for Figma designs). Review this component and produce a structured JSON design review. **Component:** ${e.name} (${e.type}, family: ${((C=e.additionalContext)==null?void 0:C.componentFamily)||"generic"}) -**Deterministic Lint Results:** ${d} -${g!=="None"?`Top issues: -${g}`:""} +**Deterministic Lint Results:** ${l} +${m!=="None"?`Top issues: +${m}`:""} **Token Usage:** ${s.summary.actualTokens} tokens used, ${s.summary.hardCodedValues} hard-coded values -**Accessibility Failures:** ${l} -**Component Readiness Failures:** ${p} -**Missing States:** ${u.map(k=>k.name).join(", ")||"None"} +**Accessibility Failures:** ${d} +**Component Readiness Failures:** ${u} +**Missing States:** ${f.map(k=>k.name).join(", ")||"None"} **Naming Issues:** ${o.length} -**AI Recommendations:** ${r.length} property suggestions +**AI Recommendations:** ${i.length} property suggestions Return JSON: { @@ -333,7 +333,7 @@ Rules: - Group similar lint errors (e.g. "5 layers missing fill styles" not 5 separate findings) - Max 10 findings, prioritized by severity - nextSteps: max 5, ordered by impact -- Be specific and actionable, not generic`,m=await ae(c,i,{prompt:f,model:a,maxTokens:1024,temperature:.1}),h=pe(m.content);return h?{verdict:h.verdict||"warn",headline:h.headline||"Review completed",findings:(h.findings||[]).map(k=>({severity:k.severity||"info",category:k.category||"General",title:k.title||"",description:k.description||"",nodeId:k.nodeId,nodeName:k.nodeName,autoFixable:k.autoFixable||!1})),nextSteps:h.nextSteps||[]}:Pt(t,n,s,o)}async function Ti(e,t,n,s,o,r,i){var f;let a=!1,c="";n.type==="COMPONENT"&&((f=n.parent)==null?void 0:f.type)==="COMPONENT_SET"?(c=n.parent.description||"",a=c.trim().length>0):n.type==="COMPONENT_SET"&&(a=!!(i&&i.trim().length>0));let d=!!(i&&i.trim().length>0),l=d?"pass":"warning",p="";d?p="Component has description for MCP/AI context":a?(l="pass",p="Component set has a description. Consider adding a variant-specific description for richer context."):p="Add a component description to help MCP and AI understand the component purpose and usage";let u=[{check:"Property configuration",status:s.length>0?"pass":"warning",suggestion:s.length>0?"Component has configurable properties":"Consider adding properties for component customization"},{check:"Component description",status:l,suggestion:p}],g=Ri(n,o);return{states:o.map(m=>({name:m,found:!0})),componentReadiness:u,accessibility:g}}var Pi=["button","btn","link","anchor","checkbox","check-box","radio","toggle","switch","tab","chip","tag","input","select","dropdown","menu-item","menuitem","slider","stepper","icon-button","fab","action"];function Li(e,t){let n=e.name.toLowerCase();if(Pi.some(o=>n.includes(o)))return!0;let s=["hover","pressed","focus","focused","active","disabled"];return!!t.some(o=>s.includes(o.toLowerCase()))}function Ri(e,t){let n=[],s=Li(e,t);if(s){let o="width"in e?e.width:0,r="height"in e?e.height:0,i=Math.min(o,r);i>=44?n.push({check:"Touch target size",status:"pass",suggestion:`Target size ${Math.round(o)}\xD7${Math.round(r)}px meets recommended 44px minimum`}):i>=24?n.push({check:"Touch target size",status:"warning",suggestion:`Target size ${Math.round(o)}\xD7${Math.round(r)}px meets WCAG minimum (24px) but is below recommended 44px`}):n.push({check:"Touch target size",status:"fail",suggestion:`Target size ${Math.round(o)}\xD7${Math.round(r)}px is below WCAG 2.5.8 minimum of 24\xD724px`})}if(s){let o=t.some(r=>{let i=r.toLowerCase();return i==="focus"||i==="focused"||i.includes("focus")});n.push({check:"Focus state",status:o?"pass":"warning",suggestion:o?"Component has a focus state for keyboard navigation":"Add a visible focus state to support keyboard navigation (WCAG 2.4.7)"})}if("findAll"in e){let r=e.findAll(i=>i.type==="TEXT");if(r.length>0){let i=1/0,a=!1;for(let c of r){let d=typeof c.fontSize=="number"?c.fontSize:0;d>0&&d0&&d<12&&(a=!0)}a?n.push({check:"Minimum font size",status:"warning",suggestion:`Text as small as ${i}px detected. Consider using 12px minimum for readability`}):i!==1/0&&n.push({check:"Minimum font size",status:"pass",suggestion:`Smallest text is ${i}px, meets readability guidelines`})}}if("findAll"in e){let r=e.findAll(d=>d.type==="TEXT"),i=1/0,a=0,c="";for(let d of r){let l=d.fills;if(!Array.isArray(l)||l.length===0)continue;let p=l.find(h=>h.type==="SOLID"&&h.visible!==!1&&h.color&&!(h.boundVariables&&h.boundVariables.color));if(!p)continue;let u=Ne(d);if(!u)continue;let g=se(p.color.r,p.color.g,p.color.b),f=se(u.r,u.g,u.b),m=ke(g,f);a++,m0&&i!==1/0){let d=i.toFixed(1);i>=4.5?n.push({check:"Color contrast",status:"pass",suggestion:`Lowest contrast ratio is ${d}:1, meets WCAG AA (4.5:1)`}):i>=3?n.push({check:"Color contrast",status:"warning",suggestion:`"${c}" has ${d}:1 contrast. Meets large text AA (3:1) but not normal text (4.5:1)`}):n.push({check:"Color contrast",status:"fail",suggestion:`"${c}" has ${d}:1 contrast, below WCAG AA minimum of 3:1`})}}return n.length===0&&n.push({check:"Accessibility review",status:"pass",suggestion:"No accessibility issues detected for this component type"}),n}function Fs(e){var I,w,P,O,M,z,x,$,v,T,L;let{node:t,context:n,actualProperties:s,actualStates:o,tokens:r,componentDescription:i}=e,a=n.componentFamily||"generic",c=[],d=[],l=[];i&&i.trim().length>0?c.push("Has component description for better MCP/AI context"):(d.push("Missing component description - AI cannot understand component purpose and intent"),l.push("Add a descriptive explanation in component properties to help AI understand the component's purpose, behavior, and usage patterns")),s.length>0?c.push(`Has ${s.length} configurable properties`):(d.push("No configurable properties - component cannot be customized for different use cases"),l.push("Add component properties for customization (size, variant, text content, etc.)"));let p=n.hasInteractiveElements&&a!=="badge"&&a!=="icon";p&&(o.length>1?c.push("Includes multiple component states"):(d.push("Missing interactive states - users won't receive proper feedback for interactions"),l.push("Add hover, focus, and disabled states with clear visual feedback")));let u={colors:((w=(I=r==null?void 0:r.colors)==null?void 0:I.filter(A=>A.isActualToken))==null?void 0:w.length)||0,spacing:((O=(P=r==null?void 0:r.spacing)==null?void 0:P.filter(A=>A.isActualToken))==null?void 0:O.length)||0,typography:((z=(M=r==null?void 0:r.typography)==null?void 0:M.filter(A=>A.isActualToken))==null?void 0:z.length)||0,hardCoded:[...((x=r==null?void 0:r.colors)==null?void 0:x.filter(A=>!A.isActualToken&&!A.isDefaultVariantStyle))||[],...(($=r==null?void 0:r.spacing)==null?void 0:$.filter(A=>!A.isActualToken&&!A.isDefaultVariantStyle))||[],...((v=r==null?void 0:r.typography)==null?void 0:v.filter(A=>!A.isActualToken&&!A.isDefaultVariantStyle))||[],...((T=r==null?void 0:r.effects)==null?void 0:T.filter(A=>!A.isActualToken&&!A.isDefaultVariantStyle))||[],...((L=r==null?void 0:r.borders)==null?void 0:L.filter(A=>!A.isActualToken&&!A.isDefaultVariantStyle))||[]].length},g=u.colors+u.spacing+u.typography;g>0?(c.push("Uses design tokens for consistency"),u.hardCoded>0&&(d.push("Found hard-coded values - inconsistent with design system"),l.push("Replace remaining hard-coded colors and spacing with design tokens"))):u.hardCoded>2&&(d.push("No design tokens used - component styling is inconsistent with design system"),l.push("Replace hard-coded values with design tokens for colors, spacing, and typography"));let f=s.some(A=>A.name.toLowerCase().includes("size")||A.name.toLowerCase().includes("scale")||A.name.toLowerCase().includes("dimension")),m=s.some(A=>A.name.toLowerCase().includes("variant")||A.name.toLowerCase().includes("style")||A.name.toLowerCase().includes("type"));a==="avatar"?!f&&s.length>0&&(d.push("No size variants defined - limits reusability across different contexts"),l.push("Add size property (xs, sm, md, lg, xl) for headers, lists, and profiles")):a==="button"?(o.length<=1&&(d.push("Missing interactive states - reduces accessibility and user feedback"),l.push("Add hover, focus, and disabled states with clear visual feedback")),!m&&s.length>0&&(d.push("No visual hierarchy variants - limits design flexibility"),l.push("Add variant property (primary, secondary, danger) for proper hierarchy"))):a==="input"?o.length<=1&&(d.push("Missing form states - poor accessibility and user experience"),l.push("Add focus, error, and disabled states with clear visual indicators")):a==="container"&&(!m&&s.length>0&&(d.push("No layout variants defined - limits flexibility for different use cases"),l.push("Add orientation property (horizontal, vertical) or density variants")),s.length>0&&!s.some(A=>A.name.toLowerCase().includes("spacing"))&&(d.push("No spacing customization - may not fit all design contexts"),l.push("Add spacing property to control internal padding and gaps"))),s.length===0?(d.push("No configurable properties - component lacks flexibility for different use cases"),a==="container"?l.push("Add layout properties for customization (orientation, spacing, alignment)"):l.push("Add component properties to enable customization and reuse")):s.length===1&&!f&&!m&&(d.push("Limited customization options - consider adding more properties for flexibility"),a!=="container"&&p&&o.length<=1?l.push("Add interactive states and additional variant options"):a==="container"&&l.push("Consider adding layout variant properties (orientation, density)")),c.length===0&&c.push("Component follows basic Figma structure patterns"),d.length===0&&d.push("Well-structured component - consider minor enhancements for broader usage"),l.length===0&&l.push("Component is well-configured - ready for code generation");let h=0,C=s.length>0,k=g>0,N=g>0?g/(g+u.hardCoded):0;if(C&&(h+=22),i&&i.trim().length>0&&(h+=3),h+=Math.round(25*N),n.hasInteractiveElements&&a!=="badge"&&a!=="icon"){let A=Math.min(o.length/3,1);h+=Math.round(20*A)}else h+=20;return(t.type==="COMPONENT"||t.type==="COMPONENT_SET"||t.type==="INSTANCE")&&(h+=10),n.name&&!n.name.toLowerCase().includes("untitled")&&(h+=10),(C||k||o.length>0)&&(h+=10),h=Math.max(0,Math.min(100,h)),{score:h,strengths:c,gaps:Ts(d),recommendations:Ts(l),implementationNotes:$i(a,c,d,s,o,u)}}function $i(e,t,n,s,o,r){let i=[];return e==="button"?(o.length<3&&i.push("Implement hover, focus, and active states for better interactivity"),s.length===0&&i.push("Add variant and size properties to support different use cases")):e==="input"?(o.includes("error")||i.push("Add error state with clear visual indicators for form validation"),i.push("Ensure proper label association and placeholder text patterns")):e==="card"?(i.push("Consider implementing click handlers for interactive cards"),s.length===0&&i.push("Add elevation or variant properties for visual hierarchy")):e==="avatar"?(i.push("Implement fallback patterns for missing images"),s.some(a=>a.name.toLowerCase().includes("size"))||i.push("Add size variants for flexible usage across contexts")):e==="container"&&(i.push("Focus on layout flexibility and content composition"),i.push("Consider responsive behavior for different screen sizes")),r.hardCoded>r.colors+r.spacing&&i.push("Prioritize converting hard-coded values to design tokens"),s.length===0?i.push("Define component properties to enable customization without code changes"):s.length===1&&i.push("Consider additional properties for greater flexibility"),i.length===0&&(n.length>3?i.push("Focus on addressing the high-priority gaps identified above"):t.length>n.length?i.push("Component is well-structured for code generation with minor improvements needed"):i.push("Balance quick wins with systematic improvements for optimal results")),i.join(". ")+"."}function Ts(e){if(e.length<=1)return e;let t=[],n=new Set,s=[{pattern:/add.*component.*propert/i,message:"Add component properties for customization and reuse"},{pattern:/add.*(hover|focus|disabled|interactive).*state/i,message:"Add hover, focus, and disabled states with clear visual feedback"},{pattern:/replace.*hard.coded.*(color|spacing|token)/i,message:"Replace remaining hard-coded colors and spacing with design tokens"},{pattern:/add.*(size|variant).*propert/i,message:"Add size and style variant properties for different use cases"},{pattern:/no.*configurable.*propert.*(cannot|lacks|limited)/i,message:"No configurable properties - component lacks flexibility for different use cases"},{pattern:/(missing|no).*(interactive|hover|focus).*state/i,message:"Missing interactive states - reduces accessibility and user feedback"},{pattern:/found.*hard.coded.*value.*(inconsistent|design.*system)/i,message:"Found hard-coded values - inconsistent with design system"},{pattern:/(minimal|simple).*layer.*structure.*(lack|semantic|organization)/i,message:"Minimal layer structure - may lack semantic organization for complex use cases"}];return e.forEach(o=>{let r=o.trim();if(!r)return;let i=!0,a=r;for(let{pattern:l,message:p}of s)if(l.test(r))if(n.has(l.source)){i=!1;break}else{n.add(l.source),a=p;break}let c=r.toLowerCase(),d=t.some(l=>l.toLowerCase()===c||Mi(l.toLowerCase(),c)>.8);i&&!d&&t.push(a)}),console.log(`\u{1F50D} [DEDUP] Reduced ${e.length} items to ${t.length}`),e.length!==t.length&&(console.log("\u{1F50D} [DEDUP] Original:",e),console.log("\u{1F50D} [DEDUP] Deduplicated:",t)),t}function Mi(e,t){let n=e.length>t.length?e:t,s=e.length>t.length?t:e;if(n.length===0)return 1;let o=Oi(n,s);return(n.length-o)/n.length}function Oi(e,t){let n=[];for(let s=0;s<=t.length;s++)n[s]=[s];for(let s=0;s<=e.length;s++)n[0][s]=s;for(let s=1;s<=t.length;s++)for(let o=1;o<=e.length;o++)t.charAt(s-1)===e.charAt(o-1)?n[s][o]=n[s-1][o-1]:n[s][o]=Math.min(n[s-1][o-1]+1,n[s][o-1]+1,n[s-1][o]+1);return n[t.length][e.length]}var $t=class{constructor(t={}){this.cache=new Map;this.designSystemsKnowledge=null;this.config=R({enableCaching:!0,enableMCPIntegration:!1,consistencyThreshold:.95},t)}generateComponentHash(t,n,s){var r,i;let o={name:t.name,type:t.type,hierarchy:this.normalizeHierarchy(t.hierarchy),frameStructure:t.frameStructure,detectedStyles:t.detectedStyles,tokenFingerprint:this.generateTokenFingerprint(n),staticProperties:{hasInteractiveElements:((r=t.additionalContext)==null?void 0:r.hasInteractiveElements)||!1,componentFamily:((i=t.additionalContext)==null?void 0:i.componentFamily)||"generic"},lintSettingsFingerprint:s?this.createHash(JSON.stringify(s)):""};return this.createHash(JSON.stringify(o))}getCachedAnalysis(t){if(!this.config.enableCaching)return null;let n=this.cache.get(t);return n?Date.now()-n.timestamp>24*60*60*1e3?(this.cache.delete(t),null):(console.log("\u2705 Using cached analysis for component hash:",t),n):null}cacheAnalysis(t,n){var s;this.config.enableCaching&&(this.cache.set(t,{hash:t,result:n,timestamp:Date.now(),mcpKnowledgeVersion:((s=this.designSystemsKnowledge)==null?void 0:s.version)||"1.0.0"}),console.log("\u{1F4BE} Cached analysis for component hash:",t))}setDesignSystemsKnowledge(t){this.designSystemsKnowledge=t}async loadDesignSystemsKnowledge(){this.loadFallbackKnowledge()}createDeterministicPrompt(t){let n=this.createBasePrompt(t),s=this.getMCPGuidance(t),o=this.getScoringCriteria(t);return`${n} +- Be specific and actionable, not generic`,p=await le(c,r,{prompt:g,model:a,maxTokens:1024,temperature:.1}),h=ye(p.content);return h?{verdict:h.verdict||"warn",headline:h.headline||"Review completed",findings:(h.findings||[]).map(k=>({severity:k.severity||"info",category:k.category||"General",title:k.title||"",description:k.description||"",nodeId:k.nodeId,nodeName:k.nodeName,autoFixable:k.autoFixable||!1})),nextSteps:h.nextSteps||[]}:Kt(t,n,s,o)}async function Ia(e,t,n,s,o,i,r){var g;let a=!1,c="";n.type==="COMPONENT"&&((g=n.parent)==null?void 0:g.type)==="COMPONENT_SET"?(c=n.parent.description||"",a=c.trim().length>0):n.type==="COMPONENT_SET"&&(a=!!(r&&r.trim().length>0));let l=!!(r&&r.trim().length>0),d=l?"pass":"warning",u="";l?u="Component has description for MCP/AI context":a?(d="pass",u="Component set has a description. Consider adding a variant-specific description for richer context."):u="Add a component description to help MCP and AI understand the component purpose and usage";let f=[{check:"Property configuration",status:s.length>0?"pass":"warning",suggestion:s.length>0?"Component has configurable properties":"Consider adding properties for component customization"},{check:"Component description",status:d,suggestion:u}],m=Aa(n,o);return{states:o.map(p=>({name:p,found:!0})),componentReadiness:f,accessibility:m}}var Ca=["button","btn","link","anchor","checkbox","check-box","radio","toggle","switch","tab","chip","tag","input","select","dropdown","menu-item","menuitem","slider","stepper","icon-button","fab","action"];function xa(e,t){let n=e.name.toLowerCase();if(Ca.some(o=>n.includes(o)))return!0;let s=["hover","pressed","focus","focused","active","disabled"];return!!t.some(o=>s.includes(o.toLowerCase()))}function Aa(e,t){let n=[],s=xa(e,t);if(s){let o="width"in e?e.width:0,i="height"in e?e.height:0,r=Math.min(o,i);r>=44?n.push({check:"Touch target size",status:"pass",suggestion:`Target size ${Math.round(o)}\xD7${Math.round(i)}px meets recommended 44px minimum`}):r>=24?n.push({check:"Touch target size",status:"warning",suggestion:`Target size ${Math.round(o)}\xD7${Math.round(i)}px meets WCAG minimum (24px) but is below recommended 44px`}):n.push({check:"Touch target size",status:"fail",suggestion:`Target size ${Math.round(o)}\xD7${Math.round(i)}px is below WCAG 2.5.8 minimum of 24\xD724px`})}if(s){let o=t.some(i=>{let r=i.toLowerCase();return r==="focus"||r==="focused"||r.includes("focus")});n.push({check:"Focus state",status:o?"pass":"warning",suggestion:o?"Component has a focus state for keyboard navigation":"Add a visible focus state to support keyboard navigation (WCAG 2.4.7)"})}if("findAll"in e){let i=e.findAll(r=>r.type==="TEXT");if(i.length>0){let r=1/0,a=!1;for(let c of i){let l=typeof c.fontSize=="number"?c.fontSize:0;l>0&&l0&&l<12&&(a=!0)}a?n.push({check:"Minimum font size",status:"warning",suggestion:`Text as small as ${r}px detected. Consider using 12px minimum for readability`}):r!==1/0&&n.push({check:"Minimum font size",status:"pass",suggestion:`Smallest text is ${r}px, meets readability guidelines`})}}if("findAll"in e){let i=e.findAll(l=>l.type==="TEXT"),r=1/0,a=0,c="";for(let l of i){let d=l.fills;if(!Array.isArray(d)||d.length===0)continue;let u=d.find(h=>h.type==="SOLID"&&h.visible!==!1&&h.color&&!(h.boundVariables&&h.boundVariables.color));if(!u)continue;let f=Ae(l);if(!f)continue;let m=J(u.color.r,u.color.g,u.color.b),g=J(f.r,f.g,f.b),p=re(m,g);a++,p0&&r!==1/0){let l=r.toFixed(1);r>=4.5?n.push({check:"Color contrast",status:"pass",suggestion:`Lowest contrast ratio is ${l}:1, meets WCAG AA (4.5:1)`}):r>=3?n.push({check:"Color contrast",status:"warning",suggestion:`"${c}" has ${l}:1 contrast. Meets large text AA (3:1) but not normal text (4.5:1)`}):n.push({check:"Color contrast",status:"fail",suggestion:`"${c}" has ${l}:1 contrast, below WCAG AA minimum of 3:1`})}}return n.length===0&&n.push({check:"Accessibility review",status:"pass",suggestion:"No accessibility issues detected for this component type"}),n}function no(e){var x,I,L,O,M,z,A,$,v,T,P;let{node:t,context:n,actualProperties:s,actualStates:o,tokens:i,componentDescription:r}=e,a=n.componentFamily||"generic",c=[],l=[],d=[];r&&r.trim().length>0?c.push("Has component description for better MCP/AI context"):(l.push("Missing component description - AI cannot understand component purpose and intent"),d.push("Add a descriptive explanation in component properties to help AI understand the component's purpose, behavior, and usage patterns")),s.length>0?c.push(`Has ${s.length} configurable properties`):(l.push("No configurable properties - component cannot be customized for different use cases"),d.push("Add component properties for customization (size, variant, text content, etc.)"));let u=n.hasInteractiveElements&&a!=="badge"&&a!=="icon";u&&(o.length>1?c.push("Includes multiple component states"):(l.push("Missing interactive states - users won't receive proper feedback for interactions"),d.push("Add hover, focus, and disabled states with clear visual feedback")));let f={colors:((I=(x=i==null?void 0:i.colors)==null?void 0:x.filter(w=>w.isActualToken))==null?void 0:I.length)||0,spacing:((O=(L=i==null?void 0:i.spacing)==null?void 0:L.filter(w=>w.isActualToken))==null?void 0:O.length)||0,typography:((z=(M=i==null?void 0:i.typography)==null?void 0:M.filter(w=>w.isActualToken))==null?void 0:z.length)||0,hardCoded:[...((A=i==null?void 0:i.colors)==null?void 0:A.filter(w=>!w.isActualToken&&!w.isDefaultVariantStyle))||[],...(($=i==null?void 0:i.spacing)==null?void 0:$.filter(w=>!w.isActualToken&&!w.isDefaultVariantStyle))||[],...((v=i==null?void 0:i.typography)==null?void 0:v.filter(w=>!w.isActualToken&&!w.isDefaultVariantStyle))||[],...((T=i==null?void 0:i.effects)==null?void 0:T.filter(w=>!w.isActualToken&&!w.isDefaultVariantStyle))||[],...((P=i==null?void 0:i.borders)==null?void 0:P.filter(w=>!w.isActualToken&&!w.isDefaultVariantStyle))||[]].length},m=f.colors+f.spacing+f.typography;m>0?(c.push("Uses design tokens for consistency"),f.hardCoded>0&&(l.push("Found hard-coded values - inconsistent with design system"),d.push("Replace remaining hard-coded colors and spacing with design tokens"))):f.hardCoded>2&&(l.push("No design tokens used - component styling is inconsistent with design system"),d.push("Replace hard-coded values with design tokens for colors, spacing, and typography"));let g=s.some(w=>w.name.toLowerCase().includes("size")||w.name.toLowerCase().includes("scale")||w.name.toLowerCase().includes("dimension")),p=s.some(w=>w.name.toLowerCase().includes("variant")||w.name.toLowerCase().includes("style")||w.name.toLowerCase().includes("type"));a==="avatar"?!g&&s.length>0&&(l.push("No size variants defined - limits reusability across different contexts"),d.push("Add size property (xs, sm, md, lg, xl) for headers, lists, and profiles")):a==="button"?(o.length<=1&&(l.push("Missing interactive states - reduces accessibility and user feedback"),d.push("Add hover, focus, and disabled states with clear visual feedback")),!p&&s.length>0&&(l.push("No visual hierarchy variants - limits design flexibility"),d.push("Add variant property (primary, secondary, danger) for proper hierarchy"))):a==="input"?o.length<=1&&(l.push("Missing form states - poor accessibility and user experience"),d.push("Add focus, error, and disabled states with clear visual indicators")):a==="container"&&(!p&&s.length>0&&(l.push("No layout variants defined - limits flexibility for different use cases"),d.push("Add orientation property (horizontal, vertical) or density variants")),s.length>0&&!s.some(w=>w.name.toLowerCase().includes("spacing"))&&(l.push("No spacing customization - may not fit all design contexts"),d.push("Add spacing property to control internal padding and gaps"))),s.length===0?(l.push("No configurable properties - component lacks flexibility for different use cases"),a==="container"?d.push("Add layout properties for customization (orientation, spacing, alignment)"):d.push("Add component properties to enable customization and reuse")):s.length===1&&!g&&!p&&(l.push("Limited customization options - consider adding more properties for flexibility"),a!=="container"&&u&&o.length<=1?d.push("Add interactive states and additional variant options"):a==="container"&&d.push("Consider adding layout variant properties (orientation, density)")),c.length===0&&c.push("Component follows basic Figma structure patterns"),l.length===0&&l.push("Well-structured component - consider minor enhancements for broader usage"),d.length===0&&d.push("Component is well-configured - ready for code generation");let h=0,C=s.length>0,k=m>0,N=m>0?m/(m+f.hardCoded):0;if(C&&(h+=22),r&&r.trim().length>0&&(h+=3),h+=Math.round(25*N),n.hasInteractiveElements&&a!=="badge"&&a!=="icon"){let w=Math.min(o.length/3,1);h+=Math.round(20*w)}else h+=20;return(t.type==="COMPONENT"||t.type==="COMPONENT_SET"||t.type==="INSTANCE")&&(h+=10),n.name&&!n.name.toLowerCase().includes("untitled")&&(h+=10),(C||k||o.length>0)&&(h+=10),h=Math.max(0,Math.min(100,h)),{score:h,strengths:c,gaps:Xs(l),recommendations:Xs(d),implementationNotes:wa(a,c,l,s,o,f)}}function wa(e,t,n,s,o,i){let r=[];return e==="button"?(o.length<3&&r.push("Implement hover, focus, and active states for better interactivity"),s.length===0&&r.push("Add variant and size properties to support different use cases")):e==="input"?(o.includes("error")||r.push("Add error state with clear visual indicators for form validation"),r.push("Ensure proper label association and placeholder text patterns")):e==="card"?(r.push("Consider implementing click handlers for interactive cards"),s.length===0&&r.push("Add elevation or variant properties for visual hierarchy")):e==="avatar"?(r.push("Implement fallback patterns for missing images"),s.some(a=>a.name.toLowerCase().includes("size"))||r.push("Add size variants for flexible usage across contexts")):e==="container"&&(r.push("Focus on layout flexibility and content composition"),r.push("Consider responsive behavior for different screen sizes")),i.hardCoded>i.colors+i.spacing&&r.push("Prioritize converting hard-coded values to design tokens"),s.length===0?r.push("Define component properties to enable customization without code changes"):s.length===1&&r.push("Consider additional properties for greater flexibility"),r.length===0&&(n.length>3?r.push("Focus on addressing the high-priority gaps identified above"):t.length>n.length?r.push("Component is well-structured for code generation with minor improvements needed"):r.push("Balance quick wins with systematic improvements for optimal results")),r.join(". ")+"."}function Xs(e){if(e.length<=1)return e;let t=[],n=new Set,s=[{pattern:/add.*component.*propert/i,message:"Add component properties for customization and reuse"},{pattern:/add.*(hover|focus|disabled|interactive).*state/i,message:"Add hover, focus, and disabled states with clear visual feedback"},{pattern:/replace.*hard.coded.*(color|spacing|token)/i,message:"Replace remaining hard-coded colors and spacing with design tokens"},{pattern:/add.*(size|variant).*propert/i,message:"Add size and style variant properties for different use cases"},{pattern:/no.*configurable.*propert.*(cannot|lacks|limited)/i,message:"No configurable properties - component lacks flexibility for different use cases"},{pattern:/(missing|no).*(interactive|hover|focus).*state/i,message:"Missing interactive states - reduces accessibility and user feedback"},{pattern:/found.*hard.coded.*value.*(inconsistent|design.*system)/i,message:"Found hard-coded values - inconsistent with design system"},{pattern:/(minimal|simple).*layer.*structure.*(lack|semantic|organization)/i,message:"Minimal layer structure - may lack semantic organization for complex use cases"}];return e.forEach(o=>{let i=o.trim();if(!i)return;let r=!0,a=i;for(let{pattern:d,message:u}of s)if(d.test(i))if(n.has(d.source)){r=!1;break}else{n.add(d.source),a=u;break}let c=i.toLowerCase(),l=t.some(d=>d.toLowerCase()===c||Ea(d.toLowerCase(),c)>.8);r&&!l&&t.push(a)}),console.log(`\u{1F50D} [DEDUP] Reduced ${e.length} items to ${t.length}`),e.length!==t.length&&(console.log("\u{1F50D} [DEDUP] Original:",e),console.log("\u{1F50D} [DEDUP] Deduplicated:",t)),t}function Ea(e,t){let n=e.length>t.length?e:t,s=e.length>t.length?t:e;if(n.length===0)return 1;let o=Ta(n,s);return(n.length-o)/n.length}function Ta(e,t){let n=[];for(let s=0;s<=t.length;s++)n[s]=[s];for(let s=0;s<=e.length;s++)n[0][s]=s;for(let s=1;s<=t.length;s++)for(let o=1;o<=e.length;o++)t.charAt(s-1)===e.charAt(o-1)?n[s][o]=n[s-1][o-1]:n[s][o]=Math.min(n[s-1][o-1]+1,n[s][o-1]+1,n[s-1][o]+1);return n[t.length][e.length]}var Jt=class{constructor(t={}){this.cache=new Map;this.designSystemsKnowledge=null;this.config=R({enableCaching:!0,enableMCPIntegration:!1,consistencyThreshold:.95},t)}generateComponentHash(t,n,s){var i,r;let o={name:t.name,type:t.type,hierarchy:this.normalizeHierarchy(t.hierarchy),frameStructure:t.frameStructure,detectedStyles:t.detectedStyles,tokenFingerprint:this.generateTokenFingerprint(n),staticProperties:{hasInteractiveElements:((i=t.additionalContext)==null?void 0:i.hasInteractiveElements)||!1,componentFamily:((r=t.additionalContext)==null?void 0:r.componentFamily)||"generic"},lintSettingsFingerprint:s?this.createHash(JSON.stringify(s)):""};return this.createHash(JSON.stringify(o))}getCachedAnalysis(t){if(!this.config.enableCaching)return null;let n=this.cache.get(t);return n?Date.now()-n.timestamp>24*60*60*1e3?(this.cache.delete(t),null):(console.log("\u2705 Using cached analysis for component hash:",t),n):null}cacheAnalysis(t,n){var s;this.config.enableCaching&&(this.cache.set(t,{hash:t,result:n,timestamp:Date.now(),mcpKnowledgeVersion:((s=this.designSystemsKnowledge)==null?void 0:s.version)||"1.0.0"}),console.log("\u{1F4BE} Cached analysis for component hash:",t))}setDesignSystemsKnowledge(t){this.designSystemsKnowledge=t}async loadDesignSystemsKnowledge(){this.loadFallbackKnowledge()}createDeterministicPrompt(t){let n=this.createBasePrompt(t),s=this.getMCPGuidance(t),o=this.getScoringCriteria(t);return`${n} **CONSISTENCY REQUIREMENTS:** - Use DETERMINISTIC analysis based on the exact component structure provided @@ -375,7 +375,7 @@ ${o} "tokens": {...}, "audit": {...}, "mcpReadiness": {...} -}`}validateAnalysisConsistency(t,n){var r,i,a,c,d;let s=[];(r=t.metadata)!=null&&r.component||s.push("Missing component name"),(i=t.metadata)!=null&&i.description||s.push("Missing component description"),this.isValidScore((c=(a=t.metadata)==null?void 0:a.mcpReadiness)==null?void 0:c.score)||s.push("Invalid or missing MCP readiness score");let o=(d=n.additionalContext)==null?void 0:d.componentFamily;return o&&!this.validateComponentFamilyConsistency(t,o)&&s.push(`Inconsistent analysis for ${o} component family`),this.validateTokenRecommendations(t.tokens)||s.push("Inconsistent token recommendations"),s.length>0?(console.warn("\u26A0\uFE0F Analysis consistency issues found:",s),!1):!0}applyConsistencyCorrections(t,n){var o;let s=R({},t);return(o=n.additionalContext)!=null&&o.componentFamily&&(s.metadata=this.applyComponentFamilyCorrections(s.metadata,n.additionalContext.componentFamily)),s.tokens=this.applyTokenConsistencyCorrections(s.tokens),s.metadata.mcpReadiness=this.ensureConsistentScoring(s.metadata.mcpReadiness||{},n),s}normalizeHierarchy(t){return t.map(n=>({name:n.name.toLowerCase().trim(),type:n.type,depth:n.depth}))}generateTokenFingerprint(t){let n=t.map(s=>`${s.type}:${s.isToken}:${s.source}`).sort().join("|");return this.createHash(n)}createHash(t){let n=0;if(t.length===0)return n.toString();for(let s=0;s0?(console.warn("\u26A0\uFE0F Analysis consistency issues found:",s),!1):!0}applyConsistencyCorrections(t,n){var o;let s=R({},t);return(o=n.additionalContext)!=null&&o.componentFamily&&(s.metadata=this.applyComponentFamilyCorrections(s.metadata,n.additionalContext.componentFamily)),s.tokens=this.applyTokenConsistencyCorrections(s.tokens),s.metadata.mcpReadiness=this.ensureConsistentScoring(s.metadata.mcpReadiness||{},n),s}normalizeHierarchy(t){return t.map(n=>({name:n.name.toLowerCase().trim(),type:n.type,depth:n.depth}))}generateTokenFingerprint(t){let n=t.map(s=>`${s.type}:${s.isToken}:${s.source}`).sort().join("|");return this.createHash(n)}createHash(t){let n=0;if(t.length===0)return n.toString();for(let s=0;s=0&&t<=100}validateComponentFamilyConsistency(t,n){let s=t.metadata;switch(n){case"button":return this.validateButtonComponent(s);case"avatar":return this.validateAvatarComponent(s);case"input":return this.validateInputComponent(s);default:return!0}}validateButtonComponent(t){var s;return((s=t.states)==null?void 0:s.some(o=>["hover","focus","active","disabled"].includes(o.toLowerCase())))||!1}validateAvatarComponent(t){var o,r,i;let n=((r=(o=t.variants)==null?void 0:o.size)==null?void 0:r.length)>0,s=(i=t.props)==null?void 0:i.some(a=>a.name.toLowerCase().includes("size"));return n||s||!1}validateInputComponent(t){var s;return((s=t.states)==null?void 0:s.some(o=>["focus","error","disabled","filled"].includes(o.toLowerCase())))||!1}validateTokenRecommendations(t){var s;return((s=t.colors)==null?void 0:s.some(o=>o.name.includes("semantic-")||o.name.includes("primary")||o.name.includes("secondary")))!==!1}applyComponentFamilyCorrections(t,n){var o,r,i;let s=R({},t);switch(n){case"button":(o=s.states)!=null&&o.includes("hover")||(s.states=[...s.states||[],"hover","focus","active","disabled"]);break;case"avatar":!((r=s.variants)!=null&&r.size)&&!((i=s.props)!=null&&i.some(a=>a.name.includes("size")))&&(s.variants=K(R({},s.variants),{size:["small","medium","large"]}));break}return s}applyTokenConsistencyCorrections(t){return t&&R({},t)}ensureConsistentScoring(t,n){return K(R({},t),{score:t.score||0})}},Ds=$t;J();function Mt(e,t,n){let s=f=>f<=.04045?f/12.92:Math.pow((f+.055)/1.055,2.4),o=s(e),r=s(t),i=s(n),a=(o*.4124564+r*.3575761+i*.1804375)/.95047,c=o*.2126729+r*.7151522+i*.072175,d=(o*.0193339+r*.119192+i*.9503041)/1.08883,l=f=>f>.008856?Math.cbrt(f):7.787*f+16/116,p=l(a),u=l(c),g=l(d);return{L:116*u-16,a:500*(p-u),b:200*(u-g)}}function Vs(e,t){let{L:n,a:s,b:o}=e,{L:r,a:i,b:a}=t,c=1,d=1,l=1,p=Math.sqrt(s*s+o*o),u=Math.sqrt(i*i+a*a),g=(p+u)/2,f=Math.pow(g,7),m=.5*(1-Math.sqrt(f/(f+6103515625))),h=s*(1+m),C=i*(1+m),k=Math.sqrt(h*h+o*o),N=Math.sqrt(C*C+a*a),y=Math.atan2(o,h)*180/Math.PI,b=Math.atan2(a,C)*180/Math.PI,I=(y%360+360)%360,w=(b%360+360)%360,P=r-n,O=N-k,M;k*N===0?M=0:Math.abs(w-I)<=180?M=w-I:w-I>180?M=w-I-360:M=w-I+360;let z=2*Math.sqrt(k*N)*Math.sin(M*Math.PI/360),x=(n+r)/2,$=(k+N)/2,v;k*N===0?v=I+w:Math.abs(I-w)<=180?v=(I+w)/2:I+w<360?v=(I+w+360)/2:v=(I+w-360)/2;let T=1-.17*Math.cos((v-30)*Math.PI/180)+.24*Math.cos(2*v*Math.PI/180)+.32*Math.cos((3*v+6)*Math.PI/180)-.2*Math.cos((4*v-63)*Math.PI/180),L=1+.015*Math.pow(x-50,2)/Math.sqrt(20+Math.pow(x-50,2)),A=1+.045*$,ee=1+.015*$*T,Y=Math.pow($,7),ve=-2*Math.sqrt(Y/(Y+6103515625))*Math.sin(60*Math.exp(-Math.pow((v-275)/25,2))*Math.PI/180);return Math.sqrt(Math.pow(P/(c*L),2)+Math.pow(O/(d*A),2)+Math.pow(z/(l*ee),2)+ve*(O/(d*A))*(z/(l*ee)))}async function Fi(e,t,n,s=0){try{if(!(t in e))return{success:!1,message:`Node does not support ${t}`,error:`Property ${t} not found on node type ${e.type}`};let o=await figma.variables.getVariableByIdAsync(n);if(!o)return{success:!1,message:"Variable not found",error:`Could not find variable with ID: ${n}`};if(o.resolvedType!=="COLOR")return{success:!1,message:"Variable is not a color type",error:`Variable ${o.name} is of type ${o.resolvedType}, expected COLOR`};let i=[...e[t]];if(s>=i.length)return{success:!1,message:"Paint index out of range",error:`Paint index ${s} does not exist. Node has ${i.length} ${t}.`};let a=i[s];if(a.type!=="SOLID")return{success:!1,message:"Can only bind to solid paints",error:`Paint at index ${s} is of type ${a.type}, expected SOLID`};let c=figma.variables.setBoundVariableForPaint(a,"color",o);return i[s]=c,t==="fills"?e.fills=i:e.strokes=i,{success:!0,message:`Successfully bound ${o.name} to ${t}[${s}]`,appliedFix:{nodeId:e.id,nodeName:e.name,propertyPath:`${t}[${s}]`,beforeValue:a.type==="SOLID"&&a.color?F(a.color.r,a.color.g,a.color.b):"unknown",afterValue:o.name,tokenId:n,tokenName:o.name,fixType:"color"}}}catch(o){return{success:!1,message:"Failed to bind color token",error:o instanceof Error?o.message:String(o)}}}async function _s(e,t,n){try{if(!(t in e))return{success:!1,message:`Node does not support ${t}`,error:`Property ${t} not found on node type ${e.type}`};let s=await figma.variables.getVariableByIdAsync(n);if(!s)return{success:!1,message:"Variable not found",error:`Could not find variable with ID: ${n}`};if(s.resolvedType!=="FLOAT")return{success:!1,message:"Variable is not a number type",error:`Variable ${s.name} is of type ${s.resolvedType}, expected FLOAT`};let o=e[t];return e.setBoundVariable(t,s),{success:!0,message:`Successfully bound ${s.name} to ${t}`,appliedFix:{nodeId:e.id,nodeName:e.name,propertyPath:t,beforeValue:typeof o=="number"?`${o}px`:String(o),afterValue:s.name,tokenId:n,tokenName:s.name,fixType:t.includes("Radius")?"border":"spacing"}}}catch(s){return{success:!1,message:"Failed to bind spacing token",error:s instanceof Error?s.message:String(s)}}}async function Ot(e,t=0){try{let n=Vi(e);if(!n)return[];let s=[],o=await figma.variables.getLocalVariablesAsync("COLOR"),r=await figma.variables.getLocalVariableCollectionsAsync(),i=new Map;for(let a of r)i.set(a.id,a);for(let a of o){let c=i.get(a.variableCollectionId);if(!c)continue;let d=c.modes[0].modeId,l=a.valuesByMode[d];if(!l||typeof l!="object"||!("r"in l))continue;let p=l,u=_i(n,p);u>=1-t&&s.push({variableId:a.id,variableName:a.name,collectionName:c.name,value:F(p.r,p.g,p.b),matchScore:u,type:"color"})}return s.sort((a,c)=>c.matchScore-a.matchScore)}catch(n){return console.error("Error finding matching color variable:",n),[]}}async function Di(e,t=0){try{let n=[],s=await figma.variables.getLocalVariablesAsync("FLOAT"),o=await figma.variables.getLocalVariableCollectionsAsync(),r=new Map;for(let i of o)r.set(i.id,i);for(let i of s){let a=r.get(i.variableCollectionId);if(!a)continue;let c=a.modes[0].modeId,d=i.valuesByMode[c];if(typeof d!="number")continue;let l=Math.abs(d-e);if(l<=t){let p=l===0?1:1-l/(t||1);n.push({variableId:i.id,variableName:i.name,collectionName:a.name,value:`${d}px`,matchScore:p,type:"number"})}}return n.sort((i,a)=>a.matchScore-i.matchScore)}catch(n){return console.error("Error finding matching spacing variable:",n),[]}}async function Ft(e,t,n=2){let s=await Di(e,n);if(s.length===0)return s;let r={strokeWeight:["stroke","border-width","border/width","borderwidth"],cornerRadius:["radius","corner","round","border-radius"],topLeftRadius:["radius","corner","round"],topRightRadius:["radius","corner","round"],bottomLeftRadius:["radius","corner","round"],bottomRightRadius:["radius","corner","round"],paddingTop:["padding","spacing","space"],paddingRight:["padding","spacing","space"],paddingBottom:["padding","spacing","space"],paddingLeft:["padding","spacing","space"],itemSpacing:["gap","spacing","space"],counterAxisSpacing:["gap","spacing","space"]}[t]||[];return r.length===0?s:s.map(a=>{let c=a.variableName.toLowerCase(),d=r.some(l=>c.includes(l));return K(R({},a),{matchScore:d?Math.min(a.matchScore+.3,1):a.matchScore})}).sort((a,c)=>c.matchScore-a.matchScore)}async function Dt(e,t,n){let s=t.match(/^(fills|strokes)\[(\d+)\]$/);if(!s)return{success:!1,message:"Invalid property path",error:`Expected format: fills[n] or strokes[n], got: ${t}`};let[,o,r]=s,i=parseInt(r,10);return Fi(e,o,n,i)}async function Vt(e,t,n){if(!["paddingTop","paddingRight","paddingBottom","paddingLeft","itemSpacing","counterAxisSpacing","cornerRadius","topLeftRadius","topRightRadius","bottomLeftRadius","bottomRightRadius","strokeWeight"].includes(t))return{success:!1,message:"Invalid property path",error:`Property ${t} is not a valid spacing property`};if(t==="cornerRadius"){let o=["topLeftRadius","topRightRadius","bottomLeftRadius","bottomRightRadius"],r=[];for(let i of o){let a=await _s(e,i,n);if(r.push(a),!a.success)return{success:!1,message:`Failed to bind ${i}`,error:a.error}}return{success:!0,message:"Successfully bound variable to all 4 corner radii",appliedFix:r[0].appliedFix?K(R({},r[0].appliedFix),{propertyPath:"cornerRadius"}):void 0}}return _s(e,t,n)}async function _t(e,t,n){try{let s=await figma.variables.getVariableByIdAsync(n);if(!s)return null;let o,r,i=t.match(/^(fills|strokes)\[(\d+)\]$/);if(i){o="color";let[,d,l]=i,p=parseInt(l,10);if(!(d in e))return null;let g=e[d];if(p>=g.length)return null;let f=g[p];f.type==="SOLID"&&f.color?r=F(f.color.r,f.color.g,f.color.b):r=f.type}else{if(!(t in e))return null;let d=e[t];r=typeof d=="number"?`${d}px`:String(d),o=t.includes("Radius")?"border":"spacing"}let a=s.name,c=await figma.variables.getVariableCollectionByIdAsync(s.variableCollectionId);if(c){let d=c.modes[0].modeId,l=s.valuesByMode[d];if(typeof l=="number")a=`${s.name} (${l}px)`;else if(l&&typeof l=="object"&&"r"in l){let p=l;a=`${s.name} (${F(p.r,p.g,p.b)})`}}return{nodeId:e.id,nodeName:e.name,propertyPath:t,beforeValue:r,afterValue:a,tokenId:n,tokenName:s.name,fixType:o}}catch(s){return console.error("Error generating fix preview:",s),null}}function Vi(e){let t=e.replace(/^#/,""),n=t;if(t.length===3&&(n=t[0]+t[0]+t[1]+t[1]+t[2]+t[2]),n.length!==6)return null;let s=parseInt(n.substring(0,2),16),o=parseInt(n.substring(2,4),16),r=parseInt(n.substring(4,6),16);return isNaN(s)||isNaN(o)||isNaN(r)?null:{r:s/255,g:o/255,b:r/255}}function _i(e,t){let n=Mt(e.r,e.g,e.b),s=Mt(t.r,t.g,t.b),o=Vs(n,s);return o<3?1:o>=10?0:1-(o-3)/7}async function ze(e,t=1024){let n=Math.max(1,Math.min(t,Math.round(e.width))),s=await e.exportAsync({format:"PNG",constraint:{type:"WIDTH",value:n}});return Bi(s)}function Bi(e){let t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",n="",s=e.length;for(let o=0;o>2],n+=t[(r&3)<<4|i>>4],n+=o+1>6]:"=",n+=o+20)for(let i of r){let a=i.actions||(i.action?[i.action]:[]);for(let c of a)c.type==="NODE"&&c.destinationId&&n.push({sourceFrameId:t,sourceNodeId:e.id,sourceNodeName:e.name,destinationFrameId:c.destinationId,trigger:((s=i.trigger)==null?void 0:s.type)||"UNKNOWN",navigation:c.navigation||"NAVIGATE",hasTransition:!!c.transition}),c.type==="BACK"&&n.push({sourceFrameId:t,sourceNodeId:e.id,sourceNodeName:e.name,destinationFrameId:"__BACK__",trigger:((o=i.trigger)==null?void 0:o.type)||"UNKNOWN",navigation:"BACK",hasTransition:!!c.transition})}}if("children"in e)for(let r of e.children)Us(r,t,n)}function Gs(e){var z,x,$;let t=e||figma.currentPage,n=t.children.filter(v=>v.type==="FRAME"||v.type==="COMPONENT"),s=new Set((t.flowStartingPoints||[]).map(v=>v.nodeId)),o=n.map(v=>({id:v.id,name:v.name,pageId:t.id,pageName:t.name,width:v.width,height:v.height,isFlowStartingPoint:s.has(v.id),childCount:"children"in v?v.children.length:0,hasInteractiveElements:Bs(v)})),r=new Set(o.map(v=>v.id)),i=[];for(let v of n)Us(v,v.id,i);let a=i.filter(v=>v.destinationFrameId==="__BACK__"||r.has(v.destinationFrameId)),c=o.filter(v=>v.isFlowStartingPoint).map(v=>v.id),d=new Map,l=new Map;for(let v of r)d.set(v,new Set),l.set(v,new Set);for(let v of a)v.destinationFrameId!=="__BACK__"&&((z=d.get(v.sourceFrameId))==null||z.add(v.destinationFrameId),(x=l.get(v.destinationFrameId))==null||x.add(v.sourceFrameId));let p=new Set;for(let v of a)v.destinationFrameId==="__BACK__"&&p.add(v.sourceFrameId);let u=o.filter(v=>{var T;return(((T=d.get(v.id))==null?void 0:T.size)||0)===0&&!p.has(v.id)}).map(v=>v.id),g=o.filter(v=>{var T;return(((T=l.get(v.id))==null?void 0:T.size)||0)===0&&!s.has(v.id)}).map(v=>v.id),f=new Set,m=[...c];if(m.length===0)for(let v of o)((($=l.get(v.id))==null?void 0:$.size)||0)===0&&m.push(v.id);for(;m.length>0;){let v=m.shift();if(f.has(v))continue;f.add(v);let T=d.get(v);if(T)for(let L of T)f.has(L)||m.push(L)}let h=o.filter(v=>!f.has(v.id)).map(v=>v.id),C=[],k=new Set,N=new Set,y=[];function b(v){if(N.has(v)){let L=y.indexOf(v);L!==-1&&C.push(y.slice(L));return}if(k.has(v))return;k.add(v),N.add(v),y.push(v);let T=d.get(v);if(T)for(let L of T)b(L);y.pop(),N.delete(v)}for(let v of r)b(v);let I=o.map(v=>{var T;return((T=d.get(v.id))==null?void 0:T.size)||0}),w=I.length>0?I.reduce((v,T)=>v+T,0)/I.length:0,P=0,O=c.map(v=>({id:v,depth:0})),M=new Set;for(;O.length>0;){let{id:v,depth:T}=O.shift();if(M.has(v))continue;M.add(v),T>P&&(P=T);let L=d.get(v);if(L)for(let A of L)M.has(A)||O.push({id:A,depth:T+1})}return{frames:o,edges:a,entryPoints:c,deadEnds:u,orphans:g,unreachable:h,loops:C,stats:{totalFrames:o.length,totalEdges:a.length,totalEntryPoints:c.length,maxDepth:P,avgBranching:Math.round(w*100)/100}}}function zs(e){let t=[],n=new Map(e.frames.map(i=>[i.id,i.name])),s=i=>i.map(a=>`"${n.get(a)||a}"`).join(", ");for(let i of e.deadEnds){let a=n.get(i)||"";/success|confirm|done|complete|thank|receipt|summary/i.test(a)||t.push({type:"dead-end",severity:"warning",frameIds:[i],message:`${s([i])} has no outgoing connections \u2014 user gets stuck here.`})}e.orphans.length>0&&t.push({type:"orphan",severity:"warning",frameIds:e.orphans,message:`${s(e.orphans)} ${e.orphans.length===1?"has":"have"} no incoming connections \u2014 unreachable by navigation.`});let o=e.unreachable.filter(i=>!e.orphans.includes(i));o.length>0&&t.push({type:"unreachable",severity:"critical",frameIds:o,message:`${s(o)} ${o.length===1?"is":"are"} not reachable from any flow entry point.`});for(let i of e.loops){let a=new Set(i);i.some(d=>e.edges.filter(p=>p.sourceFrameId===d).some(p=>!a.has(p.destinationFrameId)))||t.push({type:"loop",severity:"warning",frameIds:i,message:`Circular flow without exit: ${s(i)}. User cannot leave this loop.`})}e.stats.maxDepth>3&&t.push({type:"deep-navigation",severity:"info",frameIds:[],message:`Navigation depth is ${e.stats.maxDepth} levels. Consider flattening to \u22643 levels for better UX (3-click rule).`});let r=e.frames.filter(i=>{if(i.isFlowStartingPoint)return!1;let a=e.edges.some(d=>d.sourceFrameId===i.id&&(d.navigation==="BACK"||d.navigation==="CLOSE"));return e.edges.some(d=>d.destinationFrameId===i.id)&&!a});return r.length>0&&t.push({type:"missing-back",severity:"info",frameIds:r.map(i=>i.id),message:`${r.length} frame${r.length===1?"":"s"} missing back/close navigation: ${s(r.map(i=>i.id))}.`}),t}J();function Hs(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if("fills"in e){let o=e.fills;if(o!==figma.mixed&&Array.isArray(o))for(let r of o)r.type==="SOLID"&&r.visible!==!1&&t.colors.add(F(r.color.r,r.color.g,r.color.b))}if("strokes"in e){let o=e.strokes;if(Array.isArray(o))for(let r of o)r.type==="SOLID"&&r.visible!==!1&&t.colors.add(F(r.color.r,r.color.g,r.color.b))}if(e.type==="TEXT"){let o=e;o.fontName!==figma.mixed&&t.fontFamilies.add(o.fontName.family),o.fontSize!==figma.mixed&&t.fontSizes.add(o.fontSize)}if("layoutMode"in e&&e.layoutMode!=="NONE"){let o=e;typeof o.itemSpacing=="number"&&t.spacingValues.add(o.itemSpacing),typeof o.paddingTop=="number"&&t.spacingValues.add(o.paddingTop),typeof o.paddingBottom=="number"&&t.spacingValues.add(o.paddingBottom),typeof o.paddingLeft=="number"&&t.spacingValues.add(o.paddingLeft),typeof o.paddingRight=="number"&&t.spacingValues.add(o.paddingRight)}if(e.type==="INSTANCE"){let o=e.mainComponent;o&&t.componentNames.add(o.name)}if("children"in e)for(let o of e.children)Hs(o,t,n,s)}}function Ws(e,t){let n=new Set;for(let s of e)t.has(s)||n.add(s);return n}function Ks(e,t={}){var g,f;let n=(g=t.skipLocked)!=null?g:!0,s=(f=t.skipHidden)!=null?f:!0,o=[];if(e.length<2)return o;let r=e.map(({frame:m,node:h})=>{let C={frameId:m.id,frameName:m.name,colors:new Set,fontFamilies:new Set,fontSizes:new Set,spacingValues:new Set,componentNames:new Set};return Hs(h,C,n,s),C}),i=new Map;for(let m of r)for(let h of m.colors)i.set(h,(i.get(h)||0)+1);let a=r.length*.5,c=new Set;for(let[m,h]of i)h>=a&&c.add(m);for(let m of r){let h=Ws(m.colors,c);h.size>3&&o.push({type:"dead-end",severity:"warning",frameIds:[m.frameId],message:`"${m.frameName}" uses ${h.size} colors not found in other screens (${[...h].slice(0,3).join(", ")}${h.size>3?"...":""}). Check for color inconsistency.`})}let d=new Set;for(let m of r)for(let h of m.fontFamilies)d.add(h);if(d.size>3){let m=[...d].join(", ");o.push({type:"dead-end",severity:"warning",frameIds:r.map(h=>h.frameId),message:`${d.size} different font families across flow: ${m}. Flows should use 1-2 font families for consistency.`})}for(let m of r){let h=new Set;for(let k of r)if(k.frameId!==m.frameId)for(let N of k.fontFamilies)h.add(N);let C=Ws(m.fontFamilies,h);C.size>0&&r.length>2&&o.push({type:"dead-end",severity:"info",frameIds:[m.frameId],message:`"${m.frameName}" uses font${C.size>1?"s":""} not seen elsewhere: ${[...C].join(", ")}.`})}let l=new Set;for(let m of r)for(let h of m.fontSizes)l.add(h);l.size>10&&o.push({type:"dead-end",severity:"info",frameIds:r.map(m=>m.frameId),message:`${l.size} unique font sizes across the flow. Consider using a type scale with fewer sizes for consistency.`});let p=new Set;for(let m of r)for(let h of m.spacingValues)h>0&&p.add(h);let u=[...p].filter(m=>m%4!==0&&m!==2);return u.length>3&&o.push({type:"dead-end",severity:"info",frameIds:r.map(m=>m.frameId),message:`${u.length} non-standard spacing values across flow (${u.slice(0,4).join(", ")}px). Consider aligning to a 4px/8px grid.`}),o}var He=Ro(js()),qs=8e4,Js="baseline::";function Ke(e){return`${Js}${e}::meta`}function Ut(e,t){return`${Js}${e}::chunk_${t}`}function Xs(e){let t=JSON.stringify(e),n=(0,He.compressToUTF16)(t),s=[];for(let r=0;rMath.abs(m.delta)-Math.abs(f.delta));let r=new Set(e.errors.map(je)),i=new Set(t.errors.map(je)),a=[],c=[],d=[];for(let f of t.errors){let m=je(f);r.has(m)?d.push({errorType:f.errorType,severity:f.severity,nodeId:f.nodeId,message:f.message}):a.push({errorType:f.errorType,severity:f.severity,nodeId:f.nodeId,message:f.message})}for(let f of e.errors){let m=je(f);i.has(m)||c.push({errorType:f.errorType,severity:f.severity,nodeId:f.nodeId,message:f.message})}return{baselineTimestamp:e.timestamp,currentTimestamp:n,scoreDelta:{overall:t.overall-e.overall,oldOverall:e.overall,newOverall:t.overall,categories:o},newIssues:a,fixedIssues:c,remainingIssues:d,summary:{totalNew:a.length,totalFixed:c.length,totalRemaining:d.length,oldTotal:e.errors.length,newTotal:t.errors.length}}}ft();var Gi=["itemSpacing","paddingTop","paddingBottom","paddingLeft","paddingRight","counterAxisSpacing"];function Gt(e,t,n){let s=figma.getNodeById(e);if(!s)return{success:!1,nodeId:e,nodeName:"",property:t,oldValue:0,newValue:n,error:"Node not found"};if(s.type!=="FRAME"&&s.type!=="COMPONENT"&&s.type!=="INSTANCE")return{success:!1,nodeId:e,nodeName:s.name,property:t,oldValue:0,newValue:n,error:"Node is not a frame"};let o=s;if(o.layoutMode==="NONE")return{success:!1,nodeId:e,nodeName:s.name,property:t,oldValue:0,newValue:n,error:"Frame has no auto-layout"};try{let r=o[t];return o[t]=n,{success:!0,nodeId:e,nodeName:s.name,property:t,oldValue:r,newValue:n}}catch(r){return{success:!1,nodeId:e,nodeName:s.name,property:t,oldValue:0,newValue:n,error:r instanceof Error?r.message:String(r)}}}function Ae(e,t){let n=figma.getNodeById(e);if(!n||n.type!=="FRAME"&&n.type!=="COMPONENT"&&n.type!=="INSTANCE")return{success:!1,nodeId:e,nodeName:(n==null?void 0:n.name)||"",property:t,oldValue:0,newValue:0,error:"Invalid node"};let o=n[t];if(typeof o!="number")return{success:!1,nodeId:e,nodeName:n.name,property:t,oldValue:0,newValue:0,error:"Property is not a number"};if(mt.includes(o))return{success:!0,nodeId:e,nodeName:n.name,property:t,oldValue:o,newValue:o};let r=Ve(o);if(r.length===0)return{success:!1,nodeId:e,nodeName:n.name,property:t,oldValue:o,newValue:o,error:"No suggestion found"};let i=r.reduce((a,c)=>Math.abs(a-o)<=Math.abs(c-o)?a:c);return Gt(e,t,i)}function no(e){let t=figma.getNodeById(e);if(!t||t.type!=="FRAME"&&t.type!=="COMPONENT"&&t.type!=="INSTANCE")return[];let n=t;if(n.layoutMode==="NONE")return[];let s=[];for(let o of Gi){if(!(o in n))continue;let r=n[o];if(typeof r!="number"||mt.includes(r))continue;let i=Ae(e,o);s.push(i)}return s}function Ee(e,t){return t.length===0?e:t.reduce((n,s)=>Math.abs(s-e)0,c=Array.isArray(i.strokes)&&i.strokes.length>0,d=Array.isArray(i.effects)&&i.effects.length>0,l=r.type==="TEXT",p=r.type==="FRAME"||r.type==="COMPONENT"||r.type==="INSTANCE";(a||c||d||l||p)&&n++;let u=!1;if("boundVariables"in r&&i.boundVariables){let g=i.boundVariables;for(let f of Object.keys(g)){let m=g[f];if(Array.isArray(m))for(let h of m)h&&h.id&&(t.set(h.id,(t.get(h.id)||0)+1),u=!0);else m&&m.id&&(t.set(m.id,(t.get(m.id)||0)+1),u=!0)}}if(Array.isArray(i.fills)){for(let g of i.fills)if(g.boundVariables)for(let f of Object.keys(g.boundVariables)){let m=g.boundVariables[f];m&&m.id&&(t.set(m.id,(t.get(m.id)||0)+1),u=!0)}}if(u&&s++,"children"in r&&i.children)for(let g of i.children)o(g)}for(let r of e)o(r);return{consumerMap:t,totalEligible:n,boundCount:s}}async function qt(){let e=await figma.variables.getLocalVariableCollectionsAsync(),t=figma.currentPage.findAll(()=>!0),{consumerMap:n,totalEligible:s,boundCount:o}=Wi(t),r=[],i=0,a=[],c={};for(let p of e){let u=[];for(let g of p.modes)c[g.name]||(c[g.name]={total:0,withValue:0});for(let g of p.variableIds){let f=await figma.variables.getVariableByIdAsync(g);if(!f)continue;i++;let m=n.get(f.id)||0;m===0&&a.push(f.name);let h={};for(let[C,k]of Object.entries(f.valuesByMode))h[C]=Hi(k);for(let C of p.modes){c[C.name].total++;let k=f.valuesByMode[C.modeId];k!=null&&c[C.name].withValue++}u.push({id:f.id,name:f.name,resolvedType:f.resolvedType,description:f.description,valuesByMode:h,scopes:f.scopes,consumers:m})}r.push({id:p.id,name:p.name,modes:p.modes.map(g=>({modeId:g.modeId,name:g.name})),variables:u})}let d=s>0?Math.round(o/s*100):0,l={};for(let[p,u]of Object.entries(c))l[p]=u.total>0?Math.round(u.withValue/u.total*100):100;return{collections:r,totalVariables:i,unusedVariables:a,adoptionRate:d,modesCoverage:l}}function Hi(e){if(e==null||typeof e=="boolean"||typeof e=="string"||typeof e=="number")return e;if(typeof e=="object"&&e.type==="VARIABLE_ALIAS")return{type:"VARIABLE_ALIAS",id:e.id};if(typeof e=="object"&&"r"in e){let t=e,n=s=>{let o=Math.round(s*255).toString(16);return o.length===1?"0"+o:o};return t.a!==void 0&&t.a<1?`rgba(${Math.round(t.r*255)}, ${Math.round(t.g*255)}, ${Math.round(t.b*255)}, ${t.a.toFixed(2)})`:`#${n(t.r)}${n(t.g)}${n(t.b)}`}return e}function ro(e){let t=JSON.parse(e),n=[];return io(t,[],void 0,n),n}function io(e,t,n,s){let o=typeof e.$type=="string"?e.$type:n;if("$value"in e){let r=typeof e.$type=="string"?e.$type:n||"unknown",i=typeof e.$description=="string"?e.$description:void 0;s.push({path:[...t],name:t.join("."),$type:r,$value:e.$value,$description:i});return}for(let[r,i]of Object.entries(e))r.startsWith("$")||typeof i=="object"&&i!==null&&!Array.isArray(i)&&io(i,[...t,r],o,s)}function ao(e,t,n){let s=new Map;for(let C of t)s.set(Te(C.name),C);let o=[];for(let C of e.collections)for(let k of C.variables)o.push(k);let r=[],i=[],a=new Set,c=new Set;for(let C of o){let k=Te(C.name);if(c.add(k),s.has(k)){a.add(k),r.push({token:C.name,nodeCount:C.consumers,usage:C.consumers>0?"correct":"overridden"});continue}let N=Ki(k,t);N&&N.distance<=3?(a.add(Te(N.token.name)),r.push({token:C.name,nodeCount:C.consumers,usage:"correct"})):i.push({value:C.name,nodeCount:C.consumers,nearestToken:N?N.token.name:"(none)",distance:N?N.distance:1/0})}let d=[];for(let C of t)a.has(Te(C.name))||d.push(C.name);let l=i.filter(C=>C.nodeCount>0).map(C=>C.value),p=t.length,u=r.filter(C=>C.nodeCount>0).length,g=i.filter(C=>C.nodeCount>0).length,f=u+g,m=f>0?Math.round(u/f*100):p>0?0:100;return{adoptionScore:Math.round(e.adoptionRate*.5+m*.5),matched:r,unmatched:i,orphanTokens:d,missingFromSystem:l,summary:{totalTokenDefs:p,usedInDesign:u,hardCodedValues:g,compliance:m}}}function Te(e){return e.replace(/\//g,".").replace(/\s+/g,"-").toLowerCase().trim()}function Ki(e,t){if(t.length===0)return null;let n=null;for(let s of t){let o=ji(e,Te(s.name));if((!n||on)return n+1;[s,o]=[o,s]}return s[t.length]}async function co(e){let n=(await figma.variables.getLocalVariableCollectionsAsync()).find(i=>i.id===e);if(!n)throw new Error(`Collection not found: ${e}`);let s=n.modes.map(i=>({modeId:i.modeId,modeName:i.name})),o=[],r=[];for(let i of n.variableIds){let a=await figma.variables.getVariableByIdAsync(i);if(!a)continue;let c={},d=[],l=!1,p,u=!1;for(let g of n.modes){let f=a.valuesByMode[g.modeId];f==null?d.push(g.name):(c[g.name]=Jt(f),u?JSON.stringify(Jt(f))!==p&&(l=!0):(p=JSON.stringify(Jt(f)),u=!0))}(l||d.length>0)&&o.push({variableName:a.name,type:a.resolvedType,values:c}),d.length>0&&r.push({variableName:a.name,missingModes:d})}return{collection:n.name,modes:s,variableDiffs:o,missingValues:r}}function Jt(e){if(e==null||typeof e=="boolean"||typeof e=="string"||typeof e=="number")return e;if(typeof e=="object"&&e.type==="VARIABLE_ALIAS")return{type:"VARIABLE_ALIAS",id:e.id};if(typeof e=="object"&&"r"in e){let t=e,n=s=>{let o=Math.round(s*255).toString(16);return o.length===1?"0"+o:o};return t.a!==void 0&&t.a<1?`rgba(${Math.round(t.r*255)}, ${Math.round(t.g*255)}, ${Math.round(t.b*255)}, ${t.a.toFixed(2)})`:`#${n(t.r)}${n(t.g)}${n(t.b)}`}return e}be();J();var ce=null,le=null,Pe=new Set,Xe=!1;function lo(e){if(!ce||!ce.enabled)return;for(let n of e.documentChanges)(n.type==="PROPERTY_CHANGE"||n.type==="CREATE"||n.type==="STYLE_PROPERTY_CHANGE")&&"id"in n&&typeof n.id=="string"&&Pe.add(n.id);if(Pe.size===0)return;le!==null&&clearTimeout(le);let t=ce.debounceMs||500;le=setTimeout(()=>{qi()},t)}async function qi(){if(!ce)return;let e=Array.from(Pe);Pe.clear(),le=null;let t=[],n=[];for(let s of e)try{let o=await figma.getNodeByIdAsync(s);o&&"type"in o&&o.type!=="PAGE"&&o.type!=="DOCUMENT"&&(t.push(o),n.push(s))}catch(o){}if(t.length!==0)try{let s=ne(t,ce.settings);S("realtime-lint-update",{errors:s.errors,changedNodeIds:n})}catch(s){console.error("Realtime lint error:",s)}}function uo(e){ce={enabled:e.enabled,debounceMs:e.debounceMs||500,settings:e.settings||j},Xe||(figma.on("documentchange",lo),Xe=!0)}function po(){ce=null,Xe&&(figma.off("documentchange",lo),Xe=!1),le!==null&&(clearTimeout(le),le=null),Pe.clear()}var mo=/^(Frame|Group|Rectangle|Ellipse|Vector|Line|Polygon|Star|Component|Instance|Boolean)\s*\d+$/i;function fo(e,t){let n=e.errors,o=n.filter(m=>m.errorType==="fill"||m.errorType==="stroke"||m.errorType==="effect"||m.errorType==="text").length,i=n.filter(m=>m.message.toLowerCase().includes("detach")).length,a=t?t.hardCodedValues:0,c=new Set;for(let m of n)mo.test(m.nodeName)&&c.add(m.nodeId);let d=n.filter(m=>m.errorType==="accessibility"&&mo.test(m.nodeName));for(let m of d)c.add(m.nodeId);let l=c.size,p=n.filter(m=>m.errorType==="autoLayout").length,u=n.filter(m=>m.errorType==="spacing").length,g=100;return g-=o*2,g-=i*5,g-=a*1,g-=l*.5,g-=p*1,g-=u*.5,{overall:Math.max(0,Math.min(100,Math.round(g))),components:{orphanedStyles:{count:o,score:Math.max(0,Math.round(100-o*2))},detachedInstances:{count:i,score:Math.max(0,Math.round(100-i*5))},hardcodedValues:{count:a,score:Math.max(0,Math.round(100-a*1))},namingViolations:{count:l,score:Math.max(0,Math.round(100-l*.5))},missingAutoLayout:{count:p,score:Math.max(0,Math.round(100-p*1))},inconsistentSpacing:{count:u,score:Math.max(0,Math.round(100-u*.5))}}}}be();var Z=null,H="claude-sonnet-4-5-20250929",_="anthropic";function go(e,t=_){let n=(e==null?void 0:e.trim())||"";switch(t){case"anthropic":return n.startsWith("sk-ant-")&&n.length>=40;case"openai":return n.startsWith("sk-")&&n.length>=20;case"google":return n.startsWith("AIza")&&n.length>=35;default:return!1}}var yo=null,ho=null,Q=new Ds({enableCaching:!0,enableMCPIntegration:!0,mcpServerUrl:"https://design-systems-mcp.southleft-llc.workers.dev/mcp"});async function bo(e){let{type:t,data:n}=e,s=t==="save-api-key"?`${t} [redacted]`:t;console.log("Received message:",s);try{switch(t){case"check-api-key":await Ji();break;case"save-api-key":await Xi(n.apiKey,n.model,n.provider);break;case"update-model":await Yi(n.model);break;case"analyze":await Qi();break;case"analyze-enhanced":await vo(n);break;case"clear-api-key":await ea();break;case"chat-message":await ta(n);break;case"chat-clear-history":await na();break;case"select-node":await sa(n);break;case"preview-fix":await Oa(n);break;case"apply-token-fix":await Fa(n);break;case"apply-naming-fix":await Da(n);break;case"apply-batch-fix":await Va(n);break;case"update-description":await _a(n);break;case"add-component-property":await Ba(n);break;case"run-design-lint":Re(n);break;case"lint-ignore-node":la(n);break;case"lint-ignore-error":da(n);break;case"lint-ignore-all-of-type":ua(n);break;case"lint-clear-ignored":pa();break;case"lint-select-node":ma(n);break;case"lint-select-all-with-value":fa(n);break;case"lint-save-settings":ga(n);break;case"lint-load-settings":ya();break;case"lint-save-team-config":ha(n);break;case"lint-load-team-config":ba();break;case"jump-to-node":va(n);break;case"fix-spacing":ka(n);break;case"fix-spacing-to-nearest":wa(n);break;case"fix-all-spacing":Ia(n);break;case"apply-style-fix":await xa(n);break;case"rename-layer-fix":Aa(n);break;case"fix-radius-to-nearest":Ca(n);break;case"batch-fix-v2":await Ea(n);break;case"rescan-lint":So();break;case"export-screenshot":await Na(n);break;case"analyze-flow":await Ta();break;case"analyze-page":await Pa();break;case"save-baseline":La(n);break;case"load-baseline":Ra(n);break;case"compare-baseline":$a(n);break;case"delete-baseline":Ma(n);break;case"collect-variables":await Ua();break;case"check-dtcg-compliance":await Ga(n);break;case"compare-modes":await za(n);break;case"enable-realtime-lint":Wa(n);break;case"disable-realtime-lint":Ha();break;case"calculate-design-debt":Ka(n);break;default:console.warn("Unknown message type:",t)}}catch(o){console.error("Error handling message:",o);let r=o instanceof Error?o.message:"Unknown error occurred";S("analysis-error",{error:r})}}async function Ji(){try{await lt();let e=await dt();if(_=e.providerId,H=e.modelId,Z){S("api-key-status",{hasKey:!0,provider:_,model:H});return}e.apiKey&&go(e.apiKey,e.providerId)?(Z=e.apiKey,S("api-key-status",{hasKey:!0,provider:_,model:H})):S("api-key-status",{hasKey:!1,provider:_,model:H})}catch(e){console.error("Error checking API key:",e),S("api-key-status",{hasKey:!1,provider:"anthropic"})}}async function Xi(e,t,n){try{let s=n||_;if(!go(e,s)){let r=ie(s);throw new Error(`Invalid API key format for ${r.name}. Expected format: ${r.keyPlaceholder}`)}_=s,Z=e,t&&(H=t),await ut(s,H,e),console.log(`${s} API key and model saved successfully`);let o=ie(s);S("api-key-saved",{success:!0,provider:s}),figma.notify(`${o.name} API key saved successfully`,{timeout:2e3})}catch(s){console.error("Error saving API key:",s);let o=s instanceof Error?s.message:"Unknown error occurred";S("api-key-saved",{success:!1,error:o}),figma.notify(`Failed to save API key: ${o}`,{error:!0})}}async function Yi(e){try{H=e,await ut(_,e),console.log("Model updated to:",e),figma.notify(`Model updated to ${e}`,{timeout:2e3})}catch(t){console.error("Error updating model:",t),figma.notify("Failed to update model",{error:!0})}}async function vo(e){var t,n;try{if(!Z){let c=ie(_).name;throw new Error(`API key not found. Please save your ${c} API key first.`)}let s=figma.currentPage.selection;if(s.length===0)throw new Error("No component selected. Please select a Figma component to analyze.");if(e.batchMode&&s.length>1){await Zi(s,e);return}let o=s[0];if(o.type==="INSTANCE"){let c=o;try{let d=await c.getMainComponentAsync();if(d)figma.notify("Analyzing main component instead of instance...",{timeout:2e3}),o=d;else throw new Error("This instance has no main component. Please select a component directly.")}catch(d){throw console.error("Error accessing main component:",d),new Error("Could not access main component. Please select a component directly.")}}if(o.type==="COMPONENT"&&((t=o.parent)==null?void 0:t.type)==="COMPONENT_SET"){let d=o.parent;figma.notify("Analyzing parent component set to include all variants...",{timeout:2e3}),o=d}if(!Se(o)){let c=new Set(["COMPONENT_SET","COMPONENT","INSTANCE"]),d=null,l=null,p=o.parent;for(;p&&"type"in p;){let g=p;if(c.has(g.type)&&!d){d=g;break}!l&&Se(g)&&(l=g),p=p.parent}let u=d||l;u&&(figma.notify(`Analyzing parent ${u.type.toLowerCase()} "${u.name}"...`,{timeout:2e3}),o=u)}if(o.type==="INSTANCE"){let c=o;try{let d=await c.getMainComponentAsync();d&&(figma.notify("Analyzing main component instead of instance...",{timeout:2e3}),o=d)}catch(d){}}if(o.type==="COMPONENT"&&((n=o.parent)==null?void 0:n.type)==="COMPONENT_SET"){let c=o.parent;figma.notify("Analyzing parent component set to include all variants...",{timeout:2e3}),o=c}if(!Se(o))throw new Error("Please select a Frame, Component, Component Set, or Instance to analyze");await Q.loadDesignSystemsKnowledge();let r=await Lt(o),i=R({enableMCPEnhancement:!0,batchMode:e.batchMode||!1,enableAudit:e.enableAudit!==!1,includeTokenAnalysis:e.includeTokenAnalysis!==!1},e);figma.notify("Performing enhanced analysis with design systems knowledge...",{timeout:3e3});let a=await Os(r,Z,H,i,_);yo=a.metadata,ho=o,S("enhanced-analysis-result",K(R({},a),{analyzedNodeId:o.id})),figma.notify("Enhanced analysis complete! Check the results panel.",{timeout:3e3})}catch(s){console.error("Error during enhanced analysis:",s);let o=s instanceof Error?s.message:"Unknown error occurred";figma.notify(`Analysis failed: ${o}`,{error:!0}),S("analysis-error",{error:o})}}async function Qi(){await vo({batchMode:!1})}async function Zi(e,t){let n=[];await Q.loadDesignSystemsKnowledge();for(let r of e)if(Se(r))try{let i=await Lt(r),a=await ue(r),c=[...a.colors,...a.spacing,...a.typography,...a.effects,...a.borders],d=Q.generateComponentHash(i,c,D),l=Q.getCachedAnalysis(d);if(l){console.log(`\u2705 Using cached analysis for ${r.name}`),n.push({node:r.name,success:!0,data:l.result.metadata,cached:!0});continue}let p=Q.createDeterministicPrompt(i),u=await ae(_,Z,{prompt:p,model:H,maxTokens:2048,temperature:.1}),g=pe(u.content),f=Fe(g),m=await Rt(f,i,{batchMode:!0});Q.validateAnalysisConsistency(m,i)||(m=Q.applyConsistencyCorrections(m,i)),Q.cacheAnalysis(d,m),n.push({node:r.name,success:!0,data:m.metadata,cached:!1})}catch(i){n.push({node:r.name,success:!1,error:i instanceof Error?i.message:"Analysis failed"})}let s=n.filter(r=>r.success&&r.cached).length,o=n.filter(r=>r.success&&!r.cached).length;S("batch-analysis-result",{results:n}),figma.notify(`Batch analysis complete: ${o} analyzed, ${s} from cache`,{timeout:3e3})}async function ea(){try{Z=null,await on(_),await figma.clientStorage.setAsync("claude-api-key","");let e=ie(_).name;S("api-key-cleared",{success:!0}),figma.notify(`${e} API key cleared`,{timeout:2e3})}catch(e){console.error("Error clearing API key:",e)}}async function ta(e){try{if(console.log("Processing chat message:",e.message),!Z){let i=ie(_).name;throw new Error(`API key not found. Please save your ${i} API key first.`)}S("chat-response-loading",{isLoading:!0});let t=aa(),n=await ia(e.message),s=ca(e.message,n,e.history,t),r={message:(await ae(_,Z,{prompt:s,model:H,maxTokens:2048,temperature:.7})).content,sources:n.sources||[]};S("chat-response",{response:r})}catch(t){console.error("Error handling chat message:",t);let n=t instanceof Error?t.message:"Unknown error occurred";S("chat-error",{error:n})}}async function na(){try{S("chat-history-cleared",{success:!0}),figma.notify("Chat history cleared",{timeout:2e3})}catch(e){console.error("Error clearing chat history:",e)}}async function sa(e){try{console.log("\u{1F3AF} Attempting to select node:",e.nodeId);let t=await figma.getNodeByIdAsync(e.nodeId);if(!t){console.warn("\u26A0\uFE0F Node not found:",e.nodeId),figma.notify("Node not found - it may have been deleted or moved",{error:!0});return}if(!oa(t)){console.warn("\u26A0\uFE0F Node is not on current page:",e.nodeId),figma.notify("Node is on a different page",{error:!0});return}figma.currentPage.selection=[t],figma.viewport.scrollAndZoomIntoView([t]),console.log("\u2705 Successfully selected and zoomed to node:",t.name),figma.notify(`Selected "${t.name}"`,{timeout:2e3})}catch(t){console.error("Error selecting node:",t);let n=t instanceof Error?t.message:"Unknown error occurred";figma.notify(`Failed to select node: ${n}`,{error:!0})}}function oa(e){try{let t=e,n=50,s=0;for(;t&&t.parent&&ss.id===t)}catch(n){return!1}}async function ia(e){var t;try{console.log("\u{1F50D} Querying MCP for chat:",e);let n=((t=Q.config)==null?void 0:t.mcpServerUrl)||"https://design-systems-mcp.southleft-llc.workers.dev/mcp",s=[Xt(n,e,{category:"general",limit:3}),e.toLowerCase().includes("component")?Xt(n,e,{category:"components",limit:2}):Promise.resolve({results:[]}),e.toLowerCase().includes("token")||e.toLowerCase().includes("design token")?Xt(n,e,{category:"tokens",limit:2}):Promise.resolve({results:[]})],o=await Promise.allSettled(s),r=[];return o.forEach(i=>{i.status==="fulfilled"&&i.value.results&&r.push(...i.value.results)}),console.log(`\u2705 Found ${r.length} relevant sources for chat query`),{sources:r.slice(0,5)}}catch(n){return console.warn("\u26A0\uFE0F MCP query failed for chat:",n),{sources:[]}}}async function Xt(e,t,n={}){let s={jsonrpc:"2.0",id:Math.floor(Math.random()*1e3)+100,method:"tools/call",params:{name:"search_design_knowledge",arguments:R({query:t,limit:n.limit||5},n.category&&{category:n.category})}},o=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!o.ok)throw new Error(`MCP search failed: ${o.status}`);let r=await o.json();return r.result&&r.result.content?{results:r.result.content.map(i=>({title:i.title||"Design System Knowledge",content:i.content||i.description||"",category:i.category||"general"}))}:{results:[]}}function aa(){try{let e=yo,t=ho;if(!e&&!t)return null;let n={hasCurrentComponent:!0,timestamp:Date.now()};if(t){n.component={name:t.name,type:t.type,id:t.id};let s=figma.currentPage.selection;s.length>0&&(n.selection={count:s.length,types:s.map(o=>o.type),names:s.map(o=>o.name)})}return e&&(n.analysis={component:e.component,description:e.description,props:e.props||[],states:e.states||[],accessibility:e.accessibility,audit:e.audit,mcpReadiness:e.mcpReadiness}),n}catch(e){return console.warn("Failed to get component context:",e),null}}function ca(e,t,n,s){let o="";n.length>0&&(o=` + `}loadFallbackKnowledge(){this.designSystemsKnowledge={version:"1.0.0-fallback",components:{button:"Button components require comprehensive state management",avatar:"Avatar components should support size variants and interactive states",card:"Card components need consistent spacing and content hierarchy",badge:"Badge components should use semantic colors for status indication",input:"Input components require comprehensive accessibility and validation",generic:"Generic components should follow basic design system principles"},tokens:"Use semantic token naming: semantic-color-primary, spacing-md-16px, text-size-lg-18px",accessibility:"Ensure WCAG 2.1 AA compliance with proper ARIA labels and keyboard support",scoring:this.getFallbackScoringCriteria(),lastUpdated:Date.now()}}isValidScore(t){return typeof t=="number"&&t>=0&&t<=100}validateComponentFamilyConsistency(t,n){let s=t.metadata;switch(n){case"button":return this.validateButtonComponent(s);case"avatar":return this.validateAvatarComponent(s);case"input":return this.validateInputComponent(s);default:return!0}}validateButtonComponent(t){var s;return((s=t.states)==null?void 0:s.some(o=>["hover","focus","active","disabled"].includes(o.toLowerCase())))||!1}validateAvatarComponent(t){var o,i,r;let n=((i=(o=t.variants)==null?void 0:o.size)==null?void 0:i.length)>0,s=(r=t.props)==null?void 0:r.some(a=>a.name.toLowerCase().includes("size"));return n||s||!1}validateInputComponent(t){var s;return((s=t.states)==null?void 0:s.some(o=>["focus","error","disabled","filled"].includes(o.toLowerCase())))||!1}validateTokenRecommendations(t){var s;return((s=t.colors)==null?void 0:s.some(o=>o.name.includes("semantic-")||o.name.includes("primary")||o.name.includes("secondary")))!==!1}applyComponentFamilyCorrections(t,n){var o,i,r;let s=R({},t);switch(n){case"button":(o=s.states)!=null&&o.includes("hover")||(s.states=[...s.states||[],"hover","focus","active","disabled"]);break;case"avatar":!((i=s.variants)!=null&&i.size)&&!((r=s.props)!=null&&r.some(a=>a.name.includes("size")))&&(s.variants=j(R({},s.variants),{size:["small","medium","large"]}));break}return s}applyTokenConsistencyCorrections(t){return t&&R({},t)}ensureConsistentScoring(t,n){return j(R({},t),{score:t.score||0})}},so=Jt;q();function Yt(e,t,n){let s=g=>g<=.04045?g/12.92:Math.pow((g+.055)/1.055,2.4),o=s(e),i=s(t),r=s(n),a=(o*.4124564+i*.3575761+r*.1804375)/.95047,c=o*.2126729+i*.7151522+r*.072175,l=(o*.0193339+i*.119192+r*.9503041)/1.08883,d=g=>g>.008856?Math.cbrt(g):7.787*g+16/116,u=d(a),f=d(c),m=d(l);return{L:116*f-16,a:500*(u-f),b:200*(f-m)}}function oo(e,t){let{L:n,a:s,b:o}=e,{L:i,a:r,b:a}=t,c=1,l=1,d=1,u=Math.sqrt(s*s+o*o),f=Math.sqrt(r*r+a*a),m=(u+f)/2,g=Math.pow(m,7),p=.5*(1-Math.sqrt(g/(g+6103515625))),h=s*(1+p),C=r*(1+p),k=Math.sqrt(h*h+o*o),N=Math.sqrt(C*C+a*a),y=Math.atan2(o,h)*180/Math.PI,b=Math.atan2(a,C)*180/Math.PI,x=(y%360+360)%360,I=(b%360+360)%360,L=i-n,O=N-k,M;k*N===0?M=0:Math.abs(I-x)<=180?M=I-x:I-x>180?M=I-x-360:M=I-x+360;let z=2*Math.sqrt(k*N)*Math.sin(M*Math.PI/360),A=(n+i)/2,$=(k+N)/2,v;k*N===0?v=x+I:Math.abs(x-I)<=180?v=(x+I)/2:x+I<360?v=(x+I+360)/2:v=(x+I-360)/2;let T=1-.17*Math.cos((v-30)*Math.PI/180)+.24*Math.cos(2*v*Math.PI/180)+.32*Math.cos((3*v+6)*Math.PI/180)-.2*Math.cos((4*v-63)*Math.PI/180),P=1+.015*Math.pow(A-50,2)/Math.sqrt(20+Math.pow(A-50,2)),w=1+.045*$,te=1+.015*$*T,Q=Math.pow($,7),Ce=-2*Math.sqrt(Q/(Q+6103515625))*Math.sin(60*Math.exp(-Math.pow((v-275)/25,2))*Math.PI/180);return Math.sqrt(Math.pow(L/(c*P),2)+Math.pow(O/(l*w),2)+Math.pow(z/(d*te),2)+Ce*(O/(l*w))*(z/(d*te)))}async function La(e,t,n,s=0){try{if(!(t in e))return{success:!1,message:`Node does not support ${t}`,error:`Property ${t} not found on node type ${e.type}`};let o=await figma.variables.getVariableByIdAsync(n);if(!o)return{success:!1,message:"Variable not found",error:`Could not find variable with ID: ${n}`};if(o.resolvedType!=="COLOR")return{success:!1,message:"Variable is not a color type",error:`Variable ${o.name} is of type ${o.resolvedType}, expected COLOR`};let r=[...e[t]];if(s>=r.length)return{success:!1,message:"Paint index out of range",error:`Paint index ${s} does not exist. Node has ${r.length} ${t}.`};let a=r[s];if(a.type!=="SOLID")return{success:!1,message:"Can only bind to solid paints",error:`Paint at index ${s} is of type ${a.type}, expected SOLID`};let c=figma.variables.setBoundVariableForPaint(a,"color",o);return r[s]=c,t==="fills"?e.fills=r:e.strokes=r,{success:!0,message:`Successfully bound ${o.name} to ${t}[${s}]`,appliedFix:{nodeId:e.id,nodeName:e.name,propertyPath:`${t}[${s}]`,beforeValue:a.type==="SOLID"&&a.color?F(a.color.r,a.color.g,a.color.b):"unknown",afterValue:o.name,tokenId:n,tokenName:o.name,fixType:"color"}}}catch(o){return{success:!1,message:"Failed to bind color token",error:o instanceof Error?o.message:String(o)}}}async function io(e,t,n){try{if(!(t in e))return{success:!1,message:`Node does not support ${t}`,error:`Property ${t} not found on node type ${e.type}`};let s=await figma.variables.getVariableByIdAsync(n);if(!s)return{success:!1,message:"Variable not found",error:`Could not find variable with ID: ${n}`};if(s.resolvedType!=="FLOAT")return{success:!1,message:"Variable is not a number type",error:`Variable ${s.name} is of type ${s.resolvedType}, expected FLOAT`};let o=e[t];return e.setBoundVariable(t,s),{success:!0,message:`Successfully bound ${s.name} to ${t}`,appliedFix:{nodeId:e.id,nodeName:e.name,propertyPath:t,beforeValue:typeof o=="number"?`${o}px`:String(o),afterValue:s.name,tokenId:n,tokenName:s.name,fixType:t.includes("Radius")?"border":"spacing"}}}catch(s){return{success:!1,message:"Failed to bind spacing token",error:s instanceof Error?s.message:String(s)}}}async function Qt(e,t=0){try{let n=Ra(e);if(!n)return[];let s=[],o=await figma.variables.getLocalVariablesAsync("COLOR"),i=await figma.variables.getLocalVariableCollectionsAsync(),r=new Map;for(let a of i)r.set(a.id,a);for(let a of o){let c=r.get(a.variableCollectionId);if(!c)continue;let l=c.modes[0].modeId,d=a.valuesByMode[l];if(!d||typeof d!="object"||!("r"in d))continue;let u=d,f=$a(n,u);f>=1-t&&s.push({variableId:a.id,variableName:a.name,collectionName:c.name,value:F(u.r,u.g,u.b),matchScore:f,type:"color"})}return s.sort((a,c)=>c.matchScore-a.matchScore)}catch(n){return console.error("Error finding matching color variable:",n),[]}}async function Pa(e,t=0){try{let n=[],s=await figma.variables.getLocalVariablesAsync("FLOAT"),o=await figma.variables.getLocalVariableCollectionsAsync(),i=new Map;for(let r of o)i.set(r.id,r);for(let r of s){let a=i.get(r.variableCollectionId);if(!a)continue;let c=a.modes[0].modeId,l=r.valuesByMode[c];if(typeof l!="number")continue;let d=Math.abs(l-e);if(d<=t){let u=d===0?1:1-d/(t||1);n.push({variableId:r.id,variableName:r.name,collectionName:a.name,value:`${l}px`,matchScore:u,type:"number"})}}return n.sort((r,a)=>a.matchScore-r.matchScore)}catch(n){return console.error("Error finding matching spacing variable:",n),[]}}async function Zt(e,t,n=2){let s=await Pa(e,n);if(s.length===0)return s;let i={strokeWeight:["stroke","border-width","border/width","borderwidth"],cornerRadius:["radius","corner","round","border-radius"],topLeftRadius:["radius","corner","round"],topRightRadius:["radius","corner","round"],bottomLeftRadius:["radius","corner","round"],bottomRightRadius:["radius","corner","round"],paddingTop:["padding","spacing","space"],paddingRight:["padding","spacing","space"],paddingBottom:["padding","spacing","space"],paddingLeft:["padding","spacing","space"],itemSpacing:["gap","spacing","space"],counterAxisSpacing:["gap","spacing","space"]}[t]||[];return i.length===0?s:s.map(a=>{let c=a.variableName.toLowerCase(),l=i.some(d=>c.includes(d));return j(R({},a),{matchScore:l?Math.min(a.matchScore+.3,1):a.matchScore})}).sort((a,c)=>c.matchScore-a.matchScore)}async function en(e,t,n){let s=t.match(/^(fills|strokes)\[(\d+)\]$/);if(!s)return{success:!1,message:"Invalid property path",error:`Expected format: fills[n] or strokes[n], got: ${t}`};let[,o,i]=s,r=parseInt(i,10);return La(e,o,n,r)}async function tn(e,t,n){if(!["paddingTop","paddingRight","paddingBottom","paddingLeft","itemSpacing","counterAxisSpacing","cornerRadius","topLeftRadius","topRightRadius","bottomLeftRadius","bottomRightRadius","strokeWeight"].includes(t))return{success:!1,message:"Invalid property path",error:`Property ${t} is not a valid spacing property`};if(t==="cornerRadius"){let o=["topLeftRadius","topRightRadius","bottomLeftRadius","bottomRightRadius"],i=[];for(let r of o){let a=await io(e,r,n);if(i.push(a),!a.success)return{success:!1,message:`Failed to bind ${r}`,error:a.error}}return{success:!0,message:"Successfully bound variable to all 4 corner radii",appliedFix:i[0].appliedFix?j(R({},i[0].appliedFix),{propertyPath:"cornerRadius"}):void 0}}return io(e,t,n)}async function nn(e,t,n){try{let s=await figma.variables.getVariableByIdAsync(n);if(!s)return null;let o,i,r=t.match(/^(fills|strokes)\[(\d+)\]$/);if(r){o="color";let[,l,d]=r,u=parseInt(d,10);if(!(l in e))return null;let m=e[l];if(u>=m.length)return null;let g=m[u];g.type==="SOLID"&&g.color?i=F(g.color.r,g.color.g,g.color.b):i=g.type}else{if(!(t in e))return null;let l=e[t];i=typeof l=="number"?`${l}px`:String(l),o=t.includes("Radius")?"border":"spacing"}let a=s.name,c=await figma.variables.getVariableCollectionByIdAsync(s.variableCollectionId);if(c){let l=c.modes[0].modeId,d=s.valuesByMode[l];if(typeof d=="number")a=`${s.name} (${d}px)`;else if(d&&typeof d=="object"&&"r"in d){let u=d;a=`${s.name} (${F(u.r,u.g,u.b)})`}}return{nodeId:e.id,nodeName:e.name,propertyPath:t,beforeValue:i,afterValue:a,tokenId:n,tokenName:s.name,fixType:o}}catch(s){return console.error("Error generating fix preview:",s),null}}function Ra(e){let t=e.replace(/^#/,""),n=t;if(t.length===3&&(n=t[0]+t[0]+t[1]+t[1]+t[2]+t[2]),n.length!==6)return null;let s=parseInt(n.substring(0,2),16),o=parseInt(n.substring(2,4),16),i=parseInt(n.substring(4,6),16);return isNaN(s)||isNaN(o)||isNaN(i)?null:{r:s/255,g:o/255,b:i/255}}function $a(e,t){let n=Yt(e.r,e.g,e.b),s=Yt(t.r,t.g,t.b),o=oo(n,s);return o<3?1:o>=10?0:1-(o-3)/7}async function et(e,t=1024){let n=Math.max(1,Math.min(t,Math.round(e.width))),s=await e.exportAsync({format:"PNG",constraint:{type:"WIDTH",value:n}});return Ma(s)}function Ma(e){let t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",n="",s=e.length;for(let o=0;o>2],n+=t[(i&3)<<4|r>>4],n+=o+1>6]:"=",n+=o+20)for(let r of i){let a=r.actions||(r.action?[r.action]:[]);for(let c of a)c.type==="NODE"&&c.destinationId&&n.push({sourceFrameId:t,sourceNodeId:e.id,sourceNodeName:e.name,destinationFrameId:c.destinationId,trigger:((s=r.trigger)==null?void 0:s.type)||"UNKNOWN",navigation:c.navigation||"NAVIGATE",hasTransition:!!c.transition}),c.type==="BACK"&&n.push({sourceFrameId:t,sourceNodeId:e.id,sourceNodeName:e.name,destinationFrameId:"__BACK__",trigger:((o=r.trigger)==null?void 0:o.type)||"UNKNOWN",navigation:"BACK",hasTransition:!!c.transition})}}if("children"in e)for(let i of e.children)ao(i,t,n)}function co(e){var z,A,$;let t=e||figma.currentPage,n=t.children.filter(v=>v.type==="FRAME"||v.type==="COMPONENT"),s=new Set((t.flowStartingPoints||[]).map(v=>v.nodeId)),o=n.map(v=>({id:v.id,name:v.name,pageId:t.id,pageName:t.name,width:v.width,height:v.height,isFlowStartingPoint:s.has(v.id),childCount:"children"in v?v.children.length:0,hasInteractiveElements:ro(v)})),i=new Set(o.map(v=>v.id)),r=[];for(let v of n)ao(v,v.id,r);let a=r.filter(v=>v.destinationFrameId==="__BACK__"||i.has(v.destinationFrameId)),c=o.filter(v=>v.isFlowStartingPoint).map(v=>v.id),l=new Map,d=new Map;for(let v of i)l.set(v,new Set),d.set(v,new Set);for(let v of a)v.destinationFrameId!=="__BACK__"&&((z=l.get(v.sourceFrameId))==null||z.add(v.destinationFrameId),(A=d.get(v.destinationFrameId))==null||A.add(v.sourceFrameId));let u=new Set;for(let v of a)v.destinationFrameId==="__BACK__"&&u.add(v.sourceFrameId);let f=o.filter(v=>{var T;return(((T=l.get(v.id))==null?void 0:T.size)||0)===0&&!u.has(v.id)}).map(v=>v.id),m=o.filter(v=>{var T;return(((T=d.get(v.id))==null?void 0:T.size)||0)===0&&!s.has(v.id)}).map(v=>v.id),g=new Set,p=[...c];if(p.length===0)for(let v of o)((($=d.get(v.id))==null?void 0:$.size)||0)===0&&p.push(v.id);for(;p.length>0;){let v=p.shift();if(g.has(v))continue;g.add(v);let T=l.get(v);if(T)for(let P of T)g.has(P)||p.push(P)}let h=o.filter(v=>!g.has(v.id)).map(v=>v.id),C=[],k=new Set,N=new Set,y=[];function b(v){if(N.has(v)){let P=y.indexOf(v);P!==-1&&C.push(y.slice(P));return}if(k.has(v))return;k.add(v),N.add(v),y.push(v);let T=l.get(v);if(T)for(let P of T)b(P);y.pop(),N.delete(v)}for(let v of i)b(v);let x=o.map(v=>{var T;return((T=l.get(v.id))==null?void 0:T.size)||0}),I=x.length>0?x.reduce((v,T)=>v+T,0)/x.length:0,L=0,O=c.map(v=>({id:v,depth:0})),M=new Set;for(;O.length>0;){let{id:v,depth:T}=O.shift();if(M.has(v))continue;M.add(v),T>L&&(L=T);let P=l.get(v);if(P)for(let w of P)M.has(w)||O.push({id:w,depth:T+1})}return{frames:o,edges:a,entryPoints:c,deadEnds:f,orphans:m,unreachable:h,loops:C,stats:{totalFrames:o.length,totalEdges:a.length,totalEntryPoints:c.length,maxDepth:L,avgBranching:Math.round(I*100)/100}}}function lo(e){let t=[],n=new Map(e.frames.map(r=>[r.id,r.name])),s=r=>r.map(a=>`"${n.get(a)||a}"`).join(", ");for(let r of e.deadEnds){let a=n.get(r)||"";/success|confirm|done|complete|thank|receipt|summary/i.test(a)||t.push({type:"dead-end",severity:"warning",frameIds:[r],message:`${s([r])} has no outgoing connections \u2014 user gets stuck here.`})}e.orphans.length>0&&t.push({type:"orphan",severity:"warning",frameIds:e.orphans,message:`${s(e.orphans)} ${e.orphans.length===1?"has":"have"} no incoming connections \u2014 unreachable by navigation.`});let o=e.unreachable.filter(r=>!e.orphans.includes(r));o.length>0&&t.push({type:"unreachable",severity:"critical",frameIds:o,message:`${s(o)} ${o.length===1?"is":"are"} not reachable from any flow entry point.`});for(let r of e.loops){let a=new Set(r);r.some(l=>e.edges.filter(u=>u.sourceFrameId===l).some(u=>!a.has(u.destinationFrameId)))||t.push({type:"loop",severity:"warning",frameIds:r,message:`Circular flow without exit: ${s(r)}. User cannot leave this loop.`})}e.stats.maxDepth>3&&t.push({type:"deep-navigation",severity:"info",frameIds:[],message:`Navigation depth is ${e.stats.maxDepth} levels. Consider flattening to \u22643 levels for better UX (3-click rule).`});let i=e.frames.filter(r=>{if(r.isFlowStartingPoint)return!1;let a=e.edges.some(l=>l.sourceFrameId===r.id&&(l.navigation==="BACK"||l.navigation==="CLOSE"));return e.edges.some(l=>l.destinationFrameId===r.id)&&!a});return i.length>0&&t.push({type:"missing-back",severity:"info",frameIds:i.map(r=>r.id),message:`${i.length} frame${i.length===1?"":"s"} missing back/close navigation: ${s(i.map(r=>r.id))}.`}),t}q();function fo(e,t,n,s){if(!(n&&"locked"in e&&e.locked)&&!(s&&"visible"in e&&!e.visible)){if("fills"in e){let o=e.fills;if(o!==figma.mixed&&Array.isArray(o))for(let i of o)i.type==="SOLID"&&i.visible!==!1&&t.colors.add(F(i.color.r,i.color.g,i.color.b))}if("strokes"in e){let o=e.strokes;if(Array.isArray(o))for(let i of o)i.type==="SOLID"&&i.visible!==!1&&t.colors.add(F(i.color.r,i.color.g,i.color.b))}if(e.type==="TEXT"){let o=e;o.fontName!==figma.mixed&&t.fontFamilies.add(o.fontName.family),o.fontSize!==figma.mixed&&t.fontSizes.add(o.fontSize)}if("layoutMode"in e&&e.layoutMode!=="NONE"){let o=e;typeof o.itemSpacing=="number"&&t.spacingValues.add(o.itemSpacing),typeof o.paddingTop=="number"&&t.spacingValues.add(o.paddingTop),typeof o.paddingBottom=="number"&&t.spacingValues.add(o.paddingBottom),typeof o.paddingLeft=="number"&&t.spacingValues.add(o.paddingLeft),typeof o.paddingRight=="number"&&t.spacingValues.add(o.paddingRight)}if(e.type==="INSTANCE"){let o=e.mainComponent;o&&t.componentNames.add(o.name)}if("children"in e)for(let o of e.children)fo(o,t,n,s)}}function uo(e,t){let n=new Set;for(let s of e)t.has(s)||n.add(s);return n}function po(e,t={}){var m,g;let n=(m=t.skipLocked)!=null?m:!0,s=(g=t.skipHidden)!=null?g:!0,o=[];if(e.length<2)return o;let i=e.map(({frame:p,node:h})=>{let C={frameId:p.id,frameName:p.name,colors:new Set,fontFamilies:new Set,fontSizes:new Set,spacingValues:new Set,componentNames:new Set};return fo(h,C,n,s),C}),r=new Map;for(let p of i)for(let h of p.colors)r.set(h,(r.get(h)||0)+1);let a=i.length*.5,c=new Set;for(let[p,h]of r)h>=a&&c.add(p);for(let p of i){let h=uo(p.colors,c);h.size>3&&o.push({type:"dead-end",severity:"warning",frameIds:[p.frameId],message:`"${p.frameName}" uses ${h.size} colors not found in other screens (${[...h].slice(0,3).join(", ")}${h.size>3?"...":""}). Check for color inconsistency.`})}let l=new Set;for(let p of i)for(let h of p.fontFamilies)l.add(h);if(l.size>3){let p=[...l].join(", ");o.push({type:"dead-end",severity:"warning",frameIds:i.map(h=>h.frameId),message:`${l.size} different font families across flow: ${p}. Flows should use 1-2 font families for consistency.`})}for(let p of i){let h=new Set;for(let k of i)if(k.frameId!==p.frameId)for(let N of k.fontFamilies)h.add(N);let C=uo(p.fontFamilies,h);C.size>0&&i.length>2&&o.push({type:"dead-end",severity:"info",frameIds:[p.frameId],message:`"${p.frameName}" uses font${C.size>1?"s":""} not seen elsewhere: ${[...C].join(", ")}.`})}let d=new Set;for(let p of i)for(let h of p.fontSizes)d.add(h);d.size>10&&o.push({type:"dead-end",severity:"info",frameIds:i.map(p=>p.frameId),message:`${d.size} unique font sizes across the flow. Consider using a type scale with fewer sizes for consistency.`});let u=new Set;for(let p of i)for(let h of p.spacingValues)h>0&&u.add(h);let f=[...u].filter(p=>p%4!==0&&p!==2);return f.length>3&&o.push({type:"dead-end",severity:"info",frameIds:i.map(p=>p.frameId),message:`${f.length} non-standard spacing values across flow (${f.slice(0,4).join(", ")}px). Consider aligning to a 4px/8px grid.`}),o}var nt=Ai(mo()),go=8e4,yo="baseline::";function st(e){return`${yo}${e}::meta`}function on(e,t){return`${yo}${e}::chunk_${t}`}function ho(e){let t=JSON.stringify(e),n=(0,nt.compressToUTF16)(t),s=[];for(let i=0;iMath.abs(p.delta)-Math.abs(g.delta));let i=new Set(e.errors.map(ot)),r=new Set(t.errors.map(ot)),a=[],c=[],l=[];for(let g of t.errors){let p=ot(g);i.has(p)?l.push({errorType:g.errorType,severity:g.severity,nodeId:g.nodeId,message:g.message}):a.push({errorType:g.errorType,severity:g.severity,nodeId:g.nodeId,message:g.message})}for(let g of e.errors){let p=ot(g);r.has(p)||c.push({errorType:g.errorType,severity:g.severity,nodeId:g.nodeId,message:g.message})}return{baselineTimestamp:e.timestamp,currentTimestamp:n,scoreDelta:{overall:t.overall-e.overall,oldOverall:e.overall,newOverall:t.overall,categories:o},newIssues:a,fixedIssues:c,remainingIssues:l,summary:{totalNew:a.length,totalFixed:c.length,totalRemaining:l.length,oldTotal:e.errors.length,newTotal:t.errors.length}}}Xe();var Fa=["itemSpacing","paddingTop","paddingBottom","paddingLeft","paddingRight","counterAxisSpacing"];function rn(e,t,n){let s=figma.getNodeById(e);if(!s)return{success:!1,nodeId:e,nodeName:"",property:t,oldValue:0,newValue:n,error:"Node not found"};if(s.type!=="FRAME"&&s.type!=="COMPONENT"&&s.type!=="INSTANCE")return{success:!1,nodeId:e,nodeName:s.name,property:t,oldValue:0,newValue:n,error:"Node is not a frame"};let o=s;if(o.layoutMode==="NONE")return{success:!1,nodeId:e,nodeName:s.name,property:t,oldValue:0,newValue:n,error:"Frame has no auto-layout"};try{let i=o[t];return o[t]=n,{success:!0,nodeId:e,nodeName:s.name,property:t,oldValue:i,newValue:n}}catch(i){return{success:!1,nodeId:e,nodeName:s.name,property:t,oldValue:0,newValue:n,error:i instanceof Error?i.message:String(i)}}}function Le(e,t){let n=figma.getNodeById(e);if(!n||n.type!=="FRAME"&&n.type!=="COMPONENT"&&n.type!=="INSTANCE")return{success:!1,nodeId:e,nodeName:(n==null?void 0:n.name)||"",property:t,oldValue:0,newValue:0,error:"Invalid node"};let o=n[t];if(typeof o!="number")return{success:!1,nodeId:e,nodeName:n.name,property:t,oldValue:0,newValue:0,error:"Property is not a number"};if(Pt.includes(o))return{success:!0,nodeId:e,nodeName:n.name,property:t,oldValue:o,newValue:o};let i=qe(o);if(i.length===0)return{success:!1,nodeId:e,nodeName:n.name,property:t,oldValue:o,newValue:o,error:"No suggestion found"};let r=i.reduce((a,c)=>Math.abs(a-o)<=Math.abs(c-o)?a:c);return rn(e,t,r)}function Io(e){let t=figma.getNodeById(e);if(!t||t.type!=="FRAME"&&t.type!=="COMPONENT"&&t.type!=="INSTANCE")return[];let n=t;if(n.layoutMode==="NONE")return[];let s=[];for(let o of Fa){if(!(o in n))continue;let i=n[o];if(typeof i!="number"||Pt.includes(i))continue;let r=Le(e,o);s.push(r)}return s}function Pe(e,t){return t.length===0?e:t.reduce((n,s)=>Math.abs(s-e)0,c=Array.isArray(r.strokes)&&r.strokes.length>0,l=Array.isArray(r.effects)&&r.effects.length>0,d=i.type==="TEXT",u=i.type==="FRAME"||i.type==="COMPONENT"||i.type==="INSTANCE";(a||c||l||d||u)&&n++;let f=!1;if("boundVariables"in i&&r.boundVariables){let m=r.boundVariables;for(let g of Object.keys(m)){let p=m[g];if(Array.isArray(p))for(let h of p)h&&h.id&&(t.set(h.id,(t.get(h.id)||0)+1),f=!0);else p&&p.id&&(t.set(p.id,(t.get(p.id)||0)+1),f=!0)}}if(Array.isArray(r.fills)){for(let m of r.fills)if(m.boundVariables)for(let g of Object.keys(m.boundVariables)){let p=m.boundVariables[g];p&&p.id&&(t.set(p.id,(t.get(p.id)||0)+1),f=!0)}}if(f&&s++,"children"in i&&r.children)for(let m of r.children)o(m)}for(let i of e)o(i);return{consumerMap:t,totalEligible:n,boundCount:s}}async function fn(){let e=await figma.variables.getLocalVariableCollectionsAsync(),t=figma.currentPage.findAll(()=>!0),{consumerMap:n,totalEligible:s,boundCount:o}=Da(t),i=[],r=0,a=[],c={};for(let u of e){let f=[];for(let m of u.modes)c[m.name]||(c[m.name]={total:0,withValue:0});for(let m of u.variableIds){let g=await figma.variables.getVariableByIdAsync(m);if(!g)continue;r++;let p=n.get(g.id)||0;p===0&&a.push(g.name);let h={};for(let[C,k]of Object.entries(g.valuesByMode))h[C]=_a(k);for(let C of u.modes){c[C.name].total++;let k=g.valuesByMode[C.modeId];k!=null&&c[C.name].withValue++}f.push({id:g.id,name:g.name,resolvedType:g.resolvedType,description:g.description,valuesByMode:h,scopes:g.scopes,consumers:p})}i.push({id:u.id,name:u.name,modes:u.modes.map(m=>({modeId:m.modeId,name:m.name})),variables:f})}let l=s>0?Math.round(o/s*100):0,d={};for(let[u,f]of Object.entries(c))d[u]=f.total>0?Math.round(f.withValue/f.total*100):100;return{collections:i,totalVariables:r,unusedVariables:a,adoptionRate:l,modesCoverage:d}}function _a(e){if(e==null||typeof e=="boolean"||typeof e=="string"||typeof e=="number")return e;if(typeof e=="object"&&e.type==="VARIABLE_ALIAS")return{type:"VARIABLE_ALIAS",id:e.id};if(typeof e=="object"&&"r"in e){let t=e,n=s=>{let o=Math.round(s*255).toString(16);return o.length===1?"0"+o:o};return t.a!==void 0&&t.a<1?`rgba(${Math.round(t.r*255)}, ${Math.round(t.g*255)}, ${Math.round(t.b*255)}, ${t.a.toFixed(2)})`:`#${n(t.r)}${n(t.g)}${n(t.b)}`}return e}function Ao(e){let t=JSON.parse(e),n=[];return wo(t,[],void 0,n),n}function wo(e,t,n,s){let o=typeof e.$type=="string"?e.$type:n;if("$value"in e){let i=typeof e.$type=="string"?e.$type:n||"unknown",r=typeof e.$description=="string"?e.$description:void 0;s.push({path:[...t],name:t.join("."),$type:i,$value:e.$value,$description:r});return}for(let[i,r]of Object.entries(e))i.startsWith("$")||typeof r=="object"&&r!==null&&!Array.isArray(r)&&wo(r,[...t,i],o,s)}function Eo(e,t,n){let s=new Map;for(let C of t)s.set(Re(C.name),C);let o=[];for(let C of e.collections)for(let k of C.variables)o.push(k);let i=[],r=[],a=new Set,c=new Set;for(let C of o){let k=Re(C.name);if(c.add(k),s.has(k)){a.add(k),i.push({token:C.name,nodeCount:C.consumers,usage:C.consumers>0?"correct":"overridden"});continue}let N=Ba(k,t);N&&N.distance<=3?(a.add(Re(N.token.name)),i.push({token:C.name,nodeCount:C.consumers,usage:"correct"})):r.push({value:C.name,nodeCount:C.consumers,nearestToken:N?N.token.name:"(none)",distance:N?N.distance:1/0})}let l=[];for(let C of t)a.has(Re(C.name))||l.push(C.name);let d=r.filter(C=>C.nodeCount>0).map(C=>C.value),u=t.length,f=i.filter(C=>C.nodeCount>0).length,m=r.filter(C=>C.nodeCount>0).length,g=f+m,p=g>0?Math.round(f/g*100):u>0?0:100;return{adoptionScore:Math.round(e.adoptionRate*.5+p*.5),matched:i,unmatched:r,orphanTokens:l,missingFromSystem:d,summary:{totalTokenDefs:u,usedInDesign:f,hardCodedValues:m,compliance:p}}}function Re(e){return e.replace(/\//g,".").replace(/\s+/g,"-").toLowerCase().trim()}function Ba(e,t){if(t.length===0)return null;let n=null;for(let s of t){let o=Ga(e,Re(s.name));if((!n||on)return n+1;[s,o]=[o,s]}return s[t.length]}async function To(e){let n=(await figma.variables.getLocalVariableCollectionsAsync()).find(r=>r.id===e);if(!n)throw new Error(`Collection not found: ${e}`);let s=n.modes.map(r=>({modeId:r.modeId,modeName:r.name})),o=[],i=[];for(let r of n.variableIds){let a=await figma.variables.getVariableByIdAsync(r);if(!a)continue;let c={},l=[],d=!1,u,f=!1;for(let m of n.modes){let g=a.valuesByMode[m.modeId];g==null?l.push(m.name):(c[m.name]=pn(g),f?JSON.stringify(pn(g))!==u&&(d=!0):(u=JSON.stringify(pn(g)),f=!0))}(d||l.length>0)&&o.push({variableName:a.name,type:a.resolvedType,values:c}),l.length>0&&i.push({variableName:a.name,missingModes:l})}return{collection:n.name,modes:s,variableDiffs:o,missingValues:i}}function pn(e){if(e==null||typeof e=="boolean"||typeof e=="string"||typeof e=="number")return e;if(typeof e=="object"&&e.type==="VARIABLE_ALIAS")return{type:"VARIABLE_ALIAS",id:e.id};if(typeof e=="object"&&"r"in e){let t=e,n=s=>{let o=Math.round(s*255).toString(16);return o.length===1?"0"+o:o};return t.a!==void 0&&t.a<1?`rgba(${Math.round(t.r*255)}, ${Math.round(t.g*255)}, ${Math.round(t.b*255)}, ${t.a.toFixed(2)})`:`#${n(t.r)}${n(t.g)}${n(t.b)}`}return e}Ne();q();var ue=null,fe=null,$e=new Set,at=!1;function Lo(e){if(!ue||!ue.enabled)return;for(let n of e.documentChanges)(n.type==="PROPERTY_CHANGE"||n.type==="CREATE"||n.type==="STYLE_PROPERTY_CHANGE")&&"id"in n&&typeof n.id=="string"&&$e.add(n.id);if($e.size===0)return;fe!==null&&clearTimeout(fe);let t=ue.debounceMs||500;fe=setTimeout(()=>{Ua()},t)}async function Ua(){if(!ue)return;let e=Array.from($e);$e.clear(),fe=null;let t=[],n=[];for(let s of e)try{let o=await figma.getNodeByIdAsync(s);o&&"type"in o&&o.type!=="PAGE"&&o.type!=="DOCUMENT"&&(t.push(o),n.push(s))}catch(o){}if(t.length!==0)try{let s=se(t,ue.settings);S("realtime-lint-update",{errors:s.errors,changedNodeIds:n})}catch(s){console.error("Realtime lint error:",s)}}function Po(e){ue={enabled:e.enabled,debounceMs:e.debounceMs||500,settings:e.settings||K},at||(figma.on("documentchange",Lo),at=!0)}function Ro(){ue=null,at&&(figma.off("documentchange",Lo),at=!1),fe!==null&&(clearTimeout(fe),fe=null),$e.clear()}var $o=/^(Frame|Group|Rectangle|Ellipse|Vector|Line|Polygon|Star|Component|Instance|Boolean)\s*\d+$/i;function Mo(e,t){let n=e.errors,o=n.filter(p=>p.errorType==="fill"||p.errorType==="stroke"||p.errorType==="effect"||p.errorType==="text").length,r=n.filter(p=>p.message.toLowerCase().includes("detach")).length,a=t?t.hardCodedValues:0,c=new Set;for(let p of n)$o.test(p.nodeName)&&c.add(p.nodeId);let l=n.filter(p=>p.errorType==="accessibility"&&$o.test(p.nodeName));for(let p of l)c.add(p.nodeId);let d=c.size,u=n.filter(p=>p.errorType==="autoLayout").length,f=n.filter(p=>p.errorType==="spacing").length,m=100;return m-=o*2,m-=r*5,m-=a*1,m-=d*.5,m-=u*1,m-=f*.5,{overall:Math.max(0,Math.min(100,Math.round(m))),components:{orphanedStyles:{count:o,score:Math.max(0,Math.round(100-o*2))},detachedInstances:{count:r,score:Math.max(0,Math.round(100-r*5))},hardcodedValues:{count:a,score:Math.max(0,Math.round(100-a*1))},namingViolations:{count:d,score:Math.max(0,Math.round(100-d*.5))},missingAutoLayout:{count:u,score:Math.max(0,Math.round(100-u*1))},inconsistentSpacing:{count:f,score:Math.max(0,Math.round(100-f*.5))}}}}var Oo=0;function za(){return`layout-sizing-${++Oo}`}function Me(e){return e.type==="FRAME"||e.type==="COMPONENT"||e.type==="INSTANCE"}function mn(e){return Me(e)?e.layoutMode!=="NONE":!1}function ie(e,t,n,s,o,i,r){e.push({id:za(),type:"autoLayout",severity:t,nodeId:n,nodeName:s,message:o,currentValue:i,suggestions:r,autoFixable:!1})}function Ha(e,t){var r,a;if(!("children"in e))return 0;let n=e.children.filter(c=>Me(c)&&c.visible!==!1);if(n.length<2)return 0;let s=0,o=new Map,i=new Map;for(let c of n){let l=c,d=(r=l.primaryAxisSizingMode)!=null?r:"UNKNOWN",u=(a=l.counterAxisSizingMode)!=null?a:"UNKNOWN";o.has(d)||o.set(d,[]),o.get(d).push(c),i.has(u)||i.set(u,[]),i.get(u).push(c)}if(s+=n.length,o.size>1){let c=0,l="";for(let[d,u]of o)u.length>c&&(c=u.length,l=d);for(let[d,u]of o)if(d!==l)for(let f of u)ie(t,"warning",f.id,f.name,`Primary axis sizing "${d}" differs from siblings' "${l}" in "${e.name}"`,d,[l])}if(i.size>1){let c=0,l="";for(let[d,u]of i)u.length>c&&(c=u.length,l=d);for(let[d,u]of i)if(d!==l)for(let f of u)ie(t,"warning",f.id,f.name,`Counter axis sizing "${d}" differs from siblings' "${l}" in "${e.name}"`,d,[l])}return s}function Wa(e,t){var s,o;if(!mn(e)||!("children"in e))return 0;let n=0;for(let i of e.children){if(i.visible===!1||!Me(i))continue;let r=i;if(r.layoutPositioning==="ABSOLUTE")continue;n++;let c=(s=r.primaryAxisSizingMode)!=null?s:void 0,l=(o=r.counterAxisSizingMode)!=null?o:void 0;c==="FIXED"&&ie(t,"warning",i.id,i.name,`FIXED primary axis sizing inside auto-layout parent "${e.name}" may break layout`,"FIXED (primary)",["HUG","FILL"]),l==="FIXED"&&ie(t,"warning",i.id,i.name,`FIXED counter axis sizing inside auto-layout parent "${e.name}" may break layout`,"FIXED (counter)",["HUG","FILL"])}return n}function ja(e,t){var a;if(!mn(e)||e.layoutMode!=="HORIZONTAL"||!("children"in e))return 0;let n=e.children.filter(c=>c.visible!==!1);if(n.length<2)return 0;let s=0,o=!1,i=!1,r=[];for(let c of n){let l=c;if(l.layoutPositioning==="ABSOLUTE")continue;s++,((a=l.layoutGrow)!=null?a:0)===1?(o=!0,r.push(c)):i=!0}if(o&&i)for(let c of r)ie(t,"info",c.id,c.name,`layoutGrow: 1 while siblings have layoutGrow: 0 in horizontal layout "${e.name}" \u2014 may cause unexpected stretching`,"layoutGrow: 1",["Verify stretching is intentional"]);return s}function Ka(e,t){if(!mn(e)||!("children"in e))return 0;let n=0;for(let s of e.children){if(s.visible===!1)continue;s.layoutPositioning==="ABSOLUTE"&&(n++,ie(t,"info",s.id,s.name,`Absolute positioning inside auto-layout parent "${e.name}" \u2014 verify this is intentional`,"layoutPositioning: ABSOLUTE",["Remove absolute positioning or confirm intentional overlay"]))}return n}function qa(e,t){var r,a,c,l,d,u;if(!Me(e))return 0;let n=e,s=(r=n.primaryAxisSizingMode)!=null?r:void 0,o=(a=n.counterAxisSizingMode)!=null?a:void 0,i=0;if(s==="FILL"||o==="FILL"){i++;let f=(c=n.minWidth)!=null?c:null,m=(l=n.maxWidth)!=null?l:null,g=(d=n.minHeight)!=null?d:null,p=(u=n.maxHeight)!=null?u:null,h=f!==null&&f>0||m!==null&&m<1/0&&m>0,C=g!==null&&g>0||p!==null&&p<1/0&&p>0;s==="FILL"&&!h&&!C?ie(t,"info",e.id,e.name,"FILL sizing without min/max constraints \u2014 frame may collapse or overflow","FILL, no min/max",["Add minWidth/maxWidth or minHeight/maxHeight"]):o==="FILL"&&!h&&!C&&ie(t,"info",e.id,e.name,"FILL counter-axis sizing without min/max constraints \u2014 frame may collapse or overflow","FILL (counter), no min/max",["Add minWidth/maxWidth or minHeight/maxHeight"])}return i}function Fo(e,t,n,s,o){let i=o||"locked"in e&&e.locked,r="visible"in e&&!e.visible;if(n&&i||s&&r)return ct();let a=ct();if(Me(e)){let c=e,l=t.length;a.totalChecked+=Ha(c,t),a.inconsistentSizing+=t.length-l;let d=t.length;a.totalChecked+=Wa(c,t),a.fixedInAutoLayout+=t.length-d;let u=t.length;a.totalChecked+=ja(c,t),a.layoutGrowMismatch+=t.length-u;let f=t.length;a.totalChecked+=Ka(c,t),a.absoluteInAutoLayout+=t.length-f;let m=t.length;a.totalChecked+=qa(e,t),a.missingConstraints+=t.length-m}if("children"in e)for(let c of e.children){let l=Fo(c,t,n,s,i);a.totalChecked+=l.totalChecked,a.inconsistentSizing+=l.inconsistentSizing,a.fixedInAutoLayout+=l.fixedInAutoLayout,a.layoutGrowMismatch+=l.layoutGrowMismatch,a.absoluteInAutoLayout+=l.absoluteInAutoLayout,a.missingConstraints+=l.missingConstraints}return a}function ct(){return{totalChecked:0,inconsistentSizing:0,fixedInAutoLayout:0,layoutGrowMismatch:0,absoluteInAutoLayout:0,missingConstraints:0}}function Vo(e,t){var r,a,c,l;let n=(a=(r=t==null?void 0:t.settings)==null?void 0:r.skipLockedLayers)!=null?a:!0,s=(l=(c=t==null?void 0:t.settings)==null?void 0:c.skipHiddenLayers)!=null?l:!0;Oo=0;let o=[],i=ct();for(let d of e){let u=Fo(d,o,n,s,!1);i.totalChecked+=u.totalChecked,i.inconsistentSizing+=u.inconsistentSizing,i.fixedInAutoLayout+=u.fixedInAutoLayout,i.layoutGrowMismatch+=u.layoutGrowMismatch,i.absoluteInAutoLayout+=u.absoluteInAutoLayout,i.missingConstraints+=u.missingConstraints}return{issues:o,summary:{totalChecked:i.totalChecked,inconsistentSizing:i.inconsistentSizing,fixedInAutoLayout:i.fixedInAutoLayout,layoutGrowMismatch:i.layoutGrowMismatch,absoluteInAutoLayout:i.absoluteInAutoLayout,missingConstraints:i.missingConstraints}}}var Do=0;function Xa(){return`constraints-${++Do}`}function dt(e){return e.type==="FRAME"||e.type==="COMPONENT"||e.type==="INSTANCE"}function ut(e){return dt(e)?e.layoutMode!=="NONE":!1}function Fe(e){let t=e==null?void 0:e.constraints;return t&&typeof t.horizontal=="string"&&typeof t.vertical=="string"?t:null}function Oe(e,t,n,s,o,i,r){e.push({id:Xa(),type:"autoLayout",severity:t,nodeId:n,nodeName:s,message:o,currentValue:i,suggestions:r,autoFixable:!1})}function Ja(e,t,n){if(ut(t))return!1;let s=Fe(e);return s&&s.horizontal==="MIN"&&s.vertical==="MIN"?(Oe(n,"warning",e.id,e.name,`Default constraints (MIN, MIN) inside fixed frame "${t.name}" \u2014 will break on resize`,"MIN, MIN",["STRETCH","CENTER","MAX"]),!0):!1}function Ya(e,t){if(e.type!=="TEXT")return!1;let n=Fe(e);if(!n)return!1;if(n.horizontal==="SCALE"||n.vertical==="SCALE"){let o=n.horizontal==="SCALE"?"horizontal":"vertical";return Oe(t,"critical",e.id,e.name,"SCALE constraint on text node will distort text \u2014 use MIN or STRETCH instead",`SCALE (${o})`,["MIN","STRETCH"]),!0}return!1}function Qa(e,t){let n=Fe(e);if(!n)return 0;let s=0,o=e.layoutSizingHorizontal,i=e.layoutSizingVertical;if(n.horizontal==="STRETCH"&&o==="FIXED"){let r=e.parent;if(r&&dt(r)&&!ut(r)){let c=e==null?void 0:e.width;Oe(t,"warning",e.id,e.name,`STRETCH horizontal constraint but node has fixed width${typeof c=="number"?` ${Math.round(c)}px`:""} \u2014 potentially contradictory`,"STRETCH + FIXED width",["Remove fixed width or change constraint to MIN/CENTER"]),s++}}if(n.vertical==="STRETCH"&&i==="FIXED"){let r=e.parent;if(r&&dt(r)&&!ut(r)){let c=e==null?void 0:e.height;Oe(t,"warning",e.id,e.name,`STRETCH vertical constraint but node has fixed height${typeof c=="number"?` ${Math.round(c)}px`:""} \u2014 potentially contradictory`,"STRETCH + FIXED height",["Remove fixed height or change constraint to MIN/CENTER"]),s++}}return s}function Za(e,t,n){if(!ut(t)||(e==null?void 0:e.layoutPositioning)==="ABSOLUTE")return!1;let o=Fe(e);return!o||o.horizontal==="MIN"&&o.vertical==="MIN"?!1:(Oe(n,"info",e.id,e.name,`Explicit constraints (${o.horizontal}, ${o.vertical}) inside auto-layout parent "${t.name}" \u2014 constraints are ignored`,`${o.horizontal}, ${o.vertical}`,["Remove explicit constraints or switch parent to fixed layout"]),!0)}function lt(){return{totalChecked:0,noConstraints:0,scaleOnText:0,conflicting:0,ignoredInAutoLayout:0}}function _o(e,t,n,s,o,i){let r=o||"locked"in e&&e.locked,a="visible"in e&&!e.visible;if(n&&r||s&&a)return lt();let c=lt();if(i&&dt(i)&&Fe(e)&&(c.totalChecked++,Ya(e,t)&&c.scaleOnText++,Ja(e,i,t)&&c.noConstraints++,c.conflicting+=Qa(e,t),Za(e,i,t)&&c.ignoredInAutoLayout++),"children"in e)for(let l of e.children){let d=_o(l,t,n,s,r,e);c.totalChecked+=d.totalChecked,c.noConstraints+=d.noConstraints,c.scaleOnText+=d.scaleOnText,c.conflicting+=d.conflicting,c.ignoredInAutoLayout+=d.ignoredInAutoLayout}return c}function Bo(e,t){var r,a,c,l;let n=(a=(r=t==null?void 0:t.settings)==null?void 0:r.skipLockedLayers)!=null?a:!0,s=(l=(c=t==null?void 0:t.settings)==null?void 0:c.skipHiddenLayers)!=null?l:!0;Do=0;let o=[],i=lt();for(let d of e){let u=_o(d,o,n,s,!1,null);i.totalChecked+=u.totalChecked,i.noConstraints+=u.noConstraints,i.scaleOnText+=u.scaleOnText,i.conflicting+=u.conflicting,i.ignoredInAutoLayout+=u.ignoredInAutoLayout}return{issues:o,summary:{totalChecked:i.totalChecked,noConstraints:i.noConstraints,scaleOnText:i.scaleOnText,conflicting:i.conflicting,ignoredInAutoLayout:i.ignoredInAutoLayout}}}var Uo=0;function ec(){return`typo-${++Uo}`}function pe(e,t,n,s,o,i,r){e.push({id:ec(),type:"textStyle",severity:t,nodeId:n,nodeName:s,message:o,currentValue:i,suggestions:r,autoFixable:!1})}function gn(e){return e.type==="FRAME"||e.type==="COMPONENT"||e.type==="INSTANCE"}var Go=/\b(link|anchor|href|url|nav-link|breadcrumb|hyperlink)\b/i;function tc(e){return Go.test(e.name)?!0:e.parent&&"name"in e.parent?Go.test(e.parent.name):!1}function yn(e,t){let n=t.get(e.id);if(n)return n;let s=[];if(e.type==="TEXT"&&s.push(e),"children"in e)for(let o of e.children)s.push(...yn(o,t));return t.set(e.id,s),s}function nc(e,t,n){var l;if(!gn(e))return 0;let s=yn(e,n);if(s.length<2)return 0;let o=s.filter(d=>{let u=d.fontSize;return typeof u=="number"&&u<=18});if(o.length<2)return 0;let i=new Map;for(let d of o){let u=(l=d.textAlignHorizontal)!=null?l:"LEFT";i.has(u)||i.set(u,[]),i.get(u).push(d)}if(i.size<=1)return 0;let r=0,a="LEFT";for(let[d,u]of i)u.length>r&&(r=u.length,a=d);let c=0;for(let[d,u]of i)if(d!==a)for(let f of u)c++,pe(t,"info",f.id,f.name,`Text alignment "${d}" differs from majority body text alignment "${a}" in "${e.name}"`,d,[a]);return c}function sc(e,t){let n=e.fontSize;if(typeof n!="number"||n>16)return!1;let s=e.letterSpacing;if(!s||s===figma.mixed)return!1;let o=typeof s=="object"?s.value:void 0;return typeof o!="number"||o===0?!1:(pe(t,"info",e.id,e.name,`Body text (${n}px) has non-zero letterSpacing (${o}${s.unit==="PERCENT"?"%":"px"}) \u2014 unusual for body text`,`${o}${s.unit==="PERCENT"?"%":"px"}`,["0px (default)","Remove letterSpacing for body text"]),!0)}function oc(e,t){let n=e.textCase;if(n!=="UPPER")return!1;let s=e.letterSpacing,o=s&&typeof s=="object"?s.value:0;return typeof o=="number"&&o<=0?(pe(t,"info",e.id,e.name,"UPPERCASE text without positive letterSpacing \u2014 add spacing for readability",`textCase: UPPER, letterSpacing: ${o}`,["letterSpacing: 0.5px","letterSpacing: 1px","letterSpacing: 2%"]),!0):!1}function ic(e,t,n){if(!gn(e))return 0;let s=yn(e,n);if(s.length<2)return 0;let o=0,i=0;for(let r of s){let a=r.paragraphSpacing;typeof a=="number"&&a===0&&i++,o++}return i===s.length&&i>=2&&pe(t,"info",e.id,e.name,`${i} text blocks without paragraphSpacing set \u2014 add spacing for better readability`,`${i} texts, all paragraphSpacing: 0`,["Set paragraphSpacing to match line height or spacing scale"]),o}function rc(e,t){let n=e.fontSize;if(typeof n!="number"||n===0)return!1;let s=e.lineHeight;if(!s||s===figma.mixed)return!1;let o=null;if(typeof s=="object"&&"unit"in s){if(s.unit==="PIXELS")o=s.value;else if(s.unit==="PERCENT")o=s.value/100*n;else if(s.unit==="AUTO")return!1}if(o===null||o===0)return!1;let i=o/n;return i<1.2?(pe(t,"warning",e.id,e.name,`Line height ratio ${i.toFixed(2)} (${Math.round(o)}px / ${n}px) is below 1.2 \u2014 text may be cramped`,`${i.toFixed(2)} ratio`,["1.2 (minimum)","1.4 (comfortable)","1.5 (spacious)"]),!0):i>2?(pe(t,"info",e.id,e.name,`Line height ratio ${i.toFixed(2)} (${Math.round(o)}px / ${n}px) exceeds 2.0 \u2014 may be unintentional`,`${i.toFixed(2)} ratio`,["1.4 (body)","1.2 (heading)","1.6 (loose)"]),!0):!1}function ac(e,t){return e.textDecoration!=="UNDERLINE"||tc(e)?!1:(pe(t,"info",e.id,e.name,"Underline decoration on text that does not appear to be a link \u2014 may confuse users","textDecoration: UNDERLINE",["Remove underline or rename layer to indicate link purpose"]),!0)}function ft(){return{totalChecked:0,inconsistentAlignment:0,nonStandardLetterSpacing:0,uppercaseMissingSpacing:0,missingParagraphSpacing:0,badLineHeightRatio:0,suspiciousDecoration:0}}function zo(e,t,n,s,o,i,r){let a=o||"locked"in e&&e.locked,c="visible"in e&&!e.visible;if(n&&a||s&&c)return ft();let l=ft();if(e.type==="TEXT"){let d=e;l.totalChecked++,sc(d,t)&&l.nonStandardLetterSpacing++,oc(d,t)&&l.uppercaseMissingSpacing++,rc(d,t)&&l.badLineHeightRatio++,ac(d,t)&&l.suspiciousDecoration++}if(gn(e)){let d=e.id;if(!i.has(d)){i.add(d);let u=t.length;l.totalChecked+=nc(e,t,r),l.inconsistentAlignment+=t.length-u;let f=t.length,m=ic(e,t,r);l.totalChecked+=m,l.missingParagraphSpacing+=t.length-f}}if("children"in e)for(let d of e.children){let u=zo(d,t,n,s,a,i,r);l.totalChecked+=u.totalChecked,l.inconsistentAlignment+=u.inconsistentAlignment,l.nonStandardLetterSpacing+=u.nonStandardLetterSpacing,l.uppercaseMissingSpacing+=u.uppercaseMissingSpacing,l.missingParagraphSpacing+=u.missingParagraphSpacing,l.badLineHeightRatio+=u.badLineHeightRatio,l.suspiciousDecoration+=u.suspiciousDecoration}return l}function Ho(e,t){var c,l,d,u;let n=(l=(c=t==null?void 0:t.settings)==null?void 0:c.skipLockedLayers)!=null?l:!0,s=(u=(d=t==null?void 0:t.settings)==null?void 0:d.skipHiddenLayers)!=null?u:!0;Uo=0;let o=[],i=new Set,r=new Map,a=ft();for(let f of e){let m=zo(f,o,n,s,!1,i,r);a.totalChecked+=m.totalChecked,a.inconsistentAlignment+=m.inconsistentAlignment,a.nonStandardLetterSpacing+=m.nonStandardLetterSpacing,a.uppercaseMissingSpacing+=m.uppercaseMissingSpacing,a.missingParagraphSpacing+=m.missingParagraphSpacing,a.badLineHeightRatio+=m.badLineHeightRatio,a.suspiciousDecoration+=m.suspiciousDecoration}return{issues:o,summary:{totalChecked:a.totalChecked,inconsistentAlignment:a.inconsistentAlignment,nonStandardLetterSpacing:a.nonStandardLetterSpacing,uppercaseMissingSpacing:a.uppercaseMissingSpacing,missingParagraphSpacing:a.missingParagraphSpacing,badLineHeightRatio:a.badLineHeightRatio,suspiciousDecoration:a.suspiciousDecoration}}}var jo=0;function Ve(){return`cprop-${++jo}`}var Wo=5;function cc(e){if(e.length<2)return!1;let t=new Set;for(let n of e){let s=n.trim();s.length!==0&&(s===s.toUpperCase()?t.add("UPPER"):s===s.toLowerCase()?t.add("lower"):s[0]===s[0].toUpperCase()&&s.slice(1)!==s.slice(1).toUpperCase()?t.add("Title"):t.add("mixed"))}return t.size>1}function lc(e){let t=e.includes("#")?e.substring(0,e.indexOf("#")):e;return/\s/.test(t.trim())}function dc(e,t,n){let s=Object.entries(t).filter(([o,i])=>(i==null?void 0:i.type)==="BOOLEAN");s.length>Wo&&n.push({id:Ve(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`Component has ${s.length} boolean properties (>${Wo}) \u2014 consider using variants instead`,currentValue:`${s.length} booleans`,suggestions:["Group related booleans into a single variant property"],autoFixable:!1})}function uc(e,t,n){let s=e.description;(s==null||s==="")&&n.push({id:Ve(),type:"naming",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Component "${e.name}" has no description \u2014 consumers may not understand its purpose`,currentValue:"(no description)",suggestions:["Add a short description to the component explaining its intended usage"],autoFixable:!1})}function fc(e,t,n){if(e.type==="COMPONENT_SET")for(let[s,o]of Object.entries(t)){if(!o||o.type!=="VARIANT")continue;let i=o.variantOptions;if(!(!Array.isArray(i)||i.length<2)&&cc(i)){let r=s.includes("#")?s.substring(0,s.indexOf("#")):s;n.push({id:Ve(),type:"naming",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Variant property "${r}" has inconsistent casing: ${i.map(a=>`"${a}"`).join(", ")}`,currentValue:i.join(", "),suggestions:["Use a consistent naming convention (e.g., all lowercase or all Title Case)"],autoFixable:!1})}}}function pc(e,t,n){if(e.type!=="COMPONENT_SET")return;let o=e.children;if(!Array.isArray(o)||o.length===0)return;let i={};for(let r of o){let a=r.name.split(",").map(c=>c.trim());for(let c of a){let l=c.indexOf("=");if(l===-1)continue;let d=c.substring(0,l).trim(),u=c.substring(l+1).trim();i[d]||(i[d]=new Set),i[d].add(u)}}for(let[r,a]of Object.entries(t)){if(!a||a.type!=="VARIANT")continue;let c=a.variantOptions;if(!Array.isArray(c))continue;let l=r.includes("#")?r.substring(0,r.indexOf("#")):r,d=i[l];for(let u of c)d!=null&&d.has(u)||n.push({id:Ve(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`Variant value "${u}" of "${l}" is defined but no child component uses it`,currentValue:`${l}=${u}`,suggestions:["Remove the unused variant value or add a variant component for it"],autoFixable:!1})}}function mc(e,t,n){for(let[s,o]of Object.entries(t))if(o&&o.type!=="VARIANT"&&lc(s)){let i=s.includes("#")?s.substring(0,s.indexOf("#")):s;n.push({id:Ve(),type:"naming",severity:"info",nodeId:e.id,nodeName:e.name,message:`Property name "${i}" contains spaces \u2014 use camelCase or kebab-case instead`,currentValue:i,suggestions:[i.replace(/\s+(.)/g,(r,a)=>a.toUpperCase()),i.replace(/\s+/g,"-").toLowerCase()],autoFixable:!1})}}function Ko(e,t,n,s,o){let i=o||"locked"in e&&e.locked===!0,r="visible"in e&&!e.visible;if(n&&i||s&&r)return 0;let a=0;if(e.type==="COMPONENT"||e.type==="COMPONENT_SET"){let c=e.componentPropertyDefinitions;c&&typeof c=="object"&&(a++,dc(e,c,t),uc(e,c,t),fc(e,c,t),pc(e,c,t),mc(e,c,t))}if("children"in e){let c=e.children;if(Array.isArray(c))for(let l of c)a+=Ko(l,t,n,s,i)}return a}function qo(e,t={}){let{skipLocked:n=!0,skipHidden:s=!0}=t;jo=0;let o=[],i=0;for(let r of e)i+=Ko(r,o,n,s,!1);return{issues:o,summary:{totalChecked:i,booleanOveruse:o.filter(r=>r.message.includes("boolean properties")).length,missingDescription:o.filter(r=>r.message.includes("no description")).length,inconsistentNaming:o.filter(r=>r.message.includes("inconsistent casing")).length,unusedVariants:o.filter(r=>r.message.includes("no child component uses it")).length,spacedNames:o.filter(r=>r.message.includes("contains spaces")).length}}}var Xo=0;function gc(){return`style-audit-${++Xo}`}function Ie(e,t,n,s,o,i,r){e.push({id:gc(),type:"fill",severity:t,nodeId:n,nodeName:s,message:o,currentValue:i,suggestions:r,autoFixable:!1})}function yc(e,t,n,s){let o=Math.round(e*255),i=Math.round(t*255),r=Math.round(n*255),a=`#${o.toString(16).padStart(2,"0")}${i.toString(16).padStart(2,"0")}${r.toString(16).padStart(2,"0")}`;return s!==void 0&&s<1?`${a} @ ${Math.round(s*100)}%`:a}function De(e){let t=e.find(s=>s.type==="SOLID"&&s.visible!==!1);if(!t||t.type!=="SOLID")return null;let n=t;return yc(n.color.r,n.color.g,n.color.b,t.opacity)}async function hc(){try{let e=figma;if(typeof e.getLocalPaintStylesAsync=="function")return await e.getLocalPaintStylesAsync();if(typeof e.getLocalPaintStyles=="function")return e.getLocalPaintStyles()}catch(e){}return[]}async function bc(){try{let e=figma;if(typeof e.getLocalTextStylesAsync=="function")return await e.getLocalTextStylesAsync();if(typeof e.getLocalTextStyles=="function")return e.getLocalTextStyles()}catch(e){}return[]}async function Sc(){try{let e=figma;if(typeof e.getLocalEffectStylesAsync=="function")return await e.getLocalEffectStylesAsync();if(typeof e.getLocalEffectStyles=="function")return e.getLocalEffectStyles()}catch(e){}return[]}function Jo(e,t,n,s){let o=s||"locked"in e&&e.locked,i="visible"in e&&!e.visible;if(t&&o)return[];if(n&&i)return[];let r=[e];if("children"in e)for(let a of e.children)r.push(...Jo(a,t,n,o));return r}function vc(e,t,n,s,o){let i=0,r=new Set;for(let a of s){let c=a;c.fillStyleId&&typeof c.fillStyleId=="string"&&c.fillStyleId!==""&&r.add(c.fillStyleId),c.strokeStyleId&&typeof c.strokeStyleId=="string"&&c.strokeStyleId!==""&&r.add(c.strokeStyleId),c.textStyleId&&typeof c.textStyleId=="string"&&c.textStyleId!==""&&r.add(c.textStyleId),c.effectStyleId&&typeof c.effectStyleId=="string"&&c.effectStyleId!==""&&r.add(c.effectStyleId)}for(let a of e)r.has(a.id)||(i++,Ie(o,"info",a.id,a.name,`Paint style "${a.name}" is defined but not used by any visible node`,"Orphaned paint style",["Remove unused style or apply it to a node"]));for(let a of t)r.has(a.id)||(i++,Ie(o,"info",a.id,a.name,`Text style "${a.name}" is defined but not used by any visible node`,"Orphaned text style",["Remove unused style or apply it to a node"]));for(let a of n)r.has(a.id)||(i++,Ie(o,"info",a.id,a.name,`Effect style "${a.name}" is defined but not used by any visible node`,"Orphaned effect style",["Remove unused style or apply it to a node"]));return i}function kc(e,t,n){if(!("fills"in e))return!1;let s=e.fills;if(s===figma.mixed||!Array.isArray(s))return!1;let o=e.fillStyleId;if(o&&typeof o=="string"&&o!=="")return!1;try{let a=e.boundVariables;if(a!=null&&a.fills)return!1}catch(a){}let i=De(s);if(!i)return!1;let r=t.get(i);return r&&r.length>0?(Ie(n,"warning",e.id,e.name,`Fill color ${i} matches paint style "${r[0]}" but is not linked`,i,r.map(a=>`Apply style "${a}"`)),!0):!1}function Nc(e,t,n){if(!("fills"in e))return!1;let s=e.fillStyleId;if(!s||typeof s!="string"||s==="")return!1;let o=t.get(s);if(!o)return!1;let i=e.fills;if(i===figma.mixed||!Array.isArray(i))return!1;let r=De(i),a=De(o.paints);return r&&a&&r!==a?(Ie(n,"warning",e.id,e.name,`Fill style "${o.name}" is applied but overridden \u2014 node fill ${r} differs from style ${a}`,`${r} (node) vs ${a} (style)`,["Reset fill to style definition","Detach style and keep override"]),!0):!1}function Ic(e,t){let n=new Map;for(let o of e){let i=De(o.paints);i&&(n.has(i)||n.set(i,[]),n.get(i).push(o))}let s=0;for(let[o,i]of n)if(!(i.length<2))for(let r=1;rc.name).join(", ");Ie(t,"info",i[r].id,i[r].name,`Paint style "${i[r].name}" has the same color (${o}) as "${i[0].name}" \u2014 possible duplicate`,`${o} shared by: ${a}`,[`Merge into "${i[0].name}"`,"Verify they serve different purposes"])}return s}async function Yo(e,t){var f,m,g,p;let n=(m=(f=t==null?void 0:t.settings)==null?void 0:f.skipLockedLayers)!=null?m:!0,s=(p=(g=t==null?void 0:t.settings)==null?void 0:g.skipHiddenLayers)!=null?p:!0;Xo=0;let o=[],i={totalChecked:0,orphanedStyles:0,hardCodedMatches:0,overriddenStyles:0,duplicateStyles:0},[r,a,c]=await Promise.all([hc(),bc(),Sc()]);if(r.length===0&&a.length===0&&c.length===0)return{issues:o,summary:i};let l=[];for(let h of e)l.push(...Jo(h,n,s,!1));i.totalChecked=l.length;let d=new Map,u=new Map;for(let h of r){d.set(h.id,h);let C=De(h.paints);C&&(u.has(C)||u.set(C,[]),u.get(C).push(h.name))}i.orphanedStyles=vc(r,a,c,l,o);for(let h of l)kc(h,u,o)&&i.hardCodedMatches++,Nc(h,d,o)&&i.overriddenStyles++;return i.duplicateStyles=Ic(r,o),{issues:o,summary:i}}var Qo=0;function pt(){return`vscope-${++Qo}`}var Zo={fills:["ALL_FILLS","FRAME_FILL","SHAPE_FILL","TEXT_FILL","ALL_SCOPES"],strokes:["STROKE_COLOR","ALL_SCOPES"],itemSpacing:["GAP","ALL_SCOPES"],paddingLeft:["GAP","ALL_SCOPES"],paddingRight:["GAP","ALL_SCOPES"],paddingTop:["GAP","ALL_SCOPES"],paddingBottom:["GAP","ALL_SCOPES"],counterAxisSpacing:["GAP","ALL_SCOPES"],topLeftRadius:["CORNER_RADIUS","ALL_SCOPES"],topRightRadius:["CORNER_RADIUS","ALL_SCOPES"],bottomLeftRadius:["CORNER_RADIUS","ALL_SCOPES"],bottomRightRadius:["CORNER_RADIUS","ALL_SCOPES"],width:["WIDTH_HEIGHT","ALL_SCOPES"],height:["WIDTH_HEIGHT","ALL_SCOPES"],minWidth:["WIDTH_HEIGHT","ALL_SCOPES"],maxWidth:["WIDTH_HEIGHT","ALL_SCOPES"],minHeight:["WIDTH_HEIGHT","ALL_SCOPES"],maxHeight:["WIDTH_HEIGHT","ALL_SCOPES"],strokeWeight:["STROKE_FLOAT","ALL_SCOPES"],strokeTopWeight:["STROKE_FLOAT","ALL_SCOPES"],strokeRightWeight:["STROKE_FLOAT","ALL_SCOPES"],strokeBottomWeight:["STROKE_FLOAT","ALL_SCOPES"],strokeLeftWeight:["STROKE_FLOAT","ALL_SCOPES"],opacity:["OPACITY","ALL_SCOPES"],fontFamily:["FONT_FAMILY","ALL_SCOPES"],fontSize:["FONT_SIZE","ALL_SCOPES"],fontStyle:["FONT_STYLE","ALL_SCOPES"],fontWeight:["FONT_WEIGHT","ALL_SCOPES"],lineHeight:["LINE_HEIGHT","ALL_SCOPES"],letterSpacing:["LETTER_SPACING","ALL_SCOPES"],paragraphSpacing:["PARAGRAPH_SPACING","ALL_SCOPES"],paragraphIndent:["PARAGRAPH_INDENT","ALL_SCOPES"]};function _e(e){var n;return(n={fills:"fill color",strokes:"stroke color",itemSpacing:"gap",paddingLeft:"padding",paddingRight:"padding",paddingTop:"padding",paddingBottom:"padding",counterAxisSpacing:"counter-axis gap",topLeftRadius:"corner radius",topRightRadius:"corner radius",bottomLeftRadius:"corner radius",bottomRightRadius:"corner radius",width:"width",height:"height",strokeWeight:"stroke weight",opacity:"opacity",fontSize:"font size",fontFamily:"font family",lineHeight:"line height"}[e])!=null?n:e}var Cc=[{pattern:/^spacing[-_/]|[-_/]spacing$|^space[-_/]|[-_/]gap|^gap[-_/]/i,expectedScopes:["GAP"],label:"GAP/padding"},{pattern:/^radius[-_/]|[-_/]radius$|^corner[-_/]|^rounded[-_/]/i,expectedScopes:["CORNER_RADIUS"],label:"CORNER_RADIUS"},{pattern:/^size[-_/]|[-_/]size$|^width[-_/]|^height[-_/]/i,expectedScopes:["WIDTH_HEIGHT"],label:"WIDTH_HEIGHT"},{pattern:/^color[-_/]|[-_/]color$|^fg[-_/]|^bg[-_/]|^fill[-_/]|[-_/]fill$/i,expectedScopes:["ALL_FILLS","STROKE_COLOR"],label:"color fills/strokes"},{pattern:/^stroke[-_/]|[-_/]stroke$|^border[-_/]|[-_/]border$/i,expectedScopes:["STROKE_COLOR","STROKE_FLOAT"],label:"stroke"},{pattern:/^font[-_/]|[-_/]font$|^text[-_/]|[-_/]text$|^type[-_/]/i,expectedScopes:["FONT_SIZE","FONT_FAMILY","FONT_WEIGHT","FONT_STYLE","LINE_HEIGHT","LETTER_SPACING","TEXT_FILL"],label:"typography"},{pattern:/^opacity[-_/]|[-_/]opacity$/i,expectedScopes:["OPACITY"],label:"OPACITY"}];function ei(e,t,n,s,o){let i=o||"locked"in e&&e.locked===!0,r="visible"in e&&!e.visible;if(n&&i||s&&r)return;let a=e.boundVariables;if(a&&typeof a=="object"){for(let[c,l]of Object.entries(a))if(l){if(Array.isArray(l))for(let d of l)d&&typeof d=="object"&&"id"in d&&t.push({variableId:d.id,field:c,nodeId:e.id,nodeName:e.name});else if(typeof l=="object"&&"id"in l)t.push({variableId:l.id,field:c,nodeId:e.id,nodeName:e.name});else if(typeof l=="object")for(let[d,u]of Object.entries(l))u&&typeof u=="object"&&"id"in u&&t.push({variableId:u.id,field:c,nodeId:e.id,nodeName:e.name})}}if("children"in e){let c=e.children;if(Array.isArray(c))for(let l of c)ei(l,t,n,s,i)}}async function xc(e){var t,n;try{let s=figma.variables.getVariableByIdAsync,o=null;return typeof s=="function"?o=await s(e):typeof figma.variables.getVariableById=="function"&&(o=figma.variables.getVariableById(e)),o?{id:e,name:(t=o.name)!=null?t:"",resolvedType:(n=o.resolvedType)!=null?n:"",scopes:Array.isArray(o.scopes)?o.scopes:[]}:null}catch(s){return null}}var Ac={fills:["ALL_FILLS","FRAME_FILL","SHAPE_FILL","TEXT_FILL"],strokes:["STROKE_COLOR"],textRangeFills:["TEXT_FILL","ALL_FILLS"]};function wc(e,t,n){let s=Zo[e.field];if(!s||t.scopes.includes("ALL_SCOPES"))return;if(!t.scopes.some(i=>s.includes(i))&&t.scopes.length>0){let i="warning";if(t.resolvedType==="COLOR"){let r=Ac[e.field];if(!(r?t.scopes.some(c=>r.includes(c)):!1)){let c=r?r.join(" or "):_e(e.field);n.push({id:pt(),type:"naming",severity:i,nodeId:e.nodeId,nodeName:e.nodeName,message:`COLOR variable "${t.name}" is bound to ${_e(e.field)} \u2014 its scopes [${t.scopes.join(", ")}] don't include ${c}`,currentValue:`${t.name} on ${e.field}`,suggestions:[`Add ${c} scope for ${_e(e.field)} usage`],autoFixable:!1})}}else t.resolvedType==="FLOAT"&&n.push({id:pt(),type:"naming",severity:i,nodeId:e.nodeId,nodeName:e.nodeName,message:`FLOAT variable "${t.name}" scoped to [${t.scopes.join(", ")}] but bound to ${_e(e.field)}`,currentValue:`${t.name} on ${e.field}`,suggestions:[`Verify scope includes ${s.filter(r=>r!=="ALL_SCOPES").join(" or ")}`],autoFixable:!1})}}function Ec(e,t,n,s,o){if(o.has(e.id)||(o.add(e.id),!e.scopes.includes("ALL_SCOPES")))return;for(let{pattern:r,label:a}of Cc)if(r.test(e.name)){s.push({id:pt(),type:"naming",severity:"info",nodeId:n.nodeId,nodeName:n.nodeName,message:`Variable "${e.name}" has ALL_SCOPES but its name suggests it should be restricted to ${a}`,currentValue:`${e.name}: ALL_SCOPES`,suggestions:[`Restrict scopes to ${a} for better picker organization`],autoFixable:!1});return}let i=[...new Set(t)];if(i.length===1){let r=Zo[i[0]],a=r==null?void 0:r.find(c=>c!=="ALL_SCOPES");a&&s.push({id:pt(),type:"naming",severity:"info",nodeId:n.nodeId,nodeName:n.nodeName,message:`Variable "${e.name}" is only used for ${_e(i[0])} but scoped to ALL \u2014 consider narrowing to ${a}`,currentValue:`${e.name}: ALL_SCOPES (used in 1 context)`,suggestions:[`Narrow scope to ${a}`],autoFixable:!1})}}async function ti(e,t={}){var d;let{skipLocked:n=!0,skipHidden:s=!0}=t;Qo=0;let o=[],i=[];for(let u of e)ei(u,i,n,s,!1);let r=[...new Set(i.map(u=>u.variableId))],a=new Map;for(let u of r)a.set(u,await xc(u));let c=new Map;for(let u of i){let f=(d=c.get(u.variableId))!=null?d:[];f.push(u),c.set(u.variableId,f)}let l=new Set;for(let u of i){let f=a.get(u.variableId);f&&wc(u,f,o)}for(let[u,f]of c){let m=a.get(u);!m||f.length===0||Ec(m,f.map(g=>g.field),f[0],o,l)}return{issues:o,summary:{totalChecked:i.length,colorScopeMismatch:o.filter(u=>u.message.includes("COLOR variable")).length,floatScopeMismatch:o.filter(u=>u.message.includes("FLOAT variable")).length,allScopesOveruse:o.filter(u=>u.message.includes("ALL_SCOPES")&&u.message.includes("name suggests")).length,narrowingSuggestions:o.filter(u=>u.message.includes("consider narrowing")).length}}}q();var si=0;function mt(){return`mtheme-${++si}`}function Tc(e,t){if(e===t)return!0;if(e==null||t==null||typeof e!=typeof t)return!1;if(typeof e=="object"&&typeof t=="object"){let n=e,s=t;if(n.type==="VARIABLE_ALIAS"&&s.type==="VARIABLE_ALIAS")return n.id===s.id;if("r"in n&&"g"in n&&"b"in n&&"r"in s&&"g"in s&&"b"in s)return!(Math.abs(n.r-s.r)>.001||Math.abs(n.g-s.g)>.001||Math.abs(n.b-s.b)>.001||"a"in n&&"a"in s&&Math.abs(n.a-s.a)>.001)}return!1}function ni(e){if(!e||typeof e!="object")return null;let t=e;return t.type==="VARIABLE_ALIAS"?null:typeof t.r=="number"&&typeof t.g=="number"&&typeof t.b=="number"?{r:t.r,g:t.g,b:t.b}:null}function Lc(e){if(e==null)return"(undefined)";if(typeof e=="boolean"||typeof e=="string"||typeof e=="number")return String(e);let t=e;if(t.type==="VARIABLE_ALIAS")return`alias(${t.id})`;if(typeof t.r=="number"&&typeof t.g=="number"&&typeof t.b=="number"){let n=Math.round(t.r*255),s=Math.round(t.g*255),o=Math.round(t.b*255);return`rgb(${n}, ${s}, ${o})`}return JSON.stringify(e)}function Pc(e){var a;let t=[],n=e.filter(c=>c.resolvedType==="COLOR"),s=["fg","text","foreground","on"],o=["bg","surface","background"],i=[],r=[];for(let c of n){let d=c.name.toLowerCase().split(/[-_/]/),u=(a=d[0])!=null?a:"",f=d.slice(1).join("-");s.includes(u)&&f?i.push({variable:c,suffix:f}):o.includes(u)&&f&&r.push({variable:c,suffix:f})}for(let c of i){let l=r.find(d=>d.suffix===c.suffix);l&&t.push({fg:c.variable,bg:l.variable})}return t}async function Rc(){try{let e=figma.variables,t=e.getLocalVariableCollectionsAsync,n;if(typeof t=="function")n=await t();else if(typeof e.getLocalVariableCollections=="function")n=e.getLocalVariableCollections();else return[];return(n!=null?n:[]).map(s=>{var o,i;return{id:(o=s==null?void 0:s.id)!=null?o:"",name:(i=s==null?void 0:s.name)!=null?i:"",modes:Array.isArray(s==null?void 0:s.modes)?s.modes:[],variableIds:Array.isArray(s==null?void 0:s.variableIds)?s.variableIds:[]}})}catch(e){return[]}}async function $c(e){var t,n,s,o;try{let i=figma.variables,r=null;return typeof i.getVariableByIdAsync=="function"?r=await i.getVariableByIdAsync(e):typeof i.getVariableById=="function"&&(r=figma.variables.getVariableById(e)),r?{id:e,name:(t=r.name)!=null?t:"",resolvedType:(n=r.resolvedType)!=null?n:"",variableCollectionId:(s=r.variableCollectionId)!=null?s:"",valuesByMode:(o=r.valuesByMode)!=null?o:{}}:null}catch(i){return null}}function Mc(e,t,n){if(t.modes.length<2)return;let o=t.modes.map(a=>a.modeId).map(a=>e.valuesByMode[a]),i=o[0];if(o.every(a=>Tc(a,i))&&i!==void 0){let a=e.resolvedType==="COLOR"?"warning":"info";if(a==="info")return;let c=t.modes.map(l=>l.name).join(", ");n.push({id:mt(),type:"theme",severity:a,nodeId:e.id,nodeName:e.name,message:`Variable "${e.name}" has identical value across modes [${c}] \u2014 may need per-mode values`,currentValue:Lc(i),suggestions:["Set distinct values for each mode (e.g., light vs dark)"],autoFixable:!1})}}function Oc(e,t,n){if(t.modes.length<2)return;let s=[];for(let o of t.modes)e.valuesByMode[o.modeId]===void 0&&s.push(o.name);s.length>0&&s.lengthe.valuesByMode[o]!==void 0).length;if(s>0&&sa.modeId));[...o].filter(a=>!i.has(a)).length>0&&n.push({id:mt(),type:"theme",severity:"warning",nodeId:e.id,nodeName:e.name,message:`Variable "${e.name}" has ${s} values but collection "${t.name}" has ${t.modes.length} modes`,currentValue:`${s}/${t.modes.length} modes defined`,suggestions:["Ensure all collection modes have values assigned"],autoFixable:!1})}}function Vc(e,t,n,s){if(n.modes.length<2)return;let o=[];for(let a of n.modes){let c=e.valuesByMode[a.modeId],l=t.valuesByMode[a.modeId],d=ni(c),u=ni(l);if(!d||!u)continue;let f=J(d.r,d.g,d.b),m=J(u.r,u.g,u.b),g=re(f,m);o.push({modeName:a.name,ratio:g,passes:g>=4.5})}if(o.length<2)return;let i=o.filter(a=>a.passes),r=o.filter(a=>!a.passes);if(i.length>0&&r.length>0){let a=r.map(l=>`${l.modeName}: ${l.ratio.toFixed(1)}:1`).join(", "),c=i.map(l=>`${l.modeName}: ${l.ratio.toFixed(1)}:1`).join(", ");s.push({id:mt(),type:"accessibility",severity:"critical",nodeId:e.id,nodeName:`${e.name} / ${t.name}`,message:`Contrast passes in [${c}] but fails in [${a}] \u2014 WCAG AA requires 4.5:1`,currentValue:a,suggestions:["Adjust colors so contrast meets WCAG AA in all modes"],autoFixable:!1})}}async function oi(e,t={}){si=0;let n=[],s=await Rc();if(s.length===0)return{issues:[],summary:{totalVariables:0,identicalAcrossModes:0,missingModeValues:0,contrastDegradation:0,modeCountMismatch:0}};let o=0;for(let i of s){if(i.modes.length<2)continue;let r=[];for(let c of i.variableIds){let l=await $c(c);l&&(r.push(l),o++,Mc(l,i,n),Oc(l,i,n),Fc(l,i,n))}let a=Pc(r);for(let{fg:c,bg:l}of a)Vc(c,l,i,n)}return{issues:n,summary:{totalVariables:o,identicalAcrossModes:n.filter(i=>i.message.includes("identical value across modes")).length,missingModeValues:n.filter(i=>i.message.includes("missing values for modes")).length,contrastDegradation:n.filter(i=>i.message.includes("Contrast passes")).length,modeCountMismatch:n.filter(i=>i.message.includes("modes defined")).length}}}Xe();var ri=0;function Be(){return`grid-${++ri}`}function ai(e){if(!e||typeof e!="object")return null;let t=e,n=t.pattern;if(!n||t.visible===!1)return null;let s={pattern:n};return n==="COLUMNS"||n==="ROWS"?(typeof t.count=="number"&&(s.count=t.count),typeof t.gutterSize=="number"&&(s.gutterSize=t.gutterSize),typeof t.alignment=="string"&&(s.alignment=t.alignment),typeof t.sectionSize=="number"&&(s.sectionSize=t.sectionSize),typeof t.offset=="number"&&(s.offset=t.offset)):n==="GRID"&&typeof t.sectionSize=="number"&&(s.sectionSize=t.sectionSize),s}function ii(e){let t=e.parent;if(!t)return!0;let n=t.type;return n==="PAGE"||n==="SECTION"}function Dc(e,t){let n=e.layoutGrids;return!Array.isArray(n)||n.length===0?(t.push({id:Be(),type:"spacing",severity:"info",nodeId:e.id,nodeName:e.name,message:`Top-level frame "${e.name}" has no layout grid attached`,currentValue:"No grid",suggestions:["Add a layout grid for consistent alignment"],autoFixable:!1}),!1):n.filter(o=>!o||typeof o!="object"?!1:o.visible!==!1).length===0?(t.push({id:Be(),type:"spacing",severity:"info",nodeId:e.id,nodeName:e.name,message:`Top-level frame "${e.name}" has layout grids but all are hidden`,currentValue:`${n.length} hidden grid(s)`,suggestions:["Enable at least one layout grid for development reference"],autoFixable:!1}),!1):!0}function _c(e,t,n){let s=e.layoutGrids;if(!Array.isArray(s)||s.length===0)return;let o=e.gridStyleId;!o||o===""?n.push({id:Be(),type:"spacing",severity:"info",nodeId:e.id,nodeName:e.name,message:`Frame "${e.name}" has a hard-coded layout grid (not from a grid style)`,currentValue:"Hard-coded grid",suggestions:["Create a grid style and apply it for consistency across frames"],autoFixable:!1}):t.size>0&&t.has(o)}function Bc(e,t,n){let s=e.layoutGrids;if(Array.isArray(s))for(let o of s){let i=ai(o);i&&i.gutterSize!==void 0&&!t.includes(i.gutterSize)&&n.push({id:Be(),type:"spacing",severity:"info",nodeId:e.id,nodeName:e.name,message:`Grid gutter ${i.gutterSize}px on "${e.name}" is not in the spacing scale [${t.join(", ")}]`,currentValue:`${i.gutterSize}px gutter`,suggestions:t.filter(r=>{var a;return Math.abs(r-((a=i.gutterSize)!=null?a:0))<=8}).map(r=>`${r}px`),autoFixable:!1})}}function Gc(e,t,n){let s=[];function o(i,r){let a=r||"locked"in i&&i.locked===!0,c="visible"in i&&!i.visible;if(!(t&&a)&&!(n&&c)&&((i.type==="FRAME"||i.type==="COMPONENT"||i.type==="COMPONENT_SET")&&ii(i)&&s.push(i),i.type==="SECTION"||!ii(i)&&"children"in i,"children"in i)){let l=i.children;if(Array.isArray(l))for(let d of l){let u=i.type;(u==="SECTION"||u==="GROUP")&&o(d,a)}}}for(let i of e){let r="locked"in i&&i.locked===!0,a="visible"in i&&!i.visible;if(!(t&&r)&&!(n&&a)){if(i.type==="FRAME"||i.type==="COMPONENT"||i.type==="COMPONENT_SET")s.push(i);else if("children"in i){let c=i.children;if(Array.isArray(c))for(let l of c)o(l,!1)}}}return s}function Uc(e,t){var a;let n=[];for(let c of e){let l=c.layoutGrids;if(Array.isArray(l))for(let d of l){let u=ai(d);!u||u.pattern!=="COLUMNS"||u.count!==void 0&&isFinite(u.count)&&n.push({nodeId:c.id,nodeName:c.name,count:u.count})}}if(n.length<2)return;let s=[...new Set(n.map(c=>c.count))];if(s.length<=1)return;let o=new Map;for(let{count:c}of n)o.set(c,((a=o.get(c))!=null?a:0)+1);let i=s[0],r=0;for(let[c,l]of o)l>r&&(i=c,r=l);for(let c of n)c.count!==i&&t.push({id:Be(),type:"spacing",severity:"warning",nodeId:c.nodeId,nodeName:c.nodeName,message:`Frame "${c.nodeName}" uses ${c.count}-column grid while most frames use ${i} columns`,currentValue:`${c.count} columns`,suggestions:[`Change to ${i} columns for consistency`],autoFixable:!1})}async function zc(){try{let e=figma,t=[];return typeof e.getLocalGridStylesAsync=="function"?t=await e.getLocalGridStylesAsync():typeof e.getLocalGridStyles=="function"&&(t=e.getLocalGridStyles()),new Set((t!=null?t:[]).map(n=>n.id))}catch(e){return new Set}}async function ci(e,t={}){let{skipLocked:n=!0,skipHidden:s=!0,spacingScale:o=de}=t;ri=0;let i=[],r=Gc(e,n,s),a=await zc(),c=0;for(let l of r)Dc(l,i)&&(c++,_c(l,a,i),Bc(l,o,i));return Uc(r,i),{issues:i,summary:{totalFrames:r.length,framesWithGrid:c,framesWithoutGrid:r.length-c,inconsistentColumns:i.filter(l=>l.message.includes("column grid while")).length,hardCodedGrids:i.filter(l=>l.message.includes("hard-coded")).length,gutterMismatches:i.filter(l=>l.message.includes("gutter")).length}}}Ne();var ee=null,W="claude-sonnet-4-5-20250929",_="anthropic";function li(e,t=_){let n=(e==null?void 0:e.trim())||"";switch(t){case"anthropic":return n.startsWith("sk-ant-")&&n.length>=40;case"openai":return n.startsWith("sk-")&&n.length>=20;case"google":return n.startsWith("AIza")&&n.length>=35;default:return!1}}var di=null,ui=null,Z=new so({enableCaching:!0,enableMCPIntegration:!0,mcpServerUrl:"https://design-systems-mcp.southleft-llc.workers.dev/mcp"});async function fi(e){let{type:t,data:n}=e,s=t==="save-api-key"?`${t} [redacted]`:t;console.log("Received message:",s);try{switch(t){case"check-api-key":await Hc();break;case"save-api-key":await Wc(n.apiKey,n.model,n.provider);break;case"update-model":await jc(n.model);break;case"analyze":await Kc();break;case"analyze-enhanced":await pi(n);break;case"clear-api-key":await Xc();break;case"chat-message":await Jc(n);break;case"chat-clear-history":await Yc();break;case"select-node":await Qc(n);break;case"preview-fix":await Ll(n);break;case"apply-token-fix":await Pl(n);break;case"apply-naming-fix":await Rl(n);break;case"apply-batch-fix":await $l(n);break;case"update-description":await Ml(n);break;case"add-component-property":await Ol(n);break;case"run-design-lint":Ue(n);break;case"lint-ignore-node":ol(n);break;case"lint-ignore-error":il(n);break;case"lint-ignore-all-of-type":rl(n);break;case"lint-clear-ignored":al();break;case"lint-select-node":cl(n);break;case"lint-select-all-with-value":ll(n);break;case"lint-save-settings":dl(n);break;case"lint-load-settings":ul();break;case"lint-save-team-config":fl(n);break;case"lint-load-team-config":pl();break;case"jump-to-node":ml(n);break;case"fix-spacing":yl(n);break;case"fix-spacing-to-nearest":bl(n);break;case"fix-all-spacing":vl(n);break;case"apply-style-fix":await kl(n);break;case"rename-layer-fix":Nl(n);break;case"fix-radius-to-nearest":Sl(n);break;case"batch-fix-v2":await Il(n);break;case"rescan-lint":mi();break;case"export-screenshot":await hl(n);break;case"analyze-flow":await Cl();break;case"analyze-page":await xl();break;case"save-baseline":Al(n);break;case"load-baseline":wl(n);break;case"compare-baseline":El(n);break;case"delete-baseline":Tl(n);break;case"collect-variables":await Fl();break;case"check-dtcg-compliance":await Vl(n);break;case"compare-modes":await Dl(n);break;case"enable-realtime-lint":Bl(n);break;case"disable-realtime-lint":Gl();break;case"calculate-design-debt":Ul(n);break;case"run-extended-lint":await zl();break;default:console.warn("Unknown message type:",t)}}catch(o){console.error("Error handling message:",o);let i=o instanceof Error?o.message:"Unknown error occurred";S("analysis-error",{error:i})}}async function Hc(){try{await wt();let e=await Et();if(_=e.providerId,W=e.modelId,ee){S("api-key-status",{hasKey:!0,provider:_,model:W});return}e.apiKey&&li(e.apiKey,e.providerId)?(ee=e.apiKey,S("api-key-status",{hasKey:!0,provider:_,model:W})):S("api-key-status",{hasKey:!1,provider:_,model:W})}catch(e){console.error("Error checking API key:",e),S("api-key-status",{hasKey:!1,provider:"anthropic"})}}async function Wc(e,t,n){try{let s=n||_;if(!li(e,s)){let i=ce(s);throw new Error(`Invalid API key format for ${i.name}. Expected format: ${i.keyPlaceholder}`)}_=s,ee=e,t&&(W=t),await Tt(s,W,e),console.log(`${s} API key and model saved successfully`);let o=ce(s);S("api-key-saved",{success:!0,provider:s}),figma.notify(`${o.name} API key saved successfully`,{timeout:2e3})}catch(s){console.error("Error saving API key:",s);let o=s instanceof Error?s.message:"Unknown error occurred";S("api-key-saved",{success:!1,error:o}),figma.notify(`Failed to save API key: ${o}`,{error:!0})}}async function jc(e){try{W=e,await Tt(_,e),console.log("Model updated to:",e),figma.notify(`Model updated to ${e}`,{timeout:2e3})}catch(t){console.error("Error updating model:",t),figma.notify("Failed to update model",{error:!0})}}async function pi(e){var t,n;try{if(!ee){let c=ce(_).name;throw new Error(`API key not found. Please save your ${c} API key first.`)}let s=figma.currentPage.selection;if(s.length===0)throw new Error("No component selected. Please select a Figma component to analyze.");if(e.batchMode&&s.length>1){await qc(s,e);return}let o=s[0];if(o.type==="INSTANCE"){let c=o;try{let l=await c.getMainComponentAsync();if(l)figma.notify("Analyzing main component instead of instance...",{timeout:2e3}),o=l;else throw new Error("This instance has no main component. Please select a component directly.")}catch(l){throw console.error("Error accessing main component:",l),new Error("Could not access main component. Please select a component directly.")}}if(o.type==="COMPONENT"&&((t=o.parent)==null?void 0:t.type)==="COMPONENT_SET"){let l=o.parent;figma.notify("Analyzing parent component set to include all variants...",{timeout:2e3}),o=l}if(!xe(o)){let c=new Set(["COMPONENT_SET","COMPONENT","INSTANCE"]),l=null,d=null,u=o.parent;for(;u&&"type"in u;){let m=u;if(c.has(m.type)&&!l){l=m;break}!d&&xe(m)&&(d=m),u=u.parent}let f=l||d;f&&(figma.notify(`Analyzing parent ${f.type.toLowerCase()} "${f.name}"...`,{timeout:2e3}),o=f)}if(o.type==="INSTANCE"){let c=o;try{let l=await c.getMainComponentAsync();l&&(figma.notify("Analyzing main component instead of instance...",{timeout:2e3}),o=l)}catch(l){}}if(o.type==="COMPONENT"&&((n=o.parent)==null?void 0:n.type)==="COMPONENT_SET"){let c=o.parent;figma.notify("Analyzing parent component set to include all variants...",{timeout:2e3}),o=c}if(!xe(o))throw new Error("Please select a Frame, Component, Component Set, or Instance to analyze");await Z.loadDesignSystemsKnowledge();let i=await qt(o),r=R({enableMCPEnhancement:!0,batchMode:e.batchMode||!1,enableAudit:e.enableAudit!==!1,includeTokenAnalysis:e.includeTokenAnalysis!==!1},e);figma.notify("Performing enhanced analysis with design systems knowledge...",{timeout:3e3});let a=await to(i,ee,W,r,_);di=a.metadata,ui=o,S("enhanced-analysis-result",j(R({},a),{analyzedNodeId:o.id})),figma.notify("Enhanced analysis complete! Check the results panel.",{timeout:3e3})}catch(s){console.error("Error during enhanced analysis:",s);let o=s instanceof Error?s.message:"Unknown error occurred";figma.notify(`Analysis failed: ${o}`,{error:!0}),S("analysis-error",{error:o})}}async function Kc(){await pi({batchMode:!1})}async function qc(e,t){let n=[];await Z.loadDesignSystemsKnowledge();for(let i of e)if(xe(i))try{let r=await qt(i),a=await ge(i),c=[...a.colors,...a.spacing,...a.typography,...a.effects,...a.borders],l=Z.generateComponentHash(r,c,V),d=Z.getCachedAnalysis(l);if(d){console.log(`\u2705 Using cached analysis for ${i.name}`),n.push({node:i.name,success:!0,data:d.result.metadata,cached:!0});continue}let u=Z.createDeterministicPrompt(r),f=await le(_,ee,{prompt:u,model:W,maxTokens:2048,temperature:.1}),m=ye(f.content),g=je(m),p=await Xt(g,r,{batchMode:!0});Z.validateAnalysisConsistency(p,r)||(p=Z.applyConsistencyCorrections(p,r)),Z.cacheAnalysis(l,p),n.push({node:i.name,success:!0,data:p.metadata,cached:!1})}catch(r){n.push({node:i.name,success:!1,error:r instanceof Error?r.message:"Analysis failed"})}let s=n.filter(i=>i.success&&i.cached).length,o=n.filter(i=>i.success&&!i.cached).length;S("batch-analysis-result",{results:n}),figma.notify(`Batch analysis complete: ${o} analyzed, ${s} from cache`,{timeout:3e3})}async function Xc(){try{ee=null,await xn(_),await figma.clientStorage.setAsync("claude-api-key","");let e=ce(_).name;S("api-key-cleared",{success:!0}),figma.notify(`${e} API key cleared`,{timeout:2e3})}catch(e){console.error("Error clearing API key:",e)}}async function Jc(e){try{if(console.log("Processing chat message:",e.message),!ee){let r=ce(_).name;throw new Error(`API key not found. Please save your ${r} API key first.`)}S("chat-response-loading",{isLoading:!0});let t=nl(),n=await tl(e.message),s=sl(e.message,n,e.history,t),i={message:(await le(_,ee,{prompt:s,model:W,maxTokens:2048,temperature:.7})).content,sources:n.sources||[]};S("chat-response",{response:i})}catch(t){console.error("Error handling chat message:",t);let n=t instanceof Error?t.message:"Unknown error occurred";S("chat-error",{error:n})}}async function Yc(){try{S("chat-history-cleared",{success:!0}),figma.notify("Chat history cleared",{timeout:2e3})}catch(e){console.error("Error clearing chat history:",e)}}async function Qc(e){try{console.log("\u{1F3AF} Attempting to select node:",e.nodeId);let t=await figma.getNodeByIdAsync(e.nodeId);if(!t){console.warn("\u26A0\uFE0F Node not found:",e.nodeId),figma.notify("Node not found - it may have been deleted or moved",{error:!0});return}if(!Zc(t)){console.warn("\u26A0\uFE0F Node is not on current page:",e.nodeId),figma.notify("Node is on a different page",{error:!0});return}figma.currentPage.selection=[t],figma.viewport.scrollAndZoomIntoView([t]),console.log("\u2705 Successfully selected and zoomed to node:",t.name),figma.notify(`Selected "${t.name}"`,{timeout:2e3})}catch(t){console.error("Error selecting node:",t);let n=t instanceof Error?t.message:"Unknown error occurred";figma.notify(`Failed to select node: ${n}`,{error:!0})}}function Zc(e){try{let t=e,n=50,s=0;for(;t&&t.parent&&ss.id===t)}catch(n){return!1}}async function tl(e){var t;try{console.log("\u{1F50D} Querying MCP for chat:",e);let n=((t=Z.config)==null?void 0:t.mcpServerUrl)||"https://design-systems-mcp.southleft-llc.workers.dev/mcp",s=[hn(n,e,{category:"general",limit:3}),e.toLowerCase().includes("component")?hn(n,e,{category:"components",limit:2}):Promise.resolve({results:[]}),e.toLowerCase().includes("token")||e.toLowerCase().includes("design token")?hn(n,e,{category:"tokens",limit:2}):Promise.resolve({results:[]})],o=await Promise.allSettled(s),i=[];return o.forEach(r=>{r.status==="fulfilled"&&r.value.results&&i.push(...r.value.results)}),console.log(`\u2705 Found ${i.length} relevant sources for chat query`),{sources:i.slice(0,5)}}catch(n){return console.warn("\u26A0\uFE0F MCP query failed for chat:",n),{sources:[]}}}async function hn(e,t,n={}){let s={jsonrpc:"2.0",id:Math.floor(Math.random()*1e3)+100,method:"tools/call",params:{name:"search_design_knowledge",arguments:R({query:t,limit:n.limit||5},n.category&&{category:n.category})}},o=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!o.ok)throw new Error(`MCP search failed: ${o.status}`);let i=await o.json();return i.result&&i.result.content?{results:i.result.content.map(r=>({title:r.title||"Design System Knowledge",content:r.content||r.description||"",category:r.category||"general"}))}:{results:[]}}function nl(){try{let e=di,t=ui;if(!e&&!t)return null;let n={hasCurrentComponent:!0,timestamp:Date.now()};if(t){n.component={name:t.name,type:t.type,id:t.id};let s=figma.currentPage.selection;s.length>0&&(n.selection={count:s.length,types:s.map(o=>o.type),names:s.map(o=>o.name)})}return e&&(n.analysis={component:e.component,description:e.description,props:e.props||[],states:e.states||[],accessibility:e.accessibility,audit:e.audit,mcpReadiness:e.mcpReadiness}),n}catch(e){return console.warn("Failed to get component context:",e),null}}function sl(e,t,n,s){let o="";n.length>0&&(o=` **Previous Conversation:** -`,n.slice(-6).forEach(d=>{o+=`${d.role==="user"?"User":"Assistant"}: ${d.content} +`,n.slice(-6).forEach(l=>{o+=`${l.role==="user"?"User":"Assistant"}: ${l.content} `}),o+=` -`);let r="";if(s&&s.hasCurrentComponent){if(r=` +`);let i="";if(s&&s.hasCurrentComponent){if(i=` **Current Component Context:** -`,s.component&&(r+=`- Currently analyzing: ${s.component.name} (${s.component.type}) -`),s.selection&&(r+=`- Selected: ${s.selection.count} item(s) - ${s.selection.names.join(", ")} -`),s.analysis){if(r+=`- Component: ${s.analysis.component} -`,r+=`- Description: ${s.analysis.description} -`,s.analysis.props&&s.analysis.props.length>0&&(r+=`- Properties: ${s.analysis.props.map(c=>typeof c=="string"?c:c.name).join(", ")} -`),s.analysis.states&&s.analysis.states.length>0&&(r+=`- States: ${s.analysis.states.join(", ")} -`),s.analysis.audit){let c=[...s.analysis.audit.accessibilityIssues||[],...s.analysis.audit.namingIssues||[],...s.analysis.audit.consistencyIssues||[]];c.length>0&&(r+=`- Current Issues: ${c.slice(0,3).join("; ")}${c.length>3?"...":""} -`)}s.analysis.mcpReadiness&&(r+=`- MCP Readiness Score: ${s.analysis.mcpReadiness.score||"Not scored"} -`)}r+=` -`}let i="";t.sources&&t.sources.length>0&&(i=` +`,s.component&&(i+=`- Currently analyzing: ${s.component.name} (${s.component.type}) +`),s.selection&&(i+=`- Selected: ${s.selection.count} item(s) - ${s.selection.names.join(", ")} +`),s.analysis){if(i+=`- Component: ${s.analysis.component} +`,i+=`- Description: ${s.analysis.description} +`,s.analysis.props&&s.analysis.props.length>0&&(i+=`- Properties: ${s.analysis.props.map(c=>typeof c=="string"?c:c.name).join(", ")} +`),s.analysis.states&&s.analysis.states.length>0&&(i+=`- States: ${s.analysis.states.join(", ")} +`),s.analysis.audit){let c=[...s.analysis.audit.accessibilityIssues||[],...s.analysis.audit.namingIssues||[],...s.analysis.audit.consistencyIssues||[]];c.length>0&&(i+=`- Current Issues: ${c.slice(0,3).join("; ")}${c.length>3?"...":""} +`)}s.analysis.mcpReadiness&&(i+=`- MCP Readiness Score: ${s.analysis.mcpReadiness.score||"Not scored"} +`)}i+=` +`}let r="";t.sources&&t.sources.length>0&&(r=` **Relevant Design Systems Knowledge:** -`,t.sources.forEach((c,d)=>{i+=` -${d+1}. **${c.title}** (${c.category}) +`,t.sources.forEach((c,l)=>{r+=` +${l+1}. **${c.title}** (${c.category}) ${c.content} -`}),i+=` +`}),r+=` `);let a=s&&s.hasCurrentComponent;return`You are a specialized design systems assistant with access to comprehensive design systems knowledge. You're helping a user with their Figma plugin for design system analysis. ${o}**Current User Question:** ${e} -${r}${i}**Instructions:** +${i}${r}**Instructions:** 1. ${a?"The user is currently working on a specific component in Figma. Use the component context above to provide specific, actionable advice about their current work.":"Provide helpful, accurate answers based on the design systems knowledge provided"} 2. ${a?'If they ask about "this component" or "my component", refer to the current component context provided above':"If you need context about a specific component, suggest they select and analyze a component first"} 3. Be conversational and practical in your responses @@ -435,4 +435,4 @@ ${r}${i}**Instructions:** ${a?"Since you have context about their current component, prioritize advice that directly applies to what they're working on.":"If the user wants component-specific advice, suggest they select and analyze a component in Figma first."} -Respond naturally and helpfully to the user's question.`}var D=R({},j),Le=null;function Re(e){let t=(e==null?void 0:e.settings)||D;(!Le||e!=null&&e.resetScope)&&(Le=figma.currentPage.selection.map(o=>o.id));let n=Le.map(o=>figma.getNodeById(o)).filter(o=>o!==null&&o.type!=="DOCUMENT"&&o.type!=="PAGE");n.length>0&&(figma.currentPage.selection=n);let s=xe(t);S("design-lint-result",s)}function Ye(){try{let e=It();figma.root.setPluginData("ignoredState",JSON.stringify(e))}catch(e){}}function la(e){kt(e.nodeId),Ye(),Re()}function da(e){Nt(e.nodeId,e.errorType,e.value),Ye(),Re()}function ua(e){let t=xe(D);wt(t.errors,e.errorType),Ye(),Re()}function pa(){Ct(),Ye(),Re()}function ma(e){try{let t=figma.getNodeById(e.nodeId);t&&t.type!=="DOCUMENT"&&t.type!=="PAGE"&&(figma.currentPage.selection=[t],figma.viewport.scrollAndZoomIntoView([t]))}catch(t){console.warn("Could not select node:",e.nodeId,t)}}function fa(e){let t=figma.currentPage.selection;if(t.length===0)return;let n=At(t,e.errorType,e.value,D),o=[...new Set(n.map(r=>r.nodeId))].map(r=>figma.getNodeById(r)).filter(r=>r!==null&&r.type!=="DOCUMENT"&&r.type!=="PAGE");o.length>0&&(figma.currentPage.selection=o,figma.viewport.scrollAndZoomIntoView(o),S("lint-selected-nodes",{count:o.length,value:e.value}))}async function ga(e){D=e.settings;try{await figma.clientStorage.setAsync("design-lint-settings",e.settings)}catch(t){console.warn("Could not save lint settings:",t)}}async function ya(){try{let e=await figma.clientStorage.getAsync("design-lint-settings");e&&(D=R(R({},j),e)),S("lint-settings-loaded",D)}catch(e){console.warn("Could not load lint settings:",e),S("lint-settings-loaded",j)}}function ha(e){try{let t=e.config;if(!t||t.version!==1){S("team-config-saved",{success:!1,error:"Invalid config version"});return}figma.root.setSharedPluginData("figmalint","config",JSON.stringify(t)),S("team-config-saved",{success:!0})}catch(t){S("team-config-saved",{success:!1,error:String(t)})}}function ba(){var e,t;try{let n=figma.root.getSharedPluginData("figmalint","config");if(n){let s=JSON.parse(n);D=R({},j),(e=s.scales)!=null&&e.spacing&&(D.spacingScale=s.scales.spacing),(t=s.scales)!=null&&t.radius&&(D.allowedRadii=s.scales.radius),s.severityOverrides&&(D.severityOverrides=s.severityOverrides),s.ignorePatterns&&(D.ignorePatterns=s.ignorePatterns),S("team-config-loaded",{config:s,settings:D})}else S("team-config-loaded",{config:null,settings:D})}catch(n){console.warn("Could not load team config:",n),S("team-config-loaded",{config:null,settings:D})}}function va(e){try{let t=figma.getNodeById(e.nodeId);t&&t.type!=="DOCUMENT"&&t.type!=="PAGE"&&(figma.currentPage.selection=[t],figma.viewport.scrollAndZoomIntoView([t]))}catch(t){console.warn("Could not jump to node:",e.nodeId,t)}}var Sa=new Set(["itemSpacing","paddingTop","paddingBottom","paddingLeft","paddingRight","counterAxisSpacing"]);function ka(e){try{if(!Sa.has(e.property)){S("fix-error",{error:`Invalid spacing property: ${e.property}`});return}let t=figma.getNodeById(e.nodeId);if(t&&(t.type==="FRAME"||t.type==="COMPONENT"||t.type==="INSTANCE")){let n=t[e.property];t[e.property]=e.value,S("fix-applied",{type:"spacing",nodeId:e.nodeId,nodeName:t.name,property:e.property,oldValue:n,newValue:e.value})}}catch(t){console.warn("Could not fix spacing:",t),S("fix-error",{error:"Failed to apply spacing fix"})}}async function Na(e){try{let t=null;if(e!=null&&e.nodeId){let i=figma.getNodeById(e.nodeId);i&&i.type!=="DOCUMENT"&&i.type!=="PAGE"&&(t=i)}else figma.currentPage.selection.length>0&&(t=figma.currentPage.selection[0]);if(!t){S("screenshot-error",{error:"No node selected"});return}let n=await ze(t),s="layoutMode"in t&&t.layoutMode!=="NONE",o="children"in t?t.children.length:0,r;try{let i=await ue(t),a=[...i.colors,...i.spacing,...i.typography,...i.effects,...i.borders];r={totalTokens:a.length,boundToVariables:a.filter(c=>c.source==="figma-variable").length,boundToStyles:a.filter(c=>c.source==="figma-style").length,hardCoded:a.filter(c=>c.source==="hard-coded").length}}catch(i){}S("screenshot-result",{nodeId:t.id,nodeName:t.name,nodeType:t.type,screenshot:n,width:t.width,height:t.height,hasAutoLayout:s,childCount:o,tokenSummary:r})}catch(t){console.warn("Could not export screenshot:",t),S("screenshot-error",{error:"Failed to export screenshot"})}}function wa(e){try{let t=Ae(e.nodeId,e.property);S("fix-applied",{type:"spacing",nodeId:t.nodeId,nodeName:t.nodeName,property:e.property,oldValue:t.oldValue,newValue:t.newValue,success:t.success,error:t.error})}catch(t){S("fix-error",{error:"Failed to auto-fix spacing"})}}function Ca(e){try{let t=e.allowedRadii||[0,2,4,8,12,16,20,24,32],n=qe(e.nodeId,t);S("fix-applied",{type:"radius",nodeId:n.nodeId,nodeName:n.nodeName,oldValue:n.oldValue,newValue:n.newValue,success:n.success,error:n.error})}catch(t){S("fix-error",{error:"Failed to auto-fix radius"})}}function Ia(e){try{let t=no(e.nodeId),n=t.filter(s=>s.success).length;for(let s of t)S("fix-applied",{type:"spacing",nodeId:s.nodeId,nodeName:s.nodeName,property:s.property,oldValue:s.oldValue,newValue:s.newValue,success:s.success});n>0&&figma.notify(`Fixed ${n} spacing value${n!==1?"s":""}`,{timeout:2e3})}catch(t){S("fix-error",{error:"Failed to fix all spacing"})}}async function xa(e){try{let{applyFillStyle:t,applyStrokeStyle:n,applyTextStyle:s,applyEffectStyle:o}=await Promise.resolve().then(()=>(jt(),so)),r;switch(e.styleType){case"fill":r=await t(e.nodeId,e.styleKey);break;case"stroke":r=await n(e.nodeId,e.styleKey);break;case"text":r=await s(e.nodeId,e.styleKey);break;case"effect":r=await o(e.nodeId,e.styleKey);break;default:S("fix-error",{error:`Unknown style type: ${e.styleType}`});return}S("fix-applied",{type:"style",nodeId:r.nodeId,nodeName:r.nodeName,property:r.property,oldValue:r.oldValue,newValue:r.newValue,success:r.success,error:r.error})}catch(t){S("fix-error",{error:"Failed to apply style"})}}function Aa(e){try{let t=Je(e.nodeId,e.newName);S("fix-applied",{type:"rename",nodeId:t.nodeId,nodeName:t.newName,oldValue:t.oldName,newValue:t.newName,success:t.success,error:t.error})}catch(t){S("fix-error",{error:"Failed to rename layer"})}}async function Ea(e){try{let t=await oo(e.fixes);S("batch-fix-v2-result",t),t.failed===0?figma.notify(`Applied ${t.applied} fix${t.applied!==1?"es":""} successfully`,{timeout:2e3}):t.applied>0?figma.notify(`Applied ${t.applied}, ${t.failed} failed`,{timeout:3e3}):figma.notify(`All ${t.failed} fixes failed`,{error:!0}),So()}catch(t){S("fix-error",{error:"Batch fix failed"})}}function So(){if(Le){let t=Le.map(n=>figma.getNodeById(n)).filter(n=>n!==null&&n.type!=="DOCUMENT"&&n.type!=="PAGE");t.length>0&&(figma.currentPage.selection=t)}let e=xe(D);S("design-lint-result",e),S("rescan-complete",{totalErrors:e.summary.totalErrors,nodesWithErrors:e.summary.nodesWithErrors})}async function Ta(){try{S("flow-analysis-started",{status:"building-graph"});let e=Gs();if(e.frames.length===0){S("flow-analysis-error",{error:"No top-level frames found on current page."});return}if(e.frames.length>50){S("flow-analysis-error",{error:`Too many frames (${e.frames.length}). Select a page with \u226450 frames for flow analysis.`});return}let t=zs(e);S("flow-analysis-started",{status:"capturing-screenshots",total:e.frames.length});let n={},s={},o=10;for(let c=0;c{let u=await figma.getNodeByIdAsync(p.id);if(!(!u||!("exportAsync"in u))){try{let g=await ze(u);n[p.id]=g}catch(g){}try{let{runDesignLint:g}=await Promise.resolve().then(()=>(be(),Et)),f=g([u],D);s[p.id]=f}catch(g){}}});await Promise.all(l),S("flow-analysis-started",{status:"capturing-screenshots",progress:Math.min(c+o,e.frames.length),total:e.frames.length})}let r=[];for(let c of e.frames){let d=await figma.getNodeByIdAsync(c.id);d&&r.push({frame:c,node:d})}let i=Ks(r,{skipLocked:D.skipLockedLayers,skipHidden:D.skipHiddenLayers}),a=[...t,...i];S("flow-analysis-result",{graph:e,graphIssues:a,screenshots:n,lintResults:s})}catch(e){let t=e instanceof Error?e.message:"Unknown error";S("flow-analysis-error",{error:t})}}async function Pa(){try{let t=figma.currentPage.children.filter(l=>l.type==="FRAME"||l.type==="COMPONENT_SET");if(t.length===0){S("analysis-error",{error:"No top-level frames found on current page."});return}let n=t.slice(0,50),s=n.length,{runDesignLint:o}=await Promise.resolve().then(()=>(be(),Et)),r=[],i=5;for(let l=0;l{let h=l+m+1;S("page-sweep-progress",{current:h,total:s,frameName:f.name});let C={summary:{totalErrors:0,byType:{},totalNodes:0,nodesWithErrors:0},errors:[]};try{C=o([f],D)}catch(N){}let k="";try{k=await ze(f,800)}catch(N){}return{id:f.id,name:f.name,screenshot:k,lintResult:{summary:C.summary,errors:C.errors},width:Math.round(f.width),height:Math.round(f.height)}}),g=await Promise.all(u);r.push(...g)}let a=0,c={};for(let l of r){a+=l.lintResult.summary.totalErrors||0;for(let p of l.lintResult.errors){let u=p.errorType;c[u]||(c[u]={count:0,severity:p.severity||"warning"}),c[u].count++}}let d=Object.entries(c).sort((l,p)=>p[1].count-l[1].count).slice(0,10).map(([l,{count:p,severity:u}])=>({type:l,count:p,severity:u}));S("page-sweep-result",{frames:r,aggregated:{totalFrames:s,totalIssues:a,topIssues:d}})}catch(e){let t=e instanceof Error?e.message:"Unknown error";S("analysis-error",{error:`Page sweep failed: ${t}`})}}async function ko(){var e,t;try{let n=await dt();_=n.providerId,H=n.modelId,n.apiKey?(Z=n.apiKey,S("api-key-status",{hasKey:!0,provider:_,model:H})):S("api-key-status",{hasKey:!1,provider:_,model:H}),console.log(`Plugin initialized with provider: ${_}, model: ${H}`);try{let s=figma.root.getPluginData("ignoredState");if(s){let o=JSON.parse(s);xt(o),console.log(`Restored ${((e=o.nodeIds)==null?void 0:e.length)||0} ignored nodes, ${((t=o.errorKeys)==null?void 0:t.length)||0} ignored errors`)}}catch(s){}console.log("\u{1F504} Initializing design systems knowledge..."),Q.loadDesignSystemsKnowledge().then(()=>{console.log("\u2705 Design systems knowledge loaded successfully")}).catch(s=>{console.warn("\u26A0\uFE0F Failed to load design systems knowledge, using fallback:",s)}),console.log("Plugin initialized successfully")}catch(n){console.error("Error initializing plugin:",n)}}function No(){let e=figma.currentPage.selection;if(e.length!==0)try{let t=e[0],n=ne([t],D),s=n.summary.totalNodes||1,o={critical:10,warning:3,info:1},r=n.errors.reduce((l,p)=>l+(o[p.severity||"warning"]||3),0),i=Math.max(0,s-n.errors.length)*10,a=i+r,c=a>0?Math.round(i/a*100):100,d=n.errors.some(l=>l.severity==="critical")?"critical":n.errors.some(l=>l.severity==="warning")?"warning":n.errors.length>0?"info":"none";S("selection-mini-score",{nodeId:t.id,nodeName:t.name,score:c,issueCount:n.summary.totalErrors,topSeverity:d})}catch(t){}}function La(e){let t={version:1,timestamp:Date.now(),nodeId:e.nodeId,nodeName:e.nodeName,overall:e.overall,grade:e.grade,categories:e.categories,summary:e.summary,errors:e.errors.map(n=>({errorType:n.errorType,severity:n.severity||"warning",nodeId:n.nodeId,message:n.message}))};Xs(t),S("baseline-saved",{nodeId:e.nodeId,nodeName:e.nodeName,timestamp:t.timestamp,overall:e.overall})}function Ra(e){let t=Zs(e.nodeId);S("baseline-loaded",t)}function $a(e){let t=Ys(e.nodeId);if(!t){S("diff-result",null);return}let n=e.errors.map(o=>({errorType:o.errorType,severity:o.severity||"warning",nodeId:o.nodeId,message:o.message})),s=to(t,{overall:e.overall,grade:e.grade,categories:e.categories,errors:n,summary:e.summary});S("diff-result",s)}function Ma(e){Qs(e.nodeId)}async function Oa(e){try{let t=await figma.getNodeByIdAsync(e.nodeId);if(!t||!("type"in t)){S("fix-preview",{success:!1,error:"Node not found or is not a valid scene node"});return}let n=t,s=null;if(e.type==="token"){if(!e.propertyPath){S("fix-preview",{success:!1,error:"Property path is required for token fixes"});return}if(e.propertyPath.match(/^(fills|strokes)\[(\d+)\]$/)){let r=await Ot(e.suggestedValue||"",.1);r.length>0&&(s=await _t(n,e.propertyPath,r[0].variableId))}else{let r=parseFloat(e.suggestedValue||"0"),i=await Ft(r,e.propertyPath||"",2);i.length>0&&(s=await _t(n,e.propertyPath,i[0].variableId))}if(s){let r=s;S("fix-preview",{success:!0,type:"token",nodeId:r.nodeId,nodeName:r.nodeName,propertyPath:r.propertyPath,beforeValue:r.beforeValue,afterValue:r.afterValue,tokenId:r.tokenId,tokenName:r.tokenName})}else S("fix-preview",{success:!1,error:"No matching token found for this value"})}else if(e.type==="naming"){let o=e.suggestedValue||fe(n);s=mn(n,o),S("fix-preview",{success:!0,preview:s})}else S("fix-preview",{success:!1,error:`Unknown fix type: ${e.type}`})}catch(t){console.error("Error previewing fix:",t);let n=t instanceof Error?t.message:"Unknown error occurred";S("fix-preview",{success:!1,error:n})}}async function Fa(e){try{let t=await figma.getNodeByIdAsync(e.nodeId);if(!t||!("type"in t)){S("fix-applied",{success:!1,error:"Node not found or is not a valid scene node"}),figma.notify("Failed to apply fix: Node not found",{error:!0});return}let n=t;if(!e.propertyPath){S("fix-applied",{success:!1,error:"Property path is required for token fixes"}),figma.notify("Failed to apply fix: Property path missing",{error:!0});return}if(!e.tokenId){S("fix-applied",{success:!1,error:"Token ID is required for token fixes"}),figma.notify("Failed to apply fix: Token ID missing",{error:!0});return}let s;/^(fills|strokes)\[\d+\]$/.test(e.propertyPath)?s=await Dt(n,e.propertyPath,e.tokenId):s=await Vt(n,e.propertyPath,e.tokenId),S("fix-applied",K(R({},s),{fixType:"token",nodeId:e.nodeId,propertyPath:e.propertyPath})),s.success?figma.notify(`Applied token to ${n.name}`,{timeout:2e3}):figma.notify(`Failed to apply token: ${s.error||s.message}`,{error:!0})}catch(t){console.error("Error applying token fix:",t);let n=t instanceof Error?t.message:"Unknown error occurred";S("fix-applied",{success:!1,error:n,fixType:"token",nodeId:e.nodeId}),figma.notify(`Failed to apply fix: ${n}`,{error:!0})}}async function Da(e){try{let t=await figma.getNodeByIdAsync(e.nodeId);if(!t||!("type"in t)){S("fix-applied",{success:!1,error:"Node not found or is not a valid scene node"}),figma.notify("Failed to rename: Node not found",{error:!0});return}let n=t,s=e.newValue||fe(n),o=n.name;if(o===s){S("fix-applied",{success:!0,fixType:"naming",nodeId:e.nodeId,message:`Layer already named "${s}"`,oldName:o,newName:s}),figma.notify(`Layer already named "${s}"`,{timeout:2e3});return}let r=pt(n,s),i={success:r,fixType:"naming",nodeId:e.nodeId,message:r?`Renamed "${o}" to "${s}"`:"Failed to rename layer",oldName:o,newName:r?s:o};S("fix-applied",i),r?figma.notify(`Renamed "${o}" to "${s}"`,{timeout:2e3}):figma.notify("Failed to rename layer",{error:!0})}catch(t){console.error("Error applying naming fix:",t);let n=t instanceof Error?t.message:"Unknown error occurred";S("fix-applied",{success:!1,error:n}),figma.notify(`Failed to rename: ${n}`,{error:!0})}}async function Va(e){try{let t=[],n=0,s=0;for(let r of e.fixes)try{let i=await figma.getNodeByIdAsync(r.nodeId);if(!i||!("type"in i)){t.push({nodeId:r.nodeId,success:!1,message:"Node not found",error:"Node not found or is not a valid scene node"}),s++;continue}let a=i;if(r.type==="token"){if(!r.propertyPath){t.push({nodeId:r.nodeId,success:!1,message:"Missing property path",error:"Token fixes require a propertyPath"}),s++;continue}let c=r.tokenId,d=/^(fills|strokes)\[\d+\]$/.test(r.propertyPath);if(!c&&r.newValue)try{if(d){let p=await Ot(r.newValue,.1);p.length>0&&(c=p[0].variableId)}else{let p=parseFloat(r.newValue);if(!isNaN(p)){let u=await Ft(p,r.propertyPath||"",2);u.length>0&&(c=u[0].variableId)}}}catch(p){console.warn("Could not find matching variable:",p)}if(!c){t.push({nodeId:r.nodeId,success:!1,message:"No matching design token found for this value",error:"Could not find a matching variable to bind"}),s++;continue}let l;d?l=await Dt(a,r.propertyPath,c):l=await Vt(a,r.propertyPath,c),t.push({nodeId:r.nodeId,success:l.success,message:l.message,error:l.error}),l.success?n++:s++}else if(r.type==="naming"){let c=r.newValue||fe(a),d=a.name,l=pt(a,c);t.push({nodeId:r.nodeId,success:l,message:l?`Renamed "${d}" to "${c}"`:"Failed to rename layer"}),l?n++:s++}else t.push({nodeId:r.nodeId,success:!1,message:`Unknown fix type: ${r.type}`,error:`Unsupported fix type: ${r.type}`}),s++}catch(i){let a=i instanceof Error?i.message:"Unknown error";t.push({nodeId:r.nodeId,success:!1,message:"Error applying fix",error:a}),s++}let o={total:e.fixes.length,success:n,errors:s,results:t};S("batch-fix-applied",o),s===0?figma.notify(`Applied ${n} fix${n!==1?"es":""} successfully`,{timeout:2e3}):n>0?figma.notify(`Applied ${n} fix${n!==1?"es":""}, ${s} failed`,{timeout:3e3}):figma.notify(`Failed to apply ${s} fix${s!==1?"es":""}`,{error:!0})}catch(t){console.error("Error applying batch fixes:",t);let n=t instanceof Error?t.message:"Unknown error occurred";S("batch-fix-applied",{total:e.fixes.length,success:0,errors:e.fixes.length,error:n}),figma.notify(`Batch fix failed: ${n}`,{error:!0})}}async function _a(e){try{let t=await figma.getNodeByIdAsync(e.nodeId);if(!t){S("description-updated",{success:!1,error:"Node not found"}),figma.notify("Failed to update description: Node not found",{error:!0});return}if(t.type!=="COMPONENT"&&t.type!=="COMPONENT_SET"){S("description-updated",{success:!1,error:"Node is not a component or component set"}),figma.notify("Description can only be set on components",{error:!0});return}let n=t,s=n.description;n.description=e.description,S("description-updated",{success:!0,oldDescription:s,newDescription:e.description}),figma.notify("Component description updated",{timeout:2e3})}catch(t){console.error("Error updating description:",t);let n=t instanceof Error?t.message:"Unknown error occurred";S("description-updated",{success:!1,error:n}),figma.notify(`Failed to update description: ${n}`,{error:!0})}}async function Ba(e){try{let{nodeId:t,propertyName:n,propertyType:s,defaultValue:o}=e,r=await figma.getNodeByIdAsync(t);if(!r){S("property-added",{success:!1,propertyName:n,message:"Node not found"}),figma.notify("Node not found",{error:!0});return}let i=null;if(r.type==="COMPONENT"){let l=r;l.parent&&l.parent.type==="COMPONENT_SET"?i=l.parent:i=l}else if(r.type==="COMPONENT_SET")i=r;else if(r.type==="INSTANCE"){let l=await r.getMainComponentAsync();l&&(l.parent&&l.parent.type==="COMPONENT_SET"?i=l.parent:i=l)}if(!i){S("property-added",{success:!1,propertyName:n,message:"Selected node is not a component"}),figma.notify("Selected node is not a component",{error:!0});return}let a=i.componentPropertyDefinitions;for(let l of Object.keys(a))if(l.replace(/#\d+:\d+$/,"").toLowerCase()===n.toLowerCase()){S("property-added",{success:!1,propertyName:n,message:`Property "${n}" already exists`}),figma.notify(`Property "${n}" already exists`,{error:!0});return}let c;switch(s.toLowerCase()){case"boolean":c="BOOLEAN";break;case"text":c="TEXT";break;case"slot":c="INSTANCE_SWAP";break;case"variant":i.type==="COMPONENT_SET"?c="VARIANT":c="TEXT";break;default:c="TEXT"}i.addComponentProperty(n,c,o);let d="";if(c==="VARIANT"&&i.type==="COMPONENT_SET"&&e.variantOptions&&e.variantOptions.length>1){let l=i,p=[...l.children],u=e.variantOptions.slice(1),g=`${n}=${o}`,f=figma.currentPage,m=l;for(;m.parent&&m.parent.type!=="PAGE";)m=m.parent;let h=m.absoluteTransform[0][2],C=m.absoluteTransform[1][2],k=h,N=C+m.height+50,y=figma.createSection();y.name=`FigmaLint: ${n} Variants`,f.appendChild(y),y.x=k,y.y=N;let b=figma.createText();await figma.loadFontAsync({family:"Inter",style:"Medium"}),b.fontName={family:"Inter",style:"Medium"},b.characters=`New "${n}" variants \u2014 drag into the ComponentSet`,b.fontSize=14,b.fills=[{type:"SOLID",color:{r:.4,g:.4,b:.4}}],y.appendChild(b),b.x=24,b.y=24;let I=24,w=32,P=b.y+b.height+24,O=b.width+I*2;for(let M of u){let z=`${n}=${M}`,x=figma.createText();await figma.loadFontAsync({family:"Inter",style:"Semi Bold"}),x.fontName={family:"Inter",style:"Semi Bold"},x.characters=`${n}=${M}`,x.fontSize=12,x.fills=[{type:"SOLID",color:{r:.6,g:.3,b:.9}}],y.appendChild(x),x.x=I,x.y=P,P+=x.height+12;let $=I,v=0;for(let T of p){let L=T.clone();L.name=L.name.replace(g,z),y.appendChild(L),L.x=$,L.y=P,$+=L.width+w,v=Math.max(v,L.height)}O=Math.max(O,$-w+I),P+=v+w}y.resizeWithoutConstraints(Math.max(O,400),P+I),d=" \u2014 new variants created in staging section to the right"}S("property-added",{success:!0,propertyName:n,message:`Property "${n}" added successfully${d}`}),figma.notify(`Property "${n}" added${d?" (see staging section)":""}`,{timeout:3e3})}catch(t){console.error("Error adding component property:",t);let n=t instanceof Error?t.message:"Unknown error occurred";S("property-added",{success:!1,propertyName:e.propertyName,message:n}),figma.notify(`Failed to add property: ${n}`,{error:!0})}}async function Ua(){try{let e=await qt();S("variable-system-result",e)}catch(e){console.error("Error collecting variables:",e);let t=e instanceof Error?e.message:"Unknown error";S("variable-system-error",{error:t})}}async function Ga(e){try{let t=ro(e.dtcgJson),n=await qt(),s=ao(n,t,null);S("dtcg-compliance-result",s)}catch(t){console.error("Error checking DTCG compliance:",t);let n=t instanceof Error?t.message:"Unknown error";S("dtcg-compliance-error",{error:n})}}async function za(e){try{let t=await co(e.collectionId);S("mode-comparison-result",t)}catch(t){console.error("Error comparing modes:",t);let n=t instanceof Error?t.message:"Unknown error";S("mode-comparison-error",{error:n})}}function Wa(e){let t=e.settings||j;uo({enabled:!0,debounceMs:e.debounceMs||500,settings:t})}function Ha(){po()}function Ka(e){let t=fo(e.lintResult,e.tokenSummary||null);S("design-debt-result",t)}var ja={width:380,height:600,themeColors:!0};try{figma.showUI(__html__,ja),console.log("\u2705 FigmaLint v2.0 - UI shown successfully")}catch(e){console.log("\u2139\uFE0F UI might already be shown in inspect panel:",e)}figma.ui.onmessage=bo;figma.on("selectionchange",()=>{let e=figma.currentPage.selection;figma.ui.postMessage({type:"selection-changed",data:{hasSelection:e.length>0,nodeId:e.length>0?e[0].id:null,nodeName:e.length>0?e[0].name:null}}),e.length>0&&No()});ko();console.log("\u{1F680} FigmaLint v2.0 initialized with modular architecture");})(); +Respond naturally and helpfully to the user's question.`}var V=R({},K),Ge=null;function Ue(e){let t=(e==null?void 0:e.settings)||V;(!Ge||e!=null&&e.resetScope)&&(Ge=figma.currentPage.selection.map(o=>o.id));let n=Ge.map(o=>figma.getNodeById(o)).filter(o=>o!==null&&o.type!=="DOCUMENT"&&o.type!=="PAGE");n.length>0&&(figma.currentPage.selection=n);let s=Te(t);S("design-lint-result",s)}function gt(){try{let e=Ut();figma.root.setPluginData("ignoredState",JSON.stringify(e))}catch(e){}}function ol(e){Dt(e.nodeId),gt(),Ue()}function il(e){_t(e.nodeId,e.errorType,e.value),gt(),Ue()}function rl(e){let t=Te(V);Bt(t.errors,e.errorType),gt(),Ue()}function al(){Gt(),gt(),Ue()}function cl(e){try{let t=figma.getNodeById(e.nodeId);t&&t.type!=="DOCUMENT"&&t.type!=="PAGE"&&(figma.currentPage.selection=[t],figma.viewport.scrollAndZoomIntoView([t]))}catch(t){console.warn("Could not select node:",e.nodeId,t)}}function ll(e){let t=figma.currentPage.selection;if(t.length===0)return;let n=Ht(t,e.errorType,e.value,V),o=[...new Set(n.map(i=>i.nodeId))].map(i=>figma.getNodeById(i)).filter(i=>i!==null&&i.type!=="DOCUMENT"&&i.type!=="PAGE");o.length>0&&(figma.currentPage.selection=o,figma.viewport.scrollAndZoomIntoView(o),S("lint-selected-nodes",{count:o.length,value:e.value}))}async function dl(e){V=e.settings;try{await figma.clientStorage.setAsync("design-lint-settings",e.settings)}catch(t){console.warn("Could not save lint settings:",t)}}async function ul(){try{let e=await figma.clientStorage.getAsync("design-lint-settings");e&&(V=R(R({},K),e)),S("lint-settings-loaded",V)}catch(e){console.warn("Could not load lint settings:",e),S("lint-settings-loaded",K)}}function fl(e){try{let t=e.config;if(!t||t.version!==1){S("team-config-saved",{success:!1,error:"Invalid config version"});return}figma.root.setSharedPluginData("figmalint","config",JSON.stringify(t)),S("team-config-saved",{success:!0})}catch(t){S("team-config-saved",{success:!1,error:String(t)})}}function pl(){var e,t;try{let n=figma.root.getSharedPluginData("figmalint","config");if(n){let s=JSON.parse(n);V=R({},K),(e=s.scales)!=null&&e.spacing&&(V.spacingScale=s.scales.spacing),(t=s.scales)!=null&&t.radius&&(V.allowedRadii=s.scales.radius),s.severityOverrides&&(V.severityOverrides=s.severityOverrides),s.ignorePatterns&&(V.ignorePatterns=s.ignorePatterns),S("team-config-loaded",{config:s,settings:V})}else S("team-config-loaded",{config:null,settings:V})}catch(n){console.warn("Could not load team config:",n),S("team-config-loaded",{config:null,settings:V})}}function ml(e){try{let t=figma.getNodeById(e.nodeId);t&&t.type!=="DOCUMENT"&&t.type!=="PAGE"&&(figma.currentPage.selection=[t],figma.viewport.scrollAndZoomIntoView([t]))}catch(t){console.warn("Could not jump to node:",e.nodeId,t)}}var gl=new Set(["itemSpacing","paddingTop","paddingBottom","paddingLeft","paddingRight","counterAxisSpacing"]);function yl(e){try{if(!gl.has(e.property)){S("fix-error",{error:`Invalid spacing property: ${e.property}`});return}let t=figma.getNodeById(e.nodeId);if(t&&(t.type==="FRAME"||t.type==="COMPONENT"||t.type==="INSTANCE")){let n=t[e.property];t[e.property]=e.value,S("fix-applied",{type:"spacing",nodeId:e.nodeId,nodeName:t.name,property:e.property,oldValue:n,newValue:e.value})}}catch(t){console.warn("Could not fix spacing:",t),S("fix-error",{error:"Failed to apply spacing fix"})}}async function hl(e){try{let t=null;if(e!=null&&e.nodeId){let r=figma.getNodeById(e.nodeId);r&&r.type!=="DOCUMENT"&&r.type!=="PAGE"&&(t=r)}else figma.currentPage.selection.length>0&&(t=figma.currentPage.selection[0]);if(!t){S("screenshot-error",{error:"No node selected"});return}let n=await et(t),s="layoutMode"in t&&t.layoutMode!=="NONE",o="children"in t?t.children.length:0,i;try{let r=await ge(t),a=[...r.colors,...r.spacing,...r.typography,...r.effects,...r.borders];i={totalTokens:a.length,boundToVariables:a.filter(c=>c.source==="figma-variable").length,boundToStyles:a.filter(c=>c.source==="figma-style").length,hardCoded:a.filter(c=>c.source==="hard-coded").length}}catch(r){}S("screenshot-result",{nodeId:t.id,nodeName:t.name,nodeType:t.type,screenshot:n,width:t.width,height:t.height,hasAutoLayout:s,childCount:o,tokenSummary:i})}catch(t){console.warn("Could not export screenshot:",t),S("screenshot-error",{error:"Failed to export screenshot"})}}function bl(e){try{let t=Le(e.nodeId,e.property);S("fix-applied",{type:"spacing",nodeId:t.nodeId,nodeName:t.nodeName,property:e.property,oldValue:t.oldValue,newValue:t.newValue,success:t.success,error:t.error})}catch(t){S("fix-error",{error:"Failed to auto-fix spacing"})}}function Sl(e){try{let t=e.allowedRadii||[0,2,4,8,12,16,20,24,32],n=it(e.nodeId,t);S("fix-applied",{type:"radius",nodeId:n.nodeId,nodeName:n.nodeName,oldValue:n.oldValue,newValue:n.newValue,success:n.success,error:n.error})}catch(t){S("fix-error",{error:"Failed to auto-fix radius"})}}function vl(e){try{let t=Io(e.nodeId),n=t.filter(s=>s.success).length;for(let s of t)S("fix-applied",{type:"spacing",nodeId:s.nodeId,nodeName:s.nodeName,property:s.property,oldValue:s.oldValue,newValue:s.newValue,success:s.success});n>0&&figma.notify(`Fixed ${n} spacing value${n!==1?"s":""}`,{timeout:2e3})}catch(t){S("fix-error",{error:"Failed to fix all spacing"})}}async function kl(e){try{let{applyFillStyle:t,applyStrokeStyle:n,applyTextStyle:s,applyEffectStyle:o}=await Promise.resolve().then(()=>(un(),Co)),i;switch(e.styleType){case"fill":i=await t(e.nodeId,e.styleKey);break;case"stroke":i=await n(e.nodeId,e.styleKey);break;case"text":i=await s(e.nodeId,e.styleKey);break;case"effect":i=await o(e.nodeId,e.styleKey);break;default:S("fix-error",{error:`Unknown style type: ${e.styleType}`});return}S("fix-applied",{type:"style",nodeId:i.nodeId,nodeName:i.nodeName,property:i.property,oldValue:i.oldValue,newValue:i.newValue,success:i.success,error:i.error})}catch(t){S("fix-error",{error:"Failed to apply style"})}}function Nl(e){try{let t=rt(e.nodeId,e.newName);S("fix-applied",{type:"rename",nodeId:t.nodeId,nodeName:t.newName,oldValue:t.oldName,newValue:t.newName,success:t.success,error:t.error})}catch(t){S("fix-error",{error:"Failed to rename layer"})}}async function Il(e){try{let t=await xo(e.fixes);S("batch-fix-v2-result",t),t.failed===0?figma.notify(`Applied ${t.applied} fix${t.applied!==1?"es":""} successfully`,{timeout:2e3}):t.applied>0?figma.notify(`Applied ${t.applied}, ${t.failed} failed`,{timeout:3e3}):figma.notify(`All ${t.failed} fixes failed`,{error:!0}),mi()}catch(t){S("fix-error",{error:"Batch fix failed"})}}function mi(){if(Ge){let t=Ge.map(n=>figma.getNodeById(n)).filter(n=>n!==null&&n.type!=="DOCUMENT"&&n.type!=="PAGE");t.length>0&&(figma.currentPage.selection=t)}let e=Te(V);S("design-lint-result",e),S("rescan-complete",{totalErrors:e.summary.totalErrors,nodesWithErrors:e.summary.nodesWithErrors})}async function Cl(){try{S("flow-analysis-started",{status:"building-graph"});let e=co();if(e.frames.length===0){S("flow-analysis-error",{error:"No top-level frames found on current page."});return}if(e.frames.length>50){S("flow-analysis-error",{error:`Too many frames (${e.frames.length}). Select a page with \u226450 frames for flow analysis.`});return}let t=lo(e);S("flow-analysis-started",{status:"capturing-screenshots",total:e.frames.length});let n={},s={},o=10;for(let c=0;c{let f=await figma.getNodeByIdAsync(u.id);if(!(!f||!("exportAsync"in f))){try{let m=await et(f);n[u.id]=m}catch(m){}try{let{runDesignLint:m}=await Promise.resolve().then(()=>(Ne(),Wt)),g=m([f],V);s[u.id]=g}catch(m){}}});await Promise.all(d),S("flow-analysis-started",{status:"capturing-screenshots",progress:Math.min(c+o,e.frames.length),total:e.frames.length})}let i=[];for(let c of e.frames){let l=await figma.getNodeByIdAsync(c.id);l&&i.push({frame:c,node:l})}let r=po(i,{skipLocked:V.skipLockedLayers,skipHidden:V.skipHiddenLayers}),a=[...t,...r];S("flow-analysis-result",{graph:e,graphIssues:a,screenshots:n,lintResults:s})}catch(e){let t=e instanceof Error?e.message:"Unknown error";S("flow-analysis-error",{error:t})}}async function xl(){try{let t=figma.currentPage.children.filter(d=>d.type==="FRAME"||d.type==="COMPONENT_SET");if(t.length===0){S("analysis-error",{error:"No top-level frames found on current page."});return}let n=t.slice(0,50),s=n.length,{runDesignLint:o}=await Promise.resolve().then(()=>(Ne(),Wt)),i=[],r=5;for(let d=0;d{let h=d+p+1;S("page-sweep-progress",{current:h,total:s,frameName:g.name});let C={summary:{totalErrors:0,byType:{},totalNodes:0,nodesWithErrors:0},errors:[]};try{C=o([g],V)}catch(N){}let k="";try{k=await et(g,800)}catch(N){}return{id:g.id,name:g.name,screenshot:k,lintResult:{summary:C.summary,errors:C.errors},width:Math.round(g.width),height:Math.round(g.height)}}),m=await Promise.all(f);i.push(...m)}let a=0,c={};for(let d of i){a+=d.lintResult.summary.totalErrors||0;for(let u of d.lintResult.errors){let f=u.errorType;c[f]||(c[f]={count:0,severity:u.severity||"warning"}),c[f].count++}}let l=Object.entries(c).sort((d,u)=>u[1].count-d[1].count).slice(0,10).map(([d,{count:u,severity:f}])=>({type:d,count:u,severity:f}));S("page-sweep-result",{frames:i,aggregated:{totalFrames:s,totalIssues:a,topIssues:l}})}catch(e){let t=e instanceof Error?e.message:"Unknown error";S("analysis-error",{error:`Page sweep failed: ${t}`})}}async function gi(){var e,t;try{let n=await Et();_=n.providerId,W=n.modelId,n.apiKey?(ee=n.apiKey,S("api-key-status",{hasKey:!0,provider:_,model:W})):S("api-key-status",{hasKey:!1,provider:_,model:W}),console.log(`Plugin initialized with provider: ${_}, model: ${W}`);try{let s=figma.root.getPluginData("ignoredState");if(s){let o=JSON.parse(s);zt(o),console.log(`Restored ${((e=o.nodeIds)==null?void 0:e.length)||0} ignored nodes, ${((t=o.errorKeys)==null?void 0:t.length)||0} ignored errors`)}}catch(s){}console.log("\u{1F504} Initializing design systems knowledge..."),Z.loadDesignSystemsKnowledge().then(()=>{console.log("\u2705 Design systems knowledge loaded successfully")}).catch(s=>{console.warn("\u26A0\uFE0F Failed to load design systems knowledge, using fallback:",s)}),console.log("Plugin initialized successfully")}catch(n){console.error("Error initializing plugin:",n)}}function yi(){let e=figma.currentPage.selection;if(e.length!==0)try{let t=e[0],n=se([t],V),s=n.summary.totalNodes||1,o={critical:10,warning:3,info:1},i=n.errors.reduce((d,u)=>d+(o[u.severity||"warning"]||3),0),r=Math.max(0,s-n.errors.length)*10,a=r+i,c=a>0?Math.round(r/a*100):100,l=n.errors.some(d=>d.severity==="critical")?"critical":n.errors.some(d=>d.severity==="warning")?"warning":n.errors.length>0?"info":"none";S("selection-mini-score",{nodeId:t.id,nodeName:t.name,score:c,issueCount:n.summary.totalErrors,topSeverity:l})}catch(t){}}function Al(e){let t={version:1,timestamp:Date.now(),nodeId:e.nodeId,nodeName:e.nodeName,overall:e.overall,grade:e.grade,categories:e.categories,summary:e.summary,errors:e.errors.map(n=>({errorType:n.errorType,severity:n.severity||"warning",nodeId:n.nodeId,message:n.message}))};ho(t),S("baseline-saved",{nodeId:e.nodeId,nodeName:e.nodeName,timestamp:t.timestamp,overall:e.overall})}function wl(e){let t=vo(e.nodeId);S("baseline-loaded",t)}function El(e){let t=bo(e.nodeId);if(!t){S("diff-result",null);return}let n=e.errors.map(o=>({errorType:o.errorType,severity:o.severity||"warning",nodeId:o.nodeId,message:o.message})),s=No(t,{overall:e.overall,grade:e.grade,categories:e.categories,errors:n,summary:e.summary});S("diff-result",s)}function Tl(e){So(e.nodeId)}async function Ll(e){try{let t=await figma.getNodeByIdAsync(e.nodeId);if(!t||!("type"in t)){S("fix-preview",{success:!1,error:"Node not found or is not a valid scene node"});return}let n=t,s=null;if(e.type==="token"){if(!e.propertyPath){S("fix-preview",{success:!1,error:"Property path is required for token fixes"});return}if(e.propertyPath.match(/^(fills|strokes)\[(\d+)\]$/)){let i=await Qt(e.suggestedValue||"",.1);i.length>0&&(s=await nn(n,e.propertyPath,i[0].variableId))}else{let i=parseFloat(e.suggestedValue||"0"),r=await Zt(i,e.propertyPath||"",2);r.length>0&&(s=await nn(n,e.propertyPath,r[0].variableId))}if(s){let i=s;S("fix-preview",{success:!0,type:"token",nodeId:i.nodeId,nodeName:i.nodeName,propertyPath:i.propertyPath,beforeValue:i.beforeValue,afterValue:i.afterValue,tokenId:i.tokenId,tokenName:i.tokenName})}else S("fix-preview",{success:!1,error:"No matching token found for this value"})}else if(e.type==="naming"){let o=e.suggestedValue||be(n);s=$n(n,o),S("fix-preview",{success:!0,preview:s})}else S("fix-preview",{success:!1,error:`Unknown fix type: ${e.type}`})}catch(t){console.error("Error previewing fix:",t);let n=t instanceof Error?t.message:"Unknown error occurred";S("fix-preview",{success:!1,error:n})}}async function Pl(e){try{let t=await figma.getNodeByIdAsync(e.nodeId);if(!t||!("type"in t)){S("fix-applied",{success:!1,error:"Node not found or is not a valid scene node"}),figma.notify("Failed to apply fix: Node not found",{error:!0});return}let n=t;if(!e.propertyPath){S("fix-applied",{success:!1,error:"Property path is required for token fixes"}),figma.notify("Failed to apply fix: Property path missing",{error:!0});return}if(!e.tokenId){S("fix-applied",{success:!1,error:"Token ID is required for token fixes"}),figma.notify("Failed to apply fix: Token ID missing",{error:!0});return}let s;/^(fills|strokes)\[\d+\]$/.test(e.propertyPath)?s=await en(n,e.propertyPath,e.tokenId):s=await tn(n,e.propertyPath,e.tokenId),S("fix-applied",j(R({},s),{fixType:"token",nodeId:e.nodeId,propertyPath:e.propertyPath})),s.success?figma.notify(`Applied token to ${n.name}`,{timeout:2e3}):figma.notify(`Failed to apply token: ${s.error||s.message}`,{error:!0})}catch(t){console.error("Error applying token fix:",t);let n=t instanceof Error?t.message:"Unknown error occurred";S("fix-applied",{success:!1,error:n,fixType:"token",nodeId:e.nodeId}),figma.notify(`Failed to apply fix: ${n}`,{error:!0})}}async function Rl(e){try{let t=await figma.getNodeByIdAsync(e.nodeId);if(!t||!("type"in t)){S("fix-applied",{success:!1,error:"Node not found or is not a valid scene node"}),figma.notify("Failed to rename: Node not found",{error:!0});return}let n=t,s=e.newValue||be(n),o=n.name;if(o===s){S("fix-applied",{success:!0,fixType:"naming",nodeId:e.nodeId,message:`Layer already named "${s}"`,oldName:o,newName:s}),figma.notify(`Layer already named "${s}"`,{timeout:2e3});return}let i=Lt(n,s),r={success:i,fixType:"naming",nodeId:e.nodeId,message:i?`Renamed "${o}" to "${s}"`:"Failed to rename layer",oldName:o,newName:i?s:o};S("fix-applied",r),i?figma.notify(`Renamed "${o}" to "${s}"`,{timeout:2e3}):figma.notify("Failed to rename layer",{error:!0})}catch(t){console.error("Error applying naming fix:",t);let n=t instanceof Error?t.message:"Unknown error occurred";S("fix-applied",{success:!1,error:n}),figma.notify(`Failed to rename: ${n}`,{error:!0})}}async function $l(e){try{let t=[],n=0,s=0;for(let i of e.fixes)try{let r=await figma.getNodeByIdAsync(i.nodeId);if(!r||!("type"in r)){t.push({nodeId:i.nodeId,success:!1,message:"Node not found",error:"Node not found or is not a valid scene node"}),s++;continue}let a=r;if(i.type==="token"){if(!i.propertyPath){t.push({nodeId:i.nodeId,success:!1,message:"Missing property path",error:"Token fixes require a propertyPath"}),s++;continue}let c=i.tokenId,l=/^(fills|strokes)\[\d+\]$/.test(i.propertyPath);if(!c&&i.newValue)try{if(l){let u=await Qt(i.newValue,.1);u.length>0&&(c=u[0].variableId)}else{let u=parseFloat(i.newValue);if(!isNaN(u)){let f=await Zt(u,i.propertyPath||"",2);f.length>0&&(c=f[0].variableId)}}}catch(u){console.warn("Could not find matching variable:",u)}if(!c){t.push({nodeId:i.nodeId,success:!1,message:"No matching design token found for this value",error:"Could not find a matching variable to bind"}),s++;continue}let d;l?d=await en(a,i.propertyPath,c):d=await tn(a,i.propertyPath,c),t.push({nodeId:i.nodeId,success:d.success,message:d.message,error:d.error}),d.success?n++:s++}else if(i.type==="naming"){let c=i.newValue||be(a),l=a.name,d=Lt(a,c);t.push({nodeId:i.nodeId,success:d,message:d?`Renamed "${l}" to "${c}"`:"Failed to rename layer"}),d?n++:s++}else t.push({nodeId:i.nodeId,success:!1,message:`Unknown fix type: ${i.type}`,error:`Unsupported fix type: ${i.type}`}),s++}catch(r){let a=r instanceof Error?r.message:"Unknown error";t.push({nodeId:i.nodeId,success:!1,message:"Error applying fix",error:a}),s++}let o={total:e.fixes.length,success:n,errors:s,results:t};S("batch-fix-applied",o),s===0?figma.notify(`Applied ${n} fix${n!==1?"es":""} successfully`,{timeout:2e3}):n>0?figma.notify(`Applied ${n} fix${n!==1?"es":""}, ${s} failed`,{timeout:3e3}):figma.notify(`Failed to apply ${s} fix${s!==1?"es":""}`,{error:!0})}catch(t){console.error("Error applying batch fixes:",t);let n=t instanceof Error?t.message:"Unknown error occurred";S("batch-fix-applied",{total:e.fixes.length,success:0,errors:e.fixes.length,error:n}),figma.notify(`Batch fix failed: ${n}`,{error:!0})}}async function Ml(e){try{let t=await figma.getNodeByIdAsync(e.nodeId);if(!t){S("description-updated",{success:!1,error:"Node not found"}),figma.notify("Failed to update description: Node not found",{error:!0});return}if(t.type!=="COMPONENT"&&t.type!=="COMPONENT_SET"){S("description-updated",{success:!1,error:"Node is not a component or component set"}),figma.notify("Description can only be set on components",{error:!0});return}let n=t,s=n.description;n.description=e.description,S("description-updated",{success:!0,oldDescription:s,newDescription:e.description}),figma.notify("Component description updated",{timeout:2e3})}catch(t){console.error("Error updating description:",t);let n=t instanceof Error?t.message:"Unknown error occurred";S("description-updated",{success:!1,error:n}),figma.notify(`Failed to update description: ${n}`,{error:!0})}}async function Ol(e){try{let{nodeId:t,propertyName:n,propertyType:s,defaultValue:o}=e,i=await figma.getNodeByIdAsync(t);if(!i){S("property-added",{success:!1,propertyName:n,message:"Node not found"}),figma.notify("Node not found",{error:!0});return}let r=null;if(i.type==="COMPONENT"){let d=i;d.parent&&d.parent.type==="COMPONENT_SET"?r=d.parent:r=d}else if(i.type==="COMPONENT_SET")r=i;else if(i.type==="INSTANCE"){let d=await i.getMainComponentAsync();d&&(d.parent&&d.parent.type==="COMPONENT_SET"?r=d.parent:r=d)}if(!r){S("property-added",{success:!1,propertyName:n,message:"Selected node is not a component"}),figma.notify("Selected node is not a component",{error:!0});return}let a=r.componentPropertyDefinitions;for(let d of Object.keys(a))if(d.replace(/#\d+:\d+$/,"").toLowerCase()===n.toLowerCase()){S("property-added",{success:!1,propertyName:n,message:`Property "${n}" already exists`}),figma.notify(`Property "${n}" already exists`,{error:!0});return}let c;switch(s.toLowerCase()){case"boolean":c="BOOLEAN";break;case"text":c="TEXT";break;case"slot":c="INSTANCE_SWAP";break;case"variant":r.type==="COMPONENT_SET"?c="VARIANT":c="TEXT";break;default:c="TEXT"}r.addComponentProperty(n,c,o);let l="";if(c==="VARIANT"&&r.type==="COMPONENT_SET"&&e.variantOptions&&e.variantOptions.length>1){let d=r,u=[...d.children],f=e.variantOptions.slice(1),m=`${n}=${o}`,g=figma.currentPage,p=d;for(;p.parent&&p.parent.type!=="PAGE";)p=p.parent;let h=p.absoluteTransform[0][2],C=p.absoluteTransform[1][2],k=h,N=C+p.height+50,y=figma.createSection();y.name=`FigmaLint: ${n} Variants`,g.appendChild(y),y.x=k,y.y=N;let b=figma.createText();await figma.loadFontAsync({family:"Inter",style:"Medium"}),b.fontName={family:"Inter",style:"Medium"},b.characters=`New "${n}" variants \u2014 drag into the ComponentSet`,b.fontSize=14,b.fills=[{type:"SOLID",color:{r:.4,g:.4,b:.4}}],y.appendChild(b),b.x=24,b.y=24;let x=24,I=32,L=b.y+b.height+24,O=b.width+x*2;for(let M of f){let z=`${n}=${M}`,A=figma.createText();await figma.loadFontAsync({family:"Inter",style:"Semi Bold"}),A.fontName={family:"Inter",style:"Semi Bold"},A.characters=`${n}=${M}`,A.fontSize=12,A.fills=[{type:"SOLID",color:{r:.6,g:.3,b:.9}}],y.appendChild(A),A.x=x,A.y=L,L+=A.height+12;let $=x,v=0;for(let T of u){let P=T.clone();P.name=P.name.replace(m,z),y.appendChild(P),P.x=$,P.y=L,$+=P.width+I,v=Math.max(v,P.height)}O=Math.max(O,$-I+x),L+=v+I}y.resizeWithoutConstraints(Math.max(O,400),L+x),l=" \u2014 new variants created in staging section to the right"}S("property-added",{success:!0,propertyName:n,message:`Property "${n}" added successfully${l}`}),figma.notify(`Property "${n}" added${l?" (see staging section)":""}`,{timeout:3e3})}catch(t){console.error("Error adding component property:",t);let n=t instanceof Error?t.message:"Unknown error occurred";S("property-added",{success:!1,propertyName:e.propertyName,message:n}),figma.notify(`Failed to add property: ${n}`,{error:!0})}}async function Fl(){try{let e=await fn();S("variable-system-result",e)}catch(e){console.error("Error collecting variables:",e);let t=e instanceof Error?e.message:"Unknown error";S("variable-system-error",{error:t})}}async function Vl(e){try{let t=await fn(),n=e!=null&&e.dtcgJson?Ao(e.dtcgJson):[],s=Eo(t,n,null);S("dtcg-compliance-result",s)}catch(t){console.error("Error checking DTCG compliance:",t);let n=t instanceof Error?t.message:"Unknown error";S("dtcg-compliance-error",{error:n})}}async function Dl(e){try{let t=e==null?void 0:e.collectionId;if(!t){let i=(await figma.variables.getLocalVariableCollectionsAsync()).find(r=>r.modes.length>=2);if(!i){S("mode-comparison-error",{error:"No collections with multiple modes found. Create light/dark modes first."});return}t=i.id}let n=await To(t);S("mode-comparison-result",n);let s=_l(n);S("dark-mode-card-result",s)}catch(t){console.error("Error comparing modes:",t);let n=t instanceof Error?t.message:"Unknown error";S("mode-comparison-error",{error:n})}}function _l(e){let t=[],n=0,s=0,o=0;for(let c of e.variableDiffs)if(c.type==="COLOR")for(let[l,d]of Object.entries(c.values)){let u=String(d).toLowerCase();/dark/i.test(l)&&((u==="#000000"||u==="rgb(0, 0, 0)")&&(n++,/bg|background|surface/i.test(c.variableName)&&t.push({type:"pure-black",severity:"warning",nodeName:c.variableName,message:`Pure black (#000000) used for background in ${l}`,currentValue:u,suggestions:["Use #121212 or #1a1a1a for softer dark backgrounds"]})),(u==="#ffffff"||u==="rgb(255, 255, 255)")&&/fg|foreground|text|on/i.test(c.variableName)&&(s++,t.push({type:"pure-white",severity:"info",nodeName:c.variableName,message:`Pure white (#ffffff) text in ${l} \u2014 can cause eye strain`,currentValue:u,suggestions:["Use #e0e0e0 or #f0f0f0 for softer text on dark backgrounds"]})))}let i=e.missingValues.length;for(let c of e.missingValues)t.push({type:"missing-mode",severity:"critical",nodeName:c.variableName,message:`Missing values for modes: ${c.missingModes.join(", ")}`,suggestions:[`Add values for: ${c.missingModes.join(", ")}`]});let r=e.variableDiffs.length+e.missingValues.length,a=t.length;return{issues:t,metrics:{pureBlackBackgrounds:n,pureWhiteText:s,lowContrastOnDark:o,missingModeValues:i},summary:{totalChecked:r,passed:Math.max(0,r-a),failed:a}}}function Bl(e){let t=e.settings||K;Po({enabled:!0,debounceMs:e.debounceMs||500,settings:t})}function Gl(){Ro()}function Ul(e){let t=Mo(e.lintResult,e.tokenSummary||null),n={overall:t.overall,components:{orphanedStyles:t.components.orphanedStyles.count,detachedInstances:t.components.detachedInstances.count,hardcodedValues:t.components.hardcodedValues.count,namingViolations:t.components.namingViolations.count,missingAutoLayout:t.components.missingAutoLayout.count,inconsistentSpacing:t.components.inconsistentSpacing.count}};t.trend&&(n.trend={direction:t.trend.direction==="degrading"?"declining":t.trend.direction,delta:Math.abs(t.trend.delta)}),S("design-debt-result",n)}async function zl(){try{let e=figma.currentPage.selection;if(e.length===0){S("extended-lint-error",{error:"No nodes selected. Select a frame or component to run extended lint."});return}let t=e,n=Vo(t),s=Bo(t),o=Ho(t),i=qo(t),[r,a,c,l]=await Promise.allSettled([Yo(t),ti(t),oi(t),ci(t)]),d={issues:[],summary:{}},u=r.status==="fulfilled"?r.value:d,f=a.status==="fulfilled"?a.value:d,m=c.status==="fulfilled"?c.value:d,g=l.status==="fulfilled"?l.value:d;S("extended-lint-result",{layoutSizing:n,constraints:s,typography:o,componentProps:i,styleAudit:u,variableScope:f,multiTheme:m,gridCheck:g})}catch(e){console.error("Error running extended lint:",e);let t=e instanceof Error?e.message:"Unknown error";S("extended-lint-error",{error:t})}}var Hl={width:380,height:600,themeColors:!0};try{figma.showUI(__html__,Hl),console.log("\u2705 FigmaLint v2.0 - UI shown successfully")}catch(e){console.log("\u2139\uFE0F UI might already be shown in inspect panel:",e)}figma.ui.onmessage=fi;figma.on("selectionchange",()=>{let e=figma.currentPage.selection;figma.ui.postMessage({type:"selection-changed",data:{hasSelection:e.length>0,nodeId:e.length>0?e[0].id:null,nodeName:e.length>0?e[0].name:null}}),e.length>0&&yi()});gi();console.log("\u{1F680} FigmaLint v2.0 initialized with modular architecture");})(); diff --git a/dist/ui.html b/dist/ui.html index 16dd432..c4c1305 100644 --- a/dist/ui.html +++ b/dist/ui.html @@ -4,7 +4,7 @@ Design Review Chat - +Layer: **${B.nodeName}** (${B.nodeType})`});const L=[];B.errorType==="spacing"&&B.property?L.push({id:`fix-${B.nodeId}`,label:"Fix to nearest",variant:"primary",action:"fix-single-spacing",params:{nodeId:B.nodeId,property:B.property}}):B.errorType==="radius"&&L.push({id:`fix-radius-${B.nodeId}`,label:"Fix radius to nearest",variant:"primary",action:"fix-single-radius",params:{nodeId:B.nodeId}}),L.push({id:`skip-${S}`,label:S+1{c.addMessage({kind:"ai-text",content:"Full report copied to clipboard!"})},()=>{c.addMessage({kind:"ai-text",content:"Failed to copy report to clipboard."})})}break}case"export-json":{const d=c.lintResult;if(d){const S={component:p||"Component",timestamp:new Date().toISOString(),lint:{summary:d.summary,errors:d.errors,issuesFixed:c.issuesFixed},aiReview:c.aiReview||void 0,diff:c.lastDiff||void 0};navigator.clipboard.writeText(JSON.stringify(S,null,2)).then(()=>c.addMessage({kind:"ai-text",content:"JSON report copied to clipboard!"}),()=>c.addMessage({kind:"ai-text",content:"Failed to copy JSON to clipboard."}))}break}case"save-baseline":{if(!c.score||!c.lintResult){c.addMessage({kind:"ai-text",content:"Run an analysis first before saving a baseline."});break}const d=se.current;if(!d)break;f("save-baseline",{nodeId:d,nodeName:p||"Component",overall:c.score.overall,grade:c.score.grade,categories:{tokens:c.score.tokens,spacing:c.score.spacing,layout:c.score.layout,accessibility:c.score.accessibility,naming:c.score.naming,visualQuality:c.score.visualQuality,microcopy:c.score.microcopy,conversion:c.score.conversion,cognitive:c.score.cognitive},errors:c.lintResult.errors.map(S=>({errorType:S.errorType,severity:S.severity,nodeId:S.nodeId,message:S.message})),summary:c.lintResult.summary});break}case"compare-baseline":{if(!c.score||!c.lintResult){c.addMessage({kind:"ai-text",content:"Run an analysis first before comparing."});break}const d=se.current;if(!d)break;f("compare-baseline",{nodeId:d,overall:c.score.overall,grade:c.score.grade,categories:{tokens:c.score.tokens,spacing:c.score.spacing,layout:c.score.layout,accessibility:c.score.accessibility,naming:c.score.naming,visualQuality:c.score.visualQuality,microcopy:c.score.microcopy,conversion:c.score.conversion,cognitive:c.score.cognitive},errors:c.lintResult.errors.map(S=>({errorType:S.errorType,severity:S.severity,nodeId:S.nodeId,message:S.message})),summary:c.lintResult.summary});break}case"analyze-flow":{c.addMessage({kind:"ai-text",content:"Starting flow analysis on current page..."}),f("analyze-flow");break}case"analyze-page":{c.addMessage({kind:"ai-text",content:"Starting whole-page sweep..."}),f("analyze-page");break}case"toggle-mode":{const d=A==="quick"?"deep":"quick";y(d),c.addMessage({kind:"ai-text",content:`Analysis mode: **${d}**. ${d==="deep"?"Refero comparison will be included in the initial response.":"Refero data loads in the background."}`});break}case"design-debt":{if(!c.lintResult){c.addMessage({kind:"ai-text",content:"Run an analysis first to calculate design debt."});break}c.addMessage({kind:"ai-text",content:"Calculating design debt..."}),f("calculate-design-debt",{lintResult:{errors:c.lintResult.errors,summary:c.lintResult.summary}});break}case"dark-mode":{c.addMessage({kind:"ai-text",content:"Comparing variable modes..."}),f("compare-modes");break}case"token-audit":{c.addMessage({kind:"ai-text",content:"Running token audit (DTCG compliance + variable collection)..."}),f("check-dtcg-compliance"),f("collect-variables");break}}},[c,f,p,A]),ee=H.useCallback(q=>{f("jump-to-node",{nodeId:q})},[f]);return i.jsxs("div",{className:"h-full flex flex-col relative",children:[z&&i.jsx(Yx,{hasApiKey:h,analysisMode:A,backendAvailable:b,onSaveApiKey:(q,w)=>f("save-api-key",{apiKey:q,provider:w}),onClearApiKey:()=>{f("clear-api-key"),g(!1)},onToggleMode:()=>{y(A==="quick"?"deep":"quick")},onClose:()=>O(!1)}),Q&&i.jsx($x,{initialConfig:ce,onClose:()=>F(!1)}),c.messages.length===0&&!c.isAnalyzing&&i.jsxs("div",{className:"flex items-center gap-2 px-3 py-2 border-b border-border",children:[i.jsx("button",{className:"flex-1 py-2 bg-bg-brand text-fg-onbrand text-12 font-medium rounded-md hover:opacity-90 transition-opacity",onClick:We,children:"Analyze Selection"}),i.jsx("button",{className:"flex-1 py-2 bg-bg-secondary text-fg text-12 font-medium rounded-md hover:bg-bg-hover transition-colors border border-border",onClick:()=>G("analyze-flow"),children:"Analyze Flow"}),i.jsx("button",{className:"flex-1 py-2 bg-bg-secondary text-fg text-12 font-medium rounded-md hover:bg-bg-hover transition-colors border border-border",onClick:()=>G("analyze-page"),title:"Sweep all top-level frames on the page",children:"Sweep Page"}),i.jsx("button",{onClick:()=>F(!0),className:"shrink-0 w-8 h-8 flex items-center justify-center text-fg-tertiary hover:text-fg rounded-md hover:bg-bg-hover transition-colors",title:"Team Config",children:i.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2","aria-hidden":"true",children:[i.jsx("path",{d:"M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"}),i.jsx("circle",{cx:"9",cy:"7",r:"4"}),i.jsx("path",{d:"M23 21v-2a4 4 0 0 0-3-3.87"}),i.jsx("path",{d:"M16 3.13a4 4 0 0 1 0 7.75"})]})}),i.jsx("button",{onClick:()=>O(!0),className:"shrink-0 w-8 h-8 flex items-center justify-center text-fg-tertiary hover:text-fg rounded-md hover:bg-bg-hover transition-colors",title:"Settings",children:i.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2","aria-hidden":"true",children:[i.jsx("circle",{cx:"12",cy:"12",r:"3"}),i.jsx("path",{d:"M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"})]})})]}),$&&i.jsxs("div",{className:"flex items-center gap-2 px-3 py-1.5 bg-bg-warning text-fg-warning text-11 border-b border-border",children:[i.jsxs("span",{className:"flex-1",children:["Selection changed",Y?` to "${Y}"`:"",". Results may be stale."]}),i.jsx("button",{className:"shrink-0 px-2 py-0.5 bg-bg-brand text-fg-onbrand text-11 font-medium rounded hover:opacity-90",onClick:We,children:"Re-analyze"})]}),i.jsx(qx,{state:c,componentName:p,analysisMode:A,miniScore:Z,onAnalyze:We,onSendMessage:D,onAction:G,onJumpToNode:ee,onOpenSettings:()=>O(!0)})]})}function gy(c,f,p,o,h){const g=[`# Design Review Report: ${f||"Component"}`,"",`Total lint issues: ${c.summary.totalErrors} across ${c.summary.nodesWithErrors} layers`,...p?[`Fixed: ${p}`]:[],""];g.push("## Lint Issues","");const b=c.summary.byType;if(b.fill>0&&g.push(`- **Fill styles:** ${b.fill} missing`),b.stroke>0&&g.push(`- **Stroke styles:** ${b.stroke} missing`),b.effect>0&&g.push(`- **Effect styles:** ${b.effect} missing`),b.text>0&&g.push(`- **Text styles:** ${b.text} missing`),b.radius>0&&g.push(`- **Border radius:** ${b.radius} non-standard`),b.spacing>0&&g.push(`- **Spacing:** ${b.spacing} off-grid`),b.autoLayout>0&&g.push(`- **Auto Layout:** ${b.autoLayout} missing`),b.visualQuality>0&&g.push(`- **Visual Quality:** ${b.visualQuality} issues`),b.microcopy>0&&g.push(`- **Microcopy:** ${b.microcopy} issues`),o){g.push("","## AI Design Review",""),g.push("| Category | Rating |"),g.push("|----------|--------|"),g.push(`| Visual Hierarchy | ${o.visualHierarchy.rating.toUpperCase()} |`),g.push(`| States Coverage | ${o.statesCoverage.rating.toUpperCase()} |`),g.push(`| Platform Alignment | ${o.platformAlignment.rating.toUpperCase()} (${o.platformAlignment.detectedPlatform}) |`),g.push(`| Color Harmony | ${o.colorHarmony.rating.toUpperCase()} |`),o.visualBalance&&g.push(`| Visual Balance | ${o.visualBalance.rating.toUpperCase()} |`),o.microcopyQuality&&g.push(`| Microcopy Quality | ${o.microcopyQuality.rating.toUpperCase()} |`),o.cognitiveLoad&&g.push(`| Cognitive Load | ${o.cognitiveLoad.rating.toUpperCase()} |`);const E=o.statesCoverage?.missingStates||[];if(E.length>0&&g.push("",`**Missing states:** ${E.join(", ")}`),o.recommendations.length>0){g.push("","### Recommendations","");for(const A of o.recommendations)g.push(`- **[${A.severity.toUpperCase()}]** ${A.title}: ${A.description}`)}o.summary&&g.push("",`> ${o.summary}`)}if(h){g.push("","## Baseline Comparison","");const E=h.scoreDelta.overall,A=E>0?"+":"";g.push(`Score: ${h.scoreDelta.oldOverall} → ${h.scoreDelta.newOverall} (${A}${E})`),g.push(`Baseline from: ${new Date(h.baselineTimestamp).toLocaleString()}`),g.push(""),h.summary.totalFixed>0&&g.push(`- **Fixed:** ${h.summary.totalFixed} issues`),h.summary.totalNew>0&&g.push(`- **New:** ${h.summary.totalNew} issues`),g.push(`- **Remaining:** ${h.summary.totalRemaining} issues`);const y=h.scoreDelta.categories.filter(z=>z.delta!==0);if(y.length>0){g.push("","| Category | Before | After | Delta |"),g.push("|----------|--------|-------|-------|");for(const z of y){const O=z.delta>0?`+${z.delta}`:`${z.delta}`;g.push(`| ${z.category} | ${z.oldScore} | ${z.newScore} | ${O} |`)}}}if(c.errors.length>0){g.push("","## All Issues","");for(const E of c.errors)g.push(`- **[${E.errorType.toUpperCase()}]** ${E.nodeName}: ${E.message}`)}return g.join(` +`)}function ag(c){const f={critical:10,warning:3,info:1},p=c.frames.map(y=>{const z=y.lintResult.errors,O=Math.max(y.lintResult.summary.totalNodes,1),Q=z.reduce((Y,U)=>Y+(f[U.severity||"warning"]||3),0),F=Math.max(0,O-z.length)*10,ce=F+Q,te=ce>0?Math.round(F/ce*100):100,$={};for(const Y of z)$[Y.errorType]=($[Y.errorType]||0)+1;const k=Object.entries($).sort((Y,U)=>U[1]-Y[1]).slice(0,3).map(([Y,U])=>`${Y} (${U})`);return{id:y.id,name:y.name,score:te,issueCount:y.lintResult.summary.totalErrors,topIssues:k}}),o=p.map(y=>y.score),h=o.length>0?Math.round(o.reduce((y,z)=>y+z,0)/o.length):100,g=h,b=o.length>0?o.reduce((y,z)=>y+Math.pow(z-g,2),0)/o.length:0,E=Math.max(0,Math.round(100-Math.sqrt(b))),A=h>=90?"excellent":h>=70?"needs-work":"poor";return{fileHealth:{overallScore:h,grade:A,totalFrames:c.aggregated.totalFrames,totalIssues:c.aggregated.totalIssues,topIssues:c.aggregated.topIssues,consistencyScore:E},frames:p,aiInsights:{strengths:[],weaknesses:[],recommendations:[],summary:"AI analysis unavailable. Scores are based on deterministic lint rules only."}}}qh.createRoot(document.getElementById("root")).render(i.jsx(Rh.StrictMode,{children:i.jsx(dy,{})})); diff --git a/src/types.ts b/src/types.ts index b6330e0..e38f54d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -235,7 +235,9 @@ export type UIMessageType = | 'enable-realtime-lint' | 'disable-realtime-lint' // Design debt - | 'calculate-design-debt'; + | 'calculate-design-debt' + // Extended lint checks (8 new modules) + | 'run-extended-lint'; // Auto-fix Types export interface FixRequest { diff --git a/src/ui/message-handler.ts b/src/ui/message-handler.ts index 074ffeb..a741e73 100644 --- a/src/ui/message-handler.ts +++ b/src/ui/message-handler.ts @@ -47,6 +47,14 @@ import { checkTokenCompliance } from '../baseline/token-compliance'; import { compareModes } from '../extract/mode-comparator'; import { enableRealtimeLint, disableRealtimeLint } from '../lint/realtime-lint'; import { calculateDesignDebt } from '../baseline/design-debt'; +import { checkLayoutSizing } from '../lint/layout-sizing'; +import { checkConstraints } from '../lint/constraints'; +import { checkTypography } from '../lint/typography'; +import { checkComponentProps } from '../lint/component-props'; +import { checkStyleAudit } from '../lint/style-audit'; +import { checkVariableScope } from '../lint/variable-scope'; +import { checkMultiTheme } from '../lint/multi-theme'; +import { checkGrid } from '../lint/grid-check'; import { lintSelection, runDesignLint, @@ -254,6 +262,10 @@ export async function handleUIMessage(msg: PluginMessage): Promise { case 'calculate-design-debt': handleCalculateDesignDebt(data); break; + // Extended lint checks (8 new modules) + case 'run-extended-lint': + await handleRunExtendedLint(); + break; default: console.warn('Unknown message type:', type); } @@ -2491,14 +2503,14 @@ async function handleCollectVariables(): Promise { } } -async function handleCheckDTCGCompliance(data: { dtcgJson: string }): Promise { +async function handleCheckDTCGCompliance(data: { dtcgJson?: string }): Promise { try { - // Parse the DTCG JSON - const dtcgTokens = parseDTCG(data.dtcgJson); - // Collect the current variable system const variableReport = await collectVariableSystem(); + // Parse DTCG JSON if provided, otherwise run self-compliance (no external token file) + const dtcgTokens = data?.dtcgJson ? parseDTCG(data.dtcgJson) : []; + // Run compliance check const result = checkTokenCompliance(variableReport, dtcgTokens, null); @@ -2514,10 +2526,27 @@ async function handleCheckDTCGCompliance(data: { dtcgJson: string }): Promise { +async function handleCompareModes(data: { collectionId?: string }): Promise { try { - const modeData = await compareModes(data.collectionId); + let collectionId = data?.collectionId; + // Auto-detect first multi-mode collection when no ID provided + if (!collectionId) { + const collections = await figma.variables.getLocalVariableCollectionsAsync(); + const multiMode = collections.find(c => c.modes.length >= 2); + if (!multiMode) { + sendMessageToUI('mode-comparison-error', { error: 'No collections with multiple modes found. Create light/dark modes first.' }); + return; + } + collectionId = multiMode.id; + } + const modeData = await compareModes(collectionId); + + // Send raw mode comparison data (existing contract) sendMessageToUI('mode-comparison-result', modeData); + + // Also send a DarkModeCard-compatible transform so the UI can render the card + const darkModeCardData = transformModeToDarkModeCard(modeData); + sendMessageToUI('dark-mode-card-result', darkModeCardData); } catch (error) { console.error('Error comparing modes:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; @@ -2525,6 +2554,98 @@ async function handleCompareModes(data: { collectionId: string }): Promise } } +/** + * Transform ModeComparisonData into the shape expected by DarkModeCard: + * { issues[], metrics, summary } + */ +function transformModeToDarkModeCard(modeData: { + collection: string; + modes: Array<{ modeId: string; modeName: string }>; + variableDiffs: Array<{ variableName: string; type: string; values: Record }>; + missingValues: Array<{ variableName: string; missingModes: string[] }>; +}): { + issues: Array<{ type: string; severity: string; nodeName: string; message: string; currentValue?: string; suggestions?: string[] }>; + metrics: { pureBlackBackgrounds: number; pureWhiteText: number; lowContrastOnDark: number; missingModeValues: number }; + summary: { totalChecked: number; passed: number; failed: number }; +} { + const issues: Array<{ type: string; severity: string; nodeName: string; message: string; currentValue?: string; suggestions?: string[] }> = []; + let pureBlackBackgrounds = 0; + let pureWhiteText = 0; + let lowContrastOnDark = 0; + + // Analyse variable diffs for dark-mode-specific issues + for (const diff of modeData.variableDiffs) { + if (diff.type !== 'COLOR') continue; + + for (const [modeName, value] of Object.entries(diff.values)) { + const strVal = String(value).toLowerCase(); + const isDarkMode = /dark/i.test(modeName); + + if (isDarkMode) { + // Pure black background check + if (strVal === '#000000' || strVal === 'rgb(0, 0, 0)') { + pureBlackBackgrounds++; + if (/bg|background|surface/i.test(diff.variableName)) { + issues.push({ + type: 'pure-black', + severity: 'warning', + nodeName: diff.variableName, + message: `Pure black (#000000) used for background in ${modeName}`, + currentValue: strVal, + suggestions: ['Use #121212 or #1a1a1a for softer dark backgrounds'], + }); + } + } + + // Pure white text check + if (strVal === '#ffffff' || strVal === 'rgb(255, 255, 255)') { + if (/fg|foreground|text|on/i.test(diff.variableName)) { + pureWhiteText++; + issues.push({ + type: 'pure-white', + severity: 'info', + nodeName: diff.variableName, + message: `Pure white (#ffffff) text in ${modeName} — can cause eye strain`, + currentValue: strVal, + suggestions: ['Use #e0e0e0 or #f0f0f0 for softer text on dark backgrounds'], + }); + } + } + } + } + } + + // Missing mode values + const missingModeValues = modeData.missingValues.length; + for (const mv of modeData.missingValues) { + issues.push({ + type: 'missing-mode', + severity: 'critical', + nodeName: mv.variableName, + message: `Missing values for modes: ${mv.missingModes.join(', ')}`, + suggestions: [`Add values for: ${mv.missingModes.join(', ')}`], + }); + } + + const totalChecked = modeData.variableDiffs.length + modeData.missingValues.length; + const failed = issues.length; + + return { + issues, + metrics: { + pureBlackBackgrounds, + pureWhiteText, + lowContrastOnDark, + missingModeValues, + }, + summary: { + totalChecked, + passed: Math.max(0, totalChecked - failed), + failed, + }, + }; +} + // ============================================================================ // Realtime Lint Handlers // ============================================================================ @@ -2554,6 +2675,9 @@ function handleDisableRealtimeLint(): void { /** * Calculate design debt score from lint results and token summary. + * Transforms the DesignDebtScore shape to match DesignDebtCard props: + * components values are plain counts (not { count, score } objects), + * trend.direction maps 'degrading' -> 'declining' for the UI. */ function handleCalculateDesignDebt(data: { lintResult: { @@ -2563,5 +2687,101 @@ function handleCalculateDesignDebt(data: { tokenSummary?: { totalTokens: number; actualTokens: number; hardCodedValues: number; aiSuggestions: number }; }): void { const score = calculateDesignDebt(data.lintResult, data.tokenSummary || null); - sendMessageToUI('design-debt-result', score); + + // Transform to DesignDebtCard-compatible shape + const cardData: { + overall: number; + components: { + orphanedStyles: number; + detachedInstances: number; + hardcodedValues: number; + namingViolations: number; + missingAutoLayout: number; + inconsistentSpacing: number; + }; + trend?: { + direction: 'improving' | 'declining' | 'stable'; + delta: number; + }; + } = { + overall: score.overall, + components: { + orphanedStyles: score.components.orphanedStyles.count, + detachedInstances: score.components.detachedInstances.count, + hardcodedValues: score.components.hardcodedValues.count, + namingViolations: score.components.namingViolations.count, + missingAutoLayout: score.components.missingAutoLayout.count, + inconsistentSpacing: score.components.inconsistentSpacing.count, + }, + }; + + if (score.trend) { + cardData.trend = { + direction: score.trend.direction === 'degrading' ? 'declining' : score.trend.direction, + delta: Math.abs(score.trend.delta), + }; + } + + sendMessageToUI('design-debt-result', cardData); +} + +// ============================================================================ +// Extended Lint Handler (8 new modules) +// ============================================================================ + +/** + * Run all 8 extended lint modules on the current selection and send aggregated + * results to the UI as 'extended-lint-result'. + * + * Sync modules (layout-sizing, constraints, typography, component-props) run + * immediately; async modules (style-audit, variable-scope, multi-theme, + * grid-check) run via Promise.allSettled. + */ +async function handleRunExtendedLint(): Promise { + try { + const selection = figma.currentPage.selection; + if (selection.length === 0) { + sendMessageToUI('extended-lint-error', { error: 'No nodes selected. Select a frame or component to run extended lint.' }); + return; + } + + const nodes: readonly SceneNode[] = selection; + + // ── Sync modules ── + const layoutSizing = checkLayoutSizing(nodes); + const constraints = checkConstraints(nodes); + const typography = checkTypography(nodes); + const componentProps = checkComponentProps(nodes); + + // ── Async modules ── + const [styleAuditResult, variableScopeResult, multiThemeResult, gridCheckResult] = + await Promise.allSettled([ + checkStyleAudit(nodes), + checkVariableScope(nodes), + checkMultiTheme(nodes), + checkGrid(nodes), + ]); + + // Unwrap settled results — use empty result on rejection + const emptyIssues = { issues: [] as unknown[], summary: {} }; + const styleAudit = styleAuditResult.status === 'fulfilled' ? styleAuditResult.value : emptyIssues; + const variableScope = variableScopeResult.status === 'fulfilled' ? variableScopeResult.value : emptyIssues; + const multiTheme = multiThemeResult.status === 'fulfilled' ? multiThemeResult.value : emptyIssues; + const gridCheck = gridCheckResult.status === 'fulfilled' ? gridCheckResult.value : emptyIssues; + + sendMessageToUI('extended-lint-result', { + layoutSizing, + constraints, + typography, + componentProps, + styleAudit, + variableScope, + multiTheme, + gridCheck, + }); + } catch (error) { + console.error('Error running extended lint:', error); + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + sendMessageToUI('extended-lint-error', { error: errorMessage }); + } } diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 20eb583..d861020 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,6 +1,7 @@ import { useState, useCallback, useEffect, useRef } from 'react'; import ChatContainer from './components/chat/ChatContainer'; import SettingsPanel from './components/shared/SettingsPanel'; +import TeamConfigPanel from './components/shared/TeamConfigPanel'; import { useChat } from './hooks/useChat'; import { usePluginMessages, usePostToPlugin } from './hooks/usePluginMessages'; import type { PluginEvent, LintResult, LintError, AiReviewData, ReferoComparisonData, FlowAnalysisData, DiffResultData, PageSweepData, PageSweepRawData, MiniScoreData } from './lib/messages'; @@ -14,6 +15,8 @@ export default function App() { const [backendAvailable, setBackendAvailable] = useState(false); const [analysisMode, setAnalysisMode] = useState<'quick' | 'deep'>('quick'); const [showSettings, setShowSettings] = useState(false); + const [showTeamConfig, setShowTeamConfig] = useState(false); + const [teamConfig, setTeamConfig] = useState | undefined>(undefined); const [selectionStale, setSelectionStale] = useState(false); const [currentNodeName, setCurrentNodeName] = useState(null); const [miniScore, setMiniScore] = useState(null); @@ -298,6 +301,107 @@ export default function App() { setMiniScore(event.data as MiniScoreData); break; } + + // ── New analysis feature events ──────────────────── + + case 'design-debt-result': { + chat.addMessage({ kind: 'design-debt', data: event.data }); + break; + } + + case 'dark-mode-card-result': { + chat.addMessage({ kind: 'dark-mode', data: event.data }); + break; + } + + case 'mode-comparison-result': { + // Raw mode data — show text summary (card comes via dark-mode-card-result) + const modeRaw = event.data as { collection: string; variableDiffs: unknown[]; missingValues: unknown[] }; + chat.addMessage({ + kind: 'ai-text', + content: `Mode comparison for "${modeRaw.collection}": ${(modeRaw.variableDiffs as unknown[]).length} variable diffs, ${(modeRaw.missingValues as unknown[]).length} missing values.`, + }); + break; + } + + case 'mode-comparison-error': { + chat.addMessage({ + kind: 'ai-text', + content: `Dark mode comparison failed: ${(event.data as { error: string }).error}`, + }); + break; + } + + case 'dtcg-compliance-result': { + chat.addMessage({ kind: 'token-compliance', data: event.data }); + break; + } + + case 'dtcg-compliance-error': { + chat.addMessage({ + kind: 'ai-text', + content: `Token compliance check failed: ${(event.data as { error: string }).error}`, + }); + break; + } + + case 'variable-system-result': { + const report = event.data as { totalVariables: number; unusedVariables: string[]; adoptionRate: number; collections: unknown[] }; + chat.addMessage({ + kind: 'ai-text', + content: `**Variable System Report**\n- Total variables: ${report.totalVariables}\n- Adoption rate: ${Math.round(report.adoptionRate * 100)}%\n- Unused variables: ${report.unusedVariables.length}\n- Collections: ${report.collections.length}`, + }); + break; + } + + case 'variable-system-error': { + chat.addMessage({ + kind: 'ai-text', + content: `Variable collection failed: ${(event.data as { error: string }).error}`, + }); + break; + } + + case 'extended-lint-result': { + const extResult = event.data as Record; + const totalIssues = Object.values(extResult).reduce( + (sum, r) => sum + (r?.issues?.length ?? 0), 0 + ); + chat.addMessage({ + kind: 'ai-text', + content: `**Extended lint complete** — ${totalIssues} issue${totalIssues !== 1 ? 's' : ''} across ${Object.keys(extResult).length} modules.`, + }); + break; + } + + case 'extended-lint-error': { + chat.addMessage({ + kind: 'ai-text', + content: `Extended lint failed: ${(event.data as { error: string }).error}`, + }); + break; + } + + case 'team-config-loaded': { + const payload = event.data as { config: Record | null; settings: unknown }; + if (payload.config) { + setTeamConfig(payload.config); + chat.addMessage({ kind: 'ai-text', content: 'Team config loaded from file.' }); + } else { + chat.addMessage({ kind: 'ai-text', content: 'No team config found in this file.' }); + } + break; + } + + case 'team-config-saved': { + const saveResult = event.data as { success: boolean; error?: string }; + if (saveResult.success) { + chat.addMessage({ kind: 'ai-text', content: 'Team config saved to file.' }); + } else { + chat.addMessage({ kind: 'ai-text', content: `Failed to save team config: ${saveResult.error || 'Unknown error'}` }); + } + break; + } } }, [chat, post, tryBackendAnalysis] @@ -618,6 +722,36 @@ export default function App() { }); break; } + + // ── New analysis feature actions ──────────────────── + + case 'design-debt': { + if (!chat.lintResult) { + chat.addMessage({ kind: 'ai-text', content: 'Run an analysis first to calculate design debt.' }); + break; + } + chat.addMessage({ kind: 'ai-text', content: 'Calculating design debt...' }); + post('calculate-design-debt', { + lintResult: { + errors: chat.lintResult.errors, + summary: chat.lintResult.summary, + }, + }); + break; + } + + case 'dark-mode': { + chat.addMessage({ kind: 'ai-text', content: 'Comparing variable modes...' }); + post('compare-modes'); + break; + } + + case 'token-audit': { + chat.addMessage({ kind: 'ai-text', content: 'Running token audit (DTCG compliance + variable collection)...' }); + post('check-dtcg-compliance'); + post('collect-variables'); + break; + } } }, [chat, post, componentName, analysisMode] @@ -648,6 +782,14 @@ export default function App() { /> )} + {/* Team config panel (overlay) */} + {showTeamConfig && ( + setShowTeamConfig(false)} + /> + )} + {/* Top analyze bar (shown when no results yet) */} {chat.messages.length === 0 && !chat.isAnalyzing && (
@@ -670,6 +812,18 @@ export default function App() { > Sweep Page + + )} + + {hasBaseline && ( + + )} + + + - )} - - - {hasBaseline && ( + +
+ + {/* Secondary row — advanced actions */} + {showMore && ( +
+ + + +
)} - - - -
); } diff --git a/ui/src/lib/api.ts b/ui/src/lib/api.ts index ecf5297..cf8f353 100644 --- a/ui/src/lib/api.ts +++ b/ui/src/lib/api.ts @@ -297,3 +297,185 @@ export async function analyzeFlow(data: { return resp.json(); } + +// ── New Analysis Routes ────────────────────────────────────── + +/** + * POST /api/brand-consistency — brand guide compliance analysis. + */ +export async function analyzeBrandConsistency(data: { + screenshot: string; + brandGuide: { + colors: Record; + typography: { + heading: { family: string; weights: number[] }; + body: { family: string; weights: number[] }; + }; + spacing: { base: number; scale: number[] }; + personality: string[]; + rules?: Array<{ id: string; description: string; severity: 'error' | 'warning' }>; + }; + lintResult?: unknown; + sessionId?: string; +}): Promise<{ + success: boolean; + brandConsistency: { + overallScore: number; + colorCompliance: { + score: number; + violations: Array<{ element: string; found: string; expected: string; tolerance: number }>; + }; + typographyCompliance: { + score: number; + violations: Array<{ element: string; found: string; expected: string }>; + }; + spacingCompliance: { score: number }; + personalityMatch: { rating: 'strong' | 'moderate' | 'weak'; evidence: string[] }; + recommendations: Array<{ title: string; description: string; severity: string }>; + summary: string; + }; +}> { + const resp = await fetch(`${backendUrl}/api/brand-consistency`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + + if (!resp.ok) { + const err = await resp.json().catch(() => ({ error: resp.statusText })); + throw new Error(err.error || 'Brand consistency analysis failed'); + } + + return resp.json(); +} + +/** + * POST /api/copy-tone — copy and tone analysis across screens. + */ +export async function analyzeCopyTone(data: { + screens: Array<{ name: string; textContent: string[] }>; + personality?: string[]; + sessionId?: string; +}): Promise<{ + success: boolean; + copyTone: unknown; +}> { + const resp = await fetch(`${backendUrl}/api/copy-tone`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + + if (!resp.ok) { + const err = await resp.json().catch(() => ({ error: resp.statusText })); + throw new Error(err.error || 'Copy tone analysis failed'); + } + + return resp.json(); +} + +/** + * POST /api/persona-research — persona-based UX research analysis. + */ +export async function analyzePersonaResearch(data: { + screenshot: string; + taskDescription: string; + lintContext?: string; + sessionId?: string; +}): Promise<{ + success: boolean; + personaResearch: unknown; +}> { + const resp = await fetch(`${backendUrl}/api/persona-research`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + + if (!resp.ok) { + const err = await resp.json().catch(() => ({ error: resp.statusText })); + throw new Error(err.error || 'Persona research failed'); + } + + return resp.json(); +} + +/** + * POST /api/generate-a11y-spec — accessibility specification generation. + */ +export async function generateA11ySpec(data: { + screenshot: string; + extractedData: { + componentName: string; + componentDescription?: string; + properties?: Array<{ name: string; type: string }>; + states?: string[]; + metadata?: { + nodeId: string; + nodeType: string; + width: number; + height: number; + hasAutoLayout: boolean; + childCount: number; + }; + }; + lintResult: { + summary: { + totalErrors: number; + byType: Record; + totalNodes: number; + nodesWithErrors: number; + }; + errors: Array<{ + nodeId: string; + nodeName: string; + errorType: string; + message: string; + value: string; + }>; + }; + sessionId?: string; +}): Promise<{ + success: boolean; + spec: { + landmarks: Array<{ role: string; label: string; element: string }>; + headingStructure: Array<{ level: number; text: string; element: string }>; + focusOrder: Array<{ order: number; element: string; type: string; notes: string }>; + ariaAnnotations: Array<{ + element: string; + role: string; + ariaLabel?: string; + ariaDescribedBy?: string; + ariaLive?: string; + notes: string; + }>; + keyboardShortcuts: Array<{ key: string; action: string; element: string }>; + liveRegions: Array<{ element: string; type: 'polite' | 'assertive'; trigger: string }>; + colorContrastReport: Array<{ + element: string; + foreground: string; + background: string; + ratio: number; + passes: 'AA' | 'AAA' | 'fail'; + }>; + recommendations: Array<{ + title: string; + description: string; + wcagCriterion: string; + level: 'A' | 'AA' | 'AAA'; + }>; + }; +}> { + const resp = await fetch(`${backendUrl}/api/generate-a11y-spec`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + + if (!resp.ok) { + const err = await resp.json().catch(() => ({ error: resp.statusText })); + throw new Error(err.error || 'Accessibility spec generation failed'); + } + + return resp.json(); +} diff --git a/ui/src/lib/messages.ts b/ui/src/lib/messages.ts index 814e52a..18bbe82 100644 --- a/ui/src/lib/messages.ts +++ b/ui/src/lib/messages.ts @@ -235,7 +235,20 @@ export type PluginEvent = | { type: 'diff-result'; data: DiffResultData } | { type: 'page-sweep-progress'; data: { current: number; total: number; frameName: string } } | { type: 'page-sweep-result'; data: PageSweepRawData } - | { type: 'selection-mini-score'; data: MiniScoreData }; + | { type: 'selection-mini-score'; data: MiniScoreData } + | { type: 'design-debt-result'; data: DesignDebtScore } + | { type: 'variable-system-result'; data: VariableSystemReport } + | { type: 'variable-system-error'; data: { error: string } } + | { type: 'dtcg-compliance-result'; data: DTCGComplianceResult } + | { type: 'dtcg-compliance-error'; data: { error: string } } + | { type: 'mode-comparison-result'; data: ModeComparisonData } + | { type: 'mode-comparison-error'; data: { error: string } } + | { type: 'realtime-lint-update'; data: { errors: LintError[]; changedNodeIds: string[] } } + | { type: 'team-config-loaded'; data: { config: unknown; settings: unknown } } + | { type: 'team-config-saved'; data: { success: boolean; error?: string } } + | { type: 'dark-mode-card-result'; data: DarkModeResult } + | { type: 'extended-lint-result'; data: Record } + | { type: 'extended-lint-error'; data: { error: string } }; // Flow Analysis Types export interface FlowGraphIssue { @@ -344,6 +357,25 @@ export interface BaselineMetaData { overall: number; } +// ── Design Debt Score Types ────────────────────────────────── + +export interface DesignDebtScore { + overall: number; + components: { + orphanedStyles: { count: number; score: number }; + detachedInstances: { count: number; score: number }; + hardcodedValues: { count: number; score: number }; + namingViolations: { count: number; score: number }; + missingAutoLayout: { count: number; score: number }; + inconsistentSpacing: { count: number; score: number }; + }; + trend?: { + previousScore: number; + delta: number; + direction: 'improving' | 'stable' | 'degrading'; + }; +} + // ── Variable System & DTCG Compliance Types ────────────────── export interface VariableCollectionData {