Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/lib/components/toast/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "ng-packagr/ng-package.schema.json",
"lib": {
"entryFile": "public_api.ts"
}
}

17 changes: 17 additions & 0 deletions src/lib/components/toast/provide-toast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
import { MessageService } from 'primeng/api';

/**
* Регистрирует зависимости, необходимые для работы `ExtraToastService` и `<extra-toast>`.
* Вызывать один раз в `ApplicationConfig.providers` или в `bootstrapApplication`.
*
* @example
* // app.config.ts
* export const appConfig: ApplicationConfig = {
* providers: [provideExtraToast()],
* };
*/
export function provideExtraToast(): EnvironmentProviders {
return makeEnvironmentProviders([MessageService]);
}

6 changes: 6 additions & 0 deletions src/lib/components/toast/public_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export * from './toast.component';
export * from './toast.service';
export * from './provide-toast';



50 changes: 50 additions & 0 deletions src/lib/components/toast/toast.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Component, Input } from '@angular/core';
import { Toast } from 'primeng/toast';
import { SharedModule } from 'primeng/api';

export type ToastSeverity = 'success' | 'info' | 'warn' | 'error' | 'secondary' | 'contrast';
export type ToastPosition =
| 'top-right'
| 'top-left'
| 'top-center'
| 'bottom-right'
| 'bottom-left'
| 'bottom-center'
| 'center';

const SEVERITY_ICONS: Record<string, string> = {
info: 'ti ti-info-circle',
success: 'ti ti-circle-check',
warn: 'ti ti-alert-triangle',
error: 'ti ti-alert-circle',
};

@Component({
selector: 'extra-toast',
standalone: true,
imports: [Toast, SharedModule],
template: `
<p-toast [position]="position" [key]="key" [life]="life" [pt]="pt">
<ng-template #message let-message>
<div class="p-toast-accent-line"></div>
<i [class]="resolveIcon(message) + ' p-toast-message-icon'"></i>
<div class="p-toast-message-text">
<span class="p-toast-summary">{{ message.summary }}</span>
@if (message.detail) {
<div class="p-toast-detail">{{ message.detail }}</div>
}
</div>
</ng-template>
</p-toast>
`,
})
export class ExtraToastComponent {
@Input() position: ToastPosition = 'top-right';
@Input() key: string | undefined = undefined;
@Input() life = 5000;
@Input() pt: Record<string, any> | undefined = undefined;

resolveIcon(message: { severity?: string; icon?: string }): string {
return message.icon ?? SEVERITY_ICONS[message.severity ?? 'info'] ?? 'ti ti-info-circle';
}
}
28 changes: 28 additions & 0 deletions src/lib/components/toast/toast.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Injectable } from '@angular/core';
import { MessageService } from 'primeng/api';
import { ToastSeverity } from './toast.component';

export interface ExtraToastMessage {
key?: string;
severity?: ToastSeverity;
summary?: string;
detail?: string;
life?: number;
icon?: string;
closable?: boolean;
data?: unknown;
}

