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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@elcrm/components

v0.2.14

Published

Shell UI elCRM: Layout, Header, Sidebar, Popover, Healthmap, PanelInfo, PageHead, PageShell, Section, Stack, Stat и др.

Readme

@elcrm/components

Shell UI для elCRM: Layout, Header, Section, Stack, NavSections, TabSections и др.

Стили — CSS Modules с короткими уникальными классами; значения токенов задаёт приложение (theme-dark.css / theme-light.css). Fallback в пакете нет.

Визуальный стенд

Как у @elcrm/form — страница с галереей компонентов (@elcrm/storybook):

bun i
bun run dev

Откроется http://localhost:3001. В сайдбаре стенда: section (компонент / all), size, тон Badge, вариант Menu.

Установка

bun i @elcrm/components
import {
  Layout,
  Header,
  Footer,
  List,
  Item,
  ItemColumn,
  Card,
  ActionGroup,
  Toolbar,
  TextGroup,
  Text,
  Stat,
  Stack,
  Menu,
  Loading,
  Section,
  NavSections,
  TabSections,
  Avatar,
  AvatarGroup,
  AvatarName,
  Badge,
  Brand,
  EmptyState,
  RadioGroup,
  PricingGroup,
  PricingTable,
  Table,
  CodeBlock,
  SplitPane,
  Banner,
  IconButton,
  Dropdown,
  Breadcrumb,
  PageHead,
  PageShell,
  ChatList,
  ChatMessages,
  ChatSplit,
  Sidebar,
  Popover,
  createLazyResolver,
} from "@elcrm/components";
import type { PricingPlan } from "@elcrm/components";
// CSS подтягивается с чанками; при необходимости единый файл:
// import "@elcrm/components/style.css";

Точечный импорт:

import { Header } from "@elcrm/components/Header";
import { Avatar } from "@elcrm/components/Avatar";
import { RadioGroup } from "@elcrm/components/RadioGroup";

Компоненты

| | Назначение | Ключевые props | | ---------------------- | -------------------------------- | -------------------------------------------------------------------------------------- | | Layout | Оболочка страницы | sidebar, header, footer, fixed, children | | Header | Шапка со слотами | brand, nav, search, actions | | Footer | Подвал | children | | Brand | Название / логотип | as, children, onClick | | Avatar | Фото или инициалы | src, name, size, radius, status, as | | AvatarName | Аватар + title + description | title, description, radius, --avatar-name-*-color | | AvatarGroup | Стопка аватаров + «+N» | items (value/title/name), max, add | | Badge | Метка / статус | tone, size, dot, children | | RadioGroup | Выбор одной опции-карточки | items, value, indicator, indicatorSide | | PricingGroup | Тарифные карточки в ряд | items (PricingPlan), selectable, value | | PricingTable | Сравнение тарифов (таблица) | те же items + sections | | Table | Таблица данных со скроллом | layout (auto/fixed), стандартные props <table> | | CodeBlock | Блок кода с bar и копированием | title, code / children, onCopy, actions | | SplitPane | Две панели с разделителем | direction, ratio, onRatioChange, first, second, minFirst / minSecond | | Banner | Статусная полоса | tone (info/warning/danger/success), не toast | | IconButton | Иконка + tooltip | name, обязательный label, tooltip (false — без подсказки) | | Menu | Навигация в шапке (не popover) | items (value, badge: 0 скрыт), value, onSelect, variant (plain/line) | | Dropdown | Выпадающее меню (items, portal) | label/trigger, items/groups, onSelect | | Popover | Свой попап в portal | trigger, children, align, open | | Breadcrumb | Хлебные крошки | items, separator, collapseAfter | | Sidebar | Левое меню приложения | groups, value, item.items + defaultOpen, open / onOpenChange, footerItems | | PageShell | Каркас модуля (скролл + footer) | wide, scroll, footer, children | | ChatList | Колонка диалогов | header, footer, children (List/Item), bodyRef | | ChatMessages | Колонка переписки | header, footer, children, bodyRef, onBodyScroll | | ChatSplit | Список + лента | list, messages | | PageHead | Заголовок страницы + actions | title, sup (текст/Badge), subtitle, actions, sticky, children | | Section | Блок контента / auth / подраздел | title, description, icon, actions, variant | | NavSections | Сайдбар + lazy-панель | groups, item.items + defaultOpen, open / onOpenChange, resolveSection | | TabSections | Горизонтальные вкладки + lazy | items (value + badge), resolveSection | | Card | Карточка | title, --card-pointer | | ActionGroup | Ряд иконок в карточке | label, items и/или children | | Toolbar | Фильтры одной высоты | size, width (l = 100%, s = по контенту), align, label, children | | TextGroup | Title + description | title, description, size | | Text | Короткий текст | tone (default/muted), as | | ItemColumn | Иконка + столбик в Item | icon, title, extra, aside, description, footer / meta, unread | | Stat | Карточка-метрика | label, value, tone, labelMuted | | List / Item | Список и строка | List: row; Item: as, active, variant (plain/card) | | Stack | Ряд / колонка / сетка | direction, columns (N или ["1fr", "100px"]), gap | | Loading | Плейсхолдер загрузки | children; --loading-min-height | | EmptyState | Пустой / ошибка | title, description, icon, actions; --empty-* | | createLazyResolver | Кэш import() секций | (key) => import(…) |

