1.10.0 • Published 5 months ago

ui-parking-icons v1.10.0

Weekly downloads
-
License
UNLICENSED
Repository
-
Last release
5 months 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

6 months ago

1.7.2

6 months ago

1.8.0

6 months ago

1.10.0

5 months ago

1.7.1

6 months ago

1.6.1-tests-fixed

6 months ago