# @cloud-ru/ds-uikit-product-notification > Пакет уведомлений — карточка NotificationCard, лента NotificationPanelContent со стеками и группами и адаптивная обёртка NotificationPanel (drawer на desktop, bottom-sheet на mobile). Docs: /snack-v2/components/uikit-product-notification/ ## Установка ```sh pnpm add @cloud-ru/ds-uikit-product-notification ``` ## API ### ActionsButton | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `actions` | `Action[]` | — | yes | | | `className` | `string` | — | no | | | `icon` | `ReactElement>` | — | yes | | | `open` | `boolean` | — | yes | | | `setDroplistOpen` | `Dispatch>` | — | yes | | | `testIds` | `ActionsButtonTestIds` | — | yes | | | `triggerAriaLabel` | `string` | — | yes | Доступное имя кнопки-триггера (icon-only кнопка иначе безымянна для скринридера). | ### NotificationCard | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `actions` | `Action[]` | — | no | Дополнительные действия у карточки | | `appearance` | `default \| error \| success \| warning` | `default` | no | Тип уведомления | | `className` | `string` | — | no | CSS-класс | | `data-test-id` | `string` | — | no | | | `date` | `string` | — | yes | Дата уведомления | | `description` | `ReactNode` | — | no | Описание уведомления — вторичный текст под заголовком | | `id` | `string` | — | yes | Идентификатор уведомления | | `label` | `string` | — | no | Лейбл перед заголовком | | `link` | `PickLinkProps<"a", "label" \| "truncateVariant" \| "insideText">` | — | no | Ссылка (рендерится как `` через `@cloud-ru/ds-link`) | | `onClick` | `MouseEventHandler` | — | no | Колбэк клика по карточке. Делает карточку кликабельной (`role='button'`, фокус с клавиатуры). Активация по карточке срабатывает только при фокусе на самой карточке — вложенные кнопки, ссылка и меню действий активируются независимо. | | `onVisible` | `((cardId: string) => void)` | — | no | Колбэк при попадании карточки в область видимости на 80% | | `primaryButton` | `NotificationButtonProps` | — | no | Кнопка главного действия у карточки (рендерится как Button view='tonal') | | `secondaryButton` | `NotificationButtonProps` | — | no | Кнопка второстепенного действия у карточки (рендерится как Button view='simple') | | `title` | `string` | — | yes | Заголовок уведомления | | `unread` | `boolean` | — | no | Управление состоянием прочитано/не прочитано | #### Related types - `Action` (interface) - `Appearance` = `default | error | success | warning` - `CounterProps` (interface) - `IconPosition` = `after | before` - `ItemContent` (interface) - `NotificationButtonProps` (interface) - `PickLinkProps` (alias) - `PolymorphicRef` (alias) ### NotificationCardSkeleton | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `className` | `string` | — | no | CSS-класс | | `data-test-id` | `string` | — | no | | ### NotificationCardStack | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `actions` | `Action[]` | — | no | Список действий в выпадающем меню | | `children` | `Iterable` | — | yes | Карточки в стопке, видна первая карточка, остальные схлопываются под нее. | | `data-test-id` | `string` | — | no | | | `defaultOpen` | `boolean` | — | no | Состояние открыт/закрыт по умолчанию | | `onOpenChanged` | `((open: boolean) => void)` | — | no | Колбек смены состояния открыт/закрыт | | `title` | `string` | — | yes | Заголовок стопки карточек | | `unread` | `boolean` | — | no | Состояние непрочитанных карточек | #### Related types - `Action` (interface) - `ItemContent` (interface) ### NotificationPanel | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `className` | `string` | — | no | CSS-класс для элемента с контентом | | `closeOnPopstate` | `boolean` | — | no | Закрывать дровер при перемещении по истории браузера | | `container` | `string \| HTMLElement` | — | no | Контейнер в котором будет рендерится Drawer. По-умолчанию - body | | `content` | `ReactElement` | — | yes | Контент панели (`NotificationPanelContent`), отображаемый внутри обёртки | | `data-test-id` | `string` | — | no | | | `onClose` | `() => void` | — | yes | Колбэк закрытия | | `open` | `boolean` | — | yes | Управление состоянием показан/не показан. | | `position` | `bottom \| left \| right \| top` | `right` | no | Расположение | | `rootClassName` | `string` | — | no | CSS-класс для корневого элемента | | `showBlackout` | `boolean` | `true` | no | Отображение темной подложки | | `width` | `string \| number` | `s` | no | Ширина (только при position: "left" \| "right") | #### Related types - `ButtonProps` (interface) - `NotificationPanelContentProps` (interface) - `NotificationPanelSettingsProps` (interface) - `Position` = `bottom | left | right | top` - `SegmentControlProps` (interface) - `TooltipProps` (interface) - `Width` = `auto | full` ### NotificationPanelBlank | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `className` | `string` | — | no | Дополнительный класс | | `content` | `ReactNode` | — | no | Подзаголовок | | `data-test-id` | `string` | — | no | | | `icon` | `IconPredefinedProps` | — | no | Иконка | | `title` | `string` | — | no | Заголовок | #### Related types - `Appearance` = `default | error | success | warning` - `IconPredefinedProps` (interface) - `Size` = `s | xs` ### NotificationPanelContent | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `chipToggle` | `{ label: string; checked: boolean; onChange(checked: boolean): void; }` | — | no | Переключатель для фильтрации | | `className` | `string` | — | no | | | `content` | `ReactNode` | — | no | Контент для отрисовки (e.g NotificationCard \| NotificationPanelContent.Blank) | | `data-test-id` | `string` | — | no | | | `loading` | `boolean` | — | no | Состояние загрузки | | `readAllButton` | `(Omit & { tooltip?: TooltipProps; })` | — | no | Кнопка в "шапке" панели | | `scrollContainerRef` | `RefObject` | — | no | Ссылка на контейнер, который скроллится | | `scrollEndRef` | `RefObject` | — | no | Ссылка на элемент, обозначающий самый конец прокручиваемого списка | | `segments` | `Omit` | — | no | Сегменты для фильтрации | | `settings` | `NotificationPanelSettingsProps` | — | no | Кнопка настроек и выпадающий список | | `skeletonsAmount` | `number` | `2` | no | Количество скелетонов карточек для отображения при загрузке | | `title` | `string` | — | yes | Заголовок панели | #### Related types - `Action` (interface) - `ButtonProps` (interface) - `ChildrenFunction` (alias) - `NotificationPanelSettingsProps` (interface) - `Placement` = `bottom | bottom-end | bottom-start | left | left-end | left-start | right | right-end | right-start | top | top-end | top-start` - `Segment` (interface) - `SegmentControlProps` (interface) - `Size` = `s | xs` - `TooltipProps` (interface) - `Trigger` = `click | clickAndFocusVisible | focus | focusVisible | hover | hoverAndFocus | hoverAndFocusVisible` - `Value` = `0% | 100% | 50%` - `Width` = `auto | full` ### NotificationPanelGroup | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `children` | `ReactNode` | — | yes | Содержимое группы | | `className` | `string` | — | no | CSS-класс | | `data-test-id` | `string` | — | no | | | `title` | `string` | — | yes | Заголовок группы | ### NotificationPanelSettings | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `actions` | `Action[]` | — | no | Дополнительные действия панели | | `button` | `Omit, "data-test-id" \| "view" \| "label" \| "size">` | — | no | Кнопка дополнительного действия панели | | `size` | `m \| s` | `m` | no | | #### Related types - `Action` (interface) - `Appearance` = `default | error | success | warning` - `ButtonProps` (interface) - `CounterProps` (interface) - `IconPosition` = `after | before` - `ItemContent` (interface) - `PolymorphicRef` (alias) - `Size` = `s | xs` - `View` = `elevated | filled | function | outline | simple | tonal` ### NotificationPanelSettingsDroplist | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `actions` | `Action[]` | — | yes | | | `button` | `ButtonProps` | — | yes | | ### StackTail | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `count` | `number` | — | yes | | | `open` | `boolean` | — | yes | | ## Примеры ### BasicCard ```tsx import { APPEARANCE, NotificationCard } from '@cloud-ru/ds-uikit-product-notification'; export function BasicCard() { return ( ); } ``` ### DeployFailureCard ```tsx import { APPEARANCE, NotificationCard } from '@cloud-ru/ds-uikit-product-notification'; import { useState } from 'react'; export function DeployFailureCard() { const [status, setStatus] = useState<'failed' | 'retrying' | 'dismissed'>('failed'); if (status === 'dismissed') { return null; } return ( { setStatus('retrying'); // Если карточку закрыли во время повтора, по таймеру не возвращаем её в `failed`. setTimeout(() => setStatus(prev => (prev === 'retrying' ? 'failed' : prev)), 1200); }, }} secondaryButton={{ label: 'Логи пода', onClick: () => window.open('/logs/api-gateway/2.18.3', '_blank'), }} actions={[ { content: { label: 'Отметить прочитанным' }, onClick: () => setStatus('dismissed') }, { content: { label: 'Заглушить на 1 час' }, onClick: () => setStatus('dismissed') }, { content: { label: 'Удалить' }, onClick: () => setStatus('dismissed') }, ]} /> ); } ``` ### IncidentCard ```tsx import { APPEARANCE, NotificationCard } from '@cloud-ru/ds-uikit-product-notification'; export function IncidentCard() { return ( ); } ``` ### PanelBasic ```tsx import { APPEARANCE, NotificationCard, NotificationPanelContent } from '@cloud-ru/ds-uikit-product-notification'; import { useState } from 'react'; export function PanelBasic() { const [readIds, setReadIds] = useState>(new Set()); const markRead = (id: string) => setReadIds(prev => (prev.has(id) ? prev : new Set(prev).add(id))); return ( setReadIds(new Set(['inc-4821', 'quota-var-data', 'deploy-9217', 'backup-1729'])), }} content={ <> } /> ); } ``` ### PanelError ```tsx import { CrossSVG } from '@cloud-ru/ds-icons/interface/system'; import { NotificationPanelContent } from '@cloud-ru/ds-uikit-product-notification'; export function PanelError() { return ( } /> ); } ``` ### PanelFull ```tsx import { APPEARANCE, NotificationCard, NotificationPanelContent } from '@cloud-ru/ds-uikit-product-notification'; import { useMemo, useState } from 'react'; type Filter = 'all' | 'unread' | 'mentions'; type CardId = 'inc-4821' | 'deploy-9217' | 'stack-quota' | 'mention-1' | 'backup-1729'; export function PanelFull() { const [filter, setFilter] = useState('all'); const [importantOnly, setImportantOnly] = useState(false); const [muted, setMuted] = useState(false); const [readIds, setReadIds] = useState>(new Set()); const markRead = (id: string) => setReadIds(prev => (prev.has(id as CardId) ? prev : new Set(prev).add(id as CardId))); const isUnread = (id: CardId) => !readIds.has(id); // Какие карточки попадают под текущий фильтр. `important` — критичные алерты (error/warning). const visible = useMemo(() => { const matches = (id: CardId, important: boolean, mention: boolean) => { if (filter === 'unread' && !isUnread(id)) return false; if (filter === 'mentions' && !mention) return false; if (importantOnly && !important) return false; return true; }; return { inc: matches('inc-4821', true, false), deploy: matches('deploy-9217', true, false), quota: matches('stack-quota', true, false), mention: matches('mention-1', false, true), backup: matches('backup-1729', false, false), }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [filter, importantOnly, readIds]); const attentionVisible = visible.inc || visible.deploy; if (muted) { return ( setMuted(false) }, actions: [{ content: { label: 'Снять заглушение' }, onClick: () => setMuted(false) }], }} content={} /> ); } return ( setFilter(value as Filter), }} chipToggle={{ label: 'Только важные', checked: importantOnly, onChange: setImportantOnly, }} settings={{ button: {}, actions: [ { content: { label: 'Только непрочитанные' }, onClick: () => setFilter('unread') }, { content: { label: 'Показать все' }, onClick: () => setFilter('all') }, { content: { label: 'Заглушить на 1 час' }, onClick: () => setMuted(true) }, ], }} readAllButton={{ label: 'Прочитать всё', onClick: () => setReadIds(new Set(['inc-4821', 'deploy-9217', 'stack-quota', 'mention-1'])), }} content={ <> {attentionVisible && ( {visible.inc && ( )} {visible.deploy && ( markRead('deploy-9217') }} secondaryButton={{ label: 'Логи', onClick: () => window.open('/logs/api-gateway/2.18.3', '_blank'), }} unread={isUnread('deploy-9217')} onVisible={markRead} /> )} )} {visible.quota && ( <> markRead('stack-quota') }, { content: { label: 'Заглушить группу' }, onClick: () => setMuted(true) }, ]} onOpenChanged={open => open && markRead('stack-quota')} > )} {visible.mention && ( )} {visible.backup && ( <> )} } /> ); } ``` ### PanelLoading ```tsx import { NotificationPanelContent } from '@cloud-ru/ds-uikit-product-notification'; export function PanelLoading() { return ( {}, }} readAllButton={{ label: 'Прочитать всё', onClick: () => {} }} /> ); } ``` ### PanelScenario ```tsx import { Button } from '@cloud-ru/ds-button'; import { APPEARANCE, NotificationCard, NotificationPanel, NotificationPanelContent, } from '@cloud-ru/ds-uikit-product-notification'; import { useMemo, useState } from 'react'; export function PanelScenario() { const [open, setOpen] = useState(false); const [readIds, setReadIds] = useState>(new Set()); const unreadCount = useMemo(() => ['inc-4821', 'deploy-9217'].filter(id => !readIds.has(id)).length, [readIds]); const markRead = (id: string) => setReadIds(prev => (prev.has(id) ? prev : new Set(prev).add(id))); return (
); } ``` ### UnreadCard ```tsx import { APPEARANCE, NotificationCard } from '@cloud-ru/ds-uikit-product-notification'; import { useState } from 'react'; export function UnreadCard() { const [readIds, setReadIds] = useState>(new Set()); const markRead = (id: string) => setReadIds(prev => (prev.has(id) ? prev : new Set(prev).add(id))); return (
); } ```