@elcrm/components
v0.2.14
Published
Shell UI elCRM: Layout, Header, Sidebar, Popover, Healthmap, PanelInfo, PageHead, PageShell, Section, Stack, Stat и др.
Maintainers
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/componentsimport {
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.tsx — export 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-zshell (цвет → 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/activeKey → value; старые --item-* / --menu-* / … → --shell-*.
Лицензия
MIT © MaSkal
