Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/hidden-from-publishing-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tokens-studio/figma-plugin": patch
---

Add support for hiddenFromPublishing metadata on Figma variables with comprehensive test coverage. This feature allows users to control whether variables are hidden when publishing the current file as a library, with three states: inherit from collection (undefined), show in publishing (false), and hide from publishing (true).
123 changes: 123 additions & 0 deletions packages/tokens-studio-for-figma/cypress/e2e/tokens.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -571,4 +571,127 @@ describe('TokenListing', () => {
]
})
});

it('can configure figma variable extensions for a color token', () => {
cy.startup(mockStartupParams);
cy.receiveSetTokens({
version: '5',
values: {
global: [{
name: 'colors.primary',
value: '#0000ff',
type: 'color'
}],
},
});

// Click on an existing color token to edit it
cy.get('[data-testid="colors.primary"]').click();

// Add Figma variable configuration
cy.get('[data-testid="button-add-figma-variable"]').should('be.visible');
cy.get('[data-testid="button-add-figma-variable"]').click();

// Test variable scopes
cy.get('[id="scope-ALL_FILLS"]').should('be.visible');
cy.get('[id="scope-ALL_FILLS"]').check();
cy.get('[id="scope-TEXT_FILL"]').check();

// Test code syntax
cy.get('[id="syntax-Web"]').check();
cy.get('input[placeholder="Web syntax (e.g., --token-name)"]').type('--color-primary');

cy.get('[id="syntax-Android"]').check();
cy.get('input[placeholder="Android syntax (e.g., --token-name)"]').type('color_primary');

// Test publishing settings
cy.get('select').should('be.visible');
cy.get('select').select('hide');

// Save the token
cy.get('[data-testid="button-save-token"]').click();

// Verify the token has been updated with extensions
cy.get('@postMessage').should('be.called');
});

it('can remove figma variable extensions', () => {
cy.startup(mockStartupParams);
cy.receiveSetTokens({
version: '5',
values: {
global: [{
name: 'colors.secondary',
value: '#ff0000',
type: 'color',
$extensions: {
'com.figma': {
scopes: ['ALL_FILLS'],
codeSyntax: { Web: '--color-secondary' },
hiddenFromPublishing: true
}
}
}],
},
});

// Click on token with existing Figma extensions
cy.get('[data-testid="colors.secondary"]').click();

// Verify Figma section is visible with data
cy.get('[data-testid="button-remove-figma-variable"]').should('be.visible');
cy.get('[id="scope-ALL_FILLS"]').should('be.checked');
cy.get('[id="syntax-Web"]').should('be.checked');
cy.get('input[value="--color-secondary"]').should('exist');

// Remove Figma variable configuration
cy.get('[data-testid="button-remove-figma-variable"]').click();

// Verify sections are hidden
cy.get('[data-testid="button-add-figma-variable"]').should('be.visible');
cy.get('[id="scope-ALL_FILLS"]').should('not.exist');

// Save the token
cy.get('[data-testid="button-save-token"]').click();

cy.get('@postMessage').should('be.called');
});

