Skip to content

Commit a650982

Browse files
Copilotsix7
andauthored
Add search input to Export to Figma dialog for themes and sets (#3716)
* Initial plan * Add search functionality to Export to Figma dialog Co-authored-by: six7 <[email protected]> * Enhance ExportSetsTab and ExportThemesTab with conditional heading rendering and excluded themes count display during search. This improves user experience by showing relevant information based on search activity. * Add new language support for theme selection in manageStylesAndVariables.json files Added translations for the "other themes selected" message in English, Spanish, French, Hindi, Dutch, and Chinese. This enhances the user experience by providing clearer feedback on theme selection across multiple languages. --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: six7 <[email protected]> Co-authored-by: Jan Six <[email protected]>
1 parent 0ecc244 commit a650982

File tree

9 files changed

+199
-34
lines changed

9 files changed

+199
-34
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tokens-studio/figma-plugin": patch
3+
---
4+
5+
Add search functionality to the Export to Figma dialog for filtering themes and sets

packages/tokens-studio-for-figma/src/app/components/ManageStylesAndVariables/ExportSetsTab.tsx

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { FileDirectoryIcon } from '@primer/octicons-react';
22
import {
3-
Tabs, Stack, Heading, Button,
3+
Tabs, Stack, Heading, Button, Box,
44
} from '@tokens-studio/ui';
5-
import React, { useMemo } from 'react';
5+
import React, { useMemo, useState, useCallback } from 'react';
66
import { useDispatch, useSelector, useStore } from 'react-redux';
77
import { useForm, Controller } from 'react-hook-form';
88
import { useTranslation } from 'react-i18next';
@@ -23,6 +23,7 @@ import { TokenSetThemeItem } from '../ManageThemesModal/TokenSetThemeItem';
2323
import { FormValues } from '../ManageThemesModal/CreateOrEditThemeForm';
2424
import { TokenSetStatus } from '@/constants/TokenSetStatus';
2525
import { ExportTokenSet } from '@/types/ExportTokenSet';
26+
import { SearchInputWithToggle } from '../SearchInputWithToggle';
2627

2728
export default function ExportSetsTab({ selectedSets, setSelectedSets }: { selectedSets: ExportTokenSet[], setSelectedSets: (sets: ExportTokenSet[]) => void }) {
2829
const dispatch = useDispatch<Dispatch>();
@@ -49,20 +50,75 @@ export default function ExportSetsTab({ selectedSets, setSelectedSets }: { selec
4950

5051
const [showChangeSets, setShowChangeSets] = React.useState(false);
5152
const [previousSetSelection, setPreviousSetSelection] = React.useState({});
53+
const [isSearchActive, setIsSearchActive] = useState(false);
54+
const [searchTerm, setSearchTerm] = useState('');
55+
56+
const handleToggleSearch = useCallback(() => {
57+
setIsSearchActive(!isSearchActive);
58+
if (isSearchActive) {
59+
setSearchTerm('');
60+
}
61+
}, [isSearchActive]);
62+
63+
const handleSearchTermChange = useCallback((term: string) => {
64+
setSearchTerm(term);
65+
}, []);
5266

5367
const allSets = useSelector(allTokenSetsSelector);
5468

5569
const availableTokenSets = useSelector(allTokenSetsSelector);
5670

5771
const setsTree = React.useMemo(() => tokenSetListToTree(availableTokenSets), [availableTokenSets]);
5872

73+
const filteredSetsTree = useMemo(() => {
74+
if (!searchTerm || !isSearchActive) {
75+
return setsTree;
76+
}
77+
78+
const lowerSearchTerm = searchTerm.toLowerCase();
79+
const matchingItems = new Set<string>();
80+
81+
// First pass: find all items that match the search term
82+
setsTree.forEach((item) => {
83+
if (item.label.toLowerCase().includes(lowerSearchTerm)) {
84+
matchingItems.add(item.path);
85+
86+
// Add all parent paths
87+
let currentParentPath = item.parent;
88+
while (currentParentPath && currentParentPath !== '') {
89+
matchingItems.add(currentParentPath);
90+
const pathToFind = currentParentPath;
91+
const parentItem = setsTree.find((i) => i.path === pathToFind);
92+
currentParentPath = parentItem?.parent || null;
93+
}
94+
}
95+
});
96+
97+
// Second pass: include children of matching folders
98+
setsTree.forEach((item) => {
99+
if (!item.isLeaf && matchingItems.has(item.path)) {
100+
setsTree.forEach((child) => {
101+
if (child.path.startsWith(`${item.path}/`)) {
102+
matchingItems.add(child.path);
103+
}
104+
});
105+
}
106+
});
107+
108+
return setsTree.filter((item) => matchingItems.has(item.path));
109+
}, [setsTree, searchTerm, isSearchActive]);
110+
59111
const handleCancelChangeSets = React.useCallback(() => {
60112
reset(previousSetSelection);
61113
setShowChangeSets(false);
114+
setIsSearchActive(false);
115+
setSearchTerm('');
62116
}, [previousSetSelection, reset]);
63117

64118
const handleSaveChangeSets = React.useCallback(() => {
65119
setShowChangeSets(false);
120+
setIsSearchActive(false);
121+
setSearchTerm('');
66122
}, []);
67123

68124
const handleShowChangeSets = React.useCallback(() => {
@@ -161,9 +217,22 @@ export default function ExportSetsTab({ selectedSets, setSelectedSets }: { selec
161217
</Stack>
162218
)}
163219
>
164-
<Heading>
165-
{t('exportSetsTab.changeSetsHeading')}
166-
</Heading>
220+
<Stack direction="row" justify="between" align="center" css={{ width: '100%' }}>
221+
{!isSearchActive && (
222+
<Heading>
223+
{t('exportSetsTab.changeSetsHeading')}
224+
</Heading>
225+
)}
226+
<SearchInputWithToggle
227+
isSearchActive={isSearchActive}
228+
searchTerm={searchTerm}
229+
onToggleSearch={handleToggleSearch}
230+
onSearchTermChange={handleSearchTermChange}
231+
placeholder={t('searchSets')}
232+
tooltip={t('searchSets')}
233+
autofocus
234+
/>
235+
</Stack>
167236
{/* Commenting until we have docs <Link target="_blank" href={docsLinks.sets}>{`${t('generic.learnMore')} – ${t('docs.referenceOnlyMode')}`}</Link> */}
168237
<Stack
169238
direction="column"
@@ -172,7 +241,13 @@ export default function ExportSetsTab({ selectedSets, setSelectedSets }: { selec
172241
marginBlockStart: '$4',
173242
}}
174243
>
175-
<TokenSetTreeContent items={setsTree} renderItemContent={TokenSetThemeItemInput} keyPosition="end" />
244+
{filteredSetsTree.length === 0 && isSearchActive && searchTerm ? (
245+
<Box css={{ padding: '$4', textAlign: 'center', color: '$fgMuted' }}>
246+
{t('noSetsFound')}
247+
</Box>
248+
) : (
249+
<TokenSetTreeContent items={filteredSetsTree} renderItemContent={TokenSetThemeItemInput} keyPosition="end" />
250+
)}
176251
</Stack>
177252
</Modal>
178253
</Tabs.Content>

packages/tokens-studio-for-figma/src/app/components/ManageStylesAndVariables/ExportThemesTab.tsx

Lines changed: 91 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useState, useCallback, useMemo } from 'react';
22
import { useSelector } from 'react-redux';
33
import {
44
Button, Heading, Tabs, Box, Stack, Checkbox, Label,
@@ -11,23 +11,54 @@ import {
1111
import { useIsProUser } from '@/app/hooks/useIsProUser';
1212
import { ThemeObject } from '@/types';
1313
import { LabelledCheckbox } from './LabelledCheckbox';
14+
import { SearchInputWithToggle } from '../SearchInputWithToggle';
1415

1516
export default function ExportThemesTab({ selectedThemes, setSelectedThemes }: { selectedThemes: string[], setSelectedThemes: (themes: string[]) => void }) {
1617
const { t } = useTranslation(['manageStylesAndVariables']);
1718
const themes = useSelector(themesListSelector);
1819
const isProUser = useIsProUser();
20+
const [isSearchActive, setIsSearchActive] = useState(false);
21+
const [searchTerm, setSearchTerm] = useState('');
22+
23+
const handleToggleSearch = useCallback(() => {
24+
setIsSearchActive(!isSearchActive);
25+
if (isSearchActive) {
26+
setSearchTerm('');
27+
}
28+
}, [isSearchActive]);
29+
30+
const handleSearchTermChange = useCallback((term: string) => {
31+
setSearchTerm(term);
32+
}, []);
33+
34+
const filteredThemes = useMemo(() => {
35+
if (!searchTerm || !isSearchActive) {
36+
return themes;
37+
}
38+
const lowerSearchTerm = searchTerm.toLowerCase();
39+
return themes.filter((theme) => theme.name.toLowerCase().includes(lowerSearchTerm)
40+
|| (theme.group && theme.group.toLowerCase().includes(lowerSearchTerm)));
41+
}, [themes, searchTerm, isSearchActive]);
1942

2043
const ThemeGroups = React.useMemo(() => {
21-
const uniqueGroups: string[] = themes.reduce((unique: string[], theme) => {
44+
const uniqueGroups: string[] = filteredThemes.reduce((unique: string[], theme) => {
2245
if (theme.group && !unique.includes(theme.group)) {
2346
unique.push(theme.group);
2447
}
2548
return unique;
2649
}, []);
2750
return uniqueGroups;
28-
}, [themes]);
51+
}, [filteredThemes]);
2952

30-
const ungroupedThemes = React.useMemo(() => themes.filter((theme) => !theme.group), [themes]);
53+
const ungroupedThemes = React.useMemo(() => filteredThemes.filter((theme) => !theme.group), [filteredThemes]);
54+
55+
const excludedSelectedThemesCount = useMemo(() => {
56+
if (!isSearchActive || !searchTerm) {
57+
return 0;
58+
}
59+
const filteredThemeIds = new Set(filteredThemes.map((theme) => theme.id));
60+
return selectedThemes.filter((themeId) => !filteredThemeIds.has(themeId)).length;
61+
}, [selectedThemes, filteredThemes, isSearchActive, searchTerm]);
3162

3263
const handleSelectTheme = React.useCallback((themeId: string) => {
3364
if (selectedThemes.includes(themeId)) {
@@ -38,12 +69,24 @@ export default function ExportThemesTab({ selectedThemes, setSelectedThemes }: {
3869
}, [selectedThemes, setSelectedThemes]);
3970

4071
const handleSelectAllThemes = React.useCallback(() => {
41-
if (selectedThemes.length === themes.length) {
42-
setSelectedThemes([]);
72+
// When filtering, select/deselect all visible (filtered) themes
73+
const themesToToggle = filteredThemes;
74+
const allFilteredSelected = themesToToggle.every((theme) => selectedThemes.includes(theme.id));
75+
76+
if (allFilteredSelected) {
77+
// Deselect all filtered themes
78+
setSelectedThemes(selectedThemes.filter((id) => !themesToToggle.some((theme) => theme.id === id)));
4379
} else {
44-
setSelectedThemes(themes.map((theme) => theme.id));
80+
// Select all filtered themes (add to existing selection)
81+
const newSelection = [...selectedThemes];
82+
themesToToggle.forEach((theme) => {
83+
if (!newSelection.includes(theme.id)) {
84+
newSelection.push(theme.id);
85+
}
86+
});
87+
setSelectedThemes(newSelection);
4588
}
46-
}, [themes, selectedThemes, setSelectedThemes]);
89+
}, [filteredThemes, selectedThemes, setSelectedThemes]);
4790

4891
function createThemeRow(theme: ThemeObject) {
4992
return (
@@ -99,26 +142,52 @@ export default function ExportThemesTab({ selectedThemes, setSelectedThemes }: {
99142
) : (
100143
<StyledCard>
101144
<Stack direction="column" align="start" gap={4}>
102-
<Heading>{t('exportThemesTab.confirmThemes')}</Heading>
145+
<Stack direction="row" justify="between" align="center" css={{ width: '100%' }}>
146+
{!isSearchActive && (
147+
<Heading>{t('exportThemesTab.confirmThemes')}</Heading>
148+
)}
149+
<SearchInputWithToggle
150+
isSearchActive={isSearchActive}
151+
searchTerm={searchTerm}
152+
onToggleSearch={handleToggleSearch}
153+
onSearchTermChange={handleSearchTermChange}
154+
placeholder={t('searchThemes')}
155+
tooltip={t('searchThemes')}
156+
autofocus
157+
/>
158+
</Stack>
103159
<p>{t('exportThemesTab.combinationsOfSetsMakeThemes')}</p>
104160
<Stack direction="column" width="full" gap={4}>
105161
<Stack direction="row" gap={3} align="center">
106-
<Checkbox id="check-all-themes" checked={selectedThemes.length === themes.length} onCheckedChange={handleSelectAllThemes} />
162+
<Checkbox id="check-all-themes" checked={filteredThemes.length > 0 && filteredThemes.every((theme) => selectedThemes.includes(theme.id))} onCheckedChange={handleSelectAllThemes} />
107163
<Label htmlFor="check-all-themes">{t('generic.selectAll')}</Label>
108164
</Stack>
109-
{ThemeGroups.map((group) => (
110-
<Stack direction="column" gap={2}>
111-
<Heading size="small">{group}</Heading>
112-
{themes.filter((theme) => theme.group === group).map((theme) => createThemeRow(theme))}
113-
</Stack>
114-
))}
115-
{ungroupedThemes.length ? (
116-
<Stack direction="column" gap={2}>
117-
<Heading size="small">{t('generic.noGroup')}</Heading>
118-
{ungroupedThemes.map((theme) => createThemeRow(theme))}
119-
</Stack>
120-
) : null}
165+
{filteredThemes.length === 0 && isSearchActive && searchTerm ? (
166+
<Box css={{ padding: '$4', textAlign: 'center', color: '$fgMuted' }}>
167+
{t('noThemesFound')}
168+
</Box>
169+
) : (
170+
<>
171+
{ThemeGroups.map((group) => (
172+
<Stack direction="column" gap={2} key={group}>
173+
<Heading size="small">{group}</Heading>
174+
{filteredThemes.filter((theme) => theme.group === group).map((theme) => createThemeRow(theme))}
175+
</Stack>
176+
))}
177+
{ungroupedThemes.length ? (
178+
<Stack direction="column" gap={2}>
179+
<Heading size="small">{t('generic.noGroup')}</Heading>
180+
{ungroupedThemes.map((theme) => createThemeRow(theme))}
181+
</Stack>
182+
) : null}
183+
</>
184+
)}
121185
</Stack>
186+
{excludedSelectedThemesCount > 0 && isSearchActive && (
187+
<Box css={{ color: '$fgMuted' }}>
188+
{t('exportThemesTab.otherThemesSelected', { count: excludedSelectedThemesCount })}
189+
</Box>
190+
)}
122191
</Stack>
123192

124193
</StyledCard>

packages/tokens-studio-for-figma/src/i18n/lang/en/manageStylesAndVariables.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
"modalTitle": "Export to Figma",
33
"buttonLabel": "Styles and Variables",
44
"optionsModalTitle": "Export to Figma: Options",
5+
"searchThemes": "Search themes",
6+
"searchSets": "Search sets",
7+
"noThemesFound": "No themes found",
8+
"noSetsFound": "No sets found",
59
"actions": {
610
"export": "Export to Figma",
711
"import": "Import from Figma",
@@ -65,7 +69,9 @@
6569
"introPro": "Combinations of token sets create themes. Use themes to create multiple collections of variables or styles for easy switching between design concepts like light or dark color modes, multiple brands, products or platforms. Or switch to Sets to export without using Themes",
6670
"introBasic": "Combinations of token sets create themes. Use themes to create multiple collections of variables or styles for easy switching between design concepts like light or dark color modes, multiple brands, products or platforms.",
6771
"confirmThemes": "Create styles or variables based on themes",
68-
"combinationsOfSetsMakeThemes": "Choose which themes should be created or updated."
72+
"combinationsOfSetsMakeThemes": "Choose which themes should be created or updated.",
73+
"otherThemesSelected_one": "{{count}} other theme selected",
74+
"otherThemesSelected_other": "{{count}} other themes selected"
6975

7076
},
7177
"exportSetsTab": {

packages/tokens-studio-for-figma/src/i18n/lang/es/manageStylesAndVariables.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@
6868
"introPro": "Las combinaciones de conjuntos de tokens crean temas. Usa Temas para crear múltiples colecciones de variables o estilos para cambiar fácilmente entre conceptos de diseño como modos de color claro u oscuro, múltiples marcas, productos o plataformas. O cambia a Conjuntos para exportar sin usar Temas",
6969
"introBasic": "Las combinaciones de conjuntos de tokens crean temas. Usa Temas para crear múltiples colecciones de variables o estilos para cambiar fácilmente entre conceptos de diseño como modos de color claro u oscuro, múltiples marcas, productos o plataformas.",
7070
"confirmThemes": "Confirmar Temas",
71-
"combinationsOfSetsMakeThemes": "Las combinaciones de conjuntos de tokens crean temas"
71+
"combinationsOfSetsMakeThemes": "Las combinaciones de conjuntos de tokens crean temas",
72+
"otherThemesSelected_one": "{{count}} otro tema seleccionado",
73+
"otherThemesSelected_other": "{{count}} otros temas seleccionados"
7274
},
7375
"exportSetsTab": {
7476
"changeSetsHeading": "Los conjuntos habilitados se exportarán a Figma",

packages/tokens-studio-for-figma/src/i18n/lang/fr/manageStylesAndVariables.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@
6868
"introPro": "Les combinaisons d'ensembles de jetons créent des thèmes. Utilisez les thèmes pour créer plusieurs collections de variables ou de styles pour basculer facilement entre des concepts de design tels que les modes de couleur clair ou sombre, plusieurs marques, produits ou plates-formes. Ou passez aux ensembles pour exporter sans utiliser de thèmes.",
6969
"introBasic": "Les combinaisons d'ensembles de jetons créent des thèmes. Utilisez les thèmes pour créer plusieurs collections de variables ou de styles pour basculer facilement entre des concepts de design tels que les modes de couleur clair ou sombre, plusieurs marques, produits ou plates-formes.",
7070
"confirmThemes": "Confirmer les thèmes",
71-
"combinationsOfSetsMakeThemes": "Les combinaisons d'ensembles de jetons créent des thèmes"
71+
"combinationsOfSetsMakeThemes": "Les combinaisons d'ensembles de jetons créent des thèmes",
72+
"otherThemesSelected_one": "{{count}} autre thème sélectionné",
73+
"otherThemesSelected_other": "{{count}} autres thèmes sélectionnés"
7274
},
7375
"exportSetsTab": {
7476
"changeSetsHeading": "Les ensembles activés seront exportés vers Figma",

packages/tokens-studio-for-figma/src/i18n/lang/hi/manageStylesAndVariables.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@
6868
"introPro": "टोकन सेट के संयोजन से थीमें बनाई जाती हैं। थीमें का उपयोग डिजाइन कॉन्सेप्ट के बीच आसानी से स्विच करने के लिए किया जाता है, जैसे प्रकाश या अंधेरे रंग मोड, कई ब्रांड, उत्पाद या प्लेटफ़ॉर्म। या सेट का उपयोग किए बिना निर्यात करने के लिए सेट पर स्विच करें",
6969
"introBasic": "टोकन सेट के संयोजन से थीमें बनाई जाती हैं। थीमें का उपयोग डिजाइन कॉन्सेप्ट के बीच आसानी से स्विच करने के लिए किया जाता है, जैसे प्रकाश या अंधेरे रंग मोड, कई ब्रांड, उत्पाद या प्लेटफ़ॉर्म।",
7070
"confirmThemes": "थीमें पुष्टि करें",
71-
"combinationsOfSetsMakeThemes": "टोकन सेट के संयोजन से थीमें बनाई जाती हैं"
71+
"combinationsOfSetsMakeThemes": "टोकन सेट के संयोजन से थीमें बनाई जाती हैं",
72+
"otherThemesSelected_one": "{{count}} अन्य थीम चयनित",
73+
"otherThemesSelected_other": "{{count}} अन्य थीमें चयनित"
7274
},
7375
"exportSetsTab": {
7476
"changeSetsHeading": "सक्रिय सेट Figma में निर्यात करेंगे",

packages/tokens-studio-for-figma/src/i18n/lang/nl/manageStylesAndVariables.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@
6868
"introPro": "Combinaties van token sets creëren thema's. Gebruik Thema's om meerdere verzamelingen van variabelen of stijlen te maken voor eenvoudig schakelen tussen ontwerpconcepten zoals lichte of donkere kleurmodi, meerdere merken, producten of platforms. Of schakel over naar Sets om te exporteren zonder gebruik te maken van Thema's",
6969
"introBasic": "Combinaties van token sets creëren thema's. Gebruik Thema's om meerdere verzamelingen van variabelen of stijlen te maken voor eenvoudig schakelen tussen ontwerpconcepten zoals lichte of donkere kleurmodi, meerdere merken, producten of platforms.",
7070
"confirmThemes": "Thema's bevestigen",
71-
"combinationsOfSetsMakeThemes": "Combinaties van token sets creëren thema's"
71+
"combinationsOfSetsMakeThemes": "Combinaties van token sets creëren thema's",
72+
"otherThemesSelected_one": "{{count}} ander thema geselecteerd",
73+
"otherThemesSelected_other": "{{count}} andere thema's geselecteerd"
7274
},
7375
"exportSetsTab": {
7476
"changeSetsHeading": "Ingeschakelde sets worden geëxporteerd naar Figma",

packages/tokens-studio-for-figma/src/i18n/lang/zh/manageStylesAndVariables.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@
6868
"introPro": "令牌集合的组合创建主题。使用主题创建多个变量或样式的集合,以便在设计概念之间轻松切换,如浅色或深色模式、多个品牌、产品或平台。或者切换到集合以在不使用主题的情况下导出",
6969
"introBasic": "令牌集合的组合创建主题。使用主题创建多个变量或样式的集合,以便在设计概念之间轻松切换,如浅色或深色模式、多个品牌、产品或平台。",
7070
"confirmThemes": "确认主题",
71-
"combinationsOfSetsMakeThemes": "令牌集合的组合创建主题"
71+
"combinationsOfSetsMakeThemes": "令牌集合的组合创建主题",
72+
"otherThemesSelected_one": "已选择 {{count}} 个其他主题",
73+
"otherThemesSelected_other": "已选择 {{count}} 个其他主题"
7274
},
7375
"exportSetsTab": {
7476
"changeSetsHeading": "启用的集合将导出到 Figma",

0 commit comments

Comments
 (0)