1.9.0 • Published 1 month ago

@skbkontur/icons v1.9.0

Weekly downloads
-
License
UNLICENSED
Repository
-
Last release
1 month ago

Пак современных иконок для использования в интерфейсах Контура.

Ссылка на пакет в nexus

Библиотека предоставляет два варианта иконок:

  1. Цельные иконки (например, CheckIcon): такие иконки содержат все доступные начертания иконок, но при этом занимают приблизительно в 10 раз больше места в бандле, чем гранулярные иконки
  2. Гранулярные иконки (например, 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 цельных иконок можно свести к трём правилам:

  1. Если вам не нужна фишка цельных иконок с умным размером - используйте используйте гранулярные иконки
  2. Если вам нужно менять размер иконок (например, в зависимости от размера экрана), но при этом вы не хотите чтобы ваш бандл разрастался - используйте гранулярные иконки со своей логикой, которая будет подменять иконки
  3. Если вам нужно менять размер иконок и у вас нет возможности написать свою логику для определения размера иконки или если для вас не критичен размер бандла - используйте цельные иконки

Пропы иконок:

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 { OldNewIconsCorrelation } from './__stories__/OldNewIconsCorrelation';

<OldNewIconsCorrelation />;

Шоу-кейс всех иконок

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>;
1.9.0

1 month ago

1.8.0

2 months ago

1.7.3

5 months ago

1.2.0

10 months ago

1.1.1

11 months ago

1.7.2

5 months ago

1.7.1

6 months ago

1.7.0

6 months ago

1.6.1

6 months ago

1.5.2

7 months ago

1.4.3

9 months ago

1.6.0

6 months ago

1.5.1

7 months ago

1.4.2

9 months ago

1.5.0

9 months ago

1.4.1

9 months ago

1.2.3

10 months ago

1.4.0

9 months ago

1.2.2

10 months ago

1.3.0

10 months ago

1.2.1

10 months ago

1.1.0

11 months ago

1.0.2

11 months ago

1.0.1

11 months ago

1.0.0

11 months ago

1.0.3

11 months ago

0.17.2

11 months ago

0.17.0

11 months ago

0.17.1

11 months ago

0.14.0

1 year ago

0.15.0

1 year ago

0.16.0

1 year ago

0.13.1

1 year ago

0.13.0

1 year ago

0.12.1

1 year ago

0.12.0

2 years ago

0.11.2

2 years ago