@cloud-ru/ds-uikit-product-widget
v1.0.18
Published
Карточка продуктового виджета с кликабельным заголовком, SegmentControl, действиями и состояниями loading/error.
Downloads
9,933
Readme
Widget
@cloud-ru/ds-uikit-product-widget — Карточка продуктового виджета с кликабельным заголовком, SegmentControl, действиями и состояниями loading/error.
Widget — контейнер продуктовой карточки: TitleClickable в шапке, опциональный SegmentControl, слот управления, массив действий (Button, kebab/droplist) и body с состояниями default / loading / error.
Когда использовать
- Компактные блоки на dashboard и overview-страницах: заголовок-ссылка, переключатель вкладок, действия и контент в одной карточке.
- Нужны единые состояния загрузки и ошибки с
InfoBlockи кнопкой повтора без ручной вёрстки. - Действия должны адаптироваться по ширине: primary в шапке (wide desktop), overflow в kebab, на узком layout — кнопки в footer.
Когда не нужен Widget:
Простая карточка без шапки и действий:
- используйте
@cloud-ru/ds-blockили@cloud-ru/ds-card.
- используйте
Только кликабельный заголовок без оболочки:
- используйте
TitleClickable.
- используйте
Сложная таблица или список с сортировкой и пагинацией:
- используйте отдельный data-компонент, не оборачивайте в виджет.
✅ Передавайте
errorState.onClickUpdateи переключайтеstateобратно вdefaultпосле успешного retry.❌ Оставлять
state='error'без обработчика повтора — кнопка вInfoBlockне сможет восстановить контент.✅ На desktop с несколькими действиями включайте
wide, чтобы primary и kebab жили в шапке.❌ Ожидать wide-раскладку на mobile — флаг
wideпринудительно отключается.✅ Оборачивайте демо и страницу с kebab/droplist в
PortalContextProvider, если порталы рендерятся вне корня приложения.❌ Полагаться на глобальный portal-context из layout docs-сайта — каждый
client:visible-островок изолирован.✅ Скрывайте лишние действия через
hidden: true, не удаляя элемент из массива.❌ Дублировать один и тот же primary CTA в
actionsи вchildren— достаточно одного места.
Анатомия
State (default default)
Состояние из WIDGET_STATE:
default— рендеритchildrenв body.loading— skeleton в шапке; body —loadingState.loadingContentили skeleton приloadingState.showSkeleton.error—InfoBlockсerrorStateи кнопкойonClickUpdate; видимыеactionsостаются в шапке для retry/навигации.
Wide (default false)
false— legacy layout: overflow-действия в kebab шапки, primary-кнопки на всю ширину под контентом (кромеerror).true— primary и kebab в одной строке шапки рядом сSegmentControl/actionsChildren. На mobile-раскладке игнорируется.
Action variant
Элемент actions[i] — discriminated union по variant (default — filled Button):
filled/outline/tonal/function/simple— пропсы@cloud-ru/ds-button+ опциональныйtooltip.kebab—ButtonKebab+list.items(группы и пункты меню).droplist— кнопка-триггер + выпадающий список.
Меню обоих вариантов рендерит Droplist из @cloud-ru/ds-list: на mobile список открывается в BottomSheet, на desktop — анкорным popover'ом. Раскладка берётся из AdaptiveProvider.
Общие поля: hidden, tooltip. Для списков: closeDroplistOnItemClick, controlled open / onOpenChange.
Slots
header— пропсыTitleClickable:title,href,icon,avatar,onClick, …children— основной контент body.segmentControl— пропсыSegmentControlв шапке (частоwidth: fullна desktop).actionsChildren— произвольный узел слева от кнопок (фильтр, badge, …).loadingState/errorState— настройки соответствующих состояний.
Установка
pnpm add @cloud-ru/ds-uikit-product-widgetimport { Widget, BUTTON_TYPE, WIDGET_STATE } from '@cloud-ru/ds-uikit-product-widget'Примеры использования
Контент и SegmentControl
Uncontrolled сегменты через defaultValue
import { WIDTH } from '@cloud-ru/ds-segment-control';
import { Widget } from '@cloud-ru/ds-uikit-product-widget';
export function DefaultContent() {
return (
<Widget
header={{ title: 'Cloud servers', href: '#' }}
segmentControl={{
width: WIDTH.Full,
defaultValue: 'overview',
items: [
{ value: 'overview', label: 'Overview' },
{ value: 'events', label: 'Events' },
],
}}
>
Keep product metrics, shortcuts, and status details in one compact card.
</Widget>
);
}Wide desktop и действия
Primary в шапке, overflow в kebab; PortalContextProvider для dropdown
import { WIDTH } from '@cloud-ru/ds-segment-control';
import { BUTTON_TYPE, Widget } from '@cloud-ru/ds-uikit-product-widget';
import { useState } from 'react';
export function WithActions() {
const [lastAction, setLastAction] = useState<string | null>(null);
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<Widget
wide
header={{ title: 'Managed databases', href: '#' }}
segmentControl={{
width: WIDTH.Auto,
defaultValue: 'overview',
items: [
{ value: 'overview', label: 'Overview' },
{ value: 'events', label: 'Events' },
],
}}
actions={[
{ label: 'Create', onClick: () => setLastAction('Create') },
{
variant: BUTTON_TYPE.Outline,
label: 'Settings',
onClick: () => setLastAction('Settings'),
},
{
variant: BUTTON_TYPE.Kebab,
list: {
items: [
{ content: { label: 'Export' }, onClick: () => setLastAction('Export') },
{ content: { label: 'Archive' }, onClick: () => setLastAction('Archive') },
],
},
},
]}
>
Actions are shown in the header for wide desktop widgets.
</Widget>
{lastAction ? <span>Last action: {lastAction}</span> : null}
</div>
);
}Loading
import { Widget } from '@cloud-ru/ds-uikit-product-widget';
export function LoadingState() {
return (
<Widget
header={{ title: 'Billing', href: '#' }}
state='loading'
loadingState={{ showSkeleton: true }}
actions={[{ label: 'Refresh' }]}
>
Billing summary
</Widget>
);
}Error и повтор
onClickUpdate переключает state обратно в default
import { Widget } from '@cloud-ru/ds-uikit-product-widget';
import { useState } from 'react';
export function ErrorState() {
const [state, setState] = useState<'default' | 'error'>('error');
return (
<Widget
header={{ title: 'Monitoring', href: '#' }}
state={state}
errorState={{
errorTitle: 'Metrics are unavailable',
errorDescription: 'Try reloading the widget.',
updateButtonLabel: 'Reload',
onClickUpdate: () => setState('default'),
}}
>
{state === 'error' ? 'Metrics' : 'Metrics loaded successfully.'}
</Widget>
);
}Props
WidgetProps
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| actions | Action | BaseAction | ButtonDroplistProps | ButtonKebabProps | — | Действия в шапке/footer. |
| actionsChildren | ReactNode | — | Дополнительный слот рядом с действиями. |
| children | ReactNode | — | Контент виджета. |
| className | string | — | Дополнительный CSS-класс. |
| data-test-id | string | — | |
| errorState | WidgetErrorStateProps | — | Настройки error-состояния. |
| header | WidgetHeaderProps | — | Пропсы кликабельного заголовка. |
| loadingState | WidgetLoadingStateProps | — | Настройки loading-состояния. |
| segmentControl | SegmentControlProps | — | Пропсы SegmentControl в шапке. |
| state | "default" | "error" | "loading" | — | Состояние виджета. |
| wide | boolean | — | Только desktop: wide-раскладка виджета. На mobile принудительно выключается (wide && !isMobile). |
Related types
Action=ButtonAction | OutlineAction | TonalAction | FunctionAction | SimpleAction | KebabAction | DroplistAction
BaseAction
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| hidden | boolean \| undefined | — | Скрыть действие без удаления из массива. |
| tooltip | TooltipProps | — | Tooltip вокруг кнопки действия. |
ButtonDroplistProps
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| button | ButtonProps | — | |
| list | WidgetActionListProps | — | |
ButtonKebabProps
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| button | ButtonProps | — | |
| list | WidgetActionListProps | — | |
WidgetActionListProps
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| className | string \| undefined | — | |
| closeDroplistOnItemClick | boolean \| undefined | — | |
| items | WidgetActionListEntry | WidgetActionListGroup | WidgetActionListItem | — | |
| onOpenChange | ((open: boolean) => void) \| undefined | — | |
| open | boolean \| undefined | — | |
WidgetErrorStateProps
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| errorDescription | string \| undefined | — | Описание ошибки. |
| errorIcon | InfoBlockProps | — | Иконка InfoBlock. |
| errorTitle | string \| undefined | — | Заголовок ошибки. |
| onClickUpdate | (event: MouseEvent<HTMLElement, MouseEvent>) => void | — | Клик по кнопке повтора. |
| updateButtonLabel | string \| undefined | — | Текст кнопки повтора. |
WidgetHeaderProps
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| avatar | AvatarProps | — | Аватар с subtitle (Figma userTitle). Рендерится после заголовка, если children не передан. |
| children | string \| number \| boolean \| ReactElement<any, string \| JSXElementConstructor<any>> \| Iterable<ReactNode> \| ReactPortal \| null \| undefined | — | Произвольная нода после заголовка. Имеет приоритет над avatar. |
| className | string \| undefined | — | CSS-класс |
| fullWidth | boolean \| undefined | — | Занимает ли всю ширину |
| icon | string \| number \| boolean \| ReactElement<any, string \| JSXElementConstructor<any>> \| Iterable<ReactNode> \| ReactPortal \| null \| undefined | — | Иконка слева от заголовка. |
| title | string \| undefined | — | Заголовок |
| titleTag | ElementType \| undefined | — | Тег заголовка для семантики (например 'h2', 'h3', 'span') |
WidgetLoadingStateProps
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| loadingContent | string \| number \| boolean \| ReactElement<any, string \| JSXElementConstructor<any>> \| Iterable<ReactNode> \| ReactPortal \| null \| undefined | — | Кастомный контент для состояния загрузки. |
| showSkeleton | boolean \| undefined | — | Показывать skeleton-заглушку в body. |
WidgetState="default"|"error"|"loading"
Смотри также
TitleClickable— заголовок-ссылка в шапке виджета.SegmentControl— переключатель вкладок в шапке.InfoBlock— блок ошибки внутриstate='error'.
Адаптивность
Widget — адаптивный компонент: DOM остаётся единым, но при mobile-раскладке карточка перестраивается. Раскладку он берёт из AdaptiveProvider (контекст @cloud-ru/ds-adaptive); публичный API единый для обеих платформ:
- desktop (по умолчанию) — учитывает
wideи динамическое схлопывание действий в kebab по ширине контейнера. - mobile — узкий режим:
wideпринудительно выключен, primary-кнопки уезжают в footer.
Верстайте под desktop и поставьте один <AdaptiveProvider> в корне приложения — mobile-перестроение включается автоматически (desktop-first). Пропа layoutType у компонента нет: источник раскладки — только контекст.
Mobile layout
wide игнорируется, primary уезжает в footer
import { AdaptiveProvider, LAYOUT_TYPE } from '@cloud-ru/ds-adaptive';
import { BUTTON_TYPE, Widget } from '@cloud-ru/ds-uikit-product-widget';
import { useState } from 'react';
export function MobileLayout() {
const [lastAction, setLastAction] = useState<string | null>(null);
return (
<AdaptiveProvider layoutType={LAYOUT_TYPE.Mobile}>
<div style={{ maxWidth: 360, display: 'flex', flexDirection: 'column', gap: 8 }}>
<Widget
wide
header={{ title: 'Object storage', href: '#' }}
actions={[
{ label: 'Upload', onClick: () => setLastAction('Upload') },
{
variant: BUTTON_TYPE.Kebab,
list: {
items: [
{
content: { label: 'Delete bucket' },
onClick: () => setLastAction('Delete bucket'),
},
],
},
},
]}
>
On mobile, wide is ignored: primary actions move to the footer, overflow goes to kebab.
</Widget>
{lastAction ? <span>Last action: {lastAction}</span> : null}
</div>
</AdaptiveProvider>
);
}Как форсировать платформу
Форс — только контекстом, не пропом:
- Поддерево — вложенный провайдер:
import { AdaptiveProvider } from '@cloud-ru/ds-adaptive' <AdaptiveProvider layoutType='mobile'> <Widget header={header} actions={actions}>{content}</Widget> </AdaptiveProvider> - Отдельный компонент —
withLayoutType(module-scope, сахар над провайдером):import { withLayoutType } from '@cloud-ru/ds-adaptive' import { Widget } from '@cloud-ru/ds-uikit-product-widget' const MobileWidget = withLayoutType(Widget, 'mobile')
Платформенные пропы
Таблица синхронизирована с JSDoc-пометками у WidgetProps.
| Проп | desktop | mobile |
|------|---------|--------|
| wide | используется | игнорируется (принудительно выключен) |
Подробнее о модели адаптивности — Адаптивность — паттерн.