@Injectable({ providedIn: 'root' })
export class ExtraToastService {

constructor(private readonly messageService: MessageService) {}

add(message: ExtraToastMessage): void {
this.messageService.add(message);
}

clear(key?: string): void {
this.messageService.clear(key);
}
}
144 changes: 144 additions & 0 deletions src/lib/providers/prime-preset/tokens/components/toast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
export const toastCss = ({ dt }: { dt: (token: string) => string }): string => `
/* Основной контейнер toast-сообщения */
.p-toast-message {
width: ${dt('toast.root.width')};
overflow: hidden;
border-width: ${dt('toast.root.borderWidth')};
border-radius: ${dt('toast.root.borderRadius')};
box-shadow: ${dt('toast.colorScheme.light.info.shadow')};
position: relative;
}

/* border-radius для контента toast-сообщения */
.p-toast .p-toast-message .p-toast-message-content {
border-radius: ${dt('toast.root.borderRadius')};
}

/* Текстовый блок toast */
.p-toast-message-text {
flex: 1;
display: flex;
flex-direction: column;
gap: ${dt('toast.text.gap')};
}

/* Заголовок toast */
.p-toast-summary {
font-family: ${dt('fonts.fontFamily.base')};
line-height: ${dt('fonts.lineHeight.250')};
}

/* Детальное описание toast */
.p-toast-message .p-toast-detail {
font-family: ${dt('fonts.fontFamily.base')};
line-height: ${dt('fonts.lineHeight.250')};
}

/* Кнопка закрытия toast-сообщения */
.p-toast-message .p-toast-message-content .p-toast-close-button {
margin: 0;
padding: 0;
right: 0;
}

/* Общие стили border для кнопки закрытия всех типов toast */
.p-toast-message-info .p-toast-close-button,
.p-toast-message-success .p-toast-close-button,
.p-toast-message-warn .p-toast-close-button,
.p-toast-message-error .p-toast-close-button {
border: ${dt('toast.extend.extCloseButton.width')} solid;
}

/* Общие стили для акцентной линии всех типов toast */
.p-toast-message-info .p-toast-accent-line,
.p-toast-message-success .p-toast-accent-line,
.p-toast-message-warn .p-toast-accent-line,
.p-toast-message-error .p-toast-accent-line {
width: ${dt('toast.extend.extAccentLine.width')};
position: absolute;
left: 0;
top: 0;
bottom: 0;
border-radius: ${dt('toast.root.borderRadius')} 0 0 ${dt('toast.root.borderRadius')};
}

/* Стили для toast типа Info */
.p-toast-message-info .p-toast-message-icon {
color: ${dt('toast.extend.extInfo.color')};
}

.p-toast-message-info .p-toast-close-button {
color: ${dt('toast.extend.extInfo.closeButton.color')};
border-color: ${dt('toast.extend.extInfo.closeButton.borderColor')};
}

.p-toast-message.p-toast-message-info .p-toast-close-button.p-button-text:not(:disabled):hover {
background: ${dt('toast.colorScheme.light.info.closeButton.hoverBackground')};
border-color: ${dt('toast.extend.extInfo.closeButton.borderColor')};
color: ${dt('toast.extend.extInfo.closeButton.color')};
}

.p-toast-message-info .p-toast-accent-line {
background: ${dt('toast.extend.extInfo.color')};
}

/* Стили для toast типа Success */
.p-toast-message-success .p-toast-message-icon {
color: ${dt('toast.extend.extSuccess.color')};
}

.p-toast-message-success .p-toast-close-button {
color: ${dt('toast.extend.extSuccess.closeButton.color')};
border-color: ${dt('toast.extend.extSuccess.closeButton.borderColor')};
}

.p-toast-message.p-toast-message-success .p-toast-close-button.p-button-text:not(:disabled):hover {
background: ${dt('toast.colorScheme.light.success.closeButton.hoverBackground')};
border-color: ${dt('toast.extend.extSuccess.closeButton.borderColor')};
color: ${dt('toast.extend.extSuccess.closeButton.color')};
}

.p-toast-message-success .p-toast-accent-line {
background: ${dt('toast.extend.extSuccess.color')};
}

/* Стили для toast типа Warn */
.p-toast-message-warn .p-toast-message-icon {
color: ${dt('toast.extend.extWarn.color')};
}

.p-toast-message-warn .p-toast-close-button {
color: ${dt('toast.extend.extWarn.closeButton.color')};
border-color: ${dt('toast.extend.extWarn.closeButton.borderColor')};
}

.p-toast-message.p-toast-message-warn .p-toast-close-button.p-button-text:not(:disabled):hover {
background: ${dt('toast.colorScheme.light.warn.closeButton.hoverBackground')};
border-color: ${dt('toast.extend.extWarn.closeButton.borderColor')};
color: ${dt('toast.extend.extWarn.closeButton.color')};
}

.p-toast-message-warn .p-toast-accent-line {
background: ${dt('toast.extend.extWarn.color')};
}

/* Стили для toast типа Error */
.p-toast-message-error .p-toast-message-icon {
color: ${dt('toast.extend.extError.color')};
}

.p-toast-message-error .p-toast-close-button {
color: ${dt('toast.extend.extError.closeButton.color')};
border-color: ${dt('toast.extend.extError.closeButton.borderColor')};
}

.p-toast-message.p-toast-message-error .p-toast-close-button.p-button-text:not(:disabled):hover {
background: ${dt('toast.colorScheme.light.error.closeButton.hoverBackground')};
border-color: ${dt('toast.extend.extError.closeButton.borderColor')};
color: ${dt('toast.extend.extError.closeButton.color')};
}

.p-toast-message-error .p-toast-accent-line {
background: ${dt('toast.extend.extError.color')};
}
`;
120 changes: 120 additions & 0 deletions src/stories/components/toast/examples/toast-position.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { Component } from '@angular/core';
import { StoryObj } from '@storybook/angular';
import { Button } from 'primeng/button';
import { ExtraToastComponent } from '../../../../lib/components/toast/toast.component';
import { ExtraToastService } from '../../../../lib/components/toast/toast.service';

