Add comprehensive UI component expansion
- Add new components: enhanced-table, fab-menu, icon-button, snackbar, select, tag-input, bottom-navigation, stepper, command-palette, floating-toolbar, transfer-list - Refactor table-actions and select components - Update component styles and exports - Add corresponding demo components 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
@use '../../../../../shared-ui/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
padding-bottom: 120px; // Extra space to accommodate bottom navigation examples
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-lg;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&--vertical {
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
|
||||
h4 {
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
color: $semantic-color-text-primary;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
.demo-bottom-nav-container {
|
||||
position: relative;
|
||||
height: 80px;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
overflow: hidden;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
|
||||
// Override the fixed positioning for demo purposes
|
||||
:deep(.ui-bottom-navigation) {
|
||||
position: absolute !important;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-info {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
padding: $semantic-spacing-component-md;
|
||||
margin: $semantic-spacing-component-md 0;
|
||||
|
||||
p {
|
||||
margin: 0 0 $semantic-spacing-component-xs 0;
|
||||
color: $semantic-color-text-primary;
|
||||
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);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border: none;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
// Typography
|
||||
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);
|
||||
|
||||
&:hover {
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: $semantic-shadow-button-rest;
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: $semantic-color-secondary;
|
||||
color: $semantic-color-on-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
.demo-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import { faHome, faSearch, faUser, faHeart, faCog, faShoppingCart, faBell, faBookmark } from '@fortawesome/free-solid-svg-icons';
|
||||
import { BottomNavigationComponent, BottomNavigationItem } from '../../../../../ui-essentials/src/lib/components/navigation/bottom-navigation';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-bottom-navigation-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, BottomNavigationComponent, FontAwesomeModule],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Bottom Navigation Demo</h2>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Sizes</h3>
|
||||
<div class="demo-row demo-row--vertical">
|
||||
@for (size of sizes; track size) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ size.toUpperCase() }} Size</h4>
|
||||
<div class="demo-bottom-nav-container">
|
||||
<ui-bottom-navigation
|
||||
[size]="size"
|
||||
[items]="basicItems"
|
||||
[activeItemId]="activeItemIds[size]"
|
||||
(activeItemChanged)="handleActiveItemChange(size, $event)"
|
||||
(itemClicked)="handleItemClick($event)">
|
||||
|
||||
<fa-icon [icon]="faHome" data-icon="home"></fa-icon>
|
||||
<fa-icon [icon]="faSearch" data-icon="search"></fa-icon>
|
||||
<fa-icon [icon]="faUser" data-icon="profile"></fa-icon>
|
||||
<fa-icon [icon]="faCog" data-icon="settings"></fa-icon>
|
||||
</ui-bottom-navigation>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Variant Styles -->
|
||||
<section class="demo-section">
|
||||
<h3>Variants</h3>
|
||||
<div class="demo-row demo-row--vertical">
|
||||
@for (variant of variants; track variant) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ variant.charAt(0).toUpperCase() + variant.slice(1) }} Variant</h4>
|
||||
<div class="demo-bottom-nav-container">
|
||||
<ui-bottom-navigation
|
||||
[variant]="variant"
|
||||
[items]="basicItems"
|
||||
[activeItemId]="activeItemIds[variant]"
|
||||
(activeItemChanged)="handleActiveItemChange(variant, $event)"
|
||||
(itemClicked)="handleItemClick($event)">
|
||||
|
||||
<fa-icon [icon]="faHome" data-icon="home"></fa-icon>
|
||||
<fa-icon [icon]="faSearch" data-icon="search"></fa-icon>
|
||||
<fa-icon [icon]="faUser" data-icon="profile"></fa-icon>
|
||||
<fa-icon [icon]="faCog" data-icon="settings"></fa-icon>
|
||||
</ui-bottom-navigation>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- With Badges -->
|
||||
<section class="demo-section">
|
||||
<h3>With Badges</h3>
|
||||
<div class="demo-bottom-nav-container">
|
||||
<ui-bottom-navigation
|
||||
[items]="itemsWithBadges"
|
||||
[activeItemId]="badgeActiveItemId"
|
||||
(activeItemChanged)="badgeActiveItemId = $event"
|
||||
(itemClicked)="handleItemClick($event)">
|
||||
|
||||
<fa-icon [icon]="faHome" data-icon="home"></fa-icon>
|
||||
<fa-icon [icon]="faShoppingCart" data-icon="cart"></fa-icon>
|
||||
<fa-icon [icon]="faBell" data-icon="notifications"></fa-icon>
|
||||
<fa-icon [icon]="faBookmark" data-icon="saved"></fa-icon>
|
||||
<fa-icon [icon]="faUser" data-icon="profile"></fa-icon>
|
||||
</ui-bottom-navigation>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- States -->
|
||||
<section class="demo-section">
|
||||
<h3>States</h3>
|
||||
<div class="demo-row demo-row--vertical">
|
||||
<div class="demo-item">
|
||||
<h4>Elevated</h4>
|
||||
<div class="demo-bottom-nav-container">
|
||||
<ui-bottom-navigation
|
||||
[items]="basicItems"
|
||||
[elevated]="true"
|
||||
[activeItemId]="elevatedActiveItemId"
|
||||
(activeItemChanged)="elevatedActiveItemId = $event"
|
||||
(itemClicked)="handleItemClick($event)">
|
||||
|
||||
<fa-icon [icon]="faHome" data-icon="home"></fa-icon>
|
||||
<fa-icon [icon]="faSearch" data-icon="search"></fa-icon>
|
||||
<fa-icon [icon]="faUser" data-icon="profile"></fa-icon>
|
||||
<fa-icon [icon]="faCog" data-icon="settings"></fa-icon>
|
||||
</ui-bottom-navigation>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>With Disabled Item</h4>
|
||||
<div class="demo-bottom-nav-container">
|
||||
<ui-bottom-navigation
|
||||
[items]="itemsWithDisabled"
|
||||
[activeItemId]="disabledActiveItemId"
|
||||
(activeItemChanged)="disabledActiveItemId = $event"
|
||||
(itemClicked)="handleItemClick($event)">
|
||||
|
||||
<fa-icon [icon]="faHome" data-icon="home"></fa-icon>
|
||||
<fa-icon [icon]="faSearch" data-icon="search"></fa-icon>
|
||||
<fa-icon [icon]="faUser" data-icon="profile"></fa-icon>
|
||||
<fa-icon [icon]="faCog" data-icon="settings"></fa-icon>
|
||||
</ui-bottom-navigation>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Example</h3>
|
||||
<div class="demo-bottom-nav-container">
|
||||
<ui-bottom-navigation
|
||||
[items]="interactiveItems"
|
||||
[activeItemId]="interactiveActiveItemId"
|
||||
(activeItemChanged)="handleInteractiveItemChange($event)"
|
||||
(itemClicked)="handleInteractiveItemClick($event)">
|
||||
|
||||
<fa-icon [icon]="faHome" data-icon="home"></fa-icon>
|
||||
<fa-icon [icon]="faSearch" data-icon="search"></fa-icon>
|
||||
<fa-icon [icon]="faHeart" data-icon="favorites"></fa-icon>
|
||||
<fa-icon [icon]="faUser" data-icon="profile"></fa-icon>
|
||||
</ui-bottom-navigation>
|
||||
</div>
|
||||
|
||||
<div class="demo-info">
|
||||
<p><strong>Active Item:</strong> {{ getActiveItemLabel() }}</p>
|
||||
<p><strong>Click Count:</strong> {{ clickCount }}</p>
|
||||
<p><strong>Last Clicked:</strong> {{ lastClickedItem || 'None' }}</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-controls">
|
||||
<button
|
||||
class="demo-button"
|
||||
(click)="toggleHidden()"
|
||||
[class.active]="isHidden">
|
||||
{{ isHidden ? 'Show' : 'Hide' }} Navigation
|
||||
</button>
|
||||
<button
|
||||
class="demo-button"
|
||||
(click)="incrementBadge()">
|
||||
Increment Heart Badge
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './bottom-navigation-demo.component.scss'
|
||||
})
|
||||
export class BottomNavigationDemoComponent {
|
||||
// FontAwesome icons
|
||||
faHome = faHome;
|
||||
faSearch = faSearch;
|
||||
faUser = faUser;
|
||||
faHeart = faHeart;
|
||||
faCog = faCog;
|
||||
faShoppingCart = faShoppingCart;
|
||||
faBell = faBell;
|
||||
faBookmark = faBookmark;
|
||||
|
||||
// Demo data
|
||||
sizes = ['sm', 'md', 'lg'] as const;
|
||||
variants = ['default', 'primary', 'secondary'] as const;
|
||||
|
||||
// Active item tracking for different variants
|
||||
activeItemIds: Record<string, string> = {
|
||||
sm: 'home',
|
||||
md: 'home',
|
||||
lg: 'home',
|
||||
default: 'home',
|
||||
primary: 'home',
|
||||
secondary: 'home'
|
||||
};
|
||||
|
||||
badgeActiveItemId = 'home';
|
||||
elevatedActiveItemId = 'home';
|
||||
disabledActiveItemId = 'home';
|
||||
interactiveActiveItemId = 'home';
|
||||
|
||||
// Interactive demo state
|
||||
clickCount = 0;
|
||||
lastClickedItem = '';
|
||||
isHidden = false;
|
||||
heartBadgeCount = 3;
|
||||
|
||||
// Item configurations
|
||||
basicItems: BottomNavigationItem[] = [
|
||||
{ id: 'home', label: 'Home', icon: 'home' },
|
||||
{ id: 'search', label: 'Search', icon: 'search' },
|
||||
{ id: 'profile', label: 'Profile', icon: 'profile' },
|
||||
{ id: 'settings', label: 'Settings', icon: 'settings' }
|
||||
];
|
||||
|
||||
itemsWithBadges: BottomNavigationItem[] = [
|
||||
{ id: 'home', label: 'Home', icon: 'home' },
|
||||
{ id: 'cart', label: 'Cart', icon: 'cart', badge: 2 },
|
||||
{ id: 'notifications', label: 'Alerts', icon: 'notifications', badge: '99+' },
|
||||
{ id: 'saved', label: 'Saved', icon: 'saved', badge: 15 },
|
||||
{ id: 'profile', label: 'Profile', icon: 'profile' }
|
||||
];
|
||||
|
||||
itemsWithDisabled: BottomNavigationItem[] = [
|
||||
{ id: 'home', label: 'Home', icon: 'home' },
|
||||
{ id: 'search', label: 'Search', icon: 'search', disabled: true },
|
||||
{ id: 'profile', label: 'Profile', icon: 'profile' },
|
||||
{ id: 'settings', label: 'Settings', icon: 'settings' }
|
||||
];
|
||||
|
||||
get interactiveItems(): BottomNavigationItem[] {
|
||||
return [
|
||||
{ id: 'home', label: 'Home', icon: 'home' },
|
||||
{ id: 'search', label: 'Search', icon: 'search' },
|
||||
{ id: 'favorites', label: 'Favorites', icon: 'favorites', badge: this.heartBadgeCount > 0 ? this.heartBadgeCount : undefined },
|
||||
{ id: 'profile', label: 'Profile', icon: 'profile' }
|
||||
];
|
||||
}
|
||||
|
||||
handleActiveItemChange(context: string, itemId: string): void {
|
||||
this.activeItemIds[context] = itemId;
|
||||
}
|
||||
|
||||
handleItemClick(item: BottomNavigationItem): void {
|
||||
console.log('Item clicked:', item);
|
||||
}
|
||||
|
||||
handleInteractiveItemChange(itemId: string): void {
|
||||
this.interactiveActiveItemId = itemId;
|
||||
}
|
||||
|
||||
handleInteractiveItemClick(item: BottomNavigationItem): void {
|
||||
this.clickCount++;
|
||||
this.lastClickedItem = item.label;
|
||||
console.log('Interactive item clicked:', item);
|
||||
}
|
||||
|
||||
getActiveItemLabel(): string {
|
||||
const activeItem = this.interactiveItems.find(item => item.id === this.interactiveActiveItemId);
|
||||
return activeItem?.label || 'None';
|
||||
}
|
||||
|
||||
toggleHidden(): void {
|
||||
this.isHidden = !this.isHidden;
|
||||
// In a real implementation, you would update the hidden property of the navigation
|
||||
// For demo purposes, this just toggles a state variable
|
||||
}
|
||||
|
||||
incrementBadge(): void {
|
||||
this.heartBadgeCount++;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
@use '../../../../../shared-ui/src/styles/semantic' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-lg;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
text-align: center;
|
||||
margin-bottom: $semantic-spacing-layout-section-xl;
|
||||
}
|
||||
|
||||
.demo-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
margin: 0 0 $semantic-spacing-component-md 0;
|
||||
font-family: map-get($semantic-typography-heading-h2, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h2, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h2, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h2, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
.demo-title-icon {
|
||||
color: $semantic-color-primary;
|
||||
}
|
||||
|
||||
.demo-description {
|
||||
margin: 0;
|
||||
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: $semantic-color-text-secondary;
|
||||
|
||||
kbd {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px $semantic-spacing-component-xs;
|
||||
margin: 0 2px;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-xl;
|
||||
}
|
||||
|
||||
.demo-section-title {
|
||||
margin: 0 0 $semantic-spacing-component-lg 0;
|
||||
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;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
padding-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
.demo-actions {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-md;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
|
||||
border: $semantic-border-width-1 solid transparent;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
background: transparent;
|
||||
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;
|
||||
text-decoration: none;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border-color: $semantic-color-primary;
|
||||
|
||||
&:hover {
|
||||
background: darken($semantic-color-primary, 10%);
|
||||
border-color: darken($semantic-color-primary, 10%);
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
background: $semantic-color-secondary;
|
||||
color: $semantic-color-on-secondary;
|
||||
border-color: $semantic-color-secondary;
|
||||
|
||||
&:hover {
|
||||
background: darken($semantic-color-secondary, 10%);
|
||||
border-color: darken($semantic-color-secondary, 10%);
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&--outline {
|
||||
color: $semantic-color-text-primary;
|
||||
border-color: $semantic-color-border-primary;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-color: $semantic-color-border-secondary;
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-config-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: $semantic-spacing-component-lg;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.demo-config-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
.demo-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
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;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.demo-checkbox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: $semantic-color-primary;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.demo-input,
|
||||
.demo-select {
|
||||
padding: $semantic-spacing-interactive-input-padding-y $semantic-spacing-interactive-input-padding-x;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-input-radius;
|
||||
background: $semantic-color-surface-primary;
|
||||
font-family: map-get($semantic-typography-input, font-family);
|
||||
font-size: map-get($semantic-typography-input, font-size);
|
||||
font-weight: map-get($semantic-typography-input, font-weight);
|
||||
line-height: map-get($semantic-typography-input, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
transition: border-color $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $semantic-color-focus;
|
||||
box-shadow: 0 0 0 2px rgba($semantic-color-focus, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.demo-features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.demo-feature {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
background: $semantic-color-surface-primary;
|
||||
text-align: center;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
border-color: $semantic-color-primary;
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: $semantic-spacing-component-sm 0 $semantic-spacing-component-xs 0;
|
||||
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;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-feature-icon {
|
||||
font-size: 2rem;
|
||||
color: $semantic-color-primary;
|
||||
}
|
||||
|
||||
.demo-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.demo-stat {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
background: $semantic-color-surface-primary;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.demo-stat-value {
|
||||
font-family: map-get($semantic-typography-heading-h2, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h2, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h2, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h2, line-height);
|
||||
color: $semantic-color-primary;
|
||||
margin-bottom: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
.demo-stat-label {
|
||||
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);
|
||||
color: $semantic-color-text-secondary;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.demo-shortcuts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-shortcut {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
background: $semantic-color-surface-primary;
|
||||
|
||||
span {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-kbd {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 28px;
|
||||
height: 28px;
|
||||
padding: 0 $semantic-spacing-component-xs;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// Toast animation styles
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: calc(#{$semantic-breakpoint-md} - 1px)) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
|
||||
.demo-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.demo-config-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.demo-features {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.demo-stats {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.demo-shortcuts {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: calc(#{$semantic-breakpoint-sm} - 1px)) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.demo-title {
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
|
||||
.demo-title-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.demo-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,574 @@
|
||||
import { Component, OnInit, OnDestroy, ViewChild, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Router } from '@angular/router';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import {
|
||||
faHome,
|
||||
faCog,
|
||||
faUser,
|
||||
faSearch,
|
||||
faFile,
|
||||
faEdit,
|
||||
faEye,
|
||||
faTools,
|
||||
faQuestion,
|
||||
faPlus,
|
||||
faSave,
|
||||
faCopy,
|
||||
faPaste,
|
||||
faUndo,
|
||||
faRedo,
|
||||
faKeyboard,
|
||||
faPalette,
|
||||
faRocket
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { Command, CommandCategory, CommandExecutionContext, CommandPaletteComponent, CommandPaletteService, createShortcuts, GlobalKeyboardDirective } from '../../../../../ui-essentials/src/lib/components/overlays/command-palette';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'ui-command-palette-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FontAwesomeModule,
|
||||
CommandPaletteComponent,
|
||||
GlobalKeyboardDirective
|
||||
],
|
||||
template: `
|
||||
<div class="demo-container" uiGlobalKeyboard [shortcuts]="globalShortcuts" (shortcutTriggered)="handleGlobalShortcut($event)">
|
||||
<div class="demo-header">
|
||||
<h2 class="demo-title">
|
||||
<fa-icon [icon]="faPalette" class="demo-title-icon"></fa-icon>
|
||||
Command Palette Demo
|
||||
</h2>
|
||||
<p class="demo-description">
|
||||
A powerful command palette for quick navigation and actions. Press <kbd>Ctrl+K</kbd> (or <kbd>⌘K</kbd> on Mac) to open.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Quick Start Section -->
|
||||
<section class="demo-section">
|
||||
<h3 class="demo-section-title">Quick Start</h3>
|
||||
<div class="demo-actions">
|
||||
<button
|
||||
class="demo-button demo-button--primary"
|
||||
(click)="openCommandPalette()"
|
||||
>
|
||||
<fa-icon [icon]="faKeyboard"></fa-icon>
|
||||
Open Command Palette
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="demo-button demo-button--secondary"
|
||||
(click)="registerSampleCommands()"
|
||||
>
|
||||
<fa-icon [icon]="faPlus"></fa-icon>
|
||||
Add Sample Commands
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="demo-button demo-button--outline"
|
||||
(click)="clearCommands()"
|
||||
>
|
||||
Clear Commands
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Configuration Section -->
|
||||
<section class="demo-section">
|
||||
<h3 class="demo-section-title">Configuration</h3>
|
||||
<div class="demo-config-grid">
|
||||
<div class="demo-config-item">
|
||||
<label class="demo-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="config.showCategories"
|
||||
(change)="updateConfig('showCategories', $event)"
|
||||
class="demo-checkbox"
|
||||
/>
|
||||
Show Categories
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="demo-config-item">
|
||||
<label class="demo-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="config.showShortcuts"
|
||||
(change)="updateConfig('showShortcuts', $event)"
|
||||
class="demo-checkbox"
|
||||
/>
|
||||
Show Shortcuts
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="demo-config-item">
|
||||
<label class="demo-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="config.showRecent"
|
||||
(change)="updateConfig('showRecent', $event)"
|
||||
class="demo-checkbox"
|
||||
/>
|
||||
Show Recent Commands
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="demo-config-item">
|
||||
<label class="demo-label">
|
||||
Max Results:
|
||||
<input
|
||||
type="number"
|
||||
[value]="config.maxResults"
|
||||
(input)="updateConfig('maxResults', $event)"
|
||||
class="demo-input"
|
||||
min="1"
|
||||
max="50"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="demo-config-item">
|
||||
<label class="demo-label">
|
||||
Recent Limit:
|
||||
<input
|
||||
type="number"
|
||||
[value]="config.recentLimit"
|
||||
(input)="updateConfig('recentLimit', $event)"
|
||||
class="demo-input"
|
||||
min="1"
|
||||
max="20"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="demo-config-item">
|
||||
<label class="demo-label">
|
||||
Size:
|
||||
<select
|
||||
[value]="paletteSize"
|
||||
(change)="updatePaletteSize($event)"
|
||||
class="demo-select"
|
||||
>
|
||||
<option value="md">Medium</option>
|
||||
<option value="lg">Large</option>
|
||||
<option value="xl">Extra Large</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features Section -->
|
||||
<section class="demo-section">
|
||||
<h3 class="demo-section-title">Features</h3>
|
||||
<div class="demo-features">
|
||||
<div class="demo-feature">
|
||||
<fa-icon [icon]="faKeyboard" class="demo-feature-icon"></fa-icon>
|
||||
<h4>Keyboard Navigation</h4>
|
||||
<p>Full keyboard support with arrow keys, Enter, and Escape</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-feature">
|
||||
<fa-icon [icon]="faSearch" class="demo-feature-icon"></fa-icon>
|
||||
<h4>Fuzzy Search</h4>
|
||||
<p>Intelligent search with typo tolerance and keyword matching</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-feature">
|
||||
<fa-icon [icon]="faRocket" class="demo-feature-icon"></fa-icon>
|
||||
<h4>Quick Actions</h4>
|
||||
<p>Execute commands instantly with global keyboard shortcuts</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-feature">
|
||||
<fa-icon [icon]="faCog" class="demo-feature-icon"></fa-icon>
|
||||
<h4>Categorization</h4>
|
||||
<p>Organize commands by category for better discoverability</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-feature">
|
||||
<fa-icon [icon]="faCopy" class="demo-feature-icon"></fa-icon>
|
||||
<h4>Recent Commands</h4>
|
||||
<p>Track and show recently used commands for quick access</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-feature">
|
||||
<fa-icon [icon]="faEye" class="demo-feature-icon"></fa-icon>
|
||||
<h4>Context Aware</h4>
|
||||
<p>Commands can be shown or hidden based on application state</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Usage Statistics -->
|
||||
<section class="demo-section">
|
||||
<h3 class="demo-section-title">Usage Statistics</h3>
|
||||
<div class="demo-stats">
|
||||
<div class="demo-stat">
|
||||
<div class="demo-stat-value">{{ registeredCommandsCount }}</div>
|
||||
<div class="demo-stat-label">Registered Commands</div>
|
||||
</div>
|
||||
<div class="demo-stat">
|
||||
<div class="demo-stat-value">{{ recentCommandsCount }}</div>
|
||||
<div class="demo-stat-label">Recent Commands</div>
|
||||
</div>
|
||||
<div class="demo-stat">
|
||||
<div class="demo-stat-value">{{ executionCount }}</div>
|
||||
<div class="demo-stat-label">Commands Executed</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Keyboard Shortcuts Guide -->
|
||||
<section class="demo-section">
|
||||
<h3 class="demo-section-title">Keyboard Shortcuts</h3>
|
||||
<div class="demo-shortcuts">
|
||||
<div class="demo-shortcut">
|
||||
<kbd class="demo-kbd">Ctrl</kbd> + <kbd class="demo-kbd">K</kbd>
|
||||
<span>Open Command Palette</span>
|
||||
</div>
|
||||
<div class="demo-shortcut">
|
||||
<kbd class="demo-kbd">↑</kbd> / <kbd class="demo-kbd">↓</kbd>
|
||||
<span>Navigate Results</span>
|
||||
</div>
|
||||
<div class="demo-shortcut">
|
||||
<kbd class="demo-kbd">Enter</kbd>
|
||||
<span>Execute Selected Command</span>
|
||||
</div>
|
||||
<div class="demo-shortcut">
|
||||
<kbd class="demo-kbd">Esc</kbd>
|
||||
<span>Close Palette</span>
|
||||
</div>
|
||||
<div class="demo-shortcut">
|
||||
<kbd class="demo-kbd">Tab</kbd>
|
||||
<span>Navigate Results (Alternative)</span>
|
||||
</div>
|
||||
<div class="demo-shortcut">
|
||||
<kbd class="demo-kbd">/</kbd>
|
||||
<span>Quick Search</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Command Palette Component -->
|
||||
<ui-command-palette
|
||||
#commandPalette
|
||||
[size]="paletteSize"
|
||||
[showFooter]="true"
|
||||
[autoFocus]="true"
|
||||
(opened)="onPaletteOpened()"
|
||||
(closed)="onPaletteClosed()"
|
||||
(commandExecuted)="onCommandExecuted($event)"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './command-palette-demo.component.scss'
|
||||
})
|
||||
export class CommandPaletteDemoComponent implements OnInit, OnDestroy {
|
||||
@ViewChild('commandPalette') commandPalette!: CommandPaletteComponent;
|
||||
|
||||
private router = inject(Router);
|
||||
private commandService = inject(CommandPaletteService);
|
||||
|
||||
// Icons
|
||||
readonly faPalette = faPalette;
|
||||
readonly faKeyboard = faKeyboard;
|
||||
readonly faPlus = faPlus;
|
||||
readonly faSearch = faSearch;
|
||||
readonly faRocket = faRocket;
|
||||
readonly faCog = faCog;
|
||||
readonly faCopy = faCopy;
|
||||
readonly faEye = faEye;
|
||||
readonly faHome = faHome;
|
||||
readonly faUser = faUser;
|
||||
readonly faFile = faFile;
|
||||
readonly faEdit = faEdit;
|
||||
readonly faTools = faTools;
|
||||
readonly faQuestion = faQuestion;
|
||||
readonly faSave = faSave;
|
||||
readonly faUndo = faUndo;
|
||||
readonly faRedo = faRedo;
|
||||
readonly faPaste = faPaste;
|
||||
|
||||
// Component state
|
||||
paletteSize: 'md' | 'lg' | 'xl' = 'lg';
|
||||
executionCount = 0;
|
||||
|
||||
config = {
|
||||
showCategories: true,
|
||||
showShortcuts: true,
|
||||
showRecent: true,
|
||||
maxResults: 20,
|
||||
recentLimit: 5
|
||||
};
|
||||
|
||||
globalShortcuts = [
|
||||
createShortcuts().commandPalette(),
|
||||
createShortcuts().quickSearch(),
|
||||
createShortcuts().create('/', { preventDefault: true })
|
||||
];
|
||||
|
||||
get registeredCommandsCount(): number {
|
||||
return this.commandService.commands().length;
|
||||
}
|
||||
|
||||
get recentCommandsCount(): number {
|
||||
return this.commandService.recentCommands().length;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.registerSampleCommands();
|
||||
this.applyConfig();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Clean up registered commands
|
||||
this.commandService.clearCommands();
|
||||
}
|
||||
|
||||
openCommandPalette(): void {
|
||||
this.commandPalette.open();
|
||||
}
|
||||
|
||||
handleGlobalShortcut(event: any): void {
|
||||
const shortcut = event.shortcut;
|
||||
|
||||
if (shortcut.key === 'k' && (shortcut.ctrlKey || shortcut.metaKey)) {
|
||||
this.commandPalette.toggle();
|
||||
} else if (shortcut.key === '/') {
|
||||
this.commandPalette.open();
|
||||
}
|
||||
}
|
||||
|
||||
registerSampleCommands(): void {
|
||||
const sampleCommands: Command[] = [
|
||||
// Navigation Commands
|
||||
{
|
||||
id: 'nav-home',
|
||||
title: 'Go Home',
|
||||
description: 'Navigate to the home page',
|
||||
icon: faHome,
|
||||
category: CommandCategory.NAVIGATION,
|
||||
keywords: ['home', 'dashboard', 'main'],
|
||||
shortcut: ['Ctrl', 'H'],
|
||||
handler: () => this.showToast('Navigating to home...'),
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
id: 'nav-profile',
|
||||
title: 'View Profile',
|
||||
description: 'Go to user profile page',
|
||||
icon: faUser,
|
||||
category: CommandCategory.NAVIGATION,
|
||||
keywords: ['profile', 'user', 'account'],
|
||||
handler: () => this.showToast('Opening profile...'),
|
||||
order: 2
|
||||
},
|
||||
{
|
||||
id: 'nav-settings',
|
||||
title: 'Open Settings',
|
||||
description: 'Access application settings',
|
||||
icon: faCog,
|
||||
category: CommandCategory.SETTINGS,
|
||||
keywords: ['settings', 'preferences', 'config'],
|
||||
shortcut: ['Ctrl', ','],
|
||||
handler: () => this.showToast('Opening settings...'),
|
||||
order: 1
|
||||
},
|
||||
|
||||
// File Commands
|
||||
{
|
||||
id: 'file-new',
|
||||
title: 'New File',
|
||||
description: 'Create a new file',
|
||||
icon: faFile,
|
||||
category: CommandCategory.FILE,
|
||||
keywords: ['new', 'create', 'file', 'document'],
|
||||
shortcut: ['Ctrl', 'N'],
|
||||
handler: () => this.showToast('Creating new file...'),
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
id: 'file-save',
|
||||
title: 'Save File',
|
||||
description: 'Save the current file',
|
||||
icon: faSave,
|
||||
category: CommandCategory.FILE,
|
||||
keywords: ['save', 'file', 'document'],
|
||||
shortcut: ['Ctrl', 'S'],
|
||||
handler: () => this.showToast('Saving file...'),
|
||||
order: 2
|
||||
},
|
||||
|
||||
// Edit Commands
|
||||
{
|
||||
id: 'edit-copy',
|
||||
title: 'Copy',
|
||||
description: 'Copy selected content',
|
||||
icon: faCopy,
|
||||
category: CommandCategory.EDIT,
|
||||
keywords: ['copy', 'clipboard'],
|
||||
shortcut: ['Ctrl', 'C'],
|
||||
handler: () => this.showToast('Content copied!'),
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
id: 'edit-paste',
|
||||
title: 'Paste',
|
||||
description: 'Paste from clipboard',
|
||||
icon: faPaste,
|
||||
category: CommandCategory.EDIT,
|
||||
keywords: ['paste', 'clipboard'],
|
||||
shortcut: ['Ctrl', 'V'],
|
||||
handler: () => this.showToast('Content pasted!'),
|
||||
order: 2
|
||||
},
|
||||
{
|
||||
id: 'edit-undo',
|
||||
title: 'Undo',
|
||||
description: 'Undo last action',
|
||||
icon: faUndo,
|
||||
category: CommandCategory.EDIT,
|
||||
keywords: ['undo', 'revert'],
|
||||
shortcut: ['Ctrl', 'Z'],
|
||||
handler: () => this.showToast('Action undone!'),
|
||||
order: 3
|
||||
},
|
||||
{
|
||||
id: 'edit-redo',
|
||||
title: 'Redo',
|
||||
description: 'Redo last undone action',
|
||||
icon: faRedo,
|
||||
category: CommandCategory.EDIT,
|
||||
keywords: ['redo', 'repeat'],
|
||||
shortcut: ['Ctrl', 'Y'],
|
||||
handler: () => this.showToast('Action redone!'),
|
||||
order: 4
|
||||
},
|
||||
|
||||
// Search Commands
|
||||
{
|
||||
id: 'search-global',
|
||||
title: 'Global Search',
|
||||
description: 'Search across all content',
|
||||
icon: faSearch,
|
||||
category: CommandCategory.SEARCH,
|
||||
keywords: ['search', 'find', 'global'],
|
||||
shortcut: ['Ctrl', 'Shift', 'F'],
|
||||
handler: () => this.showToast('Opening global search...'),
|
||||
order: 1
|
||||
},
|
||||
|
||||
// Tools Commands
|
||||
{
|
||||
id: 'tools-theme',
|
||||
title: 'Toggle Theme',
|
||||
description: 'Switch between light and dark theme',
|
||||
icon: faTools,
|
||||
category: CommandCategory.TOOLS,
|
||||
keywords: ['theme', 'dark', 'light', 'toggle'],
|
||||
handler: () => this.showToast('Theme toggled!'),
|
||||
order: 1
|
||||
},
|
||||
|
||||
// Help Commands
|
||||
{
|
||||
id: 'help-docs',
|
||||
title: 'Documentation',
|
||||
description: 'Open help documentation',
|
||||
icon: faQuestion,
|
||||
category: CommandCategory.HELP,
|
||||
keywords: ['help', 'docs', 'documentation'],
|
||||
handler: () => this.showToast('Opening documentation...'),
|
||||
order: 1
|
||||
},
|
||||
|
||||
// Action Commands
|
||||
{
|
||||
id: 'action-refresh',
|
||||
title: 'Refresh Page',
|
||||
description: 'Reload the current page',
|
||||
icon: faRocket,
|
||||
category: CommandCategory.ACTIONS,
|
||||
keywords: ['refresh', 'reload', 'update'],
|
||||
shortcut: ['F5'],
|
||||
handler: () => this.showToast('Page refreshed!'),
|
||||
order: 1
|
||||
}
|
||||
];
|
||||
|
||||
this.commandService.registerCommands(sampleCommands);
|
||||
}
|
||||
|
||||
clearCommands(): void {
|
||||
this.commandService.clearCommands();
|
||||
this.commandService.clearRecentCommands();
|
||||
}
|
||||
|
||||
updateConfig(key: string, event: Event): void {
|
||||
const target = event.target as HTMLInputElement;
|
||||
let value: any = target.type === 'checkbox' ? target.checked :
|
||||
target.type === 'number' ? parseInt(target.value, 10) :
|
||||
target.value;
|
||||
|
||||
(this.config as any)[key] = value;
|
||||
this.applyConfig();
|
||||
}
|
||||
|
||||
updatePaletteSize(event: Event): void {
|
||||
const target = event.target as HTMLSelectElement;
|
||||
this.paletteSize = target.value as 'md' | 'lg' | 'xl';
|
||||
}
|
||||
|
||||
private applyConfig(): void {
|
||||
this.commandService.updateConfig({
|
||||
showCategories: this.config.showCategories,
|
||||
showShortcuts: this.config.showShortcuts,
|
||||
showRecent: this.config.showRecent,
|
||||
maxResults: this.config.maxResults,
|
||||
recentLimit: this.config.recentLimit
|
||||
});
|
||||
}
|
||||
|
||||
onPaletteOpened(): void {
|
||||
console.log('Command palette opened');
|
||||
}
|
||||
|
||||
onPaletteClosed(): void {
|
||||
console.log('Command palette closed');
|
||||
}
|
||||
|
||||
onCommandExecuted(event: { commandId: string; context: CommandExecutionContext }): void {
|
||||
this.executionCount++;
|
||||
console.log('Command executed:', event);
|
||||
}
|
||||
|
||||
private showToast(message: string): void {
|
||||
// Simple toast implementation for demo
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'demo-toast';
|
||||
toast.textContent = message;
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: #333;
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
z-index: 10000;
|
||||
font-size: 14px;
|
||||
animation: slideIn 0.3s ease;
|
||||
`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.animation = 'slideOut 0.3s ease forwards';
|
||||
setTimeout(() => document.body.removeChild(toast), 300);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
@@ -78,12 +78,10 @@
|
||||
margin: 0 auto; // Center the container
|
||||
position: relative; // For proper positioning context
|
||||
|
||||
// Override container component's auto margins that might be causing issues
|
||||
// Ensure proper centering
|
||||
&.ui-container {
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
left: auto !important;
|
||||
right: auto !important;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
&.demo-container-tall {
|
||||
@@ -187,13 +185,9 @@
|
||||
|
||||
// Additional container demo specific styles
|
||||
.demo-containers-showcase {
|
||||
// Force all containers to behave properly
|
||||
// Ensure containers behave properly
|
||||
.ui-container {
|
||||
// Ensure containers don't push beyond boundaries
|
||||
position: relative;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
// Size-specific constraints to prevent overflow
|
||||
|
||||
@@ -59,6 +59,8 @@ import { SplitButtonDemoComponent } from './split-button-demo/split-button-demo.
|
||||
import { CommandPaletteDemoComponent } from './command-palette-demo/command-palette-demo.component';
|
||||
import { TransferListDemoComponent } from './transfer-list-demo/transfer-list-demo.component';
|
||||
import { FloatingToolbarDemoComponent } from './floating-toolbar-demo/floating-toolbar-demo.component';
|
||||
import { TagInputDemoComponent } from './tag-input-demo/tag-input-demo.component';
|
||||
import { IconButtonDemoComponent } from './icon-button-demo/icon-button-demo.component';
|
||||
|
||||
|
||||
@Component({
|
||||
@@ -87,6 +89,10 @@ import { FloatingToolbarDemoComponent } from './floating-toolbar-demo/floating-t
|
||||
<ui-button-demo></ui-button-demo>
|
||||
}
|
||||
|
||||
@case ("icon-button") {
|
||||
<ui-icon-button-demo></ui-icon-button-demo>
|
||||
}
|
||||
|
||||
@case ("cards") {
|
||||
<ui-card-demo></ui-card-demo>
|
||||
}
|
||||
@@ -293,10 +299,14 @@ import { FloatingToolbarDemoComponent } from './floating-toolbar-demo/floating-t
|
||||
<ui-transfer-list-demo></ui-transfer-list-demo>
|
||||
}
|
||||
|
||||
@case ("tag-input") {
|
||||
<ui-tag-input-demo></ui-tag-input-demo>
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
`,
|
||||
imports: [AvatarDemoComponent, ButtonDemoComponent, CardDemoComponent,
|
||||
imports: [AvatarDemoComponent, ButtonDemoComponent, IconButtonDemoComponent, CardDemoComponent,
|
||||
ChipDemoComponent, TableDemoComponent, BadgeDemoComponent,
|
||||
MenuDemoComponent, InputDemoComponent,
|
||||
LayoutDemoComponent, RadioDemoComponent, CheckboxDemoComponent,
|
||||
@@ -308,7 +318,7 @@ import { FloatingToolbarDemoComponent } from './floating-toolbar-demo/floating-t
|
||||
SkeletonLoaderDemoComponent, EmptyStateDemoComponent, FileUploadDemoComponent, FormFieldDemoComponent,
|
||||
AutocompleteDemoComponent, BackdropDemoComponent, OverlayContainerDemoComponent, LoadingSpinnerDemoComponent,
|
||||
ProgressCircleDemoComponent, RangeSliderDemoComponent, ColorPickerDemoComponent, DividerDemoComponent, TooltipDemoComponent, AccordionDemoComponent,
|
||||
PopoverDemoComponent, AlertDemoComponent, SnackbarDemoComponent, ToastDemoComponent, TreeViewDemoComponent, TimelineDemoComponent, StepperDemoComponent, FabMenuDemoComponent, EnhancedTableDemoComponent, SplitButtonDemoComponent, CommandPaletteDemoComponent, FloatingToolbarDemoComponent, TransferListDemoComponent]
|
||||
PopoverDemoComponent, AlertDemoComponent, SnackbarDemoComponent, ToastDemoComponent, TreeViewDemoComponent, TimelineDemoComponent, StepperDemoComponent, FabMenuDemoComponent, EnhancedTableDemoComponent, SplitButtonDemoComponent, CommandPaletteDemoComponent, FloatingToolbarDemoComponent, TransferListDemoComponent, TagInputDemoComponent]
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
@use '../../../../../shared-ui/src/styles/semantic' as *;
|
||||
|
||||
.enhanced-table-demo {
|
||||
padding: $semantic-spacing-layout-section-lg;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
|
||||
h2 {
|
||||
font-family: map-get($semantic-typography-heading-h2, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h2, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h2, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h2, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
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-component-sm;
|
||||
}
|
||||
|
||||
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 Controls
|
||||
.demo-controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
margin-bottom: $semantic-spacing-layout-section-lg;
|
||||
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;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
}
|
||||
|
||||
.demo-control-group {
|
||||
h3 {
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
color: $semantic-color-text-primary;
|
||||
cursor: pointer;
|
||||
|
||||
input[type="checkbox"] {
|
||||
accent-color: $semantic-color-primary;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-input-radius;
|
||||
background: $semantic-color-surface-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $semantic-color-focus;
|
||||
box-shadow: 0 0 0 2px rgba($semantic-color-focus, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
color: $semantic-color-text-tertiary;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
|
||||
border: $semantic-border-width-1 solid $semantic-color-primary;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-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);
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
margin: $semantic-spacing-component-xs;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
background: $semantic-color-surface-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
border-color: $semantic-color-border-primary;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: $semantic-color-surface-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
&--small {
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
font-size: map-get($semantic-typography-button-small, font-size);
|
||||
}
|
||||
}
|
||||
|
||||
// Performance Metrics
|
||||
.performance-metrics {
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-input-radius;
|
||||
border-left: 4px solid $semantic-color-info;
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
|
||||
.metric-label {
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
color: $semantic-color-text-secondary;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
color: $semantic-color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
// Table Container
|
||||
.table-container {
|
||||
margin-bottom: $semantic-spacing-layout-section-lg;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
overflow: hidden;
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
}
|
||||
|
||||
// Cell Templates
|
||||
.salary-cell {
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
color: $semantic-color-success;
|
||||
}
|
||||
|
||||
.performance-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.performance-bar {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-full;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.performance-fill {
|
||||
height: 100%;
|
||||
border-radius: $semantic-border-radius-full;
|
||||
transition: width $semantic-motion-duration-normal $semantic-motion-easing-ease;
|
||||
|
||||
&.performance-excellent {
|
||||
background: $semantic-color-success;
|
||||
}
|
||||
|
||||
&.performance-good {
|
||||
background: $semantic-color-info;
|
||||
}
|
||||
|
||||
&.performance-average {
|
||||
background: $semantic-color-warning;
|
||||
}
|
||||
|
||||
&.performance-poor {
|
||||
background: $semantic-color-danger;
|
||||
}
|
||||
}
|
||||
|
||||
.performance-text {
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: $semantic-typography-font-weight-medium;
|
||||
min-width: 35px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
border: none;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: $semantic-typography-font-weight-medium;
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&--primary {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
|
||||
&:hover {
|
||||
opacity: $semantic-opacity-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&--danger {
|
||||
background: $semantic-color-danger;
|
||||
color: $semantic-color-on-danger;
|
||||
|
||||
&:hover {
|
||||
opacity: $semantic-opacity-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
// State Display
|
||||
.state-display {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: $semantic-spacing-grid-gap-md;
|
||||
margin-bottom: $semantic-spacing-layout-section-lg;
|
||||
}
|
||||
|
||||
.state-section {
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-input-radius;
|
||||
|
||||
h4 {
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.state-content {
|
||||
p {
|
||||
margin: 0;
|
||||
color: $semantic-color-text-tertiary;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.sort-item,
|
||||
.filter-item {
|
||||
padding: $semantic-spacing-component-xs 0;
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
color: $semantic-color-text-primary;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: $semantic-color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
// Feature Demo
|
||||
.feature-demo {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
border-left: 4px solid $semantic-color-primary;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
h4 {
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
margin-bottom: $semantic-spacing-component-xs;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: 1200px) {
|
||||
.enhanced-table-demo {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.state-display {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.performance-cell {
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
|
||||
.performance-bar {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
// Print styles
|
||||
@media print {
|
||||
.demo-controls,
|
||||
.performance-metrics,
|
||||
.state-display,
|
||||
.feature-demo {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,719 @@
|
||||
import { Component, ChangeDetectionStrategy, OnInit, TemplateRef, ViewChild, AfterViewInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import {
|
||||
EnhancedTableComponent,
|
||||
EnhancedTableColumn,
|
||||
EnhancedTableVariant,
|
||||
TableSort,
|
||||
TableFilter,
|
||||
EnhancedTableSortEvent,
|
||||
EnhancedTableFilterEvent,
|
||||
EnhancedTableColumnResizeEvent,
|
||||
EnhancedTableColumnReorderEvent,
|
||||
VirtualScrollConfig
|
||||
} from '../../../../../ui-essentials/src/lib/components/data-display/table/enhanced-table.component';
|
||||
import { StatusBadgeComponent } from '../../../../../ui-essentials/src/lib/components/feedback/status-badge.component';
|
||||
|
||||
interface Employee {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
department: string;
|
||||
position: string;
|
||||
salary: number;
|
||||
status: 'active' | 'inactive' | 'pending';
|
||||
joinDate: string;
|
||||
performance: number;
|
||||
location: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ui-enhanced-table-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
EnhancedTableComponent,
|
||||
StatusBadgeComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="padding: 2rem;">
|
||||
<h2>Enhanced Table Component Showcase</h2>
|
||||
<p style="margin-bottom: 2rem; color: #6c757d;">Enterprise-grade data table with virtual scrolling, column resizing/reordering, and advanced filtering.</p>
|
||||
|
||||
<!-- Basic Enhanced Table -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Basic Enhanced Table</h3>
|
||||
<ui-enhanced-table
|
||||
[data]="smallDataset"
|
||||
[columns]="basicColumns"
|
||||
variant="default"
|
||||
[virtualScrolling]="{ enabled: false, itemHeight: 48, buffer: 5 }"
|
||||
[showFilterRow]="true"
|
||||
[maxHeight]="'400px'"
|
||||
(sortChange)="handleSort($event)"
|
||||
(filterChange)="handleFilter($event)"
|
||||
(columnResize)="handleColumnResize($event)"
|
||||
(columnReorder)="handleColumnReorder($event)"
|
||||
(rowClick)="handleRowClick($event)">
|
||||
</ui-enhanced-table>
|
||||
</section>
|
||||
|
||||
<!-- Table Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Table Variants</h3>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Striped Enhanced Table</h4>
|
||||
<ui-enhanced-table
|
||||
[data]="smallDataset.slice(0, 5)"
|
||||
[columns]="basicColumns"
|
||||
variant="striped"
|
||||
[virtualScrolling]="{ enabled: false, itemHeight: 48, buffer: 5 }"
|
||||
[showFilterRow]="false"
|
||||
[maxHeight]="'300px'">
|
||||
</ui-enhanced-table>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Bordered Enhanced Table</h4>
|
||||
<ui-enhanced-table
|
||||
[data]="smallDataset.slice(0, 5)"
|
||||
[columns]="basicColumns"
|
||||
variant="bordered"
|
||||
[virtualScrolling]="{ enabled: false, itemHeight: 48, buffer: 5 }"
|
||||
[showFilterRow]="false"
|
||||
[maxHeight]="'300px'">
|
||||
</ui-enhanced-table>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Minimal Enhanced Table</h4>
|
||||
<ui-enhanced-table
|
||||
[data]="smallDataset.slice(0, 5)"
|
||||
[columns]="basicColumns"
|
||||
variant="minimal"
|
||||
[virtualScrolling]="{ enabled: false, itemHeight: 48, buffer: 5 }"
|
||||
[showFilterRow]="false"
|
||||
[maxHeight]="'300px'">
|
||||
</ui-enhanced-table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Virtual Scrolling Demo -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Virtual Scrolling ({{ largeDataset.length.toLocaleString() }} Records)</h3>
|
||||
<p style="margin-bottom: 1rem; color: #6c757d;">
|
||||
Virtual scrolling efficiently handles large datasets by only rendering visible rows.
|
||||
Performance metrics are displayed below the table.
|
||||
</p>
|
||||
|
||||
<ui-enhanced-table
|
||||
[data]="largeDataset"
|
||||
[columns]="performanceColumns"
|
||||
variant="default"
|
||||
[virtualScrolling]="virtualConfig"
|
||||
[showFilterRow]="true"
|
||||
[maxHeight]="'500px'"
|
||||
[cellTemplates]="cellTemplates"
|
||||
(sortChange)="handleSort($event)"
|
||||
(filterChange)="handleFilter($event)"
|
||||
(columnResize)="handleColumnResize($event)"
|
||||
(columnReorder)="handleColumnReorder($event)">
|
||||
</ui-enhanced-table>
|
||||
|
||||
@if (performanceMetrics) {
|
||||
<div style="margin-top: 1rem; padding: 1rem; background: #f8f9fa; border-radius: 8px; border-left: 4px solid #007bff;">
|
||||
<h4>Performance Metrics:</h4>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-top: 0.5rem;">
|
||||
<div><strong>Render Time:</strong> {{ performanceMetrics.renderTime }}ms</div>
|
||||
<div><strong>Sort Time:</strong> {{ performanceMetrics.sortTime }}ms</div>
|
||||
<div><strong>Filter Time:</strong> {{ performanceMetrics.filterTime }}ms</div>
|
||||
<div><strong>Visible Rows:</strong> {{ performanceMetrics.visibleRows }}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- Advanced Features -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Advanced Features with Custom Cell Templates</h3>
|
||||
<ui-enhanced-table
|
||||
[data]="smallDataset"
|
||||
[columns]="advancedColumns"
|
||||
variant="default"
|
||||
[virtualScrolling]="{ enabled: false, itemHeight: 48, buffer: 5 }"
|
||||
[showFilterRow]="true"
|
||||
[maxHeight]="'400px'"
|
||||
[cellTemplates]="cellTemplates"
|
||||
(sortChange)="handleSort($event)"
|
||||
(filterChange)="handleFilter($event)"
|
||||
(rowClick)="handleRowClick($event)">
|
||||
</ui-enhanced-table>
|
||||
|
||||
<!-- Templates -->
|
||||
<ng-template #statusTemplate let-value let-row="row">
|
||||
<ui-status-badge
|
||||
[variant]="getStatusVariant(value)"
|
||||
[dot]="true"
|
||||
size="medium">
|
||||
{{ value | titlecase }}
|
||||
</ui-status-badge>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #salaryTemplate let-value>
|
||||
<span style="font-weight: 600; color: #28a745;">
|
||||
{{ value | currency:'USD':'symbol':'1.0-0' }}
|
||||
</span>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #performanceTemplate let-value>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<div style="flex: 1; background: #e9ecef; border-radius: 4px; height: 8px; overflow: hidden;">
|
||||
<div
|
||||
[style.width.%]="value"
|
||||
[style.background-color]="getPerformanceColor(value)"
|
||||
style="height: 100%; transition: width 0.3s ease;">
|
||||
</div>
|
||||
</div>
|
||||
<span style="font-size: 0.875rem; min-width: 35px;">{{ value }}%</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #actionsTemplate let-row="row" let-index="index">
|
||||
<div style="display: flex; gap: 0.5rem; justify-content: center;">
|
||||
<button
|
||||
(click)="editEmployee(row)"
|
||||
style="padding: 0.25rem 0.5rem; border: 1px solid #007bff; background: #007bff; color: white; border-radius: 4px; cursor: pointer; font-size: 0.75rem;">
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
(click)="deleteEmployee(row.id)"
|
||||
style="padding: 0.25rem 0.5rem; border: 1px solid #dc3545; background: #dc3545; color: white; border-radius: 4px; cursor: pointer; font-size: 0.75rem;">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</section>
|
||||
|
||||
<!-- Column Configuration Demo -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Interactive Column Configuration</h3>
|
||||
<div style="display: flex; gap: 2rem; margin-bottom: 1.5rem; flex-wrap: wrap;">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<input type="checkbox" [(ngModel)]="enableResize" (change)="updateTableConfig()">
|
||||
Enable Column Resizing
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<input type="checkbox" [(ngModel)]="enableReorder" (change)="updateTableConfig()">
|
||||
Enable Column Reordering
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<input type="checkbox" [(ngModel)]="showTableFilters" (change)="updateTableConfig()">
|
||||
Show Filters
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<input type="checkbox" [(ngModel)]="enableVirtual" (change)="updateTableConfig()">
|
||||
Enable Virtual Scrolling
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<ui-enhanced-table
|
||||
[data]="configDataset"
|
||||
[columns]="configColumns"
|
||||
variant="default"
|
||||
[virtualScrolling]="configVirtualConfig"
|
||||
[showFilterRow]="showTableFilters"
|
||||
[maxHeight]="'400px'"
|
||||
(sortChange)="handleSort($event)"
|
||||
(filterChange)="handleFilter($event)"
|
||||
(columnResize)="handleColumnResize($event)"
|
||||
(columnReorder)="handleColumnReorder($event)">
|
||||
</ui-enhanced-table>
|
||||
</section>
|
||||
|
||||
<!-- Usage Examples -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Usage Examples</h3>
|
||||
<div style="background: #f8f9fa; padding: 1.5rem; border-radius: 8px; border-left: 4px solid #007bff;">
|
||||
<h4>Basic Enhanced Table:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-enhanced-table
|
||||
[data]="employees"
|
||||
[columns]="columns"
|
||||
variant="default"
|
||||
[enableColumnResize]="true"
|
||||
[enableColumnReorder]="true"
|
||||
[showFilters]="true"
|
||||
[height]="500"
|
||||
(sort)="handleSort($event)"
|
||||
(filter)="handleFilter($event)">
|
||||
</ui-enhanced-table></code></pre>
|
||||
|
||||
<h4>With Virtual Scrolling:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-enhanced-table
|
||||
[data]="largeDataset"
|
||||
[columns]="columns"
|
||||
[virtualScrollConfig]="{{ '{' }}
|
||||
enabled: true,
|
||||
itemHeight: 48,
|
||||
bufferSize: 10
|
||||
{{ '}' }}"
|
||||
[height]="600">
|
||||
</ui-enhanced-table></code></pre>
|
||||
|
||||
<h4>Column Configuration:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>columns: EnhancedTableColumn[] = [
|
||||
{{ '{' }}
|
||||
key: 'name',
|
||||
label: 'Name',
|
||||
width: 150,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
filterType: 'text',
|
||||
resizable: true,
|
||||
draggable: true
|
||||
{{ '}' }},
|
||||
{{ '{' }}
|
||||
key: 'salary',
|
||||
label: 'Salary',
|
||||
width: 120,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
filterType: 'number',
|
||||
resizable: true
|
||||
{{ '}' }}
|
||||
];</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Controls -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Interactive Demo Controls</h3>
|
||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<button (click)="addRandomEmployee()" style="padding: 0.5rem 1rem; border: 1px solid #28a745; background: #28a745; color: white; border-radius: 4px; cursor: pointer;">
|
||||
Add Random Employee
|
||||
</button>
|
||||
<button (click)="generateLargeDataset()" style="padding: 0.5rem 1rem; border: 1px solid #17a2b8; background: #17a2b8; color: white; border-radius: 4px; cursor: pointer;">
|
||||
Generate Large Dataset (10k)
|
||||
</button>
|
||||
<button (click)="clearAllEmployees()" style="padding: 0.5rem 1rem; border: 1px solid #dc3545; background: #dc3545; color: white; border-radius: 4px; cursor: pointer;">
|
||||
Clear All Data
|
||||
</button>
|
||||
<button (click)="resetDemo()" style="padding: 0.5rem 1rem; border: 1px solid #6c757d; background: #6c757d; color: white; border-radius: 4px; cursor: pointer;">
|
||||
Reset Demo
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (lastAction) {
|
||||
<div style="padding: 1rem; background: #d4edda; border: 1px solid #c3e6cb; border-radius: 4px; color: #155724;">
|
||||
<strong>Last Action:</strong> {{ lastAction }}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
h2 {
|
||||
color: hsl(279, 14%, 11%);
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 2px solid hsl(258, 100%, 47%);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: hsl(279, 14%, 25%);
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: hsl(287, 12%, 35%);
|
||||
font-size: 1.125rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
section {
|
||||
border: 1px solid hsl(289, 14%, 90%);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
background: hsl(286, 20%, 99%);
|
||||
}
|
||||
|
||||
pre {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: #d63384;
|
||||
}
|
||||
|
||||
label {
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class EnhancedTableDemoComponent implements OnInit, AfterViewInit {
|
||||
@ViewChild('statusTemplate') statusTemplate!: TemplateRef<any>;
|
||||
@ViewChild('salaryTemplate') salaryTemplate!: TemplateRef<any>;
|
||||
@ViewChild('performanceTemplate') performanceTemplate!: TemplateRef<any>;
|
||||
@ViewChild('actionsTemplate') actionsTemplate!: TemplateRef<any>;
|
||||
|
||||
// Demo state
|
||||
lastAction = '';
|
||||
performanceMetrics: any = null;
|
||||
|
||||
// Configuration
|
||||
enableResize = true;
|
||||
enableReorder = true;
|
||||
showTableFilters = true;
|
||||
enableVirtual = false;
|
||||
|
||||
// Cell templates
|
||||
cellTemplates: Record<string, TemplateRef<any>> = {};
|
||||
|
||||
// Virtual scroll config
|
||||
virtualConfig: VirtualScrollConfig = {
|
||||
enabled: true,
|
||||
itemHeight: 48,
|
||||
buffer: 10
|
||||
};
|
||||
|
||||
configVirtualConfig: VirtualScrollConfig = {
|
||||
enabled: false,
|
||||
itemHeight: 48,
|
||||
buffer: 10
|
||||
};
|
||||
|
||||
// Sample data
|
||||
smallDataset: Employee[] = [];
|
||||
largeDataset: Employee[] = [];
|
||||
configDataset: Employee[] = [];
|
||||
|
||||
// Column definitions
|
||||
basicColumns: EnhancedTableColumn[] = [
|
||||
{ key: 'name', label: 'Name', width: 150, sortable: true, filterable: true, filterType: 'text', resizable: true, draggable: true },
|
||||
{ key: 'email', label: 'Email', width: 200, sortable: true, filterable: true, filterType: 'text', resizable: true, draggable: true },
|
||||
{ key: 'department', label: 'Department', width: 140, sortable: true, filterable: true, filterType: 'select', filterOptions: this.getDepartmentOptions(), resizable: true, draggable: true },
|
||||
{ key: 'position', label: 'Position', width: 160, sortable: true, filterable: true, filterType: 'text', resizable: true, draggable: true },
|
||||
{ key: 'joinDate', label: 'Join Date', width: 120, sortable: true, filterable: true, filterType: 'date', resizable: true, draggable: true }
|
||||
];
|
||||
|
||||
performanceColumns: EnhancedTableColumn[] = [
|
||||
{ key: 'name', label: 'Name', width: 120, sortable: true, filterable: true, filterType: 'text', resizable: true, draggable: true, sticky: true },
|
||||
{ key: 'department', label: 'Department', width: 120, sortable: true, filterable: true, filterType: 'select', filterOptions: this.getDepartmentOptions(), resizable: true, draggable: true },
|
||||
{ key: 'position', label: 'Position', width: 140, sortable: true, filterable: true, filterType: 'text', resizable: true, draggable: true },
|
||||
{ key: 'salary', label: 'Salary', width: 100, sortable: true, filterable: true, filterType: 'number', resizable: true, draggable: true, align: 'right' },
|
||||
{ key: 'performance', label: 'Performance', width: 120, sortable: true, filterable: true, filterType: 'number', resizable: true, draggable: true },
|
||||
{ key: 'location', label: 'Location', width: 120, sortable: true, filterable: true, filterType: 'select', filterOptions: this.getLocationOptions(), resizable: true, draggable: true }
|
||||
];
|
||||
|
||||
advancedColumns: EnhancedTableColumn[] = [
|
||||
{ key: 'name', label: 'Name', width: 150, sortable: true, filterable: true, filterType: 'text', resizable: true, draggable: true },
|
||||
{ key: 'email', label: 'Email', width: 200, sortable: true, filterable: true, filterType: 'text', resizable: true, draggable: true },
|
||||
{ key: 'status', label: 'Status', width: 100, sortable: true, filterable: true, filterType: 'select', filterOptions: this.getStatusOptions(), resizable: true, draggable: true, align: 'center' },
|
||||
{ key: 'salary', label: 'Salary', width: 120, sortable: true, filterable: true, filterType: 'number', resizable: true, draggable: true, align: 'right' },
|
||||
{ key: 'performance', label: 'Performance', width: 140, sortable: true, filterable: true, filterType: 'number', resizable: true, draggable: true },
|
||||
{ key: 'actions', label: 'Actions', width: 100, resizable: true, align: 'center' }
|
||||
];
|
||||
|
||||
configColumns: EnhancedTableColumn[] = [
|
||||
{ key: 'name', label: 'Name', width: 150, sortable: true, filterable: true, filterType: 'text', resizable: true, draggable: true },
|
||||
{ key: 'department', label: 'Department', width: 140, sortable: true, filterable: true, filterType: 'select', filterOptions: this.getDepartmentOptions(), resizable: true, draggable: true },
|
||||
{ key: 'salary', label: 'Salary', width: 120, sortable: true, filterable: true, filterType: 'number', resizable: true, draggable: true, align: 'right' },
|
||||
{ key: 'joinDate', label: 'Join Date', width: 120, sortable: true, filterable: true, filterType: 'date', resizable: true, draggable: true }
|
||||
];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initializeData();
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
// Set up cell templates after view init
|
||||
setTimeout(() => {
|
||||
this.cellTemplates = {
|
||||
'status': this.statusTemplate,
|
||||
'salary': this.salaryTemplate,
|
||||
'performance': this.performanceTemplate,
|
||||
'actions': this.actionsTemplate
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
handleSort(event: EnhancedTableSortEvent): void {
|
||||
this.lastAction = `Sorted by ${event.sorting.map(s => `${s.column} (${s.direction})`).join(', ')}`;
|
||||
console.log('Sort event:', event);
|
||||
}
|
||||
|
||||
handleFilter(event: EnhancedTableFilterEvent): void {
|
||||
this.lastAction = `Filtered: ${event.filters.length} filters active`;
|
||||
console.log('Filter event:', event);
|
||||
}
|
||||
|
||||
handleColumnResize(event: EnhancedTableColumnResizeEvent): void {
|
||||
this.lastAction = `Resized column "${event.column}" to ${event.width}px`;
|
||||
console.log('Column resize event:', event);
|
||||
}
|
||||
|
||||
handleColumnReorder(event: EnhancedTableColumnReorderEvent): void {
|
||||
this.lastAction = `Reordered columns from index ${event.fromIndex} to ${event.toIndex}`;
|
||||
console.log('Column reorder event:', event);
|
||||
}
|
||||
|
||||
handleRowClick(event: {row: Employee, index: number}): void {
|
||||
this.lastAction = `Clicked row: ${event.row.name} (index: ${event.index})`;
|
||||
console.log('Row click:', event);
|
||||
}
|
||||
|
||||
editEmployee(employee: Employee): void {
|
||||
this.lastAction = `Editing employee: ${employee.name}`;
|
||||
console.log('Edit employee:', employee);
|
||||
}
|
||||
|
||||
// Configuration updates
|
||||
updateTableConfig(): void {
|
||||
this.configVirtualConfig = {
|
||||
...this.configVirtualConfig,
|
||||
enabled: this.enableVirtual
|
||||
};
|
||||
this.lastAction = `Updated table configuration`;
|
||||
}
|
||||
|
||||
// Template helpers
|
||||
getStatusVariant(status: string): 'success' | 'warning' | 'danger' | 'info' | 'neutral' | 'primary' | 'secondary' {
|
||||
switch (status?.toLowerCase()) {
|
||||
case 'active':
|
||||
return 'success';
|
||||
case 'pending':
|
||||
return 'warning';
|
||||
case 'inactive':
|
||||
return 'danger';
|
||||
default:
|
||||
return 'neutral';
|
||||
}
|
||||
}
|
||||
|
||||
getPerformanceColor(performance: number): string {
|
||||
if (performance >= 90) return '#28a745';
|
||||
if (performance >= 80) return '#17a2b8';
|
||||
if (performance >= 70) return '#ffc107';
|
||||
return '#dc3545';
|
||||
}
|
||||
|
||||
|
||||
// Filter options
|
||||
getDepartmentOptions() {
|
||||
return [
|
||||
{ label: 'Engineering', value: 'Engineering' },
|
||||
{ label: 'Marketing', value: 'Marketing' },
|
||||
{ label: 'Sales', value: 'Sales' },
|
||||
{ label: 'HR', value: 'HR' },
|
||||
{ label: 'Finance', value: 'Finance' }
|
||||
];
|
||||
}
|
||||
|
||||
getStatusOptions() {
|
||||
return [
|
||||
{ label: 'Active', value: 'active' },
|
||||
{ label: 'Inactive', value: 'inactive' },
|
||||
{ label: 'Pending', value: 'pending' }
|
||||
];
|
||||
}
|
||||
|
||||
getLocationOptions() {
|
||||
return [
|
||||
{ label: 'New York', value: 'New York' },
|
||||
{ label: 'San Francisco', value: 'San Francisco' },
|
||||
{ label: 'London', value: 'London' },
|
||||
{ label: 'Toronto', value: 'Toronto' },
|
||||
{ label: 'Sydney', value: 'Sydney' }
|
||||
];
|
||||
}
|
||||
|
||||
// Demo actions
|
||||
addRandomEmployee(): void {
|
||||
const names = ['John Doe', 'Jane Smith', 'Mike Johnson', 'Sarah Wilson', 'Tom Anderson', 'Lisa Brown', 'David Clark', 'Emma Davis'];
|
||||
const departments = ['Engineering', 'Marketing', 'Sales', 'HR', 'Finance'];
|
||||
const positions = ['Manager', 'Developer', 'Analyst', 'Coordinator', 'Specialist'];
|
||||
const statuses: ('active' | 'inactive' | 'pending')[] = ['active', 'inactive', 'pending'];
|
||||
const locations = ['New York', 'San Francisco', 'London', 'Toronto', 'Sydney'];
|
||||
|
||||
const newEmployee: Employee = {
|
||||
id: Math.max(...this.smallDataset.map(e => e.id), 0) + 1,
|
||||
name: names[Math.floor(Math.random() * names.length)],
|
||||
email: `user${Date.now()}@company.com`,
|
||||
department: departments[Math.floor(Math.random() * departments.length)],
|
||||
position: positions[Math.floor(Math.random() * positions.length)],
|
||||
salary: Math.floor(Math.random() * 100000) + 50000,
|
||||
status: statuses[Math.floor(Math.random() * statuses.length)],
|
||||
joinDate: new Date(2020 + Math.floor(Math.random() * 5), Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1).toISOString().split('T')[0],
|
||||
performance: Math.floor(Math.random() * 41) + 60,
|
||||
location: locations[Math.floor(Math.random() * locations.length)]
|
||||
};
|
||||
|
||||
this.smallDataset = [...this.smallDataset, newEmployee];
|
||||
this.configDataset = [...this.configDataset, newEmployee];
|
||||
this.lastAction = `Added new employee: ${newEmployee.name}`;
|
||||
}
|
||||
|
||||
generateLargeDataset(): void {
|
||||
this.largeDataset = this.createLargeDataset(10000);
|
||||
this.performanceMetrics = {
|
||||
renderTime: Math.floor(Math.random() * 50) + 10,
|
||||
sortTime: 0,
|
||||
filterTime: 0,
|
||||
visibleRows: Math.min(10, this.largeDataset.length)
|
||||
};
|
||||
this.lastAction = `Generated ${this.largeDataset.length.toLocaleString()} employee records`;
|
||||
}
|
||||
|
||||
clearAllEmployees(): void {
|
||||
this.smallDataset = [];
|
||||
this.largeDataset = [];
|
||||
this.configDataset = [];
|
||||
this.performanceMetrics = null;
|
||||
this.lastAction = 'Cleared all employee data';
|
||||
}
|
||||
|
||||
resetDemo(): void {
|
||||
this.initializeData();
|
||||
this.lastAction = 'Demo reset to initial state';
|
||||
}
|
||||
|
||||
// Data generation
|
||||
private initializeData(): void {
|
||||
this.smallDataset = this.createSmallDataset();
|
||||
this.configDataset = [...this.smallDataset];
|
||||
this.largeDataset = this.createLargeDataset(50000);
|
||||
this.performanceMetrics = {
|
||||
renderTime: Math.floor(Math.random() * 30) + 10,
|
||||
sortTime: 0,
|
||||
filterTime: 0,
|
||||
visibleRows: Math.min(10, this.largeDataset.length)
|
||||
};
|
||||
}
|
||||
|
||||
private createSmallDataset(): Employee[] {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Alice Johnson',
|
||||
email: 'alice.johnson@company.com',
|
||||
department: 'Engineering',
|
||||
position: 'Senior Developer',
|
||||
salary: 95000,
|
||||
status: 'active',
|
||||
joinDate: '2022-01-15',
|
||||
performance: 92,
|
||||
location: 'San Francisco'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Bob Smith',
|
||||
email: 'bob.smith@company.com',
|
||||
department: 'Marketing',
|
||||
position: 'Marketing Manager',
|
||||
salary: 78000,
|
||||
status: 'active',
|
||||
joinDate: '2021-06-10',
|
||||
performance: 85,
|
||||
location: 'New York'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Carol Williams',
|
||||
email: 'carol.williams@company.com',
|
||||
department: 'Sales',
|
||||
position: 'Sales Representative',
|
||||
salary: 65000,
|
||||
status: 'pending',
|
||||
joinDate: '2023-03-22',
|
||||
performance: 78,
|
||||
location: 'London'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'David Brown',
|
||||
email: 'david.brown@company.com',
|
||||
department: 'HR',
|
||||
position: 'HR Specialist',
|
||||
salary: 58000,
|
||||
status: 'active',
|
||||
joinDate: '2022-08-05',
|
||||
performance: 88,
|
||||
location: 'Toronto'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Eve Davis',
|
||||
email: 'eve.davis@company.com',
|
||||
department: 'Finance',
|
||||
position: 'Financial Analyst',
|
||||
salary: 72000,
|
||||
status: 'active',
|
||||
joinDate: '2021-11-18',
|
||||
performance: 91,
|
||||
location: 'Sydney'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Frank Miller',
|
||||
email: 'frank.miller@company.com',
|
||||
department: 'Engineering',
|
||||
position: 'DevOps Engineer',
|
||||
salary: 88000,
|
||||
status: 'inactive',
|
||||
joinDate: '2020-04-12',
|
||||
performance: 82,
|
||||
location: 'San Francisco'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private createLargeDataset(count: number): Employee[] {
|
||||
const dataset: Employee[] = [];
|
||||
const firstNames = ['Alice', 'Bob', 'Carol', 'David', 'Eve', 'Frank', 'Grace', 'Henry', 'Ivy', 'Jack'];
|
||||
const lastNames = ['Johnson', 'Smith', 'Williams', 'Brown', 'Davis', 'Miller', 'Wilson', 'Moore', 'Taylor', 'Anderson'];
|
||||
const departments = ['Engineering', 'Marketing', 'Sales', 'HR', 'Finance'];
|
||||
const positions = ['Manager', 'Senior Developer', 'Developer', 'Analyst', 'Coordinator', 'Specialist', 'Representative'];
|
||||
const statuses: ('active' | 'inactive' | 'pending')[] = ['active', 'inactive', 'pending'];
|
||||
const locations = ['New York', 'San Francisco', 'London', 'Toronto', 'Sydney'];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const firstName = firstNames[Math.floor(Math.random() * firstNames.length)];
|
||||
const lastName = lastNames[Math.floor(Math.random() * lastNames.length)];
|
||||
|
||||
dataset.push({
|
||||
id: i + 1000,
|
||||
name: `${firstName} ${lastName}`,
|
||||
email: `${firstName.toLowerCase()}.${lastName.toLowerCase()}@company.com`,
|
||||
department: departments[Math.floor(Math.random() * departments.length)],
|
||||
position: positions[Math.floor(Math.random() * positions.length)],
|
||||
salary: Math.floor(Math.random() * 100000) + 50000,
|
||||
status: statuses[Math.floor(Math.random() * statuses.length)],
|
||||
joinDate: new Date(2020 + Math.floor(Math.random() * 5), Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1).toISOString().split('T')[0],
|
||||
performance: Math.floor(Math.random() * 41) + 60,
|
||||
location: locations[Math.floor(Math.random() * locations.length)]
|
||||
});
|
||||
}
|
||||
|
||||
return dataset;
|
||||
}
|
||||
|
||||
deleteEmployee(id: number): void {
|
||||
this.smallDataset = this.smallDataset.filter(emp => emp.id !== id);
|
||||
this.configDataset = this.configDataset.filter(emp => emp.id !== id);
|
||||
this.lastAction = `Deleted employee with ID: ${id}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
@use '../../../../../shared-ui/src/styles/semantic' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-lg;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
h2 {
|
||||
font-family: map-get($semantic-typography-heading-h2, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h2, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h2, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h2, 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-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-lg;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-primary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
|
||||
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;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
padding-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
align-items: flex-start;
|
||||
|
||||
&--spaced {
|
||||
justify-content: space-around;
|
||||
|
||||
.demo-example {
|
||||
min-width: 200px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-example {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-md;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-input-radius;
|
||||
min-height: 120px;
|
||||
position: relative;
|
||||
|
||||
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: 0 0 $semantic-spacing-component-sm 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
|
||||
border: $semantic-border-width-1 solid $semantic-color-primary;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-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;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
transform: translateY(1px);
|
||||
box-shadow: $semantic-shadow-button-rest;
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
background: $semantic-color-surface-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
border-color: $semantic-color-border-primary;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: $semantic-color-surface-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-log {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-input-radius;
|
||||
padding: $semantic-spacing-component-md;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
|
||||
&__empty {
|
||||
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-tertiary;
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__entry {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
padding: $semantic-spacing-component-xs;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__time {
|
||||
font-family: $semantic-typography-font-family-mono;
|
||||
font-size: map-get($semantic-typography-caption, font-size);
|
||||
color: $semantic-color-text-tertiary;
|
||||
flex-shrink: 0;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
&__message {
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
&--spaced {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-example {
|
||||
min-height: auto;
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure positioned FAB menus are visible above demo content
|
||||
:host {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FabMenuComponent, FabMenuDirection, FabMenuItem, FabMenuPosition, FabMenuSize, FabMenuVariant } from '../../../../../ui-essentials/src/lib/components/buttons/fab-menu';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-fab-menu-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FabMenuComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Floating Action Button Menu Demo</h2>
|
||||
<p>An interactive floating action button menu that expands to reveal multiple action items.</p>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Sizes</h3>
|
||||
<div class="demo-row">
|
||||
@for (size of sizes; track size) {
|
||||
<div class="demo-example">
|
||||
<h4>{{ size | titlecase }}</h4>
|
||||
<ui-fab-menu
|
||||
[size]="size"
|
||||
[menuItems]="basicMenuItems"
|
||||
[triggerIcon]="plusIcon"
|
||||
[closeIcon]="closeIcon"
|
||||
triggerLabel="Open {{ size }} menu"
|
||||
(itemClicked)="handleItemClick($event)">
|
||||
</ui-fab-menu>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Color Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Color Variants</h3>
|
||||
<div class="demo-row">
|
||||
@for (variant of variants; track variant) {
|
||||
<div class="demo-example">
|
||||
<h4>{{ variant | titlecase }}</h4>
|
||||
<ui-fab-menu
|
||||
[variant]="variant"
|
||||
[menuItems]="getVariantMenuItems(variant)"
|
||||
[triggerIcon]="plusIcon"
|
||||
[closeIcon]="closeIcon"
|
||||
[triggerLabel]="variant + ' menu'"
|
||||
(itemClicked)="handleItemClick($event)">
|
||||
</ui-fab-menu>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Direction Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Direction Variants</h3>
|
||||
<div class="demo-row demo-row--spaced">
|
||||
@for (direction of directions; track direction) {
|
||||
<div class="demo-example">
|
||||
<h4>{{ direction | titlecase }}</h4>
|
||||
<ui-fab-menu
|
||||
[direction]="direction"
|
||||
[menuItems]="basicMenuItems"
|
||||
[triggerIcon]="plusIcon"
|
||||
[closeIcon]="closeIcon"
|
||||
[triggerLabel]="direction + ' direction menu'"
|
||||
(itemClicked)="handleItemClick($event)">
|
||||
</ui-fab-menu>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Position Variants (Fixed Positioning) -->
|
||||
<section class="demo-section">
|
||||
<h3>Fixed Positions</h3>
|
||||
<p>These examples show fixed positioning variants (click to activate):</p>
|
||||
<div class="demo-row">
|
||||
@for (position of positions; track position) {
|
||||
<button
|
||||
class="demo-button"
|
||||
(click)="togglePositionDemo(position)">
|
||||
Show {{ position | titlecase }} FAB Menu
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (activePositionDemo) {
|
||||
<ui-fab-menu
|
||||
[position]="activePositionDemo"
|
||||
[menuItems]="positionMenuItems"
|
||||
[triggerIcon]="menuIcon"
|
||||
[closeIcon]="closeIcon"
|
||||
[triggerLabel]="activePositionDemo + ' positioned menu'"
|
||||
(opened)="onMenuOpened()"
|
||||
(closed)="onMenuClosed()"
|
||||
(itemClicked)="handlePositionItemClick($event)">
|
||||
</ui-fab-menu>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- States -->
|
||||
<section class="demo-section">
|
||||
<h3>States</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-example">
|
||||
<h4>Disabled</h4>
|
||||
<ui-fab-menu
|
||||
[disabled]="true"
|
||||
[menuItems]="basicMenuItems"
|
||||
[triggerIcon]="plusIcon"
|
||||
triggerLabel="Disabled menu">
|
||||
</ui-fab-menu>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>No Backdrop</h4>
|
||||
<ui-fab-menu
|
||||
[backdrop]="false"
|
||||
[menuItems]="basicMenuItems"
|
||||
[triggerIcon]="plusIcon"
|
||||
[closeIcon]="closeIcon"
|
||||
triggerLabel="Menu without backdrop"
|
||||
(itemClicked)="handleItemClick($event)">
|
||||
</ui-fab-menu>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Stay Open</h4>
|
||||
<ui-fab-menu
|
||||
[closeOnItemClick]="false"
|
||||
[menuItems]="basicMenuItems"
|
||||
[triggerIcon]="plusIcon"
|
||||
[closeIcon]="closeIcon"
|
||||
triggerLabel="Menu stays open"
|
||||
(itemClicked)="handleItemClick($event)">
|
||||
</ui-fab-menu>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Advanced Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Advanced Examples</h3>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Mixed Item Variants</h4>
|
||||
<ui-fab-menu
|
||||
[menuItems]="mixedMenuItems"
|
||||
[triggerIcon]="settingsIcon"
|
||||
[closeIcon]="closeIcon"
|
||||
triggerLabel="Settings menu"
|
||||
(itemClicked)="handleItemClick($event)">
|
||||
</ui-fab-menu>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Large Menu with Icons</h4>
|
||||
<ui-fab-menu
|
||||
size="lg"
|
||||
[menuItems]="iconMenuItems"
|
||||
[triggerIcon]="appsIcon"
|
||||
[closeIcon]="closeIcon"
|
||||
triggerLabel="App launcher"
|
||||
(itemClicked)="handleItemClick($event)">
|
||||
</ui-fab-menu>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Feedback -->
|
||||
<section class="demo-section">
|
||||
<h3>Event Log</h3>
|
||||
<div class="demo-log">
|
||||
@if (eventLog.length === 0) {
|
||||
<p class="demo-log__empty">No events yet. Interact with the FAB menus above.</p>
|
||||
} @else {
|
||||
@for (event of eventLog.slice(-10); track $index) {
|
||||
<div class="demo-log__entry">
|
||||
<span class="demo-log__time">{{ event.timestamp | date:'HH:mm:ss' }}</span>
|
||||
<span class="demo-log__message">{{ event.message }}</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<button class="demo-button demo-button--secondary" (click)="clearLog()">Clear Log</button>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './fab-menu-demo.component.scss'
|
||||
})
|
||||
export class FabMenuDemoComponent {
|
||||
sizes: FabMenuSize[] = ['sm', 'md', 'lg'];
|
||||
variants: FabMenuVariant[] = ['primary', 'secondary', 'success', 'danger'];
|
||||
directions: FabMenuDirection[] = ['up', 'down', 'left', 'right'];
|
||||
positions: FabMenuPosition[] = ['bottom-right', 'bottom-left', 'top-right', 'top-left'];
|
||||
|
||||
activePositionDemo: FabMenuPosition | null = null;
|
||||
eventLog: Array<{timestamp: Date, message: string}> = [];
|
||||
|
||||
// Icons (using simple text for demo - could be replaced with FontAwesome or other icon library)
|
||||
plusIcon = '<span>+</span>';
|
||||
closeIcon = '<span>×</span>';
|
||||
menuIcon = '<span>☰</span>';
|
||||
settingsIcon = '<span>⚙</span>';
|
||||
appsIcon = '<span>⊞</span>';
|
||||
|
||||
basicMenuItems: FabMenuItem[] = [
|
||||
{ id: 'action1', label: 'Action 1', icon: '<span>📝</span>' },
|
||||
{ id: 'action2', label: 'Action 2', icon: '<span>📁</span>' },
|
||||
{ id: 'action3', label: 'Action 3', icon: '<span>📊</span>' }
|
||||
];
|
||||
|
||||
positionMenuItems: FabMenuItem[] = [
|
||||
{ id: 'close', label: 'Close Menu', icon: '<span>×</span>' },
|
||||
{ id: 'home', label: 'Go Home', icon: '<span>🏠</span>' },
|
||||
{ id: 'profile', label: 'Profile', icon: '<span>👤</span>' },
|
||||
{ id: 'settings', label: 'Settings', icon: '<span>⚙</span>' }
|
||||
];
|
||||
|
||||
mixedMenuItems: FabMenuItem[] = [
|
||||
{ id: 'save', label: 'Save', variant: 'success', icon: '<span>💾</span>' },
|
||||
{ id: 'edit', label: 'Edit', variant: 'primary', icon: '<span>✏️</span>' },
|
||||
{ id: 'delete', label: 'Delete', variant: 'danger', icon: '<span>🗑️</span>' },
|
||||
{ id: 'disabled', label: 'Disabled', disabled: true, icon: '<span>🚫</span>' }
|
||||
];
|
||||
|
||||
iconMenuItems: FabMenuItem[] = [
|
||||
{ id: 'email', label: 'Email', icon: '<span>📧</span>' },
|
||||
{ id: 'calendar', label: 'Calendar', icon: '<span>📅</span>' },
|
||||
{ id: 'documents', label: 'Documents', icon: '<span>📄</span>' },
|
||||
{ id: 'photos', label: 'Photos', icon: '<span>📸</span>' },
|
||||
{ id: 'music', label: 'Music', icon: '<span>🎵</span>' },
|
||||
{ id: 'videos', label: 'Videos', icon: '<span>🎬</span>' }
|
||||
];
|
||||
|
||||
getVariantMenuItems(variant: FabMenuVariant): FabMenuItem[] {
|
||||
return [
|
||||
{ id: `${variant}1`, label: `${variant} Item 1`, variant },
|
||||
{ id: `${variant}2`, label: `${variant} Item 2`, variant },
|
||||
{ id: `${variant}3`, label: `${variant} Item 3`, variant }
|
||||
];
|
||||
}
|
||||
|
||||
handleItemClick(event: {item: FabMenuItem, event: Event}): void {
|
||||
this.logEvent(`Item clicked: ${event.item.label} (${event.item.id})`);
|
||||
}
|
||||
|
||||
togglePositionDemo(position: FabMenuPosition): void {
|
||||
if (this.activePositionDemo === position) {
|
||||
this.activePositionDemo = null;
|
||||
this.logEvent(`Closed ${position} positioned menu`);
|
||||
} else {
|
||||
this.activePositionDemo = position;
|
||||
this.logEvent(`Opened ${position} positioned menu`);
|
||||
}
|
||||
}
|
||||
|
||||
handlePositionItemClick(event: {item: FabMenuItem, event: Event}): void {
|
||||
if (event.item.id === 'close') {
|
||||
this.activePositionDemo = null;
|
||||
this.logEvent('Closed positioned menu via menu item');
|
||||
} else {
|
||||
this.logEvent(`Position menu item clicked: ${event.item.label}`);
|
||||
}
|
||||
}
|
||||
|
||||
onMenuOpened(): void {
|
||||
this.logEvent('Menu opened');
|
||||
}
|
||||
|
||||
onMenuClosed(): void {
|
||||
this.logEvent('Menu closed');
|
||||
}
|
||||
|
||||
logEvent(message: string): void {
|
||||
this.eventLog.push({
|
||||
timestamp: new Date(),
|
||||
message
|
||||
});
|
||||
}
|
||||
|
||||
clearLog(): void {
|
||||
this.eventLog = [];
|
||||
this.logEvent('Event log cleared');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,509 @@
|
||||
@use '../../../../../shared-ui/src/styles/semantic/index' 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-layout-section-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-subsection {
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
|
||||
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-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-md;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
|
||||
&--center {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
.demo-trigger-button {
|
||||
padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border: none;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
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 {
|
||||
background: $semantic-color-primary;
|
||||
opacity: $semantic-opacity-hover;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
}
|
||||
|
||||
.demo-position-container {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
". top ."
|
||||
"left center right"
|
||||
". bottom .";
|
||||
gap: $semantic-spacing-component-lg;
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
}
|
||||
|
||||
.demo-position-button {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
background: $semantic-color-surface-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
font-family: map-get($semantic-typography-button-small, font-family);
|
||||
font-size: map-get($semantic-typography-button-small, font-size);
|
||||
font-weight: map-get($semantic-typography-button-small, font-weight);
|
||||
line-height: map-get($semantic-typography-button-small, line-height);
|
||||
|
||||
&--top {
|
||||
grid-area: top;
|
||||
}
|
||||
|
||||
&--bottom {
|
||||
grid-area: bottom;
|
||||
}
|
||||
|
||||
&--left {
|
||||
grid-area: left;
|
||||
}
|
||||
|
||||
&--right {
|
||||
grid-area: right;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-color: $semantic-color-border-primary;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-context-area {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-primary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
min-height: 120px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
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-primary;
|
||||
margin: 0 0 $semantic-spacing-content-paragraph 0;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 200px;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 0 auto $semantic-spacing-component-sm auto;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-color: $semantic-color-border-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-hint {
|
||||
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);
|
||||
color: $semantic-color-text-secondary;
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.demo-editor {
|
||||
min-height: 120px;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-primary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-input-radius;
|
||||
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;
|
||||
cursor: text;
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
border-color: $semantic-color-primary;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 $semantic-spacing-content-paragraph 0;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: $semantic-typography-font-weight-bold;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
u {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: $semantic-color-surface-primary;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
overflow: hidden;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
|
||||
text-align: left;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
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);
|
||||
}
|
||||
|
||||
th {
|
||||
background: $semantic-color-surface-secondary;
|
||||
color: $semantic-color-text-primary;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
td {
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-table-row {
|
||||
cursor: pointer;
|
||||
transition: background-color $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-elevated;
|
||||
}
|
||||
|
||||
&--selected {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
|
||||
td {
|
||||
color: $semantic-color-on-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-table-action {
|
||||
background: none;
|
||||
border: none;
|
||||
color: $semantic-color-text-secondary;
|
||||
font-size: $semantic-typography-font-size-lg;
|
||||
cursor: pointer;
|
||||
padding: $semantic-spacing-component-xs;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-elevated;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.demo-table-row--selected & {
|
||||
color: $semantic-color-on-primary;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-md;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
}
|
||||
|
||||
.demo-control-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $semantic-spacing-component-md;
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
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;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-output {
|
||||
margin-top: $semantic-spacing-layout-section-md;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
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: 0 0 $semantic-spacing-component-md 0;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-log {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background: $semantic-color-surface-primary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
|
||||
// Custom scrollbar styling
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: $semantic-color-border-subtle $semantic-color-surface-secondary;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-border-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-log-entry {
|
||||
font-family: $semantic-typography-font-family-mono;
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
color: $semantic-color-text-primary;
|
||||
padding: $semantic-spacing-component-xs 0;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
color: $semantic-color-success;
|
||||
font-weight: $semantic-typography-font-weight-medium;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-log-empty {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
color: $semantic-color-text-secondary;
|
||||
text-align: center;
|
||||
padding: $semantic-spacing-component-md;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-sm;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
.demo-position-container {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-control-group {
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
.demo-context-area {
|
||||
padding: $semantic-spacing-component-md;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.demo-editor {
|
||||
min-height: 100px;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
.demo-table {
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-position-container {
|
||||
grid-template-areas:
|
||||
"top"
|
||||
"left"
|
||||
"center"
|
||||
"right"
|
||||
"bottom";
|
||||
gap: $semantic-spacing-component-sm;
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-position-button {
|
||||
min-width: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
// High contrast support
|
||||
@media (prefers-contrast: high) {
|
||||
.demo-trigger-button,
|
||||
.demo-position-button,
|
||||
.demo-table,
|
||||
.demo-context-area,
|
||||
.demo-editor,
|
||||
.demo-controls,
|
||||
.demo-output,
|
||||
.demo-log {
|
||||
border-width: $semantic-border-width-2;
|
||||
}
|
||||
}
|
||||
|
||||
// Reduced motion support
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.demo-trigger-button,
|
||||
.demo-position-button,
|
||||
.demo-table-row,
|
||||
.demo-table-action,
|
||||
.demo-context-area {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,670 @@
|
||||
import { Component, ViewChild, AfterViewInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FloatingToolbarComponent, ToolbarAction, FloatingToolbarConfig } from '../../../../../ui-essentials/src/lib/components/overlays/floating-toolbar/floating-toolbar.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-floating-toolbar-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, FloatingToolbarComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Floating Toolbar Demo</h2>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Size Variants</h3>
|
||||
<div class="demo-row">
|
||||
@for (size of sizes; track size) {
|
||||
<div class="demo-item">
|
||||
<button
|
||||
class="demo-trigger-button"
|
||||
#triggerButton
|
||||
(click)="showToolbar('size-' + size, triggerButton, { size: size })"
|
||||
>
|
||||
{{ size }} Toolbar
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Variant Styles -->
|
||||
<section class="demo-section">
|
||||
<h3>Style Variants</h3>
|
||||
<div class="demo-row">
|
||||
@for (variant of variants; track variant) {
|
||||
<div class="demo-item">
|
||||
<button
|
||||
class="demo-trigger-button"
|
||||
#triggerButton
|
||||
(click)="showToolbar('variant-' + variant, triggerButton, { variant: variant })"
|
||||
>
|
||||
{{ variant | titlecase }} Style
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Position Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Position Variants</h3>
|
||||
<div class="demo-row demo-row--center">
|
||||
<div class="demo-position-container">
|
||||
@for (position of positions; track position) {
|
||||
<button
|
||||
class="demo-position-button demo-position-button--{{ position }}"
|
||||
#triggerButton
|
||||
(click)="showToolbar('position-' + position, triggerButton, { position: position })"
|
||||
>
|
||||
{{ position }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Context-Sensitive Actions -->
|
||||
<section class="demo-section">
|
||||
<h3>Context-Sensitive Actions</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-context-area" #textContext>
|
||||
<p>
|
||||
Select this text to see a text editing toolbar with cut, copy, paste, and formatting options.
|
||||
The toolbar will appear automatically when you make a selection.
|
||||
</p>
|
||||
</div>
|
||||
<div class="demo-context-area" #imageContext>
|
||||
<img
|
||||
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZGRkIi8+CiAgPHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzk5OSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPkltYWdlPC90ZXh0Pgo8L3N2Zz4K"
|
||||
alt="Demo Image"
|
||||
(contextmenu)="showContextualToolbar($event, imageActions, 'image')"
|
||||
/>
|
||||
<p class="demo-hint">Right-click the image for image editing options</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Auto-Hide Behavior -->
|
||||
<section class="demo-section">
|
||||
<h3>Auto-Hide Behavior</h3>
|
||||
<div class="demo-row">
|
||||
<button
|
||||
class="demo-trigger-button"
|
||||
#autoHideButton
|
||||
(click)="showAutoHideToolbar(autoHideButton)"
|
||||
>
|
||||
Show Auto-Hide Toolbar (3 seconds)
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Custom Actions Demo -->
|
||||
<section class="demo-section">
|
||||
<h3>Custom Actions</h3>
|
||||
<div class="demo-row">
|
||||
<button
|
||||
class="demo-trigger-button"
|
||||
#customButton
|
||||
(click)="showCustomActionsToolbar(customButton)"
|
||||
>
|
||||
Custom Actions Toolbar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="demo-output">
|
||||
<h4>Action Log:</h4>
|
||||
<div class="demo-log">
|
||||
@for (logEntry of actionLog; track $index) {
|
||||
<div class="demo-log-entry">{{ logEntry }}</div>
|
||||
}
|
||||
@if (actionLog.length === 0) {
|
||||
<div class="demo-log-empty">No actions performed yet...</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Examples</h3>
|
||||
|
||||
<!-- Editable Text Area -->
|
||||
<div class="demo-subsection">
|
||||
<h4>Editable Text with Formatting Toolbar</h4>
|
||||
<div
|
||||
class="demo-editor"
|
||||
contenteditable="true"
|
||||
#editableArea
|
||||
(mouseup)="handleTextSelection(editableArea)"
|
||||
(keyup)="handleTextSelection(editableArea)"
|
||||
>
|
||||
<p>This is an editable text area. Select text to see formatting options in the floating toolbar.</p>
|
||||
<p><strong>Bold text</strong>, <em>italic text</em>, and <u>underlined text</u> are supported.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Table with Row Actions -->
|
||||
<div class="demo-subsection">
|
||||
<h4>Data Table with Row Actions</h4>
|
||||
<table class="demo-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (user of sampleUsers; track user.id) {
|
||||
<tr
|
||||
#tableRow
|
||||
class="demo-table-row"
|
||||
[class.demo-table-row--selected]="selectedUserId === user.id"
|
||||
(click)="selectUser(user.id, tableRow)"
|
||||
>
|
||||
<td>{{ user.name }}</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>{{ user.status }}</td>
|
||||
<td>
|
||||
<button
|
||||
class="demo-table-action"
|
||||
(click)="showUserActions($event, tableRow, user)"
|
||||
>
|
||||
⋮
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Configuration Demo -->
|
||||
<section class="demo-section">
|
||||
<h3>Configuration Options</h3>
|
||||
<div class="demo-controls">
|
||||
<div class="demo-control-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
[(ngModel)]="demoConfig.backdrop"
|
||||
(change)="updateDemoConfig()"
|
||||
/>
|
||||
Show Backdrop
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
[(ngModel)]="demoConfig.autoHide"
|
||||
(change)="updateDemoConfig()"
|
||||
/>
|
||||
Auto Hide
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
[(ngModel)]="demoConfig.escapeClosable"
|
||||
(change)="updateDemoConfig()"
|
||||
/>
|
||||
ESC to Close
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="demo-trigger-button"
|
||||
#configButton
|
||||
(click)="showConfigurableToolbar(configButton)"
|
||||
>
|
||||
Test Configuration
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Floating Toolbars -->
|
||||
<ui-floating-toolbar
|
||||
#floatingToolbar
|
||||
[actions]="currentActions"
|
||||
[size]="getConfigValue('size')"
|
||||
[variant]="getConfigValue('variant')"
|
||||
[position]="getConfigValue('position')"
|
||||
[visible]="getConfigValue('visible')"
|
||||
[autoHide]="getConfigValue('autoHide')"
|
||||
[hideDelay]="getConfigValue('hideDelay')"
|
||||
[backdrop]="getConfigValue('backdrop')"
|
||||
[escapeClosable]="getConfigValue('escapeClosable')"
|
||||
[label]="getConfigValue('label')"
|
||||
[showLabel]="getConfigValue('showLabel')"
|
||||
[showShortcuts]="getConfigValue('showShortcuts')"
|
||||
(actionClicked)="onActionClicked($event)"
|
||||
(visibleChange)="onToolbarVisibilityChange($event)"
|
||||
(hidden)="onToolbarHidden()"
|
||||
></ui-floating-toolbar>
|
||||
|
||||
<!-- Text Selection Toolbar -->
|
||||
<ui-floating-toolbar
|
||||
#textSelectionToolbar
|
||||
[actions]="textActions"
|
||||
size="sm"
|
||||
variant="contextual"
|
||||
position="top"
|
||||
[visible]="false"
|
||||
trigger="selection"
|
||||
[autoHide]="false"
|
||||
label="Text Formatting"
|
||||
[showShortcuts]="true"
|
||||
(actionClicked)="onTextActionClicked($event)"
|
||||
></ui-floating-toolbar>
|
||||
`,
|
||||
styleUrl: './floating-toolbar-demo.component.scss'
|
||||
})
|
||||
export class FloatingToolbarDemoComponent implements AfterViewInit {
|
||||
@ViewChild('floatingToolbar') floatingToolbar!: FloatingToolbarComponent;
|
||||
@ViewChild('textSelectionToolbar') textSelectionToolbar!: FloatingToolbarComponent;
|
||||
|
||||
// Demo Data
|
||||
sizes = ['sm', 'md', 'lg'] as const;
|
||||
variants = ['default', 'elevated', 'floating', 'compact', 'contextual'] as const;
|
||||
positions = ['top', 'bottom', 'left', 'right'] as const;
|
||||
|
||||
sampleUsers = [
|
||||
{ id: 1, name: 'Alice Johnson', email: 'alice@example.com', status: 'Active' },
|
||||
{ id: 2, name: 'Bob Smith', email: 'bob@example.com', status: 'Pending' },
|
||||
{ id: 3, name: 'Carol Davis', email: 'carol@example.com', status: 'Inactive' },
|
||||
];
|
||||
|
||||
// Demo State
|
||||
actionLog: string[] = [];
|
||||
selectedUserId: number | null = null;
|
||||
currentConfig: FloatingToolbarConfig = {
|
||||
visible: false,
|
||||
size: 'md',
|
||||
variant: 'default',
|
||||
position: 'top',
|
||||
autoHide: false,
|
||||
hideDelay: 3000,
|
||||
backdrop: false,
|
||||
escapeClosable: true,
|
||||
label: '',
|
||||
showLabel: false,
|
||||
showShortcuts: false
|
||||
};
|
||||
|
||||
demoConfig = {
|
||||
backdrop: false,
|
||||
autoHide: false,
|
||||
escapeClosable: true
|
||||
};
|
||||
|
||||
currentActions: ToolbarAction[] = [];
|
||||
|
||||
// Action Sets
|
||||
basicActions: ToolbarAction[] = [
|
||||
{
|
||||
id: 'edit',
|
||||
label: 'Edit',
|
||||
icon: 'fas fa-edit',
|
||||
callback: (action, event) => this.logAction('Edit clicked')
|
||||
},
|
||||
{
|
||||
id: 'delete',
|
||||
label: 'Delete',
|
||||
icon: 'fas fa-trash',
|
||||
callback: (action, event) => this.logAction('Delete clicked')
|
||||
},
|
||||
{
|
||||
id: 'share',
|
||||
label: 'Share',
|
||||
icon: 'fas fa-share',
|
||||
callback: (action, event) => this.logAction('Share clicked')
|
||||
}
|
||||
];
|
||||
|
||||
textActions: ToolbarAction[] = [
|
||||
{
|
||||
id: 'cut',
|
||||
label: 'Cut',
|
||||
icon: 'fas fa-cut',
|
||||
shortcut: 'Ctrl+X',
|
||||
callback: (action, event) => this.executeTextAction('cut')
|
||||
},
|
||||
{
|
||||
id: 'copy',
|
||||
label: 'Copy',
|
||||
icon: 'fas fa-copy',
|
||||
shortcut: 'Ctrl+C',
|
||||
callback: (action, event) => this.executeTextAction('copy')
|
||||
},
|
||||
{
|
||||
id: 'paste',
|
||||
label: 'Paste',
|
||||
icon: 'fas fa-paste',
|
||||
shortcut: 'Ctrl+V',
|
||||
callback: (action, event) => this.executeTextAction('paste')
|
||||
},
|
||||
{
|
||||
id: 'divider1',
|
||||
label: '',
|
||||
divider: true
|
||||
},
|
||||
{
|
||||
id: 'bold',
|
||||
label: 'Bold',
|
||||
icon: 'fas fa-bold',
|
||||
shortcut: 'Ctrl+B',
|
||||
callback: (action, event) => this.executeTextAction('bold')
|
||||
},
|
||||
{
|
||||
id: 'italic',
|
||||
label: 'Italic',
|
||||
icon: 'fas fa-italic',
|
||||
shortcut: 'Ctrl+I',
|
||||
callback: (action, event) => this.executeTextAction('italic')
|
||||
},
|
||||
{
|
||||
id: 'underline',
|
||||
label: 'Underline',
|
||||
icon: 'fas fa-underline',
|
||||
shortcut: 'Ctrl+U',
|
||||
callback: (action, event) => this.executeTextAction('underline')
|
||||
}
|
||||
];
|
||||
|
||||
imageActions: ToolbarAction[] = [
|
||||
{
|
||||
id: 'resize',
|
||||
label: 'Resize',
|
||||
icon: 'fas fa-expand-arrows-alt',
|
||||
callback: (action, event) => this.logAction('Resize image')
|
||||
},
|
||||
{
|
||||
id: 'crop',
|
||||
label: 'Crop',
|
||||
icon: 'fas fa-crop',
|
||||
callback: (action, event) => this.logAction('Crop image')
|
||||
},
|
||||
{
|
||||
id: 'filters',
|
||||
label: 'Filters',
|
||||
icon: 'fas fa-filter',
|
||||
callback: (action, event) => this.logAction('Apply filters')
|
||||
},
|
||||
{
|
||||
id: 'divider2',
|
||||
label: '',
|
||||
divider: true
|
||||
},
|
||||
{
|
||||
id: 'download',
|
||||
label: 'Download',
|
||||
icon: 'fas fa-download',
|
||||
callback: (action, event) => this.logAction('Download image')
|
||||
}
|
||||
];
|
||||
|
||||
customActions: ToolbarAction[] = [
|
||||
{
|
||||
id: 'action1',
|
||||
label: 'Action 1',
|
||||
icon: 'fas fa-star',
|
||||
tooltip: 'This is action 1',
|
||||
callback: (action, event) => this.logAction('Custom Action 1 executed')
|
||||
},
|
||||
{
|
||||
id: 'action2',
|
||||
label: 'Action 2',
|
||||
icon: 'fas fa-heart',
|
||||
tooltip: 'This is action 2 with a longer tooltip',
|
||||
disabled: false,
|
||||
callback: (action, event) => this.logAction('Custom Action 2 executed')
|
||||
},
|
||||
{
|
||||
id: 'divider3',
|
||||
label: '',
|
||||
divider: true
|
||||
},
|
||||
{
|
||||
id: 'action3',
|
||||
label: 'Disabled Action',
|
||||
icon: 'fas fa-ban',
|
||||
disabled: true,
|
||||
tooltip: 'This action is disabled',
|
||||
callback: (action, event) => this.logAction('This should not execute')
|
||||
},
|
||||
{
|
||||
id: 'action4',
|
||||
label: 'Toggle Action',
|
||||
icon: 'fas fa-toggle-on',
|
||||
callback: (action, event) => this.toggleAction(action)
|
||||
}
|
||||
];
|
||||
|
||||
userActions: ToolbarAction[] = [
|
||||
{
|
||||
id: 'view',
|
||||
label: 'View Details',
|
||||
icon: 'fas fa-eye',
|
||||
callback: (action, event) => this.logAction('View user details')
|
||||
},
|
||||
{
|
||||
id: 'edit-user',
|
||||
label: 'Edit User',
|
||||
icon: 'fas fa-edit',
|
||||
callback: (action, event) => this.logAction('Edit user')
|
||||
},
|
||||
{
|
||||
id: 'delete-user',
|
||||
label: 'Delete User',
|
||||
icon: 'fas fa-trash',
|
||||
callback: (action, event) => this.logAction('Delete user')
|
||||
}
|
||||
];
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
// Setup text selection toolbar
|
||||
this.setupTextSelectionToolbar();
|
||||
}
|
||||
|
||||
showToolbar(id: string, triggerElement: HTMLElement, config: Partial<FloatingToolbarConfig>): void {
|
||||
console.log('Demo: showToolbar called', { id, config, basicActions: this.basicActions });
|
||||
this.currentConfig = {
|
||||
...this.currentConfig,
|
||||
...config,
|
||||
visible: true
|
||||
};
|
||||
this.currentActions = [...this.basicActions];
|
||||
console.log('Demo: config and actions set', {
|
||||
currentConfig: this.currentConfig,
|
||||
currentActions: this.currentActions
|
||||
});
|
||||
this.floatingToolbar.setAnchorElement(triggerElement);
|
||||
}
|
||||
|
||||
showAutoHideToolbar(triggerElement: HTMLElement): void {
|
||||
this.currentConfig = {
|
||||
...this.currentConfig,
|
||||
visible: true,
|
||||
autoHide: true,
|
||||
hideDelay: 3000,
|
||||
label: 'Auto-hide in 3 seconds'
|
||||
};
|
||||
this.currentActions = [...this.basicActions];
|
||||
this.floatingToolbar.setAnchorElement(triggerElement);
|
||||
}
|
||||
|
||||
showCustomActionsToolbar(triggerElement: HTMLElement): void {
|
||||
this.currentConfig = {
|
||||
...this.currentConfig,
|
||||
visible: true,
|
||||
autoHide: false,
|
||||
showShortcuts: true,
|
||||
label: 'Custom Actions'
|
||||
};
|
||||
this.currentActions = [...this.customActions];
|
||||
this.floatingToolbar.setAnchorElement(triggerElement);
|
||||
}
|
||||
|
||||
showContextualToolbar(event: MouseEvent, actions: ToolbarAction[], context: string): void {
|
||||
event.preventDefault();
|
||||
|
||||
// Create temporary anchor at mouse position
|
||||
const tempAnchor = {
|
||||
getBoundingClientRect: () => ({
|
||||
top: event.clientY,
|
||||
left: event.clientX,
|
||||
right: event.clientX,
|
||||
bottom: event.clientY,
|
||||
width: 0,
|
||||
height: 0
|
||||
} as DOMRect)
|
||||
} as HTMLElement;
|
||||
|
||||
this.currentConfig = {
|
||||
...this.currentConfig,
|
||||
visible: true,
|
||||
variant: 'contextual',
|
||||
position: 'bottom',
|
||||
autoHide: true,
|
||||
hideDelay: 5000,
|
||||
label: `${context} actions`
|
||||
};
|
||||
this.currentActions = [...actions];
|
||||
this.floatingToolbar.setAnchorElement(tempAnchor);
|
||||
}
|
||||
|
||||
showConfigurableToolbar(triggerElement: HTMLElement): void {
|
||||
this.currentConfig = {
|
||||
...this.currentConfig,
|
||||
...this.demoConfig,
|
||||
visible: true,
|
||||
label: 'Configured Toolbar'
|
||||
};
|
||||
this.currentActions = [...this.basicActions];
|
||||
this.floatingToolbar.setAnchorElement(triggerElement);
|
||||
}
|
||||
|
||||
updateDemoConfig(): void {
|
||||
// Update the demo configuration
|
||||
Object.assign(this.currentConfig, this.demoConfig);
|
||||
}
|
||||
|
||||
handleTextSelection(element: HTMLElement): void {
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.toString().trim()) {
|
||||
const range = selection.getRangeAt(0);
|
||||
const rect = range.getBoundingClientRect();
|
||||
|
||||
if (rect.width > 0 && rect.height > 0) {
|
||||
// Create anchor at selection
|
||||
const selectionAnchor = {
|
||||
getBoundingClientRect: () => rect
|
||||
} as HTMLElement;
|
||||
|
||||
this.textSelectionToolbar.setAnchorElement(selectionAnchor);
|
||||
this.textSelectionToolbar.show();
|
||||
}
|
||||
} else {
|
||||
this.textSelectionToolbar.hide();
|
||||
}
|
||||
}
|
||||
|
||||
selectUser(userId: number, rowElement: HTMLElement): void {
|
||||
this.selectedUserId = userId;
|
||||
}
|
||||
|
||||
showUserActions(event: Event, rowElement: HTMLElement, user: any): void {
|
||||
event.stopPropagation();
|
||||
|
||||
this.currentConfig = {
|
||||
...this.currentConfig,
|
||||
visible: true,
|
||||
variant: 'elevated',
|
||||
position: 'left',
|
||||
label: `Actions for ${user.name}`
|
||||
};
|
||||
this.currentActions = [...this.userActions];
|
||||
this.floatingToolbar.setAnchorElement(rowElement);
|
||||
}
|
||||
|
||||
onActionClicked(event: { action: ToolbarAction; event: Event }): void {
|
||||
this.logAction(`Action clicked: ${event.action.label}`);
|
||||
}
|
||||
|
||||
onTextActionClicked(event: { action: ToolbarAction; event: Event }): void {
|
||||
this.logAction(`Text action: ${event.action.label}`);
|
||||
}
|
||||
|
||||
onToolbarVisibilityChange(visible: boolean): void {
|
||||
this.currentConfig.visible = visible;
|
||||
}
|
||||
|
||||
onToolbarHidden(): void {
|
||||
this.logAction('Toolbar hidden');
|
||||
}
|
||||
|
||||
executeTextAction(command: string): void {
|
||||
try {
|
||||
document.execCommand(command, false);
|
||||
this.logAction(`Text formatting: ${command}`);
|
||||
} catch (error) {
|
||||
this.logAction(`Text formatting failed: ${command}`);
|
||||
}
|
||||
}
|
||||
|
||||
toggleAction(action: ToolbarAction): void {
|
||||
const isToggled = action.icon === 'fas fa-toggle-on';
|
||||
action.icon = isToggled ? 'fas fa-toggle-off' : 'fas fa-toggle-on';
|
||||
this.logAction(`Toggle action: ${isToggled ? 'OFF' : 'ON'}`);
|
||||
}
|
||||
|
||||
private setupTextSelectionToolbar(): void {
|
||||
document.addEventListener('selectionchange', () => {
|
||||
const selection = window.getSelection();
|
||||
if (!selection || !selection.toString().trim()) {
|
||||
this.textSelectionToolbar.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getConfigValue<K extends keyof FloatingToolbarConfig>(key: K): NonNullable<FloatingToolbarConfig[K]> {
|
||||
const defaultValues: Required<FloatingToolbarConfig> = {
|
||||
size: 'md',
|
||||
variant: 'default',
|
||||
position: 'top',
|
||||
autoPosition: true,
|
||||
visible: false,
|
||||
actions: [],
|
||||
context: undefined as any,
|
||||
trigger: 'manual',
|
||||
autoHide: false,
|
||||
hideDelay: 3000,
|
||||
showAnimation: 'slide-fade',
|
||||
backdrop: false,
|
||||
backdropClosable: true,
|
||||
escapeClosable: true,
|
||||
offset: 12,
|
||||
label: '',
|
||||
showLabel: false,
|
||||
showShortcuts: false
|
||||
};
|
||||
|
||||
const value = this.currentConfig[key];
|
||||
return (value !== undefined ? value : defaultValues[key]) as NonNullable<FloatingToolbarConfig[K]>;
|
||||
}
|
||||
|
||||
private logAction(message: string): void {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
this.actionLog.unshift(`[${timestamp}] ${message}`);
|
||||
|
||||
// Keep only the last 10 entries
|
||||
if (this.actionLog.length > 10) {
|
||||
this.actionLog = this.actionLog.slice(0, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Demo-specific styles for the icon button showcase
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
// Danger button styling for destructive actions demo
|
||||
::ng-deep .danger-button {
|
||||
&.ui-icon-button--outlined {
|
||||
color: #dc3545;
|
||||
border-color: #dc3545;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: rgba(220, 53, 69, 0.1);
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline-color: #dc3545;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { IconButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons/icon-button/icon-button.component';
|
||||
import {
|
||||
faHeart,
|
||||
faBookmark,
|
||||
faShare,
|
||||
faDownload,
|
||||
faEdit,
|
||||
faTrash,
|
||||
faSave,
|
||||
faSearch,
|
||||
faPlus,
|
||||
faMinus,
|
||||
faHome,
|
||||
faUser,
|
||||
faCog,
|
||||
faClose,
|
||||
faCheck,
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faEllipsisV,
|
||||
faStar
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-icon-button-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
IconButtonComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="padding: 2rem;">
|
||||
<h2>Icon Button Component Showcase</h2>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Size Variants</h3>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;">
|
||||
<ui-icon-button
|
||||
[icon]="faHeart"
|
||||
size="small"
|
||||
ariaLabel="Like (small)"
|
||||
(clicked)="handleClick('small heart')">
|
||||
</ui-icon-button>
|
||||
<ui-icon-button
|
||||
[icon]="faHeart"
|
||||
size="medium"
|
||||
ariaLabel="Like (medium)"
|
||||
(clicked)="handleClick('medium heart')">
|
||||
</ui-icon-button>
|
||||
<ui-icon-button
|
||||
[icon]="faHeart"
|
||||
size="large"
|
||||
ariaLabel="Like (large)"
|
||||
(clicked)="handleClick('large heart')">
|
||||
</ui-icon-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Filled Variant -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Filled Variant</h3>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<ui-icon-button
|
||||
[icon]="faHeart"
|
||||
variant="filled"
|
||||
ariaLabel="Like"
|
||||
title="Add to favorites"
|
||||
(clicked)="handleClick('filled heart')">
|
||||
</ui-icon-button>
|
||||
<ui-icon-button
|
||||
[icon]="faBookmark"
|
||||
variant="filled"
|
||||
ariaLabel="Bookmark"
|
||||
title="Save for later"
|
||||
(clicked)="handleClick('filled bookmark')">
|
||||
</ui-icon-button>
|
||||
<ui-icon-button
|
||||
[icon]="faShare"
|
||||
variant="filled"
|
||||
ariaLabel="Share"
|
||||
title="Share this item"
|
||||
(clicked)="handleClick('filled share')">
|
||||
</ui-icon-button>
|
||||
<ui-icon-button
|
||||
[icon]="faDownload"
|
||||
variant="filled"
|
||||
ariaLabel="Download"
|
||||
title="Download file"
|
||||
(clicked)="handleClick('filled download')">
|
||||
</ui-icon-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tonal Variant -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Tonal Variant</h3>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<ui-icon-button
|
||||
[icon]="faEdit"
|
||||
variant="tonal"
|
||||
ariaLabel="Edit"
|
||||
title="Edit item"
|
||||
(clicked)="handleClick('tonal edit')">
|
||||
</ui-icon-button>
|
||||
<ui-icon-button
|
||||
[icon]="faSave"
|
||||
variant="tonal"
|
||||
ariaLabel="Save"
|
||||
title="Save changes"
|
||||
(clicked)="handleClick('tonal save')">
|
||||
</ui-icon-button>
|
||||
<ui-icon-button
|
||||
[icon]="faSearch"
|
||||
variant="tonal"
|
||||
ariaLabel="Search"
|
||||
title="Search items"
|
||||
(clicked)="handleClick('tonal search')">
|
||||
</ui-icon-button>
|
||||
<ui-icon-button
|
||||
[icon]="faCog"
|
||||
variant="tonal"
|
||||
ariaLabel="Settings"
|
||||
title="Open settings"
|
||||
(clicked)="handleClick('tonal settings')">
|
||||
</ui-icon-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Outlined Variant -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Outlined Variant</h3>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<ui-icon-button
|
||||
[icon]="faPlus"
|
||||
variant="outlined"
|
||||
ariaLabel="Add"
|
||||
title="Add new item"
|
||||
(clicked)="handleClick('outlined add')">
|
||||
</ui-icon-button>
|
||||
<ui-icon-button
|
||||
[icon]="faMinus"
|
||||
variant="outlined"
|
||||
ariaLabel="Remove"
|
||||
title="Remove item"
|
||||
(clicked)="handleClick('outlined remove')">
|
||||
</ui-icon-button>
|
||||
<ui-icon-button
|
||||
[icon]="faHome"
|
||||
variant="outlined"
|
||||
ariaLabel="Home"
|
||||
title="Go home"
|
||||
(clicked)="handleClick('outlined home')">
|
||||
</ui-icon-button>
|
||||
<ui-icon-button
|
||||
[icon]="faUser"
|
||||
variant="outlined"
|
||||
ariaLabel="Profile"
|
||||
title="View profile"
|
||||
(clicked)="handleClick('outlined profile')">
|
||||
</ui-icon-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- States -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>States</h3>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<ui-icon-button
|
||||
[icon]="faHeart"
|
||||
variant="filled"
|
||||
[disabled]="true"
|
||||
ariaLabel="Disabled button">
|
||||
</ui-icon-button>
|
||||
<ui-icon-button
|
||||
[icon]="faDownload"
|
||||
variant="filled"
|
||||
[loading]="true"
|
||||
ariaLabel="Loading button">
|
||||
</ui-icon-button>
|
||||
<ui-icon-button
|
||||
[icon]="faStar"
|
||||
variant="tonal"
|
||||
[pressed]="isPressed"
|
||||
ariaLabel="Toggle star"
|
||||
title="Toggle favorite"
|
||||
(clicked)="togglePressed()">
|
||||
</ui-icon-button>
|
||||
</div>
|
||||
<p style="margin-top: 0.5rem; color: #666; font-size: 0.9rem;">
|
||||
Star button pressed state: {{ isPressed ? 'ON' : 'OFF' }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- Navigation Icons -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Navigation Examples</h3>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;">
|
||||
<ui-icon-button
|
||||
[icon]="faChevronLeft"
|
||||
variant="outlined"
|
||||
ariaLabel="Previous"
|
||||
title="Go to previous page"
|
||||
(clicked)="handleClick('previous')">
|
||||
</ui-icon-button>
|
||||
<ui-icon-button
|
||||
[icon]="faChevronRight"
|
||||
variant="outlined"
|
||||
ariaLabel="Next"
|
||||
title="Go to next page"
|
||||
(clicked)="handleClick('next')">
|
||||
</ui-icon-button>
|
||||
<ui-icon-button
|
||||
[icon]="faClose"
|
||||
variant="tonal"
|
||||
ariaLabel="Close"
|
||||
title="Close dialog"
|
||||
(clicked)="handleClick('close')">
|
||||
</ui-icon-button>
|
||||
<ui-icon-button
|
||||
[icon]="faCheck"
|
||||
variant="filled"
|
||||
ariaLabel="Confirm"
|
||||
title="Confirm action"
|
||||
(clicked)="handleClick('confirm')">
|
||||
</ui-icon-button>
|
||||
<ui-icon-button
|
||||
[icon]="faEllipsisV"
|
||||
variant="tonal"
|
||||
ariaLabel="More options"
|
||||
title="Show more options"
|
||||
(clicked)="handleClick('more')">
|
||||
</ui-icon-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Destructive Actions -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Destructive Actions</h3>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;">
|
||||
<ui-icon-button
|
||||
[icon]="faTrash"
|
||||
variant="outlined"
|
||||
ariaLabel="Delete"
|
||||
title="Delete item (destructive action)"
|
||||
class="danger-button"
|
||||
(clicked)="handleClick('delete')">
|
||||
</ui-icon-button>
|
||||
</div>
|
||||
<p style="margin-top: 0.5rem; color: #666; font-size: 0.9rem;">
|
||||
Use outlined variant for destructive actions to reduce accidental clicks
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- Click Counter -->
|
||||
<section style="margin-bottom: 2rem;">
|
||||
<h3>Interaction Test</h3>
|
||||
<p>Total clicks: {{ clickCount }}</p>
|
||||
<p>Last clicked: {{ lastClicked || 'None' }}</p>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './icon-button-demo.component.scss'
|
||||
})
|
||||
export class IconButtonDemoComponent {
|
||||
// FontAwesome icons
|
||||
faHeart = faHeart;
|
||||
faBookmark = faBookmark;
|
||||
faShare = faShare;
|
||||
faDownload = faDownload;
|
||||
faEdit = faEdit;
|
||||
faTrash = faTrash;
|
||||
faSave = faSave;
|
||||
faSearch = faSearch;
|
||||
faPlus = faPlus;
|
||||
faMinus = faMinus;
|
||||
faHome = faHome;
|
||||
faUser = faUser;
|
||||
faCog = faCog;
|
||||
faClose = faClose;
|
||||
faCheck = faCheck;
|
||||
faChevronLeft = faChevronLeft;
|
||||
faChevronRight = faChevronRight;
|
||||
faEllipsisV = faEllipsisV;
|
||||
faStar = faStar;
|
||||
|
||||
// Demo state
|
||||
clickCount = 0;
|
||||
lastClicked = '';
|
||||
isPressed = false;
|
||||
|
||||
handleClick(action: string): void {
|
||||
this.clickCount++;
|
||||
this.lastClicked = action;
|
||||
console.log('Icon button clicked:', action);
|
||||
}
|
||||
|
||||
togglePressed(): void {
|
||||
this.isPressed = !this.isPressed;
|
||||
this.handleClick(`toggle star ${this.isPressed ? 'on' : 'off'}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
@use '../../../../../shared-ui/src/styles/semantic/index' 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;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-md;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
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;
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-secondary;
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border-color: $semantic-color-primary;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-primary;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Static snackbars for demo purposes
|
||||
.demo-row ui-snackbar {
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SnackbarAction, SnackbarComponent } from '../../../../../ui-essentials/src/lib/components/feedback/snackbar/snackbar.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-snackbar-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, SnackbarComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Snackbar Demo</h2>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Sizes</h3>
|
||||
<div class="demo-row" style="gap: 16px; flex-direction: column; align-items: flex-start;">
|
||||
@for (size of sizes; track size) {
|
||||
<ui-snackbar
|
||||
[size]="size"
|
||||
[message]="size + ' snackbar - This is a brief message'"
|
||||
[autoDismiss]="false"
|
||||
[position]="'bottom-left'"
|
||||
style="position: relative; bottom: auto; left: auto; transform: none; margin: 0;">
|
||||
</ui-snackbar>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Color Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Variants</h3>
|
||||
<div class="demo-row" style="gap: 16px; flex-direction: column; align-items: flex-start;">
|
||||
@for (variant of variants; track variant) {
|
||||
<ui-snackbar
|
||||
[variant]="variant"
|
||||
[message]="variant + ' - This is a ' + variant + ' snackbar message'"
|
||||
[showIcon]="variant !== 'default'"
|
||||
[autoDismiss]="false"
|
||||
[position]="'bottom-left'"
|
||||
style="position: relative; bottom: auto; left: auto; transform: none; margin: 0;">
|
||||
</ui-snackbar>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- With Actions -->
|
||||
<section class="demo-section">
|
||||
<h3>With Actions</h3>
|
||||
<div class="demo-row" style="gap: 16px; flex-direction: column; align-items: flex-start;">
|
||||
<ui-snackbar
|
||||
message="Your file was saved successfully"
|
||||
variant="success"
|
||||
[showIcon]="true"
|
||||
[actions]="singleAction"
|
||||
[autoDismiss]="false"
|
||||
[position]="'bottom-left'"
|
||||
style="position: relative; bottom: auto; left: auto; transform: none; margin: 0;">
|
||||
</ui-snackbar>
|
||||
|
||||
<ui-snackbar
|
||||
message="Connection lost. Check your network and try again."
|
||||
variant="danger"
|
||||
[showIcon]="true"
|
||||
[actions]="multipleActions"
|
||||
[autoDismiss]="false"
|
||||
[position]="'bottom-left'"
|
||||
style="position: relative; bottom: auto; left: auto; transform: none; margin: 0;">
|
||||
</ui-snackbar>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Position Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Positions</h3>
|
||||
<div class="demo-row" style="gap: 8px;">
|
||||
@for (position of positions; track position) {
|
||||
<button
|
||||
class="demo-button"
|
||||
(click)="showPositionDemo(position)">
|
||||
{{ position }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<p>Click buttons to see snackbars in different positions</p>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Examples</h3>
|
||||
<div class="demo-row" style="gap: 8px;">
|
||||
<button
|
||||
class="demo-button demo-button--primary"
|
||||
(click)="showSuccessSnackbar()">
|
||||
Show Success
|
||||
</button>
|
||||
<button
|
||||
class="demo-button demo-button--warning"
|
||||
(click)="showWarningSnackbar()">
|
||||
Show Warning
|
||||
</button>
|
||||
<button
|
||||
class="demo-button demo-button--danger"
|
||||
(click)="showErrorSnackbar()">
|
||||
Show Error
|
||||
</button>
|
||||
<button
|
||||
class="demo-button"
|
||||
(click)="showActionSnackbar()">
|
||||
Show with Action
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Dynamic Snackbars -->
|
||||
@for (snackbar of activeSnackbars; track snackbar.id) {
|
||||
<ui-snackbar
|
||||
[size]="snackbar.size"
|
||||
[variant]="snackbar.variant"
|
||||
[message]="snackbar.message"
|
||||
[showIcon]="snackbar.showIcon"
|
||||
[actions]="snackbar.actions"
|
||||
[position]="snackbar.position"
|
||||
[autoDismiss]="snackbar.autoDismiss"
|
||||
[duration]="snackbar.duration"
|
||||
(dismissed)="removeSnackbar(snackbar.id)"
|
||||
(expired)="removeSnackbar(snackbar.id)"
|
||||
(actionClicked)="handleActionClick($event, snackbar.id)">
|
||||
</ui-snackbar>
|
||||
}
|
||||
|
||||
<!-- Status -->
|
||||
<section class="demo-section">
|
||||
<h3>Demo Status</h3>
|
||||
<p>Active snackbars: {{ activeSnackbars.length }}</p>
|
||||
<p>Total shown: {{ totalShown }}</p>
|
||||
<p>Last action: {{ lastAction || 'None' }}</p>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './snackbar-demo.component.scss'
|
||||
})
|
||||
export class SnackbarDemoComponent {
|
||||
sizes = ['sm', 'md', 'lg'] as const;
|
||||
variants = ['default', 'primary', 'success', 'warning', 'danger', 'info'] as const;
|
||||
positions = [
|
||||
'bottom-center', 'bottom-left', 'bottom-right',
|
||||
'top-center', 'top-left', 'top-right'
|
||||
] as const;
|
||||
|
||||
activeSnackbars: any[] = [];
|
||||
totalShown = 0;
|
||||
lastAction = '';
|
||||
|
||||
singleAction: SnackbarAction[] = [
|
||||
{ label: 'UNDO', handler: () => this.handleDemoAction('Undo clicked') }
|
||||
];
|
||||
|
||||
multipleActions: SnackbarAction[] = [
|
||||
{ label: 'RETRY', handler: () => this.handleDemoAction('Retry clicked') },
|
||||
{ label: 'SETTINGS', handler: () => this.handleDemoAction('Settings clicked') }
|
||||
];
|
||||
|
||||
showPositionDemo(position: string): void {
|
||||
this.addSnackbar({
|
||||
message: `Snackbar at ${position}`,
|
||||
variant: 'primary',
|
||||
position: position as any,
|
||||
showIcon: true,
|
||||
autoDismiss: true,
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
|
||||
showSuccessSnackbar(): void {
|
||||
this.addSnackbar({
|
||||
message: 'Operation completed successfully!',
|
||||
variant: 'success',
|
||||
showIcon: true,
|
||||
actions: this.singleAction
|
||||
});
|
||||
}
|
||||
|
||||
showWarningSnackbar(): void {
|
||||
this.addSnackbar({
|
||||
message: 'Warning: This action cannot be undone',
|
||||
variant: 'warning',
|
||||
showIcon: true,
|
||||
autoDismiss: false
|
||||
});
|
||||
}
|
||||
|
||||
showErrorSnackbar(): void {
|
||||
this.addSnackbar({
|
||||
message: 'Error: Failed to save changes',
|
||||
variant: 'danger',
|
||||
showIcon: true,
|
||||
actions: this.multipleActions,
|
||||
autoDismiss: false
|
||||
});
|
||||
}
|
||||
|
||||
showActionSnackbar(): void {
|
||||
this.addSnackbar({
|
||||
message: 'File moved to trash',
|
||||
variant: 'default',
|
||||
actions: [
|
||||
{
|
||||
label: 'UNDO',
|
||||
handler: () => {
|
||||
this.handleDemoAction('File restored');
|
||||
this.removeSnackbar(this.activeSnackbars[this.activeSnackbars.length - 1]?.id);
|
||||
}
|
||||
}
|
||||
],
|
||||
duration: 6000
|
||||
});
|
||||
}
|
||||
|
||||
private addSnackbar(config: any): void {
|
||||
const snackbar = {
|
||||
id: Date.now() + Math.random(),
|
||||
size: 'md',
|
||||
position: 'bottom-center',
|
||||
autoDismiss: true,
|
||||
duration: 4000,
|
||||
showIcon: false,
|
||||
actions: [],
|
||||
...config
|
||||
};
|
||||
|
||||
this.activeSnackbars.push(snackbar);
|
||||
this.totalShown++;
|
||||
}
|
||||
|
||||
removeSnackbar(id: number): void {
|
||||
this.activeSnackbars = this.activeSnackbars.filter(s => s.id !== id);
|
||||
}
|
||||
|
||||
handleActionClick(action: SnackbarAction, snackbarId: number): void {
|
||||
this.lastAction = `${action.label} clicked on snackbar ${snackbarId}`;
|
||||
}
|
||||
|
||||
private handleDemoAction(action: string): void {
|
||||
this.lastAction = action;
|
||||
}
|
||||
}
|
||||
@@ -10,10 +10,10 @@
|
||||
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);
|
||||
font-family: $semantic-typography-font-family-sans;
|
||||
font-size: $semantic-typography-font-size-lg;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
line-height: $semantic-typography-line-height-tight;
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
@@ -36,10 +36,10 @@
|
||||
border-radius: $semantic-border-radius-md;
|
||||
|
||||
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);
|
||||
font-family: $semantic-typography-font-family-sans;
|
||||
font-size: $semantic-typography-font-size-md;
|
||||
font-weight: $semantic-typography-font-weight-normal;
|
||||
line-height: $semantic-typography-line-height-normal;
|
||||
color: $semantic-color-text-primary;
|
||||
margin: $semantic-spacing-component-xs 0;
|
||||
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
@use '../../../../../shared-ui/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-sm;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
|
||||
h2 {
|
||||
font-family: map-get($semantic-typography-heading-h2, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h2, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h2, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h2, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin: 0 0 $semantic-spacing-content-heading 0;
|
||||
}
|
||||
|
||||
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: 0 0 $semantic-spacing-component-lg 0;
|
||||
border-bottom: 1px solid $semantic-color-border-primary;
|
||||
padding-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
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-secondary;
|
||||
margin: 0 0 $semantic-spacing-component-md 0;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-subsection {
|
||||
margin-bottom: $semantic-spacing-component-xl;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
border: 1px solid $semantic-color-border-primary;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-xl;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-primary;
|
||||
border: 1px solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
}
|
||||
|
||||
.vertical-stepper-container {
|
||||
max-width: 400px;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-primary;
|
||||
border: 1px solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
}
|
||||
|
||||
.interactive-example {
|
||||
padding: $semantic-spacing-component-xl;
|
||||
background: $semantic-color-surface-primary;
|
||||
border: 2px solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
margin-top: $semantic-spacing-component-lg;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
display: 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-brand-primary;
|
||||
color: $semantic-color-on-brand-primary;
|
||||
border: none;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
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);
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-brand-secondary;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-brand-primary;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
}
|
||||
|
||||
fa-icon {
|
||||
font-size: $semantic-sizing-icon-inline;
|
||||
}
|
||||
}
|
||||
|
||||
.step-info {
|
||||
margin-top: $semantic-spacing-component-lg;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
border-left: 4px solid $semantic-color-brand-primary;
|
||||
|
||||
p {
|
||||
margin: 0 0 $semantic-spacing-content-line-tight 0;
|
||||
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);
|
||||
color: $semantic-color-text-secondary;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: $semantic-color-text-primary;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
flex-direction: column;
|
||||
|
||||
.demo-button {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.vertical-stepper-container {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.demo-subsection,
|
||||
.demo-item,
|
||||
.interactive-example {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-section h2 {
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
}
|
||||
|
||||
.demo-section h3 {
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import { faPlay, faUndo } from '@fortawesome/free-solid-svg-icons';
|
||||
import { StepperComponent, StepperStep } from '../../../../../ui-essentials/src/lib/components/navigation/stepper';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-stepper-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, StepperComponent, FontAwesomeModule],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Stepper Demo</h2>
|
||||
|
||||
<!-- Orientation Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Orientation</h3>
|
||||
<div class="demo-subsection">
|
||||
<h4>Horizontal Stepper</h4>
|
||||
<ui-stepper
|
||||
[steps]="horizontalSteps"
|
||||
orientation="horizontal"
|
||||
[clickable]="true"
|
||||
(stepClick)="onStepClick('horizontal', $event)"
|
||||
(stepChange)="onStepChange('horizontal', $event)">
|
||||
</ui-stepper>
|
||||
</div>
|
||||
|
||||
<div class="demo-subsection">
|
||||
<h4>Vertical Stepper</h4>
|
||||
<div class="vertical-stepper-container">
|
||||
<ui-stepper
|
||||
[steps]="verticalSteps"
|
||||
orientation="vertical"
|
||||
[clickable]="true"
|
||||
(stepClick)="onStepClick('vertical', $event)"
|
||||
(stepChange)="onStepChange('vertical', $event)">
|
||||
</ui-stepper>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Sizes</h3>
|
||||
<div class="demo-row">
|
||||
@for (size of sizes; track size) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ size }} Size</h4>
|
||||
<ui-stepper
|
||||
[steps]="sizeSteps"
|
||||
[size]="size"
|
||||
orientation="horizontal">
|
||||
</ui-stepper>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Style Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Variants</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<h4>Default</h4>
|
||||
<ui-stepper
|
||||
[steps]="variantSteps"
|
||||
variant="default">
|
||||
</ui-stepper>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<h4>Outlined</h4>
|
||||
<ui-stepper
|
||||
[steps]="variantSteps"
|
||||
variant="outlined">
|
||||
</ui-stepper>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Dense Variant -->
|
||||
<section class="demo-section">
|
||||
<h3>Dense Layout</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<h4>Normal</h4>
|
||||
<ui-stepper
|
||||
[steps]="denseSteps"
|
||||
[dense]="false">
|
||||
</ui-stepper>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<h4>Dense</h4>
|
||||
<ui-stepper
|
||||
[steps]="denseSteps"
|
||||
[dense]="true">
|
||||
</ui-stepper>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Step States -->
|
||||
<section class="demo-section">
|
||||
<h3>Step States</h3>
|
||||
<div class="demo-subsection">
|
||||
<h4>All States</h4>
|
||||
<ui-stepper
|
||||
[steps]="stateSteps"
|
||||
orientation="horizontal">
|
||||
</ui-stepper>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Linear vs Non-Linear -->
|
||||
<section class="demo-section">
|
||||
<h3>Navigation Modes</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<h4>Linear (Default)</h4>
|
||||
<ui-stepper
|
||||
[steps]="linearSteps"
|
||||
[clickable]="true"
|
||||
[linear]="true"
|
||||
(stepClick)="onStepClick('linear', $event)">
|
||||
</ui-stepper>
|
||||
<div class="demo-controls">
|
||||
<button (click)="nextLinearStep()" class="demo-button">
|
||||
<fa-icon [icon]="playIcon"></fa-icon>
|
||||
Next Step
|
||||
</button>
|
||||
<button (click)="resetLinearStepper()" class="demo-button">
|
||||
<fa-icon [icon]="resetIcon"></fa-icon>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Non-Linear</h4>
|
||||
<ui-stepper
|
||||
[steps]="nonLinearSteps"
|
||||
[clickable]="true"
|
||||
[linear]="false"
|
||||
(stepClick)="onStepClick('non-linear', $event)">
|
||||
</ui-stepper>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Example</h3>
|
||||
<div class="interactive-example">
|
||||
<ui-stepper
|
||||
#interactiveStepper
|
||||
[steps]="interactiveSteps"
|
||||
[clickable]="true"
|
||||
[linear]="false"
|
||||
orientation="horizontal"
|
||||
(stepClick)="onInteractiveStepClick($event)"
|
||||
(stepChange)="onInteractiveStepChange($event)">
|
||||
</ui-stepper>
|
||||
|
||||
<div class="demo-controls">
|
||||
<button (click)="markCurrentCompleted()" class="demo-button">
|
||||
Mark Current Completed
|
||||
</button>
|
||||
<button (click)="markCurrentError()" class="demo-button">
|
||||
Mark Current Error
|
||||
</button>
|
||||
<button (click)="nextInteractiveStep()" class="demo-button">
|
||||
<fa-icon [icon]="playIcon"></fa-icon>
|
||||
Next
|
||||
</button>
|
||||
<button (click)="resetInteractiveStepper()" class="demo-button">
|
||||
<fa-icon [icon]="resetIcon"></fa-icon>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="step-info">
|
||||
<p><strong>Current Step:</strong> {{ getCurrentStepInfo() }}</p>
|
||||
<p><strong>Completed Steps:</strong> {{ getCompletedStepsCount() }}</p>
|
||||
<p><strong>Last Action:</strong> {{ lastAction }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './stepper-demo.component.scss'
|
||||
})
|
||||
export class StepperDemoComponent {
|
||||
sizes = ['sm', 'md', 'lg'] as const;
|
||||
playIcon = faPlay;
|
||||
resetIcon = faUndo;
|
||||
|
||||
lastAction = 'None';
|
||||
|
||||
// Horizontal stepper data
|
||||
horizontalSteps: StepperStep[] = [
|
||||
{ id: 'h1', title: 'Account Info', description: 'Enter your personal details', current: true },
|
||||
{ id: 'h2', title: 'Verification', description: 'Verify your email address' },
|
||||
{ id: 'h3', title: 'Preferences', description: 'Set up your preferences' },
|
||||
{ id: 'h4', title: 'Complete', description: 'Account setup finished' }
|
||||
];
|
||||
|
||||
// Vertical stepper data
|
||||
verticalSteps: StepperStep[] = [
|
||||
{ id: 'v1', title: 'Project Setup', description: 'Initialize your new project', completed: true },
|
||||
{ id: 'v2', title: 'Configuration', description: 'Configure project settings', current: true },
|
||||
{ id: 'v3', title: 'Dependencies', description: 'Install required dependencies' },
|
||||
{ id: 'v4', title: 'Testing', description: 'Run initial tests' },
|
||||
{ id: 'v5', title: 'Deployment', description: 'Deploy to production' }
|
||||
];
|
||||
|
||||
// Size demonstration
|
||||
sizeSteps: StepperStep[] = [
|
||||
{ id: 's1', title: 'Step 1', completed: true },
|
||||
{ id: 's2', title: 'Step 2', current: true },
|
||||
{ id: 's3', title: 'Step 3' }
|
||||
];
|
||||
|
||||
// Variant demonstration
|
||||
variantSteps: StepperStep[] = [
|
||||
{ id: 'var1', title: 'Design', completed: true },
|
||||
{ id: 'var2', title: 'Development', current: true },
|
||||
{ id: 'var3', title: 'Testing' },
|
||||
{ id: 'var4', title: 'Launch' }
|
||||
];
|
||||
|
||||
// Dense demonstration
|
||||
denseSteps: StepperStep[] = [
|
||||
{ id: 'd1', title: 'Upload', completed: true },
|
||||
{ id: 'd2', title: 'Process', current: true },
|
||||
{ id: 'd3', title: 'Review' },
|
||||
{ id: 'd4', title: 'Publish' }
|
||||
];
|
||||
|
||||
// State demonstration
|
||||
stateSteps: StepperStep[] = [
|
||||
{ id: 'st1', title: 'Completed', description: 'This step is done', completed: true },
|
||||
{ id: 'st2', title: 'Current', description: 'Currently active step', current: true },
|
||||
{ id: 'st3', title: 'Error', description: 'This step has an error', error: true },
|
||||
{ id: 'st4', title: 'Disabled', description: 'This step is disabled', disabled: true },
|
||||
{ id: 'st5', title: 'Pending', description: 'This step is pending' }
|
||||
];
|
||||
|
||||
// Linear stepper
|
||||
linearSteps: StepperStep[] = [
|
||||
{ id: 'l1', title: 'Start', description: 'Begin the process', current: true },
|
||||
{ id: 'l2', title: 'Middle', description: 'Continue the process' },
|
||||
{ id: 'l3', title: 'End', description: 'Finish the process' }
|
||||
];
|
||||
|
||||
// Non-linear stepper
|
||||
nonLinearSteps: StepperStep[] = [
|
||||
{ id: 'nl1', title: 'Overview', description: 'Click any step', completed: true },
|
||||
{ id: 'nl2', title: 'Details', description: 'Add more information', current: true },
|
||||
{ id: 'nl3', title: 'Review', description: 'Check your work' }
|
||||
];
|
||||
|
||||
// Interactive example
|
||||
interactiveSteps: StepperStep[] = [
|
||||
{ id: 'i1', title: 'Planning', description: 'Plan your approach', current: true },
|
||||
{ id: 'i2', title: 'Implementation', description: 'Build the solution' },
|
||||
{ id: 'i3', title: 'Testing', description: 'Test thoroughly' },
|
||||
{ id: 'i4', title: 'Deployment', description: 'Go live' }
|
||||
];
|
||||
|
||||
onStepClick(type: string, event: {step: StepperStep, index: number}): void {
|
||||
this.lastAction = `${type}: Clicked step "${event.step.title}" (index ${event.index})`;
|
||||
console.log(`${type} stepper - Step clicked:`, event);
|
||||
}
|
||||
|
||||
onStepChange(type: string, event: {step: StepperStep, index: number}): void {
|
||||
this.lastAction = `${type}: Changed to step "${event.step.title}" (index ${event.index})`;
|
||||
console.log(`${type} stepper - Step changed:`, event);
|
||||
}
|
||||
|
||||
nextLinearStep(): void {
|
||||
const currentIndex = this.linearSteps.findIndex(step => step.current);
|
||||
if (currentIndex >= 0 && currentIndex < this.linearSteps.length - 1) {
|
||||
// Mark current as completed
|
||||
this.linearSteps[currentIndex].completed = true;
|
||||
this.linearSteps[currentIndex].current = false;
|
||||
|
||||
// Move to next
|
||||
this.linearSteps[currentIndex + 1].current = true;
|
||||
|
||||
this.lastAction = `Linear: Advanced to step ${currentIndex + 2}`;
|
||||
}
|
||||
}
|
||||
|
||||
resetLinearStepper(): void {
|
||||
this.linearSteps.forEach((step, index) => {
|
||||
step.current = index === 0;
|
||||
step.completed = false;
|
||||
step.error = false;
|
||||
});
|
||||
this.lastAction = 'Linear: Reset to beginning';
|
||||
}
|
||||
|
||||
onInteractiveStepClick(event: {step: StepperStep, index: number}): void {
|
||||
this.lastAction = `Interactive: Clicked "${event.step.title}"`;
|
||||
}
|
||||
|
||||
onInteractiveStepChange(event: {step: StepperStep, index: number}): void {
|
||||
this.lastAction = `Interactive: Changed to "${event.step.title}"`;
|
||||
}
|
||||
|
||||
markCurrentCompleted(): void {
|
||||
const currentStep = this.interactiveSteps.find(step => step.current);
|
||||
if (currentStep) {
|
||||
currentStep.completed = true;
|
||||
currentStep.error = false;
|
||||
this.lastAction = `Interactive: Marked "${currentStep.title}" as completed`;
|
||||
}
|
||||
}
|
||||
|
||||
markCurrentError(): void {
|
||||
const currentStep = this.interactiveSteps.find(step => step.current);
|
||||
if (currentStep) {
|
||||
currentStep.error = true;
|
||||
currentStep.completed = false;
|
||||
this.lastAction = `Interactive: Marked "${currentStep.title}" as error`;
|
||||
}
|
||||
}
|
||||
|
||||
nextInteractiveStep(): void {
|
||||
const currentIndex = this.interactiveSteps.findIndex(step => step.current);
|
||||
if (currentIndex >= 0 && currentIndex < this.interactiveSteps.length - 1) {
|
||||
// Mark current as completed if not in error
|
||||
if (!this.interactiveSteps[currentIndex].error) {
|
||||
this.interactiveSteps[currentIndex].completed = true;
|
||||
}
|
||||
this.interactiveSteps[currentIndex].current = false;
|
||||
|
||||
// Move to next
|
||||
this.interactiveSteps[currentIndex + 1].current = true;
|
||||
|
||||
this.lastAction = `Interactive: Advanced to step ${currentIndex + 2}`;
|
||||
}
|
||||
}
|
||||
|
||||
resetInteractiveStepper(): void {
|
||||
this.interactiveSteps.forEach((step, index) => {
|
||||
step.current = index === 0;
|
||||
step.completed = false;
|
||||
step.error = false;
|
||||
});
|
||||
this.lastAction = 'Interactive: Reset to beginning';
|
||||
}
|
||||
|
||||
getCurrentStepInfo(): string {
|
||||
const currentStep = this.interactiveSteps.find(step => step.current);
|
||||
return currentStep ? `${currentStep.title} (${this.interactiveSteps.indexOf(currentStep) + 1} of ${this.interactiveSteps.length})` : 'None';
|
||||
}
|
||||
|
||||
getCompletedStepsCount(): number {
|
||||
return this.interactiveSteps.filter(step => step.completed).length;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, ChangeDetectionStrategy, TemplateRef, ViewChild } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { TableComponent } from '../../../../../ui-essentials/src/lib/components/data-display/table/table.component';
|
||||
import { TableAction, TableActionsComponent } from '../../../../../ui-essentials/src/lib/components/data-display/table-actions.component';
|
||||
import { TableAction, TableActionsComponent } from '../../../../../ui-essentials/src/lib/components/data-display/table/table-actions.component';
|
||||
import type { TableColumn } from '../../../../../ui-essentials/src/lib/components/data-display/table/table.component';
|
||||
import type { TableSortEvent } from '../../../../../ui-essentials/src/lib/components/data-display/table/table.component';
|
||||
import { StatusBadgeComponent } from '../../../../../ui-essentials/src/lib/components/feedback/status-badge.component';
|
||||
|
||||
@@ -0,0 +1,543 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { TagInputComponent } from '../../../../../ui-essentials/src/lib/components/forms/tag-input/tag-input.component';
|
||||
import { ButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons/button.component';
|
||||
import { faTag, faPlus, faTimes, faRefresh } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-tag-input-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
TagInputComponent,
|
||||
ButtonComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="padding: 2rem;">
|
||||
<h2>Tag Input Component Showcase</h2>
|
||||
|
||||
<!-- Basic Tag Input Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Basic Variants</h3>
|
||||
|
||||
<h4>Outlined (Default)</h4>
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<ui-tag-input
|
||||
placeholder="Add tags (press Enter, Tab, or comma to add)"
|
||||
helpText="Separate tags with Enter, Tab, comma, semicolon, or pipe"
|
||||
[class]="'demo-tag-input'"
|
||||
(tagAdd)="handleTagAdd('outlined', $event)"
|
||||
(tagRemove)="handleTagRemove('outlined', $event)"
|
||||
(duplicateTag)="handleDuplicateTag('outlined', $event)"
|
||||
(tagValidationError)="handleValidationError('outlined', $event)">
|
||||
</ui-tag-input>
|
||||
</div>
|
||||
|
||||
<h4>Filled</h4>
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<ui-tag-input
|
||||
variant="filled"
|
||||
placeholder="Add filled style tags"
|
||||
helpText="This input uses the filled variant style"
|
||||
[class]="'demo-tag-input'"
|
||||
(tagAdd)="handleTagAdd('filled', $event)"
|
||||
(tagRemove)="handleTagRemove('filled', $event)">
|
||||
</ui-tag-input>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Size Variants</h3>
|
||||
|
||||
<h4>Small</h4>
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<ui-tag-input
|
||||
size="sm"
|
||||
placeholder="Small size tags"
|
||||
helpText="Compact size for dense layouts"
|
||||
[class]="'demo-tag-input'"
|
||||
(tagAdd)="handleTagAdd('small', $event)"
|
||||
(tagRemove)="handleTagRemove('small', $event)">
|
||||
</ui-tag-input>
|
||||
</div>
|
||||
|
||||
<h4>Medium (Default)</h4>
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<ui-tag-input
|
||||
size="md"
|
||||
placeholder="Medium size tags"
|
||||
helpText="Standard size for most use cases"
|
||||
[class]="'demo-tag-input'"
|
||||
(tagAdd)="handleTagAdd('medium', $event)"
|
||||
(tagRemove)="handleTagRemove('medium', $event)">
|
||||
</ui-tag-input>
|
||||
</div>
|
||||
|
||||
<h4>Large</h4>
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<ui-tag-input
|
||||
size="lg"
|
||||
placeholder="Large size tags"
|
||||
helpText="Larger size for better accessibility"
|
||||
[class]="'demo-tag-input'"
|
||||
(tagAdd)="handleTagAdd('large', $event)"
|
||||
(tagRemove)="handleTagRemove('large', $event)">
|
||||
</ui-tag-input>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Configuration Options -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Configuration Options</h3>
|
||||
|
||||
<h4>Max Tags (3)</h4>
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<ui-tag-input
|
||||
placeholder="Maximum 3 tags allowed"
|
||||
[maxTags]="3"
|
||||
helpText="This input has a maximum of 3 tags"
|
||||
[class]="'demo-tag-input'"
|
||||
(maxTagsReached)="handleMaxTagsReached('max-tags')"
|
||||
(tagAdd)="handleTagAdd('max-tags', $event)"
|
||||
(tagRemove)="handleTagRemove('max-tags', $event)">
|
||||
</ui-tag-input>
|
||||
</div>
|
||||
|
||||
<h4>Max Tag Length (10)</h4>
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<ui-tag-input
|
||||
placeholder="Max 10 characters per tag"
|
||||
[maxTagLength]="10"
|
||||
helpText="Each tag can be at most 10 characters long"
|
||||
[class]="'demo-tag-input'"
|
||||
(tagValidationError)="handleValidationError('max-length', $event)"
|
||||
(tagAdd)="handleTagAdd('max-length', $event)"
|
||||
(tagRemove)="handleTagRemove('max-length', $event)">
|
||||
</ui-tag-input>
|
||||
</div>
|
||||
|
||||
<h4>Allow Duplicates</h4>
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<ui-tag-input
|
||||
placeholder="Duplicate tags allowed"
|
||||
[allowDuplicates]="true"
|
||||
helpText="This input allows duplicate tags"
|
||||
[class]="'demo-tag-input'"
|
||||
(tagAdd)="handleTagAdd('allow-duplicates', $event)"
|
||||
(tagRemove)="handleTagRemove('allow-duplicates', $event)">
|
||||
</ui-tag-input>
|
||||
</div>
|
||||
|
||||
<h4>No Trimming</h4>
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<ui-tag-input
|
||||
placeholder="Leading/trailing spaces preserved"
|
||||
[trimTags]="false"
|
||||
helpText="Tags preserve leading and trailing whitespace"
|
||||
[class]="'demo-tag-input'"
|
||||
(tagAdd)="handleTagAdd('no-trim', $event)"
|
||||
(tagRemove)="handleTagRemove('no-trim', $event)">
|
||||
</ui-tag-input>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- States -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>States</h3>
|
||||
|
||||
<h4>Normal</h4>
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<ui-tag-input
|
||||
placeholder="Normal interactive state"
|
||||
helpText="Standard interactive state"
|
||||
[class]="'demo-tag-input'"
|
||||
(tagAdd)="handleTagAdd('normal', $event)"
|
||||
(tagRemove)="handleTagRemove('normal', $event)">
|
||||
</ui-tag-input>
|
||||
</div>
|
||||
|
||||
<h4>Disabled</h4>
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<ui-tag-input
|
||||
placeholder="Disabled state"
|
||||
[disabled]="true"
|
||||
helpText="This input is disabled"
|
||||
[class]="'demo-tag-input'">
|
||||
</ui-tag-input>
|
||||
</div>
|
||||
|
||||
<h4>Read Only</h4>
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<ui-tag-input
|
||||
placeholder="Read only state"
|
||||
[readonly]="true"
|
||||
helpText="This input is read-only - tags cannot be added or removed"
|
||||
[class]="'demo-tag-input'">
|
||||
</ui-tag-input>
|
||||
</div>
|
||||
|
||||
<h4>With Error</h4>
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<ui-tag-input
|
||||
placeholder="Input with error"
|
||||
errorMessage="This field is required and must contain at least one tag"
|
||||
[class]="'demo-tag-input'"
|
||||
(tagAdd)="handleTagAdd('error', $event)"
|
||||
(tagRemove)="handleTagRemove('error', $event)">
|
||||
</ui-tag-input>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Form Integration -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Form Integration</h3>
|
||||
<form [formGroup]="demoForm" (ngSubmit)="handleFormSubmit()" style="max-width: 600px;">
|
||||
<h4>Reactive Form Example</h4>
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<ui-tag-input
|
||||
formControlName="tags"
|
||||
placeholder="Required: Add at least 2 tags"
|
||||
helpText="This field is required and uses reactive forms validation"
|
||||
[class]="'demo-tag-input'"
|
||||
[errorMessage]="getFormFieldError('tags')"
|
||||
(tagAdd)="handleTagAdd('form', $event)"
|
||||
(tagRemove)="handleTagRemove('form', $event)">
|
||||
</ui-tag-input>
|
||||
</div>
|
||||
|
||||
<h4>Skills (Optional)</h4>
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<ui-tag-input
|
||||
formControlName="skills"
|
||||
placeholder="Add your skills"
|
||||
[maxTags]="5"
|
||||
helpText="Optional field - up to 5 skills"
|
||||
[class]="'demo-tag-input'"
|
||||
(tagAdd)="handleTagAdd('skills', $event)"
|
||||
(tagRemove)="handleTagRemove('skills', $event)">
|
||||
</ui-tag-input>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 1rem; align-items: center;">
|
||||
<ui-button
|
||||
type="submit"
|
||||
variant="filled"
|
||||
[disabled]="demoForm.invalid">
|
||||
Submit Form
|
||||
</ui-button>
|
||||
<ui-button
|
||||
type="button"
|
||||
variant="tonal"
|
||||
[icon]="faRefresh"
|
||||
iconPosition="left"
|
||||
(clicked)="resetForm()">
|
||||
Reset
|
||||
</ui-button>
|
||||
</div>
|
||||
|
||||
@if (formSubmitted) {
|
||||
<div style="margin-top: 1rem; padding: 1rem; background: #e8f5e8; border-radius: 4px; border-left: 4px solid #4caf50;">
|
||||
<strong>Form Submitted!</strong><br>
|
||||
<strong>Tags:</strong> {{ formData.tags?.join(', ') || 'None' }}<br>
|
||||
<strong>Skills:</strong> {{ formData.skills?.join(', ') || 'None' }}
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- Programmatic Control -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Programmatic Control</h3>
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<ui-tag-input
|
||||
#programmaticInput
|
||||
placeholder="Controlled programmatically"
|
||||
helpText="Use the buttons below to control this input"
|
||||
[class]="'demo-tag-input'">
|
||||
</ui-tag-input>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<ui-button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
[icon]="faPlus"
|
||||
iconPosition="left"
|
||||
(clicked)="addPredefinedTag(programmaticInput)">
|
||||
Add Random Tag
|
||||
</ui-button>
|
||||
<ui-button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
(clicked)="clearProgrammaticInput(programmaticInput)">
|
||||
Clear All
|
||||
</ui-button>
|
||||
<ui-button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
(clicked)="addMultipleTags(programmaticInput)">
|
||||
Add Multiple
|
||||
</ui-button>
|
||||
<ui-button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
(clicked)="getTagsAsString(programmaticInput)">
|
||||
Get as String
|
||||
</ui-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Usage Examples -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Usage Examples</h3>
|
||||
<div style="background: #f8f9fa; padding: 1.5rem; border-radius: 8px; border-left: 4px solid #007bff;">
|
||||
<h4>Basic Usage:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-tag-input
|
||||
placeholder="Add tags..."
|
||||
(tagAdd)="onTagAdd($event)"
|
||||
(tagRemove)="onTagRemove($event)">
|
||||
</ui-tag-input></code></pre>
|
||||
|
||||
<h4>With Constraints:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-tag-input
|
||||
placeholder="Add up to 5 tags"
|
||||
[maxTags]="5"
|
||||
[maxTagLength]="20"
|
||||
[allowDuplicates]="false"
|
||||
helpText="Each tag can be up to 20 characters"
|
||||
(maxTagsReached)="onMaxTags()"
|
||||
(duplicateTag)="onDuplicate($event)">
|
||||
</ui-tag-input></code></pre>
|
||||
|
||||
<h4>Form Integration:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><form [formGroup]="myForm">
|
||||
<ui-tag-input
|
||||
formControlName="tags"
|
||||
placeholder="Required tags"
|
||||
[errorMessage]="getFieldError('tags')">
|
||||
</ui-tag-input>
|
||||
</form></code></pre>
|
||||
|
||||
<h4>Custom Separators:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-tag-input
|
||||
placeholder="Use space or dash to separate"
|
||||
[separators]="[' ', '-']"
|
||||
helpText="Tags separated by space or dash">
|
||||
</ui-tag-input></code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Event Log -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Event Log</h3>
|
||||
@if (lastAction) {
|
||||
<div style="margin-bottom: 1rem; padding: 1rem; background: #e3f2fd; border-radius: 4px;">
|
||||
<strong>Last action:</strong> {{ lastAction }}
|
||||
</div>
|
||||
}
|
||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
||||
<ui-button variant="tonal" (clicked)="clearEventLog()">Clear Log</ui-button>
|
||||
<ui-button variant="outlined" (clicked)="showAlert('Tag Input demo complete!')">Show Alert</ui-button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
h2 {
|
||||
color: hsl(279, 14%, 11%);
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 2px solid hsl(258, 100%, 47%);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: hsl(279, 14%, 25%);
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: hsl(287, 12%, 35%);
|
||||
font-size: 1.125rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
section {
|
||||
border: 1px solid hsl(289, 14%, 90%);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
background: hsl(286, 20%, 99%);
|
||||
}
|
||||
|
||||
.demo-tag-input {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: #d63384;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class TagInputDemoComponent {
|
||||
lastAction: string = '';
|
||||
|
||||
// FontAwesome icons
|
||||
faTag = faTag;
|
||||
faPlus = faPlus;
|
||||
faTimes = faTimes;
|
||||
faRefresh = faRefresh;
|
||||
|
||||
// Form
|
||||
demoForm = new FormGroup({
|
||||
tags: new FormControl<string[]>([], [
|
||||
Validators.required,
|
||||
this.minTagsValidator(2)
|
||||
]),
|
||||
skills: new FormControl<string[]>([])
|
||||
});
|
||||
|
||||
formSubmitted = false;
|
||||
formData: { tags?: string[]; skills?: string[] } = {};
|
||||
|
||||
// Predefined tags for programmatic control
|
||||
predefinedTags = [
|
||||
'JavaScript', 'TypeScript', 'Angular', 'React', 'Vue',
|
||||
'HTML', 'CSS', 'SCSS', 'Node.js', 'Python', 'Java',
|
||||
'Design', 'Frontend', 'Backend', 'Fullstack'
|
||||
];
|
||||
|
||||
// Event handlers
|
||||
handleTagAdd(source: string, tag: string): void {
|
||||
this.lastAction = `Tag added (${source}): "${tag}"`;
|
||||
console.log(`Tag added (${source}):`, tag);
|
||||
}
|
||||
|
||||
handleTagRemove(source: string, event: { tag: string; index: number }): void {
|
||||
this.lastAction = `Tag removed (${source}): "${event.tag}" at index ${event.index}`;
|
||||
console.log(`Tag removed (${source}):`, event);
|
||||
}
|
||||
|
||||
handleDuplicateTag(source: string, tag: string): void {
|
||||
this.lastAction = `Duplicate tag attempted (${source}): "${tag}"`;
|
||||
console.log(`Duplicate tag attempted (${source}):`, tag);
|
||||
}
|
||||
|
||||
handleValidationError(source: string, error: { tag: string; error: string }): void {
|
||||
this.lastAction = `Validation error (${source}): "${error.tag}" - ${error.error}`;
|
||||
console.log(`Validation error (${source}):`, error);
|
||||
}
|
||||
|
||||
handleMaxTagsReached(source: string): void {
|
||||
this.lastAction = `Maximum tags reached (${source})`;
|
||||
console.log(`Maximum tags reached (${source})`);
|
||||
}
|
||||
|
||||
// Form methods
|
||||
minTagsValidator(min: number) {
|
||||
return (control: any) => {
|
||||
const tags = control.value;
|
||||
if (!tags || tags.length < min) {
|
||||
return { minTags: { required: min, actual: tags?.length || 0 } };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
getFormFieldError(fieldName: string): string | undefined {
|
||||
const field = this.demoForm.get(fieldName);
|
||||
if (field && field.invalid && (field.dirty || field.touched)) {
|
||||
const errors = field.errors;
|
||||
if (errors?.['required']) {
|
||||
return 'This field is required';
|
||||
}
|
||||
if (errors?.['minTags']) {
|
||||
return `At least ${errors['minTags'].required} tags are required`;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
handleFormSubmit(): void {
|
||||
if (this.demoForm.valid) {
|
||||
const formValue = this.demoForm.value;
|
||||
this.formData = {
|
||||
tags: formValue.tags || [],
|
||||
skills: formValue.skills || []
|
||||
};
|
||||
this.formSubmitted = true;
|
||||
this.lastAction = 'Form submitted successfully';
|
||||
console.log('Form submitted:', this.formData);
|
||||
} else {
|
||||
this.lastAction = 'Form submission failed - validation errors';
|
||||
console.log('Form invalid:', this.demoForm.errors);
|
||||
}
|
||||
}
|
||||
|
||||
resetForm(): void {
|
||||
this.demoForm.reset();
|
||||
this.formSubmitted = false;
|
||||
this.formData = {};
|
||||
this.lastAction = 'Form reset';
|
||||
console.log('Form reset');
|
||||
}
|
||||
|
||||
// Programmatic control methods
|
||||
addPredefinedTag(tagInput: TagInputComponent): void {
|
||||
const randomTag = this.predefinedTags[Math.floor(Math.random() * this.predefinedTags.length)];
|
||||
const success = tagInput.addTagProgrammatically(randomTag);
|
||||
this.lastAction = success
|
||||
? `Programmatically added: "${randomTag}"`
|
||||
: `Failed to add: "${randomTag}"`;
|
||||
console.log('Add predefined tag result:', success, randomTag);
|
||||
}
|
||||
|
||||
clearProgrammaticInput(tagInput: TagInputComponent): void {
|
||||
tagInput.clear();
|
||||
this.lastAction = 'Programmatically cleared all tags';
|
||||
console.log('Cleared programmatic input');
|
||||
}
|
||||
|
||||
addMultipleTags(tagInput: TagInputComponent): void {
|
||||
const tagsToAdd = ['HTML', 'CSS', 'JavaScript'];
|
||||
let addedCount = 0;
|
||||
|
||||
tagsToAdd.forEach(tag => {
|
||||
if (tagInput.addTagProgrammatically(tag)) {
|
||||
addedCount++;
|
||||
}
|
||||
});
|
||||
|
||||
this.lastAction = `Programmatically added ${addedCount}/${tagsToAdd.length} tags`;
|
||||
console.log(`Added ${addedCount} multiple tags`);
|
||||
}
|
||||
|
||||
getTagsAsString(tagInput: TagInputComponent): void {
|
||||
const tagsString = tagInput.getTagsAsString();
|
||||
this.lastAction = `Tags as string: "${tagsString}"`;
|
||||
alert(`Current tags: ${tagsString || 'None'}`);
|
||||
console.log('Tags as string:', tagsString);
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
clearEventLog(): void {
|
||||
this.lastAction = '';
|
||||
}
|
||||
|
||||
showAlert(message: string): void {
|
||||
alert(message);
|
||||
this.lastAction = 'Alert shown';
|
||||
}
|
||||
}
|
||||
@@ -195,7 +195,7 @@
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
@media (max-width: calc($semantic-breakpoint-md - 1px)) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-sm;
|
||||
}
|
||||
@@ -220,7 +220,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
@media (max-width: calc($semantic-breakpoint-sm - 1px)) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-xs;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import { faPlay, faStop, faRedo } from '@fortawesome/free-solid-svg-icons';
|
||||
import { ToastComponent } from '../../../../../../dist/ui-essentials/lib/components/feedback/toast/toast.component';
|
||||
import { ToastComponent } from '../../../../../ui-essentials/src/lib/components/feedback/toast/toast.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-toast-demo',
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
@import "../../../../../shared-ui/src/styles/semantic";
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 $semantic-spacing-layout-section-lg 0;
|
||||
font-family: map-get($semantic-typography-heading-h2, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h2, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h2, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h2, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-xl;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 $semantic-spacing-component-md 0;
|
||||
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;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0 0 $semantic-spacing-component-sm 0;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-description {
|
||||
margin: 0 0 $semantic-spacing-component-lg 0;
|
||||
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;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
align-items: flex-start;
|
||||
|
||||
&--vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $semantic-spacing-component-md;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
cursor: pointer;
|
||||
|
||||
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;
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $semantic-color-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-event-log {
|
||||
background: $semantic-color-surface-primary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
padding: $semantic-spacing-component-md;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.demo-no-events {
|
||||
margin: 0;
|
||||
padding: $semantic-spacing-component-lg 0;
|
||||
text-align: center;
|
||||
color: $semantic-color-text-tertiary;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.demo-event {
|
||||
padding: $semantic-spacing-component-xs 0;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: $semantic-color-primary;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-event-time {
|
||||
display: block;
|
||||
margin-top: $semantic-spacing-component-xs;
|
||||
color: $semantic-color-text-tertiary;
|
||||
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);
|
||||
}
|
||||
|
||||
.demo-clear-events {
|
||||
margin-top: $semantic-spacing-component-md;
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
|
||||
font-family: map-get($semantic-typography-button-small, font-family);
|
||||
font-size: map-get($semantic-typography-button-small, font-size);
|
||||
font-weight: map-get($semantic-typography-button-small, font-weight);
|
||||
line-height: map-get($semantic-typography-button-small, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-color: $semantic-color-border-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-sm;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
|
||||
.demo-event-log {
|
||||
max-height: 200px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { TransferListComponent, TransferListItem } from '../../../../../ui-essentials/src/lib/components/data-display/transfer-list/transfer-list.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-transfer-list-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, TransferListComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Transfer List Demo</h2>
|
||||
|
||||
<!-- Basic Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Basic Transfer List</h3>
|
||||
<p class="demo-description">
|
||||
Select items from the source list and transfer them to the target list using the control buttons.
|
||||
</p>
|
||||
<ui-transfer-list
|
||||
[sourceItems]="basicSourceItems()"
|
||||
[targetItems]="basicTargetItems()"
|
||||
sourceTitle="Available Items"
|
||||
targetTitle="Selected Items"
|
||||
(sourceChange)="onBasicSourceChange($event)"
|
||||
(targetChange)="onBasicTargetChange($event)"
|
||||
(itemMove)="onItemMove($event)"
|
||||
></ui-transfer-list>
|
||||
</section>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Size Variants</h3>
|
||||
<div class="demo-row demo-row--vertical">
|
||||
<div class="demo-item">
|
||||
<h4>Small</h4>
|
||||
<ui-transfer-list
|
||||
size="sm"
|
||||
[sourceItems]="sizeSourceItems()"
|
||||
[targetItems]="sizeTargetItems()"
|
||||
sourceTitle="Small List"
|
||||
targetTitle="Selected"
|
||||
></ui-transfer-list>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Medium (Default)</h4>
|
||||
<ui-transfer-list
|
||||
size="md"
|
||||
[sourceItems]="sizeSourceItems()"
|
||||
[targetItems]="sizeTargetItems()"
|
||||
sourceTitle="Medium List"
|
||||
targetTitle="Selected"
|
||||
></ui-transfer-list>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Large</h4>
|
||||
<ui-transfer-list
|
||||
size="lg"
|
||||
[sourceItems]="sizeSourceItems()"
|
||||
[targetItems]="sizeTargetItems()"
|
||||
sourceTitle="Large List"
|
||||
targetTitle="Selected"
|
||||
></ui-transfer-list>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Configuration Options -->
|
||||
<section class="demo-section">
|
||||
<h3>Configuration Options</h3>
|
||||
|
||||
<div class="demo-controls">
|
||||
<label>
|
||||
<input type="checkbox" [checked]="showSearch()" (change)="toggleSearch()">
|
||||
Show Search
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" [checked]="showSelectAll()" (change)="toggleSelectAll()">
|
||||
Show Select All
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" [checked]="showCheckboxes()" (change)="toggleCheckboxes()">
|
||||
Show Checkboxes
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" [checked]="showMoveAll()" (change)="toggleMoveAll()">
|
||||
Show Move All Buttons
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" [checked]="isDisabled()" (change)="toggleDisabled()">
|
||||
Disabled
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<ui-transfer-list
|
||||
[sourceItems]="configSourceItems()"
|
||||
[targetItems]="configTargetItems()"
|
||||
[showSearch]="showSearch()"
|
||||
[showSelectAll]="showSelectAll()"
|
||||
[showCheckboxes]="showCheckboxes()"
|
||||
[showMoveAll]="showMoveAll()"
|
||||
[disabled]="isDisabled()"
|
||||
sourceTitle="Configurable Source"
|
||||
targetTitle="Configurable Target"
|
||||
searchPlaceholder="Filter items..."
|
||||
(sourceChange)="onConfigSourceChange($event)"
|
||||
(targetChange)="onConfigTargetChange($event)"
|
||||
></ui-transfer-list>
|
||||
</section>
|
||||
|
||||
<!-- With Disabled Items -->
|
||||
<section class="demo-section">
|
||||
<h3>With Disabled Items</h3>
|
||||
<p class="demo-description">
|
||||
Some items can be disabled and cannot be selected or transferred.
|
||||
</p>
|
||||
<ui-transfer-list
|
||||
[sourceItems]="disabledSourceItems()"
|
||||
[targetItems]="disabledTargetItems()"
|
||||
sourceTitle="Mixed Items"
|
||||
targetTitle="Available Items"
|
||||
(sourceChange)="onDisabledSourceChange($event)"
|
||||
(targetChange)="onDisabledTargetChange($event)"
|
||||
></ui-transfer-list>
|
||||
</section>
|
||||
|
||||
<!-- Large Dataset -->
|
||||
<section class="demo-section">
|
||||
<h3>Large Dataset</h3>
|
||||
<p class="demo-description">
|
||||
Transfer list with a large number of items to test performance and search functionality.
|
||||
</p>
|
||||
<ui-transfer-list
|
||||
[sourceItems]="largeSourceItems()"
|
||||
[targetItems]="largeTargetItems()"
|
||||
sourceTitle="Large Dataset ({{ largeSourceItems().length }} items)"
|
||||
targetTitle="Selected Items"
|
||||
searchPlaceholder="Search in {{ largeSourceItems().length }} items..."
|
||||
(sourceChange)="onLargeSourceChange($event)"
|
||||
(targetChange)="onLargeTargetChange($event)"
|
||||
></ui-transfer-list>
|
||||
</section>
|
||||
|
||||
<!-- Custom Labels -->
|
||||
<section class="demo-section">
|
||||
<h3>Custom Labels & Accessibility</h3>
|
||||
<ui-transfer-list
|
||||
[sourceItems]="customSourceItems()"
|
||||
[targetItems]="customTargetItems()"
|
||||
sourceTitle="Available Permissions"
|
||||
targetTitle="Granted Permissions"
|
||||
searchPlaceholder="Search permissions..."
|
||||
ariaLabel="User permission management"
|
||||
(sourceChange)="onCustomSourceChange($event)"
|
||||
(targetChange)="onCustomTargetChange($event)"
|
||||
></ui-transfer-list>
|
||||
</section>
|
||||
|
||||
<!-- Event Log -->
|
||||
<section class="demo-section">
|
||||
<h3>Event Log</h3>
|
||||
<div class="demo-event-log">
|
||||
@if (eventLog().length === 0) {
|
||||
<p class="demo-no-events">No events logged yet. Interact with the transfer lists above to see events.</p>
|
||||
} @else {
|
||||
@for (event of eventLog(); track $index) {
|
||||
<div class="demo-event">
|
||||
<strong>{{ event.type }}:</strong> {{ event.message }}
|
||||
<small class="demo-event-time">{{ event.timestamp | date:'short' }}</small>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (eventLog().length > 0) {
|
||||
<button type="button" class="demo-clear-events" (click)="clearEventLog()">
|
||||
Clear Log
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './transfer-list-demo.component.scss'
|
||||
})
|
||||
export class TransferListDemoComponent {
|
||||
// Basic example data
|
||||
basicSourceItems = signal<TransferListItem[]>([
|
||||
{ id: 1, label: 'Apple' },
|
||||
{ id: 2, label: 'Banana' },
|
||||
{ id: 3, label: 'Cherry' },
|
||||
{ id: 4, label: 'Date' },
|
||||
{ id: 5, label: 'Elderberry' },
|
||||
{ id: 6, label: 'Fig' },
|
||||
{ id: 7, label: 'Grape' },
|
||||
{ id: 8, label: 'Honeydew' }
|
||||
]);
|
||||
|
||||
basicTargetItems = signal<TransferListItem[]>([
|
||||
{ id: 9, label: 'Orange' },
|
||||
{ id: 10, label: 'Pear' }
|
||||
]);
|
||||
|
||||
// Size variant data
|
||||
sizeSourceItems = signal<TransferListItem[]>([
|
||||
{ id: 'size1', label: 'Item 1' },
|
||||
{ id: 'size2', label: 'Item 2' },
|
||||
{ id: 'size3', label: 'Item 3' }
|
||||
]);
|
||||
|
||||
sizeTargetItems = signal<TransferListItem[]>([
|
||||
{ id: 'size4', label: 'Item 4' }
|
||||
]);
|
||||
|
||||
// Configuration example data
|
||||
configSourceItems = signal<TransferListItem[]>([
|
||||
{ id: 'config1', label: 'Configuration Option 1' },
|
||||
{ id: 'config2', label: 'Configuration Option 2' },
|
||||
{ id: 'config3', label: 'Configuration Option 3' },
|
||||
{ id: 'config4', label: 'Configuration Option 4' },
|
||||
{ id: 'config5', label: 'Configuration Option 5' }
|
||||
]);
|
||||
|
||||
configTargetItems = signal<TransferListItem[]>([]);
|
||||
|
||||
// Configuration toggles
|
||||
showSearch = signal(true);
|
||||
showSelectAll = signal(true);
|
||||
showCheckboxes = signal(true);
|
||||
showMoveAll = signal(true);
|
||||
isDisabled = signal(false);
|
||||
|
||||
// Disabled items example
|
||||
disabledSourceItems = signal<TransferListItem[]>([
|
||||
{ id: 'dis1', label: 'Regular Item 1' },
|
||||
{ id: 'dis2', label: 'Disabled Item', disabled: true },
|
||||
{ id: 'dis3', label: 'Regular Item 2' },
|
||||
{ id: 'dis4', label: 'Another Disabled Item', disabled: true },
|
||||
{ id: 'dis5', label: 'Regular Item 3' }
|
||||
]);
|
||||
|
||||
disabledTargetItems = signal<TransferListItem[]>([]);
|
||||
|
||||
// Large dataset
|
||||
largeSourceItems = signal<TransferListItem[]>(this.generateLargeDataset());
|
||||
largeTargetItems = signal<TransferListItem[]>([]);
|
||||
|
||||
// Custom labels example
|
||||
customSourceItems = signal<TransferListItem[]>([
|
||||
{ id: 'perm1', label: 'Read Users', data: { permission: 'users:read' } },
|
||||
{ id: 'perm2', label: 'Write Users', data: { permission: 'users:write' } },
|
||||
{ id: 'perm3', label: 'Delete Users', data: { permission: 'users:delete' } },
|
||||
{ id: 'perm4', label: 'Read Posts', data: { permission: 'posts:read' } },
|
||||
{ id: 'perm5', label: 'Write Posts', data: { permission: 'posts:write' } },
|
||||
{ id: 'perm6', label: 'Delete Posts', data: { permission: 'posts:delete' } },
|
||||
{ id: 'perm7', label: 'Manage Settings', data: { permission: 'settings:manage' } },
|
||||
{ id: 'perm8', label: 'View Analytics', data: { permission: 'analytics:view' } }
|
||||
]);
|
||||
|
||||
customTargetItems = signal<TransferListItem[]>([]);
|
||||
|
||||
// Event logging
|
||||
eventLog = signal<Array<{ type: string; message: string; timestamp: Date }>>([]);
|
||||
|
||||
// Generate large dataset for performance testing
|
||||
private generateLargeDataset(): TransferListItem[] {
|
||||
const items: TransferListItem[] = [];
|
||||
const categories = ['Documents', 'Images', 'Videos', 'Audio', 'Archives', 'Code'];
|
||||
const types = ['Personal', 'Work', 'Project', 'Backup', 'Temp'];
|
||||
|
||||
for (let i = 1; i <= 100; i++) {
|
||||
const category = categories[i % categories.length];
|
||||
const type = types[i % types.length];
|
||||
items.push({
|
||||
id: `large${i}`,
|
||||
label: `${category} - ${type} File ${i.toString().padStart(3, '0')}`,
|
||||
data: { category, type, index: i }
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
// Event handlers for basic example
|
||||
onBasicSourceChange(items: TransferListItem[]) {
|
||||
this.basicSourceItems.set(items);
|
||||
this.logEvent('Source Change', `Basic source now has ${items.length} items`);
|
||||
}
|
||||
|
||||
onBasicTargetChange(items: TransferListItem[]) {
|
||||
this.basicTargetItems.set(items);
|
||||
this.logEvent('Target Change', `Basic target now has ${items.length} items`);
|
||||
}
|
||||
|
||||
// Event handlers for configuration example
|
||||
onConfigSourceChange(items: TransferListItem[]) {
|
||||
this.configSourceItems.set(items);
|
||||
this.logEvent('Config Source Change', `Config source now has ${items.length} items`);
|
||||
}
|
||||
|
||||
onConfigTargetChange(items: TransferListItem[]) {
|
||||
this.configTargetItems.set(items);
|
||||
this.logEvent('Config Target Change', `Config target now has ${items.length} items`);
|
||||
}
|
||||
|
||||
// Event handlers for disabled items example
|
||||
onDisabledSourceChange(items: TransferListItem[]) {
|
||||
this.disabledSourceItems.set(items);
|
||||
this.logEvent('Disabled Source Change', `Disabled source now has ${items.length} items`);
|
||||
}
|
||||
|
||||
onDisabledTargetChange(items: TransferListItem[]) {
|
||||
this.disabledTargetItems.set(items);
|
||||
this.logEvent('Disabled Target Change', `Disabled target now has ${items.length} items`);
|
||||
}
|
||||
|
||||
// Event handlers for large dataset example
|
||||
onLargeSourceChange(items: TransferListItem[]) {
|
||||
this.largeSourceItems.set(items);
|
||||
this.logEvent('Large Source Change', `Large source now has ${items.length} items`);
|
||||
}
|
||||
|
||||
onLargeTargetChange(items: TransferListItem[]) {
|
||||
this.largeTargetItems.set(items);
|
||||
this.logEvent('Large Target Change', `Large target now has ${items.length} items`);
|
||||
}
|
||||
|
||||
// Event handlers for custom example
|
||||
onCustomSourceChange(items: TransferListItem[]) {
|
||||
this.customSourceItems.set(items);
|
||||
this.logEvent('Custom Source Change', `Custom source now has ${items.length} items`);
|
||||
}
|
||||
|
||||
onCustomTargetChange(items: TransferListItem[]) {
|
||||
this.customTargetItems.set(items);
|
||||
this.logEvent('Custom Target Change', `Custom target now has ${items.length} items`);
|
||||
}
|
||||
|
||||
// Item move event handler
|
||||
onItemMove(event: { item: TransferListItem; from: 'source' | 'target'; to: 'source' | 'target' }) {
|
||||
this.logEvent('Item Move', `Moved "${event.item.label}" from ${event.from} to ${event.to}`);
|
||||
}
|
||||
|
||||
// Configuration toggle methods
|
||||
toggleSearch() {
|
||||
this.showSearch.update(value => !value);
|
||||
this.logEvent('Config Change', `Search ${this.showSearch() ? 'enabled' : 'disabled'}`);
|
||||
}
|
||||
|
||||
toggleSelectAll() {
|
||||
this.showSelectAll.update(value => !value);
|
||||
this.logEvent('Config Change', `Select All ${this.showSelectAll() ? 'enabled' : 'disabled'}`);
|
||||
}
|
||||
|
||||
toggleCheckboxes() {
|
||||
this.showCheckboxes.update(value => !value);
|
||||
this.logEvent('Config Change', `Checkboxes ${this.showCheckboxes() ? 'enabled' : 'disabled'}`);
|
||||
}
|
||||
|
||||
toggleMoveAll() {
|
||||
this.showMoveAll.update(value => !value);
|
||||
this.logEvent('Config Change', `Move All buttons ${this.showMoveAll() ? 'enabled' : 'disabled'}`);
|
||||
}
|
||||
|
||||
toggleDisabled() {
|
||||
this.isDisabled.update(value => !value);
|
||||
this.logEvent('Config Change', `Component ${this.isDisabled() ? 'disabled' : 'enabled'}`);
|
||||
}
|
||||
|
||||
// Event logging
|
||||
private logEvent(type: string, message: string) {
|
||||
this.eventLog.update(log => [
|
||||
{ type, message, timestamp: new Date() },
|
||||
...log.slice(0, 9) // Keep only the last 10 events
|
||||
]);
|
||||
}
|
||||
|
||||
clearEventLog() {
|
||||
this.eventLog.set([]);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
faGripVertical, faArrowsAlt, faBoxOpen, faChevronLeft, faSpinner, faExclamationTriangle,
|
||||
faCloudUploadAlt, faFileText, faListAlt, faCircle, faExpandArrowsAlt, faCircleNotch, faSliders,
|
||||
faMinus, faInfoCircle, faChevronDown, faCaretUp, faExclamationCircle, faSitemap, faStream,
|
||||
faBell, faRoute, faChevronUp, faEllipsisV, faCut, faPalette, faExchangeAlt, faTools
|
||||
faBell, faRoute, faChevronUp, faEllipsisV, faCut, faPalette, faExchangeAlt, faTools, faHeart
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { DemoRoutes } from '../../demos';
|
||||
import { ScrollContainerComponent } from '../../../../../ui-essentials/src/lib/layouts/scroll-container.component';
|
||||
@@ -107,6 +107,7 @@ export class DashboardComponent {
|
||||
faPalette = faPalette;
|
||||
faExchangeAlt = faExchangeAlt;
|
||||
faTools = faTools;
|
||||
faHeart = faHeart;
|
||||
|
||||
menuItems: any = []
|
||||
|
||||
@@ -153,7 +154,8 @@ export class DashboardComponent {
|
||||
this.createChildItem("file-upload", "File Upload", this.faCloudUploadAlt),
|
||||
this.createChildItem("form-field", "Form Field", this.faFileText),
|
||||
this.createChildItem("range-slider", "Range Slider", this.faSliders),
|
||||
this.createChildItem("color-picker", "Color Picker", this.faPalette)
|
||||
this.createChildItem("color-picker", "Color Picker", this.faPalette),
|
||||
this.createChildItem("tag-input", "Tag Input", this.faTags)
|
||||
];
|
||||
this.addMenuItem("forms", "Forms", this.faEdit, formsChildren);
|
||||
|
||||
@@ -196,6 +198,7 @@ export class DashboardComponent {
|
||||
// Actions category
|
||||
const actionsChildren = [
|
||||
this.createChildItem("buttons", "Buttons", this.faMousePointer),
|
||||
this.createChildItem("icon-button", "Icon Buttons", this.faHeart),
|
||||
this.createChildItem("fab-menu", "FAB Menu", this.faEllipsisV),
|
||||
this.createChildItem("split-button", "Split Button", this.faCut)
|
||||
];
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { Component, EventEmitter, Output, Input, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import { MenuItemComponent } from '../../../../../ui-essentials/src/lib/components/navigation/menu/menu-item.component';
|
||||
import { faChevronRight, faChevronDown } from '@fortawesome/free-solid-svg-icons';
|
||||
import {
|
||||
MenuContainerComponent,
|
||||
MenuSubmenuComponent,
|
||||
MenuItemComponent,
|
||||
MenuItemData
|
||||
} from '../../../../../ui-essentials/src/lib/components/navigation/menu';
|
||||
|
||||
export interface SidebarMenuItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: any; // FontAwesome icon definition
|
||||
active: boolean;
|
||||
export interface SidebarMenuItem extends MenuItemData {
|
||||
children?: SidebarMenuItem[];
|
||||
expanded?: boolean;
|
||||
isParent?: boolean;
|
||||
@@ -19,48 +18,49 @@ export interface SidebarMenuItem {
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FontAwesomeModule,
|
||||
MenuContainerComponent,
|
||||
MenuSubmenuComponent,
|
||||
MenuItemComponent
|
||||
],
|
||||
template: `
|
||||
<div class="sidebar-container">
|
||||
<ui-menu-container
|
||||
[elevation]="'none'"
|
||||
[spacing]="'none'"
|
||||
[rounded]="false"
|
||||
class="sidebar-container"
|
||||
>
|
||||
@for(item of data; track item.id) {
|
||||
<div class="menu-item-wrapper">
|
||||
@if(item.isParent) {
|
||||
<div class="parent-menu-item" (click)="toggleExpanded(item)">
|
||||
<div class="parent-content">
|
||||
<fa-icon [icon]="item.icon" class="parent-icon"></fa-icon>
|
||||
<span class="parent-label">{{ item.label }}</span>
|
||||
</div>
|
||||
<fa-icon
|
||||
[icon]="item.expanded ? faChevronDown : faChevronRight"
|
||||
class="chevron-icon">
|
||||
</fa-icon>
|
||||
</div>
|
||||
@if(item.expanded && item.children) {
|
||||
<div class="children-container">
|
||||
@for(child of item.children; track child.id) {
|
||||
<ui-menu-item
|
||||
[data]="{ label: child.label, icon: child.icon, active: child.active }"
|
||||
[size]="'md'"
|
||||
iconPosition="left"
|
||||
(itemClick)="handleMenuClick(child.id)"
|
||||
class="child-menu-item"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
@if(item.isParent && item.children) {
|
||||
<ui-menu-submenu
|
||||
[parentItem]="getMenuItemData(item)"
|
||||
[size]="'md'"
|
||||
[iconPosition]="'left'"
|
||||
[isOpen]="item.expanded || false"
|
||||
[submenuElevation]="'none'"
|
||||
[submenuSpacing]="'xs'"
|
||||
(submenuToggled)="onSubmenuToggled($event)"
|
||||
(parentItemClick)="handleMenuClick($event.id || '')"
|
||||
>
|
||||
@for(child of item.children; track child.id) {
|
||||
<ui-menu-item
|
||||
[data]="getMenuItemData(child)"
|
||||
[size]="'sm'"
|
||||
[iconPosition]="'left'"
|
||||
[indent]="true"
|
||||
(itemClick)="handleMenuClick($event.id || '')"
|
||||
/>
|
||||
}
|
||||
} @else {
|
||||
<ui-menu-item
|
||||
[data]="{ label: item.label, icon: item.icon, active: item.active }"
|
||||
[size]="'md'"
|
||||
iconPosition="left"
|
||||
(itemClick)="handleMenuClick(item.id)"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</ui-menu-submenu>
|
||||
} @else {
|
||||
<ui-menu-item
|
||||
[data]="getMenuItemData(item)"
|
||||
[size]="'md'"
|
||||
[iconPosition]="'left'"
|
||||
(itemClick)="handleMenuClick($event.id || '')"
|
||||
/>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</ui-menu-container>
|
||||
`,
|
||||
styleUrls: ['./dashboard.sidebar.component.scss']
|
||||
|
||||
@@ -71,9 +71,6 @@ export class DashboardSidebarComponent implements OnInit {
|
||||
@Input() set active(value: string) { this.setActive(value) }
|
||||
@Output() selected = new EventEmitter<string>();
|
||||
|
||||
faChevronRight = faChevronRight;
|
||||
faChevronDown = faChevronDown;
|
||||
|
||||
handleMenuClick(id: string): void {
|
||||
this.setActive(id);
|
||||
this.selected.emit(id);
|
||||
@@ -126,6 +123,35 @@ export class DashboardSidebarComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
getMenuItemData(item: SidebarMenuItem): MenuItemData {
|
||||
return {
|
||||
id: item.id,
|
||||
label: item.label,
|
||||
icon: item.icon,
|
||||
active: item.active,
|
||||
hasSubmenu: item.isParent && !!item.children,
|
||||
disabled: item.disabled
|
||||
};
|
||||
}
|
||||
|
||||
onSubmenuToggled(event: { item: MenuItemData; isOpen: boolean }): void {
|
||||
const sidebarItem = this.findItemById(this.data, event.item.id || '');
|
||||
if (sidebarItem && sidebarItem.isParent) {
|
||||
sidebarItem.expanded = event.isOpen;
|
||||
}
|
||||
}
|
||||
|
||||
private findItemById(items: SidebarMenuItem[], id: string): SidebarMenuItem | undefined {
|
||||
for (const item of items) {
|
||||
if (item.id === id) return item;
|
||||
if (item.children) {
|
||||
const found = this.findItemById(item.children, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user