Слоты Header заполняет приложение: поиск — @elcrm/search, кнопки — @elcrm/button, и т.д.

Block / Row / Column — deprecated-обёртки над Stack. В проектах: elcrm migrate components.

Sidebar

Левое меню приложения. variant="cards" — секции-карточки; variant="plain" — плоский список (backoffice). Иконки, колокольчик, тема, карточка пользователя — слоты приложения. Роутер не подключаем.

<Sidebar
  variant="cards"
  value={page}
  onSelect={setPage}
  brand={<Brand as="strong">App</Brand>}
  extra={<Notifications.Bell />}
  groups={[
    {
      value: "main",
      items: [{ value: "home", label: "Обзор", icon: <Icon /> }],
    },
    {
      value: "admin",
      title: "Админ",
      items: [
        {
          value: "bridge",
          label: "VK Bridge",
          defaultOpen: true,
          items: [
            { value: "intro", label: "Введение" },
            { value: "events", label: "События" },
          ],
        },
      ],
    },
  ]}
  footer={<AvatarName title="Паша" description="mail@…" />}
  footerItems={[
    { value: "docs", label: "Документация", icon: <Icon /> },
    { value: "theme", label: "Тема", submenu: true },
    { value: "logout", label: "Выйти", danger: true },
  ]}
  onFooterSelect={(value) => console.log(value)}
  version="v 0.2.26"
/>

Токены: --sidebar-width, --sidebar-item-font-size, --sidebar-item-padding, --sidebar-icon-size, --sidebar-group-font-size, --sidebar-indent, --sidebar-item-active. Для orange active в plain переопределите --sidebar-item-active в теме приложения.

Stack

<Stack gap="0.75rem">{/* колонка */}</Stack>
<Stack direction="row" gap={8}>{/* ряд */}</Stack>
<Stack direction="grid" gap={16}>{/* auto-fit, мин. ширина --stack-grid-min */}</Stack>
<Stack direction="grid" columns={3} gap={12}>{/* stretch; не height:100% у детей */}</Stack>

Card

Курсор задаёт приложение токеном --card-pointer (auto по умолчанию):

<Card title="Тенанты" style={{ ["--card-pointer" as string]: "pointer" }}>
  12
</Card>

RadioGroup

Декларативно через items — всё из коробки. Нет поля → блок не рисуется.