const POSITIONS = [
{ position: 'top-left', label: 'Вверх слева', key: 'pos-top-left' },
{ position: 'top-center', label: 'Вверх по центру', key: 'pos-top-center' },
{ position: 'top-right', label: 'Вверх справа', key: 'pos-top-right' },
{ position: 'bottom-left', label: 'Вниз слева', key: 'pos-bottom-left' },
{ position: 'bottom-center', label: 'Вниз по центру', key: 'pos-bottom-center' },
{ position: 'bottom-right', label: 'Вниз справа', key: 'pos-bottom-right' },
] as const;

const template = `
@for (p of positions; track p.key) {
<extra-toast [position]="p.position" [key]="p.key"></extra-toast>
}

<div class="flex flex-col gap-2 items-center justify-center min-h-48">
@for (p of positions; track p.key) {
<p-button
[label]="p.label"
severity="contrast"
(onClick)="show(p.key, p.position)"
></p-button>
}
</div>
`;
const styles = '';

@Component({
selector: 'app-toast-position',
standalone: true,
imports: [ExtraToastComponent, Button],
template,
styles,
})
export class ToastPositionComponent {
readonly positions = POSITIONS;

constructor(private readonly toastService: ExtraToastService) {}

show(key: string, position: string): void {
this.toastService.add({
key,
severity: 'info',
summary: 'Сообщение',
detail: 'Позиция: ' + position,
life: 3000,
icon: 'ti ti-info-circle',
});
}
}

export const Position: StoryObj = {
render: () => ({
template: `<app-toast-position></app-toast-position>`,
}),
parameters: {
docs: {
description: { story: 'Расположение тоста задаётся через `position` и `key`.' },
source: {
language: 'ts',
code: `
import { Component } from '@angular/core';
import { ExtraToastComponent, ExtraToastService } from '@cdek-it/angular-ui-kit';
import { Button } from 'primeng/button';

const POSITIONS = [
{ position: 'top-left', label: 'Вверх слева', key: 'pos-top-left' },
{ position: 'top-center', label: 'Вверх по центру', key: 'pos-top-center' },
{ position: 'top-right', label: 'Вверх справа', key: 'pos-top-right' },
{ position: 'bottom-left', label: 'Вниз слева', key: 'pos-bottom-left' },
{ position: 'bottom-center', label: 'Вниз по центру', key: 'pos-bottom-center' },
{ position: 'bottom-right', label: 'Вниз справа', key: 'pos-bottom-right' },
] as const;

@Component({
selector: 'app-example',
standalone: true,
imports: [ExtraToastComponent, Button],
template: \`
@for (p of positions; track p.key) {
<extra-toast [position]="p.position" [key]="p.key"></extra-toast>
}

<div class="flex flex-col gap-2 items-center justify-center min-h-48">
@for (p of positions; track p.key) {
<p-button
[label]="p.label"
severity="contrast"
(onClick)="show(p.key, p.position)"
></p-button>
}
</div>
\`,
})
export class ExampleComponent {
readonly positions = POSITIONS;

constructor(private toastService: ExtraToastService) {}

show(key: string, position: string): void {
this.toastService.add({
key,
severity: 'info',
summary: 'Сообщение',
detail: 'Позиция: ' + position,
life: 3000,
icon: 'ti ti-info-circle',
});
}
}
`,
},
},
},
};
Loading
Loading