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

@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.
  • 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

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 | используется | игнорируется (принудительно выключен) |

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