const items = [
  {
    value: "basic",
    icon: <LayersIcon />,
    title: "Basic plan",
    meta: "$10/month",
    description: "Includes up to 10 users…",
  },
  {
    value: "pro",
    title: "Pro plan",
    meta: "$20/month",
    description: "Includes up to 20 users…",
  },
  {
    value: "olivia",
    icon: <Avatar name="Olivia Rhye" src="…" />,
    iconFramed: false,
    title: "Olivia Rhye",
    meta: "@olivia",
    description: "Product Manager",
  },
  {
    value: "visa",
    icon: <VisaLogo />,
    title: "Visa ending in 1234",
    description: "Expiry 06/2028",
    actions: (
      <>
        <button type="button">Set as default</button>
        <button type="button">Edit</button>
      </>
    ),
  },
  {
    value: "enterprise",
    variant: "split",
    icon: <BoltIcon />,
    title: "Enterprise plan",
    price: "$40",
    meta: "per month",
    badge: <Badge tone="success">Limited time only</Badge>,
    description: "Unlimited users…",
  },
];

<RadioGroup
  items={items}
  value={plan}
  onValueChange={setPlan}
  indicator="check"
/>;

{
  /* Круглый radio слева */
}
<RadioGroup
  items={items}
  value={plan}
  onValueChange={setPlan}
  indicator="radio"
  indicatorSide="start"
/>;

PricingGroup / PricingTable

Один тип PricingPlan и один массив plans на оба компонента. Table только добавляет sections (матрица по plan.value).

import type { PricingPlan } from "@elcrm/components";

const plans: PricingPlan[] = [
  {
    value: "basic",
    title: "Basic plan",
    price: "$10",
    meta: "per month",
    description: "Basic features for up to 10 users.",
    actions: (
      <>
        <button type="button">Get started</button>
        <button type="button">Chat to sales</button>
      </>
    ),
    actionsPlacement: "before",
    featuresLabel: "FEATURES",
    featuresSummary: "Everything in Starter plus…",
    features: [
      "Access to basic features",
      "Basic reporting and analytics",
    ],
  },
  {
    value: "business",
    title: "Business plan",
    badge: "Popular",
    badgePlacement: "pill",
    highlighted: true,
    price: "$20",
    meta: "per month",
    description: "Growing teams up to 20 users.",
    actions: <button type="button">Get started</button>,
    features: ["200+ integrations", "Advanced custom fields"],
  },
  {
    value: "enterprise",
    badge: "Most popular plan",
    badgePlacement: "banner",
    priceFirst: true,
    price: "$40/mth",
    title: "Enterprise plan",
    description: "Advanced features + unlimited users.",
    features: ["Audit log", "Data loss prevention"],
    actions: <button type="button">Get started</button>,
  },
];

<PricingGroup items={plans} />
<PricingGroup items={plans} selectable value={plan} onValueChange={setPlan} />

const sections = [
  {
    title: "Overview",
    rows: [
      {
        key: "basic",
        label: "Basic features",
        hint: "Core product features",
        values: { basic: true, business: true, enterprise: true },
      },
      {
        key: "users",
        label: "Users",
        values: { basic: "10", business: "20", enterprise: "Unlimited" },
      },
    ],
  },
];

<PricingTable items={plans} sections={sections} />
<PricingTable
  items={plans}
  sections={sections}
  footerActions
  selectable
  value={plan}
  onValueChange={setPlan}
/>

true → галочка, false / нет значения → «—», иначе текст/node.

PageShell / PageHead / Section

Три разных роли — не путать:

| | Роль | | ------------- | -------------------------------------------------------------------------------------------------------- | | PageShell | Каркас модуля: скролл в body (scroll, по умолчанию да), опциональный footer снизу | | PageHead | Строка title (+ sup надстрочно, subtitle) слева и actions справа; sticky — прилипает к верху скролла | | Section | Блок контента / auth-карточка / подраздел с title+description |

<PageShell scroll footer={<button type="button">Сохранить</button>}>
  <PageHead
    sticky
    title="Товары"
    sup={12}
    subtitle="Каталог"
    actions={<button>Добавить</button>}
  />
  <Section title="Фильтры">{/* … */}</Section>