it('shows only relevant scopes for different token types', () => {
cy.startup(mockStartupParams);
cy.receiveSetTokens({
version: '5',
values: {
global: [
{ name: 'colors.test', value: '#ff0000', type: 'color' },
{ name: 'spacing.test', value: '16px', type: 'spacing' },
{ name: 'fontSizes.test', value: '16px', type: 'fontSizes' }
],
},
});

// Test color token scopes
cy.get('[data-testid="colors.test"]').click();
cy.get('[data-testid="button-add-figma-variable"]').click();
cy.get('[id="scope-ALL_FILLS"]').should('be.visible');
cy.get('[id="scope-TEXT_FILL"]').should('be.visible');
cy.get('[id="scope-CORNER_RADIUS"]').should('not.exist'); // Should not show for color
cy.get('[data-testid="button-cancel-edit-token"]').click();

// Test spacing token scopes
cy.get('[data-testid="spacing.test"]').click();
cy.get('[data-testid="button-add-figma-variable"]').click();
cy.get('[id="scope-GAP"]').should('be.visible');
cy.get('[id="scope-WIDTH_HEIGHT"]').should('be.visible');
cy.get('[id="scope-ALL_FILLS"]').should('not.exist'); // Should not show for spacing
cy.get('[data-testid="button-cancel-edit-token"]').click();

// Test font size token scopes
cy.get('[data-testid="fontSizes.test"]').click();
cy.get('[data-testid="button-add-figma-variable"]').click();
cy.get('[id="scope-FONT_SIZE"]').should('be.visible');
cy.get('[id="scope-ALL_SCOPES"]').should('be.visible');
cy.get('[id="scope-GAP"]').should('not.exist'); // Should not show for font size
cy.get('[data-testid="button-cancel-edit-token"]').click();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ function EditTokenForm({ resolvedTokens }: Props) {
const newValue = { ...internalEditToken.$extensions?.['com.figma'] };
delete newValue?.scopes;
delete newValue?.codeSyntax;
delete newValue?.hiddenFromPublishing;
setInternalEditToken({
...internalEditToken,
$extensions: {
Expand All @@ -362,7 +363,7 @@ function EditTokenForm({ resolvedTokens }: Props) {
}, [internalEditToken]);

const handleFigmaVariableChange = React.useCallback(
(scopes: string[], codeSyntax: Partial<Record<string, string>>) => {
(scopes: string[], codeSyntax: Partial<Record<string, string>>, hiddenFromPublishing?: boolean) => {
setInternalEditToken({
...internalEditToken,
$extensions: {
Expand All @@ -371,6 +372,7 @@ function EditTokenForm({ resolvedTokens }: Props) {
...internalEditToken.$extensions?.['com.figma'],
scopes: scopes.length > 0 ? scopes : undefined,
codeSyntax: Object.keys(codeSyntax).length > 0 ? codeSyntax : undefined,
hiddenFromPublishing,
},
} as SingleToken['$extensions'],
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useCallback, useMemo } from 'react';
import {
IconButton, Heading, Checkbox, Text, Stack, Label,
IconButton, Heading, Checkbox, Text, Stack, Label, Select,
} from '@tokens-studio/ui';
import IconPlus from '@/icons/plus.svg';
import IconMinus from '@/icons/minus.svg';
Expand All @@ -12,6 +12,7 @@ import { TokenTypes } from '@/constants/TokenTypes';

type VariableScope = 'ALL_SCOPES' | 'TEXT_CONTENT' | 'CORNER_RADIUS' | 'WIDTH_HEIGHT' | 'GAP' | 'OPACITY' | 'STROKE_FLOAT' | 'EFFECT_FLOAT' | 'FONT_WEIGHT' | 'FONT_SIZE' | 'LINE_HEIGHT' | 'LETTER_SPACING' | 'PARAGRAPH_SPACING' | 'PARAGRAPH_INDENT' | 'ALL_FILLS' | 'FRAME_FILL' | 'SHAPE_FILL' | 'TEXT_FILL' | 'STROKE_COLOR' | 'EFFECT_COLOR' | 'FONT_FAMILY' | 'FONT_STYLE';
type CodeSyntaxPlatform = 'Web' | 'Android' | 'iOS';
type HiddenFromPublishingOption = 'inherit' | 'show' | 'hide';

const variableScopeOptions: { value: VariableScope; label: string }[] = [
{ value: 'ALL_SCOPES', label: 'Show in all supported properties' },
Expand Down Expand Up @@ -97,13 +98,19 @@ const codeSyntaxPlatformOptions: { value: CodeSyntaxPlatform; label: string }[]
{ value: 'iOS', label: 'iOS' },
];

const hiddenFromPublishingOptions: { value: HiddenFromPublishingOption; label: string }[] = [
{ value: 'inherit', label: 'Inherit from collection' },
{ value: 'show', label: 'Show in publishing' },
{ value: 'hide', label: 'Hide from publishing' },
];

export default function FigmaVariableForm({
internalEditToken,
handleFigmaVariableChange,
handleRemoveFigmaVariable,
}: {
internalEditToken: EditTokenObject;
handleFigmaVariableChange: (scopes: VariableScope[], codeSyntax: Partial<Record<CodeSyntaxPlatform, string>>) => void;
handleFigmaVariableChange: (scopes: VariableScope[], codeSyntax: Partial<Record<CodeSyntaxPlatform, string>>, hiddenFromPublishing?: boolean) => void;
handleRemoveFigmaVariable: () => void;
}) {
const [figmaVariableVisible, setFigmaVariableVisible] = React.useState(false);
Expand All @@ -112,6 +119,12 @@ export default function FigmaVariableForm({

const currentCodeSyntax = useMemo(() => internalEditToken?.$extensions?.['com.figma']?.codeSyntax as Partial<Record<CodeSyntaxPlatform, string>> || {}, [internalEditToken]);

const currentHiddenFromPublishing = useMemo(() => {
const hiddenValue = internalEditToken?.$extensions?.['com.figma']?.hiddenFromPublishing;
if (hiddenValue === undefined) return 'inherit';
return hiddenValue ? 'hide' : 'show';
}, [internalEditToken]);

const shouldShowFigmaVariableSection = useMemo(() => tokenTypesToCreateVariable.includes(internalEditToken.type), [internalEditToken.type]);

// Filter variable scope options based on token type
Expand All @@ -121,14 +134,18 @@ export default function FigmaVariableForm({
}, [internalEditToken.type]);

React.useEffect(() => {
if (internalEditToken?.$extensions?.['com.figma']?.scopes || internalEditToken?.$extensions?.['com.figma']?.codeSyntax) {
if (
internalEditToken?.$extensions?.['com.figma']?.scopes
|| internalEditToken?.$extensions?.['com.figma']?.codeSyntax
|| internalEditToken?.$extensions?.['com.figma']?.hiddenFromPublishing !== undefined
) {
setFigmaVariableVisible(true);
}
}, [internalEditToken]);

const addFigmaVariable = useCallback(() => {
setFigmaVariableVisible(true);
handleFigmaVariableChange([], {});
handleFigmaVariableChange([], {}, undefined);
}, [handleFigmaVariableChange]);

const removeFigmaVariable = useCallback(() => {
Expand All @@ -151,8 +168,9 @@ export default function FigmaVariableForm({
newScopes = currentScopes.filter((s) => s !== scope);
}

handleFigmaVariableChange(newScopes, currentCodeSyntax);
}, [currentScopes, currentCodeSyntax, handleFigmaVariableChange]);
const hiddenValue = currentHiddenFromPublishing === 'inherit' ? undefined : currentHiddenFromPublishing === 'hide';
handleFigmaVariableChange(newScopes, currentCodeSyntax, hiddenValue);
}, [currentScopes, currentCodeSyntax, currentHiddenFromPublishing, handleFigmaVariableChange]);

const handleCodeSyntaxPlatformChange = useCallback((platform: CodeSyntaxPlatform, checked: boolean) => {
const newCodeSyntax = { ...currentCodeSyntax };
Expand All @@ -161,13 +179,20 @@ export default function FigmaVariableForm({
} else {
delete newCodeSyntax[platform];
}
handleFigmaVariableChange(currentScopes, newCodeSyntax);
}, [currentScopes, currentCodeSyntax, handleFigmaVariableChange]);
const hiddenValue = currentHiddenFromPublishing === 'inherit' ? undefined : currentHiddenFromPublishing === 'hide';
handleFigmaVariableChange(currentScopes, newCodeSyntax, hiddenValue);
}, [currentScopes, currentCodeSyntax, currentHiddenFromPublishing, handleFigmaVariableChange]);

const handleCodeSyntaxValueChange = useCallback((platform: CodeSyntaxPlatform, value: string) => {
const newCodeSyntax = { ...currentCodeSyntax };
newCodeSyntax[platform] = value;
handleFigmaVariableChange(currentScopes, newCodeSyntax);
const hiddenValue = currentHiddenFromPublishing === 'inherit' ? undefined : currentHiddenFromPublishing === 'hide';
handleFigmaVariableChange(currentScopes, newCodeSyntax, hiddenValue);
}, [currentScopes, currentCodeSyntax, currentHiddenFromPublishing, handleFigmaVariableChange]);

const handleHiddenFromPublishingChange = useCallback((option: HiddenFromPublishingOption) => {
const hiddenValue = option === 'inherit' ? undefined : option === 'hide';
handleFigmaVariableChange(currentScopes, currentCodeSyntax, hiddenValue);
}, [currentScopes, currentCodeSyntax, handleFigmaVariableChange]);

const handleScopeCheckedChange = useCallback((scope: VariableScope) => (checked: boolean | string) => {
Expand Down Expand Up @@ -278,6 +303,23 @@ export default function FigmaVariableForm({
))}
</Stack>
</Box>

<Box>
<Text muted size="small" css={{ marginBottom: '$2' }}>
Publishing
</Text>
<Select
value={currentHiddenFromPublishing}
onValueChange={handleHiddenFromPublishingChange}
css={{ width: '100%' }}
>
{hiddenFromPublishingOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</Select>
</Box>
</Stack>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,17 +378,25 @@ describe('radial and conic gradients', () => {
const result = convertStringToFigmaGradient('radial-gradient(#ff0000, #0000ff)');
expect(result.type).toEqual('GRADIENT_RADIAL');
expect(result.gradientStops).toHaveLength(2);
expect(result.gradientStops[0].color).toEqual({ r: 1, g: 0, b: 0, a: 1 });
expect(result.gradientStops[1].color).toEqual({ r: 0, g: 0, b: 1, a: 1 });
expect(result.gradientStops[0].color).toEqual({
r: 1, g: 0, b: 0, a: 1,
});
expect(result.gradientStops[1].color).toEqual({
r: 0, g: 0, b: 1, a: 1,
});
expect(result.gradientTransform).toEqual([[1, 0, 0], [0, 1, 0]]);
});

it('should convert conic gradient', () => {
const result = convertStringToFigmaGradient('conic-gradient(#ff0000, #0000ff)');
expect(result.type).toEqual('GRADIENT_ANGULAR');
expect(result.gradientStops).toHaveLength(2);
expect(result.gradientStops[0].color).toEqual({ r: 1, g: 0, b: 0, a: 1 });
expect(result.gradientStops[1].color).toEqual({ r: 0, g: 0, b: 1, a: 1 });
expect(result.gradientStops[0].color).toEqual({
r: 1, g: 0, b: 0, a: 1,
});
expect(result.gradientStops[1].color).toEqual({
r: 0, g: 0, b: 1, a: 1,
});
});

it('should convert conic gradient with from angle', () => {
Expand Down Expand Up @@ -429,8 +437,18 @@ describe('convertFigmaGradientToString with gradient types', () => {
const paint: GradientPaint = {
type: 'GRADIENT_RADIAL',
gradientStops: [
{ color: { r: 1, g: 0, b: 0, a: 1 }, position: 0 },
{ color: { r: 0, g: 0, b: 1, a: 1 }, position: 1 },
{
color: {
r: 1, g: 0, b: 0, a: 1,
},
position: 0,
},
{
color: {
r: 0, g: 0, b: 1, a: 1,
},
position: 1,
},
],
gradientTransform: [[1, 0, 0], [0, 1, 0]],
};
Expand All @@ -442,8 +460,18 @@ describe('convertFigmaGradientToString with gradient types', () => {
const paint: GradientPaint = {
type: 'GRADIENT_ANGULAR',
gradientStops: [
{ color: { r: 1, g: 0, b: 0, a: 1 }, position: 0 },
{ color: { r: 0, g: 0, b: 1, a: 1 }, position: 1 },
{
color: {
r: 1, g: 0, b: 0, a: 1,
},
position: 0,
},
{
color: {
r: 0, g: 0, b: 1, a: 1,
},
position: 1,
},
],
gradientTransform: [[1, 0, 0], [0, 1, 0]],
};
Expand All @@ -455,8 +483,18 @@ describe('convertFigmaGradientToString with gradient types', () => {
const paint: GradientPaint = {
type: 'GRADIENT_DIAMOND',
gradientStops: [
{ color: { r: 1, g: 0, b: 0, a: 1 }, position: 0 },
{ color: { r: 0, g: 0, b: 1, a: 1 }, position: 1 },
{
color: {
r: 1, g: 0, b: 0, a: 1,
},
position: 0,
},
{
color: {
r: 0, g: 0, b: 1, a: 1,
},
position: 1,
},
],
gradientTransform: [[1, 0, 0], [0, 1, 0]],
};
Expand Down
Loading