diff --git a/package-lock.json b/package-lock.json index 6f0812682276..20a52859a016 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@gravity-ui/date-components": "^3.4.2", "@gravity-ui/date-utils": "^2.6.0", "@gravity-ui/graph": "^1.9.1", - "@gravity-ui/icons": "^2.21.0", + "@gravity-ui/icons": "^2.22.0", "@gravity-ui/illustrations": "^2.1.1", "@gravity-ui/markdown-editor": "^15.1.0", "@gravity-ui/navigation": "^6.3.1", @@ -3738,9 +3738,9 @@ "integrity": "sha512-bVt/UGjL96Naw0h03QW+3z2tf6t4zvLh1j+eFzpdLlAIMeNu0dRjxn4X6EGtwhwKrfMyIfrlOqmLSroXtCNQAw==" }, "node_modules/@gravity-ui/icons": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@gravity-ui/icons/-/icons-2.21.0.tgz", - "integrity": "sha512-KWghsvXDq+5AapT3bh2a/p1Iv923PDFln/U51fqaK9AMOftiOlkWfJHSJEFSxpgvbCJ/MOHjCSbhCT7Jx9mJAg==", + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/@gravity-ui/icons/-/icons-2.22.0.tgz", + "integrity": "sha512-v304kVTXbtsq8b3Uq3ZnX9R3LnRdECToeLspDgCs92C9fcID4C7dJqS3PywHKEaM4PFEZO8LRp/6JQbIm9UjHA==", "license": "MIT", "dependencies": { "tslib": "^2.8.1" diff --git a/package.json b/package.json index 250b6f738a5b..4acc4f3924f3 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@gravity-ui/date-components": "^3.4.2", "@gravity-ui/date-utils": "^2.6.0", "@gravity-ui/graph": "^1.9.1", - "@gravity-ui/icons": "^2.21.0", + "@gravity-ui/icons": "^2.22.0", "@gravity-ui/illustrations": "^2.1.1", "@gravity-ui/markdown-editor": "^15.1.0", "@gravity-ui/navigation": "^6.3.1", diff --git a/public/locales/de/icons.json b/public/locales/de/icons.json index 006d84ff9e6c..376c5183186a 100644 --- a/public/locales/de/icons.json +++ b/public/locales/de/icons.json @@ -15,5 +15,37 @@ "usage_reactComponent": "React-Komponente", "usage_svg": "SVG", "imageSearch_tooltip": "Nach Bild suchen (oder aus der Zwischenablage einfügen)", - "imageSearch_drop": "Bild hierher ziehen, um ähnliche Icons zu suchen" -} \ No newline at end of file + "imageSearch_drop": "Bild hierher ziehen, um ähnliche Icons zu suchen", + "category": "Kategorie", + "allIcons": "Alle Icons", + "categoriesLabel": "Icon-Kategorien", + "categories": { + "arrows": "Pfeile", + "layout": "Layout", + "text": "Text", + "status": "Status", + "files": "Dateien", + "shapes": "Formen", + "development": "Entwicklung", + "editor": "Editor", + "people": "Personen", + "media": "Medien", + "brands": "Marken", + "charts": "Diagramme", + "commerce": "Handel", + "communication": "Kommunikation", + "games": "Spiele", + "devices": "Geräte", + "maps": "Karten", + "tools": "Werkzeuge", + "math": "Mathematik", + "security": "Sicherheit", + "cloud": "Cloud", + "finance": "Finanzen", + "health": "Gesundheit", + "transport": "Transport", + "weather": "Wetter", + "time": "Zeit", + "food": "Essen" + } +} diff --git a/public/locales/en/icons.json b/public/locales/en/icons.json index 8c5f9144d5f7..91e58da4f2a2 100644 --- a/public/locales/en/icons.json +++ b/public/locales/en/icons.json @@ -15,5 +15,37 @@ "usage_reactComponent": "React component", "usage_svg": "SVG", "imageSearch_tooltip": "Search by image (or paste from clipboard)", - "imageSearch_drop": "Drop image to search for similar icons" -} \ No newline at end of file + "imageSearch_drop": "Drop image to search for similar icons", + "category": "Category", + "allIcons": "All icons", + "categoriesLabel": "Icon categories", + "categories": { + "arrows": "Arrows", + "layout": "Layout", + "text": "Text", + "status": "Status", + "files": "Files", + "shapes": "Shapes", + "development": "Development", + "editor": "Editor", + "people": "People", + "media": "Media", + "brands": "Brands", + "charts": "Charts", + "commerce": "Commerce", + "communication": "Communication", + "games": "Games", + "devices": "Devices", + "maps": "Maps", + "tools": "Tools", + "math": "Math", + "security": "Security", + "cloud": "Cloud", + "finance": "Finance", + "health": "Health", + "transport": "Transport", + "weather": "Weather", + "time": "Time", + "food": "Food" + } +} diff --git a/public/locales/es/icons.json b/public/locales/es/icons.json index 0ab037cb357f..4fd9c6082889 100644 --- a/public/locales/es/icons.json +++ b/public/locales/es/icons.json @@ -15,5 +15,37 @@ "usage_reactComponent": "Componente React", "usage_svg": "SVG", "imageSearch_tooltip": "Buscar por imagen (o pegar desde el portapapeles)", - "imageSearch_drop": "Arrastra una imagen para buscar iconos similares" -} \ No newline at end of file + "imageSearch_drop": "Arrastra una imagen para buscar iconos similares", + "category": "Categoría", + "allIcons": "Todos los iconos", + "categoriesLabel": "Categorías de iconos", + "categories": { + "arrows": "Flechas", + "layout": "Diseño", + "text": "Texto", + "status": "Estado", + "files": "Archivos", + "shapes": "Formas", + "development": "Desarrollo", + "editor": "Editor", + "people": "Personas", + "media": "Multimedia", + "brands": "Marcas", + "charts": "Gráficos", + "commerce": "Comercio", + "communication": "Comunicación", + "games": "Juegos", + "devices": "Dispositivos", + "maps": "Mapas", + "tools": "Herramientas", + "math": "Matemáticas", + "security": "Seguridad", + "cloud": "Nube", + "finance": "Finanzas", + "health": "Salud", + "transport": "Transporte", + "weather": "Clima", + "time": "Tiempo", + "food": "Comida" + } +} diff --git a/public/locales/fr/icons.json b/public/locales/fr/icons.json index f66cdc4be5e4..cb0c4fd01748 100644 --- a/public/locales/fr/icons.json +++ b/public/locales/fr/icons.json @@ -15,5 +15,37 @@ "usage_reactComponent": "Composant React", "usage_svg": "SVG", "imageSearch_tooltip": "Rechercher par image (ou coller depuis le presse-papiers)", - "imageSearch_drop": "Déposer une image pour rechercher des icônes similaires" -} \ No newline at end of file + "imageSearch_drop": "Déposer une image pour rechercher des icônes similaires", + "category": "Catégorie", + "allIcons": "Toutes les icônes", + "categoriesLabel": "Catégories d’icônes", + "categories": { + "arrows": "Flèches", + "layout": "Mise en page", + "text": "Texte", + "status": "Statut", + "files": "Fichiers", + "shapes": "Formes", + "development": "Développement", + "editor": "Éditeur", + "people": "Personnes", + "media": "Médias", + "brands": "Marques", + "charts": "Graphiques", + "commerce": "Commerce", + "communication": "Communication", + "games": "Jeux", + "devices": "Appareils", + "maps": "Cartes", + "tools": "Outils", + "math": "Mathématiques", + "security": "Sécurité", + "cloud": "Cloud", + "finance": "Finance", + "health": "Santé", + "transport": "Transport", + "weather": "Météo", + "time": "Temps", + "food": "Alimentation" + } +} diff --git a/public/locales/ja/icons.json b/public/locales/ja/icons.json index 4d8548148906..7edd5693a897 100644 --- a/public/locales/ja/icons.json +++ b/public/locales/ja/icons.json @@ -15,5 +15,37 @@ "usage_reactComponent": "Reactコンポーネント", "usage_svg": "SVG", "imageSearch_tooltip": "画像で検索(またはクリップボードから貼り付け)", - "imageSearch_drop": "画像をドロップして類似アイコンを検索" -} \ No newline at end of file + "imageSearch_drop": "画像をドロップして類似アイコンを検索", + "category": "カテゴリー", + "allIcons": "すべてのアイコン", + "categoriesLabel": "アイコンのカテゴリー", + "categories": { + "arrows": "矢印", + "layout": "レイアウト", + "text": "テキスト", + "status": "ステータス", + "files": "ファイル", + "shapes": "図形", + "development": "開発", + "editor": "エディター", + "people": "人物", + "media": "メディア", + "brands": "ブランド", + "charts": "チャート", + "commerce": "コマース", + "communication": "コミュニケーション", + "games": "ゲーム", + "devices": "デバイス", + "maps": "地図", + "tools": "ツール", + "math": "数学", + "security": "セキュリティ", + "cloud": "クラウド", + "finance": "金融", + "health": "健康", + "transport": "交通", + "weather": "天気", + "time": "時間", + "food": "食べ物" + } +} diff --git a/public/locales/ko/icons.json b/public/locales/ko/icons.json index 97348a1b7d22..b38fc1fc0a0b 100644 --- a/public/locales/ko/icons.json +++ b/public/locales/ko/icons.json @@ -15,5 +15,37 @@ "usage_reactComponent": "React 컴포넌트", "usage_svg": "SVG", "imageSearch_tooltip": "이미지로 검색 (또는 클립보드에서 붙여넣기)", - "imageSearch_drop": "이미지를 드롭하여 유사한 아이콘 검색" -} \ No newline at end of file + "imageSearch_drop": "이미지를 드롭하여 유사한 아이콘 검색", + "category": "카테고리", + "allIcons": "모든 아이콘", + "categoriesLabel": "아이콘 카테고리", + "categories": { + "arrows": "화살표", + "layout": "레이아웃", + "text": "텍스트", + "status": "상태", + "files": "파일", + "shapes": "도형", + "development": "개발", + "editor": "편집기", + "people": "사람", + "media": "미디어", + "brands": "브랜드", + "charts": "차트", + "commerce": "상거래", + "communication": "커뮤니케이션", + "games": "게임", + "devices": "기기", + "maps": "지도", + "tools": "도구", + "math": "수학", + "security": "보안", + "cloud": "클라우드", + "finance": "금융", + "health": "건강", + "transport": "교통", + "weather": "날씨", + "time": "시간", + "food": "음식" + } +} diff --git a/public/locales/pt/icons.json b/public/locales/pt/icons.json index 459cebdbc39f..22d92468a390 100644 --- a/public/locales/pt/icons.json +++ b/public/locales/pt/icons.json @@ -15,5 +15,37 @@ "usage_reactComponent": "Componente React", "usage_svg": "SVG", "imageSearch_tooltip": "Pesquisar por imagem (ou colar da área de transferência)", - "imageSearch_drop": "Solte a imagem para pesquisar ícones semelhantes" -} \ No newline at end of file + "imageSearch_drop": "Solte a imagem para pesquisar ícones semelhantes", + "category": "Categoria", + "allIcons": "Todos os ícones", + "categoriesLabel": "Categorias de ícones", + "categories": { + "arrows": "Setas", + "layout": "Layout", + "text": "Texto", + "status": "Status", + "files": "Arquivos", + "shapes": "Formas", + "development": "Desenvolvimento", + "editor": "Editor", + "people": "Pessoas", + "media": "Mídia", + "brands": "Marcas", + "charts": "Gráficos", + "commerce": "Comércio", + "communication": "Comunicação", + "games": "Jogos", + "devices": "Dispositivos", + "maps": "Mapas", + "tools": "Ferramentas", + "math": "Matemática", + "security": "Segurança", + "cloud": "Nuvem", + "finance": "Finanças", + "health": "Saúde", + "transport": "Transporte", + "weather": "Clima", + "time": "Tempo", + "food": "Comida" + } +} diff --git a/public/locales/ru/icons.json b/public/locales/ru/icons.json index 321233726d38..3e55dac671cd 100644 --- a/public/locales/ru/icons.json +++ b/public/locales/ru/icons.json @@ -15,5 +15,37 @@ "usage_reactComponent": "React-компонент", "usage_svg": "SVG", "imageSearch_tooltip": "Поиск по изображению (или вставьте из буфера обмена)", - "imageSearch_drop": "Перетащите изображение для поиска похожих иконок" -} \ No newline at end of file + "imageSearch_drop": "Перетащите изображение для поиска похожих иконок", + "category": "Категория", + "allIcons": "Все иконки", + "categoriesLabel": "Категории иконок", + "categories": { + "arrows": "Стрелки", + "layout": "Макет", + "text": "Текст", + "status": "Статус", + "files": "Файлы", + "shapes": "Фигуры", + "development": "Разработка", + "editor": "Редактор", + "people": "Люди", + "media": "Медиа", + "brands": "Бренды", + "charts": "Диаграммы", + "commerce": "Коммерция", + "communication": "Связь", + "games": "Игры", + "devices": "Устройства", + "maps": "Карты", + "tools": "Инструменты", + "math": "Математика", + "security": "Безопасность", + "cloud": "Облако", + "finance": "Финансы", + "health": "Здоровье", + "transport": "Транспорт", + "weather": "Погода", + "time": "Время", + "food": "Еда" + } +} diff --git a/public/locales/zh/icons.json b/public/locales/zh/icons.json index 6b521da94a0a..6ac47d5cbd91 100644 --- a/public/locales/zh/icons.json +++ b/public/locales/zh/icons.json @@ -15,5 +15,37 @@ "usage_reactComponent": "React 组件", "usage_svg": "SVG", "imageSearch_tooltip": "按图片搜索(或粘贴剪贴板内容)", - "imageSearch_drop": "拖放图片以搜索相似图标" -} \ No newline at end of file + "imageSearch_drop": "拖放图片以搜索相似图标", + "category": "类别", + "allIcons": "所有图标", + "categoriesLabel": "图标类别", + "categories": { + "arrows": "箭头", + "layout": "布局", + "text": "文本", + "status": "状态", + "files": "文件", + "shapes": "形状", + "development": "开发", + "editor": "编辑器", + "people": "人物", + "media": "媒体", + "brands": "品牌", + "charts": "图表", + "commerce": "商业", + "communication": "通信", + "games": "游戏", + "devices": "设备", + "maps": "地图", + "tools": "工具", + "math": "数学", + "security": "安全", + "cloud": "云", + "finance": "金融", + "health": "健康", + "transport": "交通", + "weather": "天气", + "time": "时间", + "food": "食物" + } +} diff --git a/src/components/Icons/IconCollection/IconButton/IconButton.scss b/src/components/Icons/IconCollection/IconButton/IconButton.scss index 26b04b7714c7..a99e002ad546 100644 --- a/src/components/Icons/IconCollection/IconButton/IconButton.scss +++ b/src/components/Icons/IconCollection/IconButton/IconButton.scss @@ -7,14 +7,12 @@ $block: '.#{variables.$ns}icon-button'; &.g-button, &.g-button:has(.g-button__icon:only-child) { --g-button-border-radius: 16px; + --g-button-background-color: #281a26; + --g-button-background-color-hover: #3c2e3a; display: flex; justify-content: center; align-items: center; - width: 62px; - height: 62px; - } - - &:hover { - --g-button-background-color: rgba(255, 255, 255, 0.16); + width: 100%; + height: 150px; } } diff --git a/src/components/Icons/IconCollection/IconButton/IconButton.tsx b/src/components/Icons/IconCollection/IconButton/IconButton.tsx index 7b3e88cd8638..7e67fe3c89e1 100644 --- a/src/components/Icons/IconCollection/IconButton/IconButton.tsx +++ b/src/components/Icons/IconCollection/IconButton/IconButton.tsx @@ -8,7 +8,7 @@ import './IconButton.scss'; const b = block('icon-button'); -const ICON_SIZE = 20; +const ICON_SIZE = 24; interface IconsProps { icon: IconItem; diff --git a/src/components/Icons/IconCollection/IconCollection.scss b/src/components/Icons/IconCollection/IconCollection.scss index 8d6116e96c5f..699405305505 100644 --- a/src/components/Icons/IconCollection/IconCollection.scss +++ b/src/components/Icons/IconCollection/IconCollection.scss @@ -1,11 +1,10 @@ -@use '~@gravity-ui/page-constructor/styles/variables.scss' as pcVariables; @use '../../../variables.scss'; $block: '.#{variables.$ns}icon-collection'; #{$block} { - display: flex; - flex-wrap: wrap; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); justify-content: center; - gap: pcVariables.$indentXS; + gap: 20px; } diff --git a/src/components/Icons/IconCollection/IconCollection.tsx b/src/components/Icons/IconCollection/IconCollection.tsx index b5c29ff07277..08fa152450e5 100644 --- a/src/components/Icons/IconCollection/IconCollection.tsx +++ b/src/components/Icons/IconCollection/IconCollection.tsx @@ -13,7 +13,7 @@ interface IconsProps { onSelectIcon?: (item: IconItem) => void; } -export const IconCollection: React.FC = ({icons, onSelectIcon}) => { +export const IconCollection = React.memo(({icons, onSelectIcon}) => { const handleClick = React.useCallback((item: IconItem) => onSelectIcon?.(item), [onSelectIcon]); return ( @@ -23,4 +23,4 @@ export const IconCollection: React.FC = ({icons, onSelectIcon}) => { ))} ); -}; +}); diff --git a/src/components/Icons/Icons.scss b/src/components/Icons/Icons.scss index b677af8df9fb..7e7acdb924bd 100644 --- a/src/components/Icons/Icons.scss +++ b/src/components/Icons/Icons.scss @@ -1,13 +1,18 @@ @use '~@gravity-ui/page-constructor/styles/styles.scss' as pcStyles; @use '~@gravity-ui/page-constructor/styles/variables.scss' as pcVariables; +@use '../../mixins.scss' as baseMixins; @use '../../variables.scss'; $block: '.#{variables.$ns}icons'; +$navigation-height: 81px; +$search-row-height: 60px; +$catalog-top-spacing: 16px; +$categories-sticky-offset: $search-row-height + $catalog-top-spacing; #{$block} { margin: 56px 0 0; - @media (max-width: map-get(pcVariables.$gridBreakpoints, 'md') - 1) { + @include baseMixins.window-breakpoint('md') { margin: 32px 0 0; } @@ -17,7 +22,7 @@ $block: '.#{variables.$ns}icons'; justify-content: space-between; margin-bottom: pcVariables.$indentXS; - @media (max-width: map-get(pcVariables.$gridBreakpoints, 'md') - 1) { + @include baseMixins.window-breakpoint('md') { margin-bottom: pcVariables.$indentXXXS; } } @@ -29,7 +34,7 @@ $block: '.#{variables.$ns}icons'; color: #fff; margin: 0; - @media (max-width: map-get(pcVariables.$gridBreakpoints, 'md') - 1) { + @include baseMixins.window-breakpoint('md') { font-size: 32px; line-height: 48px; margin-bottom: pcVariables.$indentXXXS; @@ -47,13 +52,13 @@ $block: '.#{variables.$ns}icons'; & &__search { padding: 8px 0; - margin-bottom: 24px; + margin-bottom: pcVariables.$indentSM; position: sticky; top: 0; background: var(--g-color-base-background); z-index: 1; - @media (max-width: map-get(pcVariables.$gridBreakpoints, 'md') - 1) { + @include baseMixins.window-breakpoint('md') { margin-bottom: pcVariables.$indentXXXS; } } @@ -61,7 +66,7 @@ $block: '.#{variables.$ns}icons'; &__search-row { display: flex; align-items: center; - gap: 8px; + gap: pcVariables.$indentXXXS; } &__search-input { @@ -71,7 +76,7 @@ $block: '.#{variables.$ns}icons'; &__search-row { display: flex; align-items: center; - gap: 8px; + gap: pcVariables.$indentXXXS; } &__search-input { @@ -79,12 +84,12 @@ $block: '.#{variables.$ns}icons'; } &__search-icon { - padding: 0 12px; + padding: 0 pcVariables.$indentXXS; color: rgba(255, 255, 255, 0.5); } &__photo-search-icon { - padding: 0 12px; + padding: 0 pcVariables.$indentXXS; color: rgba(255, 255, 255, 0.5); cursor: pointer; transition: color 0.1s ease; @@ -95,7 +100,7 @@ $block: '.#{variables.$ns}icons'; } &__clear-icon { - padding: 0 12px; + padding: 0 pcVariables.$indentXXS; color: rgba(255, 255, 255, 0.5); cursor: pointer; transition: color 0.1s ease; @@ -104,4 +109,146 @@ $block: '.#{variables.$ns}icons'; color: rgba(255, 255, 255, 0.8); } } + + &__catalog { + display: flex; + align-items: flex-start; + gap: pcVariables.$indentM; + } + + &__categories { + position: sticky; + top: $categories-sticky-offset; + flex: 0 0 200px; + max-height: calc(100dvh - $navigation-height - $categories-sticky-offset); + overflow-y: auto; + scrollbar-width: thin; + } + + &__section-title { + margin: 0; + color: var(--g-color-text-primary); + font-size: 20px; + line-height: 24px; + font-weight: 600; + } + + &__category-list { + display: flex; + flex-direction: column; + margin-top: pcVariables.$indentS; + } + + &__category { + --g-button-border-radius: 8px; + --g-button-padding: #{pcVariables.$indentXXS}; + + width: 100%; + height: 36px; + font-size: 13px; + line-height: 20px; + text-align: left; + + .g-button__text { + display: flex; + width: 100%; + align-items: center; + justify-content: space-between; + gap: pcVariables.$indentXS; + } + } + + &__category-count, + &__results-count { + color: var(--g-color-text-secondary); + font-size: 13px; + font-weight: 400; + } + + &__results { + min-width: 0; + flex: 1 1 auto; + } + + &__results-heading { + display: flex; + align-items: center; + gap: pcVariables.$indentXXS; + margin-bottom: pcVariables.$indentS; + } + + &__mobile-category-trigger { + display: none; + } + + &__category-sheet { + --g-sheet-background-color: var(--g-color-base-modal); + --g-sheet-content-padding: 0 max(var(--g-spacing-5), env(safe-area-inset-right, 0px)) + max(var(--g-spacing-4), env(safe-area-inset-bottom, 0px)) + max(var(--g-spacing-5), env(safe-area-inset-left, 0px)); + } + + &__category-sheet-list { + padding-top: pcVariables.$indentXXXS; + + #{$block}__category { + --g-button-border-radius: 6px; + --g-button-padding: #{pcVariables.$indentXS}; + + height: 44px; + font-size: 15px; + } + + #{$block}__category-count { + font-size: 11px; + } + } + + @include baseMixins.window-breakpoint('md') { + &__catalog { + display: block; + } + + &__categories { + display: none; + } + + &__mobile-category-trigger { + display: flex; + width: 100%; + height: 44px; + align-items: center; + justify-content: space-between; + margin-bottom: pcVariables.$indentSM; + padding: pcVariables.$indentXXS pcVariables.$indentXS; + border: 0; + border-radius: 12px; + color: var(--g-color-text-primary); + background: rgba(255, 255, 255, 0.08); + font: inherit; + text-align: left; + cursor: pointer; + + &:disabled { + color: var(--g-color-text-hint); + cursor: default; + } + } + + &__mobile-category-label { + font-size: 15px; + line-height: 20px; + font-weight: 500; + } + + &__mobile-category-count { + color: var(--g-color-text-secondary); + font-size: 13px; + line-height: 18px; + } + + &__results-heading { + display: none; + } + } } diff --git a/src/components/Icons/Icons.tsx b/src/components/Icons/Icons.tsx index dd4b76fb1461..cbf07bf6d4a5 100644 --- a/src/components/Icons/Icons.tsx +++ b/src/components/Icons/Icons.tsx @@ -1,22 +1,27 @@ -import {ArrowUpRightFromSquare, Magnifier, Xmark} from '@gravity-ui/icons'; +import {ArrowUpRightFromSquare, ChevronRight, Magnifier, Xmark} from '@gravity-ui/icons'; import {Col, Grid, Row} from '@gravity-ui/page-constructor'; -import {Button, Icon, TextInput} from '@gravity-ui/uikit'; +import {Button, Icon, Sheet, TextInput} from '@gravity-ui/uikit'; import {useTranslation} from 'next-i18next'; import React from 'react'; import photoSearchIcon from '../../assets/icons/photo-search.svg'; import {useIsMobile} from '../../hooks/useIsMobile'; -import {block} from '../../utils'; +import {block, sendAnalyticsEvent} from '../../utils'; import {IconCollection} from './IconCollection'; import {IconDialog} from './IconDialog/IconDialog'; import './Icons.scss'; import {IconsNotFound} from './IconsNotFound'; import {useImageSearch} from './ImageSearch'; +import {categoryCounts, iconCategories, isIconInCategory} from './categories'; import {allIcons} from './constants'; import type {IconItem} from './types'; const b = block('icons'); +const categoryOptions = [ + {id: 'all', count: allIcons.length}, + ...iconCategories.map(({id}) => ({id, count: categoryCounts[id]})), +]; interface IconsProps { currentIcon?: string; @@ -29,6 +34,8 @@ export const Icons: React.FC = ({currentIcon, onChangeCurrentIcon}) const isMobile = useIsMobile(); const [filterString, setFilterString] = React.useState(''); + const [categoryId, setCategoryId] = React.useState('all'); + const [isCategorySheetOpen, setIsCategorySheetOpen] = React.useState(false); const [imageSearchResults, setImageSearchResults] = React.useState(null); const [isOpenIconDialog, setIsOpenIconDialog] = React.useState(false); @@ -40,6 +47,7 @@ export const Icons: React.FC = ({currentIcon, onChangeCurrentIcon}) React.useEffect(() => { if (!isMobile) { searchInputRef.current?.focus(); + setIsCategorySheetOpen(false); } }, [isMobile]); @@ -76,6 +84,7 @@ export const Icons: React.FC = ({currentIcon, onChangeCurrentIcon}) }, [onChangeCurrentIcon]); const handleClickToKeyword = React.useCallback((keyword: string) => { + setCategoryId('all'); setFilterString(keyword); handleCloseDialog(); @@ -86,6 +95,7 @@ export const Icons: React.FC = ({currentIcon, onChangeCurrentIcon}) }, []); const handleImageSearchResults = React.useCallback((componentNames: string[]) => { + setCategoryId('all'); setImageSearchResults(componentNames); setFilterString(''); }, []); @@ -100,13 +110,15 @@ export const Icons: React.FC = ({currentIcon, onChangeCurrentIcon}) isActive: imageSearchResults !== null, }); - const icons = React.useMemo(() => { + const searchedIcons = React.useMemo(() => { if (imageSearchResults) { const resultSet = new Set(imageSearchResults); const matched = allIcons.filter(({name}) => resultSet.has(name)); // preserve the ranking order from the search results matched.sort( - (a, b) => imageSearchResults.indexOf(a.name) - imageSearchResults.indexOf(b.name), + (first, second) => + imageSearchResults.indexOf(first.name) - + imageSearchResults.indexOf(second.name), ); return matched; } @@ -127,6 +139,55 @@ export const Icons: React.FC = ({currentIcon, onChangeCurrentIcon}) ); }, [filterString, imageSearchResults]); + const isSearching = Boolean(filterString) || imageSearch.isActive; + + const icons = React.useMemo( + () => + isSearching || categoryId === 'all' + ? searchedIcons + : searchedIcons.filter((icon) => isIconInCategory(icon, categoryId)), + [categoryId, isSearching, searchedIcons], + ); + + const activeCategoryId = isSearching ? 'all' : categoryId; + const allIconsTitle = t('icons:allIcons'); + const resultsTitle = + activeCategoryId === 'all' + ? allIconsTitle + : t(`icons:categories.${activeCategoryId}`, {defaultValue: activeCategoryId}); + const resultsCount = icons.length; + + const handleSelectCategory = React.useCallback( + (nextCategoryId: string, source: 'sidebar' | 'sheet') => { + setCategoryId(nextCategoryId); + setIsCategorySheetOpen(false); + sendAnalyticsEvent('icon_category_select', `${source}:${nextCategoryId}`); + + requestAnimationFrame(() => { + pageTitleRef.current?.scrollIntoView({behavior: 'smooth', block: 'start'}); + }); + }, + [], + ); + + const renderCategoryButtons = (source: 'sidebar' | 'sheet') => + categoryOptions.map(({id, count}) => ( + + )); + const searchStartContent = imageSearch.isActive ? ( imageSearch.startContent ) : ( @@ -177,6 +238,7 @@ export const Icons: React.FC = ({currentIcon, onChangeCurrentIcon}) if (!imageSearch.isActive) { setFilterString(value); if (value) { + setCategoryId('all'); setImageSearchResults(null); } } @@ -185,7 +247,7 @@ export const Icons: React.FC = ({currentIcon, onChangeCurrentIcon}) placeholder={t('icons:filterPlaceholder')} startContent={searchStartContent} autoFocus={!isMobile} - hasClear={false} + hasClear={!imageSearch.isActive} endContent={searchEndContent} controlProps={{ readOnly: imageSearch.isActive, @@ -195,11 +257,39 @@ export const Icons: React.FC = ({currentIcon, onChangeCurrentIcon}) - {icons.length ? ( - - ) : ( - - )} +
+ + +
+
+

{resultsTitle}

+ {resultsCount} +
+ {icons.length ? ( + + ) : ( + + )} +
+
@@ -210,6 +300,17 @@ export const Icons: React.FC = ({currentIcon, onChangeCurrentIcon}) onClickToKeyword={handleClickToKeyword} /> + {isMobile && ( + setIsCategorySheetOpen(false)} + title={t('icons:category')} + > +
{renderCategoryButtons('sheet')}
+
+ )} + {imageSearch.dropOverlay} ); diff --git a/src/components/Icons/categories.ts b/src/components/Icons/categories.ts new file mode 100644 index 000000000000..a421f8fdc74c --- /dev/null +++ b/src/components/Icons/categories.ts @@ -0,0 +1,53 @@ +import {allIcons} from './constants'; +import type {IconItem} from './types'; + +const preferredCategoryOrder = [ + 'arrows', + 'layout', + 'text', + 'status', + 'files', + 'shapes', + 'development', + 'editor', + 'people', + 'media', + 'brands', + 'charts', + 'commerce', + 'communication', + 'games', + 'devices', + 'maps', + 'tools', + 'math', + 'security', + 'cloud', + 'finance', + 'health', + 'transport', + 'weather', + 'time', + 'food', +] as const; + +const categoryCountsMap = new Map(); + +allIcons.forEach((icon) => { + icon.meta.categories.forEach((categoryId) => { + categoryCountsMap.set(categoryId, (categoryCountsMap.get(categoryId) ?? 0) + 1); + }); +}); + +export const categoryCounts = Object.fromEntries(categoryCountsMap); + +const categoryIds = Object.keys(categoryCounts); +const preferredCategoryIds = new Set(preferredCategoryOrder); + +export const iconCategories = [ + ...preferredCategoryOrder.filter((categoryId) => categoryCounts[categoryId]), + ...categoryIds.filter((categoryId) => !preferredCategoryIds.has(categoryId)).sort(), +].map((id) => ({id})); + +export const isIconInCategory = ({meta}: IconItem, categoryId: string) => + meta.categories.includes(categoryId); diff --git a/src/components/Icons/types.ts b/src/components/Icons/types.ts index 4f3e4724eeb3..63692ed8c89e 100644 --- a/src/components/Icons/types.ts +++ b/src/components/Icons/types.ts @@ -6,6 +6,7 @@ export type IconMeta = { svgName: string; componentName: string; keywords: string[]; + categories: string[]; }; export type IconItem = { diff --git a/src/utils/analytics.ts b/src/utils/analytics.ts index 7e17c3d178af..017c217c89ab 100644 --- a/src/utils/analytics.ts +++ b/src/utils/analytics.ts @@ -21,6 +21,7 @@ export type AnalyticsAction = | 'theme_apply' | 'theme_preset_select' | 'theme_import' + | 'icon_category_select' | 'theme_start_scratch'; // Single analytics channel: a `cta` event goes to the dataLayer, where GTM