Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions packages/dev/mcp/s2/scripts/build-data.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
#!/usr/bin/env node
import {createRequire} from 'module';
import fg from 'fast-glob';
import {fileURLToPath, pathToFileURL} from 'url';
import fs from 'fs';
import path from 'path';
// eslint-disable-next-line rulesdir/imports
import * as ts from 'typescript';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
Expand All @@ -13,6 +16,7 @@ const ICONS_DIR = path.resolve(REPO_ROOT, 'packages/@react-spectrum/s2/s2wf-icon
const ILLUSTRATIONS_DIR = path.resolve(REPO_ROOT, 'packages/@react-spectrum/s2/spectrum-illustrations/linear');
const ICON_ALIASES_JS = path.resolve(REPO_ROOT, 'packages/dev/s2-docs/src/iconAliases.js');
const ILLUSTRATION_ALIASES_JS = path.resolve(REPO_ROOT, 'packages/dev/s2-docs/src/illustrationAliases.js');
const STYLE_PROPERTIES_TS = path.resolve(REPO_ROOT, 'packages/dev/s2-docs/src/styleProperties.ts');

function ensureDir(p) {
fs.mkdirSync(p, {recursive: true});
Expand All @@ -24,6 +28,23 @@ function writeJson(file, data) {
console.log('Wrote', path.relative(REPO_ROOT, file));
}

async function importTsModule(tsFilePath) {
if (!fs.existsSync(tsFilePath)) {
throw new Error(`TS module not found: ${tsFilePath}`);
}
const sourceText = fs.readFileSync(tsFilePath, 'utf8');
const result = ts.transpileModule(sourceText, {
fileName: tsFilePath,
compilerOptions: {
target: ts.ScriptTarget.ES2022,
module: ts.ModuleKind.ESNext,
esModuleInterop: true
}
});
const url = `data:text/javascript;base64,${Buffer.from(result.outputText, 'utf8').toString('base64')}`;
return import(url);
}

function buildIconNames() {
if (!fs.existsSync(ICONS_DIR)) {
throw new Error(`Icons directory not found: ${ICONS_DIR}`);
Expand Down Expand Up @@ -61,6 +82,107 @@ async function loadAliases(modPath, exportName) {
return mod[exportName] ?? {};
}

function buildBaseColorKeysFromSpectrumTokens(tokens) {
const keys = new Set();

// Matches spectrum-theme.ts
keys.add('transparent');
keys.add('black');
keys.add('white');

const addScale = (scale) => {
const re = new RegExp(`^${scale}-\\d+$`);
for (const tokenName of Object.keys(tokens)) {
if (re.test(tokenName)) {
// Match @react-spectrum/s2/style/tokens.ts behavior: strip "-color" in the middle.
keys.add(tokenName.replace('-color', ''));
}
}
};

// Global color scales
for (const scale of [
'gray', 'blue', 'red', 'orange', 'yellow', 'chartreuse', 'celery', 'green',
'seafoam', 'cyan', 'indigo', 'purple', 'fuchsia', 'magenta', 'pink',
'turquoise', 'brown', 'silver', 'cinnamon'
]) {
addScale(scale);
}

// Semantic color scales
for (const scale of ['accent-color', 'informative-color', 'negative-color', 'notice-color', 'positive-color']) {
addScale(scale);
}

// Simple transparent scales (names remain unchanged)
for (const scale of ['transparent-white', 'transparent-black']) {
const re = new RegExp(`^${scale}-\\d+$`);
for (const tokenName of Object.keys(tokens)) {
if (re.test(tokenName)) {
keys.add(tokenName);
}
}
}

// Overlay scale keys (derived in tokens.ts, we only need the names here)
for (const n of [25, 50, 75, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]) {
keys.add(`transparent-overlay-${n}`);
}

// High contrast keywords (matches spectrum-theme.ts)
for (const k of ['Background', 'ButtonBorder', 'ButtonFace', 'ButtonText', 'Field', 'Highlight', 'HighlightText', 'GrayText', 'Mark', 'LinkText']) {
keys.add(k);
}

return Array.from(keys).sort((a, b) => a.localeCompare(b));
}

function buildExpandedStyleMacroPropertyValues(styleProperties, spacingTypeValues, baseColorKeys) {
const out = {};

for (const [propertyName, def] of Object.entries(styleProperties)) {
const values = [];
const seen = new Set();

const pushUnique = (items) => {
for (const v of items) {
const s = String(v);
if (!seen.has(s)) {
seen.add(s);
values.push(s);
}
}
};

// Expand 'baseColors' placeholder into actual color token names.
const expandedBase = [];
for (const v of def.values ?? []) {
if (v === 'baseColors') {
expandedBase.push(...baseColorKeys);
} else {
expandedBase.push(v);
}
}
pushUnique(expandedBase);

// Expand spacing type placeholders into the actual numeric values shown in docs.
const additionalTypes = Array.isArray(def.additionalTypes) ? def.additionalTypes : [];
if (additionalTypes.includes('baseSpacing')) {
pushUnique(spacingTypeValues?.baseSpacing ?? []);
}
if (additionalTypes.includes('negativeSpacing')) {
pushUnique(spacingTypeValues?.negativeSpacing ?? []);
}

out[propertyName] = {
values,
additionalTypes
};
}

return out;
}

async function main() {
const icons = buildIconNames();
const illustrations = buildIllustrationNames();
Expand All @@ -71,6 +193,22 @@ async function main() {
writeJson(path.join(OUT_DIR, 'illustrations.json'), illustrations);
writeJson(path.join(OUT_DIR, 'iconAliases.json'), iconAliases);
writeJson(path.join(OUT_DIR, 'illustrationAliases.json'), illustrationAliases);

// Style macro property definitions
const stylePropsMod = await importTsModule(STYLE_PROPERTIES_TS);
const propertyCategories = ['color', 'dimensions', 'text', 'effects', 'layout', 'misc', 'conditions'];
const styleProperties = {};
for (const category of propertyCategories) {
Object.assign(styleProperties, stylePropsMod.getPropertyDefinitions(category));
}
Object.assign(styleProperties, stylePropsMod.getShorthandDefinitions());
writeJson(path.join(OUT_DIR, 'styleProperties.json'), styleProperties);

const require = createRequire(import.meta.url);
const spectrumTokens = require('@adobe/spectrum-tokens/dist/json/variables.json');
const baseColorKeys = buildBaseColorKeysFromSpectrumTokens(spectrumTokens);
const expanded = buildExpandedStyleMacroPropertyValues(styleProperties, stylePropsMod.spacingTypeValues, baseColorKeys);
writeJson(path.join(OUT_DIR, 'styleMacroPropertyValues.json'), expanded);
}

main().catch((err) => {
Expand Down
35 changes: 34 additions & 1 deletion packages/dev/mcp/s2/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env node
/// <reference types="node" />
import {errorToString} from '../../shared/src/utils.js';
import {listIconNames, listIllustrationNames, loadIconAliases, loadIllustrationAliases} from './s2-data.js';
import {listIconNames, listIllustrationNames, loadIconAliases, loadIllustrationAliases, loadStyleMacroPropertyValues} from './s2-data.js';
import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
import {startServer} from '../../shared/src/server.js';
import {z} from 'zod';
Expand Down Expand Up @@ -87,6 +87,39 @@ import {z} from 'zod';
return {content: [{type: 'text', text: JSON.stringify(Array.from(results).sort((a, b) => a.localeCompare(b)), null, 2)}]};
}
);

server.registerTool(
'get_style_macro_property_values',
{
title: 'Get style macro property values',
description: 'Returns the allowed values for a given S2 style macro property (including expanded color/spacing value lists where applicable).',
inputSchema: {propertyName: z.string()}
},
async ({propertyName}) => {
const name = String(propertyName ?? '').trim();
if (!name) {
throw new Error('Provide a non-empty propertyName.');
}

const all = loadStyleMacroPropertyValues();
let def = all[name];
if (!def) {
// fallback to case-insensitive lookup
const lower = name.toLowerCase();
const matchKey = Object.keys(all).find(k => k.toLowerCase() === lower);
if (matchKey) {
def = all[matchKey];
}
}

if (!def) {
const available = Object.keys(all).sort((a, b) => a.localeCompare(b));
throw new Error(`Unknown style macro property '${name}'. Available properties: ${available.join(', ')}`);
}

return {content: [{type: 'text', text: JSON.stringify(def, null, 2)}]};
}
);
});
} catch (err) {
console.error(errorToString(err));
Expand Down
10 changes: 10 additions & 0 deletions packages/dev/mcp/s2/src/s2-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ let iconIdCache: string[] | null = null;
let illustrationIdCache: string[] | null = null;
let iconAliasesCache: Record<string, string[]> | null = null;
let illustrationAliasesCache: Record<string, string[]> | null = null;
let styleMacroPropertyValuesCache: Record<string, {values: string[], additionalTypes?: string[]}> | null = null;

function readBundledJson(filename: string): any | null {
try {
Expand Down Expand Up @@ -45,3 +46,12 @@ export async function loadIllustrationAliases(): Promise<Record<string, string[]
const bundled = readBundledJson('illustrationAliases.json');
return (illustrationAliasesCache = (bundled && typeof bundled === 'object') ? bundled : {});
}

export function loadStyleMacroPropertyValues(): Record<string, {values: string[], additionalTypes?: string[]}> {
if (styleMacroPropertyValuesCache) {return styleMacroPropertyValuesCache;}
const bundled = readBundledJson('styleMacroPropertyValues.json');
if (!bundled || typeof bundled !== 'object' || Array.isArray(bundled)) {
return (styleMacroPropertyValuesCache = {});
}
return (styleMacroPropertyValuesCache = bundled as any);
}