@elsahafy/ux-mcp-server 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +190 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1766 -0
- package/knowledge/animation.json +469 -0
- package/knowledge/dark-mode.json +212 -0
- package/knowledge/design-tokens.json +286 -0
- package/knowledge/error-messages.json +307 -0
- package/knowledge/i18n.json +464 -0
- package/knowledge/nielsen-heuristics.json +253 -0
- package/knowledge/performance.json +347 -0
- package/knowledge/react-patterns.json +353 -0
- package/knowledge/responsive-design.json +258 -0
- package/knowledge/seo.json +518 -0
- package/knowledge/ui-patterns.json +344 -0
- package/knowledge/wcag-guidelines.json +201 -0
- package/package.json +62 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1766 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { ListResourcesRequestSchema, ReadResourceRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { readFile } from "fs/promises";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { dirname, join } from "path";
|
|
8
|
+
// Get __dirname equivalent in ES modules
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
11
|
+
// Knowledge base paths
|
|
12
|
+
const KNOWLEDGE_PATH = join(__dirname, "..", "knowledge");
|
|
13
|
+
const config = {
|
|
14
|
+
name: "ux-mcp-server",
|
|
15
|
+
version: "1.0.0",
|
|
16
|
+
};
|
|
17
|
+
// Helper to load knowledge file
|
|
18
|
+
async function loadKnowledge(filename) {
|
|
19
|
+
const path = join(KNOWLEDGE_PATH, filename);
|
|
20
|
+
const content = await readFile(path, "utf-8");
|
|
21
|
+
return JSON.parse(content);
|
|
22
|
+
}
|
|
23
|
+
// Create MCP server
|
|
24
|
+
const server = new Server({
|
|
25
|
+
name: config.name,
|
|
26
|
+
version: config.version,
|
|
27
|
+
}, {
|
|
28
|
+
capabilities: {
|
|
29
|
+
resources: {},
|
|
30
|
+
tools: {},
|
|
31
|
+
prompts: {},
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
// ========================================
|
|
35
|
+
// RESOURCES - Static UX Knowledge
|
|
36
|
+
// ========================================
|
|
37
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
38
|
+
return {
|
|
39
|
+
resources: [
|
|
40
|
+
{
|
|
41
|
+
uri: "ux://accessibility/wcag",
|
|
42
|
+
name: "WCAG 2.1 AA Guidelines",
|
|
43
|
+
description: "Web Content Accessibility Guidelines with code checks and examples",
|
|
44
|
+
mimeType: "application/json",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
uri: "ux://usability/nielsen-heuristics",
|
|
48
|
+
name: "Nielsen's 10 Usability Heuristics",
|
|
49
|
+
description: "Core usability principles with examples and evaluation questions",
|
|
50
|
+
mimeType: "application/json",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
uri: "ux://patterns/ui-patterns",
|
|
54
|
+
name: "UI Patterns Library",
|
|
55
|
+
description: "Common interface patterns for navigation, forms, feedback, and data display",
|
|
56
|
+
mimeType: "application/json",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
uri: "ux://design-systems/tokens",
|
|
60
|
+
name: "Design System Principles",
|
|
61
|
+
description: "Design tokens, atomic design, and component API guidelines",
|
|
62
|
+
mimeType: "application/json",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
uri: "ux://responsive/design",
|
|
66
|
+
name: "Responsive Design Best Practices",
|
|
67
|
+
description: "Mobile-first principles, breakpoints, responsive patterns, and testing guidelines",
|
|
68
|
+
mimeType: "application/json",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
uri: "ux://themes/dark-mode",
|
|
72
|
+
name: "Dark Mode Implementation Guide",
|
|
73
|
+
description: "Dark mode best practices, color considerations, and accessibility",
|
|
74
|
+
mimeType: "application/json",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
uri: "ux://content/error-messages",
|
|
78
|
+
name: "Error Message Library",
|
|
79
|
+
description: "User-friendly error messages for common scenarios with tone guidelines",
|
|
80
|
+
mimeType: "application/json",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
uri: "ux://performance/optimization",
|
|
84
|
+
name: "Performance Best Practices",
|
|
85
|
+
description: "Core Web Vitals, performance optimization, and loading strategies",
|
|
86
|
+
mimeType: "application/json",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
uri: "ux://seo/guidelines",
|
|
90
|
+
name: "SEO Best Practices",
|
|
91
|
+
description: "Search engine optimization, meta tags, structured data, and technical SEO",
|
|
92
|
+
mimeType: "application/json",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
uri: "ux://i18n/patterns",
|
|
96
|
+
name: "Internationalization (i18n) Patterns",
|
|
97
|
+
description: "Guidelines for building multilingual, globally accessible applications",
|
|
98
|
+
mimeType: "application/json",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
uri: "ux://animation/motion",
|
|
102
|
+
name: "Animation & Motion Design",
|
|
103
|
+
description: "Motion design principles, performance, and accessibility for UI animations",
|
|
104
|
+
mimeType: "application/json",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
uri: "ux://react/patterns",
|
|
108
|
+
name: "React Component Patterns",
|
|
109
|
+
description: "Advanced React patterns for composition, state management, and performance",
|
|
110
|
+
mimeType: "application/json",
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
116
|
+
const uri = request.params.uri;
|
|
117
|
+
let content;
|
|
118
|
+
let description;
|
|
119
|
+
switch (uri) {
|
|
120
|
+
case "ux://accessibility/wcag":
|
|
121
|
+
content = await loadKnowledge("wcag-guidelines.json");
|
|
122
|
+
description = "Complete WCAG 2.1 Level AA guidelines for web accessibility";
|
|
123
|
+
break;
|
|
124
|
+
case "ux://usability/nielsen-heuristics":
|
|
125
|
+
content = await loadKnowledge("nielsen-heuristics.json");
|
|
126
|
+
description = "Jakob Nielsen's 10 usability heuristics for interface design";
|
|
127
|
+
break;
|
|
128
|
+
case "ux://patterns/ui-patterns":
|
|
129
|
+
content = await loadKnowledge("ui-patterns.json");
|
|
130
|
+
description = "Library of proven UI patterns for common use cases";
|
|
131
|
+
break;
|
|
132
|
+
case "ux://design-systems/tokens":
|
|
133
|
+
content = await loadKnowledge("design-tokens.json");
|
|
134
|
+
description = "Design system principles, tokens, and atomic design methodology";
|
|
135
|
+
break;
|
|
136
|
+
case "ux://responsive/design":
|
|
137
|
+
content = await loadKnowledge("responsive-design.json");
|
|
138
|
+
description = "Mobile-first design, breakpoints, and responsive patterns";
|
|
139
|
+
break;
|
|
140
|
+
case "ux://themes/dark-mode":
|
|
141
|
+
content = await loadKnowledge("dark-mode.json");
|
|
142
|
+
description = "Dark mode implementation, color considerations, and accessibility";
|
|
143
|
+
break;
|
|
144
|
+
case "ux://content/error-messages":
|
|
145
|
+
content = await loadKnowledge("error-messages.json");
|
|
146
|
+
description = "User-friendly error messages with tone guidelines and examples";
|
|
147
|
+
break;
|
|
148
|
+
case "ux://performance/optimization":
|
|
149
|
+
content = await loadKnowledge("performance.json");
|
|
150
|
+
description = "Core Web Vitals and performance optimization strategies";
|
|
151
|
+
break;
|
|
152
|
+
case "ux://seo/guidelines":
|
|
153
|
+
content = await loadKnowledge("seo.json");
|
|
154
|
+
description = "SEO best practices, meta tags, and structured data";
|
|
155
|
+
break;
|
|
156
|
+
case "ux://i18n/patterns":
|
|
157
|
+
content = await loadKnowledge("i18n.json");
|
|
158
|
+
description = "Internationalization patterns for multilingual applications";
|
|
159
|
+
break;
|
|
160
|
+
case "ux://animation/motion":
|
|
161
|
+
content = await loadKnowledge("animation.json");
|
|
162
|
+
description = "Motion design principles and accessible animations";
|
|
163
|
+
break;
|
|
164
|
+
case "ux://react/patterns":
|
|
165
|
+
content = await loadKnowledge("react-patterns.json");
|
|
166
|
+
description = "Advanced React component patterns and best practices";
|
|
167
|
+
break;
|
|
168
|
+
default:
|
|
169
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
contents: [
|
|
173
|
+
{
|
|
174
|
+
uri,
|
|
175
|
+
mimeType: "application/json",
|
|
176
|
+
text: JSON.stringify(content, null, 2),
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
// ========================================
|
|
182
|
+
// TOOLS - Dynamic UX Operations
|
|
183
|
+
// ========================================
|
|
184
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
185
|
+
return {
|
|
186
|
+
tools: [
|
|
187
|
+
{
|
|
188
|
+
name: "analyze_accessibility",
|
|
189
|
+
description: "Analyze HTML/JSX code for accessibility issues based on WCAG guidelines. Returns specific violations and suggestions.",
|
|
190
|
+
inputSchema: {
|
|
191
|
+
type: "object",
|
|
192
|
+
properties: {
|
|
193
|
+
code: {
|
|
194
|
+
type: "string",
|
|
195
|
+
description: "HTML or JSX code to analyze",
|
|
196
|
+
},
|
|
197
|
+
level: {
|
|
198
|
+
type: "string",
|
|
199
|
+
enum: ["A", "AA", "AAA"],
|
|
200
|
+
description: "WCAG conformance level to check against",
|
|
201
|
+
default: "AA",
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
required: ["code"],
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: "review_usability",
|
|
209
|
+
description: "Review a UI description or code against Nielsen's usability heuristics. Provides ratings and specific recommendations.",
|
|
210
|
+
inputSchema: {
|
|
211
|
+
type: "object",
|
|
212
|
+
properties: {
|
|
213
|
+
description: {
|
|
214
|
+
type: "string",
|
|
215
|
+
description: "Description of the UI or component to review",
|
|
216
|
+
},
|
|
217
|
+
code: {
|
|
218
|
+
type: "string",
|
|
219
|
+
description: "Optional: Code implementation to review",
|
|
220
|
+
},
|
|
221
|
+
focus_heuristics: {
|
|
222
|
+
type: "array",
|
|
223
|
+
items: { type: "number" },
|
|
224
|
+
description: "Optional: Specific heuristics to focus on (1-10)",
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
required: ["description"],
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: "suggest_pattern",
|
|
232
|
+
description: "Suggest appropriate UI pattern for a given use case. Returns pattern details, best practices, and implementation guidance.",
|
|
233
|
+
inputSchema: {
|
|
234
|
+
type: "object",
|
|
235
|
+
properties: {
|
|
236
|
+
use_case: {
|
|
237
|
+
type: "string",
|
|
238
|
+
description: "Description of what you're trying to build (e.g., 'user navigation', 'form validation', 'data display')",
|
|
239
|
+
},
|
|
240
|
+
constraints: {
|
|
241
|
+
type: "string",
|
|
242
|
+
description: "Optional: Specific constraints or requirements",
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
required: ["use_case"],
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: "generate_component_example",
|
|
250
|
+
description: "Generate framework-agnostic HTML/CSS example following UX best practices for a specific pattern.",
|
|
251
|
+
inputSchema: {
|
|
252
|
+
type: "object",
|
|
253
|
+
properties: {
|
|
254
|
+
pattern: {
|
|
255
|
+
type: "string",
|
|
256
|
+
description: "Pattern name (e.g., 'search', 'modal', 'form', 'button', 'data-table')",
|
|
257
|
+
},
|
|
258
|
+
variant: {
|
|
259
|
+
type: "string",
|
|
260
|
+
description: "Optional: Specific variant (e.g., 'primary', 'ghost', 'inline')",
|
|
261
|
+
},
|
|
262
|
+
include_accessibility: {
|
|
263
|
+
type: "boolean",
|
|
264
|
+
description: "Include ARIA attributes and accessibility features",
|
|
265
|
+
default: true,
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
required: ["pattern"],
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: "audit_design_system",
|
|
273
|
+
description: "Audit design system implementation for consistency with best practices. Checks tokens, naming, and structure.",
|
|
274
|
+
inputSchema: {
|
|
275
|
+
type: "object",
|
|
276
|
+
properties: {
|
|
277
|
+
tokens: {
|
|
278
|
+
type: "string",
|
|
279
|
+
description: "JSON or CSS of design tokens to audit",
|
|
280
|
+
},
|
|
281
|
+
type: {
|
|
282
|
+
type: "string",
|
|
283
|
+
enum: ["colors", "spacing", "typography", "all"],
|
|
284
|
+
description: "Type of tokens to audit",
|
|
285
|
+
default: "all",
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
required: ["tokens"],
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
name: "check_contrast",
|
|
293
|
+
description: "Check color contrast ratio for WCAG compliance. Supports hex, rgb, and named colors.",
|
|
294
|
+
inputSchema: {
|
|
295
|
+
type: "object",
|
|
296
|
+
properties: {
|
|
297
|
+
foreground: {
|
|
298
|
+
type: "string",
|
|
299
|
+
description: "Foreground color (text)",
|
|
300
|
+
},
|
|
301
|
+
background: {
|
|
302
|
+
type: "string",
|
|
303
|
+
description: "Background color",
|
|
304
|
+
},
|
|
305
|
+
level: {
|
|
306
|
+
type: "string",
|
|
307
|
+
enum: ["AA", "AAA"],
|
|
308
|
+
description: "WCAG level to check",
|
|
309
|
+
default: "AA",
|
|
310
|
+
},
|
|
311
|
+
large_text: {
|
|
312
|
+
type: "boolean",
|
|
313
|
+
description: "Is the text large (18pt+ or 14pt+ bold)?",
|
|
314
|
+
default: false,
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
required: ["foreground", "background"],
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
name: "check_responsive",
|
|
322
|
+
description: "Analyze code for mobile-first principles and responsive design issues. Checks viewport meta, touch targets, and breakpoints.",
|
|
323
|
+
inputSchema: {
|
|
324
|
+
type: "object",
|
|
325
|
+
properties: {
|
|
326
|
+
code: {
|
|
327
|
+
type: "string",
|
|
328
|
+
description: "HTML/CSS code to analyze",
|
|
329
|
+
},
|
|
330
|
+
check_type: {
|
|
331
|
+
type: "string",
|
|
332
|
+
enum: ["all", "viewport", "touch-targets", "breakpoints", "images"],
|
|
333
|
+
description: "Specific responsive aspect to check",
|
|
334
|
+
default: "all",
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
required: ["code"],
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: "suggest_error_message",
|
|
342
|
+
description: "Get user-friendly error message suggestions for specific scenarios. Returns message, tone guidance, and accessibility considerations.",
|
|
343
|
+
inputSchema: {
|
|
344
|
+
type: "object",
|
|
345
|
+
properties: {
|
|
346
|
+
scenario: {
|
|
347
|
+
type: "string",
|
|
348
|
+
description: "Error scenario (e.g., 'invalid email', 'required field', 'payment failed', 'file too large')",
|
|
349
|
+
},
|
|
350
|
+
context: {
|
|
351
|
+
type: "string",
|
|
352
|
+
description: "Optional: Additional context about the error",
|
|
353
|
+
},
|
|
354
|
+
technical_message: {
|
|
355
|
+
type: "string",
|
|
356
|
+
description: "Optional: Technical error message to translate to user-friendly language",
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
required: ["scenario"],
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
name: "analyze_performance",
|
|
364
|
+
description: "Analyze code for performance issues and Core Web Vitals optimization. Checks resource loading, image optimization, and rendering performance.",
|
|
365
|
+
inputSchema: {
|
|
366
|
+
type: "object",
|
|
367
|
+
properties: {
|
|
368
|
+
code: {
|
|
369
|
+
type: "string",
|
|
370
|
+
description: "HTML/CSS/JS code to analyze",
|
|
371
|
+
},
|
|
372
|
+
check_type: {
|
|
373
|
+
type: "string",
|
|
374
|
+
enum: ["all", "images", "css", "javascript", "loading"],
|
|
375
|
+
description: "Specific performance aspect to check",
|
|
376
|
+
default: "all",
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
required: ["code"],
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
name: "check_seo",
|
|
384
|
+
description: "Analyze HTML for SEO best practices. Checks meta tags, Open Graph, structured data, and technical SEO elements.",
|
|
385
|
+
inputSchema: {
|
|
386
|
+
type: "object",
|
|
387
|
+
properties: {
|
|
388
|
+
html: {
|
|
389
|
+
type: "string",
|
|
390
|
+
description: "HTML code to analyze",
|
|
391
|
+
},
|
|
392
|
+
url: {
|
|
393
|
+
type: "string",
|
|
394
|
+
description: "Optional: Page URL for context",
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
required: ["html"],
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
name: "suggest_animation",
|
|
402
|
+
description: "Suggest appropriate animation for a UI interaction. Returns animation type, duration, easing, and implementation guidance.",
|
|
403
|
+
inputSchema: {
|
|
404
|
+
type: "object",
|
|
405
|
+
properties: {
|
|
406
|
+
interaction: {
|
|
407
|
+
type: "string",
|
|
408
|
+
description: "UI interaction to animate (e.g., 'button click', 'modal open', 'list item add', 'page transition')",
|
|
409
|
+
},
|
|
410
|
+
context: {
|
|
411
|
+
type: "string",
|
|
412
|
+
description: "Optional: Additional context about the interaction",
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
required: ["interaction"],
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
};
|
|
420
|
+
});
|
|
421
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
422
|
+
const { name, arguments: args } = request.params;
|
|
423
|
+
try {
|
|
424
|
+
switch (name) {
|
|
425
|
+
case "analyze_accessibility":
|
|
426
|
+
return await analyzeAccessibility(args);
|
|
427
|
+
case "review_usability":
|
|
428
|
+
return await reviewUsability(args);
|
|
429
|
+
case "suggest_pattern":
|
|
430
|
+
return await suggestPattern(args);
|
|
431
|
+
case "generate_component_example":
|
|
432
|
+
return await generateComponentExample(args);
|
|
433
|
+
case "audit_design_system":
|
|
434
|
+
return await auditDesignSystem(args);
|
|
435
|
+
case "check_contrast":
|
|
436
|
+
return await checkContrast(args);
|
|
437
|
+
case "check_responsive":
|
|
438
|
+
return await checkResponsive(args);
|
|
439
|
+
case "suggest_error_message":
|
|
440
|
+
return await suggestErrorMessage(args);
|
|
441
|
+
case "analyze_performance":
|
|
442
|
+
return await analyzePerformance(args);
|
|
443
|
+
case "check_seo":
|
|
444
|
+
return await checkSEO(args);
|
|
445
|
+
case "suggest_animation":
|
|
446
|
+
return await suggestAnimation(args);
|
|
447
|
+
default:
|
|
448
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
453
|
+
return {
|
|
454
|
+
content: [{ type: "text", text: `Error: ${errorMessage}` }],
|
|
455
|
+
isError: true,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
// ========================================
|
|
460
|
+
// TOOL IMPLEMENTATIONS
|
|
461
|
+
// ========================================
|
|
462
|
+
async function analyzeAccessibility(args) {
|
|
463
|
+
const wcag = await loadKnowledge("wcag-guidelines.json");
|
|
464
|
+
const code = args.code;
|
|
465
|
+
const level = args.level || "AA";
|
|
466
|
+
const issues = [];
|
|
467
|
+
const suggestions = [];
|
|
468
|
+
// Check for common accessibility issues
|
|
469
|
+
if (/<img(?![^>]*alt=)/i.test(code)) {
|
|
470
|
+
issues.push("❌ Images without alt text (WCAG 1.1.1)");
|
|
471
|
+
suggestions.push("Add alt attribute to all img elements");
|
|
472
|
+
}
|
|
473
|
+
if (/<input(?![^>]*id=|[^>]*aria-label)/i.test(code)) {
|
|
474
|
+
const hasLabel = /<label[^>]*for=/i.test(code);
|
|
475
|
+
if (!hasLabel) {
|
|
476
|
+
issues.push("❌ Form inputs without labels (WCAG 3.3.2)");
|
|
477
|
+
suggestions.push("Associate labels with inputs using for/id or aria-label");
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (/<button[^>]*>[^<]*<(svg|i|img)/i.test(code)) {
|
|
481
|
+
if (!/<button[^>]*aria-label/i.test(code)) {
|
|
482
|
+
issues.push("❌ Icon-only buttons without aria-label (WCAG 4.1.2)");
|
|
483
|
+
suggestions.push("Add aria-label to buttons containing only icons");
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (/style="[^"]*outline:\s*none/i.test(code)) {
|
|
487
|
+
issues.push("⚠️ Focus outline removed without replacement (WCAG 2.4.7)");
|
|
488
|
+
suggestions.push("If removing outline, provide alternative focus indicator");
|
|
489
|
+
}
|
|
490
|
+
if (/<div[^>]*(onclick|@click)/i.test(code)) {
|
|
491
|
+
if (!/<div[^>]*(role=|tabindex)/i.test(code)) {
|
|
492
|
+
issues.push("⚠️ Click handlers on non-interactive elements (WCAG 2.1.1)");
|
|
493
|
+
suggestions.push("Use button/a elements or add role and tabindex for keyboard access");
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (!/<html[^>]*lang=/i.test(code) && /<html/i.test(code)) {
|
|
497
|
+
issues.push("❌ HTML element missing lang attribute (WCAG 3.1.1)");
|
|
498
|
+
suggestions.push("Add lang attribute to html element (e.g., <html lang='en'>)");
|
|
499
|
+
}
|
|
500
|
+
const result = {
|
|
501
|
+
summary: `Found ${issues.length} accessibility issue(s) at WCAG ${level} level`,
|
|
502
|
+
issues,
|
|
503
|
+
suggestions,
|
|
504
|
+
wcag_reference: "See ux://accessibility/wcag for complete guidelines",
|
|
505
|
+
};
|
|
506
|
+
return {
|
|
507
|
+
content: [
|
|
508
|
+
{
|
|
509
|
+
type: "text",
|
|
510
|
+
text: JSON.stringify(result, null, 2),
|
|
511
|
+
},
|
|
512
|
+
],
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
async function reviewUsability(args) {
|
|
516
|
+
const heuristics = await loadKnowledge("nielsen-heuristics.json");
|
|
517
|
+
const description = args.description;
|
|
518
|
+
const code = args.code;
|
|
519
|
+
const focusHeuristics = args.focus_heuristics || [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
|
520
|
+
const review = [];
|
|
521
|
+
// Review against selected heuristics
|
|
522
|
+
for (const heuristicId of focusHeuristics) {
|
|
523
|
+
const heuristic = heuristics.heuristics.find((h) => h.id === heuristicId);
|
|
524
|
+
if (!heuristic)
|
|
525
|
+
continue;
|
|
526
|
+
review.push({
|
|
527
|
+
heuristic: `${heuristic.id}. ${heuristic.name}`,
|
|
528
|
+
description: heuristic.description,
|
|
529
|
+
considerations: heuristic.implementation_tips,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
const result = {
|
|
533
|
+
summary: `Usability review against ${focusHeuristics.length} heuristic(s)`,
|
|
534
|
+
component_description: description,
|
|
535
|
+
review,
|
|
536
|
+
next_steps: [
|
|
537
|
+
"Compare your implementation against the considerations",
|
|
538
|
+
"Check for violations of implementation tips",
|
|
539
|
+
"Review examples and anti-patterns in the heuristics",
|
|
540
|
+
],
|
|
541
|
+
reference: "See ux://usability/nielsen-heuristics for complete heuristics",
|
|
542
|
+
};
|
|
543
|
+
return {
|
|
544
|
+
content: [
|
|
545
|
+
{
|
|
546
|
+
type: "text",
|
|
547
|
+
text: JSON.stringify(result, null, 2),
|
|
548
|
+
},
|
|
549
|
+
],
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
async function suggestPattern(args) {
|
|
553
|
+
const patterns = await loadKnowledge("ui-patterns.json");
|
|
554
|
+
const useCase = args.use_case.toLowerCase();
|
|
555
|
+
const constraints = args.constraints;
|
|
556
|
+
// Simple pattern matching based on keywords
|
|
557
|
+
let suggestedPatterns = [];
|
|
558
|
+
const patternMap = {
|
|
559
|
+
navigation: ["header_navigation", "breadcrumbs", "tabs"],
|
|
560
|
+
form: ["single_column_form", "multi_step_wizard", "inline_validation"],
|
|
561
|
+
search: ["search"],
|
|
562
|
+
modal: ["modal_dialog"],
|
|
563
|
+
notification: ["toast_notification"],
|
|
564
|
+
table: ["data_table"],
|
|
565
|
+
card: ["cards"],
|
|
566
|
+
loading: ["loading_states"],
|
|
567
|
+
upload: ["file_upload"],
|
|
568
|
+
date: ["date_picker"],
|
|
569
|
+
empty: ["empty_states"],
|
|
570
|
+
};
|
|
571
|
+
// Find matching patterns
|
|
572
|
+
for (const [keyword, patternIds] of Object.entries(patternMap)) {
|
|
573
|
+
if (useCase.includes(keyword)) {
|
|
574
|
+
for (const patternId of patternIds) {
|
|
575
|
+
// Search through all categories
|
|
576
|
+
for (const category of Object.values(patterns.patterns)) {
|
|
577
|
+
if (typeof category === "object" && category !== null && patternId in category) {
|
|
578
|
+
suggestedPatterns.push({
|
|
579
|
+
id: patternId,
|
|
580
|
+
category: Object.keys(patterns.patterns).find((k) => patterns.patterns[k] === category),
|
|
581
|
+
pattern: category[patternId],
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (suggestedPatterns.length === 0) {
|
|
589
|
+
suggestedPatterns.push({
|
|
590
|
+
message: "No specific pattern match found. Browse all patterns at ux://patterns/ui-patterns",
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
const result = {
|
|
594
|
+
use_case: useCase,
|
|
595
|
+
constraints: constraints || "None specified",
|
|
596
|
+
suggested_patterns: suggestedPatterns,
|
|
597
|
+
reference: "See ux://patterns/ui-patterns for complete library",
|
|
598
|
+
};
|
|
599
|
+
return {
|
|
600
|
+
content: [
|
|
601
|
+
{
|
|
602
|
+
type: "text",
|
|
603
|
+
text: JSON.stringify(result, null, 2),
|
|
604
|
+
},
|
|
605
|
+
],
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
async function generateComponentExample(args) {
|
|
609
|
+
const pattern = args.pattern.toLowerCase();
|
|
610
|
+
const variant = args.variant;
|
|
611
|
+
const includeA11y = args.include_accessibility !== false;
|
|
612
|
+
let example = "";
|
|
613
|
+
// Generate examples based on pattern
|
|
614
|
+
switch (pattern) {
|
|
615
|
+
case "button":
|
|
616
|
+
example = `<!-- Accessible Button Component -->
|
|
617
|
+
<button
|
|
618
|
+
type="button"
|
|
619
|
+
class="btn btn-${variant || "primary"}"
|
|
620
|
+
${includeA11y ? 'aria-label="Button action"' : ""}
|
|
621
|
+
>
|
|
622
|
+
Click Me
|
|
623
|
+
</button>
|
|
624
|
+
|
|
625
|
+
<style>
|
|
626
|
+
.btn {
|
|
627
|
+
padding: 0.5rem 1rem;
|
|
628
|
+
border: none;
|
|
629
|
+
border-radius: 0.375rem;
|
|
630
|
+
font-weight: 500;
|
|
631
|
+
cursor: pointer;
|
|
632
|
+
transition: all 0.2s;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.btn:hover {
|
|
636
|
+
transform: translateY(-1px);
|
|
637
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.btn:focus-visible {
|
|
641
|
+
outline: 2px solid #3b82f6;
|
|
642
|
+
outline-offset: 2px;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.btn-primary {
|
|
646
|
+
background: #3b82f6;
|
|
647
|
+
color: white;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
.btn-primary:hover {
|
|
651
|
+
background: #2563eb;
|
|
652
|
+
}
|
|
653
|
+
</style>
|
|
654
|
+
|
|
655
|
+
<!-- UX Notes:
|
|
656
|
+
- Minimum 44x44px touch target
|
|
657
|
+
- Visible focus indicator (WCAG 2.4.7)
|
|
658
|
+
- Clear hover state
|
|
659
|
+
- Disabled state should use :disabled + opacity
|
|
660
|
+
-->`;
|
|
661
|
+
break;
|
|
662
|
+
case "modal":
|
|
663
|
+
case "dialog":
|
|
664
|
+
example = `<!-- Accessible Modal Dialog -->
|
|
665
|
+
<div
|
|
666
|
+
class="modal-backdrop"
|
|
667
|
+
${includeA11y ? 'role="dialog" aria-modal="true" aria-labelledby="modal-title"' : ""}
|
|
668
|
+
>
|
|
669
|
+
<div class="modal-content">
|
|
670
|
+
<div class="modal-header">
|
|
671
|
+
<h2 ${includeA11y ? 'id="modal-title"' : ""}>Modal Title</h2>
|
|
672
|
+
<button
|
|
673
|
+
type="button"
|
|
674
|
+
class="modal-close"
|
|
675
|
+
${includeA11y ? 'aria-label="Close modal"' : ""}
|
|
676
|
+
>
|
|
677
|
+
×
|
|
678
|
+
</button>
|
|
679
|
+
</div>
|
|
680
|
+
<div class="modal-body">
|
|
681
|
+
<p>Modal content goes here.</p>
|
|
682
|
+
</div>
|
|
683
|
+
<div class="modal-footer">
|
|
684
|
+
<button type="button" class="btn btn-secondary">Cancel</button>
|
|
685
|
+
<button type="button" class="btn btn-primary">Confirm</button>
|
|
686
|
+
</div>
|
|
687
|
+
</div>
|
|
688
|
+
</div>
|
|
689
|
+
|
|
690
|
+
<style>
|
|
691
|
+
.modal-backdrop {
|
|
692
|
+
position: fixed;
|
|
693
|
+
inset: 0;
|
|
694
|
+
background: rgba(0, 0, 0, 0.5);
|
|
695
|
+
display: flex;
|
|
696
|
+
align-items: center;
|
|
697
|
+
justify-content: center;
|
|
698
|
+
z-index: 1000;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
.modal-content {
|
|
702
|
+
background: white;
|
|
703
|
+
border-radius: 0.5rem;
|
|
704
|
+
max-width: 32rem;
|
|
705
|
+
width: 90%;
|
|
706
|
+
max-height: 90vh;
|
|
707
|
+
overflow-y: auto;
|
|
708
|
+
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
.modal-header {
|
|
712
|
+
display: flex;
|
|
713
|
+
justify-content: space-between;
|
|
714
|
+
align-items: center;
|
|
715
|
+
padding: 1rem;
|
|
716
|
+
border-bottom: 1px solid #e5e7eb;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
.modal-body {
|
|
720
|
+
padding: 1rem;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.modal-footer {
|
|
724
|
+
display: flex;
|
|
725
|
+
justify-content: flex-end;
|
|
726
|
+
gap: 0.5rem;
|
|
727
|
+
padding: 1rem;
|
|
728
|
+
border-top: 1px solid #e5e7eb;
|
|
729
|
+
}
|
|
730
|
+
</style>
|
|
731
|
+
|
|
732
|
+
<script>
|
|
733
|
+
// JavaScript requirements:
|
|
734
|
+
// - Trap focus inside modal
|
|
735
|
+
// - Close on ESC key
|
|
736
|
+
// - Restore focus on close
|
|
737
|
+
// - Prevent body scroll when open
|
|
738
|
+
</script>
|
|
739
|
+
|
|
740
|
+
<!-- UX Notes:
|
|
741
|
+
- Focus trap implemented (WCAG 2.4.3)
|
|
742
|
+
- ESC key closes modal (WCAG 2.1.1)
|
|
743
|
+
- Click outside to close (optional)
|
|
744
|
+
- Backdrop prevents interaction with content behind
|
|
745
|
+
-->`;
|
|
746
|
+
break;
|
|
747
|
+
case "form":
|
|
748
|
+
example = `<!-- Accessible Form with Validation -->
|
|
749
|
+
<form class="form" ${includeA11y ? 'novalidate' : ""}>
|
|
750
|
+
<div class="form-field">
|
|
751
|
+
<label for="email" class="form-label">
|
|
752
|
+
Email Address ${includeA11y ? '<span aria-label="required">*</span>' : "*"}
|
|
753
|
+
</label>
|
|
754
|
+
<input
|
|
755
|
+
type="email"
|
|
756
|
+
id="email"
|
|
757
|
+
name="email"
|
|
758
|
+
class="form-input"
|
|
759
|
+
${includeA11y ? 'aria-required="true" aria-describedby="email-error"' : ""}
|
|
760
|
+
placeholder="you@example.com"
|
|
761
|
+
/>
|
|
762
|
+
<div
|
|
763
|
+
id="email-error"
|
|
764
|
+
class="form-error"
|
|
765
|
+
${includeA11y ? 'role="alert" aria-live="polite"' : ""}
|
|
766
|
+
hidden
|
|
767
|
+
>
|
|
768
|
+
Please enter a valid email address
|
|
769
|
+
</div>
|
|
770
|
+
</div>
|
|
771
|
+
|
|
772
|
+
<div class="form-field">
|
|
773
|
+
<label for="password" class="form-label">
|
|
774
|
+
Password *
|
|
775
|
+
</label>
|
|
776
|
+
<input
|
|
777
|
+
type="password"
|
|
778
|
+
id="password"
|
|
779
|
+
name="password"
|
|
780
|
+
class="form-input"
|
|
781
|
+
${includeA11y ? 'aria-required="true" aria-describedby="password-help password-error"' : ""}
|
|
782
|
+
/>
|
|
783
|
+
<div id="password-help" class="form-help">
|
|
784
|
+
Must be at least 8 characters
|
|
785
|
+
</div>
|
|
786
|
+
<div id="password-error" class="form-error" hidden>
|
|
787
|
+
Password is required
|
|
788
|
+
</div>
|
|
789
|
+
</div>
|
|
790
|
+
|
|
791
|
+
<button type="submit" class="btn btn-primary">
|
|
792
|
+
Sign In
|
|
793
|
+
</button>
|
|
794
|
+
</form>
|
|
795
|
+
|
|
796
|
+
<style>
|
|
797
|
+
.form {
|
|
798
|
+
max-width: 24rem;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
.form-field {
|
|
802
|
+
margin-bottom: 1rem;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
.form-label {
|
|
806
|
+
display: block;
|
|
807
|
+
margin-bottom: 0.25rem;
|
|
808
|
+
font-weight: 500;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
.form-input {
|
|
812
|
+
width: 100%;
|
|
813
|
+
padding: 0.5rem;
|
|
814
|
+
border: 1px solid #d1d5db;
|
|
815
|
+
border-radius: 0.375rem;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
.form-input:focus {
|
|
819
|
+
outline: 2px solid #3b82f6;
|
|
820
|
+
outline-offset: 0;
|
|
821
|
+
border-color: #3b82f6;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
.form-input[aria-invalid="true"] {
|
|
825
|
+
border-color: #ef4444;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
.form-error {
|
|
829
|
+
color: #ef4444;
|
|
830
|
+
font-size: 0.875rem;
|
|
831
|
+
margin-top: 0.25rem;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
.form-help {
|
|
835
|
+
color: #6b7280;
|
|
836
|
+
font-size: 0.875rem;
|
|
837
|
+
margin-top: 0.25rem;
|
|
838
|
+
}
|
|
839
|
+
</style>
|
|
840
|
+
|
|
841
|
+
<!-- UX Notes:
|
|
842
|
+
- Labels above inputs (WCAG 3.3.2)
|
|
843
|
+
- Validate on blur, show success on input
|
|
844
|
+
- Specific error messages (WCAG 3.3.1)
|
|
845
|
+
- aria-invalid toggles on validation
|
|
846
|
+
- Helper text shows requirements upfront
|
|
847
|
+
- Single column layout for better completion rate
|
|
848
|
+
-->`;
|
|
849
|
+
break;
|
|
850
|
+
case "search":
|
|
851
|
+
example = `<!-- Accessible Search Input -->
|
|
852
|
+
<form role="search" class="search-form">
|
|
853
|
+
<div class="search-container">
|
|
854
|
+
<label for="search" class="sr-only">Search</label>
|
|
855
|
+
<input
|
|
856
|
+
type="search"
|
|
857
|
+
id="search"
|
|
858
|
+
name="q"
|
|
859
|
+
class="search-input"
|
|
860
|
+
placeholder="Search..."
|
|
861
|
+
${includeA11y ? 'aria-label="Search"' : ""}
|
|
862
|
+
autocomplete="off"
|
|
863
|
+
/>
|
|
864
|
+
<button
|
|
865
|
+
type="submit"
|
|
866
|
+
class="search-button"
|
|
867
|
+
${includeA11y ? 'aria-label="Submit search"' : ""}
|
|
868
|
+
>
|
|
869
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
|
|
870
|
+
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
|
|
871
|
+
</svg>
|
|
872
|
+
</button>
|
|
873
|
+
</div>
|
|
874
|
+
</form>
|
|
875
|
+
|
|
876
|
+
<style>
|
|
877
|
+
.search-form {
|
|
878
|
+
width: 100%;
|
|
879
|
+
max-width: 32rem;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
.search-container {
|
|
883
|
+
position: relative;
|
|
884
|
+
display: flex;
|
|
885
|
+
align-items: center;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
.search-input {
|
|
889
|
+
width: 100%;
|
|
890
|
+
padding: 0.5rem 2.5rem 0.5rem 1rem;
|
|
891
|
+
border: 1px solid #d1d5db;
|
|
892
|
+
border-radius: 0.5rem;
|
|
893
|
+
font-size: 1rem;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
.search-input:focus {
|
|
897
|
+
outline: 2px solid #3b82f6;
|
|
898
|
+
outline-offset: 0;
|
|
899
|
+
border-color: #3b82f6;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
.search-button {
|
|
903
|
+
position: absolute;
|
|
904
|
+
right: 0.5rem;
|
|
905
|
+
padding: 0.25rem;
|
|
906
|
+
background: none;
|
|
907
|
+
border: none;
|
|
908
|
+
cursor: pointer;
|
|
909
|
+
color: #6b7280;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
.search-button:hover {
|
|
913
|
+
color: #111827;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
.sr-only {
|
|
917
|
+
position: absolute;
|
|
918
|
+
width: 1px;
|
|
919
|
+
height: 1px;
|
|
920
|
+
padding: 0;
|
|
921
|
+
margin: -1px;
|
|
922
|
+
overflow: hidden;
|
|
923
|
+
clip: rect(0, 0, 0, 0);
|
|
924
|
+
white-space: nowrap;
|
|
925
|
+
border-width: 0;
|
|
926
|
+
}
|
|
927
|
+
</style>
|
|
928
|
+
|
|
929
|
+
<!-- UX Notes:
|
|
930
|
+
- role="search" for landmark navigation
|
|
931
|
+
- Label (can be visually hidden)
|
|
932
|
+
- Clear icon to reset search (add with JS)
|
|
933
|
+
- Search icon button is labeled
|
|
934
|
+
- Consider adding autocomplete/suggestions
|
|
935
|
+
-->`;
|
|
936
|
+
break;
|
|
937
|
+
default:
|
|
938
|
+
example = `Pattern "${pattern}" example not available.
|
|
939
|
+
|
|
940
|
+
Available patterns:
|
|
941
|
+
- button
|
|
942
|
+
- modal/dialog
|
|
943
|
+
- form
|
|
944
|
+
- search
|
|
945
|
+
|
|
946
|
+
Use suggest_pattern tool to find appropriate pattern for your use case.`;
|
|
947
|
+
}
|
|
948
|
+
return {
|
|
949
|
+
content: [
|
|
950
|
+
{
|
|
951
|
+
type: "text",
|
|
952
|
+
text: example,
|
|
953
|
+
},
|
|
954
|
+
],
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
async function auditDesignSystem(args) {
|
|
958
|
+
const tokens = args.tokens;
|
|
959
|
+
const type = args.type || "all";
|
|
960
|
+
const issues = [];
|
|
961
|
+
const recommendations = [];
|
|
962
|
+
try {
|
|
963
|
+
// Try to parse as JSON
|
|
964
|
+
const parsed = JSON.parse(tokens);
|
|
965
|
+
// Check for common design token issues
|
|
966
|
+
if (type === "all" || type === "colors") {
|
|
967
|
+
if (parsed.colors || parsed.color) {
|
|
968
|
+
// Check for semantic naming
|
|
969
|
+
const colorKeys = Object.keys(parsed.colors || parsed.color || {});
|
|
970
|
+
const hasSemanticNames = colorKeys.some((k) => /^(primary|secondary|success|error|warning|info)/.test(k));
|
|
971
|
+
if (!hasSemanticNames) {
|
|
972
|
+
issues.push("⚠️ Colors lack semantic naming (primary, secondary, etc.)");
|
|
973
|
+
recommendations.push("Use semantic color names instead of generic names like 'blue' or 'red'");
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
if (type === "all" || type === "spacing") {
|
|
978
|
+
if (parsed.spacing || parsed.space) {
|
|
979
|
+
// Check for consistent scale
|
|
980
|
+
recommendations.push("✓ Spacing tokens found - verify they follow a consistent scale");
|
|
981
|
+
}
|
|
982
|
+
else {
|
|
983
|
+
issues.push("⚠️ No spacing tokens defined");
|
|
984
|
+
recommendations.push("Define spacing scale (base 4px or 8px)");
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
if (type === "all" || type === "typography") {
|
|
988
|
+
if (parsed.typography || parsed.fonts || parsed.fontSize) {
|
|
989
|
+
recommendations.push("✓ Typography tokens found");
|
|
990
|
+
}
|
|
991
|
+
else {
|
|
992
|
+
issues.push("⚠️ No typography tokens defined");
|
|
993
|
+
recommendations.push("Define font sizes, weights, and line heights");
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
const result = {
|
|
997
|
+
audit_type: type,
|
|
998
|
+
issues,
|
|
999
|
+
recommendations,
|
|
1000
|
+
best_practices: [
|
|
1001
|
+
"Use three-tier token structure: primitive → semantic → component",
|
|
1002
|
+
"Follow naming conventions consistently",
|
|
1003
|
+
"Use scales for sizing (spacing, typography)",
|
|
1004
|
+
"Support light/dark mode variants",
|
|
1005
|
+
],
|
|
1006
|
+
reference: "See ux://design-systems/tokens for complete guidelines",
|
|
1007
|
+
};
|
|
1008
|
+
return {
|
|
1009
|
+
content: [
|
|
1010
|
+
{
|
|
1011
|
+
type: "text",
|
|
1012
|
+
text: JSON.stringify(result, null, 2),
|
|
1013
|
+
},
|
|
1014
|
+
],
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
catch (error) {
|
|
1018
|
+
return {
|
|
1019
|
+
content: [
|
|
1020
|
+
{
|
|
1021
|
+
type: "text",
|
|
1022
|
+
text: JSON.stringify({
|
|
1023
|
+
error: "Failed to parse tokens as JSON",
|
|
1024
|
+
message: "Provide valid JSON format for design tokens",
|
|
1025
|
+
}),
|
|
1026
|
+
},
|
|
1027
|
+
],
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
async function checkContrast(args) {
|
|
1032
|
+
const fg = args.foreground;
|
|
1033
|
+
const bg = args.background;
|
|
1034
|
+
const level = args.level || "AA";
|
|
1035
|
+
const largeText = args.large_text;
|
|
1036
|
+
// Simple color parsing (hex only for now)
|
|
1037
|
+
const parseHex = (hex) => {
|
|
1038
|
+
const clean = hex.replace("#", "");
|
|
1039
|
+
return [
|
|
1040
|
+
parseInt(clean.substring(0, 2), 16),
|
|
1041
|
+
parseInt(clean.substring(2, 4), 16),
|
|
1042
|
+
parseInt(clean.substring(4, 6), 16),
|
|
1043
|
+
];
|
|
1044
|
+
};
|
|
1045
|
+
const relativeLuminance = (r, g, b) => {
|
|
1046
|
+
const [rs, gs, bs] = [r, g, b].map((c) => {
|
|
1047
|
+
const s = c / 255;
|
|
1048
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
1049
|
+
});
|
|
1050
|
+
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
|
|
1051
|
+
};
|
|
1052
|
+
try {
|
|
1053
|
+
const [r1, g1, b1] = parseHex(fg);
|
|
1054
|
+
const [r2, g2, b2] = parseHex(bg);
|
|
1055
|
+
const l1 = relativeLuminance(r1, g1, b1);
|
|
1056
|
+
const l2 = relativeLuminance(r2, g2, b2);
|
|
1057
|
+
const lighter = Math.max(l1, l2);
|
|
1058
|
+
const darker = Math.min(l1, l2);
|
|
1059
|
+
const ratio = (lighter + 0.05) / (darker + 0.05);
|
|
1060
|
+
// WCAG requirements
|
|
1061
|
+
const requirements = {
|
|
1062
|
+
AA: largeText ? 3.0 : 4.5,
|
|
1063
|
+
AAA: largeText ? 4.5 : 7.0,
|
|
1064
|
+
};
|
|
1065
|
+
const required = requirements[level];
|
|
1066
|
+
const passes = ratio >= required;
|
|
1067
|
+
const result = {
|
|
1068
|
+
foreground: fg,
|
|
1069
|
+
background: bg,
|
|
1070
|
+
contrast_ratio: ratio.toFixed(2) + ":1",
|
|
1071
|
+
wcag_level: level,
|
|
1072
|
+
large_text: largeText,
|
|
1073
|
+
required_ratio: required + ":1",
|
|
1074
|
+
passes: passes ? "✓ PASS" : "✗ FAIL",
|
|
1075
|
+
recommendation: passes
|
|
1076
|
+
? "Contrast ratio meets WCAG requirements"
|
|
1077
|
+
: `Increase contrast. Need at least ${required}:1, currently ${ratio.toFixed(2)}:1`,
|
|
1078
|
+
wcag_reference: "WCAG 1.4.3 (Contrast Minimum) and 1.4.6 (Contrast Enhanced)",
|
|
1079
|
+
};
|
|
1080
|
+
return {
|
|
1081
|
+
content: [
|
|
1082
|
+
{
|
|
1083
|
+
type: "text",
|
|
1084
|
+
text: JSON.stringify(result, null, 2),
|
|
1085
|
+
},
|
|
1086
|
+
],
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
catch (error) {
|
|
1090
|
+
return {
|
|
1091
|
+
content: [
|
|
1092
|
+
{
|
|
1093
|
+
type: "text",
|
|
1094
|
+
text: JSON.stringify({
|
|
1095
|
+
error: "Failed to parse colors",
|
|
1096
|
+
message: "Use hex color format (e.g., #3b82f6)",
|
|
1097
|
+
}),
|
|
1098
|
+
},
|
|
1099
|
+
],
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
async function checkResponsive(args) {
|
|
1104
|
+
const code = args.code;
|
|
1105
|
+
const checkType = args.check_type || "all";
|
|
1106
|
+
const issues = [];
|
|
1107
|
+
const recommendations = [];
|
|
1108
|
+
// Check viewport meta tag
|
|
1109
|
+
if (checkType === "all" || checkType === "viewport") {
|
|
1110
|
+
if (/<html/i.test(code)) {
|
|
1111
|
+
const hasViewport = /<meta[^>]*name=["']viewport["']/i.test(code);
|
|
1112
|
+
if (!hasViewport) {
|
|
1113
|
+
issues.push("❌ Missing viewport meta tag");
|
|
1114
|
+
recommendations.push("Add: <meta name='viewport' content='width=device-width, initial-scale=1'>");
|
|
1115
|
+
}
|
|
1116
|
+
else {
|
|
1117
|
+
const hasUserScalableNo = /user-scalable\s*=\s*["']?no["']?/i.test(code);
|
|
1118
|
+
const hasMaxScale = /maximum-scale\s*=\s*["']?1["']?/i.test(code);
|
|
1119
|
+
if (hasUserScalableNo || hasMaxScale) {
|
|
1120
|
+
issues.push("⚠️ Viewport prevents zoom (accessibility issue)");
|
|
1121
|
+
recommendations.push("Remove user-scalable=no and maximum-scale=1 to allow users to zoom");
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
// Check for mobile-first CSS
|
|
1127
|
+
if (checkType === "all" || checkType === "breakpoints") {
|
|
1128
|
+
const maxWidthQueries = (code.match(/@media[^{]*max-width/gi) || []).length;
|
|
1129
|
+
const minWidthQueries = (code.match(/@media[^{]*min-width/gi) || []).length;
|
|
1130
|
+
if (maxWidthQueries > minWidthQueries) {
|
|
1131
|
+
issues.push("⚠️ Desktop-first approach detected (more max-width than min-width queries)");
|
|
1132
|
+
recommendations.push("Consider mobile-first approach: base styles for mobile, min-width queries for larger screens");
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
// Check touch targets
|
|
1136
|
+
if (checkType === "all" || checkType === "touch-targets") {
|
|
1137
|
+
if (/<button/i.test(code) || /<a/i.test(code)) {
|
|
1138
|
+
recommendations.push("Ensure interactive elements are at least 44x44px (iOS) or 48x48px (Android)");
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
// Check responsive images
|
|
1142
|
+
if (checkType === "all" || checkType === "images") {
|
|
1143
|
+
const hasImg = /<img/i.test(code);
|
|
1144
|
+
if (hasImg) {
|
|
1145
|
+
const hasSrcset = /srcset=/i.test(code);
|
|
1146
|
+
const hasPicture = /<picture/i.test(code);
|
|
1147
|
+
if (!hasSrcset && !hasPicture) {
|
|
1148
|
+
issues.push("⚠️ Images without responsive sizing (srcset or picture element)");
|
|
1149
|
+
recommendations.push("Use srcset attribute or picture element for responsive images");
|
|
1150
|
+
}
|
|
1151
|
+
const hasLazyLoading = /loading=["']lazy["']/i.test(code);
|
|
1152
|
+
if (!hasLazyLoading) {
|
|
1153
|
+
recommendations.push("Consider adding loading='lazy' to below-the-fold images");
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
const result = {
|
|
1158
|
+
check_type: checkType,
|
|
1159
|
+
issues,
|
|
1160
|
+
recommendations,
|
|
1161
|
+
best_practices: [
|
|
1162
|
+
"Design for mobile first, then enhance for larger screens",
|
|
1163
|
+
"Use relative units (rem, em) instead of pixels",
|
|
1164
|
+
"Test on real devices, not just browser resize",
|
|
1165
|
+
"Minimum 44x44px touch targets",
|
|
1166
|
+
"Use responsive images with srcset",
|
|
1167
|
+
"Support landscape orientation"
|
|
1168
|
+
],
|
|
1169
|
+
reference: "See ux://responsive/design for complete guidelines"
|
|
1170
|
+
};
|
|
1171
|
+
return {
|
|
1172
|
+
content: [
|
|
1173
|
+
{
|
|
1174
|
+
type: "text",
|
|
1175
|
+
text: JSON.stringify(result, null, 2),
|
|
1176
|
+
},
|
|
1177
|
+
],
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
async function suggestErrorMessage(args) {
|
|
1181
|
+
const scenario = args.scenario.toLowerCase();
|
|
1182
|
+
const context = args.context;
|
|
1183
|
+
const technicalMessage = args.technical_message;
|
|
1184
|
+
const errorMessages = await loadKnowledge("error-messages.json");
|
|
1185
|
+
let suggestion = {
|
|
1186
|
+
scenario,
|
|
1187
|
+
message: "",
|
|
1188
|
+
action: "",
|
|
1189
|
+
tone_guidance: []
|
|
1190
|
+
};
|
|
1191
|
+
// Match scenario to error message library
|
|
1192
|
+
if (scenario.includes("email") || scenario.includes("invalid") && scenario.includes("format")) {
|
|
1193
|
+
suggestion = {
|
|
1194
|
+
scenario: "Invalid email format",
|
|
1195
|
+
message: "Please enter a valid email address (e.g., name@example.com)",
|
|
1196
|
+
action: "Check your email format and try again",
|
|
1197
|
+
accessibility: [
|
|
1198
|
+
"Use aria-invalid='true' on input",
|
|
1199
|
+
"Link error with aria-describedby",
|
|
1200
|
+
"Display error with role='alert'"
|
|
1201
|
+
],
|
|
1202
|
+
visual_placement: "Below the email input field",
|
|
1203
|
+
tone_guidance: ["Be helpful, not judgmental", "Provide example format", "Keep it concise"]
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
else if (scenario.includes("required")) {
|
|
1207
|
+
suggestion = {
|
|
1208
|
+
scenario: "Required field empty",
|
|
1209
|
+
message: "Please enter [field name]",
|
|
1210
|
+
action: "Fill in the required information",
|
|
1211
|
+
accessibility: [
|
|
1212
|
+
"Mark with aria-required='true'",
|
|
1213
|
+
"Include asterisk (*) with aria-label='required'",
|
|
1214
|
+
"Announce error to screen readers"
|
|
1215
|
+
],
|
|
1216
|
+
visual: "Red border + error icon + text message",
|
|
1217
|
+
tone_guidance: ["Be clear about what's needed", "Use 'please' to be polite"]
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
else if (scenario.includes("password")) {
|
|
1221
|
+
suggestion = {
|
|
1222
|
+
scenario: "Weak password",
|
|
1223
|
+
message: "Password must include at least one uppercase letter, one number, and one special character",
|
|
1224
|
+
action: "Create a stronger password",
|
|
1225
|
+
progressive: "Show requirements checklist that updates as user types",
|
|
1226
|
+
accessibility: ["Announce each requirement met", "Use aria-live for updates"],
|
|
1227
|
+
tone_guidance: ["Be encouraging, not critical", "Show progress", "Explain why (security)"]
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
else if (scenario.includes("payment") || scenario.includes("declined")) {
|
|
1231
|
+
suggestion = {
|
|
1232
|
+
scenario: "Payment failed",
|
|
1233
|
+
message: "Your payment couldn't be processed.",
|
|
1234
|
+
reasons: [
|
|
1235
|
+
"The card details might be incorrect",
|
|
1236
|
+
"There might be insufficient funds",
|
|
1237
|
+
"Your bank might be blocking the transaction"
|
|
1238
|
+
],
|
|
1239
|
+
action: "Please check your payment details or try a different payment method",
|
|
1240
|
+
support: "Contact your bank if the problem persists",
|
|
1241
|
+
tone_guidance: ["Don't blame the user", "Provide possible reasons", "Offer alternatives", "Be reassuring"]
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
else if (scenario.includes("file") && (scenario.includes("large") || scenario.includes("size"))) {
|
|
1245
|
+
suggestion = {
|
|
1246
|
+
scenario: "File too large",
|
|
1247
|
+
message: "This file is too large. Maximum file size is X MB.",
|
|
1248
|
+
action: "Please choose a smaller file or compress your image",
|
|
1249
|
+
helpful: "Show current file size and limit",
|
|
1250
|
+
tone_guidance: ["Be specific about the limit", "Suggest solutions", "Don't lose other form data"]
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
else if (scenario.includes("offline") || scenario.includes("network") || scenario.includes("connection")) {
|
|
1254
|
+
suggestion = {
|
|
1255
|
+
scenario: "Network error",
|
|
1256
|
+
message: "You appear to be offline. Please check your internet connection and try again.",
|
|
1257
|
+
action: "Automatically retry when connection restored",
|
|
1258
|
+
visual: "Show connection status indicator",
|
|
1259
|
+
tone_guidance: ["Explain the problem clearly", "Provide reassurance", "Auto-retry when possible"]
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
else {
|
|
1263
|
+
suggestion = {
|
|
1264
|
+
scenario: scenario,
|
|
1265
|
+
message: "Please provide more specific scenario",
|
|
1266
|
+
available_categories: Object.keys(errorMessages.categories),
|
|
1267
|
+
tip: "Try scenarios like: 'invalid email', 'required field', 'password weak', 'payment failed', 'file too large', 'network error'",
|
|
1268
|
+
reference: "See ux://content/error-messages for complete library"
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
if (technicalMessage) {
|
|
1272
|
+
suggestion.technical_message = technicalMessage;
|
|
1273
|
+
suggestion.translation_tip = "Avoid exposing technical details to users. Translate to friendly language.";
|
|
1274
|
+
}
|
|
1275
|
+
if (context) {
|
|
1276
|
+
suggestion.context = context;
|
|
1277
|
+
}
|
|
1278
|
+
suggestion.general_principles = errorMessages.principles.good_error_messages;
|
|
1279
|
+
return {
|
|
1280
|
+
content: [
|
|
1281
|
+
{
|
|
1282
|
+
type: "text",
|
|
1283
|
+
text: JSON.stringify(suggestion, null, 2),
|
|
1284
|
+
},
|
|
1285
|
+
],
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
async function analyzePerformance(args) {
|
|
1289
|
+
const code = args.code;
|
|
1290
|
+
const checkType = args.check_type || "all";
|
|
1291
|
+
const issues = [];
|
|
1292
|
+
const recommendations = [];
|
|
1293
|
+
// Check images
|
|
1294
|
+
if (checkType === "all" || checkType === "images") {
|
|
1295
|
+
if (/<img/i.test(code)) {
|
|
1296
|
+
const hasWidthHeight = /<img[^>]*(width|height)=/i.test(code);
|
|
1297
|
+
if (!hasWidthHeight) {
|
|
1298
|
+
issues.push("⚠️ Images without width/height attributes (causes CLS)");
|
|
1299
|
+
recommendations.push("Add width and height to prevent Cumulative Layout Shift");
|
|
1300
|
+
}
|
|
1301
|
+
const hasLazyLoading = /loading=["']lazy["']/i.test(code);
|
|
1302
|
+
if (!hasLazyLoading) {
|
|
1303
|
+
recommendations.push("Consider adding loading='lazy' to below-fold images");
|
|
1304
|
+
}
|
|
1305
|
+
const hasModernFormat = /(\.webp|\.avif)/i.test(code);
|
|
1306
|
+
if (!hasModernFormat) {
|
|
1307
|
+
recommendations.push("Use modern image formats (WebP, AVIF) for better compression");
|
|
1308
|
+
}
|
|
1309
|
+
const hasSrcset = /srcset=/i.test(code);
|
|
1310
|
+
if (!hasSrcset) {
|
|
1311
|
+
recommendations.push("Use srcset for responsive images");
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
// Check CSS
|
|
1316
|
+
if (checkType === "all" || checkType === "css") {
|
|
1317
|
+
const hasBlockingCSS = /<link[^>]*rel=["']stylesheet["'][^>]*>/i.test(code);
|
|
1318
|
+
if (hasBlockingCSS) {
|
|
1319
|
+
recommendations.push("Consider inlining critical CSS and deferring non-critical styles");
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
// Check JavaScript
|
|
1323
|
+
if (checkType === "all" || checkType === "javascript") {
|
|
1324
|
+
const hasBlockingJS = /<script(?![^>]*defer)(?![^>]*async)[^>]*src=/i.test(code);
|
|
1325
|
+
if (hasBlockingJS) {
|
|
1326
|
+
issues.push("⚠️ Render-blocking JavaScript detected");
|
|
1327
|
+
recommendations.push("Add defer or async attribute to <script> tags");
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
// Check resource loading
|
|
1331
|
+
if (checkType === "all" || checkType === "loading") {
|
|
1332
|
+
const hasPreconnect = /<link[^>]*rel=["']preconnect["']/i.test(code);
|
|
1333
|
+
const hasThirdParty = /(fonts\.googleapis|cdnjs|cdn\.)/i.test(code);
|
|
1334
|
+
if (hasThirdParty && !hasPreconnect) {
|
|
1335
|
+
recommendations.push("Add <link rel='preconnect'> for third-party resources to improve LCP");
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
const result = {
|
|
1339
|
+
check_type: checkType,
|
|
1340
|
+
issues,
|
|
1341
|
+
recommendations,
|
|
1342
|
+
core_web_vitals: {
|
|
1343
|
+
LCP: "Target: ≤ 2.5s - Optimize images, preload critical resources",
|
|
1344
|
+
INP: "Target: ≤ 200ms - Minimize JavaScript, use code splitting",
|
|
1345
|
+
CLS: "Target: ≤ 0.1 - Add dimensions to images/embeds, avoid content shifts"
|
|
1346
|
+
},
|
|
1347
|
+
quick_wins: [
|
|
1348
|
+
"Add width/height to images",
|
|
1349
|
+
"Use defer/async on scripts",
|
|
1350
|
+
"Enable gzip/brotli compression",
|
|
1351
|
+
"Lazy load below-fold images",
|
|
1352
|
+
"Use modern image formats (WebP, AVIF)"
|
|
1353
|
+
],
|
|
1354
|
+
reference: "See ux://performance/optimization for complete guidelines"
|
|
1355
|
+
};
|
|
1356
|
+
return {
|
|
1357
|
+
content: [
|
|
1358
|
+
{
|
|
1359
|
+
type: "text",
|
|
1360
|
+
text: JSON.stringify(result, null, 2),
|
|
1361
|
+
},
|
|
1362
|
+
],
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
async function checkSEO(args) {
|
|
1366
|
+
const html = args.html;
|
|
1367
|
+
const url = args.url;
|
|
1368
|
+
const issues = [];
|
|
1369
|
+
const recommendations = [];
|
|
1370
|
+
const found = [];
|
|
1371
|
+
// Check title
|
|
1372
|
+
const titleMatch = /<title[^>]*>([^<]+)<\/title>/i.exec(html);
|
|
1373
|
+
if (!titleMatch) {
|
|
1374
|
+
issues.push("❌ Missing <title> tag");
|
|
1375
|
+
}
|
|
1376
|
+
else {
|
|
1377
|
+
const titleLength = titleMatch[1].length;
|
|
1378
|
+
found.push(`Title: "${titleMatch[1]}" (${titleLength} chars)`);
|
|
1379
|
+
if (titleLength < 30) {
|
|
1380
|
+
issues.push("⚠️ Title too short (< 30 characters)");
|
|
1381
|
+
}
|
|
1382
|
+
else if (titleLength > 60) {
|
|
1383
|
+
issues.push("⚠️ Title too long (> 60 characters, may be truncated)");
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
// Check meta description
|
|
1387
|
+
const descMatch = /<meta[^>]*name=["']description["'][^>]*content=["']([^"']+)["']/i.exec(html);
|
|
1388
|
+
if (!descMatch) {
|
|
1389
|
+
issues.push("❌ Missing meta description");
|
|
1390
|
+
}
|
|
1391
|
+
else {
|
|
1392
|
+
const descLength = descMatch[1].length;
|
|
1393
|
+
found.push(`Meta description: ${descLength} chars`);
|
|
1394
|
+
if (descLength < 120) {
|
|
1395
|
+
issues.push("⚠️ Meta description too short (< 120 characters)");
|
|
1396
|
+
}
|
|
1397
|
+
else if (descLength > 160) {
|
|
1398
|
+
issues.push("⚠️ Meta description too long (> 160 characters)");
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
// Check canonical
|
|
1402
|
+
if (/<link[^>]*rel=["']canonical["']/i.test(html)) {
|
|
1403
|
+
found.push("Canonical tag present");
|
|
1404
|
+
}
|
|
1405
|
+
else {
|
|
1406
|
+
recommendations.push("Add canonical tag to prevent duplicate content issues");
|
|
1407
|
+
}
|
|
1408
|
+
// Check Open Graph
|
|
1409
|
+
const hasOG = /<meta[^>]*property=["']og:/i.test(html);
|
|
1410
|
+
if (hasOG) {
|
|
1411
|
+
found.push("Open Graph tags present");
|
|
1412
|
+
}
|
|
1413
|
+
else {
|
|
1414
|
+
recommendations.push("Add Open Graph tags for better social media sharing");
|
|
1415
|
+
}
|
|
1416
|
+
// Check Twitter Cards
|
|
1417
|
+
if (/<meta[^>]*name=["']twitter:card["']/i.test(html)) {
|
|
1418
|
+
found.push("Twitter Card tags present");
|
|
1419
|
+
}
|
|
1420
|
+
else {
|
|
1421
|
+
recommendations.push("Add Twitter Card tags for better Twitter sharing");
|
|
1422
|
+
}
|
|
1423
|
+
// Check viewport
|
|
1424
|
+
if (!/<meta[^>]*name=["']viewport["']/i.test(html)) {
|
|
1425
|
+
issues.push("❌ Missing viewport meta tag (affects mobile SEO)");
|
|
1426
|
+
}
|
|
1427
|
+
// Check structured data
|
|
1428
|
+
if (/<script[^>]*type=["']application\/ld\+json["']/i.test(html)) {
|
|
1429
|
+
found.push("JSON-LD structured data present");
|
|
1430
|
+
}
|
|
1431
|
+
else {
|
|
1432
|
+
recommendations.push("Add structured data (JSON-LD) for rich snippets");
|
|
1433
|
+
}
|
|
1434
|
+
// Check headings
|
|
1435
|
+
const h1Count = (html.match(/<h1[^>]*>/gi) || []).length;
|
|
1436
|
+
if (h1Count === 0) {
|
|
1437
|
+
issues.push("❌ No H1 heading found");
|
|
1438
|
+
}
|
|
1439
|
+
else if (h1Count > 1) {
|
|
1440
|
+
issues.push(`⚠️ Multiple H1 headings (${h1Count}) - use only one per page`);
|
|
1441
|
+
}
|
|
1442
|
+
else {
|
|
1443
|
+
found.push("Single H1 heading (good)");
|
|
1444
|
+
}
|
|
1445
|
+
// Check alt text
|
|
1446
|
+
const imgCount = (html.match(/<img/gi) || []).length;
|
|
1447
|
+
const altCount = (html.match(/<img[^>]*alt=/gi) || []).length;
|
|
1448
|
+
if (imgCount > 0 && altCount < imgCount) {
|
|
1449
|
+
issues.push(`⚠️ ${imgCount - altCount} image(s) missing alt text`);
|
|
1450
|
+
}
|
|
1451
|
+
const result = {
|
|
1452
|
+
url: url || "Not specified",
|
|
1453
|
+
found,
|
|
1454
|
+
issues,
|
|
1455
|
+
recommendations,
|
|
1456
|
+
seo_checklist: [
|
|
1457
|
+
"Title: 50-60 characters with primary keyword",
|
|
1458
|
+
"Meta description: 120-160 characters",
|
|
1459
|
+
"Canonical tag for duplicate content",
|
|
1460
|
+
"Open Graph tags for social sharing",
|
|
1461
|
+
"Structured data (JSON-LD) for rich snippets",
|
|
1462
|
+
"Single H1 per page",
|
|
1463
|
+
"Alt text on all images",
|
|
1464
|
+
"Mobile-friendly (viewport meta tag)"
|
|
1465
|
+
],
|
|
1466
|
+
reference: "See ux://seo/guidelines for complete guide"
|
|
1467
|
+
};
|
|
1468
|
+
return {
|
|
1469
|
+
content: [
|
|
1470
|
+
{
|
|
1471
|
+
type: "text",
|
|
1472
|
+
text: JSON.stringify(result, null, 2),
|
|
1473
|
+
},
|
|
1474
|
+
],
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
async function suggestAnimation(args) {
|
|
1478
|
+
const interaction = args.interaction.toLowerCase();
|
|
1479
|
+
const context = args.context;
|
|
1480
|
+
let suggestion = {
|
|
1481
|
+
interaction,
|
|
1482
|
+
context: context || "Not specified"
|
|
1483
|
+
};
|
|
1484
|
+
// Match interaction to animation patterns
|
|
1485
|
+
if (interaction.includes("button") && (interaction.includes("click") || interaction.includes("press"))) {
|
|
1486
|
+
suggestion = {
|
|
1487
|
+
...suggestion,
|
|
1488
|
+
animation_type: "Micro-interaction",
|
|
1489
|
+
sequence: [
|
|
1490
|
+
"Scale down to 0.95 on press",
|
|
1491
|
+
"Scale back to 1.0 on release",
|
|
1492
|
+
"Optional: Ripple effect from click point"
|
|
1493
|
+
],
|
|
1494
|
+
duration: "100-150ms total",
|
|
1495
|
+
easing: "ease-out",
|
|
1496
|
+
css_example: ".button {\n transition: transform 100ms ease-out;\n}\n.button:active {\n transform: scale(0.95);\n}",
|
|
1497
|
+
accessibility: "Respect prefers-reduced-motion"
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
else if (interaction.includes("modal") || interaction.includes("dialog")) {
|
|
1501
|
+
const isOpen = interaction.includes("open") || interaction.includes("show");
|
|
1502
|
+
suggestion = {
|
|
1503
|
+
...suggestion,
|
|
1504
|
+
animation_type: "Modal transition",
|
|
1505
|
+
backdrop: "Fade in from transparent to semi-opaque (200ms)",
|
|
1506
|
+
content: isOpen ? "Scale from 0.9 to 1.0 + fade in (250-300ms)" : "Scale to 0.9 + fade out (200ms)",
|
|
1507
|
+
timing: isOpen ? "Backdrop first, then content with 100ms delay" : "Content first, then backdrop",
|
|
1508
|
+
duration: isOpen ? "300ms enter" : "200ms exit",
|
|
1509
|
+
easing: "ease-out",
|
|
1510
|
+
css_example: "@keyframes modalEnter {\n from { opacity: 0; transform: scale(0.9); }\n to { opacity: 1; transform: scale(1); }\n}\n\n.modal {\n animation: modalEnter 300ms ease-out;\n}"
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
else if (interaction.includes("list") || interaction.includes("item")) {
|
|
1514
|
+
suggestion = {
|
|
1515
|
+
...suggestion,
|
|
1516
|
+
animation_type: "List animation",
|
|
1517
|
+
pattern: "Staggered entrance",
|
|
1518
|
+
timing: "50-100ms delay between items",
|
|
1519
|
+
duration: "200-250ms per item",
|
|
1520
|
+
easing: "ease-out",
|
|
1521
|
+
limit: "Animate first 10-15 items only (rest appear instantly)",
|
|
1522
|
+
css_example: ".list-item {\n opacity: 0;\n transform: translateY(20px);\n animation: itemEnter 250ms ease-out forwards;\n}\n\n.list-item:nth-child(1) { animation-delay: 0ms; }\n.list-item:nth-child(2) { animation-delay: 50ms; }\n.list-item:nth-child(3) { animation-delay: 100ms; }\n\n@keyframes itemEnter {\n to { opacity: 1; transform: translateY(0); }\n}"
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
else if (interaction.includes("page") || interaction.includes("route") || interaction.includes("transition")) {
|
|
1526
|
+
suggestion = {
|
|
1527
|
+
...suggestion,
|
|
1528
|
+
animation_type: "Page transition",
|
|
1529
|
+
options: [
|
|
1530
|
+
{
|
|
1531
|
+
type: "Fade",
|
|
1532
|
+
use: "Unrelated pages",
|
|
1533
|
+
duration: "200-300ms",
|
|
1534
|
+
implementation: "Cross-fade old and new content"
|
|
1535
|
+
},
|
|
1536
|
+
{
|
|
1537
|
+
type: "Slide",
|
|
1538
|
+
use: "Forward/back navigation",
|
|
1539
|
+
duration: "300-400ms",
|
|
1540
|
+
direction: "Left for forward, right for back"
|
|
1541
|
+
}
|
|
1542
|
+
],
|
|
1543
|
+
recommendation: "Use fade for simplicity, slide for directional navigation",
|
|
1544
|
+
duration: "300-400ms",
|
|
1545
|
+
easing: "ease-in-out"
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
else if (interaction.includes("toast") || interaction.includes("notification")) {
|
|
1549
|
+
suggestion = {
|
|
1550
|
+
...suggestion,
|
|
1551
|
+
animation_type: "Toast notification",
|
|
1552
|
+
enter: "Slide in from top/bottom + fade (300ms)",
|
|
1553
|
+
exit: "Fade out + slight slide (200ms)",
|
|
1554
|
+
auto_dismiss: "After 4-6 seconds",
|
|
1555
|
+
easing: "ease-out for enter, ease-in for exit",
|
|
1556
|
+
placement: "Top-right or bottom-left typical",
|
|
1557
|
+
css_example: "@keyframes toastEnter {\n from {\n opacity: 0;\n transform: translateY(-100%);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n.toast {\n animation: toastEnter 300ms ease-out;\n}"
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
else if (interaction.includes("hover")) {
|
|
1561
|
+
suggestion = {
|
|
1562
|
+
...suggestion,
|
|
1563
|
+
animation_type: "Hover state",
|
|
1564
|
+
effects: [
|
|
1565
|
+
"Subtle scale (1.0 → 1.05)",
|
|
1566
|
+
"Lift effect (add shadow, translate up slightly)",
|
|
1567
|
+
"Color transition",
|
|
1568
|
+
"Underline grow"
|
|
1569
|
+
],
|
|
1570
|
+
duration: "150-200ms",
|
|
1571
|
+
easing: "ease-out",
|
|
1572
|
+
note: "Keep subtle, instant feedback is key",
|
|
1573
|
+
css_example: ".card {\n transition: transform 150ms ease-out, box-shadow 150ms ease-out;\n}\n\n.card:hover {\n transform: translateY(-2px);\n box-shadow: 0 10px 20px rgba(0,0,0,0.1);\n}"
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
else {
|
|
1577
|
+
suggestion = {
|
|
1578
|
+
...suggestion,
|
|
1579
|
+
message: "No specific match found for this interaction",
|
|
1580
|
+
general_guidelines: {
|
|
1581
|
+
duration: "200-300ms for most UI animations",
|
|
1582
|
+
easing: "ease-out for enter, ease-in for exit, ease-in-out for movement",
|
|
1583
|
+
properties: "Animate transform and opacity only (GPU accelerated)",
|
|
1584
|
+
accessibility: "Always respect prefers-reduced-motion"
|
|
1585
|
+
},
|
|
1586
|
+
reference: "See ux://animation/motion for complete animation library"
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
suggestion.performance_tip = "Use transform and opacity only for best performance (60fps)";
|
|
1590
|
+
suggestion.accessibility_requirement = "Implement prefers-reduced-motion to disable/reduce animations";
|
|
1591
|
+
suggestion.reference = "See ux://animation/motion for complete motion design guide";
|
|
1592
|
+
return {
|
|
1593
|
+
content: [
|
|
1594
|
+
{
|
|
1595
|
+
type: "text",
|
|
1596
|
+
text: JSON.stringify(suggestion, null, 2),
|
|
1597
|
+
},
|
|
1598
|
+
],
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
// ========================================
|
|
1602
|
+
// PROMPTS - Pre-configured UX Reviews
|
|
1603
|
+
// ========================================
|
|
1604
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
1605
|
+
return {
|
|
1606
|
+
prompts: [
|
|
1607
|
+
{
|
|
1608
|
+
name: "accessibility_review",
|
|
1609
|
+
description: "Comprehensive accessibility review following WCAG 2.1 AA guidelines",
|
|
1610
|
+
arguments: [
|
|
1611
|
+
{
|
|
1612
|
+
name: "component",
|
|
1613
|
+
description: "Component name or description to review",
|
|
1614
|
+
required: true,
|
|
1615
|
+
},
|
|
1616
|
+
{
|
|
1617
|
+
name: "code",
|
|
1618
|
+
description: "Code to analyze (optional)",
|
|
1619
|
+
required: false,
|
|
1620
|
+
},
|
|
1621
|
+
],
|
|
1622
|
+
},
|
|
1623
|
+
{
|
|
1624
|
+
name: "usability_audit",
|
|
1625
|
+
description: "Full usability audit using Nielsen's 10 heuristics",
|
|
1626
|
+
arguments: [
|
|
1627
|
+
{
|
|
1628
|
+
name: "interface",
|
|
1629
|
+
description: "Interface or feature to audit",
|
|
1630
|
+
required: true,
|
|
1631
|
+
},
|
|
1632
|
+
],
|
|
1633
|
+
},
|
|
1634
|
+
{
|
|
1635
|
+
name: "design_system_setup",
|
|
1636
|
+
description: "Guide for setting up a new design system with tokens and components",
|
|
1637
|
+
arguments: [
|
|
1638
|
+
{
|
|
1639
|
+
name: "project_type",
|
|
1640
|
+
description: "Type of project (web app, mobile, etc.)",
|
|
1641
|
+
required: false,
|
|
1642
|
+
},
|
|
1643
|
+
],
|
|
1644
|
+
},
|
|
1645
|
+
],
|
|
1646
|
+
};
|
|
1647
|
+
});
|
|
1648
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
1649
|
+
const { name, arguments: args } = request.params;
|
|
1650
|
+
switch (name) {
|
|
1651
|
+
case "accessibility_review":
|
|
1652
|
+
return {
|
|
1653
|
+
messages: [
|
|
1654
|
+
{
|
|
1655
|
+
role: "user",
|
|
1656
|
+
content: {
|
|
1657
|
+
type: "text",
|
|
1658
|
+
text: `Please perform a comprehensive accessibility review for: ${args?.component}
|
|
1659
|
+
|
|
1660
|
+
${args?.code ? `Code to analyze:\n\`\`\`\n${args.code}\n\`\`\`\n` : ""}
|
|
1661
|
+
|
|
1662
|
+
Review checklist:
|
|
1663
|
+
1. Use the analyze_accessibility tool to check for WCAG violations
|
|
1664
|
+
2. Check keyboard navigation (tab order, focus management, keyboard shortcuts)
|
|
1665
|
+
3. Verify screen reader compatibility (ARIA labels, roles, live regions)
|
|
1666
|
+
4. Test color contrast with check_contrast tool
|
|
1667
|
+
5. Review semantic HTML structure
|
|
1668
|
+
6. Check for proper heading hierarchy
|
|
1669
|
+
7. Verify form labels and error handling
|
|
1670
|
+
8. Test with keyboard only (no mouse)
|
|
1671
|
+
|
|
1672
|
+
Provide specific, actionable recommendations for each issue found.
|
|
1673
|
+
|
|
1674
|
+
Reference the WCAG guidelines resource: ux://accessibility/wcag for detailed standards.`,
|
|
1675
|
+
},
|
|
1676
|
+
},
|
|
1677
|
+
],
|
|
1678
|
+
};
|
|
1679
|
+
case "usability_audit":
|
|
1680
|
+
return {
|
|
1681
|
+
messages: [
|
|
1682
|
+
{
|
|
1683
|
+
role: "user",
|
|
1684
|
+
content: {
|
|
1685
|
+
type: "text",
|
|
1686
|
+
text: `Please conduct a full usability audit for: ${args?.interface}
|
|
1687
|
+
|
|
1688
|
+
Evaluate against all 10 of Nielsen's Usability Heuristics:
|
|
1689
|
+
|
|
1690
|
+
1. Visibility of System Status
|
|
1691
|
+
2. Match Between System and Real World
|
|
1692
|
+
3. User Control and Freedom
|
|
1693
|
+
4. Consistency and Standards
|
|
1694
|
+
5. Error Prevention
|
|
1695
|
+
6. Recognition Rather than Recall
|
|
1696
|
+
7. Flexibility and Efficiency of Use
|
|
1697
|
+
8. Aesthetic and Minimalist Design
|
|
1698
|
+
9. Help Users Recognize, Diagnose, and Recover from Errors
|
|
1699
|
+
10. Help and Documentation
|
|
1700
|
+
|
|
1701
|
+
For each heuristic:
|
|
1702
|
+
- Rate severity (0-4): 0=no problem, 4=usability catastrophe
|
|
1703
|
+
- Describe specific violations
|
|
1704
|
+
- Provide actionable recommendations
|
|
1705
|
+
|
|
1706
|
+
Use the review_usability tool to get detailed heuristic descriptions.
|
|
1707
|
+
|
|
1708
|
+
Reference: ux://usability/nielsen-heuristics`,
|
|
1709
|
+
},
|
|
1710
|
+
},
|
|
1711
|
+
],
|
|
1712
|
+
};
|
|
1713
|
+
case "design_system_setup":
|
|
1714
|
+
return {
|
|
1715
|
+
messages: [
|
|
1716
|
+
{
|
|
1717
|
+
role: "user",
|
|
1718
|
+
content: {
|
|
1719
|
+
type: "text",
|
|
1720
|
+
text: `Please help me set up a design system${args?.project_type ? ` for a ${args.project_type}` : ""}.
|
|
1721
|
+
|
|
1722
|
+
Guide me through:
|
|
1723
|
+
|
|
1724
|
+
1. **Design Tokens Setup**
|
|
1725
|
+
- Color palette (primary, secondary, semantic colors)
|
|
1726
|
+
- Spacing scale (base unit: 4px or 8px)
|
|
1727
|
+
- Typography scale (font families, sizes, weights)
|
|
1728
|
+
- Border radius values
|
|
1729
|
+
- Shadow levels
|
|
1730
|
+
- Animation durations
|
|
1731
|
+
|
|
1732
|
+
2. **Component Architecture**
|
|
1733
|
+
- Atomic design structure (atoms → molecules → organisms)
|
|
1734
|
+
- Component API design principles
|
|
1735
|
+
- Naming conventions
|
|
1736
|
+
- Composition patterns
|
|
1737
|
+
|
|
1738
|
+
3. **Implementation Checklist**
|
|
1739
|
+
- File structure recommendation
|
|
1740
|
+
- Tools and libraries to consider
|
|
1741
|
+
- Documentation approach
|
|
1742
|
+
- Testing strategy
|
|
1743
|
+
|
|
1744
|
+
Reference the design system resource: ux://design-systems/tokens
|
|
1745
|
+
|
|
1746
|
+
Provide code examples for token definition and a starter component.`,
|
|
1747
|
+
},
|
|
1748
|
+
},
|
|
1749
|
+
],
|
|
1750
|
+
};
|
|
1751
|
+
default:
|
|
1752
|
+
throw new Error(`Unknown prompt: ${name}`);
|
|
1753
|
+
}
|
|
1754
|
+
});
|
|
1755
|
+
// ========================================
|
|
1756
|
+
// START SERVER
|
|
1757
|
+
// ========================================
|
|
1758
|
+
async function main() {
|
|
1759
|
+
const transport = new StdioServerTransport();
|
|
1760
|
+
await server.connect(transport);
|
|
1761
|
+
console.error("UX MCP Server running on stdio");
|
|
1762
|
+
}
|
|
1763
|
+
main().catch((error) => {
|
|
1764
|
+
console.error("Fatal error in main():", error);
|
|
1765
|
+
process.exit(1);
|
|
1766
|
+
});
|