diff --git a/.changeset/tidy-kids-raise.md b/.changeset/tidy-kids-raise.md new file mode 100644 index 00000000..85738cd3 --- /dev/null +++ b/.changeset/tidy-kids-raise.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/graph-engine-nodes-figma": minor +--- + +With the new "Scope Token Array by Type" node you are able to run the scoping on a full array of tokens. diff --git a/packages/nodes-figma/src/nodes/index.ts b/packages/nodes-figma/src/nodes/index.ts index c312621c..1ce32c94 100644 --- a/packages/nodes-figma/src/nodes/index.ts +++ b/packages/nodes-figma/src/nodes/index.ts @@ -5,6 +5,7 @@ import scopeByType from './scopeByType.js'; import scopeColor from './scopeColor.js'; import scopeNumber from './scopeNumber.js'; import scopeString from './scopeString.js'; +import scopeTokenArrayByType from './scopeTokenArrayByType.js'; export const nodes = [ codeSyntax, @@ -13,5 +14,6 @@ export const nodes = [ scopeByType, scopeColor, scopeNumber, - scopeString + scopeString, + scopeTokenArrayByType ]; diff --git a/packages/nodes-figma/src/nodes/scopeTokenArrayByType.ts b/packages/nodes-figma/src/nodes/scopeTokenArrayByType.ts new file mode 100644 index 00000000..879e3741 --- /dev/null +++ b/packages/nodes-figma/src/nodes/scopeTokenArrayByType.ts @@ -0,0 +1,80 @@ +import { + INodeDefinition, + Node, + ToInput, + ToOutput +} from '@tokens-studio/graph-engine'; +import { SingleToken } from '@tokens-studio/types'; +import { TokenSchema } from '@tokens-studio/graph-engine-nodes-design-tokens/schemas/index.js'; +import { arrayOf } from '@tokens-studio/graph-engine-nodes-design-tokens/schemas/utils.js'; +import { mergeTokenExtensions } from '../utils/tokenMerge.js'; + +export default class ScopeTokenArrayByType extends Node { + static title = 'Scope Token Array by Type'; + static type = 'studio.tokens.figma.scopeTokenArrayByType'; + static description = + 'Automatically sets Figma scopes based on token type for an array of tokens'; + + declare inputs: ToInput<{ tokens: SingleToken[] }>; + declare outputs: ToOutput<{ tokens: SingleToken[] }>; + + constructor(props: INodeDefinition) { + super(props); + this.addInput('tokens', { + type: arrayOf(TokenSchema) + }); + this.addOutput('tokens', { + type: arrayOf(TokenSchema) + }); + } + + // Reuse the same scopes mapping as the single token version + getScopesByType = token => { + switch (token.type) { + case 'color': + return ['ALL_FILLS', 'STROKE_COLOR', 'EFFECT_COLOR']; + case 'dimension': + return [ + 'GAP', + 'WIDTH_HEIGHT', + 'CORNER_RADIUS', + 'STROKE_FLOAT', + 'EFFECT_FLOAT', + 'PARAGRAPH_INDENT' + ]; + case 'spacing': + return ['GAP', 'WIDTH_HEIGHT']; + case 'borderRadius': + return ['CORNER_RADIUS']; + case 'fontFamilies': + return ['FONT_FAMILY']; + case 'fontWeights': + return ['FONT_WEIGHT']; + case 'fontSizes': + return ['FONT_SIZE']; + case 'lineHeights': + return ['LINE_HEIGHT']; + case 'letterSpacing': + return ['LETTER_SPACING']; + case 'paragraphSpacing': + return ['PARAGRAPH_SPACING']; + case 'opacity': + return ['OPACITY']; + case 'sizing': + return ['WIDTH_HEIGHT']; + default: + return []; + } + }; + + execute(): void | Promise { + const { tokens } = this.getAllInputs(); + + const modifiedTokens = tokens.map((token: SingleToken) => { + const newScopes = this.getScopesByType(token); + return mergeTokenExtensions(token, { scopes: newScopes }); + }); + + this.outputs.tokens.set(modifiedTokens); + } +} diff --git a/packages/nodes-figma/tests/scopeTokenArrayByType.test.ts b/packages/nodes-figma/tests/scopeTokenArrayByType.test.ts new file mode 100644 index 00000000..e74ca1bb --- /dev/null +++ b/packages/nodes-figma/tests/scopeTokenArrayByType.test.ts @@ -0,0 +1,101 @@ +import { Graph } from '@tokens-studio/graph-engine'; +import { SingleToken } from '@tokens-studio/types'; +import { describe, expect, test } from 'vitest'; +import ScopeTokenArrayByType from '../src/nodes/scopeTokenArrayByType.js'; + +describe('nodes/scopeTokenArrayByType', () => { + test('adds color scopes to color tokens in array', async () => { + const graph = new Graph(); + const node = new ScopeTokenArrayByType({ graph }); + + const mockToken = { + name: 'test', + value: '#ff0000', + type: 'color' + } as SingleToken; + + node.inputs.tokens.setValue([mockToken]); + await node.execute(); + + expect(node.outputs.tokens.value).toEqual([ + { + ...mockToken, + $extensions: { + 'com.figma': { + scopes: ['ALL_FILLS', 'STROKE_COLOR', 'EFFECT_COLOR'] + } + } + } + ]); + }); + + test('adds dimension scopes to dimension tokens in array', async () => { + const graph = new Graph(); + const node = new ScopeTokenArrayByType({ graph }); + + const mockToken = { + name: 'test', + value: '16px', + type: 'dimension' + } as SingleToken; + + node.inputs.tokens.setValue([mockToken]); + await node.execute(); + + expect(node.outputs.tokens.value).toEqual([ + { + ...mockToken, + $extensions: { + 'com.figma': { + scopes: [ + 'GAP', + 'WIDTH_HEIGHT', + 'CORNER_RADIUS', + 'STROKE_FLOAT', + 'EFFECT_FLOAT', + 'PARAGRAPH_INDENT' + ] + } + } + } + ]); + }); + + test('preserves existing extensions and merges scopes for tokens in array', async () => { + const graph = new Graph(); + const node = new ScopeTokenArrayByType({ graph }); + + const mockToken = { + name: 'test', + value: '#ff0000', + type: 'color', + $extensions: { + 'com.figma': { + scopes: ['TEXT_FILL'], + otherProp: true + }, + 'other.extension': { + someProp: 'value' + } + } + } as unknown as SingleToken; + + node.inputs.tokens.setValue([mockToken]); + await node.execute(); + + expect(node.outputs.tokens.value).toEqual([ + { + ...mockToken, + $extensions: { + 'com.figma': { + scopes: ['TEXT_FILL', 'ALL_FILLS', 'STROKE_COLOR', 'EFFECT_COLOR'], + otherProp: true + }, + 'other.extension': { + someProp: 'value' + } + } + } + ]); + }); +});