Skip to content
Open
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/orange-geckos-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tokens-studio/figma-plugin": minor
---

Add token deprecation feature
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@ export default function ChangedTokenItem({
</Stack>
</Stack>
)}
{((token.importType === 'UPDATE' && typeof token.oldDeprecated === 'boolean') || (token.importType === 'NEW' && token.$deprecated)) && (
<Stack direction="row" align="center" justify="between" gap={1}>
<Text size="small">{t('deprecated')}</Text>
<Stack direction="row" align="center" gap={1}>
{typeof token.oldDeprecated === 'boolean' ? (
<StyledDiff type="danger">
{String(token.oldDeprecated)}
</StyledDiff>
) : null}
<StyledDiff type="success">
{String(!!token.$deprecated)}
</StyledDiff>
</Stack>
</Stack>
)}
{
token.importType === 'REMOVE' && (
<StyledDiff type="danger">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import {
Button, Heading, Textarea, Label, Stack,
Button, Heading, Textarea, Label, Stack, Switch,
} from '@tokens-studio/ui';
import { track } from '@/utils/analytics';
import { Dispatch } from '../store';
Expand All @@ -24,7 +24,7 @@ import { checkIfAlias, checkIfContainsAlias, getAliasValue } from '@/utils/alias
import { ResolveTokenValuesResult } from '@/utils/tokenHelpers';
import {
activeTokenSetSelector, editTokenSelector, themesListSelector, tokensSelector,
showEditFormSelector,
showEditFormSelector, hideDeprecatedTokensSelector,
} from '@/selectors';
import { TokenTypes } from '@/constants/TokenTypes';
import TypographyInput from './TypographyInput';
Expand Down Expand Up @@ -56,10 +56,11 @@ type Choice = { key: string; label: string; enabled?: boolean; unique?: boolean

// @TODO this needs to be reviewed from a typings perspective + performance
function EditTokenForm({ resolvedTokens }: Props) {
const { t } = useTranslation(['tokens', 'errors']);
const { t } = useTranslation(['tokens', 'errors', 'general']);
const activeTokenSet = useSelector(activeTokenSetSelector);
const tokens = useSelector(tokensSelector);
const editToken = useSelector(editTokenSelector);
const hideDeprecatedTokens = useSelector(hideDeprecatedTokensSelector);
const themes = useSelector(themesListSelector);
const [selectedTokenSets, setSelectedTokenSets] = React.useState<string[]>([activeTokenSet]);
const {
Expand Down Expand Up @@ -414,6 +415,16 @@ function EditTokenForm({ resolvedTokens }: Props) {
[internalEditToken],
);

const handleDeprecatedChange = React.useCallback(
(checked: boolean) => {
setInternalEditToken({
...internalEditToken,
$deprecated: checked ? true : undefined,
});
},
[internalEditToken],
);

const resolvedValue = React.useMemo(() => {
if (internalEditToken) {
return typeof internalEditToken?.value === 'string'
Expand All @@ -425,7 +436,7 @@ function EditTokenForm({ resolvedTokens }: Props) {

// @TODO update to useCallback
const submitTokenValue = async ({
type, value, name, $extensions,
type, value, name, $extensions, $deprecated,
}: EditTokenObject) => {
if (internalEditToken && value && name) {
let oldName: string | undefined;
Expand All @@ -441,6 +452,7 @@ function EditTokenForm({ resolvedTokens }: Props) {
track('Create token', { type: internalEditToken.type, isModifier: !!$extensions?.['studio.tokens']?.modify });
createSingleToken({
description: internalEditToken.description ?? internalEditToken.oldDescription,
$deprecated: $deprecated ?? undefined,
parent: activeTokenSet,
name: newName,
type,
Expand All @@ -450,6 +462,7 @@ function EditTokenForm({ resolvedTokens }: Props) {
} else if (internalEditToken.status === EditTokenFormStatus.EDIT) {
editSingleToken({
description: internalEditToken.description ?? internalEditToken.oldDescription,
$deprecated: $deprecated ?? undefined,
parent: activeTokenSet,
name: newName,
oldName,
Expand Down Expand Up @@ -748,6 +761,21 @@ function EditTokenForm({ resolvedTokens }: Props) {
css={{ fontSize: '$xsmall', padding: '$3' }}
/>
</Box>
<Box css={{ display: 'flex', flexDirection: 'column', gap: '$2' }}>
<Box css={{ display: 'flex', alignItems: 'center', gap: '$3' }}>
<Switch
id="deprecated"
checked={!!internalEditToken?.$deprecated}
onCheckedChange={handleDeprecatedChange}
/>
<Label css={{ fontWeight: '$sansRegular', fontSize: '$xsmall' }} htmlFor="deprecated">{t('deprecated', { ns: 'general' })}</Label>
</Box>
{internalEditToken?.$deprecated && hideDeprecatedTokens && (
<Text size="xsmall" muted css={{ color: '$dangerFg' }}>
{t('deprecatedHiddenWarning')}
</Text>
)}
</Box>
<FigmaVariableForm
internalEditToken={internalEditToken}
handleFigmaVariableChange={handleFigmaVariableChange}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,7 @@ describe('Initiator', () => {
stylesColor: true,
removeStylesAndVariablesWithoutConnection: false,
renameExistingStylesAndVariables: false,
skipDeprecatedTokensInVariableSync: false,
createStylesWithVariableReferences: false,
ignoreFirstPartForStyles: false,
prefixStylesWithThemeName: false,
Expand Down Expand Up @@ -657,6 +658,7 @@ describe('Initiator', () => {
stylesGradient: false,
removeStylesAndVariablesWithoutConnection: false,
renameExistingStylesAndVariables: false,
skipDeprecatedTokensInVariableSync: false,
createStylesWithVariableReferences: false,
ignoreFirstPartForStyles: false,
prefixStylesWithThemeName: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
stylesEffectSelector,
stylesTypographySelector,
stylesGradientSelector,
skipDeprecatedTokensInVariableSyncSelector,
} from '@/selectors';
import ignoreFirstPartImage from '@/app/assets/hints/ignoreFirstPartForStyles.png';
import prefixStylesImage from '@/app/assets/hints/prefixStyles.png';
Expand All @@ -49,6 +50,7 @@ export default function OptionsModal({ isOpen, title, closeAction }: { isOpen: b
const variablesNumber = useSelector(variablesNumberSelector);
const variablesBoolean = useSelector(variablesBooleanSelector);
const variablesString = useSelector(variablesStringSelector);
const skipDeprecatedTokensInVariableSync = useSelector(skipDeprecatedTokensInVariableSyncSelector);
const stylesColor = useSelector(stylesColorSelector);
const stylesTypography = useSelector(stylesTypographySelector);
const stylesEffect = useSelector(stylesEffectSelector);
Expand Down Expand Up @@ -145,6 +147,13 @@ export default function OptionsModal({ isOpen, title, closeAction }: { isOpen: b
[dispatch.settings],
);

const handleSkipDeprecatedChange = React.useCallback(
(checked: boolean) => {
dispatch.settings.setSkipDeprecatedTokensInVariableSync(checked);
},
[dispatch.settings],
);

const handleSaveOptions = React.useCallback(() => {
closeAction();
}, [closeAction]);
Expand Down Expand Up @@ -271,6 +280,21 @@ export default function OptionsModal({ isOpen, title, closeAction }: { isOpen: b
</ExplainerModal>
</StyledCheckboxGrid>
</Stack>
<Stack direction="column" gap={3}>
<Text bold css={{ fontSize: '$medium' }}>{t('options.deprecatedTokens')}</Text>
<StyledCheckboxGrid>
<Switch
data-testid="skipDeprecatedTokens"
id="skipDeprecatedTokens"
checked={!!skipDeprecatedTokensInVariableSync}
onCheckedChange={handleSkipDeprecatedChange}
/>
<Label css={{ fontWeight: '$sansRegular', fontSize: '$xsmall' }} htmlFor="skipDeprecatedTokens">{t('options.skipDeprecatedTokens')}</Label>
<ExplainerModal title={t('options.skipDeprecatedTokens')}>
<Box>{t('options.skipDeprecatedTokensExplanation')}</Box>
</ExplainerModal>
</StyledCheckboxGrid>
</Stack>
</form>
</Stack>
</Modal>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Box, DropdownMenu, IconButton } from '@tokens-studio/ui';
import { Check, Settings } from 'iconoir-react';
import { Dispatch } from '../store';
import {
settingsStateSelector, localApiStateSelector, autoApplyThemeOnDropSelector, shouldSwapFigmaModesSelector,
settingsStateSelector, localApiStateSelector, autoApplyThemeOnDropSelector, shouldSwapFigmaModesSelector, hideDeprecatedTokensSelector,
} from '@/selectors';
import { isEqual } from '@/utils/isEqual';
import { track } from '@/utils/analytics';
Expand All @@ -21,6 +21,7 @@ export default function SettingsDropdown() {
} = useSelector(settingsStateSelector, isEqual);
const autoApplyThemeOnDrop = useSelector(autoApplyThemeOnDropSelector);
const shouldSwapFigmaModes = useSelector(shouldSwapFigmaModesSelector);
const hideDeprecatedTokens = useSelector(hideDeprecatedTokensSelector);

const {
setUpdateOnChange, setUpdateRemote, setShouldSwapStyles, setShouldSwapFigmaModes, setShouldUpdateStyles, setAutoApplyThemeOnDrop,
Expand Down Expand Up @@ -62,6 +63,12 @@ export default function SettingsDropdown() {
setShouldSwapFigmaModes(newValue);
}, [shouldSwapFigmaModes, setShouldSwapFigmaModes]);

const { toggleHideDeprecatedTokens } = useDispatch<Dispatch>().uiState;

const handleToggleHideDeprecated = React.useCallback(() => {
toggleHideDeprecatedTokens();
}, [toggleHideDeprecatedTokens]);

return (
<DropdownMenu>
<DropdownMenu.Trigger asChild data-testid="bottom-bar-settings">
Expand Down Expand Up @@ -150,6 +157,20 @@ export default function SettingsDropdown() {
{t('update.shouldSwapFigmaModes.description')}
</Box>
</DropdownMenu.CheckboxItem>
<DropdownMenu.Separator />
<DropdownMenu.CheckboxItem
data-testid="hide-deprecated-toggle"
checked={hideDeprecatedTokens}
onCheckedChange={handleToggleHideDeprecated}
>
<DropdownMenu.ItemIndicator>
<Check />
</DropdownMenu.ItemIndicator>
{t('update.hideDeprecated.title')}
<Box css={{ color: '$fgMuted', fontSize: '$xxsmall' }}>
{t('update.hideDeprecated.description')}
</Box>
</DropdownMenu.CheckboxItem>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ export const StyledTokenButton = styled('button', {
borderColor: '$borderDefault',
},
},
deprecated: {
true: {
boxShadow: '0 0 0 2px $colors$dangerFg',
'&:hover, &:focus': {
boxShadow: '0 0 0 2px $colors$dangerFg',
opacity: 0.6,
},
},
},
},
compoundVariants: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export default function TokenButtonContent({

return (
<TokenTooltip token={token}>
<StyledTokenButton tokenType={type as TokenTypes.COLOR} displayType={type === TokenTypes.COLOR ? displayType : 'GRID'} active={active} disabled={uiDisabled} type="button" onClick={handleButtonClick} css={cssOverrides}>
<StyledTokenButton tokenType={type as TokenTypes.COLOR} displayType={type === TokenTypes.COLOR ? displayType : 'GRID'} active={active} disabled={uiDisabled} deprecated={!!token.$deprecated} type="button" onClick={handleButtonClick} css={cssOverrides}>
<BrokenReferenceIndicator token={token} />
<StyledTokenButtonText>{showValue && <span>{visibleName}</span>}</StyledTokenButtonText>
</StyledTokenButton>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import useTokens from '../store/useTokens';
import AttentionIcon from '@/icons/attention.svg';
import { TokensContext } from '@/context';
import {
activeTokenSetSelector, aliasBaseFontSizeSelector, manageThemesModalOpenSelector, scrollPositionSetSelector, showEditFormSelector, tokenFilterSelector, tokensSelector, tokenTypeSelector, usedTokenSetSelector,
activeTokenSetSelector, aliasBaseFontSizeSelector, manageThemesModalOpenSelector, scrollPositionSetSelector, showEditFormSelector, tokenFilterSelector, tokensSelector, tokenTypeSelector, usedTokenSetSelector, hideDeprecatedTokensSelector,
} from '@/selectors';
import { ThemeSelector } from './ThemeSelector';
import { ManageThemesModal } from './ManageThemesModal';
Expand Down Expand Up @@ -92,6 +92,7 @@ function Tokens({ isActive }: { isActive: boolean }) {
const manageThemesModalOpen = useSelector(manageThemesModalOpenSelector);
const scrollPositionSet = useSelector(scrollPositionSetSelector);
const tokenFilter = useSelector(tokenFilterSelector);
const hideDeprecatedTokens = useSelector(hideDeprecatedTokensSelector);
const aliasBaseFontSize = useSelector(aliasBaseFontSizeSelector);
const dispatch = useDispatch<Dispatch>();
const [tokenSetsVisible, setTokenSetsVisible] = React.useState(true);
Expand Down Expand Up @@ -126,7 +127,7 @@ function Tokens({ isActive }: { isActive: boolean }) {

const memoizedTokens = React.useMemo(() => {
if (tokens[activeTokenSet]) {
const mapped = mappedTokens(tokens[activeTokenSet], tokenFilter).sort((a, b) => {
const mapped = mappedTokens(tokens[activeTokenSet], tokenFilter, hideDeprecatedTokens).sort((a, b) => {
if (b[1].values) {
return 1;
}
Expand All @@ -143,7 +144,7 @@ function Tokens({ isActive }: { isActive: boolean }) {
}));
}
return [];
}, [tokens, activeTokenSet, tokenFilter]);
}, [tokens, activeTokenSet, tokenFilter, hideDeprecatedTokens]);

const handleToggleTokenSetsVisibility = React.useCallback(() => {
setTokenSetsVisible(!tokenSetsVisible);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,3 +256,22 @@ describe('createTokenObj', () => {
expect(result).toEqual(RESULT_TOKENS);
});
});

describe('createTokensObject — hideDeprecated', () => {
const tokens = [
{ id: '1', type: TokenTypes.COLOR, name: 'color.active', value: '#ff0000' },
{ id: '2', type: TokenTypes.COLOR, name: 'color.deprecated', value: '#0000ff', $deprecated: true },
];

it('includes deprecated tokens when hideDeprecated is false (default)', () => {
const result = createTokensObject(tokens);
expect((result[TokenTypes.COLOR]?.values as any)?.color?.active).toBeDefined();
expect((result[TokenTypes.COLOR]?.values as any)?.color?.deprecated).toBeDefined();
});

it('excludes deprecated tokens when hideDeprecated is true', () => {
const result = createTokensObject(tokens, '', true);
expect((result[TokenTypes.COLOR]?.values as any)?.color?.active).toBeDefined();
expect((result[TokenTypes.COLOR]?.values as any)?.color?.deprecated).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,10 @@ export function appendTypeToToken(token: Omit<SingleToken, 'type'> & { type?: To
}

// Creates a tokens object so that tokens are displayed in groups in the UI.
export function createTokensObject(tokens: (Omit<SingleToken, 'type'> & { type?: TokenTypes; })[], tokenFilter = '') {
export function createTokensObject(tokens: (Omit<SingleToken, 'type'> & { type?: TokenTypes; })[], tokenFilter = '', hideDeprecated = false) {
if (tokens.length > 0) {
const obj = tokens.reduce<CreateTokensObjectResult>((acc, cur) => {
if (hideDeprecated && (cur as SingleToken).$deprecated) return acc;
let hasSubstring:boolean = false;
try {
hasSubstring = cur.name?.toLowerCase().search(tokenFilter?.toLowerCase()) >= 0;
Expand All @@ -90,12 +91,12 @@ export function createTokensObject(tokens: (Omit<SingleToken, 'type'> & { type?:

// Takes an array of tokens, transforms them into
// an object and merges that with values we require for the UI
export function mappedTokens(tokens: SingleToken[], tokenFilter: string) {
export function mappedTokens(tokens: SingleToken[], tokenFilter: string, hideDeprecated = false) {
const tokenObj = extend(true, {}, tokenTypes) as Record<
TokenTypes,
TokenTypeSchema & { values: DeepKeyTokenMap }
>;
const tokenObjects = createTokensObject(tokens, tokenFilter);
const tokenObjects = createTokensObject(tokens, tokenFilter, hideDeprecated);

Object.entries(tokenObjects).forEach(([key, group]) => {
tokenObj[key as TokenTypes] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export * from './setCreateStylesWithVariableReferences';
export * from './setIgnoreFirstPartForStyles';
export * from './setRenameExistingStylesAndVariables';
export * from './setRemoveStylesAndVariablesWithoutConnection';
export * from './setSkipDeprecatedTokensInVariableSync';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { SettingsState } from '../../settings';

export function setSkipDeprecatedTokensInVariableSync(state: SettingsState, payload: boolean): SettingsState {
return {
...state,
skipDeprecatedTokensInVariableSync: payload,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export interface SettingsState {
renameExistingStylesAndVariables?: boolean;
removeStylesAndVariablesWithoutConnection?: boolean;
autoApplyThemeOnDrop?: boolean;
skipDeprecatedTokensInVariableSync?: boolean;
seenGenericVersionedHeaderMigrationDialog?: boolean;
seenTermsUpdate2026?: boolean;
}
Expand Down Expand Up @@ -100,6 +101,7 @@ export const settings = createModel<RootModel>()({
variablesString: true,
variablesNumber: true,
variablesBoolean: true,
skipDeprecatedTokensInVariableSync: false,
stylesColor: true,
stylesTypography: true,
stylesEffect: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ export const tokenState = createModel<RootModel>()({
const index = state.tokens[data.parent].findIndex((token) => token.name === nameToFind);
const newArray = [...state.tokens[data.parent]];
newArray[index] = {
...omit(newArray[index], 'description'),
...omit(newArray[index], 'description', '$deprecated'),
...updateTokenPayloadToSingleToken(data),
} as SingleToken;
return {
Expand Down
Loading