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:
skyai_dev
2025-09-03 21:50:45 +10:00
parent e9f975eb02
commit 2bbbf1b9f1
87 changed files with 15454 additions and 206 deletions

View File

@@ -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%;
}
}

View File

@@ -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++;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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]
})

View File

@@ -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;
}
}

View File

@@ -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>&lt;ui-enhanced-table
[data]="employees"
[columns]="columns"
variant="default"
[enableColumnResize]="true"
[enableColumnReorder]="true"
[showFilters]="true"
[height]="500"
(sort)="handleSort($event)"
(filter)="handleFilter($event)"&gt;
&lt;/ui-enhanced-table&gt;</code></pre>
<h4>With Virtual Scrolling:</h4>
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>&lt;ui-enhanced-table
[data]="largeDataset"
[columns]="columns"
[virtualScrollConfig]="{{ '{' }}
enabled: true,
itemHeight: 48,
bufferSize: 10
{{ '}' }}"
[height]="600"&gt;
&lt;/ui-enhanced-table&gt;</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}`;
}
}

View File

@@ -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;
}

View File

@@ -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');
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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'}`);
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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';

View File

@@ -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>&lt;ui-tag-input
placeholder="Add tags..."
(tagAdd)="onTagAdd($event)"
(tagRemove)="onTagRemove($event)"&gt;
&lt;/ui-tag-input&gt;</code></pre>
<h4>With Constraints:</h4>
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>&lt;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)"&gt;
&lt;/ui-tag-input&gt;</code></pre>
<h4>Form Integration:</h4>
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>&lt;form [formGroup]="myForm"&gt;
&lt;ui-tag-input
formControlName="tags"
placeholder="Required tags"
[errorMessage]="getFieldError('tags')"&gt;
&lt;/ui-tag-input&gt;
&lt;/form&gt;</code></pre>
<h4>Custom Separators:</h4>
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>&lt;ui-tag-input
placeholder="Use space or dash to separate"
[separators]="[' ', '-']"
helpText="Tags separated by space or dash"&gt;
&lt;/ui-tag-input&gt;</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';
}
}

View File

@@ -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;
}

View File

@@ -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',

View File

@@ -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;
}
}

View File

@@ -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([]);
}
}