npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@skbkontur/icons

v1.10.0

Published

react-ui-icons

Downloads

1,243

Readme

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

Ссылка на пакет в 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>;