From 7412de96eac64553ce6b0b162b233d7d70f7c8f5 Mon Sep 17 00:00:00 2001 From: Hyko Date: Sun, 26 Apr 2026 09:31:10 -0400 Subject: [PATCH] feat(admin): add category filter and keyword search to icons devkit page - add category sidebar with icon counts derived from Icon.category metadata - extend search to match icon keywords via Icon.keywords array - add AddRemoveIcon to shared icons with category and keywords metadata - add icons README documenting category and keywords conventions --- src/features/admin/devkit/IconsPage.client.js | 140 +++++++--- src/shared/icons/README.md | 87 ++++++ src/shared/icons/add-remove.js | 254 ++++++++++++++++++ src/shared/icons/index.js | 3 +- 4 files changed, 443 insertions(+), 41 deletions(-) create mode 100644 src/shared/icons/README.md create mode 100644 src/shared/icons/add-remove.js diff --git a/src/features/admin/devkit/IconsPage.client.js b/src/features/admin/devkit/IconsPage.client.js index a79beaf..2d5da3f 100644 --- a/src/features/admin/devkit/IconsPage.client.js +++ b/src/features/admin/devkit/IconsPage.client.js @@ -9,13 +9,28 @@ const ALL_ICONS = Object.entries(Icons); export default function IconsPage() { const [query, setQuery] = useState(''); + const [selectedCategory, setSelectedCategory] = useState(null); const toast = useToast(); + const categories = useMemo(() => { + const map = new Map(); + for (const [, Icon] of ALL_ICONS) { + const cat = Icon.category; + if (cat) map.set(cat, (map.get(cat) ?? 0) + 1); + } + return Array.from(map.entries()).sort(([a], [b]) => a.localeCompare(b)); + }, []); + const filtered = useMemo(() => { - if (!query.trim()) return ALL_ICONS; + let list = ALL_ICONS; + if (selectedCategory) list = list.filter(([, Icon]) => Icon.category === selectedCategory); + if (!query.trim()) return list; const q = query.trim().toLowerCase(); - return ALL_ICONS.filter(([name]) => name.toLowerCase().includes(q)); - }, [query]); + return list.filter(([name, Icon]) => + name.toLowerCase().includes(q) || + Icon.keywords?.some(k => k.toLowerCase().includes(q)) + ); + }, [query, selectedCategory]); const handleCopy = (name) => { navigator.clipboard.writeText(`<${name} className="w-5 h-5" />`); @@ -29,45 +44,90 @@ export default function IconsPage() { description={`${ALL_ICONS.length} icônes disponibles`} /> -
- setQuery(e.target.value)} - placeholder="Rechercher une icône..." - className="w-full rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 px-4 py-2.5 text-sm text-neutral-900 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-blue-700/40" - /> - {query && ( - +
+
+
+ setQuery(e.target.value)} + placeholder="Rechercher une icône..." + className="w-full rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 px-4 py-2.5 text-sm text-neutral-900 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-blue-700/40" + /> + {query && ( + + )} +
+ + {filtered.length === 0 ? ( +

+ Aucune icône trouvée pour “{query}” +

+ ) : ( +
+ {filtered.map(([name, IconComponent]) => ( + + ))} +
+ )} +
+ + {categories.length > 0 && ( +
+
+ Catégories +
+
+ + {categories.map(([cat, count]) => ( + + ))} +
+
)}
- - {filtered.length === 0 ? ( -

- Aucune icône trouvée pour “{query}” -

- ) : ( -
- {filtered.map(([name, IconComponent]) => ( - - ))} -
- )}
); } diff --git a/src/shared/icons/README.md b/src/shared/icons/README.md new file mode 100644 index 0000000..35d7926 --- /dev/null +++ b/src/shared/icons/README.md @@ -0,0 +1,87 @@ +# Icons + +Les icônes sont des composants React SVG regroupés par catégorie. Tous les exports passent par `index.js`. + +## Structure + +``` +src/shared/icons/ + index.js → re-exporte tous les fichiers catégorie + X.js → fichier contenant les icones d'une catégorie + README.md +``` + +## Ajouter une icône + +1. Ajoute le composant dans le fichier catégorie approprié +2. Ajoute `.keywords` juste après la définition (voir convention ci-dessous) +3. Si aucune catégorie n'existe, crée un nouveau fichier et ajoute `export * from './nouveau-fichier.js'` dans `index.js` + +## Convention des mots-clés + +Chaque icône doit avoir une propriété `.keywords` attachée directement sur le composant : + +```js +export const Add01Icon = (props) => ( + ... +); +Add01Icon.keywords = ['add', 'plus', 'create', 'new', 'insert', 'ajouter', 'nouveau', 'créer', 'insérer']; +``` + +**Règles :** +- Toujours en **anglais et en français** +- Mots au sens large : inclure les synonymes et usages contextuels +- Pas de doublons inutiles entre variantes du même icône (ex. `Add01Icon` et `Add02Icon` partagent les mêmes keywords) + +## Commandes utiles + +### Lister toutes les icônes d'un fichier + +```bash +grep -n "^export const" src/shared/icons/add-remove.js +``` + +### Ajouter des mots-clés en masse + +Plutôt que d'éditer manuellement, un script Python insère les `.keywords` après chaque composant : + +```python +KEYWORDS = { + 'MyNewIcon': ['keyword1', 'keyword2', 'mot1', 'mot2'], + # ... +} + +path = 'src/shared/icons/mon-fichier.js' + +with open(path, 'r') as f: + content = f.read() + +for name, kw in KEYWORDS.items(): + if f'{name}.keywords' in content: + continue + kw_str = ', '.join(f"'{k}'" for k in kw) + keywords_line = f'{name}.keywords = [{kw_str}];' + start_idx = content.find(f'export const {name} = (props) => (') + close_idx = content.find('\n);\n', start_idx) + insert_pos = close_idx + len('\n);\n') + content = content[:insert_pos] + keywords_line + '\n' + content[insert_pos:] + +with open(path, 'w') as f: + f.write(content) +``` + +## Recherche par mots-clés + +```js +import * as Icons from '@/shared/icons' + +function searchIcons(query) { + const q = query.toLowerCase() + return Object.entries(Icons) + .filter(([name, Icon]) => + name.toLowerCase().includes(q) || + Icon.keywords?.some(k => k.includes(q)) + ) + .map(([name, Icon]) => ({ name, Icon })) +} +``` \ No newline at end of file diff --git a/src/shared/icons/add-remove.js b/src/shared/icons/add-remove.js new file mode 100644 index 0000000..46c264e --- /dev/null +++ b/src/shared/icons/add-remove.js @@ -0,0 +1,254 @@ +export const Add01Icon = (props) => ( + + + +); +Add01Icon.keywords = ['add', 'plus', 'create', 'new', 'insert', 'cross', 'ajouter', 'nouveau', 'créer', 'insérer']; +Add01Icon.category = 'Add + Remove'; + +export const Add02Icon = (props) => ( + + + +); +Add02Icon.keywords = ['add', 'plus', 'create', 'new', 'insert', 'cross', 'ajouter', 'nouveau', 'créer', 'insérer']; +Add02Icon.category = 'Add + Remove'; + +export const AddCircleIcon = (props) => ( + + + +); +AddCircleIcon.keywords = ['add', 'plus', 'create', 'new', 'insert', 'circle', 'round', 'ajouter', 'nouveau', 'créer', 'insérer', 'cercle']; +AddCircleIcon.category = 'Add + Remove'; + +export const AddCircleHalfDotIcon = (props) => ( + + + + + + +); +AddCircleHalfDotIcon.keywords = ['add', 'plus', 'create', 'new', 'insert', 'circle', 'half', 'dot', 'ajouter', 'nouveau', 'créer', 'insérer', 'cercle', 'moitié', 'point']; +AddCircleHalfDotIcon.category = 'Add + Remove'; + +export const AddSquareIcon = (props) => ( + + + +); +AddSquareIcon.keywords = ['add', 'plus', 'create', 'new', 'insert', 'square', 'ajouter', 'nouveau', 'créer', 'insérer', 'carré']; +AddSquareIcon.category = 'Add + Remove'; + +export const Cancel01Icon = (props) => ( + + + +); +Cancel01Icon.keywords = ['cancel', 'close', 'dismiss', 'remove', 'cross', 'x', 'annuler', 'fermer', 'supprimer', 'croix']; +Cancel01Icon.category = 'Add + Remove'; + +export const Cancel02Icon = (props) => ( + + + +); +Cancel02Icon.keywords = ['cancel', 'close', 'dismiss', 'remove', 'cross', 'x', 'annuler', 'fermer', 'supprimer', 'croix']; +Cancel02Icon.category = 'Add + Remove'; + +export const CancelCircleIcon = (props) => ( + + + +); +CancelCircleIcon.keywords = ['cancel', 'close', 'dismiss', 'remove', 'cross', 'x', 'circle', 'round', 'annuler', 'fermer', 'supprimer', 'croix', 'cercle']; +CancelCircleIcon.category = 'Add + Remove'; + +export const CancelCircleHalfDotIcon = (props) => ( + + + + + + +); +CancelCircleHalfDotIcon.keywords = ['cancel', 'close', 'dismiss', 'remove', 'cross', 'x', 'circle', 'half', 'dot', 'annuler', 'fermer', 'supprimer', 'croix', 'cercle', 'moitié', 'point']; +CancelCircleHalfDotIcon.category = 'Add + Remove'; + +export const CancelSquareIcon = (props) => ( + + + +); +CancelSquareIcon.keywords = ['cancel', 'close', 'dismiss', 'remove', 'cross', 'x', 'square', 'annuler', 'fermer', 'supprimer', 'croix', 'carré']; +CancelSquareIcon.category = 'Add + Remove'; + +export const Delete01Icon = (props) => ( + + + + +); +Delete01Icon.keywords = ['delete', 'trash', 'bin', 'remove', 'garbage', 'waste', 'supprimer', 'corbeille', 'poubelle', 'déchets']; +Delete01Icon.category = 'Add + Remove'; + +export const Delete02Icon = (props) => ( + + + + +); +Delete02Icon.keywords = ['delete', 'trash', 'bin', 'remove', 'garbage', 'waste', 'supprimer', 'corbeille', 'poubelle', 'déchets']; +Delete02Icon.category = 'Add + Remove'; + +export const Delete03Icon = (props) => ( + + + + +); +Delete03Icon.keywords = ['delete', 'trash', 'bin', 'remove', 'garbage', 'waste', 'supprimer', 'corbeille', 'poubelle', 'déchets']; +Delete03Icon.category = 'Add + Remove'; + +export const Delete04Icon = (props) => ( + + + + + +); +Delete04Icon.keywords = ['delete', 'trash', 'bin', 'remove', 'garbage', 'waste', 'supprimer', 'corbeille', 'poubelle', 'déchets']; +Delete04Icon.category = 'Add + Remove'; + +export const DeletePutBackIcon = (props) => ( + + + + +); +DeletePutBackIcon.keywords = ['delete', 'trash', 'bin', 'restore', 'put back', 'recover', 'undo', 'supprimer', 'corbeille', 'restaurer', 'récupérer', 'annuler']; +DeletePutBackIcon.category = 'Add + Remove'; + +export const DeleteThrowIcon = (props) => ( + + + + +); +DeleteThrowIcon.keywords = ['delete', 'trash', 'throw', 'bin', 'discard', 'waste', 'supprimer', 'jeter', 'corbeille', 'poubelle', 'éliminer']; +DeleteThrowIcon.category = 'Add + Remove'; + +export const DiamondMinusIcon = (props) => ( + + + +); +DiamondMinusIcon.keywords = ['diamond', 'minus', 'remove', 'subtract', 'shape', 'diamant', 'moins', 'soustraire', 'forme']; +DiamondMinusIcon.category = 'Add + Remove'; + +export const DiamondPlusIcon = (props) => ( + + + +); +DiamondPlusIcon.keywords = ['diamond', 'plus', 'add', 'insert', 'shape', 'diamant', 'ajouter', 'insérer', 'forme']; +DiamondPlusIcon.category = 'Add + Remove'; + +export const Eraser01Icon = (props) => ( + + + +); +Eraser01Icon.keywords = ['eraser', 'erase', 'clear', 'delete', 'undo', 'remove', 'gomme', 'effacer', 'vider', 'supprimer']; +Eraser01Icon.category = 'Add + Remove'; + +export const EraserAddIcon = (props) => ( + + + +); +EraserAddIcon.keywords = ['eraser', 'add', 'erase', 'clear', 'plus', 'create', 'gomme', 'ajouter', 'effacer', 'créer']; +EraserAddIcon.category = 'Add + Remove'; + +export const Remove01Icon = (props) => ( + + + +); +Remove01Icon.keywords = ['remove', 'minus', 'subtract', 'reduce', 'dash', 'retirer', 'soustraire', 'réduire', 'moins']; +Remove01Icon.category = 'Add + Remove'; + +export const Remove02Icon = (props) => ( + + + +); +Remove02Icon.keywords = ['remove', 'minus', 'subtract', 'reduce', 'dash', 'retirer', 'soustraire', 'réduire', 'moins']; +Remove02Icon.category = 'Add + Remove'; + +export const RemoveCircleIcon = (props) => ( + + + + +); +RemoveCircleIcon.keywords = ['remove', 'minus', 'subtract', 'circle', 'round', 'retirer', 'soustraire', 'cercle', 'moins']; +RemoveCircleIcon.category = 'Add + Remove'; + +export const RemoveCircleHalfDotIcon = (props) => ( + + + + + + +); +RemoveCircleHalfDotIcon.keywords = ['remove', 'minus', 'subtract', 'circle', 'half', 'dot', 'retirer', 'soustraire', 'cercle', 'moitié', 'point']; +RemoveCircleHalfDotIcon.category = 'Add + Remove'; + +export const RemoveSquareIcon = (props) => ( + + + +); +RemoveSquareIcon.keywords = ['remove', 'minus', 'subtract', 'square', 'retirer', 'soustraire', 'carré', 'moins']; +RemoveSquareIcon.category = 'Add + Remove'; + +export const RestoreBinIcon = (props) => ( + + + + + +); +RestoreBinIcon.keywords = ['restore', 'bin', 'recover', 'undo', 'trash', 'put back', 'restaurer', 'corbeille', 'récupérer', 'annuler', 'poubelle']; +RestoreBinIcon.category = 'Add + Remove'; + +export const UnavailableIcon = (props) => ( + + + +); +UnavailableIcon.keywords = ['unavailable', 'disabled', 'blocked', 'forbidden', 'not available', 'off', 'no', 'indisponible', 'désactivé', 'bloqué', 'interdit', 'non disponible']; +UnavailableIcon.category = 'Add + Remove'; + +export const WasteIcon = (props) => ( + + + + + +); +WasteIcon.keywords = ['waste', 'trash', 'bin', 'delete', 'garbage', 'remove', 'poubelle', 'corbeille', 'supprimer', 'déchets']; +WasteIcon.category = 'Add + Remove'; + +export const WasteRestoreIcon = (props) => ( + + + + + +); +WasteRestoreIcon.keywords = ['waste', 'trash', 'restore', 'bin', 'recover', 'undo', 'poubelle', 'restaurer', 'corbeille', 'récupérer', 'annuler']; +WasteRestoreIcon.category = 'Add + Remove'; diff --git a/src/shared/icons/index.js b/src/shared/icons/index.js index be391bc..d1db723 100644 --- a/src/shared/icons/index.js +++ b/src/shared/icons/index.js @@ -1,3 +1,5 @@ +export * from './add-remove.js' + export const ArrowDown01Icon = (props) => ( @@ -22,7 +24,6 @@ export const ArrowUp01Icon = (props) => ( ); - export const UserCircle02Icon = (props) => (