# @cloud-ru/ds-uikit-product-widget > Карточка продуктового виджета с кликабельным заголовком, SegmentControl, действиями и состояниями loading/error. Docs: /snack-v2/components/uikit-product-widget/ ## Установка ```sh pnpm add @cloud-ru/ds-uikit-product-widget ``` ## Когда использовать - Компактные блоки на dashboard и overview-страницах: заголовок-ссылка, переключатель вкладок, действия и контент в одной карточке. - Нужны единые состояния загрузки и ошибки с `InfoBlock` и кнопкой повтора без ручной вёрстки. - Действия должны адаптироваться по ширине: primary в шапке (wide desktop), overflow в kebab, на узком layout — кнопки в footer. Когда **не** нужен `Widget`: - Простая карточка без шапки и действий: - используйте [`@cloud-ru/ds-block`](/components/block) или [`@cloud-ru/ds-card`](/components/card). - Только кликабельный заголовок без оболочки: - используйте [`TitleClickable`](/components/uikit-product-title-clickable). - Сложная таблица или список с сортировкой и пагинацией: - используйте отдельный 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` — достаточно одного места. ## API ### Actions | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `actions` | `Action[]` | — | no | | | `actionsChildren` | `ReactNode` | — | no | | | `fullWidthPrimaryAction` | `boolean` | — | no | | | `showOverflowActions` | `boolean` | — | no | | | `state` | `default \| error \| loading` | — | no | | | `wide` | `boolean` | — | no | | ### ActionView | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `appearance` | `critical \| neutral \| primary` | — | no | Вариант оформления | | `as` | `button` | — | no | Элемент или компонент для рендера: 'button' \| 'a' \| ComponentType (например Link из react-router-dom) | | `button` | `Omit, "view" \| "label" \| "icon"> \| (Omit, "view" \| "appearance"> & { buttonType?: "filled"; }) \| (Omit<...> & { ...; })` | — | no | | | `className` | `string` | — | no | Дополнительный класс | | `commonProps` | `{ className?: string; size?: "s" \| "m" \| "l"; fullWidth?: boolean \| undefined; } \| undefined` | — | no | | | `counter` | `Omit` | — | no | Пропсы для counter. `appearance` можно задать явно (по умолчанию наследуется от appearance кнопки). | | `data-test-id` | `string` | — | no | | | `disabled` | `boolean` | — | no | Отключена | | `fullWidth` | `boolean` | — | no | На всю ширину | | `hidden` | `boolean` | — | no | Скрыть действие без удаления из массива. | | `icon` | `ReactNode` | — | no | Иконка | | `iconPosition` | `after \| before` | — | no | Позиция иконки относительно текста | | `innerRef` | `((instance: HTMLButtonElement \| null) => void) \| RefObject \| null` | — | no | Ref на реальный DOM-элемент/инстанс, который рендерится через `as`. Используем явный проп, чтобы не зависеть от `forwardRef` и не тащить type-assertions на экспорт. | | `label` | `string` | — | no | Текст кнопки | | `list` | `WidgetActionListProps` | — | yes | | | `loading` | `boolean` | — | no | Состояние загрузки | | `minWidth` | `boolean` | — | no | Минимальная ширина контейнера (`min-width` из токена размера). По умолчанию `true`. `false` — кнопка сжимается по контенту вместо фиксированного минимума. | | `size` | `l \| m \| s` | — | no | Размер | | `tooltip` | `TooltipProps` | — | no | Tooltip вокруг кнопки действия. | | `variant` | `droplist \| filled \| function \| kebab \| outline \| simple \| tonal` | — | no | | ### ButtonDroplist | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `button` | `(Omit, "view" \| "appearance"> & { buttonType?: "filled" \| undefined; }) \| (Omit, "view" \| "icon" \| "appearance" \| "iconPosition"> & { ...; })` | — | yes | | | `list` | `WidgetActionListProps` | — | yes | | ### ButtonKebab | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `button` | `Omit, "view" \| "label" \| "icon">` | — | no | | | `list` | `WidgetActionListProps` | — | yes | | ### Content | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `errorState` | `WidgetErrorStateProps` | — | no | | | `loadingState` | `WidgetLoadingStateProps` | — | no | | | `state` | `default \| error \| loading` | — | no | | | `wide` | `boolean` | — | no | | ### ControlBlock | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `actions` | `Action[]` | — | no | | | `actionsChildren` | `ReactNode` | — | no | | | `segmentControl` | `SegmentControlProps` | — | no | | | `state` | `default \| error \| loading` | — | no | | | `wide` | `boolean` | — | no | | ### Widget | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `actions` | `Action[]` | — | no | Действия в шапке/footer. | | `actionsChildren` | `ReactNode` | — | no | Дополнительный слот рядом с действиями. | | `children` | `ReactNode` | — | yes | Контент виджета. | | `className` | `string` | — | no | Дополнительный CSS-класс. | | `data-test-id` | `string` | — | no | | | `errorState` | `WidgetErrorStateProps` | — | no | Настройки error-состояния. | | `header` | `WidgetHeaderProps` | — | yes | Пропсы кликабельного заголовка. | | `loadingState` | `WidgetLoadingStateProps` | — | no | Настройки loading-состояния. | | `segmentControl` | `SegmentControlProps` | — | no | Пропсы SegmentControl в шапке. | | `state` | `default \| error \| loading` | — | no | Состояние виджета. | | `wide` | `boolean` | — | no | Только desktop: wide-раскладка виджета. На mobile принудительно выключается (`wide && !isMobile`). | #### Related types - `Action` (alias) - `AvatarProps` (interface) - `BaseAction` (interface) - `ButtonDroplistProps` (interface) - `ButtonKebabProps` (interface) - `ButtonProps` (interface) - `InfoBlockProps` (interface) - `Segment` (interface) - `SegmentControlProps` (interface) - `Size` = `s | xs` - `TooltipProps` (interface) - `Value` = `0% | 100% | 50%` - `WidgetActionListProps` (interface) - `WidgetErrorStateProps` (interface) - `WidgetHeaderProps` (interface) - `WidgetLoadingStateProps` (interface) - `WidgetState` = `default | error | loading` - `Width` = `auto | full` ## Примеры ### DefaultContent ```tsx import { WIDTH } from '@cloud-ru/ds-segment-control'; import { Widget } from '@cloud-ru/ds-uikit-product-widget'; export function DefaultContent() { return ( Keep product metrics, shortcuts, and status details in one compact card. ); } ``` ### ErrorState ```tsx import { Widget } from '@cloud-ru/ds-uikit-product-widget'; import { useState } from 'react'; export function ErrorState() { const [state, setState] = useState<'default' | 'error'>('error'); return ( setState('default'), }} > {state === 'error' ? 'Metrics' : 'Metrics loaded successfully.'} ); } ``` ### LoadingState ```tsx import { Widget } from '@cloud-ru/ds-uikit-product-widget'; export function LoadingState() { return ( Billing summary ); } ``` ### MobileLayout ```tsx 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(null); return (
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. {lastAction ? Last action: {lastAction} : null}
); } ``` ### WithActions ```tsx 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(null); return (
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. {lastAction ? Last action: {lastAction} : null}
); } ```