</PageShell>

ChatList / ChatMessages / ChatSplit

Каркас мессенджера, не пузыри и не композер. Строки диалогов — List + Item (+ ItemColumn / Avatar). Поиск и ввод — слоты header / footer приложения.

| | Роль | | ---------------- | ----------------------------------------------------------------------------- | | ChatList | Колонка диалогов: header (поиск), children (список), footer | | ChatMessages | Колонка переписки: header (собеседник), children (лента), footer (ввод) | | ChatSplit | Ряд list + messages (--chat-split-gap) |

Токены: --chat-list-width, --chat-split-gap, --chat-radius, --chat-background, --chat-border (рамка и разделители header/footer), --chat-header-padding / --chat-body-padding / --chat-footer-padding, узкие --chat-messages-*-padding. Узкие --chat-* по умолчанию = --shell-*.

<ChatSplit
  list={
    <ChatList header={<SearchField name="q" placeholder="Найти" />}>
      <List>
        <li>
          <Item as="button" active>
            <ItemColumn
              icon={<Avatar name="Иван" />}
              title="Иван"
              description="Привет"
            />
          </Item>
        </li>
      </List>
    </ChatList>
  }
  messages={
    <ChatMessages
      header={<AvatarName title="Иван" description="в сети" />}
      footer={<TextareaField name="body" placeholder="Сообщение" />}>
      {bubbles}
    </ChatMessages>
  }
/>

AvatarGroup

Стопка аватаров через items:

const people = [
  { value: "1", src: "/a.jpg", name: "Anna" },
  { value: "2", name: "Boris" },
  { value: "3", src: "/c.jpg", name: "Clara" },
  // …
];

<AvatarGroup items={people} size="s" max={5} />
<AvatarGroup items={people} size="m" max={5} add onAdd={() => {}} />
<AvatarGroup
  items={people}
  size="l"
  max={5}
  add
  onAdd={() => {}}
  onItemClick={(item) => console.log(item)}
/>

max — сколько показать; остаток → «+N». add — кнопка «+» справа.

Avatar / AvatarName

Канон: у текста — title + description; name — только для инициалов (если нет src).

<Avatar src="/a.jpg" name="Olivia" size="m" status="online" />
<Avatar name="Olivia Rhye" size="l" status="offline" />

<AvatarName
  src="/a.jpg"
  title="Olivia Rhye"
  description="[email protected]"
  size="m"
  status="online"
/>

{/* name — инициалы; title — подпись */}
<AvatarName
  as="button"
  name="Olivia Rhye"
  title="Olivia Rhye"
  description="Product Manager"
  size="s"
  onClick={() => {}}
/>

Dropdown

Выпадающее меню через items или groups (разделители между группами):

<Dropdown
  label="Account"
  align="end"
  groups={[
    {
      items: [
        { value: "cut", label: "Cut", shortcut: "⌘X" },
        { value: "copy", label: "Copy", shortcut: "⌘C" },
        { value: "paste", label: "Paste", shortcut: "⌘V" },
      ],
    },
    {
      items: [
        { value: "edit", label: "Edit" },
        { value: "dup", label: "Duplicate" },
        { value: "del", label: "Delete", danger: true },
      ],
    },
    {
      items: [{ value: "details", label: "View details", submenu: true }],
    },
  ]}
  onSelect={(value) => console.log(value)}
/>;

{
  /* Выбор с галочкой */
}
<Dropdown
  label="Can edit"
  items={[
    { value: "owner", label: "Owner" },
    { value: "edit", label: "Can edit", checked: true },
    { value: "view", label: "Can view" },
  ]}
/>;

