Widget

Widget — контейнер продуктовой карточки: TitleClickable в шапке, опциональный SegmentControl, слот управления, массив действий (Button, kebab/droplist) и body с состояниями default / loading / error.

Когда использовать

  • Компактные блоки на dashboard и overview-страницах: заголовок-ссылка, переключатель вкладок, действия и контент в одной карточке.
  • Нужны единые состояния загрузки и ошибки с InfoBlock и кнопкой повтора без ручной вёрстки.
  • Действия должны адаптироваться по ширине: primary в шапке (wide desktop), overflow в kebab, на узком layout — кнопки в footer.

Когда не нужен Widget:

  • Простая карточка без шапки и действий:

  • Только кликабельный заголовок без оболочки:

  • Сложная таблица или список с сортировкой и пагинацией:

    • используйте отдельный 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.
  • errorInfoBlock с 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.
  • kebabButtonKebab + 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-widget
import { Widget, BUTTON_TYPE, WIDGET_STATE } from '@cloud-ru/ds-uikit-product-widget'

Примеры использования

Контент и SegmentControl

Контент и SegmentControlUncontrolled сегменты через defaultValue
Keep product metrics, shortcuts, and status details in one compact card.
tsx
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 и действия

Wide desktop и действияPrimary в шапке, overflow в kebab; PortalContextProvider для dropdown
Actions are shown in the header for wide desktop widgets.
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<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

Loading
tsx
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 и повтор

Error и повторonClickUpdate переключает state обратно в default
Metrics are unavailable
Try reloading the widget.
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 (
    <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

Types

PropsWidgetProps
PropTypeDefaultRequiredDescription
actionsAction[]noДействия в шапке/footer.
actionsChildrenReactNodenoДополнительный слот рядом с действиями.
childrenReactNodeyesКонтент виджета.
classNamestringnoДополнительный CSS-класс.
data-test-idstringno
errorStateWidgetErrorStatePropsnoНастройки error-состояния.
headerWidgetHeaderPropsyesПропсы кликабельного заголовка.
loadingStateWidgetLoadingStatePropsnoНастройки loading-состояния.
segmentControlSegmentControlPropsnoПропсы SegmentControl в шапке.
state"default" | "error" | "loading"noСостояние виджета.
widebooleannoТолько desktop: wide-раскладка виджета. На mobile принудительно выключается (`wide && !isMobile`).

Unions

Types

WidgetProps

Unions

Адаптивность

Widget — адаптивный компонент: DOM остаётся единым, но при mobile-раскладке карточка перестраивается. Раскладку он берёт из AdaptiveProvider (контекст @cloud-ru/ds-adaptive); публичный API единый для обеих платформ:

  • desktop (по умолчанию) — учитывает wide и динамическое схлопывание действий в kebab по ширине контейнера.
  • mobile — узкий режим: wide принудительно выключен, primary-кнопки уезжают в footer.

Верстайте под desktop и поставьте один <AdaptiveProvider> в корне приложения — mobile-перестроение включается автоматически (desktop-first). Пропа layoutType у компонента нет: источник раскладки — только контекст.

Mobile layout

Mobile layoutwide игнорируется, primary уезжает в footer
On mobile, wide is ignored: primary actions move to the footer, overflow goes to kebab.
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<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.

Пропdesktopmobile
wideиспользуетсяигнорируется (принудительно выключен)

Подробнее о модели адаптивности — Адаптивность — паттерн.

Storybook

Figma

Смотри также

  • TitleClickable — заголовок-ссылка в шапке виджета.
  • SegmentControl — переключатель вкладок в шапке.
  • InfoBlock — блок ошибки внутри state='error'.