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/auto-enable-first-theme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tokens-studio/figma-plugin": patch
---

Auto-enable the first theme when no active theme is set during plugin launch or when pulling variables
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { TokenSetStatus } from '@/constants/TokenSetStatus';
import { INTERNAL_THEMES_NO_GROUP } from '@/constants/InternalTokenGroup';
import { TokenState } from '../../tokenState';
import { setTokenData } from '../tokenState/setTokenData';

describe('Auto-enable first theme', () => {
describe('setTokenData', () => {
it('should auto-enable the first theme when no active theme is set', () => {
const state = {
tokens: {},
themes: [],
activeTheme: {},
usedTokenSet: {},
activeTokenSet: 'global',
lastSyncedState: '[]',
tokensSize: 0,
themesSize: 0,
} as unknown as TokenState;

const payload = {
values: {
'collection/mode': [{ name: 'color.primary', value: '#ff0000', type: 'color' }],
},
themes: [
{
id: 'light',
name: 'Light',
group: 'Theme',
selectedTokenSets: {
'collection/mode': TokenSetStatus.ENABLED,
},
},
{
id: 'dark',
name: 'Dark',
group: 'Theme',
selectedTokenSets: {
'collection/mode': TokenSetStatus.ENABLED,
},
},
],
activeTheme: {},
};

const result = setTokenData(state, payload);

// Should enable the first theme
expect(result.activeTheme).toEqual({
Theme: 'light',
});

// Should update usedTokenSet based on the first theme
expect(result.usedTokenSet).toEqual({
'collection/mode': TokenSetStatus.ENABLED,
});
});

it('should auto-enable the first theme with INTERNAL_THEMES_NO_GROUP when theme has no group', () => {
const state = {
tokens: {},
themes: [],
activeTheme: {},
usedTokenSet: {},
activeTokenSet: 'global',
lastSyncedState: '[]',
tokensSize: 0,
themesSize: 0,
} as unknown as TokenState;

const payload = {
values: {
'collection/mode': [{ name: 'color.primary', value: '#ff0000', type: 'color' }],
},
themes: [
{
id: 'light',
name: 'Light',
selectedTokenSets: {
'collection/mode': TokenSetStatus.ENABLED,
},
},
],
activeTheme: {},
};

const result = setTokenData(state, payload);

// Should enable the first theme with INTERNAL_THEMES_NO_GROUP
expect(result.activeTheme).toEqual({
[INTERNAL_THEMES_NO_GROUP]: 'light',
});

// Should update usedTokenSet based on the first theme
expect(result.usedTokenSet).toEqual({
'collection/mode': TokenSetStatus.ENABLED,
});
});

it('should not change active theme when one is already set', () => {
const state = {
tokens: {},
themes: [],
activeTheme: { Theme: 'dark' },
usedTokenSet: {},
activeTokenSet: 'global',
lastSyncedState: '[]',
tokensSize: 0,
themesSize: 0,
} as unknown as TokenState;

const payload = {
values: {
'collection/mode': [{ name: 'color.primary', value: '#ff0000', type: 'color' }],
},
themes: [
{
id: 'light',
name: 'Light',
group: 'Theme',
selectedTokenSets: {
'collection/mode': TokenSetStatus.ENABLED,
},
},
{
id: 'dark',
name: 'Dark',
group: 'Theme',
selectedTokenSets: {
'collection/mode': TokenSetStatus.ENABLED,
},
},
],
activeTheme: { Theme: 'dark' },
};

const result = setTokenData(state, payload);

// Should keep the existing active theme
expect(result.activeTheme).toEqual({
Theme: 'dark',
});
});

it('should not auto-enable when no themes are available', () => {
const state = {
tokens: {},
themes: [],
activeTheme: {},
usedTokenSet: {},
activeTokenSet: 'global',
lastSyncedState: '[]',
tokensSize: 0,
themesSize: 0,
} as unknown as TokenState;

const payload = {
values: {
global: [{ name: 'color.primary', value: '#ff0000', type: 'color' }],
},
themes: [],
activeTheme: {},
};

const result = setTokenData(state, payload);

// Should remain empty
expect(result.activeTheme).toEqual({});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import removeIdPropertyFromTokens from '@/utils/removeIdPropertyFromTokens';
import { TokenFormat } from '@/plugin/TokenFormatStoreClass';
import { TokenStore } from '@/types/tokens';
import { checkStorageSize } from '@/utils/checkStorageSize';
import { INTERNAL_THEMES_NO_GROUP } from '@/constants/InternalTokenGroup';

export function setTokenData(state: TokenState, payload: SetTokenDataPayload): TokenState {
if (payload.values.length === 0) {
Expand All @@ -26,13 +27,38 @@ export function setTokenData(state: TokenState, payload: SetTokenDataPayload): T
const usedTokenSets = Object.fromEntries(
allAvailableTokenSets.map((tokenSet) => [tokenSet, payload.usedTokenSet?.[tokenSet] ?? TokenSetStatus.DISABLED]),
);
const newActiveTheme = payload.activeTheme;
let newActiveTheme = payload.activeTheme;
Object.entries(newActiveTheme ?? {}).forEach(([group, activeTheme]) => {
if (!payload.themes?.find((t) => t.id === activeTheme) && newActiveTheme) {
delete newActiveTheme[group];
}
});

// Auto-enable the first theme if no active theme is set and themes are available
const hasActiveTheme = newActiveTheme && Object.keys(newActiveTheme).length > 0;
const hasThemes = payload.themes && payload.themes.length > 0;
let finalUsedTokenSets = Array.isArray(payload.values) ? { global: TokenSetStatus.ENABLED } : usedTokenSets;

if (!hasActiveTheme && hasThemes) {
const firstTheme = payload.themes![0];
const groupKey = firstTheme.group || INTERNAL_THEMES_NO_GROUP;
newActiveTheme = { [groupKey]: firstTheme.id };

// Update usedTokenSet based on the first theme's selectedTokenSets
const selectedTokenSets: Record<string, TokenSetStatus> = {};
Object.entries(firstTheme.selectedTokenSets).forEach(([tokenSet, status]) => {
if (status !== TokenSetStatus.DISABLED) {
selectedTokenSets[tokenSet] = status;
}
});

finalUsedTokenSets = Object.fromEntries(
allAvailableTokenSets.map((tokenSet) => (
[tokenSet, selectedTokenSets?.[tokenSet] ?? TokenSetStatus.DISABLED]
)),
);
}

const tokenValues = Array.isArray(payload.values) ? payload.values : removeIdPropertyFromTokens(payload.values);

// When the remote data has changed, we will update the last synced state
Expand All @@ -58,7 +84,7 @@ export function setTokenData(state: TokenState, payload: SetTokenDataPayload): T
: {
activeTokenSet: Array.isArray(payload.values) ? 'global' : Object.keys(payload.values)[0],
}),
usedTokenSet: Array.isArray(payload.values) ? { global: TokenSetStatus.ENABLED } : usedTokenSets,
usedTokenSet: finalUsedTokenSets,
tokensSize,
themesSize,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import { CreateSingleTokenData, EditSingleTokenData } from '../useManageTokens';
import { singleTokensToRawTokenSet } from '@/utils/convert';
import { checkStorageSize } from '@/utils/checkStorageSize';
import { compareLastSyncedState } from '@/utils/compareLastSyncedState';
import { INTERNAL_THEMES_NO_GROUP } from '@/constants/InternalTokenGroup';

export interface TokenState {
tokens: Record<string, AnyTokenList>;
Expand Down Expand Up @@ -704,8 +705,37 @@ export const tokenState = createModel<RootModel>()({
}
});

// Auto-enable the first theme if no active theme is set and themes are available
let newActiveTheme = state.activeTheme;
let newUsedTokenSet = state.usedTokenSet;
const hasActiveTheme = Object.keys(state.activeTheme).length > 0;
const allThemes = [...state.themes, ...newThemes];

if (!hasActiveTheme && allThemes.length > 0) {
// Get the first theme
const firstTheme = allThemes[0];
const groupKey = firstTheme.group || INTERNAL_THEMES_NO_GROUP;
newActiveTheme = { [groupKey]: firstTheme.id };

// Update usedTokenSet based on the first theme's selectedTokenSets
const selectedTokenSets: Record<string, TokenSetStatus> = {};
Object.entries(firstTheme.selectedTokenSets).forEach(([tokenSet, status]) => {
if (status !== TokenSetStatus.DISABLED) {
selectedTokenSets[tokenSet] = status;
}
});

newUsedTokenSet = Object.fromEntries(
Object.keys(state.tokens).map((tokenSet) => (
[tokenSet, selectedTokenSets?.[tokenSet] ?? TokenSetStatus.DISABLED]
)),
);
}

return {
...state,
activeTheme: newActiveTheme,
usedTokenSet: newUsedTokenSet,
importedThemes: {
newThemes,
updatedThemes,
Expand Down