diff --git a/docs/components-api/toast.md b/docs/components-api/toast.md index 193f7bcb..d8710da8 100644 --- a/docs/components-api/toast.md +++ b/docs/components-api/toast.md @@ -2,8 +2,25 @@ # ExtraToast +> ✅ **Реализован**: `ExtraToastComponent` (`@cdek-it/angular-ui-kit`) соответствует спецификации. + Уведомление, отображаемое поверх интерфейса (overlay-вариант `ExtraMessage`; в потоке документа используется `ExtraMessage`). +Показ уведомлений устроен в два уровня: сам компонент `` — это контейнер, монтируемый +один раз, а показ каждого отдельного уведомления выполняется отдельным императивным вызовом. Поэтому +и свойства ниже разделены на два независимых набора. + +# Свойства компонента (``) + +| Свойство | Описание | Типизация | +| -------- | ---------------------------------------------------------------- | --------- | +| `position` | позиция группы уведомлений на экране | `top-right \| top-left \| top-center \| bottom-right \| bottom-left \| bottom-center \| center` | +| `key` | ключ контейнера; показывает только уведомления с тем же `key` (несколько независимых очередей) | `string` | +| `life` | время автозакрытия по умолчанию (мс) для уведомлений этого контейнера | `number` | +| `appendTo` | куда монтируется контейнер; по умолчанию `body`, чтобы уведомления позиционировались относительно окна браузера, а не предка с `transform`/`filter`/`contain` | `'body' \| 'self' \| HTMLElement \| ElementRef` | + +# Свойства уведомления (передаются при вызове показа) + | Свойство | Описание | Типизация | | ------------ | ------------------------------------------------------------ | -------------------------------------- | | `severity` | тип сообщения | `info \| success \| warning \| danger` | @@ -15,6 +32,8 @@ # Слоты (content projection) +Относятся к уведомлению — так же передаются при вызове показа, а не проецируются в ``. + | Слот | Описание | | --------- | --------------------- | | `content` | контент после caption | @@ -22,6 +41,8 @@ # События +Относится к уведомлению — передаётся полем при вызове показа, а не эмитится самим ``. + | Событие | Описание | Типизация | | --------- | ------------------------------------ | ------------ | | `onClose` | срабатывает при закрытии уведомления | `() => void` | diff --git a/src/lib/components/toast/provide-toast.ts b/src/lib/components/toast/provide-toast.ts index 639e9f51..3034be20 100644 --- a/src/lib/components/toast/provide-toast.ts +++ b/src/lib/components/toast/provide-toast.ts @@ -14,4 +14,3 @@ import { MessageService } from 'primeng/api'; export function provideExtraToast(): EnvironmentProviders { return makeEnvironmentProviders([MessageService]); } - diff --git a/src/lib/components/toast/public_api.ts b/src/lib/components/toast/public_api.ts index b4c66422..3f5e1fdf 100644 --- a/src/lib/components/toast/public_api.ts +++ b/src/lib/components/toast/public_api.ts @@ -1,6 +1,3 @@ export * from './toast.component'; export * from './toast.service'; export * from './provide-toast'; - - - diff --git a/src/lib/components/toast/toast.component.ts b/src/lib/components/toast/toast.component.ts index a7a9e747..da669839 100644 --- a/src/lib/components/toast/toast.component.ts +++ b/src/lib/components/toast/toast.component.ts @@ -1,8 +1,15 @@ -import { Component, Input } from '@angular/core'; -import { Toast } from 'primeng/toast'; +import { AfterViewInit, Component, ElementRef, inject, Input } from '@angular/core'; +import { DOCUMENT, NgTemplateOutlet } from '@angular/common'; +import { Toast, ToastCloseEvent } from 'primeng/toast'; +import { ButtonDirective } from 'primeng/button'; import { SharedModule } from 'primeng/api'; +import { PrimeNG } from 'primeng/config'; -export type ExtraToastSeverity = 'success' | 'info' | 'warn' | 'error' | 'secondary' | 'contrast'; +export type ExtraToastElementLike = HTMLElement | ElementRef; +/** Куда монтируется контейнер уведомлений; `'self'` оставляет его на месте в шаблоне. */ +export type ExtraToastAppendTo = 'body' | 'self' | ExtraToastElementLike; + +export type ExtraToastSeverity = 'info' | 'success' | 'warning' | 'danger'; export type ExtraToastPosition = | 'top-right' | 'top-left' @@ -12,6 +19,16 @@ export type ExtraToastPosition = | 'bottom-center' | 'center'; +/** Внутренний словарь PrimeNG-совместимых значений severity — публично не используется. */ +type PrimeToastSeverity = 'success' | 'info' | 'warn' | 'error'; + +/** Маппинг собственного severity на PrimeNG; используется сервисом при отправке сообщения. */ +export function toPrimeToastSeverity(severity: ExtraToastSeverity): PrimeToastSeverity { + if (severity === 'warning') return 'warn'; + if (severity === 'danger') return 'error'; + return severity; +} + const SEVERITY_ICONS: Record = { info: 'ti ti-info-circle', success: 'ti ti-circle-check', @@ -22,29 +39,86 @@ const SEVERITY_ICONS: Record = { @Component({ selector: 'extra-toast', standalone: true, - imports: [Toast, SharedModule], + imports: [Toast, SharedModule, NgTemplateOutlet, ButtonDirective], template: ` - - -
- -
- {{ message.summary }} - @if (message.detail) { -
{{ message.detail }}
+ + + +
+
+ +
+ @if (message.summary) { + {{ message.summary }} + } + @if (message.detail) { +
{{ message.detail }}
+ } +
+ @if (message.closable !== false) { + + } + @if (message.data?.content) { +
+ +
+ } + @if (message.data?.footer) { + }
` }) -export class ExtraToastComponent { +export class ExtraToastComponent implements AfterViewInit { @Input() position: ExtraToastPosition = 'top-right'; @Input() key: string | undefined = undefined; @Input() life = 5000; - @Input() pt: Record | undefined = undefined; + /** + * Куда монтируется контейнер уведомлений. По умолчанию `'body'`: уведомления позиционируются + * относительно окна браузера, а не ближайшего предка с `transform`/`filter`/`contain` + * (такой предок становится content block для `position: fixed` и «запирает» уведомления внутри себя). + */ + @Input() appendTo: ExtraToastAppendTo = 'body'; + + private readonly config = inject(PrimeNG); + private readonly document = inject(DOCUMENT); + private readonly elementRef = inject>(ElementRef); + + get closeAriaLabel(): string | undefined { + return this.config.translation?.aria?.close; + } + + ngAfterViewInit(): void { + const target = this.resolveAppendTarget(); + if (target && target !== this.elementRef.nativeElement.parentNode) { + target.appendChild(this.elementRef.nativeElement); + } + } + + private resolveAppendTarget(): HTMLElement | undefined { + if (this.appendTo === 'self') return undefined; + if (this.appendTo === 'body') return this.document.body ?? undefined; + return this.appendTo instanceof ElementRef ? this.appendTo.nativeElement : this.appendTo; + } resolveIcon(message: { severity?: string; icon?: string }): string { - return message.icon ?? SEVERITY_ICONS[message.severity ?? 'info'] ?? 'ti ti-info-circle'; + return message.icon ?? SEVERITY_ICONS[message.severity ?? 'info'] ?? SEVERITY_ICONS['info']; + } + + /** Диспетчеризует пользовательский onClose-колбэк конкретного сообщения (клик по крестику или таймер). */ + onMessageClose(event: ToastCloseEvent): void { + (event.message?.data as { onClose?: () => void } | undefined)?.onClose?.(); } } diff --git a/src/lib/components/toast/toast.figma.md b/src/lib/components/toast/toast.figma.md index 57763c23..2d48ee8a 100644 --- a/src/lib/components/toast/toast.figma.md +++ b/src/lib/components/toast/toast.figma.md @@ -10,41 +10,56 @@ figma: componentKey: 'e62d7e276583e57fc42b31096f545e591ec18979' name: '' status: stable -updated: '2026-06-23' +updated: '2026-09-06' --- ## Overview -`ExtraToast` — контейнер всплывающих самозакрывающихся уведомлений поверх интерфейса. Сообщения не вставляются в шаблон напрямую: компонент монтируется один раз (обычно в корне приложения), а показ выполняется императивно через `ExtraToastService`. Оборачивает PrimeNG `p-toast` и подбирает иконку автоматически по `severity`. +`ExtraToast` — всплывающее самозакрывающееся уведомление поверх интерфейса, overlay-вариант `ExtraMessage` +(инлайн-сообщение в потоке страницы — см. [Message](../message/message.figma.md)). Сообщения не +вставляются в шаблон напрямую: контейнер `` монтируется один раз (обычно в корне +приложения), а показ каждого уведомления выполняется императивно через `ExtraToastService.add()`. +Свойства спецификации (`severity`/`timer`/`message`/`caption`/`icon`/`show-close`), слоты +(`content`/`footer`) и событие `onClose` относятся к КАЖДОМУ сообщению и передаются полем в объекте +`ExtraToastService.add({...})`, а не как `@Input()`/`@Output()` самого ``. Для работы требуется зарегистрировать провайдеры через `provideExtraToast()` в `ApplicationConfig.providers`. -Компонент соответствует Figma-узлу `` (nodeId `15953:4478`) — это всплывающее самозакрывающееся уведомление, в отличие от инлайн-компонента `` (nodeId `15963:3163`), встроенного в поток страницы. В ходе рефакторинга toast-сценарий был отделён от inline `` в самостоятельный Figma-узел ``. +Компонент соответствует Figma-узлу `` (nodeId `15953:4478`), задающему два свойства: +`severity` (`info | success | warning | danger`) и `timer` (`false | true`). ## Props mapping -Входные свойства задаются на самом контейнере `` и управляют отображением всех сообщений. +Контейнер `` принимает свойства, управляющие отображением ВСЕХ его сообщений +(вне спецификации — форм-обвязка/инфраструктура, аналог `disabled`/`invalid` у форм-контролов): | Свойство | Тип | По умолчанию | Описание | |----------|-----|--------------|---------| | `position` | `'top-right' \| 'top-left' \| 'top-center' \| 'bottom-right' \| 'bottom-left' \| 'bottom-center' \| 'center'` | `'top-right'` | Позиция группы уведомлений на экране | | `key` | `string \| undefined` | `undefined` | Ключ контейнера; показывают только сообщения с тем же `key` (несколько независимых очередей) | -| `life` | `number` | `5000` | Время автозакрытия в миллисекундах | -| `pt` | `Record \| undefined` | `undefined` | PrimeNG PassThrough для тонкой настройки внутренних элементов | - -Само сообщение передаётся в `ExtraToastService.add()` объектом `ExtraToastMessage`: - -| Поле | Тип | Описание | -|------|-----|---------| -| `severity` | `'success' \| 'info' \| 'warn' \| 'error' \| 'secondary' \| 'contrast'` | Семантический акцент; определяет цвет и иконку | -| `summary` | `string` | Заголовок уведомления | -| `detail` | `string` | Подробный текст под заголовком | -| `life` | `number` | Время автозакрытия в миллисекундах (переопределяет `life` контейнера) | -| `icon` | `string` | CSS-класс иконки; по умолчанию подбирается по `severity` — доступные иконки см. [icons.md](../../figma-code-connect/icons.md) | -| `closable` | `boolean` | Показывать кнопку закрытия | -| `key` | `string` | Ключ целевого контейнера `` | - -Контейнер монтируется в шаблоне без сообщений, а показ выполняется через сервис. +| `life` | `number` | `5000` | Дефолтное время автозакрытия (мс) для всех сообщений этого контейнера; переопределяется полем `life` конкретного сообщения | +| `appendTo` | `'body' \| 'self' \| HTMLElement \| ElementRef` | `'body'` | Куда монтируется контейнер. По умолчанию переносится в `body`, чтобы уведомления позиционировались относительно окна браузера: любой предок с `transform`/`filter`/`contain` становится content block для `position: fixed` и «запирает» уведомления внутри себя (так происходит, например, в Storybook Docs и в контейнерах с CSS-анимациями). `'self'` оставляет контейнер на месте в шаблоне | + +Само сообщение передаётся в `ExtraToastService.add()` объектом `ExtraToastMessage` — это и есть +публичный API спецификации: + +| Поле | Тип | По умолчанию | Описание | +|------|-----|--------------|---------| +| `severity` | `'info' \| 'success' \| 'warning' \| 'danger'` | `'info'` | Тип сообщения — соответствует спеке `severity` | +| `timer` | `boolean` | `true` | Таймер автоскрытия — соответствует спеке `timer`. `false` делает уведомление несгораемым (sticky) | +| `message` | `string` | — | Заголовок сообщения — соответствует спеке `message` | +| `caption` | `string` | — | Подробности сообщения — соответствует спеке `caption` | +| `icon` | `string` | — | Класс иконки tabler icon вместо стандартной для `severity` — соответствует спеке `icon` | +| `showClose` | `boolean` | `false` | Кнопка закрытия — соответствует спеке `show-close` | +| `content` | `TemplateRef` | — | Слот `content` — контент после `caption` | +| `footer` | `TemplateRef` | — | Слот `footer` — контент футера | +| `onClose` | `() => void` | — | Событие `onClose` — срабатывает при закрытии этого сообщения (крестиком или по таймеру) | +| `key` | `string \| undefined` | `undefined` | Ключ целевого контейнера ``; вне спеки, но необходим для нескольких независимых очередей | +| `life` | `number \| undefined` | `undefined` | Переопределяет длительность таймера (мс) для этого сообщения; вне спеки, дополнительная точная настройка поверх булева `timer` | + +`content`/`footer` — обычные `TemplateRef`, получаемые в компоненте-инициаторе через `@ViewChild` +(см. пример ниже); контент-проекция через `` здесь невозможна, так как сообщение не +является отдельным компонентом в шаблоне, а данными, переданными в сервис. ```html @@ -63,83 +78,91 @@ export class AppComponent { private readonly toast = inject(ExtraToastService); show(): void { - this.toast.add({ severity: 'success', summary: 'Готово', detail: 'Операция выполнена' }); + this.toast.add({ severity: 'success', message: 'Готово', caption: 'Операция выполнена' }); } } ``` ## Variants -Варианты различаются по `severity` сообщения. Контейнер один и тот же; меняется объект, переданный в `ExtraToastService.add()`. Figma-узел `` задаёт два свойства: `severity` (`info | success | warning | danger`) и `timer` (`false | true`). +Варианты различаются по `severity` сообщения. Контейнер один и тот же; меняется объект, переданный в +`ExtraToastService.add()`. Figma-узел `` задаёт два свойства: `severity` +(`info | success | warning | danger`) и `timer` (`false | true`). ### Info (информационное) Figma: `severity=info` -```html - -``` - ```ts -this.toast.add({ severity: 'info', summary: 'Информация', detail: 'Дополнительный текст' }); +this.toast.add({ severity: 'info', message: 'Информация', caption: 'Дополнительный текст' }); ``` ### Success (успех) Figma: `severity=success` -```html - -``` - ```ts -this.toast.add({ severity: 'success', summary: 'Готово', detail: 'Операция выполнена' }); +this.toast.add({ severity: 'success', message: 'Готово', caption: 'Операция выполнена' }); ``` -### Warn (предупреждение) +### Warning (предупреждение) Figma: `severity=warning` -```html - -``` - ```ts -this.toast.add({ severity: 'warn', summary: 'Внимание', detail: 'Проверьте данные' }); +this.toast.add({ severity: 'warning', message: 'Внимание', caption: 'Проверьте данные' }); ``` -### Error / danger (ошибка) +### Danger (ошибка) Figma: `severity=danger` -```html - +```ts +this.toast.add({ severity: 'danger', message: 'Ошибка', caption: 'Не удалось сохранить' }); ``` +### С кнопкой закрытия (show-close) + ```ts -this.toast.add({ severity: 'error', summary: 'Ошибка', detail: 'Не удалось сохранить' }); +this.toast.add({ severity: 'info', message: 'Сообщение', showClose: true }); ``` -### С кнопкой закрытия (closable) +### Несгораемое (timer=false) -```html - -``` +Figma: `timer=false` ```ts -this.toast.add({ severity: 'info', summary: 'Сообщение', closable: true }); +this.toast.add({ severity: 'success', message: 'Сохранено', timer: false }); ``` -### С увеличенным временем жизни (life) +### С контентом и футером (content / footer) -Figma: `timer=true` +```ts +import { Component, TemplateRef, ViewChild, inject } from '@angular/core'; +import { ExtraToastComponent, ExtraToastService } from '@cdek-it/angular-ui-kit'; -```html - -``` +@Component({ + imports: [ExtraToastComponent], + template: ` + + Дополнительный контент + Футер + `, +}) +export class ExampleComponent { + @ViewChild('extraContent') contentTpl!: TemplateRef; + @ViewChild('extraFooter') footerTpl!: TemplateRef; + private readonly toast = inject(ExtraToastService); -```ts -this.toast.add({ severity: 'success', summary: 'Сохранено', life: 10000 }); + show(): void { + this.toast.add({ + severity: 'info', + message: 'Сообщение', + content: this.contentTpl, + footer: this.footerTpl, + }); + } +} ``` ### Позиционирование (position) @@ -149,7 +172,7 @@ this.toast.add({ severity: 'success', summary: 'Сохранено', life: 10000 ``` ```ts -this.toast.add({ severity: 'info', summary: 'Уведомление снизу' }); +this.toast.add({ severity: 'info', message: 'Уведомление снизу' }); ``` ### Несколько очередей (key) @@ -159,16 +182,19 @@ this.toast.add({ severity: 'info', summary: 'Уведомление снизу' ``` ```ts -this.toast.add({ key: 'orders', severity: 'success', summary: 'Заказ создан' }); +this.toast.add({ key: 'orders', severity: 'success', message: 'Заказ создан' }); ``` ## Slots -Контент уведомления формируется из полей `summary` и `detail` объекта сообщения и не проектируется через ``. Структурные слоты не используются. +| Слот | Описание | +|------|----------| +| `content` | Контент после `caption` (Figma ``). Занимает всю ширину уведомления — под строкой «иконка + заголовок + крестик», а не в колонке текста. Передаётся `TemplateRef` в поле `content` объекта `ExtraToastMessage` | +| `footer` | Контент футера (Figma `footer` со ``/``). Занимает всю ширину, содержимое прижато вправо. Передаётся `TemplateRef` в поле `footer` объекта `ExtraToastMessage` | ## Related -- [Message](../message/message.figma.md) — инлайн-вариант сообщения со статусом; покрывает Figma `` (`toast=false`) +- [Message](../message/message.figma.md) — инлайн-вариант того же уведомления, встроенный в поток страницы - [Button](../button/button.figma.md) — кнопка закрытия использует общий контракт - [Иконки](../../figma-code-connect/icons.md) — доступные иконки `severity` и `icon` - [Conventions](../../figma-code-connect/conventions.md) — соглашения маппинга Figma → Angular @@ -178,7 +204,7 @@ this.toast.add({ key: 'orders', severity: 'success', summary: 'Заказ соз **Do:** - Монтируйте один `` в корне приложения и показывайте сообщения через `ExtraToastService.add()` - Регистрируйте `provideExtraToast()` в `ApplicationConfig.providers` один раз -- Подбирайте `severity` по смыслу: `error` для ошибок, `warn` для предупреждений, `success` для подтверждений +- Подбирайте `severity` по смыслу: `danger` для ошибок, `warning` для предупреждений, `success` для подтверждений - Используйте `key` для независимых очередей уведомлений (например системные и контекстные) - Полагайтесь на автоподбор иконки по `severity`; переопределяйте `icon` только при необходимости — используйте справочник [icons.md](../../figma-code-connect/icons.md) @@ -186,4 +212,4 @@ this.toast.add({ key: 'orders', severity: 'success', summary: 'Заказ соз - Не используйте toast для устойчивого статуса рядом с формой или секцией — для инлайн-фидбэка предназначен [Message](../message/message.figma.md) - Не монтируйте несколько `` с одинаковым `position` и `key` — сообщения продублируются - Не инлайньте CSS-классы иконок вручную в обход справочника [icons.md](../../figma-code-connect/icons.md) -- Не задавайте слишком короткий `life` для текста, который нужно успеть прочитать +- Не делайте `timer: false` (несгораемым) без `showClose: true` — пользователю нечем будет закрыть уведомление diff --git a/src/lib/components/toast/toast.service.ts b/src/lib/components/toast/toast.service.ts index a8543613..4a6ac55e 100644 --- a/src/lib/components/toast/toast.service.ts +++ b/src/lib/components/toast/toast.service.ts @@ -1,16 +1,29 @@ -import { Injectable } from '@angular/core'; +import { Injectable, TemplateRef } from '@angular/core'; import { MessageService } from 'primeng/api'; -import { ExtraToastSeverity } from './toast.component'; +import { ExtraToastSeverity, toPrimeToastSeverity } from './toast.component'; export interface ExtraToastMessage { + /** Ключ целевого контейнера `` — для нескольких независимых очередей. */ key?: string; severity?: ExtraToastSeverity; - summary?: string; - detail?: string; - life?: number; + /** Заголовок сообщения. */ + message?: string; + /** Подробности сообщения. */ + caption?: string; + /** Класс иконки tabler icon; по умолчанию подбирается по `severity`. */ icon?: string; - closable?: boolean; - data?: unknown; + /** Показывать кнопку закрытия. */ + showClose?: boolean; + /** Таймер автоскрытия. По умолчанию `true` — тост скрывается сам; `false` делает его несгораемым (sticky). */ + timer?: boolean; + /** Переопределяет длительность таймера (мс) — учитывается только при `timer !== false`. */ + life?: number; + /** Срабатывает при закрытии этого сообщения (крестиком или по таймеру). */ + onClose?: () => void; + /** Слот `content` — контент после caption. */ + content?: TemplateRef; + /** Слот `footer` — контент футера. */ + footer?: TemplateRef; } @Injectable({ providedIn: 'root' }) @@ -18,7 +31,23 @@ export class ExtraToastService { constructor(private readonly messageService: MessageService) {} add(message: ExtraToastMessage): void { - this.messageService.add(message); + const timer = message.timer ?? true; + + this.messageService.add({ + key: message.key, + severity: toPrimeToastSeverity(message.severity ?? 'info'), + summary: message.message, + detail: message.caption, + icon: message.icon, + closable: message.showClose ?? false, + sticky: !timer, + life: message.life, + data: { + onClose: message.onClose, + content: message.content, + footer: message.footer + } + }); } clear(key?: string): void { diff --git a/src/lib/providers/prime-preset/tokens/components/toast.ts b/src/lib/providers/prime-preset/tokens/components/toast.ts index adc64305..8fc620ad 100644 --- a/src/lib/providers/prime-preset/tokens/components/toast.ts +++ b/src/lib/providers/prime-preset/tokens/components/toast.ts @@ -9,9 +9,11 @@ export const toastCss = ({ dt }: { dt: (token: string) => string }): string => ` position: relative; } - /* border-radius для контента toast-сообщения */ + /* Контент toast-сообщения: перенос строк нужен слотам content/footer на всю ширину */ .p-toast .p-toast-message .p-toast-message-content { border-radius: ${dt('toast.root.borderRadius')}; + flex-wrap: wrap; + align-items: flex-start; } /* Текстовый блок toast */ @@ -22,6 +24,20 @@ export const toastCss = ({ dt }: { dt: (token: string) => string }): string => ` gap: ${dt('toast.text.gap')}; } + /* Слот content: строка под заголовком, на всю ширину сообщения */ + .p-toast .p-toast-body { + flex-basis: 100%; + } + + /* Футер toast: строка под основным контентом, действия прижаты вправо */ + .p-toast .p-toast-footer { + flex-basis: 100%; + display: flex; + align-items: center; + justify-content: flex-end; + gap: ${dt('dimension.space.200')}; + } + /* Заголовок toast */ .p-toast-summary { font-family: ${dt('fonts.fontFamily.base')}; @@ -36,11 +52,19 @@ export const toastCss = ({ dt }: { dt: (token: string) => string }): string => ` /* Кнопка закрытия toast-сообщения */ .p-toast-message .p-toast-message-content .p-toast-close-button { + width: ${dt('toast.closeButton.width')}; + height: ${dt('toast.closeButton.height')}; + border-radius: ${dt('toast.closeButton.borderRadius')}; margin: 0; padding: 0; right: 0; } + /* Иконка в кнопке закрытия toast-сообщения */ + .p-toast-message .p-toast-message-content .p-toast-close-button .p-button-icon { + font-size: ${dt('toast.closeIcon.size')}; + } + /* Общие стили border для кнопки закрытия всех типов toast */ .p-toast-message-info .p-toast-close-button, .p-toast-message-success .p-toast-close-button, diff --git a/src/lib/providers/prime-preset/tokens/tokens.json b/src/lib/providers/prime-preset/tokens/tokens.json index 58a53275..28d74cd0 100644 --- a/src/lib/providers/prime-preset/tokens/tokens.json +++ b/src/lib/providers/prime-preset/tokens/tokens.json @@ -5144,8 +5144,8 @@ }, "hoverBackground": "{color.bg.status.danger.weak.active}" }, - "color": "{color.fg.inverse.default}", - "detailColor": "{color.fg.inverse.default}", + "color": "{color.fg.default}", + "detailColor": "{color.fg.default}", "shadow": "{effects.elevation.400}" }, "info": { @@ -5158,8 +5158,8 @@ }, "hoverBackground": "{color.bg.status.info.weak.active}" }, - "color": "{color.fg.inverse.default}", - "detailColor": "{color.fg.inverse.default}", + "color": "{color.fg.default}", + "detailColor": "{color.fg.default}", "shadow": "{effects.elevation.400}" }, "root": { @@ -5189,8 +5189,8 @@ }, "hoverBackground": "{color.bg.status.success.weak.active}" }, - "color": "{color.fg.inverse.default}", - "detailColor": "{color.fg.inverse.default}", + "color": "{color.fg.default}", + "detailColor": "{color.fg.default}", "shadow": "{effects.elevation.400}" }, "warn": { @@ -5203,8 +5203,8 @@ }, "hoverBackground": "{color.bg.status.warning.weak.active}" }, - "color": "{color.fg.inverse.default}", - "detailColor": "{color.fg.inverse.default}", + "color": "{color.fg.default}", + "detailColor": "{color.fg.default}", "shadow": "{effects.elevation.400}" } } diff --git a/src/stories/components/toast/examples/toast-position.component.ts b/src/stories/components/toast/examples/toast-position.component.ts index cf7e082b..9fe575f3 100644 --- a/src/stories/components/toast/examples/toast-position.component.ts +++ b/src/stories/components/toast/examples/toast-position.component.ts @@ -46,8 +46,8 @@ export class ToastPositionComponent { this.toastService.add({ key, severity: 'info', - summary: 'Сообщение', - detail: 'Позиция: ' + position, + message: 'Сообщение', + caption: 'Позиция: ' + position, life: 3000, icon: 'ti ti-info-circle' }); @@ -104,8 +104,8 @@ export class ExampleComponent { this.toastService.add({ key, severity: 'info', - summary: 'Сообщение', - detail: 'Позиция: ' + position, + message: 'Сообщение', + caption: 'Позиция: ' + position, life: 3000, icon: 'ti ti-info-circle', }); diff --git a/src/stories/components/toast/examples/toast-severities.component.ts b/src/stories/components/toast/examples/toast-severities.component.ts index 6f477370..6e4b0c3c 100644 --- a/src/stories/components/toast/examples/toast-severities.component.ts +++ b/src/stories/components/toast/examples/toast-severities.component.ts @@ -5,18 +5,18 @@ import { ExtraToastComponent } from '../../../../lib/components/toast/toast.comp import { ExtraToastService } from '../../../../lib/components/toast/toast.service'; const SEVERITIES = [ - { type: 'info', icon: 'ti ti-info-circle', label: 'Информация' }, - { type: 'success', icon: 'ti ti-circle-check', label: 'Успех' }, - { type: 'warn', icon: 'ti ti-alert-triangle', label: 'Предупреждение' }, - { type: 'error', icon: 'ti ti-alert-circle', label: 'Ошибка' } + { severity: 'info', primeClass: 'info', icon: 'ti ti-info-circle', label: 'Информация' }, + { severity: 'success', primeClass: 'success', icon: 'ti ti-circle-check', label: 'Успех' }, + { severity: 'warning', primeClass: 'warn', icon: 'ti ti-alert-triangle', label: 'Предупреждение' }, + { severity: 'danger', primeClass: 'error', icon: 'ti ti-alert-circle', label: 'Ошибка' } ] as const; const template = `
- @for (s of severities; track s.type) { -
+ @for (s of severities; track s.severity) { +
@@ -30,12 +30,12 @@ const template = `
- @for (s of severities; track s.type) { + @for (s of severities; track s.severity) { }
@@ -54,13 +54,12 @@ export class ToastSeveritiesComponent { constructor(private readonly toastService: ExtraToastService) {} - show(severity: string, icon: string): void { + show(severity: 'info' | 'success' | 'warning' | 'danger', icon: string): void { this.toastService.add({ key: 'severities', - severity: severity as any, - summary: 'Сообщение', - detail: 'Подпись', - life: 5000, + severity, + message: 'Сообщение', + caption: 'Подпись', icon }); } @@ -78,12 +77,7 @@ export const Severities: StoryObj = { code: ` import { ExtraButtonComponent, ExtraToastComponent, ExtraToastService } from '@cdek-it/angular-ui-kit'; -const SEVERITIES = [ - { type: 'info', icon: 'ti ti-info-circle', label: 'Информация' }, - { type: 'success', icon: 'ti ti-circle-check', label: 'Успех' }, - { type: 'warn', icon: 'ti ti-alert-triangle', label: 'Предупреждение' }, - { type: 'error', icon: 'ti ti-alert-circle', label: 'Ошибка' }, -] as const; +const SEVERITIES = ['info', 'success', 'warning', 'danger'] as const; @Component({ selector: 'app-example', @@ -93,12 +87,11 @@ const SEVERITIES = [
- @for (s of severities; track s.type) { + @for (severity of severities; track severity) { }
@@ -109,14 +102,12 @@ export class ExampleComponent { constructor(private toastService: ExtraToastService) {} - show(severity: string, icon: string): void { + show(severity: 'info' | 'success' | 'warning' | 'danger'): void { this.toastService.add({ key: 'severities', - severity: severity as any, - summary: 'Сообщение', - detail: 'Подпись', - life: 5000, - icon, + severity, + message: 'Сообщение', + caption: 'Подпись', }); } } diff --git a/src/stories/components/toast/examples/toast-slots.component.ts b/src/stories/components/toast/examples/toast-slots.component.ts new file mode 100644 index 00000000..04b4ec2c --- /dev/null +++ b/src/stories/components/toast/examples/toast-slots.component.ts @@ -0,0 +1,140 @@ +import { Component, TemplateRef, ViewChild } from '@angular/core'; +import { StoryObj } from '@storybook/angular'; +import { ExtraButtonComponent } from '../../../../lib/components/button/button.component'; +import { ExtraToastComponent } from '../../../../lib/components/toast/toast.component'; +import { ExtraToastService } from '../../../../lib/components/toast/toast.service'; + +const template = ` + + + +
Контент после caption (слот content)
+
+ + +
+ + +
+
+ +
+ + + +
+`; + +@Component({ + selector: 'app-toast-slots', + standalone: true, + imports: [ExtraToastComponent, ExtraButtonComponent], + template +}) +export class ToastSlotsComponent { + @ViewChild('contentOnly') contentTpl!: TemplateRef; + @ViewChild('footerOnly') footerTpl!: TemplateRef; + + constructor(private readonly toastService: ExtraToastService) {} + + showContentOnly(): void { + this.toastService.add({ + key: 'slots', + severity: 'info', + message: 'Сообщение', + caption: 'caption', + timer: false, + showClose: true, + content: this.contentTpl + }); + } + + showFooterOnly(): void { + this.toastService.add({ + key: 'slots', + severity: 'danger', + message: 'Сообщение', + caption: 'caption', + timer: false, + showClose: true, + footer: this.footerTpl + }); + } + + showBoth(): void { + this.toastService.add({ + key: 'slots', + severity: 'success', + message: 'Сообщение', + caption: 'caption', + timer: false, + showClose: true, + content: this.contentTpl, + footer: this.footerTpl + }); + } +} + +export const Slots: StoryObj = { + render: () => ({ + template: `` + }), + parameters: { + docs: { + description: { + story: + 'Слот `content` — контент после `caption` (например, произвольный текст). Слот `footer` — контент футера (например, кнопки действий, прижатые вправо). Оба занимают всю ширину уведомления и передаются как `TemplateRef` в `ExtraToastService.add()`.' + }, + source: { + language: 'ts', + code: ` +import { Component, TemplateRef, ViewChild } from '@angular/core'; +import { ExtraButtonComponent, ExtraToastComponent, ExtraToastService } from '@cdek-it/angular-ui-kit'; + +@Component({ + selector: 'app-example', + standalone: true, + imports: [ExtraToastComponent, ExtraButtonComponent], + template: \` + + + + +
Контент после caption
+
+ + + +
+ + +
+
+ + + \`, +}) +export class ExampleComponent { + @ViewChild('contentOnly') contentTpl!: TemplateRef; + @ViewChild('footerOnly') footerTpl!: TemplateRef; + + constructor(private toastService: ExtraToastService) {} + + show(): void { + this.toastService.add({ + key: 'slots', + severity: 'success', + message: 'Сообщение', + caption: 'caption', + timer: false, + showClose: true, + content: this.contentTpl, + footer: this.footerTpl, + }); + } +} + ` + } + } + } +}; diff --git a/src/stories/components/toast/examples/toast-width.component.ts b/src/stories/components/toast/examples/toast-width.component.ts index fe3ff21d..37395956 100644 --- a/src/stories/components/toast/examples/toast-width.component.ts +++ b/src/stories/components/toast/examples/toast-width.component.ts @@ -12,10 +12,7 @@ const SIZES = [ ] as const; const template = ` - +
@for (s of sizes; track s.key) { @@ -66,8 +63,8 @@ export class ToastWidthComponent { this.toastService.add({ key: 'width-preview', severity: 'info', - summary: 'Сообщение', - detail: 'Ширина: ' + cssVar, + message: 'Сообщение', + caption: 'Ширина: ' + cssVar, life: 3000, icon: 'ti ti-info-circle' }); @@ -80,7 +77,7 @@ export const Width: StoryObj = { }), parameters: { docs: { - description: { story: 'Ширина задаётся через CSS-переменную `--p-toast-width` с помощью пропа `pt`.' }, + description: { story: 'Ширина задаётся через CSS-переменную `--p-toast-width` прямо на теге ``.' }, source: { language: 'ts', code: ` @@ -98,10 +95,7 @@ const SIZES = [ standalone: true, imports: [ExtraToastComponent, ExtraButtonComponent], template: \` - +
@for (s of sizes; track s.key) { @@ -126,8 +120,8 @@ export class ExampleComponent { this.toastService.add({ key: 'width-preview', severity: 'info', - summary: 'Сообщение', - detail: 'Ширина: ' + cssVar, + message: 'Сообщение', + caption: 'Ширина: ' + cssVar, life: 3000, icon: 'ti ti-info-circle', }); diff --git a/src/stories/components/toast/examples/toast-with-close-button.component.ts b/src/stories/components/toast/examples/toast-with-close-button.component.ts index 00e27412..b1ab61bc 100644 --- a/src/stories/components/toast/examples/toast-with-close-button.component.ts +++ b/src/stories/components/toast/examples/toast-with-close-button.component.ts @@ -5,18 +5,18 @@ import { ExtraToastComponent } from '../../../../lib/components/toast/toast.comp import { ExtraToastService } from '../../../../lib/components/toast/toast.service'; const SEVERITIES = [ - { type: 'info', icon: 'ti ti-info-circle', label: 'Информация' }, - { type: 'success', icon: 'ti ti-circle-check', label: 'Успех' }, - { type: 'warn', icon: 'ti ti-alert-triangle', label: 'Предупреждение' }, - { type: 'error', icon: 'ti ti-alert-circle', label: 'Ошибка' } + { severity: 'info', primeClass: 'info', icon: 'ti ti-info-circle', label: 'Информация' }, + { severity: 'success', primeClass: 'success', icon: 'ti ti-circle-check', label: 'Успех' }, + { severity: 'warning', primeClass: 'warn', icon: 'ti ti-alert-triangle', label: 'Предупреждение' }, + { severity: 'danger', primeClass: 'error', icon: 'ti ti-alert-circle', label: 'Ошибка' } ] as const; const template = `
- @for (s of severities; track s.type) { -
+ @for (s of severities; track s.severity) { +
@@ -24,10 +24,7 @@ const template = ` Сообщение
Подпись
-
@@ -36,12 +33,12 @@ const template = `
- @for (s of severities; track s.type) { + @for (s of severities; track s.severity) { }
@@ -60,15 +57,14 @@ export class ToastWithCloseButtonComponent { constructor(private readonly toastService: ExtraToastService) {} - show(severity: string, icon: string): void { + show(severity: 'info' | 'success' | 'warning' | 'danger', icon: string): void { this.toastService.add({ key: 'with-close', - severity: severity as any, - summary: 'Сообщение', - detail: 'Подпись', - life: 5000, + severity, + message: 'Сообщение', + caption: 'Подпись', icon, - closable: true + showClose: true }); } } @@ -79,19 +75,12 @@ export const WithCloseButton: StoryObj = { }), parameters: { docs: { - description: { story: 'Уведомления с кнопкой закрытия (closable: true).' }, + description: { story: 'Уведомления с кнопкой закрытия (`showClose: true`).' }, source: { language: 'ts', code: ` import { ExtraButtonComponent, ExtraToastComponent, ExtraToastService } from '@cdek-it/angular-ui-kit'; -const SEVERITIES = [ - { type: 'info', icon: 'ti ti-info-circle', label: 'Информация' }, - { type: 'success', icon: 'ti ti-circle-check', label: 'Успех' }, - { type: 'warn', icon: 'ti ti-alert-triangle', label: 'Предупреждение' }, - { type: 'error', icon: 'ti ti-alert-circle', label: 'Ошибка' }, -] as const; - @Component({ selector: 'app-example', standalone: true, @@ -99,32 +88,19 @@ const SEVERITIES = [ template: \` -
- @for (s of severities; track s.type) { - - } -
+ \`, }) export class ExampleComponent { - readonly severities = SEVERITIES; - constructor(private toastService: ExtraToastService) {} - show(severity: string, icon: string): void { + show(): void { this.toastService.add({ key: 'with-close', - severity: severity as any, - summary: 'Сообщение', - detail: 'Подпись', - life: 5000, - icon, - closable: true, + severity: 'info', + message: 'Сообщение', + caption: 'Подпись', + showClose: true, }); } } diff --git a/src/stories/components/toast/examples/toast-with-content.component.ts b/src/stories/components/toast/examples/toast-with-content.component.ts deleted file mode 100644 index 12633f2e..00000000 --- a/src/stories/components/toast/examples/toast-with-content.component.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { Component } from '@angular/core'; -import { StoryObj } from '@storybook/angular'; -import { ExtraButtonComponent } from '../../../../lib/components/button/button.component'; -import { ExtraToastComponent } from '../../../../lib/components/toast/toast.component'; -import { ExtraToastService } from '../../../../lib/components/toast/toast.service'; - -const SEVERITIES = [ - { type: 'info', icon: 'ti ti-info-circle', label: 'Информация' }, - { type: 'success', icon: 'ti ti-circle-check', label: 'Успех' }, - { type: 'warn', icon: 'ti ti-alert-triangle', label: 'Предупреждение' }, - { type: 'error', icon: 'ti ti-alert-circle', label: 'Ошибка' } -] as const; - -const template = ` - - -
- @for (s of severities; track s.type) { -
-
-
- -
- Сообщение -
Подпись
-
-
Дополнительный контент
-
-
-
Ячейка 1
-
Ячейка 2
-
-
- -
-
- } -
- -
- @for (s of severities; track s.type) { - - } -
-`; -const styles = ''; - -@Component({ - selector: 'app-toast-with-content', - standalone: true, - imports: [ExtraToastComponent, ExtraButtonComponent], - template, - styles -}) -export class ToastWithContentComponent { - readonly severities = SEVERITIES; - - constructor(private readonly toastService: ExtraToastService) {} - - show(severity: string, icon: string): void { - this.toastService.add({ - key: 'with-content', - severity: severity as any, - summary: 'Сообщение', - detail: 'Подпись', - life: 5000, - icon, - closable: true - }); - } -} - -export const WithContent: StoryObj = { - render: () => ({ - template: `` - }), - parameters: { - docs: { - description: { story: 'Уведомления с дополнительным контентом под заголовком.' }, - source: { - language: 'ts', - code: ` -import { ExtraButtonComponent, ExtraToastComponent, ExtraToastService } from '@cdek-it/angular-ui-kit'; - -const SEVERITIES = [ - { type: 'info', icon: 'ti ti-info-circle', label: 'Информация' }, - { type: 'success', icon: 'ti ti-circle-check', label: 'Успех' }, - { type: 'warn', icon: 'ti ti-alert-triangle', label: 'Предупреждение' }, - { type: 'error', icon: 'ti ti-alert-circle', label: 'Ошибка' }, -] as const; - -// Кастомный шаблон сообщения передаётся через ng-template -@Component({ - selector: 'app-example', - standalone: true, - imports: [ExtraToastComponent, ExtraButtonComponent], - template: \` - - -
- {{ message.summary }} -
{{ message.detail }}
-
-
Дополнительный контент
-
-
-
Ячейка 1
-
Ячейка 2
-
-
-
-
- -
- @for (s of severities; track s.type) { - - } -
- \`, -}) -export class ExampleComponent { - readonly severities = SEVERITIES; - - constructor(private toastService: ExtraToastService) {} - - show(severity: string, icon: string): void { - this.toastService.add({ - key: 'with-content', - severity: severity as any, - summary: 'Сообщение', - detail: 'Подпись', - life: 5000, - icon, - closable: true, - }); - } -} - ` - } - } - } -}; diff --git a/src/stories/components/toast/toast.stories.ts b/src/stories/components/toast/toast.stories.ts index 9b22ddf9..65e0fa3b 100644 --- a/src/stories/components/toast/toast.stories.ts +++ b/src/stories/components/toast/toast.stories.ts @@ -3,7 +3,7 @@ import { ExtraToastComponent } from '../../../lib/components/toast/toast.compone import { provideExtraToast } from '../../../lib/components/toast/provide-toast'; import { Severities, ToastSeveritiesComponent } from './examples/toast-severities.component'; import { ToastWithCloseButtonComponent, WithCloseButton } from './examples/toast-with-close-button.component'; -import { ToastWithContentComponent, WithContent } from './examples/toast-with-content.component'; +import { Slots, ToastSlotsComponent } from './examples/toast-slots.component'; import { ToastWidthComponent, Width } from './examples/toast-width.component'; import { Position, ToastPositionComponent } from './examples/toast-position.component'; @@ -18,7 +18,7 @@ const meta: Meta = { ExtraToastComponent, ToastSeveritiesComponent, ToastWithCloseButtonComponent, - ToastWithContentComponent, + ToastSlotsComponent, ToastWidthComponent, ToastPositionComponent ] @@ -28,11 +28,17 @@ const meta: Meta = { designTokens: { prefix: '--p-toast' }, docs: { description: { - component: `Компонент для отображения всплывающих уведомлений поверх интерфейса. + component: `Всплывающее уведомление поверх интерфейса — overlay-вариант \`ExtraMessage\`. -## Подключение +Реализован по спецификации [toast.md](https://github.com/cdek-it/angular-ui-kit/blob/main/docs/components-api/toast.md). + +\`\`\`typescript +import { ExtraToastComponent, ExtraToastService } from '@cdek-it/angular-ui-kit'; +\`\`\` + +Контейнер \`\` монтируется один раз (обычно в корне приложения), а показ каждого уведомления выполняется императивно через \`ExtraToastService.add({...})\` — свойства спецификации (\`severity\`/\`timer\`/\`message\`/\`caption\`/\`icon\`/\`show-close\`), слоты (\`content\`/\`footer\`) и событие \`onClose\` передаются полем в этом объекте, а не как \`@Input()\`/\`@Output()\` самого контейнера. -Добавьте \`provideExtraToast()\` в провайдеры приложения: +## Подключение \`\`\`typescript // app.config.ts @@ -56,7 +62,7 @@ export class AppComponent { private toast = inject(ExtraToastService); show() { - this.toast.add({ severity: 'success', summary: 'Готово', detail: 'Операция выполнена успешно' }); + this.toast.add({ severity: 'success', message: 'Готово', caption: 'Операция выполнена успешно' }); } } \`\`\`` @@ -67,7 +73,7 @@ export class AppComponent { position: { control: 'select', options: ['top-right', 'top-left', 'top-center', 'bottom-right', 'bottom-left', 'bottom-center', 'center'], - description: 'Позиция тоста на экране.', + description: 'Позиция группы уведомлений на экране', table: { category: 'Props', defaultValue: { summary: 'top-right' }, @@ -80,9 +86,21 @@ export class AppComponent { key: { table: { disable: true } }, + appendTo: { + control: 'select', + options: ['body', 'self'], + description: + 'Куда монтируется контейнер уведомлений. `body` (по умолчанию) — уведомления позиционируются относительно окна браузера; `self` оставляет контейнер на месте в шаблоне (тогда ближайший предок с `transform`/`filter`/`contain` становится точкой отсчёта для `position: fixed`)', + table: { + category: 'Props', + defaultValue: { summary: 'body' }, + type: { summary: "'body' | 'self' | HTMLElement | ElementRef" } + } + }, life: { control: 'number', - description: 'Время (мс) до автоматического закрытия тоста.', + description: + 'Время (мс) до автоматического закрытия — общий дефолт контейнера, переопределяется полем `life` сообщения', table: { category: 'Props', defaultValue: { summary: '5000' }, @@ -93,11 +111,12 @@ export class AppComponent { args: { position: 'top-right', key: undefined, - life: 5000 + life: 5000, + appendTo: 'body' } }; export default meta; // ── Re-exports from example components ──────────────────────────────────── -export { Severities as Default, WithCloseButton, WithContent, Width, Position }; +export { Severities as Default, WithCloseButton, Slots, Width, Position };