{
  /* Иконки, статус, submenu */
}
<Dropdown
  label="Actions"
  groups={[
    {
      items: [
        { value: "back", label: "Back", icon: <BackIcon /> },
        {
          value: "reload",
          label: "Reload",
          icon: <ReloadIcon />,
          shortcut: "⌘R",
        },
      ],
    },
    {
      items: [
        { value: "bm", label: "Show bookmarks", checked: true },
        { value: "url", label: "Show full URLs" },
      ],
    },
    {
      items: [
        { value: "o", label: "Olivia Rhye", status: "online" },
        { value: "s", label: "Sienna Hewitt", status: "offline" },
      ],
    },
    {
      items: [
        {
          value: "more",
          label: "More tools",
          icon: <CubeIcon />,
          submenu: true,
        },
      ],
    },
  ]}
/>;

{
  /* Колонки: checkbox, title, не закрывать при клике */
}
<Dropdown
  trigger={<button type="button">⚙</button>}
  title="Колонки в списке"
  closeOnSelect={false}
  items={[
    { value: "photo", label: "Фото", checkbox: true, checked: true },
    {
      value: "sku",
      label: "SKU",
      checkbox: true,
      checked: true,
      disabled: true,
    },
    { value: "brand", label: "Бренд", checkbox: true, checked: false },
  ]}
  onSelect={(value) => toggleColumn(value)}
/>;

Menu — навигация в шапке (inline, не popover).

Dropdown vs Popover: Dropdown — пункты items/groups. Popover — свой children в portal (тот же z-стек, что у @elcrm/form). @elcrm/overlay Menu — устаревший алиас Popover. Select — @elcrm/form, не overlay.

Breadcrumb

<Breadcrumb
  separator="chevron" // или "slash"
  items={[
    { value: "home", icon: <HomeIcon />, href: "/" },
    { value: "settings", label: "Settings", href: "/settings" },
    { value: "team", label: "Team" }, // текущий — последний
  ]}
/>;

{
  /* Иконка + avatar + dropdown */
}
<Breadcrumb
  items={[
    { value: "org", icon: <Logo />, label: "Warpspeed", href: "/" },
    {
      value: "user",
      icon: <Avatar src="…" name="Caitlyn" size="s" />,
      label: "Caitlyn King",
      dropdown: true,
      onClick: () => openSwitcher(),
    },
    { value: "settings", label: "Settings" },
  ]}
/>;

{
  /* Длинный путь → первые 2, …, последний */
}
<Breadcrumb
  collapseAfter={2}
  separator="slash"
  items={longPath}
  onEllipsisClick={() => {}}
/>;

Пример

<Layout
  fixed={["header", "footer", "sidebar"]}
  sidebar={<Sidebar groups={groups} value={page} onSelect={setPage} />}
  header={
    <Header
      brand={<Brand onClick={() => {}}>App</Brand>}
      nav={<Menu items={[{ value: "home", label: "Главная", active: true }]} />}
      actions={
        <>
          <Avatar as="button" name="Admin" />
          <button type="button">Выйти</button>
        </>
      }
    />
  }
  footer={<Footer>© elCRM</Footer>}>
  <Section title="Профиль" description="Данные аккаунта">
    <Card>
      <List>
        <li>
          <Item as="li">Строка</Item>
        </li>
      </List>
    </Card>
  </Section>
</Layout>;

{
  /* `fixed` по умолчанию = header + footer + sidebar. Скролл только у children.
    Высота каркаса: `--layout-height` (100dvh). Вложенно: 100% + явная высота родителя.
    `fixed={[]}` — обычный поток страницы. */
}

{
  /* Экран входа */
}
<Section
  variant="auth"
  title="Вход"
  description="Демо: admin / admin"
  actions={<button type="button">Войти</button>}>
  {/* поля формы */}
</Section>;

{
  /* Настройки: меню слева + sections/*.tsx справа */
}
const resolve = createLazyResolver(
  (value) => import(`./sections/${value}.tsx`),
);

<NavSections
  defaultValue="Profile"
  resolveSection={resolve}
  groups={[
    {
      label: "Личное",
      items: [
        { value: "Profile", label: "Профиль" },
        {
          value: "bridge",
          label: "VK Bridge",
          defaultOpen: true,
          items: [
            { value: "Intro", label: "Введение" },
            { value: "Events", label: "События" },
          ],
        },
        { value: "Appearance", label: "Оформление" },
      ],
    },
  ]}
