@skbkontur/icons
v1.14.0
Published
react-ui-icons
Downloads
4,780
Maintainers
Keywords
Readme
Пак современных иконок для использования в интерфейсах Контура.
Ссылка на пакет в nexus
Библиотека предоставляет два варианта иконок:
- Цельные иконки (например,
CheckIcon
): такие иконки содержат все доступные начертания иконок, но при этом занимают приблизительно в 10 раз больше места в бандле, чем гранулярные иконки - Гранулярные иконки (например,
CheckIcon16Light
): такие иконки содержат всего одно из 10-ти доступных начертаний, но при этом могут принимать все пропы, которые принимают цельные иконки и занимают значительно меньше места в бандле. Цельные иконки под капотом состоят из гранулярных иконок
Какие иконки использовать?
У цельных иконок есть одно преимущество перед гранулярными иконками: при изменении размера, цельные иконки будут изменять своё начертание, гранулярные же иконки в свою очередь будут оставаться в своём начертании, но будут растягиваться до заданного размера
Проще всего увидеть эту разницу на примере. В примере обе иконки представлены в двух размерах: 64 пикселя и 32 пикселя. В цельной иконке, благодаря системе умного размера, при уменьшении иконки до 32-ух пикселей, изменяется её начертание. В гранулярной иконке, остаётся изначальное (64-ех пиксельное) начертание, но сама иконка уменьшается до 32-ух пикселей:
import { DivideCircleIcon, DivideCircleIcon64Regular } from './icons/DivideCircleIcon';
<div style={{ display: 'flex' }}>
<div style={{ marginRight: '30px' }}>
<div>Цельная иконка</div>
<DivideCircleIcon size={64} />
<DivideCircleIcon size={32} />
</div>
<div>
<div>Гранулярная иконка</div>
<DivideCircleIcon64Regular />
<DivideCircleIcon64Regular size={32} />
</div>
</div>;
Итого, использование гранулярных vs цельных иконок можно свести к трём правилам:
- Если вам не нужна фишка цельных иконок с умным размером - используйте используйте гранулярные иконки
- Если вам нужно менять размер иконок (например, в зависимости от размера экрана), но при этом вы не хотите чтобы ваш бандл разрастался - используйте гранулярные иконки со своей логикой, которая будет подменять иконки
- Если вам нужно менять размер иконок и у вас нет возможности написать свою логику для определения размера иконки или если для вас не критичен размер бандла - используйте цельные иконки
Пропы иконок:
type IconProps = {
size?: 16 | 20 | 24 | 32 | 64 | number; // Иконка может иметь любой размер, но будет внешне меняться в зависимости от брейкпоинтов. Так, если задать иконке размер `35`, то иконка размера `32` будет растянута до размера `35`, если задать иконке размер `100`, то иконка размера `64` будет растянута до размера `100`. Иконки размером меньше `16` будут использовать иконку размера `16` как базовую иконку.
weight?: 'light' | 'regular' | 'solid'; // Стиль иконки в соответствии с дизайном.
color?: string; // Цвет иконки. По умолчанию наследуется цвет ближайшего родителя, у которого явно задан аттрибут `color`.
align?: 'center' | 'baseline' | 'none'; // Позволяет выровнять иконку относительно остального контента. При 'baseline' иконка будет выравниваться относительно базовой линии текста, при 'center' иконка будет выравниваться относительно центра текста или друго контента, при 'none' к иконке не будут применены дополнительные стили для выравнивания. Значение по умолчанию - 'center'.
} & React.SVGAttributes<SVGElement>; // Также иконка может принимать все атрибуты элемента `svg`.
Импорт иконок
Импортировать иконки рекомендуется по одной, напрямую из целевого файла
import { CheckAIcon } from '@skbkontur/icons/icons/CheckAIcon'; // ✅
import { MathDeltaIcon } from '@skbkontur/icons/icons/MathDeltaIcon'; // ✅
import { MathDeltaIcon20Light } from '@skbkontur/icons/icons/MathDeltaIcon/MathDeltaIcon20Light'; // ✅
import { ArchiveBoxIcon24Solid } from '@skbkontur/icons/icons/ArchiveBoxIcon/ArchiveBoxIcon24Solid'; // ✅
Можно использовать упрощенный формат импорта:
import { CheckAIcon } from '@skbkontur/icons/CheckAIcon'; // ✅
import { MathDeltaIcon } from '@skbkontur/icons/MathDeltaIcon'; // ✅
import { MathDeltaIcon20Light } from '@skbkontur/icons/MathDeltaIcon20Light'; // ✅
import { ArchiveBoxIcon24Solid } from '@skbkontur/icons/ArchiveBoxIcon24Solid'; // ✅
Импорт из корня может привести к нехватке памяти во время билдинга проекта, т.к. будут импортированны сразу все файлы
import { AnimalPawIcon, ArchiveBoxIcon24Solid } from '@skbkontur/icons'; // ⛔
Выстраивание иконок относительно текста
import React, { useState } from 'react';
import * as allIcons from '../icons/index';
import { IconProps, weights } from './internal/Icon';
import { completeIcons } from './__stories__/constant';
import { ColorPicker } from './__stories__/ColorPicker';
import { ControlsWrapper, ControlsWrapperProps } from './__stories__/ControlsWrapper';
import { TemplateProps } from './__stories__/ModernIcons.stories';
import { WeightRange } from './__stories__/WeightRange';
const textWeights = [100, 200, 300, 400, 500, 600, 700, 800, 900];
const [align, setAlign] = React.useState('center');
const [fontSize, setFontSize] = React.useState(32);
const [iconWeight, setIconWeight] = React.useState(1);
const [textWeight, setTextWeight] = React.useState(3);
const [color, setColor] = React.useState('#222');
const [currentIcon, setCurrentIcon] = React.useState(completeIcons[0]);
const Icon = allIcons[currentIcon];
<div style={{ display: 'flex' }}>
<div style={{ display: 'flex', flexDirection: 'column', width: '100vw', padding: '24px' }}>
<span style={{ fontSize: fontSize, color, fontWeight: textWeights[textWeight] }}>
<Icon size={fontSize} weight={weights[iconWeight]} align={align} />
Текст слева
<Icon size={fontSize} weight={weights[iconWeight]} align={align} />
Текст справа
<Icon size={fontSize} weight={weights[iconWeight]} align={align} />
</span>
</div>
<ControlsWrapper popupTopPos={'-430px'}>
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer', marginBottom: '15px' }}>
Выравнивание:{' '}
<select defaultValue={align} onChange={(e) => setAlign(e.target.value)} style={{ marginLeft: '10px' }}>
<option value="center">по центру</option>
<option value="baseline">по базовой линии текста</option>
<option value="none">без выравнивания</option>
</select>
</label>
<label>
<span>Иконка:</span>
<select
onChange={(e) => {
setCurrentIcon(e.target.value);
}}
style={{ marginLeft: '5px' }}
>
{completeIcons.map((icon) => {
return <option key={icon}>{icon}</option>;
})}
</select>
</label>
<label style={{ display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<p>Размер текста и иконки:</p>
<p>{fontSize}px</p>
</div>
<input
type="range"
style={{ width: '100%' }}
min={12}
max={60}
value={fontSize}
onChange={(e) => setFontSize(+e.target.value)}
/>
</label>
<WeightRange weight={iconWeight} setWeight={setIconWeight} label="Вес иконки:" />
<WeightRange weight={textWeight} setWeight={setTextWeight} label="Вес текста:" weightsArray={textWeights} />
<ColorPicker color={color} setColor={setColor} label="Цвет текста и иконки:" />
</ControlsWrapper>
</div>;
Шоу-кейс всех иконок
import React, { useState, useEffect } from 'react';
import * as allIcons from '../icons/index';
import { weights, breakpoints } from './internal/Icon';
import { completeIcons } from './__stories__/constant';
import { ColorPicker } from './__stories__/ColorPicker';
import { ControlsWrapper, ControlsWrapperProps } from './__stories__/ControlsWrapper';
import { TemplateProps } from './__stories__/ModernIcons.stories';
import { WeightRange } from './__stories__/WeightRange';
const CheckAIcon = allIcons['CheckAIcon'];
const CopyIcon = allIcons['CopyIcon'];
const DEFAULT_ICON_BREAKPOINT = 3;
const DEFAULT_ICON_SIZE = breakpoints[DEFAULT_ICON_BREAKPOINT];
const capitalize = (string) => {
return string[0].toUpperCase() + string.slice(1);
};
const generateAdditionalItems = (totalNumberOfItems, numberOfItemsInRow) => {
const difference = Math.abs((totalNumberOfItems % numberOfItemsInRow) - numberOfItemsInRow);
return [...new Array(difference)].fill(undefined).map((_val, index) => {
return <div style={{ width: '13vw' }} key={index} />;
});
};
const [areHelpersEnabled, setAreHelpersEnabled] = React.useState(false);
const [isCustomSize, setIsCustomSize] = React.useState(false);
const [isInitialLoad, setIsInitialLoad] = React.useState(true);
const [copied, setCopied] = React.useState('');
React.useEffect(() => {
const timeout = setTimeout(() => {
setCopied('');
}, 2000);
return () => clearTimeout(timeout);
}, [copied]);
const [searchQuery, setSearchQuery] = React.useState('');
const filteredIcons = completeIcons.filter((icon) => {
return icon.toLowerCase().includes(searchQuery);
});
React.useEffect(() => {
setIsInitialLoad(false);
}, [isCustomSize]);
const ICONS_DEFAULT_VALUES = {
size: isCustomSize ? DEFAULT_ICON_BREAKPOINT : DEFAULT_ICON_SIZE,
weight: 1,
};
const iconSize = isInitialLoad ? DEFAULT_ICON_BREAKPOINT : ICONS_DEFAULT_VALUES.size;
const [size, setSize] = React.useState(iconSize);
const [weight, setWeight] = React.useState(ICONS_DEFAULT_VALUES.weight);
const [color, setColor] = React.useState('');
<div style={{ height: '100vh', overflow: 'scroll' }}>
<p style={{ fontWeight: 'bold', fontSize: '20px', margin: 0, padding: '20px 10px' }}>
Всего иконок: {completeIcons.length}
</p>
<div style={{ display: 'flex' }}>
<div
style={{
position: 'relative',
display: 'flex',
justifyContent: 'space-between',
padding: '24px',
width: '100vw',
}}
>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, auto)',
gap: '15px',
justifyContent: 'space-between',
padding: '24px',
}}
>
<div
style={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'space-between',
maxWidth: '100vw',
gap: '10px',
}}
>
{!filteredIcons.length && <p>Попробуйте задать другой поисковой запрос</p>}
{filteredIcons.map((name) => {
const Icon = allIcons[name];
return (
<React.Fragment key={name}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
borderRadius: '8px',
marginBottom: '10px',
background: '#fff',
boxShadow: 'rgb(0 0 0 / 10%) 0px 1px 3px 0px, rgb(0 0 0 / 6%) 0px 1px 2px 0px',
width: '15.5vw',
height: '150px',
}}
key={name}
>
<div
style={{
position: 'relative',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
}}
>
{areHelpersEnabled && (
<div
style={{
position: 'absolute',
width: '4px',
height: '4px',
backgroundColor: 'red',
borderRadius: '9999px',
}}
/>
)}
<Icon
style={{ outline: areHelpersEnabled ? '1px solid black' : undefined }}
size={isCustomSize ? size : breakpoints[size]}
weight={weights[weight]}
color={color}
/>
</div>
<div
onClick={() => {
const iconColor = color ? `color={'${color}'}` : '';
const customSizeIconName = `<${name} size={${size}} weight={'${weights[weight]}'} ${iconColor} />`;
const iconName = `<${name}${breakpoints[size]}${capitalize(weights[weight])} ${iconColor} />`;
navigator.clipboard.writeText(isCustomSize ? customSizeIconName : iconName);
setCopied(name);
}}
style={{ display: 'flex', alignItems: 'baseline', cursor: 'pointer' }}
>
<p style={{ fontSize: '12px', margin: '7px 0 14px', paddingRight: '5px', fontWeight: 'bold' }}>
{name === copied ? 'Название скопировано' : name}
</p>
<button style={{ background: 'none', border: 'none', height: '18px', padding: 0 }}>
{name === copied ? <CheckAIcon /> : <CopyIcon style={{ cursor: 'pointer' }} />}
</button>
</div>
</div>
</div>
</React.Fragment>
);
})}
{generateAdditionalItems(completeIcons.length, 5)}
</div>
</div>
<ControlsWrapper
title="Кастомизация"
popupTopPos={'50px'}
titleChildren={
<button
style={{
background: '#fff',
borderRadius: '8px',
cursor: 'pointer',
padding: '4px 6px',
border: 'none',
boxShadow: 'inset 0 0 0 1px var(--theme-ui-colors-border,#d1d5da)',
}}
onClick={() => {
setAreHelpersEnabled(false);
setSize(isCustomSize ? iconSize : DEFAULT_ICON_BREAKPOINT);
setWeight(ICONS_DEFAULT_VALUES.weight);
setIsCustomSize(false);
}}
>
Сбросить
</button>
}
>
<input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
style={{ marginBottom: '10px' }}
placeholder="Поиск по иконкам"
/>
<label
style={{
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
fontWeight: 'bold',
}}
>
<input
type="checkbox"
checked={areHelpersEnabled}
onChange={() => setAreHelpersEnabled(!areHelpersEnabled)}
/>
Вспомогательные элементы
</label>
<label style={{ display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<p style={{ marginRight: '5px' }}>Размер</p>
<label style={{ display: 'flex', alignItems: 'center' }}>
<input
type="checkbox"
checked={isCustomSize}
onChange={() => {
setIsCustomSize(!isCustomSize);
setSize(ICONS_DEFAULT_VALUES.size);
}}
/>
Кастомный
</label>
</div>
<p style={{ fontWeight: 'bold' }}>{isCustomSize ? size : breakpoints[size]}px</p>
</div>
<input
type="range"
min={isCustomSize ? 12 : 0}
max={isCustomSize ? 100 : breakpoints.length - 1}
value={size}
onChange={(e) => setSize(+e.target.value)}
/>
</label>
<WeightRange weight={weight} setWeight={setWeight} />
<ColorPicker color={color} setColor={setColor} />
</ControlsWrapper>
</div>
</div>
</div>;
Соотнесение названий старых и новых иконок
import { OldNewIconsCorrelation } from './__stories__/OldNewIconsCorrelation';
<OldNewIconsCorrelation />;