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
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,19 @@
},
"devDependencies": {
"@eslint/js": "^10.0.0",
"@types/node": "^24.13.6",
"@types/node": "^24.19.1",
"@vitest/coverage-istanbul": "5.0.2",
"@vue/eslint-config-typescript": "^14.9.0",
"eslint": "^10.11.0",
"eslint-config-flat-gitignore": "^2.4.0",
"eslint-plugin-perfectionist": "^5.12.0",
"eslint-plugin-vue": "^10.11.0",
"eslint-plugin-perfectionist": "^5.12.1",
"eslint-plugin-vue": "^10.11.1",
"globby": "^16.2.4",
"jiti": "^2.7.0",
"lefthook": "^2.1.14",
"oxfmt": "^0.70.0",
"lefthook": "^2.1.16",
"oxfmt": "^0.71.0",
"typescript": "^6.0.3",
"vitest": "5.0.2"
},
"packageManager": "pnpm@12.6.0"
"packageManager": "pnpm@12.8.2"
}
4 changes: 3 additions & 1 deletion packages/extension/mcp/local-resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,9 @@ export function getLocalGridStyles(): Promise<GridStyle[]> {
)
}

export async function getLocalStyles(): Promise<BaseStyle[]> {
export async function getLocalStyles(): Promise<
Array<PaintStyle | TextStyle | EffectStyle | GridStyle>
> {
return (
await Promise.all([
getLocalPaintStyles(),
Expand Down
2 changes: 1 addition & 1 deletion packages/extension/mcp/tools/canvas/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ async function ensureLocalIndex(state: CanvasStyleState): Promise<void> {
}
}

function createStyle(type: StyleType): BaseStyle {
function createStyle(type: CanvasStyleResource['type']): BaseStyle {
switch (type) {
case 'PAINT':
return figma.createPaintStyle()
Expand Down
3 changes: 3 additions & 0 deletions packages/extension/mcp/tools/canvas/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,9 @@ async function readVariableDefaultValue(
const value = collection ? variable.valuesByMode[collection.defaultModeId] : undefined
if (value === undefined)
throw new Error(`Theme variable "${variable.name}" has no default-mode value.`)
if (typeof value === 'object' && 'color' in value) {
throw new Error(`Theme variable "${variable.name}" has an unsupported composed-color value.`)
}
if (typeof value === 'object' && 'type' in value) {
if (value.type !== 'VARIABLE_ALIAS')
throw new Error(`Theme variable "${variable.name}" has an unsupported default-mode value.`)
Expand Down
23 changes: 17 additions & 6 deletions packages/extension/mcp/tools/design-system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,22 @@ function isSupportedVariable(variable: Variable): variable is Variable & {
}

function describeVariableValue(value: VariableValue): CanvasVariableValue | undefined {
if (typeof value === 'object' && 'color' in value) return undefined
if (typeof value !== 'object' || !('type' in value)) return value
return value.type === 'VARIABLE_ALIAS' ? { variable: describeVariableAlias(value) } : undefined
}

function describeVariableValues(
values: Record<string, VariableValue>
values: Record<string, VariableValue>,
warnings: string[]
): Record<string, CanvasVariableValue> {
return Object.fromEntries(
Object.entries(values).flatMap(([modeId, value]) => {
const described = describeVariableValue(value)
if (described === undefined) {
const warning = 'Unsupported variable values were omitted from mode definitions.'
if (!warnings.includes(warning)) warnings.push(warning)
}
return described === undefined ? [] : [[modeId, described]]
})
)
Expand Down Expand Up @@ -354,7 +360,7 @@ async function collectVariables(referencedDefinitionIds: Set<string>, warnings:
const description = boundedText(variable.description)
const scopes = variable.scopes?.map(String)
const variableAuthoringKey = readAuthoringKey(variable, CANVAS_VARIABLE_KEY_NAME)
const valuesByMode = describeVariableValues(variable.valuesByMode)
const valuesByMode = describeVariableValues(variable.valuesByMode, warnings)
return {
id: variable.id,
key: variable.key,
Expand Down Expand Up @@ -387,7 +393,10 @@ async function collectVariables(referencedDefinitionIds: Set<string>, warnings:
const variable = variablesById.get(variableId)
return !variable || isSupportedVariable(variable)
})
.map(([variableId, values]) => [variableId, describeVariableValues(values)])
.map(([variableId, values]) => [
variableId,
describeVariableValues(values, warnings)
])
)
: {}
return {
Expand Down Expand Up @@ -557,7 +566,9 @@ function describeLayoutGrid(grid: LayoutGrid): CanvasFigmaLayoutGrid {
}
}

function describeStyle(style: BaseStyle) {
type LocalStyle = Awaited<ReturnType<typeof getLocalStyles>>[number]

function describeStyle(style: LocalStyle) {
const description = boundedText(style.description)
const descriptionMarkdown = boundedText(style.descriptionMarkdown)
const documentationLinks = documentationUris(style)
Expand Down Expand Up @@ -606,7 +617,7 @@ function describeStyle(style: BaseStyle) {
}
}

async function collectStyles(warnings: string[]): Promise<BaseStyle[]> {
async function collectStyles(warnings: string[]): Promise<LocalStyle[]> {
try {
return await getLocalStyles()
} catch {
Expand Down Expand Up @@ -680,7 +691,7 @@ async function collectShaders(warnings: string[]): Promise<Shader[]> {
}
}

function collectStyleVariableIds(styles: BaseStyle[], ids: Set<string>): void {
function collectStyleVariableIds(styles: LocalStyle[], ids: Set<string>): void {
for (const style of styles) {
collectVariableAliasIds(style.boundVariables, ids)
switch (style.type) {
Expand Down
12 changes: 6 additions & 6 deletions packages/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@
},
"devDependencies": {
"@eslint/js": "^10.0.0",
"@figma/plugin-typings": "^1.138.0",
"@types/node": "^24.13.6",
"@figma/plugin-typings": "^1.140.0",
"@types/node": "^24.19.1",
"@types/prismjs": "^1.26.6",
"@types/stringify-object": "^4.0.5",
"@vitejs/plugin-vue": "6.0.9",
Expand All @@ -58,8 +58,8 @@
"esbuild": "^0.28.2",
"eslint": "^10.11.0",
"eslint-config-flat-gitignore": "^2.4.0",
"eslint-plugin-perfectionist": "^5.12.0",
"eslint-plugin-vue": "^10.11.0",
"eslint-plugin-perfectionist": "^5.12.1",
"eslint-plugin-vue": "^10.11.1",
"jiti": "^2.7.0",
"p-limit": "^7.3.3",
"pino": "^10.3.1",
Expand All @@ -68,10 +68,10 @@
"proper-lockfile": "^4.1.2",
"tsx": "^4.23.15",
"typescript": "^6.0.3",
"vite": "8.3.0",
"vite": "8.3.2",
"vite-plugin-css-injected-by-js": "^5.0.2",
"vitest": "5.0.2",
"vue-tsc": "^3.3.11",
"vue-tsc": "^3.3.12",
"wxt": "^0.21.4",
"zod": "^4.6.5"
}
Expand Down
36 changes: 36 additions & 0 deletions packages/extension/tests/mcp/tools/canvas-theme.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,42 @@ describe('Canvas theme compilation', () => {
)
})

it.each([
{ color: { r: 1, g: 0, b: 0 }, opacity: { type: 'VARIABLE_ALIAS', id: 'var:opacity' } },
{ color: { type: 'VARIABLE_ALIAS', id: 'var:color' }, opacity: 50 },
{
color: { type: 'VARIABLE_ALIAS', id: 'var:color' },
opacity: { type: 'VARIABLE_ALIAS', id: 'var:opacity' }
}
] satisfies VariableComposedColor[])(
'rejects composed theme colors before mutation: %j',
async (value) => {
vi.stubGlobal('figma', {
variables: {
getLocalVariablesAsync: vi.fn().mockResolvedValue([
{
id: 'var:color',
name: 'Surface',
resolvedType: 'COLOR',
variableCollectionId: 'collection:1',
getSharedPluginData: () => 'token',
valuesByMode: { light: value }
}
]),
getLocalVariableCollectionsAsync: vi.fn().mockResolvedValue([]),
getVariableCollectionByIdAsync: vi.fn().mockResolvedValue({ defaultModeId: 'light' })
},
createFrame: vi.fn()
})
const spec = input('bg-(--token)')
delete spec.variableCollections
await expect(prepareCanvasTheme(spec)).rejects.toThrow(
'Theme variable "Surface" has an unsupported composed-color value.'
)
expect(figma.createFrame).not.toHaveBeenCalled()
}
)

it('accepts stable keys and catalog aliases for the same native variable and text style', () => {
const catalog = registerDesignSystemCatalog([
{
Expand Down
2 changes: 1 addition & 1 deletion packages/extension/tests/mcp/tools/canvas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ function createFixture(): FigmaFixture {
})
}

function createStyle(type: StyleType): BaseStyle {
function createStyle(type: 'PAINT' | 'TEXT' | 'EFFECT' | 'GRID'): BaseStyle {
const id = `style:authored:${nextStyleId++}`
const base = withSharedPluginData({
id,
Expand Down
59 changes: 59 additions & 0 deletions packages/extension/tests/mcp/tools/design-system.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,65 @@ describe('mcp/tools/design-system', () => {
])
})

it('omits composed colors from variable modes and overrides with one warning', async () => {
const composed: VariableComposedColor = {
color: { r: 1, g: 0, b: 0 },
opacity: { type: 'VARIABLE_ALIAS', id: 'variable:opacity' }
}
const surface = variable('variable:surface', 'Surface', 'collection:tokens', {
valuesByMode: {
'collection:tokens:light': { r: 1, g: 1, b: 1, a: 1 },
'collection:tokens:dark': composed
}
})
const tokens = collection('collection:tokens', 'Tokens', { variableIds: [surface.id] })
const extended = {
...collection('collection:extended', 'Extended'),
isExtension: true,
parentVariableCollectionId: tokens.id,
rootVariableCollectionId: tokens.id,
variableOverrides: {
[surface.id]: {
'collection:extended:light': {
color: { type: 'VARIABLE_ALIAS', id: surface.id },
opacity: 50
},
'collection:extended:dark': { type: 'VARIABLE_ALIAS', id: surface.id }
}
}
} as unknown as VariableCollection
const opacity = variable('variable:opacity', 'Opacity', tokens.id, {
resolvedType: 'FLOAT',
valuesByMode: { 'collection:tokens:light': 50 }
})
stubFigma({ localCollections: [tokens, extended], localVariables: [surface, opacity] })

const result = await handleGetDesignSystem()
expect(result.warnings).toEqual([
'Unsupported variable values were omitted from mode definitions.'
])
const catalog = requireDesignSystemCatalog(result.catalogId)
const surfaceRef = result.variables.find(({ name }) => name === 'Surface')!.ref
expect(catalog.entries.get(surfaceRef)?.definition).toMatchObject({
valuesByMode: { 'collection:tokens:light': { r: 1, g: 1, b: 1, a: 1 } }
})
expect(catalog.entries.get(surfaceRef)?.definition).not.toHaveProperty([
'valuesByMode',
'collection:tokens:dark'
])
const extendedRef = result.collections.find(({ name }) => name === 'Extended')!.ref
expect(catalog.entries.get(extendedRef)?.definition).toMatchObject({
variableOverrides: {
[surface.id]: { 'collection:extended:dark': { variable: { id: surface.id } } }
}
})
expect(catalog.entries.get(extendedRef)?.definition).not.toHaveProperty([
'variableOverrides',
surface.id,
'collection:extended:light'
])
})

it('summarizes styles and shaders while keeping exact definitions on demand', async () => {
stubFigma({
localStyles: [
Expand Down
8 changes: 4 additions & 4 deletions packages/mcp-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,19 @@
"prepublishOnly": "pnpm run build"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.30.0",
"@modelcontextprotocol/sdk": "^1.32.0",
"nanoid": "^6.0.1",
"pino": "^10.3.1",
"pino-pretty": "^13.1.3",
"proper-lockfile": "^4.1.2",
"ws": "^8.21.3",
"ws": "^8.22.0",
"zod": "^4.6.5"
},
"devDependencies": {
"@tempad-dev/shared": "workspace:^0.1.0",
"@types/node": "^24.13.6",
"@types/node": "^24.19.1",
"@types/proper-lockfile": "^4.1.4",
"@types/ws": "^8.18.1",
"@types/ws": "^8.18.2",
"tsdown": "^0.23.0",
"typescript": "^6.0.3",
"unplugin-raw": "^0.8.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"zod": "^4.6.5"
},
"devDependencies": {
"@figma/plugin-typings": "^1.138.0",
"@figma/plugin-typings": "^1.140.0",
"tsdown": "^0.23.0",
"typescript": "^6.0.3",
"vitest": "5.0.2"
Expand Down
8 changes: 4 additions & 4 deletions packages/site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,19 @@
},
"devDependencies": {
"@tsconfig/node24": "^24.0.5",
"@types/node": "^24.13.6",
"@types/node": "^24.19.1",
"@vitejs/plugin-vue": "^6.0.9",
"@vitest/browser-playwright": "5.0.2",
"@vue/eslint-config-typescript": "^14.9.0",
"@vue/tsconfig": "^0.9.1",
"eslint": "^10.11.0",
"eslint-plugin-vue": "~10.11.0",
"eslint-plugin-vue": "~10.11.1",
"jiti": "^2.7.0",
"marked": "^18.0.14",
"playwright": "^1.63.0",
"typescript": "~6.0.3",
"vite": "8.3.0",
"vue-tsc": "^3.3.11",
"vite": "8.3.2",
"vue-tsc": "^3.3.12",
"yaml": "^2.9.1"
},
"engines": {
Expand Down
Loading
Loading