/>;

Файлы секций: sections/Profile.tsx, sections/Appearance.tsxexport default.

Горизонтальные вкладки (без своего заголовка — используйте Section):

<Section title="Дети" description="Список">
  <TabSections
    defaultValue="News"
    resolveSection={createLazyResolver((v) => import(`./sections/${v}.tsx`))}
    items={[
      { value: "News", label: "Новые", badge: 1 },
      { value: "Active", label: "Активные" },
    ]}
  />
</Section>

CSS-токены

Как в @elcrm/form: сначала общие, потом узкие.

| Слой | Где | Примеры | | --------- | ---------------------- | ----------------------------------------------------------------------------------------- | | Палитра | theme-light / dark | --text, --muted, --line, --panel, --accent, --shadow | | Control | theme.css | --control-height, --control-radius, --control-font-size | | Shell | theme.css + light/dark | --shell-* — общие для Dropdown, RadioGroup, Pricing*, Breadcrumb, AvatarName… | | Popover | light/dark | --popup-shadow (общий с form) | | Узкие | theme.css | --header-height, --avatar-size-*, --dropdown-min-width, --pricing-card-min-width… |

Задай --shell-* один раз — большинство новых компонентов подхватят.

shell (геометрия → theme.css)

--shell-radius          (= --control-radius)
--shell-font-size       (= --control-font-size)
--shell-font-size-sm
--shell-font-size-lg
--shell-font-weight
--shell-gap / -sm / -lg
--shell-padding         (или -block / -inline)
--shell-icon-size
--shell-check-size
--shell-status-size
--shell-disabled-opacity
--shell-z

shell (цвет → light / dark, обычно алиасы палитры)

--shell-color           (= --text)
--shell-color-muted     (= --muted)
--shell-color-accent    (= --accent)
--shell-color-danger
--shell-color-success
--shell-color-warning
--shell-border          (= --line, только цвет)
--shell-background      (= --panel)
--shell-bg              фон страницы (sticky PageHead)
--shell-background-muted / -hover / -selected
--shell-shadow

Узкие (только если нужно иначе)

--layout-height / --layout-padding / --layout-margin / --layout-max-width
--header-height / --header-z / --header-margin / --header-shadow / --header-radius / --header-border / --header-nav-justify
--section-padding / --section-title-size / --section-title-size-md / --section-auth-card-max-width
--nav-sections-padding / --nav-sections-nav-width / --nav-sections-main-max-width
--avatar-size-s|m|l / --avatar-group-overlap
--dropdown-min-width / --dropdown-max-width / --dropdown-max-height
--pricing-card-min-width / --pricing-table-label-width
--card-pointer
--stack-grid-min
--toolbar-gap
--healthmap-cell / --healthmap-gap / --healthmap-l0…l4
--table-font-size / --table-cell-padding / --table-head-font-size / --table-code-font-size
--code-block-bg / --code-block-padding / --code-block-font-size
--split-handle-size / --split-handle-bg / --split-handle-bg-hover
--banner-padding / --banner-bg / --banner-border
--loading-min-height
--empty-max-width / --empty-padding
--panel-info-width / --panel-info-width-min / --panel-info-overlay-max / --panel-info-backdrop
--row-gap / --column-gap
--popup-shadow

Эталон геометрии+shell: panel theme.css. Цвета: theme-light / theme-dark.
В пакете: tokens.css / light.css / dark.css — дефолты для elcrm css.

Миграция в приложениях

elcrm update --fix --test   # включает migrate components
# или точечно:
elcrm migrate components
elcrm css
elcrm docs && elcrm cursor

Пилюля components: Block/Row/Column → Stack; key/activeKeyvalue; старые --item-* / --menu-* / … → --shell-*.

Лицензия

MIT © MaSkal