From 550c848bbaa5bf71730e5046eeb17a4df9efa1d5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 11 Oct 2025 12:16:50 +0000 Subject: [PATCH 1/3] Initial plan From e3cbc6ee0965fa0555acc92bf662ab18fb0394f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 11 Oct 2025 12:35:44 +0000 Subject: [PATCH 2/3] Implement auto-enable first theme on startup and pull Co-authored-by: six7 <4548309+six7@users.noreply.github.com> --- .../reducers/tokenState/setTokenData.ts | 30 +++++++++++++++++-- .../src/app/store/models/tokenState.tsx | 30 +++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/packages/tokens-studio-for-figma/src/app/store/models/reducers/tokenState/setTokenData.ts b/packages/tokens-studio-for-figma/src/app/store/models/reducers/tokenState/setTokenData.ts index 13d4e6419c..2f0d98dcc4 100644 --- a/packages/tokens-studio-for-figma/src/app/store/models/reducers/tokenState/setTokenData.ts +++ b/packages/tokens-studio-for-figma/src/app/store/models/reducers/tokenState/setTokenData.ts @@ -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) { @@ -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 = {}; + 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 @@ -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, }; diff --git a/packages/tokens-studio-for-figma/src/app/store/models/tokenState.tsx b/packages/tokens-studio-for-figma/src/app/store/models/tokenState.tsx index 3295c37616..044c421b74 100644 --- a/packages/tokens-studio-for-figma/src/app/store/models/tokenState.tsx +++ b/packages/tokens-studio-for-figma/src/app/store/models/tokenState.tsx @@ -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; @@ -704,8 +705,37 @@ export const tokenState = createModel()({ } }); + // 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 = {}; + 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, From 8b98e1dc2ac86da9de49320e9b74b886b907a0ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 11 Oct 2025 12:38:47 +0000 Subject: [PATCH 3/3] Add tests and changeset for auto-enable first theme Co-authored-by: six7 <4548309+six7@users.noreply.github.com> --- .changeset/auto-enable-first-theme.md | 5 + .../__tests__/autoEnableFirstTheme.test.ts | 170 ++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 .changeset/auto-enable-first-theme.md create mode 100644 packages/tokens-studio-for-figma/src/app/store/models/reducers/__tests__/autoEnableFirstTheme.test.ts diff --git a/.changeset/auto-enable-first-theme.md b/.changeset/auto-enable-first-theme.md new file mode 100644 index 0000000000..981076de15 --- /dev/null +++ b/.changeset/auto-enable-first-theme.md @@ -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 diff --git a/packages/tokens-studio-for-figma/src/app/store/models/reducers/__tests__/autoEnableFirstTheme.test.ts b/packages/tokens-studio-for-figma/src/app/store/models/reducers/__tests__/autoEnableFirstTheme.test.ts new file mode 100644 index 0000000000..49c814269b --- /dev/null +++ b/packages/tokens-studio-for-figma/src/app/store/models/reducers/__tests__/autoEnableFirstTheme.test.ts @@ -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({}); + }); + }); +});