Added extensive component library with feedback components (empty state, loading spinner, skeleton loader), enhanced form components (autocomplete, date picker, file upload, form field, time picker), navigation components (pagination), and overlay components (backdrop, drawer, modal, overlay container). Updated demo application with comprehensive showcase components and enhanced styling throughout the project. Excluded font files from repository to reduce size. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
622 lines
20 KiB
TypeScript
622 lines
20 KiB
TypeScript
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import {
|
|
ListItemComponent,
|
|
ListContainerComponent,
|
|
ListItemData
|
|
} from '../../../../../ui-essentials/src/lib/components/data-display/list';
|
|
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
|
import {
|
|
faInbox,
|
|
faStar,
|
|
faPaperPlane,
|
|
faEdit,
|
|
faArchive,
|
|
faTrash,
|
|
faPhone,
|
|
faComment,
|
|
faPlay,
|
|
faDownload,
|
|
faCheck,
|
|
faChevronRight,
|
|
faPlus,
|
|
faMinus
|
|
} from '@fortawesome/free-solid-svg-icons';
|
|
|
|
@Component({
|
|
selector: 'ui-list-demo',
|
|
standalone: true,
|
|
imports: [
|
|
CommonModule,
|
|
ListItemComponent,
|
|
ListContainerComponent,
|
|
FontAwesomeModule
|
|
],
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
template: `
|
|
<div style="padding: 2rem;">
|
|
<h2>List Component Showcase</h2>
|
|
|
|
<!-- Basic Lists with Size Variants -->
|
|
<section style="margin-bottom: 3rem;">
|
|
<h3>Basic Lists - Size Variants</h3>
|
|
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2rem; margin-bottom: 2rem;">
|
|
<div>
|
|
<h4>Small (sm)</h4>
|
|
<ui-list-container
|
|
elevation="sm"
|
|
spacing="xs"
|
|
[rounded]="true"
|
|
ariaLabel="Small navigation list">
|
|
@for (item of navigationItems; track item.primary) {
|
|
<ui-list-item
|
|
[data]="item"
|
|
size="sm"
|
|
lines="one"
|
|
variant="text"
|
|
[divider]="true"
|
|
>
|
|
<fa-icon [icon]="getIconForItem(item.primary)" slot="trailing"></fa-icon>
|
|
</ui-list-item>
|
|
}
|
|
</ui-list-container>
|
|
</div>
|
|
|
|
<div>
|
|
<h4>Medium (md)</h4>
|
|
<ui-list-container
|
|
elevation="sm"
|
|
spacing="xs"
|
|
[rounded]="true"
|
|
ariaLabel="Medium navigation list">
|
|
@for (item of navigationItems; track item.primary) {
|
|
<ui-list-item
|
|
[data]="item"
|
|
size="md"
|
|
lines="one"
|
|
variant="text"
|
|
[divider]="true"
|
|
>
|
|
<fa-icon [icon]="getIconForItem(item.primary)" slot="trailing"></fa-icon>
|
|
</ui-list-item>
|
|
}
|
|
</ui-list-container>
|
|
</div>
|
|
|
|
<div>
|
|
<h4>Large (lg)</h4>
|
|
<ui-list-container
|
|
elevation="sm"
|
|
spacing="xs"
|
|
[rounded]="true"
|
|
ariaLabel="Large navigation list">
|
|
@for (item of navigationItems; track item.primary) {
|
|
<ui-list-item
|
|
[data]="item"
|
|
size="lg"
|
|
lines="one"
|
|
variant="text"
|
|
[divider]="true"
|
|
>
|
|
<fa-icon [icon]="getIconForItem(item.primary)" slot="trailing"></fa-icon>
|
|
</ui-list-item>
|
|
}
|
|
</ui-list-container>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Multi-line Lists -->
|
|
<section style="margin-bottom: 3rem;">
|
|
<h3>Multi-line Lists</h3>
|
|
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 2rem;">
|
|
<div>
|
|
<h4>Two Lines</h4>
|
|
<ui-list-container
|
|
elevation="md"
|
|
spacing="sm"
|
|
[rounded]="true"
|
|
ariaLabel="Two line task list">
|
|
@for (item of twoLineItems; track item.primary) {
|
|
<ui-list-item
|
|
[data]="item"
|
|
size="md"
|
|
lines="two"
|
|
variant="text"
|
|
>
|
|
<button
|
|
slot="trailing"
|
|
style="background: none; border: none; padding: 8px; cursor: pointer; color: #6c757d;"
|
|
(click)="handleAction('complete', item.primary)"
|
|
[attr.aria-label]="'Complete ' + item.primary">
|
|
<fa-icon [icon]="faCheck"></fa-icon>
|
|
</button>
|
|
</ui-list-item>
|
|
}
|
|
</ui-list-container>
|
|
</div>
|
|
|
|
<div>
|
|
<h4>Three Lines</h4>
|
|
<ui-list-container
|
|
elevation="md"
|
|
spacing="sm"
|
|
[rounded]="true"
|
|
ariaLabel="Three line task list">
|
|
@for (item of threeLineItems; track item.primary) {
|
|
<ui-list-item
|
|
[data]="item"
|
|
size="lg"
|
|
lines="three"
|
|
variant="text"
|
|
>
|
|
<div slot="trailing" style="display: flex; gap: 0.5rem;">
|
|
<button
|
|
style="background: none; border: none; padding: 4px; cursor: pointer; color: #28a745;"
|
|
(click)="handleAction('approve', item.primary)"
|
|
[attr.aria-label]="'Approve ' + item.primary">
|
|
<fa-icon [icon]="faCheck"></fa-icon>
|
|
</button>
|
|
</div>
|
|
</ui-list-item>
|
|
}
|
|
</ui-list-container>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Avatar Lists -->
|
|
<section style="margin-bottom: 3rem;">
|
|
<h3>Lists with Avatars</h3>
|
|
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 2rem;">
|
|
<div>
|
|
<h4>Contacts - Avatar + One Line</h4>
|
|
<ui-list-container
|
|
elevation="sm"
|
|
spacing="xs"
|
|
[rounded]="true"
|
|
ariaLabel="Contact list">
|
|
@for (item of contactItems; track item.primary) {
|
|
<ui-list-item
|
|
[data]="item"
|
|
size="md"
|
|
lines="one"
|
|
variant="avatar"
|
|
>
|
|
<div slot="trailing" style="display: flex; gap: 0.75rem;">
|
|
<button
|
|
style="background: none; border: none; padding: 4px; cursor: pointer; color: #007bff;"
|
|
(click)="handleAction('call', item.primary)"
|
|
[attr.aria-label]="'Call ' + item.primary">
|
|
<fa-icon [icon]="faPhone"></fa-icon>
|
|
</button>
|
|
<button
|
|
style="background: none; border: none; padding: 4px; cursor: pointer; color: #28a745;"
|
|
(click)="handleAction('message', item.primary)"
|
|
[attr.aria-label]="'Message ' + item.primary">
|
|
<fa-icon [icon]="faComment"></fa-icon>
|
|
</button>
|
|
</div>
|
|
</ui-list-item>
|
|
}
|
|
</ui-list-container>
|
|
</div>
|
|
|
|
<div>
|
|
<h4>Messages - Avatar + Two Lines</h4>
|
|
<ui-list-container
|
|
elevation="sm"
|
|
spacing="xs"
|
|
[rounded]="true"
|
|
ariaLabel="Message list">
|
|
@for (item of messageItems; track item.primary) {
|
|
<ui-list-item
|
|
[data]="item"
|
|
size="lg"
|
|
lines="two"
|
|
variant="avatar"
|
|
>
|
|
<span
|
|
slot="trailing"
|
|
style="font-size: 0.75rem; color: #6c757d;">
|
|
2h
|
|
</span>
|
|
</ui-list-item>
|
|
}
|
|
</ui-list-container>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Media Lists -->
|
|
<section style="margin-bottom: 3rem;">
|
|
<h3>Lists with Media</h3>
|
|
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); gap: 2rem;">
|
|
<div>
|
|
<h4>Music - Media + Two Lines</h4>
|
|
<ui-list-container
|
|
elevation="md"
|
|
spacing="sm"
|
|
[rounded]="true"
|
|
ariaLabel="Music playlist">
|
|
@for (item of musicItems; track item.primary) {
|
|
<ui-list-item
|
|
[data]="item"
|
|
size="lg"
|
|
lines="two"
|
|
variant="media"
|
|
>
|
|
<div slot="trailing" style="display: flex; gap: 0.75rem; align-items: center;">
|
|
<span style="font-size: 0.875rem; color: #6c757d;">3:42</span>
|
|
<button
|
|
style="background: none; border: none; padding: 8px; cursor: pointer; color: #007bff;"
|
|
(click)="handleAction('play', item.primary)"
|
|
[attr.aria-label]="'Play ' + item.primary">
|
|
<fa-icon [icon]="faPlay"></fa-icon>
|
|
</button>
|
|
</div>
|
|
</ui-list-item>
|
|
}
|
|
</ui-list-container>
|
|
</div>
|
|
|
|
<div>
|
|
<h4>Videos - Media + Three Lines</h4>
|
|
<ui-list-container
|
|
elevation="md"
|
|
spacing="sm"
|
|
[rounded]="true"
|
|
ariaLabel="Video library">
|
|
@for (item of videoItems; track item.primary) {
|
|
<ui-list-item
|
|
[data]="item"
|
|
size="lg"
|
|
lines="three"
|
|
variant="media"
|
|
>
|
|
<div slot="trailing" style="display: flex; flex-direction: column; align-items: center; gap: 0.5rem;">
|
|
<div style="display: flex; align-items: center; gap: 0.25rem;">
|
|
<fa-icon [icon]="faStar" style="color: #ffc107; font-size: 0.75rem;"></fa-icon>
|
|
<span style="font-size: 0.75rem; color: #6c757d;">4.8</span>
|
|
</div>
|
|
<button
|
|
style="background: none; border: none; padding: 4px; cursor: pointer; color: #007bff;"
|
|
(click)="handleAction('download', item.primary)"
|
|
[attr.aria-label]="'Download ' + item.primary">
|
|
<fa-icon [icon]="faDownload"></fa-icon>
|
|
</button>
|
|
</div>
|
|
</ui-list-item>
|
|
}
|
|
</ui-list-container>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Interactive Demo -->
|
|
<section style="margin-bottom: 3rem;">
|
|
<h3>Interactive Demo</h3>
|
|
<p style="margin-bottom: 1rem; color: #6c757d;">
|
|
Click items to interact with them:
|
|
</p>
|
|
|
|
<div style="max-width: 600px;">
|
|
<ui-list-container
|
|
elevation="sm"
|
|
spacing="sm"
|
|
[rounded]="true"
|
|
ariaLabel="Interactive demo list">
|
|
@for (item of interactiveItems(); track item.primary) {
|
|
<ui-list-item
|
|
[data]="item"
|
|
size="md"
|
|
lines="two"
|
|
variant="avatar"
|
|
style="cursor: pointer;"
|
|
(click)="toggleInteractiveItem(item.primary)"
|
|
>
|
|
<div slot="trailing" style="display: flex; align-items: center; gap: 0.5rem;">
|
|
@if (item.selected) {
|
|
<span style="background: #28a745; color: white; padding: 4px 8px; border-radius: 12px; font-size: 0.75rem;">
|
|
Enabled
|
|
</span>
|
|
} @else if (item.disabled) {
|
|
<span style="background: #dc3545; color: white; padding: 4px 8px; border-radius: 12px; font-size: 0.75rem;">
|
|
Disabled
|
|
</span>
|
|
} @else {
|
|
<span style="background: #6c757d; color: white; padding: 4px 8px; border-radius: 12px; font-size: 0.75rem;">
|
|
Click to enable
|
|
</span>
|
|
}
|
|
</div>
|
|
</ui-list-item>
|
|
}
|
|
</ui-list-container>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Code 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 List with Icons:</h4>
|
|
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto; font-size: 0.875rem;"><code><ui-list-container elevation="sm" spacing="xs" [rounded]="true">
|
|
<ui-list-item
|
|
[data]="{primary: 'Inbox'}"
|
|
size="md"
|
|
lines="one"
|
|
variant="text">
|
|
<fa-icon [icon]="faInbox" slot="trailing"></fa-icon>
|
|
</ui-list-item>
|
|
</ui-list-container></code></pre>
|
|
|
|
<h4>List with Avatar and Actions:</h4>
|
|
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto; font-size: 0.875rem;"><code><ui-list-container elevation="md" spacing="sm" [rounded]="true">
|
|
<ui-list-item
|
|
[data]="{
|
|
primary: 'John Doe',
|
|
secondary: 'john@example.com',
|
|
avatarSrc: 'avatar.jpg'
|
|
}"
|
|
size="lg"
|
|
lines="two"
|
|
variant="avatar">
|
|
<button slot="trailing" (click)="call()">
|
|
<fa-icon [icon]="faPhone"></fa-icon>
|
|
</button>
|
|
</ui-list-item>
|
|
</ui-list-container></code></pre>
|
|
</div>
|
|
</section>
|
|
|
|
@if (lastAction()) {
|
|
<div style="position: fixed; bottom: 20px; right: 20px; background: #007bff; color: white; padding: 1rem; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); max-width: 300px;">
|
|
<strong>Action:</strong> {{ lastAction() }}
|
|
</div>
|
|
}
|
|
</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;
|
|
margin-top: 2rem;
|
|
}
|
|
|
|
h4 {
|
|
color: hsl(287, 12%, 35%);
|
|
font-size: 1.125rem;
|
|
margin-bottom: 0.75rem;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
section {
|
|
border: 1px solid hsl(289, 14%, 90%);
|
|
border-radius: 12px;
|
|
padding: 1.5rem;
|
|
background: hsl(286, 20%, 99%);
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
pre {
|
|
font-size: 0.875rem;
|
|
line-height: 1.5;
|
|
margin: 0.5rem 0;
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
code {
|
|
font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Courier New', monospace;
|
|
color: #d63384;
|
|
}
|
|
|
|
button {
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
button:hover {
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
button:active {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
fa-icon {
|
|
transition: color 0.2s ease;
|
|
}
|
|
`]
|
|
})
|
|
export class ListDemoComponent {
|
|
// State signals
|
|
lastAction = signal('');
|
|
|
|
// Demo data
|
|
navigationItems: ListItemData[] = [
|
|
{ primary: 'Inbox' },
|
|
{ primary: 'Starred' },
|
|
{ primary: 'Sent' },
|
|
{ primary: 'Drafts' },
|
|
{ primary: 'Archive' },
|
|
{ primary: 'Trash' }
|
|
];
|
|
|
|
twoLineItems: ListItemData[] = [
|
|
{
|
|
primary: 'Review Design System Updates',
|
|
secondary: 'Check new component specifications and design tokens'
|
|
},
|
|
{
|
|
primary: 'Team Standup Meeting',
|
|
secondary: 'Daily sync with development and design teams'
|
|
},
|
|
{
|
|
primary: 'Code Review - Authentication',
|
|
secondary: 'Review pull request #247 for OAuth integration'
|
|
}
|
|
];
|
|
|
|
threeLineItems: ListItemData[] = [
|
|
{
|
|
primary: 'Angular 19 Migration',
|
|
secondary: 'Upgrade project to latest Angular version with control flow',
|
|
tertiary: 'Estimated completion: Next Sprint • Priority: High'
|
|
},
|
|
{
|
|
primary: 'Design System Documentation',
|
|
secondary: 'Create comprehensive documentation for all components',
|
|
tertiary: 'Status: In Progress • Assigned: Design Team'
|
|
}
|
|
];
|
|
|
|
contactItems: ListItemData[] = [
|
|
{
|
|
primary: 'Sarah Wilson',
|
|
avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=sarah',
|
|
avatarAlt: 'Sarah Wilson avatar'
|
|
},
|
|
{
|
|
primary: 'Alex Chen',
|
|
avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=alex',
|
|
avatarAlt: 'Alex Chen avatar'
|
|
},
|
|
{
|
|
primary: 'Mike Rodriguez',
|
|
avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=mike',
|
|
avatarAlt: 'Mike Rodriguez avatar'
|
|
}
|
|
];
|
|
|
|
messageItems: ListItemData[] = [
|
|
{
|
|
primary: 'John Doe',
|
|
secondary: 'Hey! How\'s the new component library coming along?',
|
|
avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=john',
|
|
avatarAlt: 'John Doe avatar'
|
|
},
|
|
{
|
|
primary: 'Jane Smith',
|
|
secondary: 'The design tokens look great. Ready for review when you are.',
|
|
avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=jane',
|
|
avatarAlt: 'Jane Smith avatar'
|
|
}
|
|
];
|
|
|
|
musicItems: ListItemData[] = [
|
|
{
|
|
primary: 'Bohemian Rhapsody',
|
|
secondary: 'Queen • A Night at the Opera',
|
|
mediaSrc: 'https://picsum.photos/80/80?random=1',
|
|
mediaAlt: 'Album cover'
|
|
},
|
|
{
|
|
primary: 'Hotel California',
|
|
secondary: 'Eagles • Hotel California',
|
|
mediaSrc: 'https://picsum.photos/80/80?random=2',
|
|
mediaAlt: 'Album cover'
|
|
}
|
|
];
|
|
|
|
videoItems: ListItemData[] = [
|
|
{
|
|
primary: 'Angular Fundamentals',
|
|
secondary: 'Complete guide to modern Angular development',
|
|
tertiary: 'Duration: 2h 30m • Updated: Today • 1.2M views',
|
|
mediaSrc: 'https://picsum.photos/80/80?random=5',
|
|
mediaAlt: 'Video thumbnail'
|
|
}
|
|
];
|
|
|
|
interactiveItems = signal<ListItemData[]>([
|
|
{
|
|
primary: 'Email Notifications',
|
|
secondary: 'Receive emails about important updates',
|
|
avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=email',
|
|
selected: false,
|
|
disabled: false
|
|
},
|
|
{
|
|
primary: 'SMS Alerts',
|
|
secondary: 'Get text messages for urgent notifications',
|
|
avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=sms',
|
|
selected: true,
|
|
disabled: false
|
|
},
|
|
{
|
|
primary: 'Marketing Communications',
|
|
secondary: 'Promotional offers and product updates',
|
|
avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=marketing',
|
|
selected: false,
|
|
disabled: true
|
|
}
|
|
]);
|
|
|
|
// Font Awesome icons
|
|
faInbox = faInbox;
|
|
faStar = faStar;
|
|
faPaperPlane = faPaperPlane;
|
|
faEdit = faEdit;
|
|
faArchive = faArchive;
|
|
faTrash = faTrash;
|
|
faPhone = faPhone;
|
|
faComment = faComment;
|
|
faPlay = faPlay;
|
|
faDownload = faDownload;
|
|
faCheck = faCheck;
|
|
faChevronRight = faChevronRight;
|
|
faPlus = faPlus;
|
|
faMinus = faMinus;
|
|
|
|
// Helper methods
|
|
getIconForItem(itemName: string): any {
|
|
const iconMap: { [key: string]: any } = {
|
|
'Inbox': faInbox,
|
|
'Starred': faStar,
|
|
'Sent': faPaperPlane,
|
|
'Drafts': faEdit,
|
|
'Archive': faArchive,
|
|
'Trash': faTrash
|
|
};
|
|
return iconMap[itemName] || faInbox;
|
|
}
|
|
|
|
// Event handlers
|
|
handleAction(action: string, item?: string): void {
|
|
const message = item
|
|
? `${action} action on "${item}" at ${new Date().toLocaleTimeString()}`
|
|
: `${action} action at ${new Date().toLocaleTimeString()}`;
|
|
|
|
this.lastAction.set(message);
|
|
console.log(`List action: ${action}`, item);
|
|
|
|
// Clear the message after 3 seconds
|
|
setTimeout(() => this.lastAction.set(''), 3000);
|
|
}
|
|
|
|
toggleInteractiveItem(itemName: string): void {
|
|
this.interactiveItems.update(items =>
|
|
items.map(item => {
|
|
if (item.primary === itemName && !item.disabled) {
|
|
return { ...item, selected: !item.selected };
|
|
}
|
|
return item;
|
|
})
|
|
);
|
|
|
|
this.handleAction('toggle', itemName);
|
|
}
|
|
} |