Add comprehensive toast notification component to feedback category
- Create new toast component with full TypeScript implementation - Add SCSS styling using semantic design tokens exclusively - Implement size variants (sm, md, lg) and color variants (primary, success, warning, danger, info) - Include interactive features: auto-dismiss, manual dismiss, progress bar, hover pause/resume - Add comprehensive accessibility support with ARIA attributes and keyboard navigation - Create engaging demo component with interactive examples and live statistics - Integrate toast into feedback category menu and routing system - Follow established component patterns and design system conventions 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,12 @@ import { ProgressCircleDemoComponent } from './progress-circle-demo/progress-cir
|
|||||||
import { RangeSliderDemoComponent } from './range-slider-demo/range-slider-demo.component';
|
import { RangeSliderDemoComponent } from './range-slider-demo/range-slider-demo.component';
|
||||||
import { DividerDemoComponent } from './divider-demo/divider-demo.component';
|
import { DividerDemoComponent } from './divider-demo/divider-demo.component';
|
||||||
import { TooltipDemoComponent } from './tooltip-demo/tooltip-demo.component';
|
import { TooltipDemoComponent } from './tooltip-demo/tooltip-demo.component';
|
||||||
|
import { AccordionDemoComponent } from './accordion-demo/accordion-demo.component';
|
||||||
|
import { PopoverDemoComponent } from './popover-demo/popover-demo.component';
|
||||||
|
import { AlertDemoComponent } from './alert-demo/alert-demo.component';
|
||||||
|
import { ToastDemoComponent } from './toast-demo/toast-demo.component';
|
||||||
|
import { TreeViewDemoComponent } from './tree-view-demo/tree-view-demo.component';
|
||||||
|
import { TimelineDemoComponent } from './timeline-demo/timeline-demo.component';
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -213,6 +219,30 @@ import { TooltipDemoComponent } from './tooltip-demo/tooltip-demo.component';
|
|||||||
<ui-tooltip-demo></ui-tooltip-demo>
|
<ui-tooltip-demo></ui-tooltip-demo>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@case ("accordion") {
|
||||||
|
<ui-accordion-demo></ui-accordion-demo>
|
||||||
|
}
|
||||||
|
|
||||||
|
@case ("popover") {
|
||||||
|
<ui-popover-demo></ui-popover-demo>
|
||||||
|
}
|
||||||
|
|
||||||
|
@case ("alert") {
|
||||||
|
<ui-alert-demo></ui-alert-demo>
|
||||||
|
}
|
||||||
|
|
||||||
|
@case ("toast") {
|
||||||
|
<ui-toast-demo></ui-toast-demo>
|
||||||
|
}
|
||||||
|
|
||||||
|
@case ("tree-view") {
|
||||||
|
<ui-tree-view-demo></ui-tree-view-demo>
|
||||||
|
}
|
||||||
|
|
||||||
|
@case ("timeline") {
|
||||||
|
<ui-timeline-demo></ui-timeline-demo>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -227,7 +257,8 @@ import { TooltipDemoComponent } from './tooltip-demo/tooltip-demo.component';
|
|||||||
GridSystemDemoComponent, SpacerDemoComponent, ContainerDemoComponent, PaginationDemoComponent,
|
GridSystemDemoComponent, SpacerDemoComponent, ContainerDemoComponent, PaginationDemoComponent,
|
||||||
SkeletonLoaderDemoComponent, EmptyStateDemoComponent, FileUploadDemoComponent, FormFieldDemoComponent,
|
SkeletonLoaderDemoComponent, EmptyStateDemoComponent, FileUploadDemoComponent, FormFieldDemoComponent,
|
||||||
AutocompleteDemoComponent, BackdropDemoComponent, OverlayContainerDemoComponent, LoadingSpinnerDemoComponent,
|
AutocompleteDemoComponent, BackdropDemoComponent, OverlayContainerDemoComponent, LoadingSpinnerDemoComponent,
|
||||||
ProgressCircleDemoComponent, RangeSliderDemoComponent, DividerDemoComponent, TooltipDemoComponent]
|
ProgressCircleDemoComponent, RangeSliderDemoComponent, DividerDemoComponent, TooltipDemoComponent, AccordionDemoComponent,
|
||||||
|
PopoverDemoComponent, AlertDemoComponent, ToastDemoComponent, TreeViewDemoComponent, TimelineDemoComponent]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
@use '../../../../../../../../shared-ui/src/styles/semantic' as *;
|
||||||
|
|
||||||
|
.demo-container {
|
||||||
|
padding: $semantic-spacing-layout-section-md;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-section {
|
||||||
|
margin-bottom: $semantic-spacing-layout-section-lg;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||||
|
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||||
|
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||||
|
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||||
|
color: $semantic-color-text-primary;
|
||||||
|
margin-bottom: $semantic-spacing-content-heading;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||||
|
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||||
|
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||||
|
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||||
|
color: $semantic-color-text-primary;
|
||||||
|
margin-bottom: $semantic-spacing-content-heading;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||||
|
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||||
|
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||||
|
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||||
|
color: $semantic-color-text-secondary;
|
||||||
|
margin-bottom: $semantic-spacing-content-paragraph;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: $semantic-spacing-grid-gap-md;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-wrapper {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-section {
|
||||||
|
margin-bottom: $semantic-spacing-layout-section-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: $semantic-spacing-component-sm;
|
||||||
|
margin-bottom: $semantic-spacing-component-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $semantic-spacing-component-xs;
|
||||||
|
padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
|
||||||
|
background: $semantic-color-surface-primary;
|
||||||
|
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||||
|
border-radius: $semantic-border-button-radius;
|
||||||
|
color: $semantic-color-text-primary;
|
||||||
|
font-family: map-get($semantic-typography-button-medium, font-family);
|
||||||
|
font-size: map-get($semantic-typography-button-medium, font-size);
|
||||||
|
font-weight: map-get($semantic-typography-button-medium, font-weight);
|
||||||
|
line-height: map-get($semantic-typography-button-medium, line-height);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||||
|
min-height: $semantic-sizing-button-height-md;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: $semantic-shadow-button-hover;
|
||||||
|
background: $semantic-color-surface-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid $semantic-color-focus;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
box-shadow: $semantic-shadow-button-rest;
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--primary {
|
||||||
|
background: $semantic-color-primary;
|
||||||
|
color: $semantic-color-on-primary;
|
||||||
|
border-color: $semantic-color-primary;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $semantic-color-primary;
|
||||||
|
opacity: $semantic-opacity-hover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--secondary {
|
||||||
|
background: $semantic-color-secondary;
|
||||||
|
color: $semantic-color-on-secondary;
|
||||||
|
border-color: $semantic-color-secondary;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $semantic-color-secondary;
|
||||||
|
opacity: $semantic-opacity-hover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--warning {
|
||||||
|
background: $semantic-color-warning;
|
||||||
|
color: $semantic-color-on-warning;
|
||||||
|
border-color: $semantic-color-warning;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $semantic-color-warning;
|
||||||
|
opacity: $semantic-opacity-hover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--danger {
|
||||||
|
background: $semantic-color-danger;
|
||||||
|
color: $semantic-color-on-danger;
|
||||||
|
border-color: $semantic-color-danger;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $semantic-color-danger;
|
||||||
|
opacity: $semantic-opacity-hover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-container {
|
||||||
|
position: fixed;
|
||||||
|
top: $semantic-spacing-layout-section-md;
|
||||||
|
right: $semantic-spacing-layout-section-md;
|
||||||
|
z-index: $semantic-z-index-modal;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $semantic-spacing-component-sm;
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
.ui-toast {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-section {
|
||||||
|
margin-top: $semantic-spacing-layout-section-md;
|
||||||
|
padding: $semantic-spacing-component-lg;
|
||||||
|
background: $semantic-color-surface-elevated;
|
||||||
|
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||||
|
border-radius: $semantic-border-card-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: $semantic-spacing-grid-gap-md;
|
||||||
|
margin-bottom: $semantic-spacing-component-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: $semantic-spacing-component-sm;
|
||||||
|
background: $semantic-color-surface-primary;
|
||||||
|
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||||
|
border-radius: $semantic-border-radius-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||||
|
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||||
|
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||||
|
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||||
|
color: $semantic-color-text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-family: map-get($semantic-typography-label, font-family);
|
||||||
|
font-size: map-get($semantic-typography-label, font-size);
|
||||||
|
font-weight: $semantic-typography-font-weight-semibold;
|
||||||
|
line-height: map-get($semantic-typography-label, line-height);
|
||||||
|
color: $semantic-color-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive Design
|
||||||
|
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||||
|
.demo-container {
|
||||||
|
padding: $semantic-spacing-layout-section-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-row {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-container {
|
||||||
|
top: $semantic-spacing-layout-section-sm;
|
||||||
|
right: $semantic-spacing-layout-section-sm;
|
||||||
|
left: $semantic-spacing-layout-section-sm;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||||
|
.demo-container {
|
||||||
|
padding: $semantic-spacing-layout-section-xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-container {
|
||||||
|
top: $semantic-spacing-layout-section-xs;
|
||||||
|
right: $semantic-spacing-layout-section-xs;
|
||||||
|
left: $semantic-spacing-layout-section-xs;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { ToastComponent } from '../../../../../../../ui-essentials/src/lib/components/feedback/toast/toast.component';
|
||||||
|
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||||
|
import { faPlay, faStop, faRedo } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ui-toast-demo',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, ToastComponent, FontAwesomeModule],
|
||||||
|
template: `
|
||||||
|
<div class="demo-container">
|
||||||
|
<h2>Toast Demo</h2>
|
||||||
|
<p>Toast notifications provide contextual feedback messages for user actions.</p>
|
||||||
|
|
||||||
|
<!-- Size Variants -->
|
||||||
|
<section class="demo-section">
|
||||||
|
<h3>Sizes</h3>
|
||||||
|
<div class="demo-row">
|
||||||
|
@for (size of sizes; track size) {
|
||||||
|
<div class="toast-wrapper">
|
||||||
|
<ui-toast [size]="size" [autoDismiss]="false" title="Toast notification">
|
||||||
|
This is a {{ size }} toast message.
|
||||||
|
</ui-toast>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Color Variants -->
|
||||||
|
<section class="demo-section">
|
||||||
|
<h3>Variants</h3>
|
||||||
|
<div class="demo-row">
|
||||||
|
@for (variant of variants; track variant) {
|
||||||
|
<div class="toast-wrapper">
|
||||||
|
<ui-toast
|
||||||
|
[variant]="variant"
|
||||||
|
[autoDismiss]="false"
|
||||||
|
[title]="getVariantTitle(variant)">
|
||||||
|
{{ getVariantMessage(variant) }}
|
||||||
|
</ui-toast>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- States -->
|
||||||
|
<section class="demo-section">
|
||||||
|
<h3>Configuration Options</h3>
|
||||||
|
<div class="demo-row">
|
||||||
|
<div class="toast-wrapper">
|
||||||
|
<ui-toast
|
||||||
|
variant="success"
|
||||||
|
[autoDismiss]="false"
|
||||||
|
[dismissible]="false"
|
||||||
|
title="Non-dismissible">
|
||||||
|
This toast cannot be manually dismissed.
|
||||||
|
</ui-toast>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toast-wrapper">
|
||||||
|
<ui-toast
|
||||||
|
variant="info"
|
||||||
|
[autoDismiss]="false"
|
||||||
|
[showIcon]="false"
|
||||||
|
title="No icon">
|
||||||
|
This toast has no icon displayed.
|
||||||
|
</ui-toast>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toast-wrapper">
|
||||||
|
<ui-toast
|
||||||
|
variant="warning"
|
||||||
|
[autoDismiss]="false"
|
||||||
|
[showProgress]="true"
|
||||||
|
title="With progress"
|
||||||
|
[boldTitle]="true">
|
||||||
|
This toast shows a progress indicator.
|
||||||
|
</ui-toast>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Interactive Examples -->
|
||||||
|
<section class="demo-section">
|
||||||
|
<h3>Interactive Examples</h3>
|
||||||
|
|
||||||
|
<div class="controls-section">
|
||||||
|
<h4>Toast Controls</h4>
|
||||||
|
<div class="button-group">
|
||||||
|
<button
|
||||||
|
class="demo-button demo-button--primary"
|
||||||
|
(click)="showToast('success', 'Success!', 'Your action was completed successfully.')">
|
||||||
|
<fa-icon [icon]="faPlay"></fa-icon>
|
||||||
|
Show Success Toast
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="demo-button demo-button--warning"
|
||||||
|
(click)="showToast('warning', 'Warning!', 'Please review your input before proceeding.')">
|
||||||
|
<fa-icon [icon]="faPlay"></fa-icon>
|
||||||
|
Show Warning Toast
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="demo-button demo-button--danger"
|
||||||
|
(click)="showToast('danger', 'Error!', 'Something went wrong. Please try again.')">
|
||||||
|
<fa-icon [icon]="faPlay"></fa-icon>
|
||||||
|
Show Error Toast
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="demo-button demo-button--secondary"
|
||||||
|
(click)="clearToasts()">
|
||||||
|
<fa-icon [icon]="faStop"></fa-icon>
|
||||||
|
Clear All
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast Container -->
|
||||||
|
<div class="toast-container">
|
||||||
|
@for (toast of activeToasts; track toast.id) {
|
||||||
|
<ui-toast
|
||||||
|
[variant]="toast.variant"
|
||||||
|
[title]="toast.title"
|
||||||
|
[size]="toast.size"
|
||||||
|
[duration]="toast.duration"
|
||||||
|
[autoDismiss]="toast.autoDismiss"
|
||||||
|
[showProgress]="toast.showProgress"
|
||||||
|
(dismissed)="removeToast(toast.id)"
|
||||||
|
(expired)="removeToast(toast.id)">
|
||||||
|
{{ toast.message }}
|
||||||
|
</ui-toast>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistics -->
|
||||||
|
<div class="stats-section">
|
||||||
|
<h4>Demo Statistics</h4>
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">Toasts Shown:</span>
|
||||||
|
<span class="stat-value">{{ toastCount }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">Currently Active:</span>
|
||||||
|
<span class="stat-value">{{ activeToasts.length }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">Auto Dismissed:</span>
|
||||||
|
<span class="stat-value">{{ expiredCount }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">Manually Dismissed:</span>
|
||||||
|
<span class="stat-value">{{ dismissedCount }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="demo-button demo-button--secondary"
|
||||||
|
(click)="resetStats()">
|
||||||
|
<fa-icon [icon]="faRedo"></fa-icon>
|
||||||
|
Reset Statistics
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styleUrl: './toast-demo.component.scss'
|
||||||
|
})
|
||||||
|
export class ToastDemoComponent {
|
||||||
|
sizes = ['sm', 'md', 'lg'] as const;
|
||||||
|
variants = ['primary', 'success', 'warning', 'danger', 'info'] as const;
|
||||||
|
|
||||||
|
// Icons
|
||||||
|
readonly faPlay = faPlay;
|
||||||
|
readonly faStop = faStop;
|
||||||
|
readonly faRedo = faRedo;
|
||||||
|
|
||||||
|
// Toast management
|
||||||
|
activeToasts: any[] = [];
|
||||||
|
private nextToastId = 1;
|
||||||
|
|
||||||
|
// Statistics
|
||||||
|
toastCount = 0;
|
||||||
|
expiredCount = 0;
|
||||||
|
dismissedCount = 0;
|
||||||
|
|
||||||
|
getVariantTitle(variant: string): string {
|
||||||
|
const titles = {
|
||||||
|
primary: 'Primary Toast',
|
||||||
|
success: 'Success Toast',
|
||||||
|
warning: 'Warning Toast',
|
||||||
|
danger: 'Error Toast',
|
||||||
|
info: 'Information Toast'
|
||||||
|
};
|
||||||
|
return titles[variant as keyof typeof titles] || 'Toast';
|
||||||
|
}
|
||||||
|
|
||||||
|
getVariantMessage(variant: string): string {
|
||||||
|
const messages = {
|
||||||
|
primary: 'This is a primary notification message.',
|
||||||
|
success: 'Operation completed successfully!',
|
||||||
|
warning: 'Please review your input carefully.',
|
||||||
|
danger: 'An error occurred during processing.',
|
||||||
|
info: 'Here is some helpful information.'
|
||||||
|
};
|
||||||
|
return messages[variant as keyof typeof messages] || 'Toast message';
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast(variant: any, title: string, message: string): void {
|
||||||
|
const toast = {
|
||||||
|
id: this.nextToastId++,
|
||||||
|
variant,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
size: 'md' as const,
|
||||||
|
duration: 4000,
|
||||||
|
autoDismiss: true,
|
||||||
|
showProgress: true
|
||||||
|
};
|
||||||
|
|
||||||
|
this.activeToasts.push(toast);
|
||||||
|
this.toastCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeToast(id: number): void {
|
||||||
|
const index = this.activeToasts.findIndex(toast => toast.id === id);
|
||||||
|
if (index > -1) {
|
||||||
|
this.activeToasts.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearToasts(): void {
|
||||||
|
this.dismissedCount += this.activeToasts.length;
|
||||||
|
this.activeToasts = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
resetStats(): void {
|
||||||
|
this.toastCount = 0;
|
||||||
|
this.expiredCount = 0;
|
||||||
|
this.dismissedCount = 0;
|
||||||
|
this.activeToasts = [];
|
||||||
|
this.nextToastId = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
faVideo, faComment, faMousePointer, faLayerGroup, faSquare, faCalendarDays, faClock,
|
faVideo, faComment, faMousePointer, faLayerGroup, faSquare, faCalendarDays, faClock,
|
||||||
faGripVertical, faArrowsAlt, faBoxOpen, faChevronLeft, faSpinner, faExclamationTriangle,
|
faGripVertical, faArrowsAlt, faBoxOpen, faChevronLeft, faSpinner, faExclamationTriangle,
|
||||||
faCloudUploadAlt, faFileText, faListAlt, faCircle, faExpandArrowsAlt, faCircleNotch, faSliders,
|
faCloudUploadAlt, faFileText, faListAlt, faCircle, faExpandArrowsAlt, faCircleNotch, faSliders,
|
||||||
faMinus, faInfoCircle
|
faMinus, faInfoCircle, faChevronDown, faCaretUp, faExclamationCircle, faSitemap, faStream,
|
||||||
|
faBell
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
import { DemoRoutes } from '../../demos';
|
import { DemoRoutes } from '../../demos';
|
||||||
import { ScrollContainerComponent } from '../../../../../ui-essentials/src/lib/layouts/scroll-container.component';
|
import { ScrollContainerComponent } from '../../../../../ui-essentials/src/lib/layouts/scroll-container.component';
|
||||||
@@ -94,6 +95,11 @@ export class DashboardComponent {
|
|||||||
faSliders = faSliders;
|
faSliders = faSliders;
|
||||||
faMinus = faMinus;
|
faMinus = faMinus;
|
||||||
faInfoCircle = faInfoCircle;
|
faInfoCircle = faInfoCircle;
|
||||||
|
faChevronDown = faChevronDown;
|
||||||
|
faCaretUp = faCaretUp;
|
||||||
|
faSitemap = faSitemap;
|
||||||
|
faStream = faStream;
|
||||||
|
faBell = faBell;
|
||||||
|
|
||||||
menuItems: any = []
|
menuItems: any = []
|
||||||
|
|
||||||
@@ -145,6 +151,7 @@ export class DashboardComponent {
|
|||||||
|
|
||||||
// Data Display category
|
// Data Display category
|
||||||
const dataDisplayChildren = [
|
const dataDisplayChildren = [
|
||||||
|
this.createChildItem("accordion", "Accordion", this.faChevronDown),
|
||||||
this.createChildItem("table", "Tables", this.faTable),
|
this.createChildItem("table", "Tables", this.faTable),
|
||||||
this.createChildItem("lists", "Lists", this.faList),
|
this.createChildItem("lists", "Lists", this.faList),
|
||||||
this.createChildItem("progress", "Progress Bars", this.faCheckSquare),
|
this.createChildItem("progress", "Progress Bars", this.faCheckSquare),
|
||||||
@@ -152,7 +159,9 @@ export class DashboardComponent {
|
|||||||
this.createChildItem("avatar", "Avatars", this.faUserCircle),
|
this.createChildItem("avatar", "Avatars", this.faUserCircle),
|
||||||
this.createChildItem("cards", "Cards", this.faIdCard),
|
this.createChildItem("cards", "Cards", this.faIdCard),
|
||||||
this.createChildItem("divider", "Divider", this.faMinus),
|
this.createChildItem("divider", "Divider", this.faMinus),
|
||||||
this.createChildItem("tooltip", "Tooltip", this.faInfoCircle)
|
this.createChildItem("timeline", "Timeline", this.faStream),
|
||||||
|
this.createChildItem("tooltip", "Tooltip", this.faInfoCircle),
|
||||||
|
this.createChildItem("tree-view", "Tree View", this.faSitemap)
|
||||||
];
|
];
|
||||||
this.addMenuItem("data-display", "Data Display", this.faEye, dataDisplayChildren);
|
this.addMenuItem("data-display", "Data Display", this.faEye, dataDisplayChildren);
|
||||||
|
|
||||||
@@ -180,6 +189,8 @@ export class DashboardComponent {
|
|||||||
|
|
||||||
// Feedback category
|
// Feedback category
|
||||||
const feedbackChildren = [
|
const feedbackChildren = [
|
||||||
|
this.createChildItem("alert", "Alert", faExclamationCircle),
|
||||||
|
this.createChildItem("toast", "Toast", this.faBell),
|
||||||
this.createChildItem("chips", "Chips", this.faTags),
|
this.createChildItem("chips", "Chips", this.faTags),
|
||||||
this.createChildItem("loading-spinner", "Loading Spinner", this.faSpinner),
|
this.createChildItem("loading-spinner", "Loading Spinner", this.faSpinner),
|
||||||
this.createChildItem("skeleton-loader", "Skeleton Loader", this.faSpinner),
|
this.createChildItem("skeleton-loader", "Skeleton Loader", this.faSpinner),
|
||||||
@@ -201,6 +212,7 @@ export class DashboardComponent {
|
|||||||
const overlaysChildren = [
|
const overlaysChildren = [
|
||||||
this.createChildItem("modal", "Modal/Dialog", this.faSquare),
|
this.createChildItem("modal", "Modal/Dialog", this.faSquare),
|
||||||
this.createChildItem("drawer", "Drawer/Sidebar", this.faBars),
|
this.createChildItem("drawer", "Drawer/Sidebar", this.faBars),
|
||||||
|
this.createChildItem("popover", "Popover/Dropdown", this.faCaretUp),
|
||||||
this.createChildItem("backdrop", "Backdrop", this.faCircle),
|
this.createChildItem("backdrop", "Backdrop", this.faCircle),
|
||||||
this.createChildItem("overlay-container", "Overlay Container", this.faExpandArrowsAlt)
|
this.createChildItem("overlay-container", "Overlay Container", this.faExpandArrowsAlt)
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
export * from "./alert";
|
||||||
export * from "./status-badge.component";
|
export * from "./status-badge.component";
|
||||||
export * from "./skeleton-loader";
|
export * from "./skeleton-loader";
|
||||||
export * from "./empty-state";
|
export * from "./empty-state";
|
||||||
export * from "./loading-spinner";
|
export * from "./loading-spinner";
|
||||||
export * from "./progress-circle";
|
export * from "./progress-circle";
|
||||||
|
export * from "./toast";
|
||||||
// export * from "./theme-switcher.component"; // Temporarily disabled due to CSS variable issues
|
// export * from "./theme-switcher.component"; // Temporarily disabled due to CSS variable issues
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './toast.component';
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
|
||||||
|
|
||||||
|
.ui-toast {
|
||||||
|
// Core Structure
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
|
||||||
|
// Layout & Spacing
|
||||||
|
padding: $semantic-spacing-component-md;
|
||||||
|
gap: $semantic-spacing-component-sm;
|
||||||
|
|
||||||
|
// Visual Design
|
||||||
|
background: $semantic-color-surface-elevated;
|
||||||
|
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||||
|
border-radius: $semantic-border-card-radius;
|
||||||
|
box-shadow: $semantic-shadow-elevation-3;
|
||||||
|
|
||||||
|
// Typography
|
||||||
|
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||||
|
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||||
|
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||||
|
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||||
|
color: $semantic-color-text-primary;
|
||||||
|
|
||||||
|
// Transitions
|
||||||
|
transition: all $semantic-motion-duration-normal $semantic-motion-easing-ease;
|
||||||
|
|
||||||
|
// Size Variants
|
||||||
|
&--sm {
|
||||||
|
padding: $semantic-spacing-component-sm;
|
||||||
|
gap: $semantic-spacing-component-xs;
|
||||||
|
max-width: 320px;
|
||||||
|
font-family: map-get($semantic-typography-body-small, font-family);
|
||||||
|
font-size: map-get($semantic-typography-body-small, font-size);
|
||||||
|
font-weight: map-get($semantic-typography-body-small, font-weight);
|
||||||
|
line-height: map-get($semantic-typography-body-small, line-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--md {
|
||||||
|
// Default styles already applied above
|
||||||
|
}
|
||||||
|
|
||||||
|
&--lg {
|
||||||
|
padding: $semantic-spacing-component-lg;
|
||||||
|
gap: $semantic-spacing-component-md;
|
||||||
|
max-width: 480px;
|
||||||
|
font-family: map-get($semantic-typography-body-large, font-family);
|
||||||
|
font-size: map-get($semantic-typography-body-large, font-size);
|
||||||
|
font-weight: map-get($semantic-typography-body-large, font-weight);
|
||||||
|
line-height: map-get($semantic-typography-body-large, line-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color Variants
|
||||||
|
&--primary {
|
||||||
|
border-color: $semantic-color-primary;
|
||||||
|
border-left-width: 4px;
|
||||||
|
|
||||||
|
.ui-toast__icon {
|
||||||
|
color: $semantic-color-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-toast__title {
|
||||||
|
color: $semantic-color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--success {
|
||||||
|
border-color: $semantic-color-success;
|
||||||
|
border-left-width: 4px;
|
||||||
|
|
||||||
|
.ui-toast__icon {
|
||||||
|
color: $semantic-color-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-toast__title {
|
||||||
|
color: $semantic-color-success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--warning {
|
||||||
|
border-color: $semantic-color-warning;
|
||||||
|
border-left-width: 4px;
|
||||||
|
|
||||||
|
.ui-toast__icon {
|
||||||
|
color: $semantic-color-warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-toast__title {
|
||||||
|
color: $semantic-color-warning;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--danger {
|
||||||
|
border-color: $semantic-color-danger;
|
||||||
|
border-left-width: 4px;
|
||||||
|
|
||||||
|
.ui-toast__icon {
|
||||||
|
color: $semantic-color-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-toast__title {
|
||||||
|
color: $semantic-color-danger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--info {
|
||||||
|
border-color: $semantic-color-info;
|
||||||
|
border-left-width: 4px;
|
||||||
|
|
||||||
|
.ui-toast__icon {
|
||||||
|
color: $semantic-color-info;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-toast__title {
|
||||||
|
color: $semantic-color-info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BEM Elements
|
||||||
|
&__icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: $semantic-color-text-secondary;
|
||||||
|
font-size: $semantic-sizing-icon-inline;
|
||||||
|
margin-top: 2px; // Slight optical alignment
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0; // Prevents flex overflow
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
margin: 0 0 $semantic-spacing-content-line-tight 0;
|
||||||
|
font-family: map-get($semantic-typography-label, font-family);
|
||||||
|
font-size: map-get($semantic-typography-label, font-size);
|
||||||
|
font-weight: map-get($semantic-typography-label, font-weight);
|
||||||
|
line-height: map-get($semantic-typography-label, line-height);
|
||||||
|
color: $semantic-color-text-primary;
|
||||||
|
|
||||||
|
&--bold {
|
||||||
|
font-weight: $semantic-typography-font-weight-semibold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__message {
|
||||||
|
margin: 0;
|
||||||
|
color: $semantic-color-text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
margin-top: $semantic-spacing-component-sm;
|
||||||
|
display: flex;
|
||||||
|
gap: $semantic-spacing-component-xs;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__dismiss {
|
||||||
|
position: absolute;
|
||||||
|
top: $semantic-spacing-component-xs;
|
||||||
|
right: $semantic-spacing-component-xs;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: $semantic-color-text-tertiary;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: $semantic-spacing-component-xs;
|
||||||
|
border-radius: $semantic-border-radius-sm;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: $semantic-sizing-touch-minimum;
|
||||||
|
min-height: $semantic-sizing-touch-minimum;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $semantic-color-surface-secondary;
|
||||||
|
color: $semantic-color-text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid $semantic-color-focus;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: $semantic-color-surface-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// State Variants
|
||||||
|
&--dismissible {
|
||||||
|
padding-right: calc($semantic-spacing-component-lg + $semantic-sizing-touch-minimum);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toast-specific positioning and animations
|
||||||
|
&--entering {
|
||||||
|
animation: toast-slide-in $semantic-motion-duration-normal $semantic-motion-easing-ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--exiting {
|
||||||
|
animation: toast-slide-out $semantic-motion-duration-normal $semantic-motion-easing-ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Progress bar for auto-dismiss
|
||||||
|
&__progress {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: $semantic-color-surface-secondary;
|
||||||
|
border-radius: 0 0 $semantic-border-card-radius $semantic-border-card-radius;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: currentColor;
|
||||||
|
width: 100%;
|
||||||
|
transform-origin: left;
|
||||||
|
animation: toast-progress var(--duration) linear forwards;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--with-progress {
|
||||||
|
padding-bottom: calc($semantic-spacing-component-md + 3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive Design
|
||||||
|
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||||
|
padding: $semantic-spacing-component-sm;
|
||||||
|
gap: $semantic-spacing-component-xs;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
&--lg {
|
||||||
|
padding: $semantic-spacing-component-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-family: map-get($semantic-typography-body-small, font-family);
|
||||||
|
font-size: map-get($semantic-typography-body-small, font-size);
|
||||||
|
font-weight: map-get($semantic-typography-body-small, font-weight);
|
||||||
|
line-height: map-get($semantic-typography-body-small, line-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__message {
|
||||||
|
font-family: map-get($semantic-typography-caption, font-family);
|
||||||
|
font-size: map-get($semantic-typography-caption, font-size);
|
||||||
|
font-weight: map-get($semantic-typography-caption, font-weight);
|
||||||
|
line-height: map-get($semantic-typography-caption, line-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--dismissible {
|
||||||
|
padding-right: calc($semantic-spacing-component-md + $semantic-sizing-touch-minimum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyframe animations
|
||||||
|
@keyframes toast-slide-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes toast-slide-out {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes toast-progress {
|
||||||
|
from {
|
||||||
|
transform: scaleX(1);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scaleX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, OnInit, OnDestroy, ElementRef, inject } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||||
|
import { IconDefinition } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import {
|
||||||
|
faCheckCircle,
|
||||||
|
faExclamationTriangle,
|
||||||
|
faExclamationCircle,
|
||||||
|
faInfoCircle,
|
||||||
|
faTimes
|
||||||
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
type ToastSize = 'sm' | 'md' | 'lg';
|
||||||
|
type ToastVariant = 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
||||||
|
type ToastPosition = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' | 'bottom-center';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ui-toast',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, FontAwesomeModule],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
template: `
|
||||||
|
<div
|
||||||
|
class="ui-toast"
|
||||||
|
[class.ui-toast--sm]="size === 'sm'"
|
||||||
|
[class.ui-toast--md]="size === 'md'"
|
||||||
|
[class.ui-toast--lg]="size === 'lg'"
|
||||||
|
[class.ui-toast--primary]="variant === 'primary'"
|
||||||
|
[class.ui-toast--success]="variant === 'success'"
|
||||||
|
[class.ui-toast--warning]="variant === 'warning'"
|
||||||
|
[class.ui-toast--danger]="variant === 'danger'"
|
||||||
|
[class.ui-toast--info]="variant === 'info'"
|
||||||
|
[class.ui-toast--dismissible]="dismissible"
|
||||||
|
[class.ui-toast--with-progress]="showProgress && autoDismiss"
|
||||||
|
[class.ui-toast--entering]="isEntering"
|
||||||
|
[class.ui-toast--exiting]="isExiting"
|
||||||
|
[attr.role]="role"
|
||||||
|
[attr.aria-live]="ariaLive"
|
||||||
|
[attr.aria-labelledby]="title ? 'toast-title-' + toastId : null"
|
||||||
|
[attr.aria-describedby]="'toast-content-' + toastId">
|
||||||
|
|
||||||
|
@if (showIcon && toastIcon) {
|
||||||
|
<fa-icon
|
||||||
|
class="ui-toast__icon"
|
||||||
|
[icon]="toastIcon"
|
||||||
|
[attr.aria-hidden]="true">
|
||||||
|
</fa-icon>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="ui-toast__content">
|
||||||
|
@if (title) {
|
||||||
|
<h4
|
||||||
|
class="ui-toast__title"
|
||||||
|
[class.ui-toast__title--bold]="boldTitle"
|
||||||
|
[id]="'toast-title-' + toastId">
|
||||||
|
{{ title }}
|
||||||
|
</h4>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="ui-toast__message"
|
||||||
|
[id]="'toast-content-' + toastId">
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (actions && actions.length > 0) {
|
||||||
|
<div class="ui-toast__actions">
|
||||||
|
<ng-content select="[slot=actions]"></ng-content>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (dismissible) {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ui-toast__dismiss"
|
||||||
|
[attr.aria-label]="dismissLabel"
|
||||||
|
(click)="handleDismiss()"
|
||||||
|
(keydown)="handleDismissKeydown($event)">
|
||||||
|
<fa-icon [icon]="faTimes" [attr.aria-hidden]="true"></fa-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (showProgress && autoDismiss && duration > 0) {
|
||||||
|
<div class="ui-toast__progress">
|
||||||
|
<div
|
||||||
|
class="ui-toast__progress-bar"
|
||||||
|
[style.--duration]="duration + 'ms'">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styleUrl: './toast.component.scss'
|
||||||
|
})
|
||||||
|
export class ToastComponent implements OnInit, OnDestroy {
|
||||||
|
@Input() size: ToastSize = 'md';
|
||||||
|
@Input() variant: ToastVariant = 'primary';
|
||||||
|
@Input() title?: string;
|
||||||
|
@Input() boldTitle = false;
|
||||||
|
@Input() showIcon = true;
|
||||||
|
@Input() dismissible = true;
|
||||||
|
@Input() autoDismiss = true;
|
||||||
|
@Input() duration = 5000; // 5 seconds
|
||||||
|
@Input() showProgress = true;
|
||||||
|
@Input() position: ToastPosition = 'top-right';
|
||||||
|
@Input() dismissLabel = 'Dismiss toast';
|
||||||
|
@Input() role = 'alert';
|
||||||
|
@Input() ariaLive: 'polite' | 'assertive' | 'off' = 'assertive';
|
||||||
|
@Input() actions: any[] = [];
|
||||||
|
|
||||||
|
@Output() dismissed = new EventEmitter<void>();
|
||||||
|
@Output() expired = new EventEmitter<void>();
|
||||||
|
@Output() shown = new EventEmitter<void>();
|
||||||
|
@Output() hidden = new EventEmitter<void>();
|
||||||
|
|
||||||
|
// Icons
|
||||||
|
readonly faCheckCircle = faCheckCircle;
|
||||||
|
readonly faExclamationTriangle = faExclamationTriangle;
|
||||||
|
readonly faExclamationCircle = faExclamationCircle;
|
||||||
|
readonly faInfoCircle = faInfoCircle;
|
||||||
|
readonly faTimes = faTimes;
|
||||||
|
|
||||||
|
// Generate unique ID for accessibility
|
||||||
|
readonly toastId = Math.random().toString(36).substr(2, 9);
|
||||||
|
|
||||||
|
// State management
|
||||||
|
isEntering = false;
|
||||||
|
isExiting = false;
|
||||||
|
private autoDismissTimeout?: any;
|
||||||
|
private enterTimeout?: any;
|
||||||
|
private exitTimeout?: any;
|
||||||
|
private elementRef = inject(ElementRef);
|
||||||
|
|
||||||
|
get toastIcon(): IconDefinition | null {
|
||||||
|
if (!this.showIcon) return null;
|
||||||
|
|
||||||
|
switch (this.variant) {
|
||||||
|
case 'success':
|
||||||
|
return this.faCheckCircle;
|
||||||
|
case 'warning':
|
||||||
|
return this.faExclamationTriangle;
|
||||||
|
case 'danger':
|
||||||
|
return this.faExclamationCircle;
|
||||||
|
case 'info':
|
||||||
|
return this.faInfoCircle;
|
||||||
|
case 'primary':
|
||||||
|
default:
|
||||||
|
return this.faInfoCircle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.clearTimeouts();
|
||||||
|
}
|
||||||
|
|
||||||
|
show(): void {
|
||||||
|
this.isEntering = true;
|
||||||
|
|
||||||
|
this.enterTimeout = setTimeout(() => {
|
||||||
|
this.isEntering = false;
|
||||||
|
this.shown.emit();
|
||||||
|
|
||||||
|
if (this.autoDismiss && this.duration > 0) {
|
||||||
|
this.startAutoDismissTimer();
|
||||||
|
}
|
||||||
|
}, 300); // Animation duration
|
||||||
|
}
|
||||||
|
|
||||||
|
hide(): void {
|
||||||
|
this.clearTimeouts();
|
||||||
|
this.isExiting = true;
|
||||||
|
|
||||||
|
this.exitTimeout = setTimeout(() => {
|
||||||
|
this.isExiting = false;
|
||||||
|
this.hidden.emit();
|
||||||
|
}, 300); // Animation duration
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDismiss(): void {
|
||||||
|
this.hide();
|
||||||
|
this.dismissed.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDismissKeydown(event: KeyboardEvent): void {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault();
|
||||||
|
this.handleDismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private startAutoDismissTimer(): void {
|
||||||
|
if (this.autoDismissTimeout) {
|
||||||
|
clearTimeout(this.autoDismissTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.autoDismissTimeout = setTimeout(() => {
|
||||||
|
this.hide();
|
||||||
|
this.expired.emit();
|
||||||
|
}, this.duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearTimeouts(): void {
|
||||||
|
if (this.autoDismissTimeout) {
|
||||||
|
clearTimeout(this.autoDismissTimeout);
|
||||||
|
this.autoDismissTimeout = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.enterTimeout) {
|
||||||
|
clearTimeout(this.enterTimeout);
|
||||||
|
this.enterTimeout = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.exitTimeout) {
|
||||||
|
clearTimeout(this.exitTimeout);
|
||||||
|
this.exitTimeout = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pause auto-dismiss on mouse enter
|
||||||
|
onMouseEnter(): void {
|
||||||
|
if (this.autoDismissTimeout) {
|
||||||
|
clearTimeout(this.autoDismissTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resume auto-dismiss on mouse leave
|
||||||
|
onMouseLeave(): void {
|
||||||
|
if (this.autoDismiss && this.duration > 0 && !this.isExiting) {
|
||||||
|
this.startAutoDismissTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user