@salesforce/b2c-dx-mcp 0.4.4 → 0.4.6
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 +82 -370
- package/content/pwav3/components.md +400 -0
- package/content/pwav3/config.md +124 -0
- package/content/pwav3/data-fetching.md +213 -0
- package/content/pwav3/extensibility.md +167 -0
- package/content/pwav3/i18n.md +214 -0
- package/content/pwav3/quick-reference.md +169 -0
- package/content/pwav3/routing.md +107 -0
- package/content/pwav3/state-management.md +193 -0
- package/content/pwav3/styling.md +248 -0
- package/content/pwav3/testing.md +124 -0
- package/content/site-theming/theming-accessibility.md +126 -0
- package/content/site-theming/theming-questions.md +208 -0
- package/content/site-theming/theming-validation.md +174 -0
- package/dist/commands/mcp.d.ts +3 -3
- package/dist/commands/mcp.js +7 -7
- package/dist/registry.js +1 -1
- package/dist/services.d.ts +15 -15
- package/dist/services.js +21 -14
- package/dist/tools/adapter.d.ts +2 -2
- package/dist/tools/adapter.js +2 -2
- package/dist/tools/cartridges/index.js +1 -6
- package/dist/tools/index.d.ts +1 -4
- package/dist/tools/index.js +1 -4
- package/dist/tools/mrt/index.js +4 -9
- package/dist/tools/pwav3/index.d.ts +12 -3
- package/dist/tools/pwav3/index.js +5 -63
- package/dist/tools/pwav3/pwa-kit-development-guidelines.d.ts +9 -0
- package/dist/tools/pwav3/pwa-kit-development-guidelines.js +151 -0
- package/dist/tools/scapi/index.d.ts +1 -1
- package/dist/tools/scapi/index.js +6 -1
- package/dist/tools/scapi/scapi-custom-api-scaffold.d.ts +60 -0
- package/dist/tools/scapi/scapi-custom-api-scaffold.js +175 -0
- package/dist/tools/storefrontnext/figma/figma-to-component/figma-url-parser.d.ts +24 -0
- package/dist/tools/storefrontnext/figma/figma-to-component/figma-url-parser.js +53 -0
- package/dist/tools/storefrontnext/figma/figma-to-component/index.d.ts +42 -0
- package/dist/tools/storefrontnext/figma/figma-to-component/index.js +325 -0
- package/dist/tools/storefrontnext/figma/generate-component/decision.d.ts +40 -0
- package/dist/tools/storefrontnext/figma/generate-component/decision.js +312 -0
- package/dist/tools/storefrontnext/figma/generate-component/formatter.d.ts +9 -0
- package/dist/tools/storefrontnext/figma/generate-component/formatter.js +92 -0
- package/dist/tools/storefrontnext/figma/generate-component/index.d.ts +114 -0
- package/dist/tools/storefrontnext/figma/generate-component/index.js +98 -0
- package/dist/tools/storefrontnext/figma/map-tokens/css-parser.d.ts +71 -0
- package/dist/tools/storefrontnext/figma/map-tokens/css-parser.js +260 -0
- package/dist/tools/storefrontnext/figma/map-tokens/index.d.ts +61 -0
- package/dist/tools/storefrontnext/figma/map-tokens/index.js +234 -0
- package/dist/tools/storefrontnext/figma/map-tokens/token-matcher.d.ts +65 -0
- package/dist/tools/storefrontnext/figma/map-tokens/token-matcher.js +268 -0
- package/dist/tools/storefrontnext/index.d.ts +17 -0
- package/dist/tools/storefrontnext/index.js +10 -60
- package/dist/tools/storefrontnext/page-designer-decorator/analyzer.js +15 -0
- package/dist/tools/storefrontnext/page-designer-decorator/index.js +3 -3
- package/dist/tools/storefrontnext/{developer-guidelines.js → sfnext-development-guidelines.js} +3 -3
- package/dist/tools/storefrontnext/site-theming/color-contrast.d.ts +92 -0
- package/dist/tools/storefrontnext/site-theming/color-contrast.js +186 -0
- package/dist/tools/storefrontnext/site-theming/color-mapping.d.ts +16 -0
- package/dist/tools/storefrontnext/site-theming/color-mapping.js +131 -0
- package/dist/tools/storefrontnext/site-theming/guidance-merger.d.ts +11 -0
- package/dist/tools/storefrontnext/site-theming/guidance-merger.js +78 -0
- package/dist/tools/storefrontnext/site-theming/index.d.ts +14 -0
- package/dist/tools/storefrontnext/site-theming/index.js +122 -0
- package/dist/tools/storefrontnext/site-theming/response-builder.d.ts +16 -0
- package/dist/tools/storefrontnext/site-theming/response-builder.js +316 -0
- package/dist/tools/storefrontnext/site-theming/theming-store.d.ts +62 -0
- package/dist/tools/storefrontnext/site-theming/theming-store.js +410 -0
- package/dist/tools/storefrontnext/site-theming/types.d.ts +35 -0
- package/dist/tools/storefrontnext/site-theming/types.js +7 -0
- package/oclif.manifest.json +8 -5
- package/package.json +9 -6
- /package/content/{auth.md → sfnext/auth.md} +0 -0
- /package/content/{components.md → sfnext/components.md} +0 -0
- /package/content/{config.md → sfnext/config.md} +0 -0
- /package/content/{data-fetching.md → sfnext/data-fetching.md} +0 -0
- /package/content/{extensions.md → sfnext/extensions.md} +0 -0
- /package/content/{i18n.md → sfnext/i18n.md} +0 -0
- /package/content/{page-designer.md → sfnext/page-designer.md} +0 -0
- /package/content/{performance.md → sfnext/performance.md} +0 -0
- /package/content/{pitfalls.md → sfnext/pitfalls.md} +0 -0
- /package/content/{quick-reference.md → sfnext/quick-reference.md} +0 -0
- /package/content/{state-management.md → sfnext/state-management.md} +0 -0
- /package/content/{styling.md → sfnext/styling.md} +0 -0
- /package/content/{testing.md → sfnext/testing.md} +0 -0
- /package/dist/tools/storefrontnext/{developer-guidelines.d.ts → sfnext-development-guidelines.d.ts} +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map tokens tool for Figma-to-component workflow.
|
|
3
|
+
*
|
|
4
|
+
* Maps Figma design tokens to Storefront Next theme tokens in app.css with exact/fuzzy matching.
|
|
5
|
+
*
|
|
6
|
+
* @module tools/storefrontnext/figma/map-tokens
|
|
7
|
+
*/
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import type { McpTool } from '../../../../utils/index.js';
|
|
10
|
+
import type { Services } from '../../../../services.js';
|
|
11
|
+
export declare const mapTokensToThemeSchema: z.ZodObject<{
|
|
12
|
+
figmaTokens: z.ZodArray<z.ZodObject<{
|
|
13
|
+
name: z.ZodString;
|
|
14
|
+
value: z.ZodString;
|
|
15
|
+
type: z.ZodEnum<["color", "spacing", "radius", "opacity", "fontSize", "fontFamily", "other"]>;
|
|
16
|
+
description: z.ZodOptional<z.ZodString>;
|
|
17
|
+
}, "strip", z.ZodTypeAny, {
|
|
18
|
+
type: "color" | "fontFamily" | "fontSize" | "opacity" | "other" | "radius" | "spacing";
|
|
19
|
+
name: string;
|
|
20
|
+
value: string;
|
|
21
|
+
description?: string | undefined;
|
|
22
|
+
}, {
|
|
23
|
+
type: "color" | "fontFamily" | "fontSize" | "opacity" | "other" | "radius" | "spacing";
|
|
24
|
+
name: string;
|
|
25
|
+
value: string;
|
|
26
|
+
description?: string | undefined;
|
|
27
|
+
}>, "many">;
|
|
28
|
+
themeFilePath: z.ZodOptional<z.ZodString>;
|
|
29
|
+
}, "strict", z.ZodTypeAny, {
|
|
30
|
+
figmaTokens: {
|
|
31
|
+
type: "color" | "fontFamily" | "fontSize" | "opacity" | "other" | "radius" | "spacing";
|
|
32
|
+
name: string;
|
|
33
|
+
value: string;
|
|
34
|
+
description?: string | undefined;
|
|
35
|
+
}[];
|
|
36
|
+
themeFilePath?: string | undefined;
|
|
37
|
+
}, {
|
|
38
|
+
figmaTokens: {
|
|
39
|
+
type: "color" | "fontFamily" | "fontSize" | "opacity" | "other" | "radius" | "spacing";
|
|
40
|
+
name: string;
|
|
41
|
+
value: string;
|
|
42
|
+
description?: string | undefined;
|
|
43
|
+
}[];
|
|
44
|
+
themeFilePath?: string | undefined;
|
|
45
|
+
}>;
|
|
46
|
+
export type MapTokensToThemeInput = z.infer<typeof mapTokensToThemeSchema>;
|
|
47
|
+
/**
|
|
48
|
+
* Maps Figma design tokens to existing theme tokens in app.css.
|
|
49
|
+
*
|
|
50
|
+
* @param args - Figma tokens array and optional theme file path
|
|
51
|
+
* @param workspaceRoot - Optional workspace root for theme file discovery; used when themeFilePath is not provided
|
|
52
|
+
* @returns Formatted mapping report with exact/fuzzy matches, confidence scores, and usage instructions, or error message on failure
|
|
53
|
+
*/
|
|
54
|
+
export declare function mapFigmaTokensToTheme(args: MapTokensToThemeInput, workspaceRoot?: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* Creates the storefront_next_map_tokens_to_theme MCP tool.
|
|
57
|
+
*
|
|
58
|
+
* @param loadServices - Function that loads configuration and returns Services instance
|
|
59
|
+
* @returns MCP tool for token mapping
|
|
60
|
+
*/
|
|
61
|
+
export declare function createMapTokensToThemeTool(loadServices: () => Services): McpTool;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025, Salesforce, Inc.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2
|
|
4
|
+
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Map tokens tool for Figma-to-component workflow.
|
|
8
|
+
*
|
|
9
|
+
* Maps Figma design tokens to Storefront Next theme tokens in app.css with exact/fuzzy matching.
|
|
10
|
+
*
|
|
11
|
+
* @module tools/storefrontnext/figma/map-tokens
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { createToolAdapter, textResult } from '../../../adapter.js';
|
|
15
|
+
import { parseThemeFile } from './css-parser.js';
|
|
16
|
+
import { matchTokens } from './token-matcher.js';
|
|
17
|
+
export const mapTokensToThemeSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
figmaTokens: z
|
|
20
|
+
.array(z.object({
|
|
21
|
+
name: z.string().describe('Token name from Figma (e.g., "Primary/Blue", "Spacing/Large")'),
|
|
22
|
+
value: z.string().describe('Token value (e.g., "#2563eb", "16px", "0.5rem")'),
|
|
23
|
+
type: z
|
|
24
|
+
.enum(['color', 'spacing', 'radius', 'opacity', 'fontSize', 'fontFamily', 'other'])
|
|
25
|
+
.describe('Type of the token'),
|
|
26
|
+
description: z.string().optional().describe('Optional description from Figma'),
|
|
27
|
+
}))
|
|
28
|
+
.describe('Array of design tokens extracted from Figma'),
|
|
29
|
+
themeFilePath: z
|
|
30
|
+
.string()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe('Optional absolute path to theme CSS file. If not provided, will search for app.css in common locations.'),
|
|
33
|
+
})
|
|
34
|
+
.strict();
|
|
35
|
+
function formatTokenMatch(match) {
|
|
36
|
+
let output = `### ${match.figmaToken.name}\n\n`;
|
|
37
|
+
output += `- **Figma Value**: \`${match.figmaToken.value}\`\n`;
|
|
38
|
+
output += `- **Type**: ${match.figmaToken.type}\n`;
|
|
39
|
+
if (match.figmaToken.description) {
|
|
40
|
+
output += `- **Description**: ${match.figmaToken.description}\n`;
|
|
41
|
+
}
|
|
42
|
+
output += `\n#### Match Result\n\n`;
|
|
43
|
+
output += `- **Match Type**: ${match.matchType}\n`;
|
|
44
|
+
output += `- **Confidence**: ${match.confidence}%\n`;
|
|
45
|
+
if (match.matchedToken) {
|
|
46
|
+
output += `- **Matched Token**: \`${match.matchedToken.name}\`\n`;
|
|
47
|
+
output += `- **Token Value**: \`${match.matchedToken.value}\`\n`;
|
|
48
|
+
output += `- **Resolved Value**: \`${match.matchedToken.resolvedValue || match.matchedToken.value}\`\n`;
|
|
49
|
+
output += `- **Theme**: ${match.matchedToken.theme}\n`;
|
|
50
|
+
}
|
|
51
|
+
output += `- **Reason**: ${match.reason}\n\n`;
|
|
52
|
+
if (match.suggestions && match.suggestions.length > 0) {
|
|
53
|
+
output += `#### Suggestions\n\n`;
|
|
54
|
+
for (const [index, suggestion] of match.suggestions.entries()) {
|
|
55
|
+
output += `${index + 1}. **${suggestion.tokenName}**\n`;
|
|
56
|
+
output += ` - Value: \`${suggestion.value}\`\n`;
|
|
57
|
+
output += ` - Theme: ${suggestion.theme}\n`;
|
|
58
|
+
output += ` - Reason: ${suggestion.reason}\n`;
|
|
59
|
+
if (suggestion.insertAfter) {
|
|
60
|
+
output += ` - Insert after: \`${suggestion.insertAfter}\`\n`;
|
|
61
|
+
}
|
|
62
|
+
output += `\n`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return output;
|
|
66
|
+
}
|
|
67
|
+
function generateSummary(matches) {
|
|
68
|
+
const exactMatches = matches.filter((m) => m.matchType === 'exact');
|
|
69
|
+
const fuzzyMatches = matches.filter((m) => m.matchType === 'fuzzy');
|
|
70
|
+
const noMatches = matches.filter((m) => m.matchType === 'none');
|
|
71
|
+
const highConfidence = matches.filter((m) => m.confidence >= 70 && m.matchedToken);
|
|
72
|
+
const lowConfidence = matches.filter((m) => m.confidence < 70 && m.confidence > 0);
|
|
73
|
+
let summary = `## Summary\n\n`;
|
|
74
|
+
summary += `- **Total Tokens**: ${matches.length}\n`;
|
|
75
|
+
summary += `- **Exact Matches**: ${exactMatches.length}\n`;
|
|
76
|
+
summary += `- **Fuzzy Matches**: ${fuzzyMatches.length}\n`;
|
|
77
|
+
summary += `- **No Matches**: ${noMatches.length}\n`;
|
|
78
|
+
summary += `- **High Confidence (≥70%)**: ${highConfidence.length}\n`;
|
|
79
|
+
summary += `- **Low Confidence (<70%)**: ${lowConfidence.length}\n\n`;
|
|
80
|
+
if (exactMatches.length > 0) {
|
|
81
|
+
summary += `### ✅ Exact Matches (Use these tokens directly)\n\n`;
|
|
82
|
+
for (const match of exactMatches) {
|
|
83
|
+
if (match.matchedToken) {
|
|
84
|
+
summary += `- \`${match.figmaToken.name}\` → \`${match.matchedToken.name}\`\n`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
summary += `\n`;
|
|
88
|
+
}
|
|
89
|
+
if (highConfidence.length > 0) {
|
|
90
|
+
summary += `### ⚠️ High Confidence Fuzzy Matches (Review and confirm)\n\n`;
|
|
91
|
+
for (const match of highConfidence) {
|
|
92
|
+
if (match.matchedToken) {
|
|
93
|
+
summary += `- \`${match.figmaToken.name}\` → \`${match.matchedToken.name}\` (${match.confidence}%)\n`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
summary += `\n`;
|
|
97
|
+
}
|
|
98
|
+
if (lowConfidence.length > 0) {
|
|
99
|
+
summary += `### ⚠️ Low Confidence Matches (Verify before using)\n\n`;
|
|
100
|
+
for (const match of lowConfidence) {
|
|
101
|
+
if (match.matchedToken) {
|
|
102
|
+
summary += `- \`${match.figmaToken.name}\` → \`${match.matchedToken.name}\` (${match.confidence}%)\n`;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
summary += `\n`;
|
|
106
|
+
}
|
|
107
|
+
if (noMatches.length > 0) {
|
|
108
|
+
summary += `### ❌ No Matches (New tokens needed)\n\n`;
|
|
109
|
+
for (const match of noMatches) {
|
|
110
|
+
summary += `- \`${match.figmaToken.name}\`: ${match.figmaToken.value}\n`;
|
|
111
|
+
if (match.suggestions && match.suggestions.length > 0) {
|
|
112
|
+
summary += ` - Suggested: \`${match.suggestions[0].tokenName}\`\n`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
summary += `\n`;
|
|
116
|
+
}
|
|
117
|
+
return summary;
|
|
118
|
+
}
|
|
119
|
+
function generateRecommendations(matches) {
|
|
120
|
+
const needsNewTokens = matches.filter((m) => m.matchType === 'none');
|
|
121
|
+
const needsReview = matches.filter((m) => m.matchType === 'fuzzy' && m.confidence < 70);
|
|
122
|
+
if (needsNewTokens.length === 0 && needsReview.length === 0) {
|
|
123
|
+
return `## ✅ Recommendations\n\nAll tokens have been matched with high confidence. You can proceed with using the matched tokens in your component.\n\n`;
|
|
124
|
+
}
|
|
125
|
+
let recommendations = `## 📝 Recommendations\n\n`;
|
|
126
|
+
if (needsNewTokens.length > 0) {
|
|
127
|
+
recommendations += `### Create New Tokens\n\n`;
|
|
128
|
+
recommendations += `The following tokens from Figma don't have matches in your theme. Consider adding them to your \`app.css\` file:\n\n`;
|
|
129
|
+
for (const match of needsNewTokens) {
|
|
130
|
+
if (match.suggestions && match.suggestions.length > 0) {
|
|
131
|
+
const suggestion = match.suggestions[0];
|
|
132
|
+
recommendations += `\`\`\`css\n`;
|
|
133
|
+
recommendations += `/* Add to ${suggestion.theme === 'both' ? ':root and .dark' : suggestion.theme === 'light' ? ':root' : '.dark'} section */\n`;
|
|
134
|
+
recommendations += `${suggestion.tokenName}: ${suggestion.value};\n`;
|
|
135
|
+
recommendations += `\`\`\`\n\n`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (needsReview.length > 0) {
|
|
140
|
+
recommendations += `### Review Low Confidence Matches\n\n`;
|
|
141
|
+
recommendations += `The following matches have confidence below 70%. Please review and confirm they are correct before using:\n\n`;
|
|
142
|
+
for (const match of needsReview) {
|
|
143
|
+
if (match.matchedToken) {
|
|
144
|
+
recommendations += `- **${match.figmaToken.name}** (${match.figmaToken.value})\n`;
|
|
145
|
+
recommendations += ` - Matched: \`${match.matchedToken.name}\` (${match.matchedToken.resolvedValue})\n`;
|
|
146
|
+
recommendations += ` - Confidence: ${match.confidence}%\n`;
|
|
147
|
+
recommendations += ` - Reason: ${match.reason}\n\n`;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return recommendations;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Maps Figma design tokens to existing theme tokens in app.css.
|
|
155
|
+
*
|
|
156
|
+
* @param args - Figma tokens array and optional theme file path
|
|
157
|
+
* @param workspaceRoot - Optional workspace root for theme file discovery; used when themeFilePath is not provided
|
|
158
|
+
* @returns Formatted mapping report with exact/fuzzy matches, confidence scores, and usage instructions, or error message on failure
|
|
159
|
+
*/
|
|
160
|
+
export function mapFigmaTokensToTheme(args, workspaceRoot) {
|
|
161
|
+
try {
|
|
162
|
+
const parsedTheme = parseThemeFile(args.themeFilePath, workspaceRoot);
|
|
163
|
+
const figmaTokens = args.figmaTokens.map((token) => ({
|
|
164
|
+
name: token.name,
|
|
165
|
+
value: token.value,
|
|
166
|
+
type: token.type,
|
|
167
|
+
description: token.description,
|
|
168
|
+
}));
|
|
169
|
+
const matches = matchTokens(figmaTokens, parsedTheme);
|
|
170
|
+
let response = `# Figma Design Tokens → StorefrontNext Theme Mapping\n\n`;
|
|
171
|
+
if (parsedTheme.warnings.length > 0) {
|
|
172
|
+
response += `## ⚠️ Warnings\n\n`;
|
|
173
|
+
for (const warning of parsedTheme.warnings) {
|
|
174
|
+
response += `- ${warning}\n`;
|
|
175
|
+
}
|
|
176
|
+
response += `\n`;
|
|
177
|
+
}
|
|
178
|
+
response += generateSummary(matches);
|
|
179
|
+
response += `## Detailed Mapping Results\n\n`;
|
|
180
|
+
for (const match of matches) {
|
|
181
|
+
response += formatTokenMatch(match);
|
|
182
|
+
}
|
|
183
|
+
response += generateRecommendations(matches);
|
|
184
|
+
response += `## 💡 Usage Instructions\n\n`;
|
|
185
|
+
response += `### Using Matched Tokens in Components\n\n`;
|
|
186
|
+
response += `For exact and high-confidence matches, use the token directly in your Tailwind classes:\n\n`;
|
|
187
|
+
response += `\`\`\`tsx\n`;
|
|
188
|
+
response += `// Instead of hardcoded colors\n`;
|
|
189
|
+
response += `<div className="bg-[#2563eb]">\n\n`;
|
|
190
|
+
response += `// Use theme tokens\n`;
|
|
191
|
+
response += `<div className="bg-primary">\n`;
|
|
192
|
+
response += `\`\`\`\n\n`;
|
|
193
|
+
response += `### Creating New Tokens\n\n`;
|
|
194
|
+
response += `If you need to add new tokens, add them to your \`app.css\` file in both light and dark theme sections:\n\n`;
|
|
195
|
+
response += `\`\`\`css\n`;
|
|
196
|
+
response += `:root {\n`;
|
|
197
|
+
response += ` --your-new-token: #value;\n`;
|
|
198
|
+
response += `}\n\n`;
|
|
199
|
+
response += `.dark {\n`;
|
|
200
|
+
response += ` --your-new-token: #dark-value;\n`;
|
|
201
|
+
response += `}\n`;
|
|
202
|
+
response += `\`\`\`\n\n`;
|
|
203
|
+
return response;
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
207
|
+
return `# Error: Token Mapping Failed\n\n${errorMessage}\n\nPlease ensure the theme file path is correct and accessible.`;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Creates the storefront_next_map_tokens_to_theme MCP tool.
|
|
212
|
+
*
|
|
213
|
+
* @param loadServices - Function that loads configuration and returns Services instance
|
|
214
|
+
* @returns MCP tool for token mapping
|
|
215
|
+
*/
|
|
216
|
+
export function createMapTokensToThemeTool(loadServices) {
|
|
217
|
+
return createToolAdapter({
|
|
218
|
+
name: 'storefront_next_map_tokens_to_theme',
|
|
219
|
+
description: 'Maps Figma design tokens to existing StorefrontNext theme tokens in app.css. ' +
|
|
220
|
+
'Analyzes Figma design tokens (colors, spacing, radius, etc.) and finds exact matches, ' +
|
|
221
|
+
'provides fuzzy matches with confidence scores, suggests new token names for unmatched values, ' +
|
|
222
|
+
'and recommends where to add new tokens in the CSS file. ' +
|
|
223
|
+
'Use this tool after retrieving design variables from Figma MCP to ensure components use theme tokens instead of hardcoded values.',
|
|
224
|
+
toolsets: ['STOREFRONTNEXT'],
|
|
225
|
+
isGA: false,
|
|
226
|
+
requiresInstance: false,
|
|
227
|
+
inputSchema: mapTokensToThemeSchema.shape,
|
|
228
|
+
async execute(args, context) {
|
|
229
|
+
return mapFigmaTokensToTheme(args, context.services.resolveWithProjectDirectory());
|
|
230
|
+
},
|
|
231
|
+
formatOutput: (output) => textResult(output),
|
|
232
|
+
}, loadServices);
|
|
233
|
+
}
|
|
234
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ThemeToken, ParsedTheme } from './css-parser.js';
|
|
2
|
+
/**
|
|
3
|
+
* Design token extracted from Figma.
|
|
4
|
+
*
|
|
5
|
+
* @property {string} name - Token name from Figma (e.g., "Primary/Blue", "Spacing/Large")
|
|
6
|
+
* @property {string} value - Token value (e.g., "#2563eb", "16px", "0.5rem")
|
|
7
|
+
* @property {'color'|'fontFamily'|'fontSize'|'opacity'|'other'|'radius'|'spacing'} type - Token type for matching logic
|
|
8
|
+
* @property {string} [description] - Optional description from Figma
|
|
9
|
+
*/
|
|
10
|
+
export interface FigmaToken {
|
|
11
|
+
name: string;
|
|
12
|
+
value: string;
|
|
13
|
+
type: 'color' | 'fontFamily' | 'fontSize' | 'opacity' | 'other' | 'radius' | 'spacing';
|
|
14
|
+
description?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Result of matching a Figma token to theme tokens.
|
|
18
|
+
*
|
|
19
|
+
* @property {FigmaToken} figmaToken - The Figma token that was matched
|
|
20
|
+
* @property {ThemeToken} [matchedToken] - Best-matching theme token (if found)
|
|
21
|
+
* @property {number} confidence - Match confidence (0-100)
|
|
22
|
+
* @property {'exact'|'fuzzy'|'none'} matchType - 'exact', 'fuzzy', or 'none'
|
|
23
|
+
* @property {string} reason - Human-readable explanation of the match
|
|
24
|
+
* @property {TokenSuggestion[]} [suggestions] - Suggested new tokens or alternatives (when no match or fuzzy match)
|
|
25
|
+
*/
|
|
26
|
+
export interface TokenMatch {
|
|
27
|
+
figmaToken: FigmaToken;
|
|
28
|
+
matchedToken?: ThemeToken;
|
|
29
|
+
confidence: number;
|
|
30
|
+
matchType: 'exact' | 'fuzzy' | 'none';
|
|
31
|
+
reason: string;
|
|
32
|
+
suggestions?: TokenSuggestion[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Suggestion for a new or alternative theme token.
|
|
36
|
+
*
|
|
37
|
+
* @property {string} tokenName - Suggested CSS custom property name
|
|
38
|
+
* @property {string} value - Token value
|
|
39
|
+
* @property {'both'|'dark'|'light'} theme - Which theme(s) to add to: 'both', 'dark', or 'light'
|
|
40
|
+
* @property {string} reason - Explanation for the suggestion
|
|
41
|
+
* @property {string} [insertAfter] - Optional token name to insert after in the theme file
|
|
42
|
+
*/
|
|
43
|
+
export interface TokenSuggestion {
|
|
44
|
+
tokenName: string;
|
|
45
|
+
value: string;
|
|
46
|
+
theme: 'both' | 'dark' | 'light';
|
|
47
|
+
reason: string;
|
|
48
|
+
insertAfter?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Matches a single Figma token to existing theme tokens.
|
|
52
|
+
*
|
|
53
|
+
* @param figmaToken - Figma design token to match
|
|
54
|
+
* @param parsedTheme - Parsed theme from app.css
|
|
55
|
+
* @returns TokenMatch with exact, fuzzy, or no match and optional suggestions
|
|
56
|
+
*/
|
|
57
|
+
export declare function matchToken(figmaToken: FigmaToken, parsedTheme: ParsedTheme): TokenMatch;
|
|
58
|
+
/**
|
|
59
|
+
* Matches multiple Figma tokens to existing theme tokens.
|
|
60
|
+
*
|
|
61
|
+
* @param figmaTokens - Array of Figma design tokens to match
|
|
62
|
+
* @param parsedTheme - Parsed theme from app.css
|
|
63
|
+
* @returns Array of TokenMatch results, one per input token
|
|
64
|
+
*/
|
|
65
|
+
export declare function matchTokens(figmaTokens: FigmaToken[], parsedTheme: ParsedTheme): TokenMatch[];
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025, Salesforce, Inc.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2
|
|
4
|
+
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Normalizes hex colors to lowercase 6-digit format
|
|
8
|
+
*/
|
|
9
|
+
function normalizeHexColor(hex) {
|
|
10
|
+
let normalized = hex.toLowerCase().trim();
|
|
11
|
+
// Remove # if present
|
|
12
|
+
if (normalized.startsWith('#')) {
|
|
13
|
+
normalized = normalized.slice(1);
|
|
14
|
+
}
|
|
15
|
+
// Expand 3-digit hex to 6-digit
|
|
16
|
+
if (normalized.length === 3) {
|
|
17
|
+
normalized = [...normalized].map((c) => c + c).join('');
|
|
18
|
+
}
|
|
19
|
+
return normalized;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Calculates color distance between two hex colors (0-100, lower is closer)
|
|
23
|
+
*/
|
|
24
|
+
function calculateColorDistance(hex1, hex2) {
|
|
25
|
+
const r1 = Number.parseInt(hex1.slice(0, 2), 16);
|
|
26
|
+
const g1 = Number.parseInt(hex1.slice(2, 4), 16);
|
|
27
|
+
const b1 = Number.parseInt(hex1.slice(4, 6), 16);
|
|
28
|
+
const r2 = Number.parseInt(hex2.slice(0, 2), 16);
|
|
29
|
+
const g2 = Number.parseInt(hex2.slice(2, 4), 16);
|
|
30
|
+
const b2 = Number.parseInt(hex2.slice(4, 6), 16);
|
|
31
|
+
// Euclidean distance normalized to 0-100 scale
|
|
32
|
+
const distance = Math.hypot(r1 - r2, g1 - g2, b1 - b2);
|
|
33
|
+
// Max distance is sqrt(255^2 * 3) ≈ 441
|
|
34
|
+
return (distance / 441) * 100;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Calculates string similarity between two strings (0-100, higher is more similar)
|
|
38
|
+
* Uses Levenshtein distance algorithm
|
|
39
|
+
*/
|
|
40
|
+
function calculateStringSimilarity(str1, str2) {
|
|
41
|
+
const s1 = str1.toLowerCase();
|
|
42
|
+
const s2 = str2.toLowerCase();
|
|
43
|
+
// Exact match
|
|
44
|
+
if (s1 === s2)
|
|
45
|
+
return 100;
|
|
46
|
+
// Contains match bonus
|
|
47
|
+
if (s1.includes(s2) || s2.includes(s1)) {
|
|
48
|
+
return 80 + (Math.min(s1.length, s2.length) / Math.max(s1.length, s2.length)) * 20;
|
|
49
|
+
}
|
|
50
|
+
// Levenshtein distance
|
|
51
|
+
const matrix = [];
|
|
52
|
+
const len1 = s1.length;
|
|
53
|
+
const len2 = s2.length;
|
|
54
|
+
for (let i = 0; i <= len1; i++) {
|
|
55
|
+
matrix[i] = [i];
|
|
56
|
+
}
|
|
57
|
+
for (let j = 0; j <= len2; j++) {
|
|
58
|
+
matrix[0][j] = j;
|
|
59
|
+
}
|
|
60
|
+
for (let i = 1; i <= len1; i++) {
|
|
61
|
+
for (let j = 1; j <= len2; j++) {
|
|
62
|
+
const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
|
|
63
|
+
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const distance = matrix[len1][len2];
|
|
67
|
+
const maxLen = Math.max(len1, len2);
|
|
68
|
+
return ((maxLen - distance) / maxLen) * 100;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Extracts semantic meaning from token name
|
|
72
|
+
*/
|
|
73
|
+
function extractSemantics(name) {
|
|
74
|
+
const parts = name.toLowerCase().replace(/^--/, '').split(/[-_]/);
|
|
75
|
+
const semantics = [];
|
|
76
|
+
// Color semantics
|
|
77
|
+
const colorKeywords = new Set([
|
|
78
|
+
'accent',
|
|
79
|
+
'background',
|
|
80
|
+
'border',
|
|
81
|
+
'destructive',
|
|
82
|
+
'error',
|
|
83
|
+
'foreground',
|
|
84
|
+
'info',
|
|
85
|
+
'muted',
|
|
86
|
+
'primary',
|
|
87
|
+
'secondary',
|
|
88
|
+
'success',
|
|
89
|
+
'text',
|
|
90
|
+
'warning',
|
|
91
|
+
]);
|
|
92
|
+
const lightDark = new Set(['dark', 'light']);
|
|
93
|
+
const colorNames = new Set(['black', 'blue', 'gray', 'green', 'orange', 'purple', 'red', 'white', 'yellow']);
|
|
94
|
+
for (const part of parts) {
|
|
95
|
+
if (colorKeywords.has(part)) {
|
|
96
|
+
semantics.push(`semantic:${part}`);
|
|
97
|
+
}
|
|
98
|
+
if (lightDark.has(part)) {
|
|
99
|
+
semantics.push(`theme:${part}`);
|
|
100
|
+
}
|
|
101
|
+
if (colorNames.has(part)) {
|
|
102
|
+
semantics.push(`color:${part}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return semantics;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Finds exact color match
|
|
109
|
+
*/
|
|
110
|
+
function findExactColorMatch(figmaValue, parsedTheme) {
|
|
111
|
+
const normalizedFigma = normalizeHexColor(figmaValue);
|
|
112
|
+
for (const token of parsedTheme.tokens) {
|
|
113
|
+
if (token.type === 'color' && token.resolvedValue) {
|
|
114
|
+
const normalizedToken = normalizeHexColor(token.resolvedValue);
|
|
115
|
+
if (normalizedFigma === normalizedToken) {
|
|
116
|
+
return token;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Finds fuzzy matches based on color similarity and name similarity
|
|
124
|
+
*/
|
|
125
|
+
function findFuzzyMatches(figmaToken, parsedTheme) {
|
|
126
|
+
const matches = [];
|
|
127
|
+
// Filter tokens by type
|
|
128
|
+
const relevantTokens = parsedTheme.tokens.filter((token) => token.type === figmaToken.type);
|
|
129
|
+
const figmaSemantics = extractSemantics(figmaToken.name);
|
|
130
|
+
const normalizedFigmaValue = figmaToken.type === 'color' && figmaToken.value.startsWith('#')
|
|
131
|
+
? normalizeHexColor(figmaToken.value)
|
|
132
|
+
: figmaToken.value;
|
|
133
|
+
for (const token of relevantTokens) {
|
|
134
|
+
let score = 0;
|
|
135
|
+
// Name similarity (40% weight)
|
|
136
|
+
const nameSimilarity = calculateStringSimilarity(figmaToken.name, token.name);
|
|
137
|
+
score += nameSimilarity * 0.4;
|
|
138
|
+
// Semantic similarity (30% weight)
|
|
139
|
+
const tokenSemantics = extractSemantics(token.name);
|
|
140
|
+
const semanticMatches = figmaSemantics.filter((s) => tokenSemantics.includes(s)).length;
|
|
141
|
+
const semanticScore = figmaSemantics.length > 0 ? (semanticMatches / figmaSemantics.length) * 100 : 0;
|
|
142
|
+
score += semanticScore * 0.3;
|
|
143
|
+
// Value similarity (30% weight)
|
|
144
|
+
if (figmaToken.type === 'color' && token.resolvedValue) {
|
|
145
|
+
const normalizedTokenValue = normalizeHexColor(token.resolvedValue);
|
|
146
|
+
const colorDistance = calculateColorDistance(normalizedFigmaValue, normalizedTokenValue);
|
|
147
|
+
const colorSimilarity = Math.max(0, 100 - colorDistance);
|
|
148
|
+
score += colorSimilarity * 0.3;
|
|
149
|
+
}
|
|
150
|
+
if (score > 20) {
|
|
151
|
+
// Only include matches with score > 20
|
|
152
|
+
matches.push({ token, score });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Sort by score descending
|
|
156
|
+
return matches.sort((a, b) => b.score - a.score);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Generates suggestions for new tokens if no good match found
|
|
160
|
+
*/
|
|
161
|
+
function generateTokenSuggestions(figmaToken, parsedTheme) {
|
|
162
|
+
const suggestions = [];
|
|
163
|
+
// Analyze existing token naming patterns
|
|
164
|
+
const existingNames = parsedTheme.tokens.filter((t) => t.type === figmaToken.type).map((t) => t.name);
|
|
165
|
+
// Extract common prefixes
|
|
166
|
+
const hasColorPrefix = existingNames.some((n) => n.startsWith('--color-'));
|
|
167
|
+
const hasRadiusPrefix = existingNames.some((n) => n.startsWith('--radius-'));
|
|
168
|
+
// Generate token name based on Figma token name
|
|
169
|
+
let suggestedName = figmaToken.name.toLowerCase().replaceAll(/[^a-z0-9-]/g, '-');
|
|
170
|
+
// Add appropriate prefix if not present
|
|
171
|
+
if (figmaToken.type === 'color' && !suggestedName.startsWith('--color-') && hasColorPrefix) {
|
|
172
|
+
suggestedName = `--color-${suggestedName.replace(/^--/, '')}`;
|
|
173
|
+
}
|
|
174
|
+
else if (figmaToken.type === 'radius' && !suggestedName.startsWith('--radius-') && hasRadiusPrefix) {
|
|
175
|
+
suggestedName = `--radius-${suggestedName.replace(/^--/, '')}`;
|
|
176
|
+
}
|
|
177
|
+
else if (!suggestedName.startsWith('--')) {
|
|
178
|
+
suggestedName = `--${suggestedName}`;
|
|
179
|
+
}
|
|
180
|
+
// Find a good place to insert
|
|
181
|
+
const similarTokens = existingNames.filter((name) => {
|
|
182
|
+
const similarity = calculateStringSimilarity(name, suggestedName);
|
|
183
|
+
return similarity > 30;
|
|
184
|
+
});
|
|
185
|
+
const insertAfter = similarTokens.length > 0 ? similarTokens[0] : undefined;
|
|
186
|
+
// For colors, suggest both light and dark values
|
|
187
|
+
if (figmaToken.type === 'color') {
|
|
188
|
+
suggestions.push({
|
|
189
|
+
tokenName: suggestedName,
|
|
190
|
+
value: figmaToken.value,
|
|
191
|
+
theme: 'both',
|
|
192
|
+
reason: `New token suggestion based on Figma token "${figmaToken.name}"`,
|
|
193
|
+
insertAfter,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
suggestions.push({
|
|
198
|
+
tokenName: suggestedName,
|
|
199
|
+
value: figmaToken.value,
|
|
200
|
+
theme: 'light',
|
|
201
|
+
reason: `New token suggestion based on Figma token "${figmaToken.name}"`,
|
|
202
|
+
insertAfter,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return suggestions;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Matches a single Figma token to existing theme tokens.
|
|
209
|
+
*
|
|
210
|
+
* @param figmaToken - Figma design token to match
|
|
211
|
+
* @param parsedTheme - Parsed theme from app.css
|
|
212
|
+
* @returns TokenMatch with exact, fuzzy, or no match and optional suggestions
|
|
213
|
+
*/
|
|
214
|
+
export function matchToken(figmaToken, parsedTheme) {
|
|
215
|
+
// Try exact match first (only for colors with hex values)
|
|
216
|
+
if (figmaToken.type === 'color' && figmaToken.value.startsWith('#')) {
|
|
217
|
+
const exactMatch = findExactColorMatch(figmaToken.value, parsedTheme);
|
|
218
|
+
if (exactMatch) {
|
|
219
|
+
return {
|
|
220
|
+
figmaToken,
|
|
221
|
+
matchedToken: exactMatch,
|
|
222
|
+
confidence: 100,
|
|
223
|
+
matchType: 'exact',
|
|
224
|
+
reason: `Exact color match: ${figmaToken.value} matches ${exactMatch.name}`,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Try fuzzy matching
|
|
229
|
+
const fuzzyMatches = findFuzzyMatches(figmaToken, parsedTheme);
|
|
230
|
+
if (fuzzyMatches.length > 0 && fuzzyMatches[0].score >= 50) {
|
|
231
|
+
const bestMatch = fuzzyMatches[0];
|
|
232
|
+
return {
|
|
233
|
+
figmaToken,
|
|
234
|
+
matchedToken: bestMatch.token,
|
|
235
|
+
confidence: Math.round(bestMatch.score),
|
|
236
|
+
matchType: 'fuzzy',
|
|
237
|
+
reason: `Fuzzy match based on name similarity and semantic meaning`,
|
|
238
|
+
suggestions: fuzzyMatches.length > 1
|
|
239
|
+
? fuzzyMatches.slice(1, 4).map((m) => ({
|
|
240
|
+
tokenName: m.token.name,
|
|
241
|
+
value: m.token.value,
|
|
242
|
+
theme: m.token.theme === 'shared' ? 'both' : m.token.theme,
|
|
243
|
+
reason: `Alternative match (confidence: ${Math.round(m.score)}%)`,
|
|
244
|
+
}))
|
|
245
|
+
: undefined,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
// No good match found, generate suggestions
|
|
249
|
+
const suggestions = generateTokenSuggestions(figmaToken, parsedTheme);
|
|
250
|
+
return {
|
|
251
|
+
figmaToken,
|
|
252
|
+
confidence: 0,
|
|
253
|
+
matchType: 'none',
|
|
254
|
+
reason: 'No matching token found. Consider creating a new token.',
|
|
255
|
+
suggestions,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Matches multiple Figma tokens to existing theme tokens.
|
|
260
|
+
*
|
|
261
|
+
* @param figmaTokens - Array of Figma design tokens to match
|
|
262
|
+
* @param parsedTheme - Parsed theme from app.css
|
|
263
|
+
* @returns Array of TokenMatch results, one per input token
|
|
264
|
+
*/
|
|
265
|
+
export function matchTokens(figmaTokens, parsedTheme) {
|
|
266
|
+
return figmaTokens.map((token) => matchToken(token, parsedTheme));
|
|
267
|
+
}
|
|
268
|
+
//# sourceMappingURL=token-matcher.js.map
|
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storefront Next toolset for B2C Commerce.
|
|
3
|
+
*
|
|
4
|
+
* This toolset provides MCP tools for Storefront Next development.
|
|
5
|
+
*
|
|
6
|
+
* **Implemented Tools:**
|
|
7
|
+
* - `storefront_next_development_guidelines` - Get development guidelines and best practices
|
|
8
|
+
* - `storefront_next_page_designer_decorator` - Add Page Designer decorators to React components
|
|
9
|
+
* - `storefront_next_site_theming` - Get theming guidelines, questions, and validation
|
|
10
|
+
* - `storefront_next_figma_to_component_workflow` - Convert Figma to components
|
|
11
|
+
* - `storefront_next_generate_component` - Generate new components
|
|
12
|
+
* - `storefront_next_map_tokens_to_theme` - Map design tokens
|
|
13
|
+
*
|
|
14
|
+
* Note: mrt_bundle_push is defined in the MRT toolset and appears in STOREFRONTNEXT.
|
|
15
|
+
*
|
|
16
|
+
* @module tools/storefrontnext
|
|
17
|
+
*/
|
|
1
18
|
import type { McpTool } from '../../utils/index.js';
|
|
2
19
|
import type { Services } from '../../services.js';
|
|
3
20
|
/**
|