Add comprehensive library expansion with new components and demos

- Add new libraries: ui-accessibility, ui-animations, ui-backgrounds, ui-code-display, ui-data-utils, ui-font-manager, hcl-studio
- Add extensive layout components: gallery-grid, infinite-scroll-container, kanban-board, masonry, split-view, sticky-layout
- Add comprehensive demo components for all new features
- Update project configuration and dependencies
- Expand component exports and routing structure
- Add UI landing pages planning document

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
skyai_dev
2025-09-05 05:37:37 +10:00
parent 876eb301a0
commit 5346d6d0c9
5476 changed files with 350855 additions and 10 deletions

View File

@@ -1,8 +1,48 @@
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { ApplicationConfig, provideZoneChangeDetection, importProvidersFrom } from '@angular/core';
import { provideRouter } from '@angular/router';
import { UiAccessibilityModule } from 'ui-accessibility';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes)]
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
importProvidersFrom(
UiAccessibilityModule.forRoot({
announcer: {
defaultPoliteness: 'polite',
defaultDuration: 4000,
enabled: true
},
focusManagement: {
trapFocus: true,
restoreFocus: true,
focusVisibleEnabled: true
},
keyboard: {
enableShortcuts: true,
enableArrowNavigation: true
},
skipLinks: {
enabled: true,
position: 'top',
links: [
{ href: '#main', text: 'Skip to main content' },
{ href: '#nav', text: 'Skip to navigation' }
]
},
accessibility: {
respectReducedMotion: true,
respectHighContrast: true,
injectAccessibilityStyles: true,
addBodyClasses: true
},
development: {
warnings: true,
logging: true
}
})
)
]
};

View File

@@ -0,0 +1,274 @@
<div class="accessibility-demo">
<!-- Skip Links Demo -->
<ui-skip-links></ui-skip-links>
<header class="demo-header">
<h1 id="main">UI Accessibility Library Demo</h1>
<p>Interactive examples of accessibility features with WCAG 2.1 Level AA compliance.</p>
<ui-screen-reader-only>
This page demonstrates various accessibility features. Use keyboard shortcuts: Ctrl+Shift+A to make announcements, Ctrl+M to toggle modal.
</ui-screen-reader-only>
</header>
<!-- Current Preferences Display -->
<section class="demo-section" aria-labelledby="preferences-heading">
<h2 id="preferences-heading">Accessibility Preferences</h2>
<div class="preferences-display">
<p><strong>High Contrast:</strong> {{ currentPreferences.prefersHighContrast ? 'Enabled' : 'Disabled' }}</p>
<p><strong>Reduced Motion:</strong> {{ currentPreferences.prefersReducedMotion ? 'Enabled' : 'Disabled' }}</p>
<p><strong>Color Scheme:</strong> {{ currentPreferences.colorScheme || 'Not detected' }}</p>
<ui-button variant="tonal" size="small" (click)="refreshPreferences()">
Refresh Preferences
</ui-button>
</div>
</section>
<!-- Live Announcements Demo -->
<section class="demo-section" aria-labelledby="announcements-heading">
<h2 id="announcements-heading">Live Announcements</h2>
<div class="demo-controls">
<div class="input-group">
<label for="announcement-text">Announcement Text:</label>
<input
id="announcement-text"
type="text"
[(ngModel)]="announcementText"
class="demo-input"
>
</div>
<div class="input-group">
<label for="politeness-level">Politeness Level:</label>
<select id="politeness-level" [(ngModel)]="selectedPoliteness" class="demo-select">
<option value="polite">Polite</option>
<option value="assertive">Assertive</option>
</select>
</div>
<div class="button-group">
<ui-button (click)="makeAnnouncement()">Make Announcement</ui-button>
<ui-button variant="tonal" (click)="makeSequenceAnnouncement()">Sequence Announcement</ui-button>
</div>
<div class="button-group">
<ui-button variant="filled" (click)="announceSuccess()">Success Message</ui-button>
<ui-button variant="outlined" (click)="announceError()">Error Message</ui-button>
<ui-button variant="tonal" (click)="announceLoading()">Loading Message</ui-button>
<ui-button variant="outlined" (click)="simulateFormValidation()">Simulate Validation</ui-button>
</div>
</div>
</section>
<!-- Focus Management Demo -->
<section class="demo-section" aria-labelledby="focus-heading">
<h2 id="focus-heading">Focus Management</h2>
<div class="focus-demo">
<div class="input-group">
<label for="monitored-input">Monitored Input (Focus Origin: {{ focusOrigin }}):</label>
<input
#monitoredInput
id="monitored-input"
type="text"
placeholder="Click, tab, or programmatically focus this input"
class="demo-input"
>
</div>
<div class="button-group">
<ui-button (click)="focusViaKeyboard()">Focus via Keyboard</ui-button>
<ui-button (click)="focusViaMouse()">Focus via Mouse</ui-button>
<ui-button (click)="focusViaProgram()">Focus via Program</ui-button>
</div>
</div>
</section>
<!-- Focus Trap Demo -->
<section class="demo-section" aria-labelledby="trap-heading">
<h2 id="trap-heading">Focus Trap</h2>
<div class="trap-controls">
<ui-button (click)="toggleFocusTrap()">
{{ trapEnabled ? 'Disable' : 'Enable' }} Focus Trap
</ui-button>
</div>
<div
#trapContainer
class="focus-trap-container"
[class.trap-enabled]="trapEnabled"
[uiFocusTrap]="trapEnabled"
[autoFocus]="true"
[restoreFocus]="true"
>
<h3>Focus Trap Container {{ trapEnabled ? '(Active)' : '(Inactive)' }}</h3>
<p>When enabled, Tab navigation will be trapped within this container.</p>
<input type="text" placeholder="First input" class="demo-input">
<input type="text" placeholder="Second input" class="demo-input">
<ui-button>Trapped Button 1</ui-button>
<ui-button variant="tonal">Trapped Button 2</ui-button>
<select class="demo-select">
<option>Option 1</option>
<option>Option 2</option>
</select>
<textarea placeholder="Textarea" class="demo-textarea"></textarea>
</div>
</section>
<!-- Arrow Navigation Demo -->
<section class="demo-section" aria-labelledby="navigation-heading">
<h2 id="navigation-heading">Arrow Navigation</h2>
<div class="navigation-demo">
<h3>Vertical Navigation (Use ↑↓ arrows):</h3>
<ul
class="navigation-list vertical"
uiArrowNavigation="vertical"
[wrap]="true"
(navigationChange)="onNavigationChange($event)"
>
@for (item of navigationItems; track item.id) {
<li
[tabindex]="$first ? '0' : '-1'"
[class.disabled]="item.disabled"
[attr.aria-disabled]="item.disabled"
>
{{ item.label }}
</li>
}
</ul>
<h3>Grid Navigation (Use ↑↓←→ arrows, 3 columns):</h3>
<div
class="navigation-grid"
uiArrowNavigation="grid"
[gridColumns]="3"
[wrap]="true"
(navigationChange)="onGridNavigationChange($event)"
>
@for (item of gridItems; track item.id) {
<button
class="grid-item"
[tabindex]="$first ? '0' : '-1'"
[disabled]="item.disabled"
[attr.aria-disabled]="item.disabled"
>
{{ item.label }}
</button>
}
</div>
</div>
</section>
<!-- Modal Demo -->
<section class="demo-section" aria-labelledby="modal-heading">
<h2 id="modal-heading">Modal with Focus Trap</h2>
<ui-button (click)="toggleModal()">Open Modal</ui-button>
@if (showModal) {
<div class="modal-backdrop" (click)="toggleModal()">
<div
class="modal-content"
[uiFocusTrap]="true"
[autoFocus]="true"
[restoreFocus]="true"
role="dialog"
aria-labelledby="modal-title"
aria-modal="true"
(click)="$event.stopPropagation()"
>
<header class="modal-header">
<h3 id="modal-title">Demo Modal</h3>
<button
class="modal-close"
(click)="toggleModal()"
aria-label="Close modal"
>
×
</button>
</header>
<div class="modal-body">
<p>This modal demonstrates focus trapping. Tab navigation is contained within the modal.</p>
<input type="text" placeholder="Modal input" class="demo-input">
<ui-button variant="tonal">Modal Button</ui-button>
</div>
<footer class="modal-footer">
<ui-button (click)="toggleModal()">Close</ui-button>
<ui-button variant="filled">Save Changes</ui-button>
</footer>
</div>
</div>
}
</section>
<!-- Screen Reader Components -->
<section class="demo-section" aria-labelledby="sr-heading">
<h2 id="sr-heading">Screen Reader Components</h2>
<div class="sr-demo">
<h3>Screen Reader Only Text:</h3>
<p>
This text is visible.
<ui-screen-reader-only>This text is only for screen readers.</ui-screen-reader-only>
This text is visible again.
</p>
<h3>Focusable Screen Reader Text:</h3>
<ui-screen-reader-only type="focusable">
This text becomes visible when focused (try tabbing through the page).
</ui-screen-reader-only>
<h3>Dynamic Status Messages:</h3>
<div class="status-demo">
<ui-screen-reader-only type="status" statusType="success">
Form submitted successfully!
</ui-screen-reader-only>
<ui-screen-reader-only type="status" statusType="error">
Error: Please check your input.
</ui-screen-reader-only>
</div>
</div>
</section>
<!-- Keyboard Shortcuts Info -->
<section class="demo-section" aria-labelledby="shortcuts-heading">
<h2 id="shortcuts-heading">Keyboard Shortcuts</h2>
<div class="shortcuts-info">
<dl class="shortcuts-list">
<dt>Ctrl + Shift + A</dt>
<dd>Make announcement</dd>
<dt>Ctrl + M</dt>
<dd>Toggle modal</dd>
<dt>Tab / Shift + Tab</dt>
<dd>Navigate through interactive elements</dd>
<dt>Arrow Keys</dt>
<dd>Navigate within lists and grids (when focused)</dd>
<dt>Home / End</dt>
<dd>Jump to first/last item in navigable containers</dd>
<dt>Escape</dt>
<dd>Close modal or exit focus trap</dd>
</dl>
</div>
</section>
<!-- Footer -->
<footer class="demo-footer">
<p>
<ui-screen-reader-only>End of accessibility demo.</ui-screen-reader-only>
This demo showcases WCAG 2.1 Level AA compliant accessibility features using semantic design tokens.
</p>
</footer>
</div>

View File

@@ -0,0 +1,476 @@
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
.accessibility-demo {
padding: $semantic-spacing-layout-md;
max-width: 1200px;
margin: 0 auto;
font-family: system-ui, -apple-system, sans-serif;
line-height: 1.5;
}
.demo-header {
text-align: center;
margin-bottom: $semantic-spacing-component-lg * 2;
h1 {
color: #1a1a1a;
font-size: 2rem;
font-weight: 600;
margin-bottom: $semantic-spacing-component-lg;
}
p {
color: #666;
font-size: 1.125rem;
}
}
.demo-section {
margin-bottom: $semantic-spacing-component-lg * 2;
padding: $semantic-spacing-component-md $semantic-spacing-component-md;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e0e0e0;
h2 {
color: #1a1a1a;
font-size: 1.5rem;
font-weight: 600;
margin-bottom: $semantic-spacing-component-lg;
border-bottom: 2px solid #007bff;
padding-bottom: $semantic-spacing-component-md / 2;
}
h3 {
color: #1a1a1a;
font-size: 1.25rem;
font-weight: 600;
margin: $semantic-spacing-component-lg 0 $semantic-spacing-component-lg / 2 0;
}
}
// Preferences Display
.preferences-display {
display: flex;
flex-direction: column;
gap: $semantic-spacing-component-sm;
padding: $semantic-spacing-component-md;
background: #fff;
border-radius: 4px;
border: 1px solid #e0e0e0;
p {
margin: 0;
font-size: 1rem;
color: #666;
}
}
// Demo Controls
.demo-controls {
display: flex;
flex-direction: column;
gap: $semantic-spacing-component-md;
}
.input-group {
display: flex;
flex-direction: column;
gap: $semantic-spacing-component-sm;
label {
font-weight: 500;
color: #1a1a1a;
font-size: 1rem;
}
}
.button-group {
display: flex;
gap: $semantic-spacing-component-sm;
flex-wrap: wrap;
align-items: center;
}
.demo-input {
padding: $semantic-spacing-component-md $semantic-spacing-component-md;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
color: #1a1a1a;
font-size: 1rem;
transition: border-color 0.15s ease-out;
&:focus {
outline: 2px solid #007bff;
outline-offset: 2px;
border-color: #007bff;
}
&::placeholder {
color: #999;
}
}
.demo-select {
@extend .demo-input;
cursor: pointer;
}
.demo-textarea {
@extend .demo-input;
min-height: 80px;
resize: vertical;
}
// Focus Demo
.focus-demo {
display: flex;
flex-direction: column;
gap: $semantic-spacing-component-md;
}
// Focus Trap Demo
.trap-controls {
margin-bottom: $semantic-spacing-component-lg;
}
.focus-trap-container {
padding: $semantic-spacing-component-md * 1.5;
border: 2px dashed #e0e0e0;
border-radius: 8px;
background: #fff;
display: flex;
flex-direction: column;
gap: $semantic-spacing-component-md;
transition: all 0.3s ease-out;
&.trap-enabled {
border-color: #007bff;
background: #f0f8ff;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
}
h3 {
margin-top: 0;
color: #1a1a1a;
}
p {
color: #666;
margin: 0;
}
}
// Navigation Demo
.navigation-demo {
display: flex;
flex-direction: column;
gap: $semantic-spacing-component-lg;
}
.navigation-list {
list-style: none;
padding: 0;
margin: 0;
border: 1px solid #e0e0e0;
border-radius: 4px;
overflow: hidden;
li {
padding: $semantic-spacing-component-md $semantic-spacing-component-md;
background: #fff;
border-bottom: 1px solid #e0e0e0;
cursor: pointer;
transition: all 0.15s ease-out;
color: #1a1a1a;
&:last-child {
border-bottom: none;
}
&:hover {
background: #f5f5f5;
}
&:focus {
outline: 2px solid #007bff;
outline-offset: -2px;
background: #f0f8ff;
}
&.disabled {
opacity: 0.6;
cursor: not-allowed;
color: #999;
}
}
}
.navigation-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: $semantic-spacing-component-sm;
padding: $semantic-spacing-component-md;
border: 1px solid #e0e0e0;
border-radius: 4px;
background: #fff;
.grid-item {
padding: $semantic-spacing-component-md;
border: 1px solid #ccc;
border-radius: 4px;
background: #e9ecef;
color: #495057;
cursor: pointer;
transition: all 0.15s ease-out;
font-size: 0.875rem;
&:hover:not(:disabled) {
background: #dee2e6;
}
&:focus {
outline: 2px solid #007bff;
outline-offset: 2px;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
background: #f8f9fa;
color: #999;
}
}
}
// Modal Demo
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: fadeIn 0.3s ease-out;
}
.modal-content {
background: #fff;
border-radius: 12px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
min-width: 400px;
max-width: 600px;
max-height: 80vh;
overflow: hidden;
animation: slideIn 0.3s ease-out;
.modal-header {
padding: $semantic-spacing-component-md $semantic-spacing-component-md;
background: #f8f9fa;
border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
h3 {
margin: 0;
color: #1a1a1a;
}
.modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.15s ease-out;
&:hover {
background: #f5f5f5;
color: #1a1a1a;
}
&:focus {
outline: 2px solid #007bff;
outline-offset: 2px;
}
}
}
.modal-body {
padding: $semantic-spacing-component-md * 1.5;
p {
color: #666;
margin-bottom: $semantic-spacing-component-lg;
}
.demo-input {
width: 100%;
margin-bottom: $semantic-spacing-component-lg;
}
}
.modal-footer {
padding: $semantic-spacing-component-md $semantic-spacing-component-md;
background: #f8f9fa;
border-top: 1px solid #e0e0e0;
display: flex;
gap: $semantic-spacing-component-sm;
justify-content: flex-end;
}
}
// Screen Reader Demo
.sr-demo {
display: flex;
flex-direction: column;
gap: $semantic-spacing-component-md;
p {
color: #666;
margin: $semantic-spacing-component-lg / 2 0;
}
}
.status-demo {
display: flex;
flex-direction: column;
gap: $semantic-spacing-component-sm;
}
// Keyboard Shortcuts
.shortcuts-info {
background: #f5f5f5;
padding: $semantic-spacing-component-md;
border-radius: 4px;
border: 1px solid #e0e0e0;
}
.shortcuts-list {
display: grid;
grid-template-columns: 200px 1fr;
gap: $semantic-spacing-component-sm $semantic-spacing-component-md;
margin: 0;
dt {
font-weight: 500;
color: #1a1a1a;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.875rem;
background: #e9ecef;
padding: 4px 8px;
border-radius: 4px;
text-align: center;
}
dd {
margin: 0;
color: #666;
display: flex;
align-items: center;
}
}
// Footer
.demo-footer {
text-align: center;
padding: $semantic-spacing-component-md * 2;
border-top: 1px solid #e0e0e0;
margin-top: $semantic-spacing-component-lg * 2;
p {
color: #666;
font-size: 0.875rem;
margin: 0;
}
}
// Animations
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
// Responsive Design
@media (max-width: 768px) {
.accessibility-demo {
padding: $semantic-spacing-component-md / 2;
}
.demo-section {
padding: $semantic-spacing-component-md / 2;
}
.button-group {
flex-direction: column;
align-items: stretch;
}
.navigation-grid {
grid-template-columns: repeat(2, 1fr);
}
.shortcuts-list {
grid-template-columns: 1fr;
gap: $semantic-spacing-component-sm;
dt {
text-align: left;
}
}
.modal-content {
min-width: 90vw;
margin: $semantic-spacing-component-md;
}
}
// High Contrast Mode Support
@media (prefers-contrast: high) {
.demo-section {
border-color: ButtonText;
}
.focus-trap-container.trap-enabled {
border-color: Highlight;
}
.navigation-list li:focus {
outline-color: Highlight;
}
.grid-item:focus {
outline-color: Highlight;
}
}
// Reduced Motion Support
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01s !important;
transition-duration: 0.01s !important;
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AccessibilityDemoComponent } from './accessibility-demo.component';
describe('AccessibilityDemoComponent', () => {
let component: AccessibilityDemoComponent;
let fixture: ComponentFixture<AccessibilityDemoComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AccessibilityDemoComponent]
})
.compileComponents();
fixture = TestBed.createComponent(AccessibilityDemoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,213 @@
import { Component, OnInit, OnDestroy, ViewChild, ElementRef, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
// Import UI Accessibility components and services
import {
UiAccessibilityModule,
LiveAnnouncerService,
FocusMonitorService,
KeyboardManagerService,
HighContrastService,
A11yConfigService
} from 'ui-accessibility';
// Import UI Essentials components
import { ButtonComponent } from 'ui-essentials';
@Component({
selector: 'app-accessibility-demo',
imports: [
CommonModule,
FormsModule,
UiAccessibilityModule,
ButtonComponent
],
templateUrl: './accessibility-demo.component.html',
styleUrl: './accessibility-demo.component.scss'
})
export class AccessibilityDemoComponent implements OnInit, OnDestroy {
private liveAnnouncer = inject(LiveAnnouncerService);
private focusMonitor = inject(FocusMonitorService);
private keyboardManager = inject(KeyboardManagerService);
private highContrast = inject(HighContrastService);
private a11yConfig = inject(A11yConfigService);
@ViewChild('trapContainer') trapContainer!: ElementRef;
@ViewChild('monitoredInput') monitoredInput!: ElementRef;
// Demo state
trapEnabled = false;
showModal = false;
currentPreferences: any = {};
focusOrigin = '';
announcementText = 'Hello, this is a test announcement!';
selectedPoliteness: 'polite' | 'assertive' = 'polite';
// Navigation demo items
navigationItems = [
{ id: 1, label: 'Profile Settings', disabled: false },
{ id: 2, label: 'Account Details', disabled: false },
{ id: 3, label: 'Privacy Controls', disabled: true },
{ id: 4, label: 'Notifications', disabled: false },
{ id: 5, label: 'Security Options', disabled: false }
];
// Grid navigation items
gridItems = Array.from({ length: 12 }, (_, i) => ({
id: i + 1,
label: `Item ${i + 1}`,
disabled: i === 5 // Make item 6 disabled
}));
ngOnInit(): void {
this.setupKeyboardShortcuts();
this.setupFocusMonitoring();
this.getCurrentPreferences();
}
ngOnDestroy(): void {
this.keyboardManager.unregisterAll();
}
private setupKeyboardShortcuts(): void {
// Global shortcuts
this.keyboardManager.registerGlobalShortcut('announce', {
key: 'a',
ctrlKey: true,
shiftKey: true,
description: 'Make announcement',
handler: () => this.makeAnnouncement()
});
this.keyboardManager.registerGlobalShortcut('toggleModal', {
key: 'm',
ctrlKey: true,
description: 'Toggle modal',
handler: () => this.toggleModal()
});
}
private setupFocusMonitoring(): void {
// Monitor focus on the input field
setTimeout(() => {
if (this.monitoredInput) {
this.focusMonitor.monitor(this.monitoredInput).subscribe(focusEvent => {
this.focusOrigin = focusEvent.origin || 'none';
if (focusEvent.origin) {
this.liveAnnouncer.announce(
`Input focused via ${focusEvent.origin}`,
'polite'
);
}
});
}
}, 100);
}
private getCurrentPreferences(): void {
this.currentPreferences = this.highContrast.getCurrentPreferences();
}
// Demo actions
makeAnnouncement(): void {
this.liveAnnouncer.announce(this.announcementText, this.selectedPoliteness);
}
makeSequenceAnnouncement(): void {
const messages = [
'Starting process...',
'Processing data...',
'Validating information...',
'Process completed successfully!'
];
this.liveAnnouncer.announceSequence(messages, 'polite', 1500);
}
toggleFocusTrap(): void {
this.trapEnabled = !this.trapEnabled;
this.liveAnnouncer.announce(
`Focus trap ${this.trapEnabled ? 'enabled' : 'disabled'}`,
'assertive'
);
}
toggleModal(): void {
this.showModal = !this.showModal;
if (this.showModal) {
this.liveAnnouncer.announce('Modal opened', 'assertive');
} else {
this.liveAnnouncer.announce('Modal closed', 'assertive');
}
}
focusViaKeyboard(): void {
if (this.monitoredInput) {
this.focusMonitor.focusVia(this.monitoredInput, 'keyboard');
}
}
focusViaMouse(): void {
if (this.monitoredInput) {
this.focusMonitor.focusVia(this.monitoredInput, 'mouse');
}
}
focusViaProgram(): void {
if (this.monitoredInput) {
this.focusMonitor.focusVia(this.monitoredInput, 'program');
}
}
onNavigationChange(event: any): void {
this.liveAnnouncer.announce(
`Navigated to ${this.navigationItems[event.nextIndex]?.label}`,
'polite'
);
}
onGridNavigationChange(event: any): void {
this.liveAnnouncer.announce(
`Navigated to ${this.gridItems[event.nextIndex]?.label}`,
'polite'
);
}
announceError(): void {
this.liveAnnouncer.announce(
'Error: Please fix the required fields before continuing',
'assertive'
);
}
announceSuccess(): void {
this.liveAnnouncer.announce(
'Success: Your changes have been saved',
'polite'
);
}
announceLoading(): void {
this.liveAnnouncer.announce(
'Loading content, please wait...',
'polite'
);
}
refreshPreferences(): void {
this.getCurrentPreferences();
this.liveAnnouncer.announce('Accessibility preferences updated', 'polite');
}
simulateFormValidation(): void {
// Simulate a form validation scenario
this.liveAnnouncer.announce('Validating form...', 'polite');
setTimeout(() => {
this.liveAnnouncer.announce(
'Form validation complete. 2 errors found.',
'assertive'
);
}, 2000);
}
}

View File

@@ -0,0 +1,223 @@
import { Component, ElementRef, ViewChild } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UiAnimationsService, AnimateDirective } from 'ui-animations';
@Component({
selector: 'app-animations-demo',
standalone: true,
imports: [CommonModule, AnimateDirective],
template: `
<div class="animations-demo">
<h2>UI Animations Demo</h2>
<!-- Entrance Animations -->
<section class="demo-section">
<h3>Entrance Animations</h3>
<div class="animation-grid">
<div class="demo-card animate-fade-in">
<h4>Fade In</h4>
<p>Basic fade in animation</p>
</div>
<div class="demo-card animate-fade-in-up animation-delay-200">
<h4>Fade In Up</h4>
<p>Fade in with upward movement</p>
</div>
<div class="demo-card animate-slide-in-up animation-delay-300">
<h4>Slide In Up</h4>
<p>Slide in from bottom</p>
</div>
<div class="demo-card animate-zoom-in animation-delay-500">
<h4>Zoom In</h4>
<p>Scale up animation</p>
</div>
</div>
</section>
<!-- Emphasis Animations -->
<section class="demo-section">
<h3>Emphasis Animations</h3>
<div class="animation-grid">
<button class="demo-button" (click)="animateElement('bounce')">
Bounce
</button>
<button class="demo-button" (click)="animateElement('shake')">
Shake
</button>
<button class="demo-button" (click)="animateElement('pulse')">
Pulse
</button>
<button class="demo-button" (click)="animateElement('wobble')">
Wobble
</button>
</div>
<div #animationTarget class="demo-target">
Click buttons above to animate me!
</div>
</section>
<!-- Directive Examples -->
<section class="demo-section">
<h3>Animation Directive Examples</h3>
<div class="animation-grid">
<div class="demo-card" uiAnimate="animate-fade-in-up" [animationTrigger]="'hover'">
<h4>Hover to Animate</h4>
<p>Uses directive with hover trigger</p>
</div>
<div class="demo-card" uiAnimate="animate-bounce" [animationTrigger]="'click'" [animationOnce]="true">
<h4>Click to Animate</h4>
<p>Uses directive with click trigger</p>
</div>
</div>
</section>
<!-- Service Examples -->
<section class="demo-section">
<h3>Animation Service Examples</h3>
<div class="service-controls">
<button (click)="serviceAnimate('animate-tada')">Tada</button>
<button (click)="serviceAnimate('animate-jello')">Jello</button>
<button (click)="serviceAnimate('animate-heartbeat')">Heartbeat</button>
<button (click)="serviceAnimate('animate-rubber-band')">Rubber Band</button>
</div>
<div #serviceTarget class="demo-target service-target">
Animation Service Target
</div>
</section>
</div>
`,
styles: [`
.animations-demo {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.demo-section {
margin-bottom: 3rem;
h3 {
margin-bottom: 1.5rem;
color: #333;
border-bottom: 2px solid #007bff;
padding-bottom: 0.5rem;
}
}
.animation-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.demo-card {
padding: 1.5rem;
border: 1px solid #ddd;
border-radius: 8px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease;
h4 {
margin: 0 0 0.5rem 0;
font-size: 1.2rem;
}
p {
margin: 0;
opacity: 0.9;
}
&:hover {
transform: translateY(-2px);
}
}
.demo-button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 6px;
background: #007bff;
color: white;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.2s ease;
&:hover {
background: #0056b3;
}
}
.demo-target {
padding: 2rem;
border: 2px dashed #007bff;
border-radius: 8px;
text-align: center;
font-size: 1.2rem;
font-weight: bold;
background: #f8f9fa;
margin-top: 1rem;
}
.service-target {
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
color: white;
border: none;
}
.service-controls {
display: flex;
gap: 1rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
/* Responsive */
@media (max-width: 768px) {
.animation-grid {
grid-template-columns: 1fr;
}
.service-controls {
justify-content: center;
}
}
`]
})
export class AnimationsDemoComponent {
@ViewChild('animationTarget', { static: true }) animationTarget!: ElementRef<HTMLElement>;
@ViewChild('serviceTarget', { static: true }) serviceTarget!: ElementRef<HTMLElement>;
constructor(private animationService: UiAnimationsService) {}
animateElement(animationType: string) {
const element = this.animationTarget.nativeElement;
const animationClass = `animate-${animationType}`;
// Remove any existing animation classes first
element.className = element.className.replace(/animate-\w+/g, '');
// Add animation class
this.animationService.animateOnce(element, animationClass);
}
serviceAnimate(animationClass: string) {
const element = this.serviceTarget.nativeElement;
// Remove any existing animation classes first
element.className = element.className.replace(/animate-\w+/g, '');
// Add animation class with service
this.animationService.animateOnce(element, animationClass);
}
}

View File

@@ -0,0 +1,294 @@
@use 'ui-design-system/src/styles' as ui;
.backgrounds-demo {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
.demo-header {
text-align: center;
margin-bottom: 3rem;
h1 {
font-size: 3rem;
font-weight: 700;
color: var(--primary-900, #1e293b);
margin-bottom: 1rem;
}
p {
font-size: 1.2rem;
color: var(--neutral-600, #64748b);
}
}
.demo-section {
margin-bottom: 4rem;
h2 {
font-size: 2rem;
font-weight: 600;
color: var(--primary-800, #334155);
margin-bottom: 2rem;
border-bottom: 2px solid var(--primary-200, #e2e8f0);
padding-bottom: 0.5rem;
}
}
.example-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
margin-bottom: 2rem;
}
.demo-card {
padding: 2rem;
border-radius: 1rem;
min-height: 200px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
color: white;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
&:hover {
transform: translateY(-5px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
}
h3 {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 1rem;
}
p {
font-size: 1rem;
opacity: 0.9;
line-height: 1.5;
}
}
.pattern-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
}
.pattern-demo {
aspect-ratio: 1;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease;
&:hover {
transform: scale(1.05);
}
.pattern-name {
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
font-size: 0.875rem;
font-weight: 500;
text-transform: capitalize;
}
}
.gradient-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
}
.gradient-demo {
aspect-ratio: 16/9;
border-radius: 1rem;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 1.2rem;
font-weight: 600;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
&:hover {
transform: translateY(-3px);
}
}
.interactive-demo {
padding: 3rem;
border-radius: 1rem;
min-height: 300px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
color: white;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
position: relative;
transition: all 0.5s ease;
.controls {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
flex-wrap: wrap;
justify-content: center;
}
h3 {
font-size: 2rem;
margin-bottom: 1rem;
}
p {
font-size: 1.1rem;
opacity: 0.9;
}
}
.fullscreen-controls {
display: flex;
gap: 1rem;
flex-wrap: wrap;
justify-content: center;
align-items: center;
}
.action-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 1rem 2rem;
border-radius: 0.5rem;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
}
&:active {
transform: translateY(0);
}
}
.preset-btn {
background: rgba(255, 255, 255, 0.1);
border: 2px solid rgba(255, 255, 255, 0.2);
color: var(--primary-700, #475569);
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
&:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.4);
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
}
}
}
// Responsive adjustments
@media (max-width: 768px) {
.backgrounds-demo {
padding: 1rem;
.demo-header h1 {
font-size: 2rem;
}
.example-grid {
grid-template-columns: 1fr;
}
.pattern-grid {
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
}
.gradient-grid {
grid-template-columns: 1fr;
}
.interactive-demo {
padding: 2rem;
.controls {
flex-direction: column;
width: 100%;
}
}
.fullscreen-controls {
flex-direction: column;
}
}
}
// Dark mode support
@media (prefers-color-scheme: dark) {
.backgrounds-demo {
.demo-header h1 {
color: var(--neutral-100, #f8fafc);
}
.demo-section h2 {
color: var(--neutral-200, #e2e8f0);
border-bottom-color: var(--primary-700, #475569);
}
.preset-btn {
color: var(--neutral-200, #e2e8f0);
}
}
}
// High contrast mode
@media (prefers-contrast: high) {
.backgrounds-demo {
.demo-card,
.pattern-demo,
.gradient-demo,
.interactive-demo {
border: 2px solid rgba(0, 0, 0, 0.8);
}
}
}
// Reduced motion
@media (prefers-reduced-motion: reduce) {
.backgrounds-demo {
* {
transition: none !important;
animation: none !important;
}
}
}

View File

@@ -0,0 +1,307 @@
import { Component, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
BackgroundDirective,
SolidBackgroundComponent,
GradientBackgroundComponent,
PatternBackgroundComponent,
ImageBackgroundComponent,
BackgroundService,
SolidBackgroundConfig,
LinearGradientConfig,
PatternConfig,
ImageBackgroundConfig
} from 'ui-backgrounds';
@Component({
selector: 'app-backgrounds-demo',
standalone: true,
imports: [
CommonModule,
BackgroundDirective,
SolidBackgroundComponent,
GradientBackgroundComponent,
PatternBackgroundComponent,
ImageBackgroundComponent
],
template: `
<div class="backgrounds-demo">
<div class="demo-header">
<h1>UI Backgrounds Demo</h1>
<p>Explore different background types and configurations</p>
</div>
<!-- Directive Examples -->
<section class="demo-section">
<h2>Background Directive Examples</h2>
<div class="example-grid">
<div class="demo-card" [uiBackground]="solidConfig()">
<h3>Solid Background</h3>
<p>Using the background directive with solid color</p>
</div>
<div class="demo-card" [uiBackground]="gradientConfig()">
<h3>Linear Gradient</h3>
<p>Beautiful gradient backgrounds</p>
</div>
<div class="demo-card" [uiBackground]="patternConfig()">
<h3>Pattern Background</h3>
<p>Geometric patterns</p>
</div>
</div>
</section>
<!-- Component Examples -->
<section class="demo-section">
<h2>Background Components</h2>
<div class="example-grid">
<ui-solid-background color="#4ade80" class="demo-card">
<h3>Solid Component</h3>
<p>Green background using component</p>
</ui-solid-background>
<ui-gradient-background
type="linear"
[colors]="['#8b5cf6', '#06b6d4']"
direction="45deg"
class="demo-card">
<h3>Gradient Component</h3>
<p>Purple to cyan gradient</p>
</ui-gradient-background>
<ui-pattern-background
pattern="dots"
primaryColor="#f59e0b"
secondaryColor="transparent"
[size]="30"
[opacity]="0.7"
class="demo-card">
<h3>Pattern Component</h3>
<p>Orange dots pattern</p>
</ui-pattern-background>
</div>
</section>
<!-- Pattern Showcase -->
<section class="demo-section">
<h2>Pattern Showcase</h2>
<div class="pattern-grid">
<div
*ngFor="let pattern of patterns"
class="pattern-demo"
[uiBackground]="getPatternConfig(pattern)">
<span class="pattern-name">{{ pattern }}</span>
</div>
</div>
</section>
<!-- Gradient Types -->
<section class="demo-section">
<h2>Gradient Types</h2>
<div class="gradient-grid">
<ui-gradient-background
type="linear"
[colors]="['#ff6b6b', '#4ecdc4', '#45b7d1']"
direction="to right"
class="gradient-demo">
<span>Linear Gradient</span>
</ui-gradient-background>
<ui-gradient-background
type="radial"
[colors]="['#667eea', '#764ba2']"
shape="circle"
class="gradient-demo">
<span>Radial Gradient</span>
</ui-gradient-background>
<ui-gradient-background
type="conic"
[colors]="['#ff6b6b', '#4ecdc4', '#45b7d1', '#f9ca24', '#ff6b6b']"
class="gradient-demo">
<span>Conic Gradient</span>
</ui-gradient-background>
</div>
</section>
<!-- Interactive Controls -->
<section class="demo-section">
<h2>Interactive Background</h2>
<div class="interactive-demo" [uiBackground]="interactiveConfig()">
<div class="controls">
<button
*ngFor="let config of presetConfigs"
(click)="setInteractiveConfig(config)"
class="preset-btn">
{{ config.name }}
</button>
</div>
<h3>Interactive Background Demo</h3>
<p>Click buttons to change the background</p>
</div>
</section>
<!-- Full Screen Examples -->
<section class="demo-section">
<h2>Full Screen Backgrounds</h2>
<div class="fullscreen-controls">
<button (click)="toggleFullScreenBackground()" class="action-btn">
{{ hasFullScreenBg() ? 'Remove' : 'Add' }} Full Screen Background
</button>
<button
*ngFor="let bg of fullScreenPresets"
(click)="setFullScreenBackground(bg)"
class="preset-btn">
{{ bg.name }}
</button>
</div>
</section>
</div>
`,
styleUrl: './backgrounds-demo.component.scss'
})
export class BackgroundsDemoComponent {
private readonly backgroundService = signal(new BackgroundService());
private fullScreenBackgroundId = signal<string | null>(null);
// Configuration signals
solidConfig = signal<SolidBackgroundConfig>({
type: 'solid',
color: '#3b82f6'
});
gradientConfig = signal<LinearGradientConfig>({
type: 'linear-gradient',
direction: 'to bottom right',
colors: [
{ color: '#ec4899' },
{ color: '#8b5cf6' }
]
});
patternConfig = signal<PatternConfig>({
type: 'pattern',
pattern: 'grid',
primaryColor: '#1f2937',
secondaryColor: 'transparent',
size: 20,
opacity: 0.3
});
interactiveConfig = signal(this.solidConfig());
// Pattern list for showcase
patterns = [
'dots', 'grid', 'stripes', 'diagonal-stripes',
'chevron', 'waves', 'circles', 'checkerboard'
];
// Preset configurations
presetConfigs = [
{
name: 'Ocean',
config: {
type: 'linear-gradient' as const,
direction: 'to bottom',
colors: [{ color: '#667eea' }, { color: '#764ba2' }]
}
},
{
name: 'Sunset',
config: {
type: 'linear-gradient' as const,
direction: 'to right',
colors: [{ color: '#ff6b6b' }, { color: '#feca57' }]
}
},
{
name: 'Forest',
config: {
type: 'solid' as const,
color: '#10b981'
}
},
{
name: 'Dots',
config: {
type: 'pattern' as const,
pattern: 'dots' as const,
primaryColor: '#6366f1',
secondaryColor: 'transparent',
size: 25,
opacity: 0.6
}
}
];
// Full screen presets
fullScreenPresets = [
{
name: 'Purple Mesh',
config: {
type: 'linear-gradient' as const,
direction: 'to bottom right',
colors: [{ color: '#667eea' }, { color: '#764ba2' }, { color: '#f093fb' }]
}
},
{
name: 'Geometric',
config: {
type: 'pattern' as const,
pattern: 'chevron' as const,
primaryColor: '#1f2937',
secondaryColor: 'transparent',
size: 40,
opacity: 0.1
}
}
];
getPatternConfig(pattern: string): PatternConfig {
return {
type: 'pattern',
pattern: pattern as any,
primaryColor: '#6366f1',
secondaryColor: 'transparent',
size: 25,
opacity: 0.4
};
}
setInteractiveConfig(preset: any) {
this.interactiveConfig.set(preset.config);
}
toggleFullScreenBackground() {
const currentId = this.fullScreenBackgroundId();
if (currentId) {
this.backgroundService().removeBackground(currentId);
this.fullScreenBackgroundId.set(null);
}
}
setFullScreenBackground(preset: any) {
// Remove existing full screen background
const currentId = this.fullScreenBackgroundId();
if (currentId) {
this.backgroundService().removeBackground(currentId);
}
// Add new full screen background
const id = this.backgroundService().applyFullScreenBackground(preset.config, {
zIndex: -1
});
this.fullScreenBackgroundId.set(id);
}
hasFullScreenBg() {
return this.fullScreenBackgroundId() !== null;
}
}

View File

@@ -0,0 +1,71 @@
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
.demo-container {
padding: $semantic-spacing-layout-section-sm;
max-width: 1200px;
margin: 0 auto;
h1 {
color: $semantic-color-text-primary;
margin-bottom: $semantic-spacing-layout-section-sm;
border-bottom: 2px solid $semantic-color-border-primary;
padding-bottom: $semantic-spacing-component-md;
}
}
.demo-section {
margin-bottom: $semantic-spacing-layout-section-md;
h2 {
color: $semantic-color-text-primary;
margin-bottom: $semantic-spacing-component-lg;
font-size: $base-typography-font-size-xl;
}
h3 {
color: $semantic-color-text-secondary;
margin: $semantic-spacing-component-lg 0 $semantic-spacing-component-md 0;
font-size: $base-typography-font-size-lg;
}
p {
margin-bottom: $semantic-spacing-component-md;
line-height: $base-typography-line-height-relaxed;
color: $semantic-color-text-primary;
}
}
.theme-controls {
display: flex;
gap: $semantic-spacing-component-sm;
margin-bottom: $semantic-spacing-component-lg;
flex-wrap: wrap;
}
.theme-btn {
padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
border: $semantic-border-button-width $semantic-border-button-style $semantic-color-border-primary;
border-radius: $semantic-border-button-radius;
background: $semantic-color-surface-primary;
color: $semantic-color-text-primary;
cursor: pointer;
font-size: $base-typography-font-size-sm;
text-transform: capitalize;
transition: all $semantic-duration-fast $semantic-easing-standard;
&:hover {
background: $semantic-color-surface-elevated;
border-color: $semantic-color-brand-primary;
}
&.active {
background: $semantic-color-brand-primary;
color: $semantic-color-on-brand-primary;
border-color: $semantic-color-brand-primary;
}
&:focus-visible {
outline: $semantic-border-focus-width solid $semantic-color-focus;
outline-offset: 2px;
}
}

View File

@@ -0,0 +1,214 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
CodeSnippetComponent,
InlineCodeComponent,
CodeBlockComponent,
CodeThemeService,
CodeTheme
} from 'ui-code-display';
@Component({
selector: 'app-code-display-demo',
standalone: true,
imports: [
CommonModule,
CodeSnippetComponent,
InlineCodeComponent,
CodeBlockComponent
],
template: `
<div class="demo-container">
<h1>Code Display Components Demo</h1>
<section class="demo-section">
<h2>Theme Selector</h2>
<div class="theme-controls">
@for (theme of themes; track theme) {
<button
(click)="setTheme(theme)"
[class.active]="currentTheme() === theme"
class="theme-btn">
{{ theme }}
</button>
}
</div>
</section>
<section class="demo-section">
<h2>Inline Code</h2>
<p>
Use <ui-inline-code code="console.log('hello')" language="javascript"></ui-inline-code>
to output to the console.
</p>
<p>
Install dependencies with <ui-inline-code code="npm install" language="bash" [copyable]="true"></ui-inline-code>
</p>
</section>
<section class="demo-section">
<h2>Code Snippet</h2>
<ui-code-snippet
[code]="typescriptCode"
language="typescript"
title="Component Example"
size="md"
[showLineNumbers]="true"
[copyable]="true"
[highlightLines]="[3, 5, '7-9']">
</ui-code-snippet>
</section>
<section class="demo-section">
<h2>Code Block</h2>
<ui-code-block
[code]="cssCode"
language="css"
variant="bordered"
[showLineNumbers]="true"
[copyable]="true">
</ui-code-block>
</section>
<section class="demo-section">
<h2>Different Sizes</h2>
<h3>Small</h3>
<ui-code-snippet
[code]="shortCode"
language="javascript"
size="sm"
title="Small Size">
</ui-code-snippet>
<h3>Medium (Default)</h3>
<ui-code-snippet
[code]="shortCode"
language="javascript"
size="md"
title="Medium Size">
</ui-code-snippet>
<h3>Large</h3>
<ui-code-snippet
[code]="shortCode"
language="javascript"
size="lg"
title="Large Size">
</ui-code-snippet>
</section>
<section class="demo-section">
<h2>Different Languages</h2>
<h3>Python</h3>
<ui-code-snippet
[code]="pythonCode"
language="python"
title="Python Example">
</ui-code-snippet>
<h3>JSON</h3>
<ui-code-snippet
[code]="jsonCode"
language="json"
title="Configuration">
</ui-code-snippet>
<h3>HTML</h3>
<ui-code-snippet
[code]="htmlCode"
language="html"
title="Template">
</ui-code-snippet>
</section>
</div>
`,
styleUrl: './code-display-demo.component.scss'
})
export class CodeDisplayDemoComponent {
constructor(private themeService: CodeThemeService) {
this.themes = this.themeService.getAvailableThemes();
this.currentTheme = this.themeService.theme;
}
readonly themes: any[];
readonly currentTheme: any;
readonly typescriptCode = `import { Component } from '@angular/core';
@Component({
selector: 'app-example',
template: \`
<div class="container">
<h1>{{ title }}</h1>
<p>{{ description }}</p>
</div>
\`
})
export class ExampleComponent {
title = 'Hello World';
description = 'This is a demo component';
}`;
readonly cssCode = `.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
h1 {
color: var(--primary-color);
font-size: 2rem;
margin-bottom: 1rem;
}
p {
line-height: 1.6;
color: var(--text-color);
}
}`;
readonly shortCode = `const greeting = 'Hello, World!';
console.log(greeting);`;
readonly pythonCode = `def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
# Generate first 10 Fibonacci numbers
for i in range(10):
print(f"F({i}) = {fibonacci(i)}")`;
readonly jsonCode = `{
"name": "ui-code-display",
"version": "1.0.0",
"description": "Code display components for Angular",
"dependencies": {
"@angular/core": "^19.0.0",
"prismjs": "^1.30.0"
},
"keywords": [
"angular",
"code",
"syntax-highlighting",
"prism"
]
}`;
readonly htmlCode = `<div class="card">
<header class="card-header">
<h2>Card Title</h2>
<button class="close-btn">&times;</button>
</header>
<div class="card-body">
<p>This is the card content.</p>
<a href="#" class="btn btn-primary">Action</a>
</div>
</div>`;
setTheme(theme: CodeTheme): void {
this.themeService.setTheme(theme);
}
}

View File

@@ -0,0 +1,741 @@
import { Component, ChangeDetectionStrategy, signal, computed } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
// Import all utilities from ui-data-utils
import {
// Types
SortConfig, FilterConfig, PaginationConfig,
// Sorting
sortBy, sortByMultiple, createComparator,
// Filtering
filterBy, filterByMultiple, searchFilter,
// Pagination
paginate, calculatePages, getPaginationRange,
// Transformation
groupBy, aggregate, pluck, flatten, pivot, unique, frequency
} from 'ui-data-utils';
interface SampleData {
id: number;
name: string;
department: string;
salary: number;
hireDate: string;
active: boolean;
skills: string[];
}
@Component({
selector: 'ui-data-utils-demo',
standalone: true,
imports: [CommonModule, FormsModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div style="padding: 2rem;">
<h2>UI Data Utils Demo</h2>
<p>Comprehensive demonstration of data manipulation utilities</p>
<!-- Sample Data Display -->
<section style="margin-bottom: 3rem;">
<h3>Sample Employee Data ({{ sampleData.length }} records)</h3>
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; margin-bottom: 1rem;">
<h4>Raw Data Preview:</h4>
<pre style="max-height: 200px; overflow-y: auto;">{{ getSampleDataPreview() }}</pre>
</div>
</section>
<!-- Sorting Utilities Demo -->
<section style="margin-bottom: 3rem;">
<h3>🔄 Sorting Utilities</h3>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;">
<!-- Single Sort -->
<div>
<h4>Single Property Sort</h4>
<div style="margin-bottom: 1rem;">
<label>Sort by: </label>
<select [(ngModel)]="sortField" (ngModelChange)="applySingleSort()">
<option value="name">Name</option>
<option value="department">Department</option>
<option value="salary">Salary</option>
<option value="hireDate">Hire Date</option>
</select>
<label style="margin-left: 1rem;">
<input type="checkbox" [(ngModel)]="sortDesc" (ngModelChange)="applySingleSort()">
Descending
</label>
</div>
<div style="background: #e3f2fd; padding: 1rem; border-radius: 4px; max-height: 200px; overflow-y: auto;">
<pre>{{ getSortedDataPreview() }}</pre>
</div>
</div>
<!-- Multi-column Sort -->
<div>
<h4>Multi-column Sort</h4>
<p style="font-size: 0.9em; color: #666;">Department → Salary → Name</p>
<div style="background: #e8f5e8; padding: 1rem; border-radius: 4px; max-height: 200px; overflow-y: auto;">
<pre>{{ getMultiSortedDataPreview() }}</pre>
</div>
</div>
</div>
</section>
<!-- Filtering Utilities Demo -->
<section style="margin-bottom: 3rem;">
<h3>🔍 Filtering Utilities</h3>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;">
<!-- Search Filter -->
<div>
<h4>Search Filter</h4>
<input
type="text"
placeholder="Search in name, department..."
[(ngModel)]="searchTerm"
(ngModelChange)="applySearchFilter()"
style="width: 100%; padding: 0.5rem; margin-bottom: 1rem;"
>
<div style="background: #fff3cd; padding: 1rem; border-radius: 4px; max-height: 200px; overflow-y: auto;">
<pre>{{ getSearchFilteredDataPreview() }}</pre>
</div>
</div>
<!-- Property Filter -->
<div>
<h4>Property Filters</h4>
<div style="margin-bottom: 1rem;">
<label>Department: </label>
<select [(ngModel)]="filterDepartment" (ngModelChange)="applyPropertyFilter()">
<option value="">All Departments</option>
<option value="Engineering">Engineering</option>
<option value="Marketing">Marketing</option>
<option value="Sales">Sales</option>
<option value="HR">HR</option>
</select>
</div>
<div style="margin-bottom: 1rem;">
<label>Min Salary: </label>
<input
type="number"
[(ngModel)]="minSalary"
(ngModelChange)="applyPropertyFilter()"
placeholder="50000"
style="width: 120px; padding: 0.25rem;"
>
</div>
<div style="background: #f3e5f5; padding: 1rem; border-radius: 4px; max-height: 200px; overflow-y: auto;">
<pre>{{ getPropertyFilteredDataPreview() }}</pre>
</div>
</div>
</div>
</section>
<!-- Pagination Demo -->
<section style="margin-bottom: 3rem;">
<h3>📄 Pagination Utilities</h3>
<div>
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;">
<label>Page Size: </label>
<select [(ngModel)]="pageSize" (ngModelChange)="updatePagination()">
<option [value]="3">3</option>
<option [value]="5">5</option>
<option [value]="7">7</option>
<option [value]="10">10</option>
</select>
<label>Page: </label>
<input
type="number"
[(ngModel)]="currentPage"
(ngModelChange)="updatePagination()"
[min]="1"
[max]="paginationResult().totalPages"
style="width: 60px; padding: 0.25rem;"
>
<span>of {{ paginationResult().totalPages }}</span>
<button
(click)="previousPage()"
[disabled]="!paginationResult().hasPrevious"
style="padding: 0.25rem 0.5rem;"
>
Previous
</button>
<button
(click)="nextPage()"
[disabled]="!paginationResult().hasNext"
style="padding: 0.25rem 0.5rem;"
>
Next
</button>
</div>
<div style="margin-bottom: 1rem;">
<strong>Page Navigation: </strong>
@for (page of pageRange(); track page) {
@if (page === 'ellipsis') {
<span style="margin: 0 0.5rem;">...</span>
} @else {
<button
(click)="goToPage(page)"
[style.font-weight]="page === currentPage() ? 'bold' : 'normal'"
[style.background]="page === currentPage() ? '#007bff' : '#f8f9fa'"
[style.color]="page === currentPage() ? 'white' : 'black'"
style="padding: 0.25rem 0.5rem; margin: 0 0.125rem; border: 1px solid #ccc; cursor: pointer;"
>
{{ page }}
</button>
}
}
</div>
<div style="background: #e1f5fe; padding: 1rem; border-radius: 4px;">
<p><strong>Showing:</strong> {{ getItemRangeText() }}</p>
<pre style="max-height: 150px; overflow-y: auto;">{{ getPaginatedDataPreview() }}</pre>
</div>
</div>
</section>
<!-- Transformation Utilities Demo -->
<section style="margin-bottom: 3rem;">
<h3>🔄 Transformation Utilities</h3>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;">
<!-- Group By -->
<div>
<h4>Group By Department</h4>
<div style="background: #f1f8e9; padding: 1rem; border-radius: 4px; max-height: 200px; overflow-y: auto;">
<pre>{{ getGroupedDataPreview() }}</pre>
</div>
</div>
<!-- Aggregation -->
<div>
<h4>Salary Statistics by Department</h4>
<div style="background: #fce4ec; padding: 1rem; border-radius: 4px; max-height: 200px; overflow-y: auto;">
<pre>{{ getAggregationPreview() }}</pre>
</div>
</div>
<!-- Pluck -->
<div>
<h4>Extract Names & Salaries</h4>
<div style="background: #f3e5f5; padding: 1rem; border-radius: 4px; max-height: 200px; overflow-y: auto;">
<pre>{{ getPluckedDataPreview() }}</pre>
</div>
</div>
<!-- Pivot Table -->
<div>
<h4>Pivot: Department vs Active Status</h4>
<div style="background: #fff3e0; padding: 1rem; border-radius: 4px; max-height: 200px; overflow-y: auto;">
<pre>{{ getPivotDataPreview() }}</pre>
</div>
</div>
</div>
</section>
<!-- Combined Operations Demo -->
<section style="margin-bottom: 3rem;">
<h3>🔄 Combined Operations</h3>
<p>Demonstrate chaining multiple operations: Filter → Sort → Paginate</p>
<div style="display: flex; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap;">
<label>
Filter Active:
<select [(ngModel)]="combinedActiveFilter" (ngModelChange)="updateCombinedDemo()">
<option value="">All</option>
<option value="true">Active Only</option>
<option value="false">Inactive Only</option>
</select>
</label>
<label>
Sort By:
<select [(ngModel)]="combinedSortField" (ngModelChange)="updateCombinedDemo()">
<option value="name">Name</option>
<option value="salary">Salary</option>
<option value="hireDate">Hire Date</option>
</select>
</label>
<label>
<input type="checkbox" [(ngModel)]="combinedSortDesc" (ngModelChange)="updateCombinedDemo()">
Descending
</label>
</div>
<div style="background: #e8f5e8; padding: 1rem; border-radius: 4px;">
<p><strong>Pipeline:</strong>
{{ sampleData.length }} records →
{{ combinedFiltered().length }} filtered →
{{ combinedSorted().length }} sorted →
{{ combinedPaginated().data.length }} on page {{ combinedPaginated().page }}
</p>
<pre style="max-height: 200px; overflow-y: auto;">{{ getCombinedResultPreview() }}</pre>
</div>
</section>
<!-- Performance Metrics -->
<section style="margin-bottom: 3rem;">
<h3>📊 Performance & Statistics</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; text-align: center;">
<h4>Total Records</h4>
<div style="font-size: 2rem; font-weight: bold; color: #007bff;">{{ sampleData.length }}</div>
</div>
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; text-align: center;">
<h4>Departments</h4>
<div style="font-size: 2rem; font-weight: bold; color: #28a745;">{{ getDepartmentCount() }}</div>
</div>
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; text-align: center;">
<h4>Avg Salary</h4>
<div style="font-size: 2rem; font-weight: bold; color: #ffc107;">{{ getAverageSalary() }}</div>
</div>
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; text-align: center;">
<h4>Active Employees</h4>
<div style="font-size: 2rem; font-weight: bold; color: #17a2b8;">{{ getActiveCount() }}%</div>
</div>
</div>
</section>
<!-- Code Examples -->
<section style="margin-bottom: 3rem;">
<h3>💻 Code Examples</h3>
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px;">
<h4>Sorting Examples:</h4>
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>// Single sort
const sorted = sortBy(data, {{ '{' }} key: 'salary', direction: 'desc', dataType: 'number' {{ '}' }});
// Multi-column sort
const multiSorted = sortByMultiple(data, {{ '{' }}
sorts: [
{{ '{' }} key: 'department', direction: 'asc', dataType: 'string' {{ '}' }},
{{ '{' }} key: 'salary', direction: 'desc', dataType: 'number' {{ '}' }}
]
{{ '}' }});</code></pre>
<h4>Filtering Examples:</h4>
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>// Property filter
const filtered = filterBy(data, {{ '{' }}
key: 'salary',
operator: 'greater_than',
value: 50000,
dataType: 'number'
{{ '}' }});
// Search across multiple properties
const searched = searchFilter(data, 'engineer', ['name', 'department']);</code></pre>
<h4>Pagination Examples:</h4>
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>// Paginate data
const paginatedResult = paginate(data, {{ '{' }} page: 1, pageSize: 10 {{ '}' }});
// Get page range for UI
const range = getPaginationRange(currentPage, totalPages, 7);</code></pre>
<h4>Transformation Examples:</h4>
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>// Group by property
const groups = groupBy(data, 'department');
// Aggregate values
const stats = aggregate(data, 'salary', ['sum', 'avg', 'min', 'max']);
// Extract specific properties
const names = pluck(data, ['name', 'salary']);</code></pre>
</div>
</section>
<!-- Library Info -->
<section style="margin-bottom: 3rem;">
<h3>📚 Library Information</h3>
<div style="background: #e3f2fd; padding: 1.5rem; border-radius: 4px;">
<h4>UI Data Utils Features:</h4>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem;">
<div>
<h5>🔄 Sorting</h5>
<ul style="margin: 0; padding-left: 1.5rem;">
<li>Single & multi-column sorting</li>
<li>Type-aware comparisons</li>
<li>Stable sort algorithms</li>
<li>Custom comparator functions</li>
</ul>
</div>
<div>
<h5>🔍 Filtering</h5>
<ul style="margin: 0; padding-left: 1.5rem;">
<li>Multiple filter operators</li>
<li>Search across properties</li>
<li>Complex filter combinations</li>
<li>Type-specific filtering</li>
</ul>
</div>
<div>
<h5>📄 Pagination</h5>
<ul style="margin: 0; padding-left: 1.5rem;">
<li>Client-side pagination</li>
<li>Page range calculations</li>
<li>Pagination metadata</li>
<li>Navigation utilities</li>
</ul>
</div>
<div>
<h5>🔄 Transformation</h5>
<ul style="margin: 0; padding-left: 1.5rem;">
<li>Group by operations</li>
<li>Data aggregations</li>
<li>Property extraction</li>
<li>Pivot tables</li>
</ul>
</div>
</div>
</div>
</section>
</div>
`,
styles: [`
h2 {
color: hsl(279, 14%, 11%);
font-size: 2rem;
margin-bottom: 1rem;
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(279, 14%, 35%);
font-size: 1.2rem;
margin-bottom: 0.5rem;
}
h5 {
color: hsl(279, 14%, 45%);
font-size: 1rem;
margin-bottom: 0.5rem;
}
section {
border: 1px solid #e9ecef;
padding: 1.5rem;
border-radius: 8px;
background: #ffffff;
}
pre {
margin: 0;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.85rem;
line-height: 1.4;
}
code {
color: hsl(279, 14%, 15%);
}
button:hover:not(:disabled) {
opacity: 0.8;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
input, select {
border: 1px solid #ccc;
border-radius: 4px;
padding: 0.25rem;
}
label {
font-weight: 500;
}
`]
})
export class DataUtilsDemoComponent {
// Sample data
sampleData: SampleData[] = [
{ id: 1, name: 'Alice Johnson', department: 'Engineering', salary: 95000, hireDate: '2022-01-15', active: true, skills: ['JavaScript', 'Angular', 'TypeScript'] },
{ id: 2, name: 'Bob Smith', department: 'Marketing', salary: 65000, hireDate: '2021-03-22', active: true, skills: ['SEO', 'Content Marketing'] },
{ id: 3, name: 'Charlie Brown', department: 'Engineering', salary: 87000, hireDate: '2023-02-10', active: false, skills: ['React', 'Node.js'] },
{ id: 4, name: 'Diana Davis', department: 'Sales', salary: 72000, hireDate: '2020-11-05', active: true, skills: ['CRM', 'Negotiation'] },
{ id: 5, name: 'Eve Wilson', department: 'Engineering', salary: 102000, hireDate: '2021-08-18', active: true, skills: ['Python', 'Machine Learning'] },
{ id: 6, name: 'Frank Miller', department: 'HR', salary: 58000, hireDate: '2022-05-30', active: false, skills: ['Recruiting', 'Employee Relations'] },
{ id: 7, name: 'Grace Lee', department: 'Marketing', salary: 69000, hireDate: '2023-01-12', active: true, skills: ['Brand Management', 'Social Media'] },
{ id: 8, name: 'Henry Taylor', department: 'Sales', salary: 78000, hireDate: '2021-12-03', active: true, skills: ['Sales Strategy', 'Lead Generation'] },
{ id: 9, name: 'Ivy Chen', department: 'Engineering', salary: 91000, hireDate: '2022-09-14', active: true, skills: ['Vue.js', 'GraphQL'] },
{ id: 10, name: 'Jack White', department: 'HR', salary: 62000, hireDate: '2020-04-25', active: false, skills: ['Payroll', 'Benefits Administration'] }
];
// Sorting controls
sortField = signal<keyof SampleData>('name');
sortDesc = signal<boolean>(false);
// Filtering controls
searchTerm = signal<string>('');
filterDepartment = signal<string>('');
minSalary = signal<number | null>(null);
// Pagination controls
currentPage = signal<number>(1);
pageSize = signal<number>(5);
// Combined demo controls
combinedActiveFilter = signal<string>('');
combinedSortField = signal<keyof SampleData>('name');
combinedSortDesc = signal<boolean>(false);
// Computed data
sortedData = computed(() => {
const config: SortConfig<SampleData> = {
key: this.sortField(),
direction: this.sortDesc() ? 'desc' : 'asc',
dataType: this.getDataType(this.sortField())
};
return sortBy(this.sampleData, config);
});
multiSortedData = computed(() => {
return sortByMultiple(this.sampleData, {
sorts: [
{ key: 'department', direction: 'asc', dataType: 'string' },
{ key: 'salary', direction: 'desc', dataType: 'number' },
{ key: 'name', direction: 'asc', dataType: 'string' }
]
});
});
searchFilteredData = computed(() => {
if (!this.searchTerm()) return this.sampleData;
return searchFilter(this.sampleData, this.searchTerm(), ['name', 'department']);
});
propertyFilteredData = computed(() => {
let filtered = this.sampleData;
const filters: FilterConfig<SampleData>[] = [];
if (this.filterDepartment()) {
filters.push({
key: 'department',
operator: 'equals',
value: this.filterDepartment(),
dataType: 'string'
});
}
if (this.minSalary() !== null) {
filters.push({
key: 'salary',
operator: 'greater_than_equal',
value: this.minSalary(),
dataType: 'number'
});
}
return filters.length > 0 ? filterByMultiple(filtered, filters) : filtered;
});
paginationResult = computed(() => {
return paginate(this.sampleData, {
page: this.currentPage(),
pageSize: this.pageSize()
});
});
pageRange = computed(() => {
return getPaginationRange(this.currentPage(), this.paginationResult().totalPages, 7);
});
// Combined operations
combinedFiltered = computed(() => {
if (!this.combinedActiveFilter()) return this.sampleData;
return filterBy(this.sampleData, {
key: 'active',
operator: 'equals',
value: this.combinedActiveFilter() === 'true',
dataType: 'boolean'
});
});
combinedSorted = computed(() => {
return sortBy(this.combinedFiltered(), {
key: this.combinedSortField(),
direction: this.combinedSortDesc() ? 'desc' : 'asc',
dataType: this.getDataType(this.combinedSortField())
});
});
combinedPaginated = computed(() => {
return paginate(this.combinedSorted(), { page: 1, pageSize: 5 });
});
// Helper methods
private getDataType(key: keyof SampleData): 'string' | 'number' | 'date' | 'boolean' {
switch (key) {
case 'salary': case 'id': return 'number';
case 'hireDate': return 'date';
case 'active': return 'boolean';
default: return 'string';
}
}
getSampleDataPreview(): string {
return JSON.stringify(this.sampleData.slice(0, 3), null, 2);
}
getSortedDataPreview(): string {
return JSON.stringify(this.sortedData().slice(0, 3).map(item => ({ name: item.name, [this.sortField()]: item[this.sortField()] })), null, 2);
}
getMultiSortedDataPreview(): string {
return JSON.stringify(this.multiSortedData().slice(0, 4).map(item => ({
name: item.name,
department: item.department,
salary: item.salary
})), null, 2);
}
getSearchFilteredDataPreview(): string {
return JSON.stringify(this.searchFilteredData().slice(0, 3).map(item => ({
name: item.name,
department: item.department
})), null, 2);
}
getPropertyFilteredDataPreview(): string {
return JSON.stringify(this.propertyFilteredData().slice(0, 3).map(item => ({
name: item.name,
department: item.department,
salary: item.salary
})), null, 2);
}
getPaginatedDataPreview(): string {
return JSON.stringify(this.paginationResult().data.map(item => ({
name: item.name,
department: item.department
})), null, 2);
}
getGroupedDataPreview(): string {
const grouped = groupBy(this.sampleData, 'department');
return JSON.stringify(grouped.map(group => ({
department: group.key,
count: group.count,
employees: group.items.map(emp => emp.name)
})), null, 2);
}
getAggregationPreview(): string {
const grouped = groupBy(this.sampleData, 'department');
const result = grouped.map(group => ({
department: group.key,
...aggregate(group.items, 'salary', ['sum', 'avg', 'min', 'max', 'count'])
}));
return JSON.stringify(result, null, 2);
}
getPluckedDataPreview(): string {
const plucked = pluck(this.sampleData, ['name', 'salary']);
return JSON.stringify(plucked.slice(0, 5), null, 2);
}
getPivotDataPreview(): string {
const pivotData = this.sampleData.map(emp => ({
department: emp.department,
status: emp.active ? 'Active' : 'Inactive',
count: 1
}));
const pivotResult = pivot(pivotData, 'department', 'status', 'count', 'sum');
return JSON.stringify(pivotResult, null, 2);
}
getCombinedResultPreview(): string {
return JSON.stringify(this.combinedPaginated().data.map(item => ({
name: item.name,
department: item.department,
salary: item.salary,
active: item.active
})), null, 2);
}
getItemRangeText(): string {
const result = this.paginationResult();
if (result.totalItems === 0) return '0 of 0';
return `${result.startIndex + 1}-${result.endIndex + 1} of ${result.totalItems}`;
}
getDepartmentCount(): number {
return unique(this.sampleData, 'department').length;
}
getAverageSalary(): string {
const avg = aggregate(this.sampleData, 'salary', ['avg']);
return '$' + Math.round(avg['avg'] || 0).toLocaleString();
}
getActiveCount(): number {
const activeCount = this.sampleData.filter(emp => emp.active).length;
return Math.round((activeCount / this.sampleData.length) * 100);
}
// Event handlers
applySingleSort(): void {
// Triggers computed update
}
applySearchFilter(): void {
// Triggers computed update
}
applyPropertyFilter(): void {
// Triggers computed update
}
updatePagination(): void {
// Ensure page is within bounds
const maxPage = this.paginationResult().totalPages;
if (this.currentPage() > maxPage) {
this.currentPage.set(maxPage || 1);
}
}
previousPage(): void {
if (this.paginationResult().hasPrevious) {
this.currentPage.update(page => page - 1);
}
}
nextPage(): void {
if (this.paginationResult().hasNext) {
this.currentPage.update(page => page + 1);
}
}
goToPage(page: number | string): void {
if (typeof page === 'number') {
this.currentPage.set(page);
}
}
updateCombinedDemo(): void {
// Triggers computed update
}
}

View File

@@ -9,6 +9,7 @@ import { TableDemoComponent } from './table-demo/table-demo.component';
import { BadgeDemoComponent } from './badge-demo/badge-demo.component';
import { MenuDemoComponent } from './menu-demo/menu-demo.component';
import { InputDemoComponent } from './input-demo/input-demo.component';
import { KanbanBoardDemoComponent } from './kanban-board-demo/kanban-board-demo.component';
import { RadioDemoComponent } from './radio-demo/radio-demo.component';
import { CheckboxDemoComponent } from './checkbox-demo/checkbox-demo.component';
import { SearchDemoComponent } from './search-demo/search-demo.component';
@@ -77,6 +78,20 @@ import { GridContainerDemoComponent } from './grid-container-demo/grid-container
import { FeedLayoutDemoComponent } from './feed-layout-demo/feed-layout-demo.component';
import { ListDetailLayoutDemoComponent } from './list-detail-layout-demo/list-detail-layout-demo.component';
import { SupportingPaneLayoutDemoComponent } from './supporting-pane-layout-demo/supporting-pane-layout-demo.component';
import { MasonryDemoComponent } from './masonry-demo/masonry-demo.component';
import { InfiniteScrollContainerDemoComponent } from './infinite-scroll-container-demo/infinite-scroll-container-demo.component';
import { StickyLayoutDemoComponent } from './sticky-layout-demo/sticky-layout-demo.component';
import { SplitViewDemoComponent } from './split-view-demo/split-view-demo.component';
import { GalleryGridDemoComponent } from './gallery-grid-demo/gallery-grid-demo.component';
import { AnimationsDemoComponent } from './animations-demo/animations-demo.component';
import { AccessibilityDemoComponent } from './accessibility-demo/accessibility-demo.component';
import { SelectDemoComponent } from './select-demo/select-demo.component';
import { TextareaDemoComponent } from './textarea-demo/textarea-demo.component';
import { DataUtilsDemoComponent } from './data-utils-demo/data-utils-demo.component';
import { HclStudioDemoComponent } from './hcl-studio-demo/hcl-studio-demo.component';
import { FontManagerDemoComponent } from './font-manager-demo/font-manager-demo.component';
import { CodeDisplayDemoComponent } from './code-display-demo/code-display-demo.component';
import { BackgroundsDemoComponent } from './backgrounds-demo/backgrounds-demo.component';
@Component({
@@ -128,6 +143,10 @@ import { SupportingPaneLayoutDemoComponent } from './supporting-pane-layout-demo
@case ("input") {
<ui-input-demo></ui-input-demo>
}
@case ("kanban-board") {
<ui-kanban-board-demo></ui-kanban-board-demo>
}
@case ("radio") {
<ui-radio-demo></ui-radio-demo>
}
@@ -140,6 +159,14 @@ import { SupportingPaneLayoutDemoComponent } from './supporting-pane-layout-demo
<ui-search-demo></ui-search-demo>
}
@case ("select") {
<ui-select-demo></ui-select-demo>
}
@case ("textarea") {
<ui-textarea-demo></ui-textarea-demo>
}
@case ("switch") {
<ui-switch-demo></ui-switch-demo>
}
@@ -360,6 +387,10 @@ import { SupportingPaneLayoutDemoComponent } from './supporting-pane-layout-demo
<ui-scroll-container-demo></ui-scroll-container-demo>
}
@case ("infinite-scroll-container") {
<ui-infinite-scroll-container-demo></ui-infinite-scroll-container-demo>
}
@case ("tabs-container") {
<ui-tabs-container-demo></ui-tabs-container-demo>
}
@@ -384,12 +415,56 @@ import { SupportingPaneLayoutDemoComponent } from './supporting-pane-layout-demo
<ui-supporting-pane-layout-demo></ui-supporting-pane-layout-demo>
}
@case ("masonry") {
<ui-masonry-demo></ui-masonry-demo>
}
@case ("sticky-layout") {
<ui-sticky-layout-demo></ui-sticky-layout-demo>
}
@case ("split-view") {
<ui-split-view-demo></ui-split-view-demo>
}
@case ("gallery-grid") {
<ui-gallery-grid-demo></ui-gallery-grid-demo>
}
@case ("animations") {
<app-animations-demo></app-animations-demo>
}
@case ("accessibility") {
<app-accessibility-demo></app-accessibility-demo>
}
@case ("data-utils") {
<ui-data-utils-demo></ui-data-utils-demo>
}
@case ("hcl-studio") {
<app-hcl-studio-demo></app-hcl-studio-demo>
}
@case ("font-manager") {
<app-font-manager-demo></app-font-manager-demo>
}
@case ("code-display") {
<app-code-display-demo></app-code-display-demo>
}
@case ("backgrounds") {
<app-backgrounds-demo></app-backgrounds-demo>
}
}
`,
imports: [AvatarDemoComponent, ButtonDemoComponent, IconButtonDemoComponent, CardDemoComponent,
ChipDemoComponent, TableDemoComponent, BadgeDemoComponent,
MenuDemoComponent, InputDemoComponent,
MenuDemoComponent, InputDemoComponent, KanbanBoardDemoComponent,
RadioDemoComponent, CheckboxDemoComponent,
SearchDemoComponent, SwitchDemoComponent, ProgressDemoComponent,
AppbarDemoComponent, BottomNavigationDemoComponent, FontAwesomeDemoComponent, ImageContainerDemoComponent,
@@ -399,7 +474,7 @@ import { SupportingPaneLayoutDemoComponent } from './supporting-pane-layout-demo
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, TagInputDemoComponent, StackDemoComponent, BoxDemoComponent, CenterDemoComponent, AspectRatioDemoComponent, BentoGridDemoComponent, BreakpointContainerDemoComponent, SectionDemoComponent, FlexDemoComponent, ColumnDemoComponent, SidebarLayoutDemoComponent, ScrollContainerDemoComponent, TabsContainerDemoComponent, DashboardShellDemoComponent, GridContainerDemoComponent, FeedLayoutDemoComponent, ListDetailLayoutDemoComponent, SupportingPaneLayoutDemoComponent]
PopoverDemoComponent, AlertDemoComponent, SnackbarDemoComponent, ToastDemoComponent, TreeViewDemoComponent, TimelineDemoComponent, StepperDemoComponent, FabMenuDemoComponent, EnhancedTableDemoComponent, SplitButtonDemoComponent, CommandPaletteDemoComponent, FloatingToolbarDemoComponent, TransferListDemoComponent, TagInputDemoComponent, StackDemoComponent, BoxDemoComponent, CenterDemoComponent, AspectRatioDemoComponent, BentoGridDemoComponent, BreakpointContainerDemoComponent, SectionDemoComponent, FlexDemoComponent, ColumnDemoComponent, SidebarLayoutDemoComponent, ScrollContainerDemoComponent, InfiniteScrollContainerDemoComponent, TabsContainerDemoComponent, DashboardShellDemoComponent, GridContainerDemoComponent, FeedLayoutDemoComponent, ListDetailLayoutDemoComponent, SupportingPaneLayoutDemoComponent, MasonryDemoComponent, StickyLayoutDemoComponent, SplitViewDemoComponent, GalleryGridDemoComponent, AnimationsDemoComponent, AccessibilityDemoComponent, SelectDemoComponent, TextareaDemoComponent, DataUtilsDemoComponent, HclStudioDemoComponent, FontManagerDemoComponent, CodeDisplayDemoComponent, BackgroundsDemoComponent]
})

View File

@@ -0,0 +1,178 @@
<div class="demo-container">
<h1>UI Font Manager Demo</h1>
<p class="demo-description">
Dynamically load Google Fonts and switch between font themes with smooth transitions.
Select a font preset to apply it globally to the entire application.
</p>
<!-- Font Preset Selection -->
<section class="demo-section">
<h2>Font Preset Selection</h2>
<p class="preset-description">
Choose from curated font combinations. The selected preset will be applied to the entire application.
</p>
<div class="preset-controls">
<div class="preset-selector">
<label for="preset-select">Choose Font Preset:</label>
<select
id="preset-select"
[value]="selectedPreset"
(change)="onPresetChange($event)">
<option value="">Select a preset...</option>
<option *ngFor="let preset of fontPresets" [value]="preset.key">
{{preset.name}} - {{preset.description}}
</option>
</select>
</div>
<div class="transition-selector" *ngIf="selectedPreset">
<label for="transition-select">Transition Effect:</label>
<select id="transition-select" [(ngModel)]="selectedTransition">
<option value="fade">Fade</option>
<option value="scale">Scale</option>
<option value="typewriter">Typewriter</option>
</select>
<button
class="apply-btn"
(click)="applyPresetWithTransition()">
Apply with {{selectedTransition | titlecase}} Effect
</button>
</div>
</div>
<!-- Current Preset Display -->
<div class="current-preset" *ngIf="currentPreset">
<h4>Currently Applied: {{currentPreset.name}}</h4>
<div class="preset-fonts">
<div class="font-assignment">
<span class="font-type">Sans-serif:</span>
<span class="font-name">{{currentPreset.fonts.sans}}</span>
</div>
<div class="font-assignment">
<span class="font-type">Serif:</span>
<span class="font-name">{{currentPreset.fonts.serif}}</span>
</div>
<div class="font-assignment">
<span class="font-type">Monospace:</span>
<span class="font-name">{{currentPreset.fonts.mono}}</span>
</div>
<div class="font-assignment">
<span class="font-type">Display:</span>
<span class="font-name">{{currentPreset.fonts.display}}</span>
</div>
</div>
</div>
</section>
<!-- Typography Preview -->
<section class="demo-section">
<h2>Typography Preview</h2>
<p class="preview-description">See how the selected fonts look in a real application context.</p>
<div class="typography-showcase">
<div class="type-sample">
<h1 style="font-family: var(--font-family-display)">Display Heading (H1)</h1>
<h2 style="font-family: var(--font-family-sans)">Sans-serif Heading (H2)</h2>
<h3 style="font-family: var(--font-family-serif)">Serif Heading (H3)</h3>
<p style="font-family: var(--font-family-sans)">
This is a paragraph using the sans-serif font. It demonstrates how readable body text
appears with the currently selected font combination. Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
</p>
<blockquote style="font-family: var(--font-family-serif)">
"This is a quote using the serif font, which often provides better readability
for longer text passages and adds elegance to quoted content."
</blockquote>
<pre><code style="font-family: var(--font-family-mono)">// Code example with monospace font
function applyFontTheme(theme) {
document.documentElement.style.setProperty('--font-family-sans', theme.sans);
document.documentElement.style.setProperty('--font-family-serif', theme.serif);
document.documentElement.style.setProperty('--font-family-mono', theme.mono);
}</code></pre>
<div class="font-info">
Current fonts: Sans (var(--font-family-sans)) | Serif (var(--font-family-serif)) |
Mono (var(--font-family-mono)) | Display (var(--font-family-display))
</div>
</div>
</div>
</section>
<!-- Available Font Presets -->
<section class="demo-section">
<h2>Available Font Presets</h2>
<div class="themes-grid">
<div
*ngFor="let preset of fontPresets"
class="theme-card"
[class.active]="preset.key === selectedPreset">
<h4>{{preset.name}}</h4>
<p class="theme-description">{{preset.description}}</p>
<div class="theme-fonts">
<div class="theme-font">
<strong>Sans:</strong>
<span>{{preset.fonts.sans}}</span>
</div>
<div class="theme-font">
<strong>Serif:</strong>
<span>{{preset.fonts.serif}}</span>
</div>
<div class="theme-font">
<strong>Mono:</strong>
<span>{{preset.fonts.mono}}</span>
</div>
<div class="theme-font">
<strong>Display:</strong>
<span>{{preset.fonts.display}}</span>
</div>
</div>
<button
class="apply-theme-btn"
(click)="applyPreset(preset.key)">
Apply {{preset.name}}
</button>
</div>
</div>
</section>
<!-- Popular Individual Fonts -->
<section class="demo-section">
<h3>Popular Individual Fonts</h3>
<p class="section-description">Load individual fonts to test and preview them.</p>
<div class="font-controls">
<div class="font-grid">
<button
*ngFor="let font of popularFonts"
class="font-button"
[class.loaded]="isLoaded(font)"
(click)="loadSingleFont(font)">
{{font}}
<span class="load-status" *ngIf="isLoaded(font)"></span>
</button>
</div>
</div>
</section>
<!-- Event Log -->
<section class="demo-section" *ngIf="eventLog.length > 0">
<h3>Event Log</h3>
<div class="event-log">
<div
*ngFor="let event of eventLog.slice(-10)"
class="event-item"
[class]="event.type">
<span class="event-time">{{event.time}}</span>
<span class="event-message">{{event.message}}</span>
</div>
</div>
</section>
</div>

View File

@@ -0,0 +1,580 @@
.demo-container {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.demo-description {
font-size: 1.1rem;
color: #666;
margin-bottom: 2rem;
line-height: 1.6;
}
.demo-section {
margin: 3rem 0;
padding: 2rem;
border: 1px solid #e0e0e0;
border-radius: 12px;
background: #fafafa;
h2, h3 {
margin-top: 0;
color: #333;
}
h2 {
font-size: 1.5rem;
margin-bottom: 1.5rem;
}
h3 {
font-size: 1.25rem;
margin-bottom: 1rem;
}
}
// Loading State Display
.state-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.state-item {
padding: 1rem;
background: white;
border-radius: 8px;
border-left: 4px solid #2196F3;
strong {
color: #333;
display: block;
margin-bottom: 0.5rem;
}
.loading {
color: #FF9800;
font-weight: 500;
}
.idle {
color: #4CAF50;
font-weight: 500;
}
}
// Manual Font Controls
.font-controls {
margin-top: 1rem;
}
.font-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 1rem;
}
.font-button {
position: relative;
padding: 1rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
background: white;
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.9rem;
&:hover {
border-color: #2196F3;
background: #f5f5f5;
}
&.loaded {
border-color: #4CAF50;
background: #e8f5e8;
color: #2e7d32;
font-weight: 500;
}
}
.load-status {
position: absolute;
top: 0.5rem;
right: 0.5rem;
color: #4CAF50;
font-weight: bold;
}
// Transition Controls
.transition-controls {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.transition-btn {
padding: 1rem;
border: 2px solid #2196F3;
border-radius: 8px;
background: white;
color: #2196F3;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 500;
&:hover {
background: #2196F3;
color: white;
}
&.scale {
border-color: #FF9800;
color: #FF9800;
&:hover {
background: #FF9800;
color: white;
}
}
&.typewriter {
border-color: #9C27B0;
color: #9C27B0;
&:hover {
background: #9C27B0;
color: white;
}
}
}
// Typography Showcase
.typography-showcase {
display: grid;
gap: 2rem;
margin-top: 1.5rem;
}
.type-sample {
padding: 2rem;
background: white;
border-radius: 12px;
border: 1px solid #e0e0e0;
transition: all 0.3s ease;
h1, h2, h3, h4 {
margin: 0 0 1rem 0;
line-height: 1.3;
}
h1 {
font-size: 2.5rem;
font-weight: 700;
}
h2 {
font-size: 2rem;
font-weight: 600;
}
h3 {
font-size: 1.75rem;
font-weight: 500;
}
h4 {
font-size: 1.5rem;
font-weight: 500;
}
p {
line-height: 1.6;
margin-bottom: 1rem;
}
pre {
background: #f5f5f5;
padding: 1rem;
border-radius: 8px;
overflow-x: auto;
margin: 1rem 0;
code {
font-size: 0.9rem;
line-height: 1.5;
}
}
}
.font-info {
font-size: 0.8rem;
color: #666;
font-style: italic;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #e0e0e0;
}
// Available Themes
.themes-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
.theme-card {
padding: 1.5rem;
background: white;
border: 1px solid #e0e0e0;
border-radius: 12px;
transition: all 0.3s ease;
&:hover {
border-color: #2196F3;
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.1);
}
h4 {
margin: 0 0 0.5rem 0;
color: #333;
font-size: 1.1rem;
}
}
.theme-description {
color: #666;
font-size: 0.9rem;
margin-bottom: 1rem;
line-height: 1.4;
}
.theme-fonts {
margin-bottom: 1.5rem;
}
.theme-font {
display: flex;
justify-content: space-between;
padding: 0.25rem 0;
font-size: 0.85rem;
strong {
color: #333;
min-width: 60px;
}
& + .theme-font {
border-top: 1px solid #f0f0f0;
}
}
.apply-theme-btn {
width: 100%;
padding: 0.75rem;
border: 1px solid #2196F3;
border-radius: 6px;
background: #2196F3;
color: white;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 500;
&:hover {
background: #1976D2;
border-color: #1976D2;
}
}
// Event Log
.event-log {
max-height: 300px;
overflow-y: auto;
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 1rem;
}
.event-item {
display: flex;
gap: 1rem;
padding: 0.5rem 0;
font-size: 0.9rem;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
&.success {
color: #4CAF50;
}
&.error {
color: #f44336;
}
&.info {
color: #2196F3;
}
}
.event-time {
color: #666;
font-size: 0.8rem;
min-width: 80px;
font-family: var(--font-family-mono, 'Courier New', monospace);
}
.event-message {
flex: 1;
}
// Font Preset Controls
.preset-controls {
display: flex;
flex-direction: column;
gap: 1.5rem;
margin-top: 1rem;
}
.preset-selector {
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: #333;
}
select {
width: 100%;
padding: 0.75rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
background: white;
transition: border-color 0.3s ease;
&:focus {
outline: none;
border-color: #2196F3;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
}
.transition-selector {
display: flex;
gap: 1rem;
align-items: end;
flex-wrap: wrap;
label {
font-weight: 600;
color: #333;
margin-bottom: 0.5rem;
display: block;
}
select {
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 6px;
font-size: 0.9rem;
min-width: 120px;
}
}
.apply-btn {
padding: 0.75rem 1.5rem;
background: #2196F3;
color: white;
border: none;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
&:hover:not(:disabled) {
background: #1976D2;
transform: translateY(-2px);
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
}
// Current Preset Display
.current-preset {
margin-top: 2rem;
padding: 1.5rem;
background: #e8f5e8;
border: 1px solid #4CAF50;
border-radius: 12px;
h4 {
margin: 0 0 1rem 0;
color: #2e7d32;
font-size: 1.1rem;
}
}
.preset-fonts {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.5rem;
}
.font-assignment {
display: flex;
justify-content: space-between;
padding: 0.5rem;
background: white;
border-radius: 6px;
font-size: 0.9rem;
.font-type {
font-weight: 600;
color: #333;
}
.font-name {
color: #666;
font-style: italic;
}
}
// Theme card active state
.theme-card.active {
border-color: #4CAF50;
background: #f1f8e9;
h4 {
color: #2e7d32;
}
}
// Section descriptions
.preset-description,
.preview-description,
.section-description {
color: #666;
font-size: 1rem;
margin-bottom: 1rem;
line-height: 1.5;
}
// Font transition animations
:global(html.font-transition-fade) {
* {
transition: font-family 0.8s ease-in-out;
}
}
:global(html.font-transition-scale) {
* {
transition: font-family 0.6s ease-in-out, transform 0.6s ease-in-out;
}
body {
transform: scale(1.02);
transition: transform 0.3s ease-in-out;
}
&.font-transition-scale body {
transform: scale(1);
}
}
:global(html.font-transition-typewriter) {
* {
transition: font-family 0.5s steps(10, end);
}
}
// Enhanced blockquote styling
blockquote {
margin: 1.5rem 0;
padding: 1rem 1.5rem;
border-left: 4px solid #2196F3;
background: #f8f9fa;
font-style: italic;
position: relative;
&::before {
content: '"';
font-size: 3rem;
color: #2196F3;
position: absolute;
top: -0.5rem;
left: 1rem;
opacity: 0.3;
}
}
// Responsive Design
@media (max-width: 768px) {
.demo-container {
padding: 1rem;
}
.demo-section {
padding: 1.5rem;
}
.font-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
}
.transition-controls {
grid-template-columns: 1fr;
}
.themes-grid {
grid-template-columns: 1fr;
}
.state-info {
grid-template-columns: 1fr;
}
.type-sample {
padding: 1.5rem;
h1 { font-size: 2rem; }
h2 { font-size: 1.75rem; }
h3 { font-size: 1.5rem; }
h4 { font-size: 1.25rem; }
}
.preset-controls {
gap: 1rem;
}
.transition-selector {
flex-direction: column;
align-items: stretch;
gap: 1rem;
select,
.apply-btn {
width: 100%;
}
}
.preset-fonts {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,448 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Subject, takeUntil } from 'rxjs';
// Mock font combinations since ui-font-manager is not yet available
const FONT_COMBINATIONS = {
'Modern Clean': {
sans: 'Inter',
serif: 'Playfair Display',
mono: 'JetBrains Mono',
display: 'Inter'
},
'Classic Editorial': {
sans: 'Source Sans Pro',
serif: 'Lora',
mono: 'Source Code Pro',
display: 'Source Sans Pro'
},
'Friendly Modern': {
sans: 'Poppins',
serif: 'Merriweather',
mono: 'Fira Code',
display: 'Poppins'
},
'Professional': {
sans: 'Roboto',
serif: 'EB Garamond',
mono: 'Roboto Mono',
display: 'Roboto'
}
};
@Component({
selector: 'app-font-manager-demo',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div class="demo-container">
<h1>UI Font Manager Demo</h1>
<p class="demo-description">
Dynamically load Google Fonts and switch between font themes with smooth transitions.
Select a font preset to apply it globally to the entire application.
</p>
<!-- Font Preset Selection -->
<section class="demo-section">
<h2>Font Preset Selection</h2>
<p class="preset-description">
Choose from curated font combinations. The selected preset will be applied to the entire application.
</p>
<div class="preset-controls">
<div class="preset-selector">
<label for="preset-select">Choose Font Preset:</label>
<select
id="preset-select"
[value]="selectedPreset"
(change)="onPresetChange($event)">
<option value="">Select a preset...</option>
<option *ngFor="let preset of fontPresets" [value]="preset.key">
{{preset.name}} - {{preset.description}}
</option>
</select>
</div>
<div class="transition-selector" *ngIf="selectedPreset">
<label for="transition-select">Transition Effect:</label>
<select id="transition-select" [(ngModel)]="selectedTransition">
<option value="fade">Fade</option>
<option value="scale">Scale</option>
<option value="typewriter">Typewriter</option>
</select>
<button
class="apply-btn"
(click)="applyPresetWithTransition()">
Apply with {{selectedTransition | titlecase}} Effect
</button>
</div>
</div>
<!-- Current Preset Display -->
<div class="current-preset" *ngIf="currentPreset">
<h4>Currently Applied: {{currentPreset.name}}</h4>
<div class="preset-fonts">
<div class="font-assignment">
<span class="font-type">Sans-serif:</span>
<span class="font-name">{{currentPreset.fonts.sans}}</span>
</div>
<div class="font-assignment">
<span class="font-type">Serif:</span>
<span class="font-name">{{currentPreset.fonts.serif}}</span>
</div>
<div class="font-assignment">
<span class="font-type">Monospace:</span>
<span class="font-name">{{currentPreset.fonts.mono}}</span>
</div>
<div class="font-assignment">
<span class="font-type">Display:</span>
<span class="font-name">{{currentPreset.fonts.display}}</span>
</div>
</div>
</div>
</section>
<!-- Typography Preview -->
<section class="demo-section">
<h2>Typography Preview</h2>
<p class="preview-description">See how the selected fonts look in a real application context.</p>
<div class="typography-showcase">
<div class="type-sample">
<h1 style="font-family: var(--font-family-display)">Display Heading (H1)</h1>
<h2 style="font-family: var(--font-family-sans)">Sans-serif Heading (H2)</h2>
<h3 style="font-family: var(--font-family-serif)">Serif Heading (H3)</h3>
<p style="font-family: var(--font-family-sans)">
This is a paragraph using the sans-serif font. It demonstrates how readable body text
appears with the currently selected font combination. Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
</p>
<blockquote style="font-family: var(--font-family-serif)">
"This is a quote using the serif font, which often provides better readability
for longer text passages and adds elegance to quoted content."
</blockquote>
<div class="font-info">
Current fonts: Sans (var(--font-family-sans)) | Serif (var(--font-family-serif)) |
Mono (var(--font-family-mono)) | Display (var(--font-family-display))
</div>
</div>
</div>
</section>
<!-- Available Font Presets -->
<section class="demo-section">
<h2>Available Font Presets</h2>
<div class="themes-grid">
<div
*ngFor="let preset of fontPresets"
class="theme-card"
[class.active]="preset.key === selectedPreset">
<h4>{{preset.name}}</h4>
<p class="theme-description">{{preset.description}}</p>
<div class="theme-fonts">
<div class="theme-font">
<strong>Sans:</strong>
<span>{{preset.fonts.sans}}</span>
</div>
<div class="theme-font">
<strong>Serif:</strong>
<span>{{preset.fonts.serif}}</span>
</div>
<div class="theme-font">
<strong>Mono:</strong>
<span>{{preset.fonts.mono}}</span>
</div>
<div class="theme-font">
<strong>Display:</strong>
<span>{{preset.fonts.display}}</span>
</div>
</div>
<button
class="apply-theme-btn"
(click)="applyPreset(preset.key)">
Apply {{preset.name}}
</button>
</div>
</div>
</section>
<!-- Popular Individual Fonts -->
<section class="demo-section">
<h3>Popular Individual Fonts</h3>
<p class="section-description">Load individual fonts to test and preview them.</p>
<div class="font-controls">
<div class="font-grid">
<button
*ngFor="let font of popularFonts"
class="font-button"
[class.loaded]="isLoaded(font)"
(click)="loadSingleFont(font)">
{{font}}
<span class="load-status" *ngIf="isLoaded(font)">✓</span>
</button>
</div>
</div>
</section>
<!-- Event Log -->
<section class="demo-section" *ngIf="eventLog.length > 0">
<h3>Event Log</h3>
<div class="event-log">
<div
*ngFor="let event of eventLog.slice(-10)"
class="event-item"
[class]="event.type">
<span class="event-time">{{event.time}}</span>
<span class="event-message">{{event.message}}</span>
</div>
</div>
</section>
</div>
`,
styleUrl: './font-manager-demo.component.scss'
})
export class FontManagerDemoComponent implements OnInit, OnDestroy {
popularFonts = ['Inter', 'Roboto', 'Playfair Display', 'JetBrains Mono', 'Montserrat', 'Lora'];
loadedFonts = new Set<string>();
// Font preset management
fontPresets = [
{
key: 'modern-clean',
name: 'Modern Clean',
description: 'Contemporary and minimal design with Inter and Playfair Display',
fonts: FONT_COMBINATIONS['Modern Clean']
},
{
key: 'classic-editorial',
name: 'Classic Editorial',
description: 'Traditional publishing style with Source Sans Pro and Lora',
fonts: FONT_COMBINATIONS['Classic Editorial']
},
{
key: 'friendly-modern',
name: 'Friendly Modern',
description: 'Approachable and warm with Poppins and Merriweather',
fonts: FONT_COMBINATIONS['Friendly Modern']
},
{
key: 'professional',
name: 'Professional',
description: 'Corporate and reliable with Roboto and EB Garamond',
fonts: FONT_COMBINATIONS['Professional']
},
{
key: 'system',
name: 'System Default',
description: 'Use system fonts for optimal performance',
fonts: {
sans: 'system-ui',
serif: 'ui-serif',
mono: 'ui-monospace',
display: 'system-ui'
}
}
];
selectedPreset: string = '';
selectedTransition: 'fade' | 'scale' | 'typewriter' = 'fade';
currentPreset: any = null;
eventLog: Array<{time: string, message: string, type: 'success' | 'error' | 'info'}> = [];
private destroy$ = new Subject<void>();
constructor() {}
ngOnInit(): void {
this.logEvent('Component initialized', 'info');
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
onThemeChanged(themeName: string): void {
this.logEvent(`Theme changed to: ${themeName}`, 'success');
}
onFontLoaded(fontName: string): void {
this.logEvent(`Font loaded: ${fontName}`, 'success');
this.loadedFonts.add(fontName);
}
onFontError(event: {fontName: string, error: string}): void {
this.logEvent(`Font error - ${event.fontName}: ${event.error}`, 'error');
}
loadSingleFont(fontName: string): void {
this.logEvent(`Loading font: ${fontName}`, 'info');
// Load font via Google Fonts API
this.loadGoogleFont(fontName);
this.loadedFonts.add(fontName);
this.logEvent(`Successfully loaded: ${fontName}`, 'success');
}
applyTheme(themeName: string): void {
this.logEvent(`Applying theme: ${themeName}`, 'info');
this.logEvent(`Successfully applied theme: ${themeName}`, 'success');
}
isLoaded(fontName: string): boolean {
return this.loadedFonts.has(fontName);
}
private loadGoogleFont(fontName: string): void {
const link = document.createElement('link');
link.href = `https://fonts.googleapis.com/css2?family=${fontName.replace(' ', '+')}&display=swap`;
link.rel = 'stylesheet';
document.head.appendChild(link);
}
onPresetChange(event: Event): void {
const target = event.target as HTMLSelectElement;
this.selectedPreset = target.value;
if (this.selectedPreset) {
const preset = this.fontPresets.find(p => p.key === this.selectedPreset);
if (preset) {
this.logEvent(`Selected preset: ${preset.name}`, 'info');
}
}
}
applyPreset(presetKey: string): void {
const preset = this.fontPresets.find(p => p.key === presetKey);
if (!preset) {
this.logEvent(`Preset not found: ${presetKey}`, 'error');
return;
}
this.selectedPreset = presetKey;
this.logEvent(`Applying preset: ${preset.name}`, 'info');
// Apply fonts to document root for global effect
this.applyFontsGlobally(preset.fonts);
this.currentPreset = preset;
}
applyPresetWithTransition(): void {
const preset = this.fontPresets.find(p => p.key === this.selectedPreset);
if (!preset) return;
this.logEvent(`Applying preset "${preset.name}" with ${this.selectedTransition} transition`, 'info');
// Apply fonts with transition effect
const fontVariables = {
'--font-family-sans': this.buildGlobalFontStack(preset.fonts.sans),
'--font-family-serif': this.buildGlobalFontStack(preset.fonts.serif),
'--font-family-mono': this.buildGlobalFontStack(preset.fonts.mono),
'--font-family-display': this.buildGlobalFontStack(preset.fonts.display)
};
this.applyFontsWithTransition(fontVariables, this.selectedTransition);
this.currentPreset = preset;
}
private applyFontsGlobally(fonts: any): void {
const root = document.documentElement;
// Set CSS custom properties on the document root
root.style.setProperty('--font-family-sans', this.buildGlobalFontStack(fonts.sans));
root.style.setProperty('--font-family-serif', this.buildGlobalFontStack(fonts.serif));
root.style.setProperty('--font-family-mono', this.buildGlobalFontStack(fonts.mono));
root.style.setProperty('--font-family-display', this.buildGlobalFontStack(fonts.display));
// Also load the fonts if they're Google Fonts
if (fonts.sans && fonts.sans !== 'system-ui') {
this.loadSingleFont(fonts.sans);
}
if (fonts.serif && fonts.serif !== 'ui-serif') {
this.loadSingleFont(fonts.serif);
}
if (fonts.mono && fonts.mono !== 'ui-monospace') {
this.loadSingleFont(fonts.mono);
}
if (fonts.display && fonts.display !== 'system-ui') {
this.loadSingleFont(fonts.display);
}
this.logEvent(`Applied fonts globally to document root`, 'success');
}
private applyFontsWithTransition(fontVariables: Record<string, string>, transitionType: 'fade' | 'scale' | 'typewriter'): void {
const root = document.documentElement;
// Add transition class
root.classList.add(`font-transition-${transitionType}`);
// Apply font changes
Object.entries(fontVariables).forEach(([property, value]) => {
root.style.setProperty(property, value);
});
// Remove transition class after animation
setTimeout(() => {
root.classList.remove(`font-transition-${transitionType}`);
this.logEvent(`${transitionType} transition completed`, 'success');
}, 1000);
}
private buildGlobalFontStack(fontName: string): string {
// Handle system fonts
if (fontName === 'system-ui') {
return 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
}
if (fontName === 'ui-serif') {
return 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif';
}
if (fontName === 'ui-monospace') {
return 'ui-monospace, SFMono-Regular, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
}
// For Google Fonts, add appropriate fallbacks
const fontMap: Record<string, string> = {
'Inter': `"Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`,
'Roboto': `"Roboto", Arial, Helvetica, sans-serif`,
'Poppins': `"Poppins", Arial, Helvetica, sans-serif`,
'Source Sans Pro': `"Source Sans Pro", Arial, Helvetica, sans-serif`,
'Montserrat': `"Montserrat", Arial, Helvetica, sans-serif`,
'Playfair Display': `"Playfair Display", Georgia, "Times New Roman", serif`,
'Lora': `"Lora", Georgia, "Times New Roman", serif`,
'Merriweather': `"Merriweather", Georgia, "Times New Roman", serif`,
'EB Garamond': `"EB Garamond", Georgia, "Times New Roman", serif`,
'JetBrains Mono': `"JetBrains Mono", Consolas, Monaco, "Courier New", monospace`,
'Source Code Pro': `"Source Code Pro", Consolas, Monaco, "Courier New", monospace`,
'Fira Code': `"Fira Code", Consolas, Monaco, "Courier New", monospace`,
'Roboto Mono': `"Roboto Mono", Consolas, Monaco, "Courier New", monospace`,
'Oswald': `"Oswald", Impact, "Arial Black", sans-serif`,
'Raleway': `"Raleway", Arial, Helvetica, sans-serif`
};
return fontMap[fontName] || `"${fontName}", sans-serif`;
}
private logEvent(message: string, type: 'success' | 'error' | 'info'): void {
const time = new Date().toLocaleTimeString();
this.eventLog.push({ time, message, type });
// Keep only last 50 events
if (this.eventLog.length > 50) {
this.eventLog = this.eventLog.slice(-50);
}
}
}

View File

@@ -0,0 +1 @@
export * from './font-manager-demo.component';

View File

@@ -0,0 +1,274 @@
@use '../../../../../ui-design-system/src/styles/semantic' as *;
.demo-container {
padding: $semantic-spacing-component-lg;
max-width: 1200px;
margin: 0 auto;
}
.demo-section {
margin-bottom: $semantic-spacing-layout-section-xl;
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-component-md;
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
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-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-component-md;
}
}
.demo-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: $semantic-spacing-grid-gap-lg;
margin-bottom: $semantic-spacing-layout-section-md;
}
.demo-item {
padding: $semantic-spacing-component-md;
background: $semantic-color-surface-secondary;
border-radius: $semantic-border-radius-md;
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
}
.demo-controls {
display: flex;
flex-wrap: wrap;
gap: $semantic-spacing-component-sm;
margin-bottom: $semantic-spacing-component-lg;
padding: $semantic-spacing-component-md;
background: $semantic-color-surface-elevated;
border-radius: $semantic-border-radius-md;
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
button {
padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
border: $semantic-border-width-1 solid $semantic-color-border-primary;
border-radius: $semantic-border-radius-sm;
background: $semantic-color-surface-primary;
color: $semantic-color-text-primary;
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);
&:hover {
background: $semantic-color-surface-elevated;
box-shadow: $semantic-shadow-button-hover;
}
&:focus-visible {
outline: 2px solid $semantic-color-focus;
outline-offset: 2px;
}
&.active {
background: $semantic-color-primary;
color: $semantic-color-on-primary;
border-color: $semantic-color-primary;
}
&:disabled {
opacity: $semantic-opacity-disabled;
cursor: not-allowed;
pointer-events: none;
}
}
}
.selection-info {
margin-top: $semantic-spacing-component-lg;
padding: $semantic-spacing-component-md;
background: $semantic-color-surface-elevated;
border-radius: $semantic-border-radius-md;
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
p {
font-weight: $semantic-typography-font-weight-semibold;
margin-bottom: $semantic-spacing-component-sm;
}
ul {
list-style: none;
padding: 0;
margin: 0;
li {
padding: $semantic-spacing-content-line-tight 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);
&:last-child {
border-bottom: none;
}
}
}
}
.event-log {
background: $semantic-color-surface-elevated;
border-radius: $semantic-border-radius-md;
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
overflow: hidden;
.event-controls {
padding: $semantic-spacing-component-md;
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
background: $semantic-color-surface-secondary;
button {
padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
border: $semantic-border-width-1 solid $semantic-color-border-primary;
border-radius: $semantic-border-radius-sm;
background: $semantic-color-surface-primary;
color: $semantic-color-text-primary;
cursor: pointer;
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
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);
&:hover {
background: $semantic-color-surface-elevated;
box-shadow: $semantic-shadow-button-hover;
}
&:focus-visible {
outline: 2px solid $semantic-color-focus;
outline-offset: 2px;
}
}
}
.event-list {
max-height: 300px;
overflow-y: auto;
.no-events {
padding: $semantic-spacing-component-lg;
text-align: center;
color: $semantic-color-text-tertiary;
font-style: italic;
}
.event-item {
display: grid;
grid-template-columns: 120px 1fr auto;
gap: $semantic-spacing-component-sm;
padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
&:last-child {
border-bottom: none;
}
.event-type {
font-weight: $semantic-typography-font-weight-semibold;
color: $semantic-color-primary;
font-family: map-get($semantic-typography-body-small, font-family);
font-size: map-get($semantic-typography-body-small, font-size);
line-height: map-get($semantic-typography-body-small, line-height);
}
.event-details {
color: $semantic-color-text-primary;
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);
}
.event-time {
color: $semantic-color-text-tertiary;
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);
white-space: nowrap;
}
}
}
}
// Responsive Design
@media (max-width: $semantic-breakpoint-md - 1) {
.demo-container {
padding: $semantic-spacing-component-md;
}
.demo-grid {
grid-template-columns: 1fr;
gap: $semantic-spacing-grid-gap-md;
}
.demo-controls {
flex-direction: column;
align-items: stretch;
button {
width: 100%;
text-align: center;
}
}
.event-item {
grid-template-columns: 1fr;
gap: $semantic-spacing-content-line-tight;
.event-time {
justify-self: end;
grid-row: 1;
grid-column: 1;
}
.event-type {
grid-row: 2;
grid-column: 1;
}
.event-details {
grid-row: 3;
grid-column: 1;
}
}
}
@media (max-width: $semantic-breakpoint-sm - 1) {
.demo-container {
padding: $semantic-spacing-component-sm;
}
.demo-section {
margin-bottom: $semantic-spacing-layout-section-md;
}
}

View File

@@ -0,0 +1,430 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { GalleryGridComponent, GalleryGridItem } from 'ui-essentials';
@Component({
selector: 'ui-gallery-grid-demo',
standalone: true,
imports: [CommonModule, GalleryGridComponent],
template: `
<div class="demo-container">
<h2>Gallery Grid Demo</h2>
<p>A responsive media grid with lightbox functionality for organizing and displaying images.</p>
<!-- Column Variants -->
<section class="demo-section">
<h3>Column Variants</h3>
<div class="demo-grid">
@for (columnCount of columnVariants; track columnCount) {
<div class="demo-item">
<h4>{{ columnCount }} Columns</h4>
<ui-gallery-grid
[items]="sampleItems.slice(0, 6)"
[columns]="columnCount">
</ui-gallery-grid>
</div>
}
</div>
</section>
<!-- Gap Size Variants -->
<section class="demo-section">
<h3>Gap Sizes</h3>
<div class="demo-grid">
@for (gap of gapSizes; track gap) {
<div class="demo-item">
<h4>{{ gap | titlecase }} Gap</h4>
<ui-gallery-grid
[items]="sampleItems.slice(0, 6)"
[columns]="3"
[gap]="gap">
</ui-gallery-grid>
</div>
}
</div>
</section>
<!-- Object Fit Variants -->
<section class="demo-section">
<h3>Object Fit Options</h3>
<div class="demo-grid">
@for (fit of objectFitOptions; track fit) {
<div class="demo-item">
<h4>{{ fit | titlecase }} Fit</h4>
<ui-gallery-grid
[items]="landscapeItems"
[columns]="3"
[objectFit]="fit">
</ui-gallery-grid>
</div>
}
</div>
</section>
<!-- Auto-fit Responsive -->
<section class="demo-section">
<h3>Auto-fit Responsive</h3>
<p>Automatically adjusts columns based on available space (resize window to see effect):</p>
<ui-gallery-grid
[items]="sampleItems"
columns="auto-fit"
[gap]="'md'">
</ui-gallery-grid>
</section>
<!-- Masonry Layout -->
<section class="demo-section">
<h3>Masonry Layout</h3>
<p>Items align to top with varying heights:</p>
<ui-gallery-grid
[items]="masonryItems"
[columns]="4"
[masonry]="true"
[gap]="'sm'">
</ui-gallery-grid>
</section>
<!-- Interactive Features -->
<section class="demo-section">
<h3>Interactive Features</h3>
<div class="demo-controls">
<button (click)="toggleOverlay()" [class.active]="showOverlay">
{{ showOverlay ? 'Hide' : 'Show' }} Overlay
</button>
<button (click)="toggleZoomIndicator()" [class.active]="showZoomIndicator">
{{ showZoomIndicator ? 'Hide' : 'Show' }} Zoom Indicator
</button>
<button (click)="toggleSelection()" [class.active]="selectionEnabled">
{{ selectionEnabled ? 'Disable' : 'Enable' }} Selection
</button>
<button (click)="selectAll()" [disabled]="!selectionEnabled">
Select All
</button>
<button (click)="deselectAll()" [disabled]="!selectionEnabled">
Deselect All
</button>
</div>
<ui-gallery-grid
[items]="interactiveItems"
[columns]="4"
[showOverlay]="showOverlay"
[showZoomIndicator]="showZoomIndicator"
(itemClick)="handleItemClick($event)"
(itemSelect)="handleItemSelect($event)"
(imageLoad)="handleImageLoad($event)"
(imageError)="handleImageError($event)">
</ui-gallery-grid>
@if (selectedItems.length > 0) {
<div class="selection-info">
<p>Selected Items: {{ selectedItems.length }}</p>
<ul>
@for (item of selectedItems; track item.id) {
<li>{{ item.title || item.alt || 'Item ' + item.id }}</li>
}
</ul>
</div>
}
</section>
<!-- Empty State -->
<section class="demo-section">
<h3>Empty State</h3>
<ui-gallery-grid
[items]="[]"
[columns]="3"
emptyText="No photos in this album yet">
</ui-gallery-grid>
</section>
<!-- Loading State -->
<section class="demo-section">
<h3>Loading State</h3>
<ui-gallery-grid
[items]="loadingItems"
[columns]="3">
</ui-gallery-grid>
</section>
<!-- Event Log -->
<section class="demo-section">
<h3>Event Log</h3>
<div class="event-log">
<div class="event-controls">
<button (click)="clearEventLog()">Clear Log</button>
</div>
<div class="event-list">
@if (eventLog.length === 0) {
<p class="no-events">No events yet. Interact with the gallery above to see events.</p>
} @else {
@for (event of eventLog; track event.id) {
<div class="event-item">
<span class="event-type">{{ event.type }}</span>
<span class="event-details">{{ event.details }}</span>
<span class="event-time">{{ event.timestamp | date:'HH:mm:ss' }}</span>
</div>
}
}
</div>
</div>
</section>
</div>
`,
styleUrl: './gallery-grid-demo.component.scss'
})
export class GalleryGridDemoComponent {
columnVariants = [2, 3, 4] as const;
gapSizes = ['sm', 'md', 'lg'] as const;
objectFitOptions = ['cover', 'contain', 'fill'] as const;
showOverlay = true;
showZoomIndicator = true;
selectionEnabled = false;
selectedItems: GalleryGridItem[] = [];
eventLog: Array<{id: number; type: string; details: string; timestamp: Date}> = [];
sampleItems: GalleryGridItem[] = [
{
id: 1,
src: 'https://picsum.photos/400/300?random=1',
thumbnail: 'https://picsum.photos/200/150?random=1',
alt: 'Sample image 1',
title: 'Mountain Landscape',
caption: 'Beautiful mountain vista at sunset'
},
{
id: 2,
src: 'https://picsum.photos/400/300?random=2',
thumbnail: 'https://picsum.photos/200/150?random=2',
alt: 'Sample image 2',
title: 'Ocean Waves',
caption: 'Crashing waves on a sandy beach'
},
{
id: 3,
src: 'https://picsum.photos/400/300?random=3',
thumbnail: 'https://picsum.photos/200/150?random=3',
alt: 'Sample image 3',
title: 'Forest Path',
caption: 'Winding trail through dense woods'
},
{
id: 4,
src: 'https://picsum.photos/400/300?random=4',
thumbnail: 'https://picsum.photos/200/150?random=4',
alt: 'Sample image 4',
title: 'City Skyline',
caption: 'Urban architecture at night'
},
{
id: 5,
src: 'https://picsum.photos/400/300?random=5',
thumbnail: 'https://picsum.photos/200/150?random=5',
alt: 'Sample image 5',
title: 'Desert Dunes',
caption: 'Rolling sand dunes in morning light'
},
{
id: 6,
src: 'https://picsum.photos/400/300?random=6',
thumbnail: 'https://picsum.photos/200/150?random=6',
alt: 'Sample image 6',
title: 'Lake Reflection',
caption: 'Mirror-like water surface'
},
{
id: 7,
src: 'https://picsum.photos/400/300?random=7',
thumbnail: 'https://picsum.photos/200/150?random=7',
alt: 'Sample image 7',
title: 'Flower Field',
caption: 'Colorful wildflowers in bloom'
},
{
id: 8,
src: 'https://picsum.photos/400/300?random=8',
thumbnail: 'https://picsum.photos/200/150?random=8',
alt: 'Sample image 8',
title: 'Snow Peaks',
caption: 'Snow-capped mountain range'
}
];
landscapeItems: GalleryGridItem[] = [
{
id: 'l1',
src: 'https://picsum.photos/800/400?random=11',
thumbnail: 'https://picsum.photos/200/100?random=11',
alt: 'Landscape image 1',
title: 'Wide Landscape',
caption: 'Panoramic view'
},
{
id: 'l2',
src: 'https://picsum.photos/800/400?random=12',
thumbnail: 'https://picsum.photos/200/100?random=12',
alt: 'Landscape image 2',
title: 'River Valley',
caption: 'Flowing water through hills'
},
{
id: 'l3',
src: 'https://picsum.photos/800/400?random=13',
thumbnail: 'https://picsum.photos/200/100?random=13',
alt: 'Landscape image 3',
title: 'Coastal View',
caption: 'Rocky coastline'
}
];
masonryItems: GalleryGridItem[] = [
{
id: 'm1',
src: 'https://picsum.photos/300/400?random=21',
thumbnail: 'https://picsum.photos/150/200?random=21',
alt: 'Tall image 1',
title: 'Portrait 1'
},
{
id: 'm2',
src: 'https://picsum.photos/300/200?random=22',
thumbnail: 'https://picsum.photos/150/100?random=22',
alt: 'Wide image 1',
title: 'Landscape 1'
},
{
id: 'm3',
src: 'https://picsum.photos/300/500?random=23',
thumbnail: 'https://picsum.photos/150/250?random=23',
alt: 'Very tall image',
title: 'Portrait 2'
},
{
id: 'm4',
src: 'https://picsum.photos/300/300?random=24',
thumbnail: 'https://picsum.photos/150/150?random=24',
alt: 'Square image',
title: 'Square 1'
},
{
id: 'm5',
src: 'https://picsum.photos/300/350?random=25',
thumbnail: 'https://picsum.photos/150/175?random=25',
alt: 'Tall image 2',
title: 'Portrait 3'
},
{
id: 'm6',
src: 'https://picsum.photos/300/250?random=26',
thumbnail: 'https://picsum.photos/150/125?random=26',
alt: 'Medium image',
title: 'Landscape 2'
}
];
interactiveItems: GalleryGridItem[] = this.sampleItems.slice(0, 8).map(item => ({
...item,
selected: false
}));
loadingItems: GalleryGridItem[] = [
{
id: 'loading1',
src: 'https://picsum.photos/400/300?random=31',
thumbnail: 'https://picsum.photos/200/150?random=31',
alt: 'Loading image 1',
title: 'Loading Item 1',
loading: true
},
{
id: 'loading2',
src: 'https://picsum.photos/400/300?random=32',
thumbnail: 'https://picsum.photos/200/150?random=32',
alt: 'Loading image 2',
title: 'Loading Item 2',
loading: true
},
{
id: 'loading3',
src: 'https://picsum.photos/400/300?random=33',
thumbnail: 'https://picsum.photos/200/150?random=33',
alt: 'Loading image 3',
title: 'Loaded Item',
loading: false
}
];
private eventCounter = 0;
toggleOverlay(): void {
this.showOverlay = !this.showOverlay;
}
toggleZoomIndicator(): void {
this.showZoomIndicator = !this.showZoomIndicator;
}
toggleSelection(): void {
this.selectionEnabled = !this.selectionEnabled;
if (!this.selectionEnabled) {
this.deselectAll();
}
}
selectAll(): void {
this.interactiveItems.forEach(item => item.selected = true);
this.updateSelectedItems();
}
deselectAll(): void {
this.interactiveItems.forEach(item => item.selected = false);
this.updateSelectedItems();
}
handleItemClick(event: { item: GalleryGridItem; event: MouseEvent }): void {
this.addEvent('Item Click', `Clicked item: ${event.item.title || event.item.id}`);
if (this.selectionEnabled) {
event.item.selected = !event.item.selected;
this.updateSelectedItems();
this.addEvent('Item Selection', `${event.item.selected ? 'Selected' : 'Deselected'} item: ${event.item.title || event.item.id}`);
}
}
handleItemSelect(item: GalleryGridItem): void {
this.updateSelectedItems();
this.addEvent('Item Select', `Selected item: ${item.title || item.id}`);
}
handleImageLoad(event: { item: GalleryGridItem; event: Event }): void {
this.addEvent('Image Load', `Loaded image: ${event.item.title || event.item.id}`);
}
handleImageError(event: { item: GalleryGridItem; event: Event }): void {
this.addEvent('Image Error', `Failed to load image: ${event.item.title || event.item.id}`);
}
clearEventLog(): void {
this.eventLog = [];
}
private updateSelectedItems(): void {
this.selectedItems = this.interactiveItems.filter(item => item.selected);
}
private addEvent(type: string, details: string): void {
this.eventLog.unshift({
id: ++this.eventCounter,
type,
details,
timestamp: new Date()
});
// Keep only the last 20 events
if (this.eventLog.length > 20) {
this.eventLog = this.eventLog.slice(0, 20);
}
}
}

View File

@@ -0,0 +1 @@
<p>hcl-studio-demo works!</p>

View File

@@ -0,0 +1,478 @@
/**
* HCL Studio Demo Styles
* Showcases dynamic theming using CSS custom properties
*/
.hcl-studio-demo {
padding: 2rem;
background: var(--color-surface);
color: var(--color-on-surface);
min-height: 100vh;
transition: background-color 0.3s ease, color 0.3s ease;
}
// ==========================================================================
// HEADER STYLES
// ==========================================================================
.demo-header {
text-align: center;
margin-bottom: 3rem;
padding: 2rem;
background: var(--color-surface-container);
border-radius: 1rem;
h1 {
margin: 0 0 0.5rem 0;
color: var(--color-primary);
font-size: 2.5rem;
font-weight: 600;
}
p {
margin: 0;
color: var(--color-on-surface-variant);
font-size: 1.2rem;
}
}
// ==========================================================================
// CURRENT THEME INFO
// ==========================================================================
.current-theme-info {
margin-bottom: 2rem;
padding: 1.5rem;
background: var(--color-surface-high);
border-radius: 0.5rem;
border: 1px solid var(--color-outline-variant);
h3 {
margin: 0 0 1rem 0;
color: var(--color-on-surface);
}
.color-swatches {
display: flex;
gap: 1rem;
.swatch {
flex: 1;
padding: 1rem;
border-radius: 0.5rem;
text-align: center;
color: white;
font-weight: 600;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
span {
font-size: 0.9rem;
}
}
}
}
// ==========================================================================
// MODE CONTROLS
// ==========================================================================
.mode-controls {
margin-bottom: 2rem;
text-align: center;
.mode-toggle-btn {
padding: 0.75rem 2rem;
border: 2px solid var(--color-outline);
border-radius: 2rem;
background: var(--color-surface);
color: var(--color-on-surface);
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: var(--color-surface-high);
border-color: var(--color-primary);
}
&.dark {
background: var(--color-primary);
color: var(--color-on-primary);
border-color: var(--color-primary);
}
}
}
// ==========================================================================
// THEMES SECTION
// ==========================================================================
.themes-section {
margin-bottom: 3rem;
h3 {
margin: 0 0 1.5rem 0;
color: var(--color-on-surface);
font-size: 1.5rem;
}
.theme-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
.theme-card {
padding: 1rem;
background: var(--color-surface-container);
border: 2px solid var(--color-outline-variant);
border-radius: 0.75rem;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
border-color: var(--color-primary);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
&.active {
border-color: var(--color-primary);
background: var(--color-primary-container);
.theme-name {
color: var(--color-on-primary-container);
font-weight: 600;
}
}
.theme-preview {
display: flex;
height: 40px;
border-radius: 0.5rem;
overflow: hidden;
margin-bottom: 0.75rem;
.color-strip {
flex: 1;
}
}
.theme-name {
display: block;
text-align: center;
color: var(--color-on-surface);
font-size: 0.9rem;
font-weight: 500;
}
}
}
}
// ==========================================================================
// CUSTOM THEME SECTION
// ==========================================================================
.custom-theme-section {
margin-bottom: 3rem;
padding: 2rem;
background: var(--color-surface-high);
border-radius: 1rem;
border: 1px solid var(--color-outline-variant);
h3 {
margin: 0 0 1.5rem 0;
color: var(--color-on-surface);
font-size: 1.5rem;
}
.custom-inputs {
display: grid;
gap: 1.5rem;
margin-bottom: 2rem;
@media (min-width: 768px) {
grid-template-columns: repeat(3, 1fr);
}
.color-input-group {
label {
display: block;
margin-bottom: 0.5rem;
color: var(--color-on-surface);
font-weight: 600;
font-size: 0.9rem;
}
input[type="color"] {
width: 50px;
height: 40px;
border: 2px solid var(--color-outline);
border-radius: 0.5rem;
margin-right: 0.5rem;
cursor: pointer;
}
input[type="text"] {
flex: 1;
padding: 0.75rem;
border: 2px solid var(--color-outline);
border-radius: 0.5rem;
background: var(--color-surface);
color: var(--color-on-surface);
font-size: 1rem;
&:focus {
outline: none;
border-color: var(--color-primary);
}
}
}
}
.custom-actions {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
button {
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
&.preview-btn {
background: var(--color-secondary);
color: var(--color-on-secondary);
border: none;
&:hover {
opacity: 0.9;
transform: translateY(-1px);
}
}
&.apply-btn {
background: var(--color-primary);
color: var(--color-on-primary);
border: none;
&:hover {
opacity: 0.9;
transform: translateY(-1px);
}
}
}
}
.color-preview {
h4 {
margin: 0 0 1rem 0;
color: var(--color-on-surface);
}
.preview-swatches {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 0.5rem;
.preview-swatch {
padding: 0.75rem 0.5rem;
border-radius: 0.5rem;
text-align: center;
font-size: 0.8rem;
font-weight: 600;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
}
}
}
// ==========================================================================
// DEMO COMPONENTS
// ==========================================================================
.demo-components {
h3 {
margin: 0 0 1.5rem 0;
color: var(--color-on-surface);
font-size: 1.5rem;
}
.component-showcase {
display: grid;
gap: 2rem;
@media (min-width: 768px) {
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}
.showcase-section {
padding: 1.5rem;
background: var(--color-surface-container);
border-radius: 0.75rem;
border: 1px solid var(--color-outline-variant);
h4 {
margin: 0 0 1rem 0;
color: var(--color-on-surface);
font-size: 1.2rem;
}
// Button Styles
.button-group {
display: flex;
flex-direction: column;
gap: 0.75rem;
button {
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
border: none;
transition: all 0.3s ease;
&.btn-primary {
background: var(--color-primary);
color: var(--color-on-primary);
&:hover {
opacity: 0.9;
transform: translateY(-1px);
}
}
&.btn-secondary {
background: var(--color-secondary);
color: var(--color-on-secondary);
&:hover {
opacity: 0.9;
transform: translateY(-1px);
}
}
&.btn-tertiary {
background: var(--color-tertiary);
color: var(--color-on-tertiary);
&:hover {
opacity: 0.9;
transform: translateY(-1px);
}
}
}
}
// Card Styles
.card-group {
display: flex;
flex-direction: column;
gap: 1rem;
.demo-card {
padding: 1.5rem;
border-radius: 0.5rem;
&.primary {
background: var(--color-primary-container);
color: var(--color-on-primary-container);
}
&.secondary {
background: var(--color-secondary-container);
color: var(--color-on-secondary-container);
}
h5 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
}
p {
margin: 0;
opacity: 0.8;
}
}
}
// Surface Styles
.surface-group {
display: flex;
flex-direction: column;
gap: 1rem;
.demo-surface {
padding: 1rem;
border-radius: 0.5rem;
text-align: center;
font-weight: 600;
&.surface-low {
background: var(--color-surface-low);
color: var(--color-on-surface);
}
&.surface-container {
background: var(--color-surface-container);
color: var(--color-on-surface);
}
&.surface-high {
background: var(--color-surface-high);
color: var(--color-on-surface);
}
}
}
}
}
}
// ==========================================================================
// RESPONSIVE ADJUSTMENTS
// ==========================================================================
@media (max-width: 768px) {
.hcl-studio-demo {
padding: 1rem;
}
.demo-header {
padding: 1.5rem;
h1 {
font-size: 2rem;
}
p {
font-size: 1rem;
}
}
.theme-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
.custom-inputs {
grid-template-columns: 1fr !important;
}
.custom-actions {
flex-direction: column;
}
}
// ==========================================================================
// TRANSITIONS FOR SMOOTH THEME CHANGES
// ==========================================================================
* {
transition: background-color 0.3s ease,
color 0.3s ease,
border-color 0.3s ease,
opacity 0.3s ease;
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HclStudioDemoComponent } from './hcl-studio-demo.component';
describe('HclStudioDemoComponent', () => {
let component: HclStudioDemoComponent;
let fixture: ComponentFixture<HclStudioDemoComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HclStudioDemoComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HclStudioDemoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,288 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import {
HCLStudioService,
DEFAULT_THEMES,
ThemePreview,
BrandColors,
HCLConverter,
PaletteGenerator
} from 'hcl-studio';
@Component({
selector: 'app-hcl-studio-demo',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div class="hcl-studio-demo">
<div class="demo-header">
<h1>HCL Studio Demo</h1>
<p>Dynamic theme switching using HCL color space</p>
</div>
<!-- Current Theme Info -->
<div class="current-theme-info" *ngIf="currentTheme">
<h3>Current Theme: {{ currentTheme.name }}</h3>
<div class="color-swatches">
<div class="swatch" [style.background]="currentTheme.primary">
<span>Primary</span>
</div>
<div class="swatch" [style.background]="currentTheme.secondary">
<span>Secondary</span>
</div>
<div class="swatch" [style.background]="currentTheme.tertiary">
<span>Tertiary</span>
</div>
</div>
</div>
<!-- Mode Toggle -->
<div class="mode-controls">
<button
class="mode-toggle-btn"
(click)="toggleMode()"
[class.dark]="isDarkMode"
>
{{ isDarkMode ? 'Switch to Light' : 'Switch to Dark' }}
</button>
</div>
<!-- Built-in Themes -->
<div class="themes-section">
<h3>Built-in Themes</h3>
<div class="theme-grid">
<div
*ngFor="let theme of availableThemes"
class="theme-card"
[class.active]="theme.id === currentTheme?.id"
(click)="switchTheme(theme.id)"
>
<div class="theme-preview">
<div class="color-strip" [style.background]="theme.primary"></div>
<div class="color-strip" [style.background]="theme.secondary"></div>
<div class="color-strip" [style.background]="theme.tertiary"></div>
</div>
<span class="theme-name">{{ theme.name }}</span>
</div>
</div>
</div>
<!-- Custom Color Input -->
<div class="custom-theme-section">
<h3>Create Custom Theme</h3>
<div class="custom-inputs">
<div class="color-input-group">
<label>Primary Color:</label>
<input
type="color"
[(ngModel)]="customColors.primary"
(change)="previewCustomTheme()"
>
<input
type="text"
[(ngModel)]="customColors.primary"
(input)="previewCustomTheme()"
placeholder="#6750A4"
>
</div>
<div class="color-input-group">
<label>Secondary Color:</label>
<input
type="color"
[(ngModel)]="customColors.secondary"
(change)="previewCustomTheme()"
>
<input
type="text"
[(ngModel)]="customColors.secondary"
(input)="previewCustomTheme()"
placeholder="#625B71"
>
</div>
<div class="color-input-group">
<label>Tertiary Color:</label>
<input
type="color"
[(ngModel)]="customColors.tertiary"
(change)="previewCustomTheme()"
>
<input
type="text"
[(ngModel)]="customColors.tertiary"
(input)="previewCustomTheme()"
placeholder="#7D5260"
>
</div>
</div>
<div class="custom-actions">
<button class="preview-btn" (click)="previewCustomTheme()">
Preview Theme
</button>
<button class="apply-btn" (click)="applyCustomTheme()">
Apply Theme
</button>
</div>
<!-- Color Preview -->
<div *ngIf="colorPreview" class="color-preview">
<h4>Generated Palette Preview</h4>
<div class="preview-swatches">
<div
*ngFor="let color of colorPreview | keyvalue"
class="preview-swatch"
[style.background]="color.value"
[style.color]="getContrastColor(color.value)"
>
{{ color.key }}
</div>
</div>
</div>
</div>
<!-- Demo Components -->
<div class="demo-components">
<h3>Theme Demo Components</h3>
<div class="component-showcase">
<!-- Buttons -->
<div class="showcase-section">
<h4>Buttons</h4>
<div class="button-group">
<button class="btn-primary">Primary Button</button>
<button class="btn-secondary">Secondary Button</button>
<button class="btn-tertiary">Tertiary Button</button>
</div>
</div>
<!-- Cards -->
<div class="showcase-section">
<h4>Cards</h4>
<div class="card-group">
<div class="demo-card primary">
<h5>Primary Card</h5>
<p>This card uses primary colors</p>
</div>
<div class="demo-card secondary">
<h5>Secondary Card</h5>
<p>This card uses secondary colors</p>
</div>
</div>
</div>
<!-- Surfaces -->
<div class="showcase-section">
<h4>Surface Variations</h4>
<div class="surface-group">
<div class="demo-surface surface-low">Surface Low</div>
<div class="demo-surface surface-container">Surface Container</div>
<div class="demo-surface surface-high">Surface High</div>
</div>
</div>
</div>
</div>
</div>
`,
styleUrls: ['./hcl-studio-demo.component.scss']
})
export class HclStudioDemoComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
// State
availableThemes: ThemePreview[] = [];
currentTheme: ThemePreview | null = null;
isDarkMode = false;
// Custom theme inputs
customColors: BrandColors = {
primary: '#6750A4',
secondary: '#625B71',
tertiary: '#7D5260'
};
colorPreview: { [key: string]: string } | null = null;
constructor(private hclStudio: HCLStudioService) {}
ngOnInit(): void {
// Initialize HCL Studio with default themes
this.hclStudio.initialize({
themes: DEFAULT_THEMES,
defaultTheme: 'material-purple',
autoMode: false
});
// Subscribe to theme changes
this.hclStudio.themeState$
.pipe(takeUntil(this.destroy$))
.subscribe(state => {
this.availableThemes = state.availableThemes;
if (state.currentTheme) {
this.currentTheme = {
id: state.currentTheme.config.id,
name: state.currentTheme.config.name,
description: state.currentTheme.config.description,
primary: state.currentTheme.config.colors.primary,
secondary: state.currentTheme.config.colors.secondary,
tertiary: state.currentTheme.config.colors.tertiary
};
}
});
// Subscribe to mode changes
this.hclStudio.currentMode$
.pipe(takeUntil(this.destroy$))
.subscribe(mode => {
this.isDarkMode = mode === 'dark';
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
switchTheme(themeId: string): void {
this.hclStudio.switchTheme(themeId);
}
toggleMode(): void {
this.hclStudio.toggleMode();
}
previewCustomTheme(): void {
try {
this.colorPreview = this.hclStudio.generateColorPreview(this.customColors);
} catch (error) {
console.warn('Failed to generate color preview:', error);
this.colorPreview = null;
}
}
applyCustomTheme(): void {
const customTheme = this.hclStudio.createCustomTheme(
'custom-theme',
'My Custom Theme',
this.customColors,
'User-created custom theme'
);
this.hclStudio.switchTheme('custom-theme');
}
getContrastColor(backgroundColor: string): string {
try {
const ratio1 = HCLConverter.getContrastRatio(backgroundColor, '#000000');
const ratio2 = HCLConverter.getContrastRatio(backgroundColor, '#FFFFFF');
return ratio1 > ratio2 ? '#000000' : '#FFFFFF';
} catch {
return '#000000';
}
}
}

View File

@@ -0,0 +1,494 @@
@use '../../../../../ui-design-system/src/styles/semantic' as *;
.demo-container {
padding: $semantic-spacing-component-lg;
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-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-component-md;
}
}
.demo-showcase {
border: $semantic-border-width-1 solid $semantic-color-border-primary;
border-radius: $semantic-border-radius-md;
background: $semantic-color-surface-secondary;
padding: $semantic-spacing-component-md;
height: 400px;
position: relative;
}
.demo-scroll-container {
width: 100%;
height: 100%;
border-radius: $semantic-border-radius-sm;
background: $semantic-color-surface-primary;
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
}
// Basic demo items
.demo-item {
padding: $semantic-spacing-component-md;
margin-bottom: $semantic-spacing-component-sm;
background: $semantic-color-surface-elevated;
border-radius: $semantic-border-radius-sm;
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-content-line-normal 0;
}
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: 0 0 $semantic-spacing-component-xs 0;
}
small {
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-tertiary;
}
}
// Template demo items
.demo-template-item {
padding: $semantic-spacing-component-lg;
background: $semantic-color-surface-elevated;
border-radius: $semantic-border-radius-md;
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
&:hover {
box-shadow: $semantic-shadow-elevation-2;
transform: translateY(-2px);
}
&--last {
border-color: $semantic-color-primary;
box-shadow: $semantic-shadow-elevation-1;
}
&__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: $semantic-spacing-component-sm;
}
&__id {
font-family: map-get($semantic-typography-body-small, font-family);
font-size: map-get($semantic-typography-body-small, font-size);
font-weight: $semantic-typography-font-weight-semibold;
line-height: map-get($semantic-typography-body-small, line-height);
color: $semantic-color-primary;
}
&__category {
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);
background: $semantic-color-surface-container;
color: $semantic-color-text-secondary;
padding: $semantic-spacing-1 $semantic-spacing-2;
border-radius: $semantic-border-radius-sm;
}
&__title {
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-xs 0;
}
&__description {
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: 0 0 $semantic-spacing-component-sm 0;
}
&__footer {
border-top: $semantic-border-width-1 solid $semantic-color-border-subtle;
padding-top: $semantic-spacing-component-xs;
small {
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-tertiary;
}
}
}
// Bidirectional demo items
.demo-bidirectional-item {
padding: $semantic-spacing-component-sm;
&__content {
background: $semantic-color-surface-elevated;
padding: $semantic-spacing-component-md;
border-radius: $semantic-border-radius-lg;
border-left: 4px solid $semantic-color-primary;
strong {
font-family: map-get($semantic-typography-body-medium, font-family);
font-size: map-get($semantic-typography-body-medium, font-size);
font-weight: $semantic-typography-font-weight-semibold;
line-height: map-get($semantic-typography-body-medium, line-height);
color: $semantic-color-text-primary;
display: block;
margin-bottom: $semantic-spacing-content-line-tight;
}
p {
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;
margin: 0 0 $semantic-spacing-content-line-tight 0;
}
small {
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-tertiary;
}
}
}
// Custom templates
.demo-custom-item {
display: flex;
gap: $semantic-spacing-component-md;
padding: $semantic-spacing-component-md;
background: $semantic-color-surface-elevated;
border-radius: $semantic-border-radius-md;
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
&__avatar {
flex-shrink: 0;
width: $semantic-sizing-touch-target;
height: $semantic-sizing-touch-target;
background: $semantic-color-primary;
color: $semantic-color-on-primary;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-family: map-get($semantic-typography-body-medium, font-family);
font-size: map-get($semantic-typography-body-medium, font-size);
font-weight: $semantic-typography-font-weight-bold;
line-height: map-get($semantic-typography-body-medium, line-height);
}
&__content {
flex: 1;
min-width: 0;
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-content-line-tight 0;
}
p {
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;
margin: 0 0 $semantic-spacing-content-line-tight 0;
}
}
&__time {
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-tertiary;
}
}
.demo-custom-loading {
display: flex;
align-items: center;
gap: $semantic-spacing-component-md;
padding: $semantic-spacing-component-lg;
background: linear-gradient(45deg, $semantic-color-primary, $semantic-color-secondary);
color: $semantic-color-on-primary;
border-radius: $semantic-border-radius-lg;
&__spinner {
width: $semantic-sizing-icon-button;
height: $semantic-sizing-icon-button;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin $semantic-motion-duration-slow linear infinite;
}
span {
font-family: map-get($semantic-typography-body-medium, font-family);
font-size: map-get($semantic-typography-body-medium, font-size);
font-weight: $semantic-typography-font-weight-medium;
line-height: map-get($semantic-typography-body-medium, line-height);
}
}
.demo-custom-error {
text-align: center;
padding: $semantic-spacing-component-xl;
background: rgba($semantic-color-danger, 0.1);
border: $semantic-border-width-1 solid $semantic-color-danger;
border-radius: $semantic-border-radius-lg;
&__icon {
font-size: $semantic-typography-font-size-2xl;
margin-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-danger;
margin: 0 0 $semantic-spacing-component-xs 0;
}
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: 0;
}
}
.demo-custom-end {
text-align: center;
padding: $semantic-spacing-component-xl;
background: linear-gradient(135deg, $semantic-color-success, $semantic-color-primary);
color: $semantic-color-on-success;
border-radius: $semantic-border-radius-lg;
&__icon {
font-size: $semantic-typography-font-size-3xl;
margin-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);
margin: 0 0 $semantic-spacing-component-xs 0;
}
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);
margin: 0;
opacity: 0.9;
}
}
// Configuration demo
.demo-config-item {
display: flex;
align-items: center;
gap: $semantic-spacing-component-md;
padding: $semantic-spacing-component-md;
background: $semantic-color-surface-elevated;
border-radius: $semantic-border-radius-sm;
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
&__index {
flex-shrink: 0;
width: $semantic-sizing-touch-minimum;
height: $semantic-sizing-touch-minimum;
background: $semantic-color-surface-container;
color: $semantic-color-text-secondary;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-family: map-get($semantic-typography-body-small, font-family);
font-size: map-get($semantic-typography-body-small, font-size);
font-weight: $semantic-typography-font-weight-semibold;
line-height: map-get($semantic-typography-body-small, line-height);
}
&__content {
flex: 1;
h4 {
font-family: map-get($semantic-typography-body-medium, font-family);
font-size: map-get($semantic-typography-body-medium, font-size);
font-weight: $semantic-typography-font-weight-semibold;
line-height: map-get($semantic-typography-body-medium, line-height);
color: $semantic-color-text-primary;
margin: 0 0 $semantic-spacing-content-line-tight 0;
}
p {
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;
margin: 0;
}
}
}
// Controls
.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-radius: $semantic-border-radius-md;
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
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-radius-sm;
cursor: pointer;
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);
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
&:hover {
box-shadow: $semantic-shadow-elevation-2;
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
box-shadow: $semantic-shadow-elevation-1;
}
&:focus-visible {
outline: 2px solid $semantic-color-focus;
outline-offset: 2px;
}
}
}
.demo-control {
display: flex;
flex-direction: column;
gap: $semantic-spacing-component-xs;
label {
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;
display: flex;
flex-direction: column;
gap: $semantic-spacing-component-xs;
}
input[type="checkbox"] {
width: auto;
margin-right: $semantic-spacing-component-xs;
}
input[type="range"] {
width: 150px;
}
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
// Reduced motion support
@media (prefers-reduced-motion: reduce) {
.demo-template-item {
transition: none;
&:hover {
transform: none;
}
}
.demo-custom-loading__spinner {
animation: none;
}
button {
transition: none;
&:hover {
transform: none;
}
&:active {
transform: none;
}
}
}

View File

@@ -0,0 +1,501 @@
import { Component, TemplateRef, ViewChild, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { InfiniteScrollContainerComponent, InfiniteScrollEvent, InfiniteScrollConfig } from 'ui-essentials';
interface DemoItem {
id: number;
title: string;
description: string;
timestamp: Date;
category: string;
}
@Component({
selector: 'ui-infinite-scroll-container-demo',
standalone: true,
imports: [CommonModule, InfiniteScrollContainerComponent],
template: `
<div class="demo-container">
<h2>Infinite Scroll Container Demo</h2>
<!-- Basic Example -->
<section class="demo-section">
<h3>Basic Infinite Scroll (Content Projection)</h3>
<div class="demo-showcase">
<ui-infinite-scroll-container
class="demo-scroll-container"
[hasMore]="basicHasMore()"
[loading]="basicLoading()"
(loadMore)="loadMoreBasic($event)">
@for (item of basicItems(); track item.id) {
<div class="demo-item">
<h4>{{ item.title }}</h4>
<p>{{ item.description }}</p>
<small>{{ item.timestamp | date:'short' }} - {{ item.category }}</small>
</div>
}
</ui-infinite-scroll-container>
</div>
</section>
<!-- Template-Based Example -->
<section class="demo-section">
<h3>Template-Based Rendering</h3>
<div class="demo-showcase">
<ui-infinite-scroll-container
class="demo-scroll-container"
[items]="templateItems()"
[itemTemplate]="itemTemplate"
[trackByFn]="trackByFn"
[hasMore]="templateHasMore()"
[loading]="templateLoading()"
[config]="templateConfig"
(loadMore)="loadMoreTemplate($event)">
</ui-infinite-scroll-container>
<ng-template #itemTemplate let-item="item" let-index="index" let-isLast="isLast">
<div class="demo-template-item" [class.demo-template-item--last]="isLast">
<div class="demo-template-item__header">
<span class="demo-template-item__id">#{{ item.id }}</span>
<span class="demo-template-item__category">{{ item.category }}</span>
</div>
<h4 class="demo-template-item__title">{{ item.title }}</h4>
<p class="demo-template-item__description">{{ item.description }}</p>
<div class="demo-template-item__footer">
<small>Index: {{ index }} | {{ item.timestamp | date:'short' }}</small>
</div>
</div>
</ng-template>
</div>
</section>
<!-- Bidirectional Scrolling -->
<section class="demo-section">
<h3>Bidirectional Infinite Scroll</h3>
<div class="demo-showcase">
<ui-infinite-scroll-container
class="demo-scroll-container"
direction="both"
[items]="bidirectionalItems()"
[itemTemplate]="bidirectionalTemplate"
[hasMore]="bidirectionalHasMore()"
[loadingUp]="bidirectionalLoadingUp()"
[loadingDown]="bidirectionalLoadingDown()"
[config]="bidirectionalConfig"
(loadMore)="loadMoreBidirectional($event)">
</ui-infinite-scroll-container>
<ng-template #bidirectionalTemplate let-item="item" let-index="index">
<div class="demo-bidirectional-item">
<div class="demo-bidirectional-item__content">
<strong>Message {{ item.id }}</strong>
<p>{{ item.description }}</p>
<small>{{ item.timestamp | date:'short' }}</small>
</div>
</div>
</ng-template>
</div>
</section>
<!-- Custom Templates -->
<section class="demo-section">
<h3>Custom Loading & Error Templates</h3>
<div class="demo-showcase">
<ui-infinite-scroll-container
class="demo-scroll-container"
[items]="customItems()"
[itemTemplate]="customItemTemplate"
[loadingTemplate]="customLoadingTemplate"
[errorTemplate]="customErrorTemplate"
[endTemplate]="customEndTemplate"
[hasMore]="customHasMore()"
[loading]="customLoading()"
[error]="customError()"
[config]="customConfig"
(loadMore)="loadMoreCustom($event)"
(retryRequested)="retryCustom()">
</ui-infinite-scroll-container>
<ng-template #customItemTemplate let-item="item">
<div class="demo-custom-item">
<div class="demo-custom-item__avatar">
{{ item.title.charAt(0) }}
</div>
<div class="demo-custom-item__content">
<h4>{{ item.title }}</h4>
<p>{{ item.description }}</p>
<span class="demo-custom-item__time">{{ getTimeAgo(item.timestamp) }}</span>
</div>
</div>
</ng-template>
<ng-template #customLoadingTemplate>
<div class="demo-custom-loading">
<div class="demo-custom-loading__spinner"></div>
<span>Loading amazing content...</span>
</div>
</ng-template>
<ng-template #customErrorTemplate let-error="error">
<div class="demo-custom-error">
<div class="demo-custom-error__icon">⚠️</div>
<h4>Oops! Something went wrong</h4>
<p>{{ error }}</p>
</div>
</ng-template>
<ng-template #customEndTemplate>
<div class="demo-custom-end">
<div class="demo-custom-end__icon">🎉</div>
<h4>That's all folks!</h4>
<p>You've reached the end of our amazing content.</p>
</div>
</ng-template>
</div>
</section>
<!-- Configuration Examples -->
<section class="demo-section">
<h3>Configuration Options</h3>
<div class="demo-controls">
<div class="demo-control">
<label>
<input type="checkbox" [checked]="disableScroll()" (change)="toggleDisabled($event)">
Disable Infinite Scroll
</label>
</div>
<div class="demo-control">
<label>
Threshold: {{ threshold() }}px
<input
type="range"
min="50"
max="500"
[value]="threshold()"
(input)="updateThreshold($event)">
</label>
</div>
<div class="demo-control">
<label>
Debounce Time: {{ debounceTime() }}ms
<input
type="range"
min="50"
max="1000"
[value]="debounceTime()"
(input)="updateDebounceTime($event)">
</label>
</div>
</div>
<div class="demo-showcase">
<ui-infinite-scroll-container
class="demo-scroll-container"
[items]="configItems()"
[itemTemplate]="configItemTemplate"
[hasMore]="configHasMore()"
[loading]="configLoading()"
[config]="currentConfig()"
(loadMore)="loadMoreConfig($event)">
</ui-infinite-scroll-container>
<ng-template #configItemTemplate let-item="item" let-index="index">
<div class="demo-config-item">
<span class="demo-config-item__index">{{ index + 1 }}</span>
<div class="demo-config-item__content">
<h4>{{ item.title }}</h4>
<p>{{ item.description }}</p>
</div>
</div>
</ng-template>
</div>
</section>
<!-- API Methods Demo -->
<section class="demo-section">
<h3>API Methods</h3>
<div class="demo-controls">
<button type="button" (click)="scrollToTop()">Scroll to Top</button>
<button type="button" (click)="scrollToBottom()">Scroll to Bottom</button>
<button type="button" (click)="scrollToIndex(50)">Scroll to Item 50</button>
<button type="button" (click)="resetApiScroll()">Reset Scroll</button>
</div>
</section>
</div>
`,
styleUrl: './infinite-scroll-container-demo.component.scss'
})
export class InfiniteScrollContainerDemoComponent {
@ViewChild('itemTemplate') itemTemplate!: TemplateRef<any>;
// Basic example
basicItems = signal<DemoItem[]>([]);
basicLoading = signal(false);
basicHasMore = signal(true);
private basicPage = 1;
// Template example
templateItems = signal<DemoItem[]>([]);
templateLoading = signal(false);
templateHasMore = signal(true);
private templatePage = 1;
templateConfig: InfiniteScrollConfig = {
threshold: 100,
debounceTime: 150,
itemHeight: 120
};
// Bidirectional example
bidirectionalItems = signal<DemoItem[]>([]);
bidirectionalLoadingUp = signal(false);
bidirectionalLoadingDown = signal(false);
bidirectionalHasMore = signal(true);
private bidirectionalTopPage = 0;
private bidirectionalBottomPage = 1;
bidirectionalConfig: InfiniteScrollConfig = {
threshold: 50,
debounceTime: 200
};
// Custom templates example
customItems = signal<DemoItem[]>([]);
customLoading = signal(false);
customHasMore = signal(true);
customError = signal<string | undefined>(undefined);
private customPage = 1;
private customFailCount = 0;
customConfig: InfiniteScrollConfig = {
threshold: 150,
debounceTime: 100
};
// Configuration example
configItems = signal<DemoItem[]>([]);
configLoading = signal(false);
configHasMore = signal(true);
private configPage = 1;
// Configuration controls
threshold = signal(200);
debounceTime = signal(300);
disableScroll = signal(false);
currentConfig = signal<InfiniteScrollConfig>({
threshold: 200,
debounceTime: 300,
disabled: false
});
constructor() {
// Initialize with some data
this.loadInitialData();
}
// Track by function for template rendering
trackByFn = (index: number, item: DemoItem) => item.id;
// Basic example methods
loadMoreBasic(event: InfiniteScrollEvent): void {
if (this.basicLoading()) return;
this.basicLoading.set(true);
// Simulate API call
setTimeout(() => {
const newItems = this.generateItems(this.basicPage * 20, 20, 'basic');
this.basicItems.update(items => [...items, ...newItems]);
this.basicPage++;
this.basicLoading.set(false);
// Stop loading after 5 pages
if (this.basicPage > 5) {
this.basicHasMore.set(false);
}
}, 1000);
}
// Template example methods
loadMoreTemplate(event: InfiniteScrollEvent): void {
if (this.templateLoading()) return;
this.templateLoading.set(true);
setTimeout(() => {
const newItems = this.generateItems(this.templatePage * 15, 15, 'template');
this.templateItems.update(items => [...items, ...newItems]);
this.templatePage++;
this.templateLoading.set(false);
if (this.templatePage > 6) {
this.templateHasMore.set(false);
}
}, 800);
}
// Bidirectional example methods
loadMoreBidirectional(event: InfiniteScrollEvent): void {
if (event.direction === 'down') {
this.loadMoreBidirectionalDown();
} else {
this.loadMoreBidirectionalUp();
}
}
private loadMoreBidirectionalDown(): void {
if (this.bidirectionalLoadingDown()) return;
this.bidirectionalLoadingDown.set(true);
setTimeout(() => {
const newItems = this.generateItems(this.bidirectionalBottomPage * 10, 10, 'chat');
this.bidirectionalItems.update(items => [...items, ...newItems]);
this.bidirectionalBottomPage++;
this.bidirectionalLoadingDown.set(false);
}, 600);
}
private loadMoreBidirectionalUp(): void {
if (this.bidirectionalLoadingUp()) return;
this.bidirectionalLoadingUp.set(true);
setTimeout(() => {
const startId = this.bidirectionalTopPage * 10;
const newItems = this.generateItems(startId, 10, 'history');
this.bidirectionalItems.update(items => [...newItems, ...items]);
this.bidirectionalTopPage--;
this.bidirectionalLoadingUp.set(false);
}, 800);
}
// Custom templates example methods
loadMoreCustom(event: InfiniteScrollEvent): void {
if (this.customLoading()) return;
this.customLoading.set(true);
this.customError.set(undefined);
// Simulate occasional failures
const shouldFail = Math.random() < 0.3 && this.customFailCount < 2;
setTimeout(() => {
if (shouldFail) {
this.customError.set('Network error: Failed to load more items');
this.customLoading.set(false);
this.customFailCount++;
} else {
const newItems = this.generateItems(this.customPage * 12, 12, 'custom');
this.customItems.update(items => [...items, ...newItems]);
this.customPage++;
this.customLoading.set(false);
if (this.customPage > 4) {
this.customHasMore.set(false);
}
}
}, 1200);
}
retryCustom(): void {
this.loadMoreCustom({ direction: 'down', currentIndex: 0, scrollPosition: 0 });
}
// Configuration example methods
loadMoreConfig(event: InfiniteScrollEvent): void {
if (this.configLoading()) return;
this.configLoading.set(true);
setTimeout(() => {
const newItems = this.generateItems(this.configPage * 10, 10, 'config');
this.configItems.update(items => [...items, ...newItems]);
this.configPage++;
this.configLoading.set(false);
if (this.configPage > 10) {
this.configHasMore.set(false);
}
}, 500);
}
// Configuration controls
toggleDisabled(event: Event): void {
const disabled = (event.target as HTMLInputElement).checked;
this.disableScroll.set(disabled);
this.updateCurrentConfig();
}
updateThreshold(event: Event): void {
const value = parseInt((event.target as HTMLInputElement).value);
this.threshold.set(value);
this.updateCurrentConfig();
}
updateDebounceTime(event: Event): void {
const value = parseInt((event.target as HTMLInputElement).value);
this.debounceTime.set(value);
this.updateCurrentConfig();
}
private updateCurrentConfig(): void {
this.currentConfig.set({
threshold: this.threshold(),
debounceTime: this.debounceTime(),
disabled: this.disableScroll()
});
}
// API methods - These would be called on component references
scrollToTop(): void {
console.log('Scroll to top - would call component method');
}
scrollToBottom(): void {
console.log('Scroll to bottom - would call component method');
}
scrollToIndex(index: number): void {
console.log(`Scroll to index ${index} - would call component method`);
}
resetApiScroll(): void {
console.log('Reset scroll - would call component method');
}
// Utility methods
getTimeAgo(date: Date): string {
const now = new Date();
const diff = now.getTime() - date.getTime();
const minutes = Math.floor(diff / (1000 * 60));
if (minutes < 1) return 'Just now';
if (minutes < 60) return `${minutes}m ago`;
if (minutes < 1440) return `${Math.floor(minutes / 60)}h ago`;
return `${Math.floor(minutes / 1440)}d ago`;
}
private loadInitialData(): void {
// Load initial data for all examples
this.basicItems.set(this.generateItems(1, 20, 'basic'));
this.templateItems.set(this.generateItems(1, 15, 'template'));
this.bidirectionalItems.set(this.generateItems(1, 20, 'chat'));
this.customItems.set(this.generateItems(1, 12, 'custom'));
this.configItems.set(this.generateItems(1, 10, 'config'));
}
private generateItems(startId: number, count: number, type: string): DemoItem[] {
const categories = ['Technology', 'Science', 'Arts', 'Sports', 'Travel', 'Food', 'Music', 'Literature'];
return Array.from({ length: count }, (_, i) => ({
id: startId + i,
title: `${type.charAt(0).toUpperCase() + type.slice(1)} Item ${startId + i}`,
description: `This is a sample description for ${type} item ${startId + i}. It contains some interesting content that demonstrates the infinite scroll functionality.`,
timestamp: new Date(Date.now() - Math.random() * 86400000 * 30), // Random date within last 30 days
category: categories[Math.floor(Math.random() * categories.length)]
}));
}
}

View File

@@ -0,0 +1,128 @@
@use "../../../../../ui-design-system/src/styles/semantic" as *;
.demo-container {
padding: $semantic-spacing-component-lg;
max-width: 100%;
h2 {
margin: 0 0 $semantic-spacing-component-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;
}
h3 {
margin: $semantic-spacing-component-xl 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-section {
margin-bottom: $semantic-spacing-component-xl;
&:last-child {
margin-bottom: 0;
}
}
.demo-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: $semantic-spacing-grid-gap-lg;
@media (max-width: 768px) {
grid-template-columns: 1fr;
gap: $semantic-spacing-grid-gap-md;
}
}
.demo-item {
background: $semantic-color-surface-secondary;
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
border-radius: $semantic-border-radius-md;
padding: $semantic-spacing-component-md;
box-shadow: $semantic-shadow-elevation-1;
}
.event-log {
margin-top: $semantic-spacing-component-lg;
padding: $semantic-spacing-component-md;
background: $semantic-color-surface-secondary;
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
border-radius: $semantic-border-radius-md;
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;
}
}
.events {
max-height: 200px;
overflow-y: auto;
// Scroll styling
scrollbar-width: thin;
scrollbar-color: $semantic-color-border-primary $semantic-color-surface-container;
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: $semantic-color-surface-container;
border-radius: $semantic-border-radius-sm;
}
&::-webkit-scrollbar-thumb {
background: $semantic-color-border-primary;
border-radius: $semantic-border-radius-sm;
}
}
.event-item {
display: flex;
flex-direction: column;
gap: $semantic-spacing-component-xs;
padding: $semantic-spacing-component-sm;
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
&:last-child {
border-bottom: none;
}
strong {
font-family: map-get($semantic-typography-body-medium, font-family);
font-size: map-get($semantic-typography-body-medium, font-size);
font-weight: $semantic-typography-font-weight-semibold;
line-height: map-get($semantic-typography-body-medium, line-height);
color: $semantic-color-text-primary;
}
small {
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-tertiary;
}
}

View File

@@ -0,0 +1,354 @@
import { Component, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { KanbanBoardComponent, KanbanColumn, KanbanItem, KanbanDragEvent } from 'ui-essentials';
@Component({
selector: 'ui-kanban-board-demo',
standalone: true,
imports: [CommonModule, KanbanBoardComponent],
template: `
<div class="demo-container">
<h2>Kanban Board Layout Demo</h2>
<!-- Size Variants -->
<section class="demo-section">
<h3>Sizes</h3>
<div class="demo-row">
<div class="demo-item">
<h4>Small</h4>
<div style="height: 400px; width: 100%;">
<ui-kanban-board
[columns]="basicColumns()"
size="sm"
title="Small Kanban Board"
(itemMoved)="onItemMoved($event)"
(itemClicked)="onItemClicked($event)">
</ui-kanban-board>
</div>
</div>
<div class="demo-item">
<h4>Medium (Default)</h4>
<div style="height: 400px; width: 100%;">
<ui-kanban-board
[columns]="basicColumns()"
size="md"
title="Medium Kanban Board"
(itemMoved)="onItemMoved($event)"
(itemClicked)="onItemClicked($event)">
</ui-kanban-board>
</div>
</div>
<div class="demo-item">
<h4>Large</h4>
<div style="height: 400px; width: 100%;">
<ui-kanban-board
[columns]="basicColumns()"
size="lg"
title="Large Kanban Board"
(itemMoved)="onItemMoved($event)"
(itemClicked)="onItemClicked($event)">
</ui-kanban-board>
</div>
</div>
</div>
</section>
<!-- Full Featured Example -->
<section class="demo-section">
<h3>Full Featured Kanban Board</h3>
<div style="height: 600px; width: 100%;">
<ui-kanban-board
[columns]="fullColumns()"
title="Project Management Board"
subtitle="Drag and drop items between columns to update their status"
size="md"
(itemMoved)="onItemMoved($event)"
(itemClicked)="onItemClicked($event)">
</ui-kanban-board>
</div>
</section>
<!-- States -->
<section class="demo-section">
<h3>States</h3>
<div class="demo-row">
<div class="demo-item">
<h4>Disabled</h4>
<div style="height: 300px; width: 100%;">
<ui-kanban-board
[columns]="basicColumns()"
[disabled]="true"
title="Disabled Kanban Board">
</ui-kanban-board>
</div>
</div>
<div class="demo-item">
<h4>Column with Limits</h4>
<div style="height: 300px; width: 100%;">
<ui-kanban-board
[columns]="limitedColumns()"
title="Limited Capacity Board"
(itemMoved)="onItemMoved($event)">
</ui-kanban-board>
</div>
</div>
</div>
</section>
<!-- Interactive Example -->
<section class="demo-section">
<h3>Interactive Events</h3>
<div style="height: 500px; width: 100%;">
<ui-kanban-board
[columns]="interactiveColumns()"
title="Interactive Kanban Board"
subtitle="Click items or drag them around to see event logging below"
(itemMoved)="onItemMoved($event)"
(itemClicked)="onItemClicked($event)">
</ui-kanban-board>
</div>
<div class="event-log">
<h4>Event Log:</h4>
<div class="events">
@for (event of eventLog(); track event.id) {
<div class="event-item">
<strong>{{ event.type }}:</strong> {{ event.message }}
<small>{{ event.timestamp | date:'medium' }}</small>
</div>
}
</div>
</div>
</section>
</div>
`,
styleUrl: './kanban-board-demo.component.scss'
})
export class KanbanBoardDemoComponent {
eventLog = signal<Array<{ id: string; type: string; message: string; timestamp: Date }>>([]);
basicColumns(): KanbanColumn[] {
return [
{
id: 'todo',
title: 'To Do',
items: [
{ id: '1', title: 'Design homepage', description: 'Create wireframes and mockups' },
{ id: '2', title: 'Setup database', priority: 'high' }
]
},
{
id: 'in-progress',
title: 'In Progress',
items: [
{ id: '3', title: 'Build API endpoints', priority: 'medium', assignee: 'John' }
]
},
{
id: 'done',
title: 'Done',
items: [
{ id: '4', title: 'Project setup', description: 'Initialize repository and dependencies' }
]
}
];
}
fullColumns(): KanbanColumn[] {
return [
{
id: 'backlog',
title: 'Backlog',
items: [
{
id: 'item-1',
title: 'User Authentication System',
description: 'Implement login, registration, and password reset functionality',
priority: 'high',
tags: ['backend', 'security', 'user-management'],
assignee: 'Sarah Connor',
dueDate: new Date(2024, 11, 15)
},
{
id: 'item-2',
title: 'Dashboard Analytics',
description: 'Create comprehensive analytics dashboard with charts and metrics',
priority: 'medium',
tags: ['frontend', 'analytics', 'charts'],
assignee: 'John Doe',
dueDate: new Date(2024, 11, 20)
},
{
id: 'item-3',
title: 'Mobile Responsiveness',
description: 'Ensure all pages work correctly on mobile devices',
priority: 'low',
tags: ['frontend', 'responsive', 'css'],
assignee: 'Jane Smith'
}
]
},
{
id: 'todo',
title: 'To Do',
items: [
{
id: 'item-4',
title: 'API Rate Limiting',
description: 'Implement rate limiting for API endpoints to prevent abuse',
priority: 'high',
tags: ['backend', 'security', 'performance'],
assignee: 'Bob Wilson',
dueDate: new Date(2024, 11, 10)
},
{
id: 'item-5',
title: 'Email Notifications',
description: 'Set up email notification system for important events',
priority: 'medium',
tags: ['backend', 'email', 'notifications'],
assignee: 'Alice Brown'
}
]
},
{
id: 'in-progress',
title: 'In Progress',
items: [
{
id: 'item-6',
title: 'Database Optimization',
description: 'Optimize database queries and add proper indexing',
priority: 'high',
tags: ['database', 'performance', 'optimization'],
assignee: 'Mike Johnson',
dueDate: new Date(2024, 11, 8)
}
]
},
{
id: 'review',
title: 'In Review',
items: [
{
id: 'item-7',
title: 'Unit Test Coverage',
description: 'Increase unit test coverage to 90%',
priority: 'medium',
tags: ['testing', 'quality', 'coverage'],
assignee: 'Lisa Davis',
dueDate: new Date(2024, 11, 12)
}
]
},
{
id: 'done',
title: 'Done',
items: [
{
id: 'item-8',
title: 'Project Setup',
description: 'Initialize project structure and dependencies',
priority: 'high',
tags: ['setup', 'infrastructure'],
assignee: 'Team Lead'
},
{
id: 'item-9',
title: 'Design System',
description: 'Create component library and design tokens',
priority: 'medium',
tags: ['design', 'components', 'ui'],
assignee: 'Design Team'
}
]
}
];
}
limitedColumns(): KanbanColumn[] {
return [
{
id: 'todo',
title: 'To Do',
items: [
{ id: '1', title: 'Task 1' },
{ id: '2', title: 'Task 2' }
]
},
{
id: 'limited',
title: 'Limited (Max 2)',
maxItems: 2,
items: [
{ id: '3', title: 'Limited Task 1' },
{ id: '4', title: 'Limited Task 2' }
]
},
{
id: 'done',
title: 'Done',
items: []
}
];
}
interactiveColumns(): KanbanColumn[] {
return [
{
id: 'new',
title: 'New',
items: [
{ id: 'i1', title: 'Interactive Item 1', description: 'Click me or drag me!' },
{ id: 'i2', title: 'Interactive Item 2', priority: 'high', assignee: 'You' }
]
},
{
id: 'active',
title: 'Active',
items: [
{ id: 'i3', title: 'Interactive Item 3', priority: 'medium' }
]
},
{
id: 'complete',
title: 'Complete',
items: []
}
];
}
onItemMoved(event: KanbanDragEvent): void {
this.logEvent('Item Moved',
`"${event.item.title}" moved from "${event.fromColumn}" to "${event.toColumn}"`
);
// In a real application, you would update your data here
console.log('Item moved:', event);
}
onItemClicked(event: { item: KanbanItem; columnId: string }): void {
this.logEvent('Item Clicked',
`"${event.item.title}" clicked in column "${event.columnId}"`
);
console.log('Item clicked:', event);
}
private logEvent(type: string, message: string): void {
const currentLog = this.eventLog();
const newEvent = {
id: Date.now().toString(),
type,
message,
timestamp: new Date()
};
// Keep only the last 10 events
const updatedLog = [newEvent, ...currentLog].slice(0, 10);
this.eventLog.set(updatedLog);
}
}

View File

@@ -0,0 +1,30 @@
.demo-container {
.demo-section {
margin-bottom: 2rem;
h3 {
margin-bottom: 1rem;
color: #333;
}
.demo-row {
display: grid;
gap: 1.5rem;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
.demo-item {
&.full-width {
grid-column: 1 / -1;
}
h4 {
margin-bottom: 0.5rem;
color: #555;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
}
}
}
}

View File

@@ -0,0 +1,225 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MasonryComponent } from 'ui-essentials';
@Component({
selector: 'ui-masonry-demo',
standalone: true,
imports: [CommonModule, MasonryComponent],
template: `
<div class="demo-container">
<h2>Masonry Layout Demo</h2>
<p>Pinterest-style dynamic grid layout that arranges items in columns with optimal vertical spacing.</p>
<!-- Column Count Variants -->
<section class="demo-section">
<h3>Column Count</h3>
<div class="demo-row">
@for (columns of columnCounts; track columns) {
<div class="demo-item">
<h4>{{ columns }} Columns</h4>
<ui-masonry [columns]="columns" gap="sm" padding="sm">
@for (item of sampleItems.slice(0, 8); track item.id) {
<div class="ui-masonry-item ui-masonry-item--comfortable"
[style.height.px]="item.height">
<h5>{{ item.title }}</h5>
<p>{{ item.content }}</p>
</div>
}
</ui-masonry>
</div>
}
</div>
</section>
<!-- Auto Responsive Variants -->
<section class="demo-section">
<h3>Auto Responsive</h3>
<div class="demo-row">
<div class="demo-item">
<h4>Auto Fit (250px min width)</h4>
<ui-masonry columns="auto-fit" gap="md" padding="md">
@for (item of sampleItems.slice(0, 12); track item.id) {
<div class="ui-masonry-item ui-masonry-item--comfortable"
[style.height.px]="item.height">
<h5>{{ item.title }}</h5>
<p>{{ item.content }}</p>
</div>
}
</ui-masonry>
</div>
<div class="demo-item">
<h4>Auto Fill (200px min width)</h4>
<ui-masonry columns="auto-fill" gap="md" padding="md">
@for (item of sampleItems.slice(0, 12); track item.id) {
<div class="ui-masonry-item ui-masonry-item--comfortable"
[style.height.px]="item.height">
<h5>{{ item.title }}</h5>
<p>{{ item.content }}</p>
</div>
}
</ui-masonry>
</div>
</div>
</section>
<!-- Gap Variants -->
<section class="demo-section">
<h3>Gap Sizes</h3>
<div class="demo-row">
@for (gap of gaps; track gap) {
<div class="demo-item">
<h4>Gap: {{ gap }}</h4>
<ui-masonry [columns]="3" [gap]="gap" padding="sm">
@for (item of sampleItems.slice(0, 6); track item.id) {
<div class="ui-masonry-item ui-masonry-item--comfortable"
[style.height.px]="item.height">
<h5>{{ item.title }}</h5>
<p>{{ item.content }}</p>
</div>
}
</ui-masonry>
</div>
}
</div>
</section>
<!-- Padding Variants -->
<section class="demo-section">
<h3>Padding Options</h3>
<div class="demo-row">
@for (padding of paddings; track padding) {
<div class="demo-item">
<h4>Padding: {{ padding }}</h4>
<ui-masonry [columns]="3" gap="sm" [padding]="padding">
@for (item of sampleItems.slice(0, 6); track item.id) {
<div class="ui-masonry-item ui-masonry-item--comfortable"
[style.height.px]="item.height">
<h5>{{ item.title }}</h5>
<p>{{ item.content }}</p>
</div>
}
</ui-masonry>
</div>
}
</div>
</section>
<!-- Alignment Options -->
<section class="demo-section">
<h3>Alignment</h3>
<div class="demo-row">
@for (alignment of alignments; track alignment) {
<div class="demo-item">
<h4>Align: {{ alignment }}</h4>
<ui-masonry [columns]="2" gap="md" padding="md" [alignment]="alignment">
@for (item of sampleItems.slice(0, 4); track item.id) {
<div class="ui-masonry-item ui-masonry-item--comfortable"
[style.height.px]="item.height">
<h5>{{ item.title }}</h5>
<p>{{ item.content }}</p>
</div>
}
</ui-masonry>
</div>
}
</div>
</section>
<!-- Item Size Variants -->
<section class="demo-section">
<h3>Item Variants</h3>
<div class="demo-row">
<div class="demo-item">
<h4>Mixed Item Sizes</h4>
<ui-masonry [columns]="3" gap="md" padding="md">
<div class="ui-masonry-item ui-masonry-item--compact" style="height: 120px;">
<h5>Compact Item</h5>
<p>Compact padding for dense layouts</p>
</div>
<div class="ui-masonry-item ui-masonry-item--comfortable" style="height: 200px;">
<h5>Comfortable Item</h5>
<p>Standard comfortable padding for most use cases</p>
</div>
<div class="ui-masonry-item ui-masonry-item--spacious" style="height: 180px;">
<h5>Spacious Item</h5>
<p>Extra padding for premium content</p>
</div>
<div class="ui-masonry-item ui-masonry-item--flush" style="height: 160px;">
<div style="padding: 16px; background: linear-gradient(45deg, #667eea 0%, #764ba2 100%); color: white;">
<h5>Full Bleed Item</h5>
<p>No padding for full-width content</p>
</div>
</div>
@for (item of sampleItems.slice(0, 6); track item.id) {
<div class="ui-masonry-item ui-masonry-item--comfortable"
[style.height.px]="item.height">
<h5>{{ item.title }}</h5>
<p>{{ item.content }}</p>
</div>
}
</ui-masonry>
</div>
</div>
</section>
<!-- Pinterest-style Example -->
<section class="demo-section">
<h3>Pinterest-style Gallery</h3>
<div class="demo-row">
<div class="demo-item full-width">
<ui-masonry columns="auto-fit" gap="lg" padding="lg" minColumnWidth="280px">
@for (item of extendedItems; track item.id) {
<div class="ui-masonry-item ui-masonry-item--comfortable"
[style.height.px]="item.height"
[style.background]="item.background">
<h5 style="color: white; text-shadow: 0 1px 2px rgba(0,0,0,0.5);">{{ item.title }}</h5>
<p style="color: rgba(255,255,255,0.9); text-shadow: 0 1px 2px rgba(0,0,0,0.3);">{{ item.content }}</p>
</div>
}
</ui-masonry>
</div>
</div>
</section>
</div>
`,
styleUrl: './masonry-demo.component.scss'
})
export class MasonryDemoComponent {
columnCounts = [2, 3, 4] as const;
gaps = ['sm', 'md', 'lg'] as const;
paddings = ['none', 'sm', 'md', 'lg'] as const;
alignments = ['start', 'center', 'end'] as const;
sampleItems = [
{ id: 1, title: 'Card 1', content: 'Short content', height: 120 },
{ id: 2, title: 'Card 2', content: 'Medium length content with more details', height: 180 },
{ id: 3, title: 'Card 3', content: 'Brief', height: 100 },
{ id: 4, title: 'Card 4', content: 'Longer content with multiple lines of text to demonstrate variable heights', height: 220 },
{ id: 5, title: 'Card 5', content: 'Standard content', height: 140 },
{ id: 6, title: 'Card 6', content: 'Extended content with even more text to show how masonry handles varying content lengths', height: 260 },
{ id: 7, title: 'Card 7', content: 'Compact', height: 90 },
{ id: 8, title: 'Card 8', content: 'Regular content block', height: 160 },
{ id: 9, title: 'Card 9', content: 'Another piece of content', height: 130 },
{ id: 10, title: 'Card 10', content: 'Different sized content', height: 200 },
{ id: 11, title: 'Card 11', content: 'Small content', height: 110 },
{ id: 12, title: 'Card 12', content: 'Large content with lots of text and information', height: 240 }
];
extendedItems = [
{ id: 1, title: 'Ocean Waves', content: 'Beautiful ocean scenery with crashing waves', height: 180, background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
{ id: 2, title: 'Mountain Vista', content: 'Majestic mountain range at sunset', height: 220, background: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' },
{ id: 3, title: 'Forest Path', content: 'Winding path through dense forest', height: 160, background: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' },
{ id: 4, title: 'Desert Dunes', content: 'Golden sand dunes stretching to horizon', height: 200, background: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)' },
{ id: 5, title: 'City Skyline', content: 'Modern city with glowing lights', height: 140, background: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' },
{ id: 6, title: 'Northern Lights', content: 'Aurora borealis dancing across night sky', height: 250, background: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)' },
{ id: 7, title: 'Tropical Beach', content: 'Palm trees and crystal clear water', height: 180, background: 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)' },
{ id: 8, title: 'Snow Peaks', content: 'Snow-capped mountain peaks', height: 190, background: 'linear-gradient(135deg, #a1c4fd 0%, #c2e9fb 100%)' },
{ id: 9, title: 'Autumn Leaves', content: 'Colorful fall foliage', height: 170, background: 'linear-gradient(135deg, #fad0c4 0%, #fad0c4 1%, #ffd1ff 100%)' },
{ id: 10, title: 'Starry Night', content: 'Countless stars in clear night sky', height: 210, background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
{ id: 11, title: 'Wildflowers', content: 'Field of colorful wildflowers', height: 150, background: 'linear-gradient(135deg, #ff9a9e 0%, #fecfef 50%, #fecfef 100%)' },
{ id: 12, title: 'Waterfall', content: 'Powerful waterfall cascading down rocks', height: 230, background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }
];
}

View File

@@ -0,0 +1,505 @@
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { SelectComponent, SelectOption } from '../../../../../ui-essentials/src/lib/components/forms/select/select.component';
@Component({
selector: 'ui-select-demo',
standalone: true,
imports: [
CommonModule,
FormsModule,
SelectComponent
],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div style="padding: 2rem;">
<h2>Select Component Showcase</h2>
<!-- Basic Select -->
<section style="margin-bottom: 3rem;">
<h3>Basic Select</h3>
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem; max-width: 400px;">
<ui-select
label="Choose a country"
placeholder="Select a country"
[options]="countryOptions"
[(ngModel)]="basicSelect1"
(valueChange)="onSelectionChange('basic-country', $event)"
/>
<ui-select
label="Choose a language"
placeholder="Select a language"
[options]="languageOptions"
[(ngModel)]="basicSelect2"
(valueChange)="onSelectionChange('basic-language', $event)"
/>
<ui-select
label="Choose a framework"
helperText="Select your preferred frontend framework"
[options]="frameworkOptions"
[(ngModel)]="basicSelect3"
(valueChange)="onSelectionChange('basic-framework', $event)"
/>
</div>
</section>
<!-- Size Variants -->
<section style="margin-bottom: 3rem;">
<h3>Size Variants</h3>
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem; max-width: 400px;">
<ui-select
label="Small select"
size="small"
placeholder="Choose an option"
[options]="sizeOptions"
[(ngModel)]="sizeSmall"
(valueChange)="onSelectionChange('size-small', $event)"
/>
<ui-select
label="Medium select (default)"
size="medium"
placeholder="Choose an option"
[options]="sizeOptions"
[(ngModel)]="sizeMedium"
(valueChange)="onSelectionChange('size-medium', $event)"
/>
<ui-select
label="Large select"
size="large"
placeholder="Choose an option"
[options]="sizeOptions"
[(ngModel)]="sizeLarge"
(valueChange)="onSelectionChange('size-large', $event)"
/>
</div>
</section>
<!-- Variant Styles -->
<section style="margin-bottom: 3rem;">
<h3>Style Variants</h3>
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem; max-width: 400px;">
<ui-select
label="Outlined variant (default)"
variant="outlined"
placeholder="Choose an option"
[options]="variantOptions"
[(ngModel)]="variantOutlined"
(valueChange)="onSelectionChange('variant-outlined', $event)"
/>
<ui-select
label="Filled variant"
variant="filled"
placeholder="Choose an option"
[options]="variantOptions"
[(ngModel)]="variantFilled"
(valueChange)="onSelectionChange('variant-filled', $event)"
/>
</div>
</section>
<!-- States and Validation -->
<section style="margin-bottom: 3rem;">
<h3>States and Validation</h3>
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem; max-width: 400px;">
<ui-select
label="Disabled select"
[disabled]="true"
placeholder="This is disabled"
[options]="stateOptions"
[(ngModel)]="stateDisabled"
/>
<ui-select
label="Required field"
[required]="true"
placeholder="This field is required"
[options]="stateOptions"
[(ngModel)]="stateRequired"
(valueChange)="onSelectionChange('state-required', $event)"
/>
<ui-select
label="Field with error"
placeholder="Select an option"
errorText="This field has an error"
[options]="stateOptions"
[(ngModel)]="stateError"
(valueChange)="onSelectionChange('state-error', $event)"
/>
<ui-select
label="Field with helper text"
placeholder="Select an option"
helperText="This is helpful information about this field"
[options]="stateOptions"
[(ngModel)]="stateHelper"
(valueChange)="onSelectionChange('state-helper', $event)"
/>
</div>
</section>
<!-- Full Width -->
<section style="margin-bottom: 3rem;">
<h3>Full Width</h3>
<div style="margin-bottom: 2rem;">
<ui-select
label="Full width select"
placeholder="This select takes full width"
[fullWidth]="true"
[options]="fullWidthOptions"
[(ngModel)]="fullWidthSelect"
(valueChange)="onSelectionChange('full-width', $event)"
/>
</div>
</section>
<!-- Complex Options -->
<section style="margin-bottom: 3rem;">
<h3>Complex Options with Disabled Items</h3>
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem; max-width: 400px;">
<ui-select
label="User roles"
placeholder="Select a role"
helperText="Some roles may be disabled based on permissions"
[options]="roleOptions"
[(ngModel)]="complexSelect1"
(valueChange)="onSelectionChange('complex-roles', $event)"
/>
<ui-select
label="Priority levels"
placeholder="Select priority"
[options]="priorityOptions"
[(ngModel)]="complexSelect2"
(valueChange)="onSelectionChange('complex-priority', $event)"
/>
</div>
</section>
<!-- Interactive Controls -->
<section style="margin-bottom: 3rem;">
<h3>Interactive Controls</h3>
<div style="display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
<button
style="padding: 0.5rem 1rem; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;"
(click)="setDefaultValues()"
>
Set Default Values
</button>
<button
style="padding: 0.5rem 1rem; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer;"
(click)="clearAllSelections()"
>
Clear All
</button>
<button
style="padding: 0.5rem 1rem; background: #6f42c1; color: white; border: none; border-radius: 4px; cursor: pointer;"
(click)="toggleDisabled()"
>
{{ globalDisabled() ? 'Enable All' : 'Disable All' }}
</button>
</div>
@if (lastSelection()) {
<div style="margin-top: 1rem; padding: 1rem; background: #e3f2fd; border-radius: 4px;">
<strong>Last selection:</strong> {{ lastSelection() }}
</div>
}
</section>
<!-- Code Examples -->
<section style="margin-bottom: 3rem;">
<h3>Code Examples</h3>
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; margin-bottom: 1rem;">
<h4>Basic Usage:</h4>
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>&lt;ui-select
label="Choose a country"
placeholder="Select a country"
[options]="countryOptions"
[(ngModel)]="selectedCountry"
(valueChange)="onCountryChange($event)"&gt;
&lt;/ui-select&gt;</code></pre>
<h4>With Helper Text:</h4>
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>&lt;ui-select
label="Framework"
placeholder="Select framework"
helperText="Choose your preferred frontend framework"
size="large"
variant="filled"
[options]="frameworks"
[(ngModel)]="selectedFramework"&gt;
&lt;/ui-select&gt;</code></pre>
<h4>Required with Error State:</h4>
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>&lt;ui-select
label="Required field"
[required]="true"
errorText="Please select an option"
[options]="options"
[(ngModel)]="selectedValue"&gt;
&lt;/ui-select&gt;</code></pre>
<h4>Options Format:</h4>
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>const options: SelectOption[] = [
{{ '{' }} value: 'option1', label: 'Option 1' {{ '}' }},
{{ '{' }} value: 'option2', label: 'Option 2' {{ '}' }},
{{ '{' }} value: 'option3', label: 'Option 3 (Disabled)', disabled: true {{ '}' }}
];</code></pre>
</div>
</section>
<!-- Current Values -->
<section style="margin-bottom: 3rem;">
<h3>Current Values</h3>
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; max-height: 400px; overflow-y: auto;">
<pre>{{ getCurrentValues() }}</pre>
</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(279, 14%, 35%);
font-size: 1.2rem;
margin-bottom: 0.5rem;
}
pre {
margin: 0;
font-family: 'Courier New', monospace;
font-size: 0.9rem;
line-height: 1.4;
}
code {
color: hsl(279, 14%, 15%);
}
section {
border: 1px solid #e9ecef;
padding: 1.5rem;
border-radius: 8px;
background: #ffffff;
}
button:hover {
opacity: 0.9;
transform: translateY(-1px);
}
`]
})
export class SelectDemoComponent {
// Basic selects
basicSelect1 = signal<string>('');
basicSelect2 = signal<string>('en');
basicSelect3 = signal<string>('');
// Size variants
sizeSmall = signal<string>('');
sizeMedium = signal<string>('medium');
sizeLarge = signal<string>('');
// Style variants
variantOutlined = signal<string>('');
variantFilled = signal<string>('filled');
// States
stateDisabled = signal<string>('disabled');
stateRequired = signal<string>('');
stateError = signal<string>('');
stateHelper = signal<string>('');
// Full width
fullWidthSelect = signal<string>('');
// Complex selects
complexSelect1 = signal<string>('');
complexSelect2 = signal<string>('medium');
// Control states
globalDisabled = signal<boolean>(false);
lastSelection = signal<string>('');
// Option arrays
countryOptions: SelectOption[] = [
{ value: 'us', label: 'United States' },
{ value: 'ca', label: 'Canada' },
{ value: 'uk', label: 'United Kingdom' },
{ value: 'de', label: 'Germany' },
{ value: 'fr', label: 'France' },
{ value: 'jp', label: 'Japan' },
{ value: 'au', label: 'Australia' }
];
languageOptions: SelectOption[] = [
{ value: 'en', label: 'English' },
{ value: 'es', label: 'Spanish' },
{ value: 'fr', label: 'French' },
{ value: 'de', label: 'German' },
{ value: 'it', label: 'Italian' },
{ value: 'pt', label: 'Portuguese' },
{ value: 'zh', label: 'Chinese' }
];
frameworkOptions: SelectOption[] = [
{ value: 'angular', label: 'Angular' },
{ value: 'react', label: 'React' },
{ value: 'vue', label: 'Vue.js' },
{ value: 'svelte', label: 'Svelte' },
{ value: 'solid', label: 'SolidJS' }
];
sizeOptions: SelectOption[] = [
{ value: 'small', label: 'Small Size' },
{ value: 'medium', label: 'Medium Size' },
{ value: 'large', label: 'Large Size' }
];
variantOptions: SelectOption[] = [
{ value: 'outlined', label: 'Outlined Style' },
{ value: 'filled', label: 'Filled Style' }
];
stateOptions: SelectOption[] = [
{ value: 'active', label: 'Active' },
{ value: 'inactive', label: 'Inactive' },
{ value: 'pending', label: 'Pending' }
];
fullWidthOptions: SelectOption[] = [
{ value: 'option1', label: 'This is a full width select option' },
{ value: 'option2', label: 'Another full width option with longer text' },
{ value: 'option3', label: 'Third option demonstrating full width behavior' }
];
roleOptions: SelectOption[] = [
{ value: 'user', label: 'User' },
{ value: 'moderator', label: 'Moderator' },
{ value: 'admin', label: 'Administrator' },
{ value: 'super-admin', label: 'Super Administrator', disabled: true },
{ value: 'guest', label: 'Guest', disabled: true }
];
priorityOptions: SelectOption[] = [
{ value: 'low', label: 'Low Priority' },
{ value: 'medium', label: 'Medium Priority' },
{ value: 'high', label: 'High Priority' },
{ value: 'urgent', label: 'Urgent' },
{ value: 'critical', label: 'Critical', disabled: true }
];
onSelectionChange(selectId: string, value: any): void {
const selectedOption = this.findOptionByValue(value);
const displayValue = selectedOption ? selectedOption.label : value;
this.lastSelection.set(`${selectId}: ${displayValue} (${value})`);
console.log(`Select ${selectId} changed:`, value);
}
private findOptionByValue(value: any): SelectOption | undefined {
const allOptions = [
...this.countryOptions,
...this.languageOptions,
...this.frameworkOptions,
...this.sizeOptions,
...this.variantOptions,
...this.stateOptions,
...this.fullWidthOptions,
...this.roleOptions,
...this.priorityOptions
];
return allOptions.find(option => option.value === value);
}
setDefaultValues(): void {
this.basicSelect1.set('us');
this.basicSelect2.set('en');
this.basicSelect3.set('angular');
this.sizeSmall.set('small');
this.sizeMedium.set('medium');
this.sizeLarge.set('large');
this.variantOutlined.set('outlined');
this.variantFilled.set('filled');
this.stateRequired.set('active');
this.stateError.set('pending');
this.stateHelper.set('inactive');
this.fullWidthSelect.set('option2');
this.complexSelect1.set('admin');
this.complexSelect2.set('high');
this.lastSelection.set('All selects set to default values');
}
clearAllSelections(): void {
this.basicSelect1.set('');
this.basicSelect2.set('');
this.basicSelect3.set('');
this.sizeSmall.set('');
this.sizeMedium.set('');
this.sizeLarge.set('');
this.variantOutlined.set('');
this.variantFilled.set('');
this.stateRequired.set('');
this.stateError.set('');
this.stateHelper.set('');
this.fullWidthSelect.set('');
this.complexSelect1.set('');
this.complexSelect2.set('');
this.lastSelection.set('All selections cleared');
}
toggleDisabled(): void {
this.globalDisabled.update(disabled => !disabled);
this.lastSelection.set(`Global disabled: ${this.globalDisabled() ? 'enabled' : 'disabled'}`);
}
getCurrentValues(): string {
const values = {
basic: {
country: this.basicSelect1(),
language: this.basicSelect2(),
framework: this.basicSelect3()
},
sizes: {
small: this.sizeSmall(),
medium: this.sizeMedium(),
large: this.sizeLarge()
},
variants: {
outlined: this.variantOutlined(),
filled: this.variantFilled()
},
states: {
disabled: this.stateDisabled(),
required: this.stateRequired(),
error: this.stateError(),
helper: this.stateHelper()
},
layout: {
fullWidth: this.fullWidthSelect()
},
complex: {
roles: this.complexSelect1(),
priority: this.complexSelect2()
},
controls: {
globalDisabled: this.globalDisabled(),
lastSelection: this.lastSelection()
}
};
return JSON.stringify(values, null, 2);
}
}

View File

@@ -0,0 +1,157 @@
@use '../../../../../ui-design-system/src/styles/semantic' as *;
.demo-container {
padding: $semantic-spacing-component-lg;
.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-component-md;
}
.demo-split-container {
height: 400px;
margin-bottom: $semantic-spacing-component-lg;
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
border-radius: $semantic-border-radius-md;
}
.demo-panel-content {
padding: $semantic-spacing-component-md;
background: $semantic-color-surface-primary;
height: 100%;
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;
}
&--primary {
background: $semantic-color-surface-primary;
}
&--secondary {
background: $semantic-color-surface-secondary;
}
&--elevated {
background: $semantic-color-surface-elevated;
box-shadow: $semantic-shadow-elevation-1;
}
}
.demo-controls {
display: flex;
gap: $semantic-spacing-component-sm;
margin-bottom: $semantic-spacing-component-md;
flex-wrap: wrap;
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;
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);
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
&:hover {
background: $semantic-color-primary;
box-shadow: $semantic-shadow-button-hover;
opacity: $semantic-opacity-hover;
}
&:focus-visible {
outline: 2px solid $semantic-color-focus;
outline-offset: 2px;
}
&--secondary {
background: $semantic-color-secondary;
color: $semantic-color-on-secondary;
}
&--success {
background: $semantic-color-success;
color: $semantic-color-on-success;
}
&--danger {
background: $semantic-color-danger;
color: $semantic-color-on-danger;
}
}
}
.demo-info {
padding: $semantic-spacing-component-md;
background: $semantic-color-surface-container;
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
border-radius: $semantic-border-radius-sm;
margin-top: $semantic-spacing-component-md;
p {
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;
margin-bottom: $semantic-spacing-content-line-tight;
&:last-child {
margin-bottom: 0;
}
}
strong {
color: $semantic-color-text-primary;
font-weight: $semantic-typography-font-weight-semibold;
}
}
}
}
@media (max-width: $semantic-breakpoint-md - 1) {
.demo-container {
padding: $semantic-spacing-component-md;
.demo-section .demo-split-container {
height: 300px;
}
.demo-section .demo-controls {
gap: $semantic-spacing-component-xs;
button {
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
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);
}
}
}
}

View File

@@ -0,0 +1,315 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SplitViewComponent, PanelConfig, ResizeEvent } from 'ui-essentials';
@Component({
selector: 'ui-split-view-demo',
standalone: true,
imports: [CommonModule, SplitViewComponent],
template: `
<div class="demo-container">
<h2>Split View / Resizable Panels Demo</h2>
<!-- Basic Horizontal Split -->
<section class="demo-section">
<h3>Basic Horizontal Split</h3>
<div class="demo-split-container">
<ui-split-view
[panels]="basicHorizontalPanels"
direction="horizontal"
size="md"
(panelResized)="onPanelResized($event)">
<div slot="left-panel" class="demo-panel-content demo-panel-content--primary">
<h4>Left Panel</h4>
<p>This is the left panel content. Drag the divider to resize!</p>
<p>You can add any content here including forms, lists, or other components.</p>
</div>
<div slot="right-panel" class="demo-panel-content demo-panel-content--secondary">
<h4>Right Panel</h4>
<p>This is the right panel content.</p>
<p>Panels maintain their content during resize operations.</p>
</div>
</ui-split-view>
</div>
<div class="demo-info">
<p><strong>Usage:</strong> Drag the vertical divider to resize panels horizontally.</p>
</div>
</section>
<!-- Basic Vertical Split -->
<section class="demo-section">
<h3>Basic Vertical Split</h3>
<div class="demo-split-container">
<ui-split-view
[panels]="basicVerticalPanels"
direction="vertical"
size="md"
(panelResized)="onPanelResized($event)">
<div slot="top-panel" class="demo-panel-content demo-panel-content--elevated">
<h4>Top Panel</h4>
<p>This is the top panel in a vertical split layout.</p>
</div>
<div slot="bottom-panel" class="demo-panel-content demo-panel-content--primary">
<h4>Bottom Panel</h4>
<p>This is the bottom panel. The divider can be dragged vertically.</p>
</div>
</ui-split-view>
</div>
<div class="demo-info">
<p><strong>Usage:</strong> Drag the horizontal divider to resize panels vertically.</p>
</div>
</section>
<!-- Three Panel Layout -->
<section class="demo-section">
<h3>Three Panel Layout</h3>
<div class="demo-split-container">
<ui-split-view
[panels]="threePanelLayout"
direction="horizontal"
size="lg"
(panelResized)="onPanelResized($event)">
<div slot="sidebar" class="demo-panel-content demo-panel-content--secondary">
<h4>Sidebar</h4>
<p>Navigation or menu content</p>
</div>
<div slot="main-content" class="demo-panel-content demo-panel-content--primary">
<h4>Main Content</h4>
<p>Primary content area with the most space.</p>
<p>This panel takes up the majority of the available space.</p>
</div>
<div slot="details-panel" class="demo-panel-content demo-panel-content--elevated">
<h4>Details Panel</h4>
<p>Additional information or tools</p>
</div>
</ui-split-view>
</div>
<div class="demo-controls">
<button (click)="collapsePanel('sidebar')">Collapse Sidebar</button>
<button (click)="expandPanel('sidebar')" class="button--secondary">Expand Sidebar</button>
<button (click)="collapsePanel('details-panel')" class="button--success">Collapse Details</button>
<button (click)="expandPanel('details-panel')" class="button--success">Expand Details</button>
<button (click)="resetThreePanelSizes()" class="button--danger">Reset Sizes</button>
</div>
<div class="demo-info">
<p><strong>Features:</strong> Multiple panels with programmatic collapse/expand controls.</p>
</div>
</section>
<!-- Size Variants -->
<section class="demo-section">
<h3>Size Variants</h3>
<h4>Small Size</h4>
<div class="demo-split-container" style="height: 200px;">
<ui-split-view
[panels]="sizeVariantPanels"
direction="horizontal"
size="sm">
<div slot="size-left" class="demo-panel-content demo-panel-content--primary">
<h4>Small Split View</h4>
<p>Compact layout with reduced spacing.</p>
</div>
<div slot="size-right" class="demo-panel-content demo-panel-content--secondary">
<h4>Right Panel</h4>
<p>Smaller padding and gaps.</p>
</div>
</ui-split-view>
</div>
<h4>Large Size</h4>
<div class="demo-split-container" style="height: 500px;">
<ui-split-view
[panels]="sizeVariantPanels2"
direction="horizontal"
size="lg">
<div slot="size-left-lg" class="demo-panel-content demo-panel-content--primary">
<h4>Large Split View</h4>
<p>Spacious layout with generous spacing and padding.</p>
<p>Perfect for complex layouts requiring more visual breathing room.</p>
</div>
<div slot="size-right-lg" class="demo-panel-content demo-panel-content--elevated">
<h4>Right Panel</h4>
<p>Enhanced spacing creates a more luxurious feel.</p>
</div>
</ui-split-view>
</div>
<div class="demo-info">
<p><strong>Sizes:</strong> sm (compact), md (default), lg (spacious) - affects padding and spacing.</p>
</div>
</section>
<!-- Panel Constraints -->
<section class="demo-section">
<h3>Panel Size Constraints</h3>
<div class="demo-split-container">
<ui-split-view
[panels]="constrainedPanels"
direction="horizontal"
size="md"
(panelResized)="onPanelResized($event)">
<div slot="constrained-left" class="demo-panel-content demo-panel-content--primary">
<h4>Constrained Panel</h4>
<p>Min: 150px, Max: 400px</p>
<p>Try resizing - this panel has size constraints!</p>
</div>
<div slot="constrained-right" class="demo-panel-content demo-panel-content--secondary">
<h4>Flexible Panel</h4>
<p>No constraints - adapts to available space.</p>
</div>
</ui-split-view>
</div>
<div class="demo-info">
<p><strong>Constraints:</strong> Left panel has min-width of 150px and max-width of 400px.</p>
</div>
</section>
<!-- Disabled State -->
<section class="demo-section">
<h3>Disabled State</h3>
<div class="demo-split-container">
<ui-split-view
[panels]="basicHorizontalPanels"
direction="horizontal"
size="md"
[disabled]="true">
<div slot="left-panel" class="demo-panel-content demo-panel-content--primary">
<h4>Disabled Split View</h4>
<p>Resizing is disabled - dividers cannot be dragged.</p>
</div>
<div slot="right-panel" class="demo-panel-content demo-panel-content--secondary">
<h4>Static Panel</h4>
<p>All interaction is disabled when the component is in disabled state.</p>
</div>
</ui-split-view>
</div>
<div class="demo-info">
<p><strong>Disabled:</strong> All resize functionality is disabled.</p>
</div>
</section>
<!-- Nested Split Views -->
<section class="demo-section">
<h3>Nested Split Views</h3>
<div class="demo-split-container" style="height: 500px;">
<ui-split-view
[panels]="outerNestedPanels"
direction="horizontal"
size="md">
<div slot="outer-left" class="demo-panel-content demo-panel-content--secondary">
<h4>Left Panel</h4>
<p>This is the outer left panel.</p>
</div>
<div slot="outer-right" style="padding: 0;">
<ui-split-view
[panels]="innerNestedPanels"
direction="vertical"
size="md">
<div slot="inner-top" class="demo-panel-content demo-panel-content--primary">
<h4>Nested Top</h4>
<p>This is a nested split view inside the right panel.</p>
</div>
<div slot="inner-bottom" class="demo-panel-content demo-panel-content--elevated">
<h4>Nested Bottom</h4>
<p>Fully functional nested resizable panels.</p>
</div>
</ui-split-view>
</div>
</ui-split-view>
</div>
<div class="demo-info">
<p><strong>Nesting:</strong> Split views can be nested to create complex layouts.</p>
</div>
</section>
<!-- Event Information -->
<section class="demo-section">
<h3>Resize Events</h3>
<div class="demo-info">
<p><strong>Last Resize Event:</strong> {{ lastResizeEvent || 'None' }}</p>
<p><strong>Resize Count:</strong> {{ resizeCount }}</p>
</div>
</section>
</div>
`,
styleUrl: './split-view-demo.component.scss'
})
export class SplitViewDemoComponent {
resizeCount = 0;
lastResizeEvent = '';
// Basic horizontal panels
basicHorizontalPanels: PanelConfig[] = [
{ id: 'left-panel', size: '40%' },
{ id: 'right-panel', size: '60%' }
];
// Basic vertical panels
basicVerticalPanels: PanelConfig[] = [
{ id: 'top-panel', size: '30%' },
{ id: 'bottom-panel', size: '70%' }
];
// Three panel layout
threePanelLayout: PanelConfig[] = [
{ id: 'sidebar', size: '250px', minSize: 200, maxSize: 400, variant: 'secondary' },
{ id: 'main-content', size: '1fr', variant: 'primary' },
{ id: 'details-panel', size: '300px', minSize: 250, maxSize: 500, variant: 'elevated' }
];
// Size variant panels
sizeVariantPanels: PanelConfig[] = [
{ id: 'size-left', size: '50%' },
{ id: 'size-right', size: '50%' }
];
sizeVariantPanels2: PanelConfig[] = [
{ id: 'size-left-lg', size: '40%' },
{ id: 'size-right-lg', size: '60%' }
];
// Constrained panels
constrainedPanels: PanelConfig[] = [
{ id: 'constrained-left', size: '300px', minSize: 150, maxSize: 400 },
{ id: 'constrained-right', size: '1fr' }
];
// Nested panels
outerNestedPanels: PanelConfig[] = [
{ id: 'outer-left', size: '30%' },
{ id: 'outer-right', size: '70%' }
];
innerNestedPanels: PanelConfig[] = [
{ id: 'inner-top', size: '40%' },
{ id: 'inner-bottom', size: '60%' }
];
onPanelResized(event: ResizeEvent): void {
this.resizeCount++;
this.lastResizeEvent = `Panel ${event.panelId} resized to ${Math.round(event.newSize)}px (${event.direction})`;
console.log('Panel resized:', event);
}
collapsePanel(panelId: string): void {
const panel = this.threePanelLayout.find(p => p.id === panelId);
if (panel) {
panel.collapsed = true;
}
}
expandPanel(panelId: string): void {
const panel = this.threePanelLayout.find(p => p.id === panelId);
if (panel) {
panel.collapsed = false;
}
}
resetThreePanelSizes(): void {
this.threePanelLayout = [
{ id: 'sidebar', size: '250px', minSize: 200, maxSize: 400, variant: 'secondary' },
{ id: 'main-content', size: '1fr', variant: 'primary' },
{ id: 'details-panel', size: '300px', minSize: 250, maxSize: 500, variant: 'elevated' }
];
}
}

View File

@@ -0,0 +1,189 @@
.demo-container {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.demo-section {
margin-bottom: 3rem;
h3 {
margin-bottom: 1.5rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #e0e0e0;
}
h4 {
margin-bottom: 1rem;
font-size: 1.1rem;
}
}
.demo-scroll-container {
height: 400px;
overflow-y: auto;
border: 2px solid #f0f0f0;
border-radius: 8px;
position: relative;
background: linear-gradient(to bottom, #fafafa, #f0f0f0);
}
.demo-content-spacer {
padding: 2rem;
min-height: 600px;
p {
margin-bottom: 2rem;
text-align: center;
font-weight: 500;
}
}
.demo-filler {
height: 100px;
margin: 1rem 0;
background: linear-gradient(135deg, #e3f2fd, #bbdefb);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 500;
color: #1976d2;
}
.demo-content {
padding: 1rem 2rem;
background: #fff;
border-radius: 4px;
font-weight: 500;
text-align: center;
color: #333;
}
.demo-content-small {
padding: 0.75rem 1.5rem;
background: #fff;
border-radius: 4px;
font-weight: 500;
text-align: center;
color: #333;
font-size: 0.9rem;
}
.demo-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 2rem;
margin-top: 1rem;
}
.demo-row {
display: flex;
gap: 1rem;
flex-wrap: wrap;
margin-top: 1rem;
}
.demo-variant-example {
padding: 1.5rem;
border: 1px solid #e0e0e0;
border-radius: 8px;
background: #fafafa;
position: relative;
min-height: 120px;
h4 {
position: absolute;
top: 0.5rem;
left: 1rem;
margin: 0;
font-size: 0.9rem;
color: #666;
}
}
.demo-effect-example {
padding: 1.5rem;
border: 1px solid #e0e0e0;
border-radius: 8px;
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
position: relative;
min-height: 120px;
h4 {
position: absolute;
top: 0.5rem;
left: 1rem;
margin: 0;
font-size: 0.9rem;
color: #666;
}
}
.demo-controls {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
flex-wrap: wrap;
label {
display: flex;
flex-direction: column;
gap: 0.5rem;
font-weight: 500;
select {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
min-width: 120px;
}
input[type="checkbox"] {
width: 18px;
height: 18px;
}
}
}
.demo-preview {
height: 300px;
border: 2px dashed #ddd;
border-radius: 8px;
position: relative;
overflow: auto;
background: linear-gradient(45deg, #f9f9f9 25%, transparent 25%),
linear-gradient(-45deg, #f9f9f9 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #f9f9f9 75%),
linear-gradient(-45deg, transparent 75%, #f9f9f9 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
}
@media (max-width: 768px) {
.demo-container {
padding: 1rem;
}
.demo-grid {
grid-template-columns: 1fr;
}
.demo-row {
flex-direction: column;
}
.demo-controls {
flex-direction: column;
label {
flex-direction: row;
align-items: center;
select {
min-width: auto;
flex: 1;
}
}
}
}

View File

@@ -0,0 +1,229 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { StickyLayoutComponent } from 'ui-essentials';
@Component({
selector: 'ui-sticky-layout-demo',
standalone: true,
imports: [CommonModule, FormsModule, StickyLayoutComponent],
template: `
<div class="demo-container">
<h2>Sticky Layout Demo</h2>
<p>Scroll down to see the sticky positioning in action.</p>
<!-- Position Variants -->
<section class="demo-section">
<h3>Position Variants</h3>
<div class="demo-scroll-container">
<ui-sticky-layout position="top" variant="header" [shadow]="true">
<div class="demo-content">Sticky Top Header</div>
</ui-sticky-layout>
<div class="demo-content-spacer">
<p>Scroll down to see sticky positioning...</p>
<div class="demo-filler">Content area 1</div>
<div class="demo-filler">Content area 2</div>
<div class="demo-filler">Content area 3</div>
<div class="demo-filler">Content area 4</div>
<div class="demo-filler">Content area 5</div>
</div>
<ui-sticky-layout position="bottom" variant="footer" [shadow]="true">
<div class="demo-content">Sticky Bottom Footer</div>
</ui-sticky-layout>
</div>
</section>
<!-- Variant Styles -->
<section class="demo-section">
<h3>Variant Styles</h3>
<div class="demo-grid">
@for (variant of variants; track variant.name) {
<div class="demo-variant-example">
<h4>{{ variant.name }}</h4>
<ui-sticky-layout
[position]="variant.position"
[variant]="variant.variant"
[shadow]="variant.shadow"
[background]="variant.background">
<div class="demo-content-small">
{{ variant.name }} Sticky
</div>
</ui-sticky-layout>
</div>
}
</div>
</section>
<!-- Offset Examples -->
<section class="demo-section">
<h3>Offset Variants</h3>
<div class="demo-row">
@for (offset of offsets; track offset) {
<ui-sticky-layout
position="top"
[offset]="offset"
variant="toolbar"
[shadow]="true">
<div class="demo-content-small">
Offset: {{ offset }}
</div>
</ui-sticky-layout>
}
</div>
</section>
<!-- Background and Effects -->
<section class="demo-section">
<h3>Background & Effects</h3>
<div class="demo-grid">
<div class="demo-effect-example">
<h4>Blur Effect</h4>
<ui-sticky-layout
position="top"
background="surface"
[blur]="true"
[shadow]="true">
<div class="demo-content-small">Blurred Background</div>
</ui-sticky-layout>
</div>
<div class="demo-effect-example">
<h4>Backdrop</h4>
<ui-sticky-layout
position="top"
background="backdrop"
[border]="true">
<div class="demo-content-small">Backdrop Filter</div>
</ui-sticky-layout>
</div>
<div class="demo-effect-example">
<h4>Floating</h4>
<ui-sticky-layout
position="top"
variant="floating"
offset="md">
<div class="demo-content-small">Floating Style</div>
</ui-sticky-layout>
</div>
<div class="demo-effect-example">
<h4>Toolbar</h4>
<ui-sticky-layout
position="top"
variant="toolbar"
[shadow]="true"
[border]="true">
<div class="demo-content-small">Toolbar Style</div>
</ui-sticky-layout>
</div>
</div>
</section>
<!-- Size Variants -->
<section class="demo-section">
<h3>Size Variants</h3>
<div class="demo-row">
@for (size of sizes; track size) {
<ui-sticky-layout
position="top"
[size]="size"
variant="header"
[shadow]="true">
<div class="demo-content-small">
Size: {{ size }}
</div>
</ui-sticky-layout>
}
</div>
</section>
<!-- Interactive Example -->
<section class="demo-section">
<h3>Interactive Example</h3>
<div class="demo-controls">
<label>
Position:
<select [(ngModel)]="selectedPosition" (change)="updateConfig()">
@for (pos of positions; track pos) {
<option [value]="pos">{{ pos }}</option>
}
</select>
</label>
<label>
Variant:
<select [(ngModel)]="selectedVariant" (change)="updateConfig()">
@for (variant of variantOptions; track variant) {
<option [value]="variant">{{ variant }}</option>
}
</select>
</label>
<label>
<input type="checkbox" [(ngModel)]="enableShadow" (change)="updateConfig()">
Shadow
</label>
<label>
<input type="checkbox" [(ngModel)]="enableBorder" (change)="updateConfig()">
Border
</label>
<label>
<input type="checkbox" [(ngModel)]="enableBlur" (change)="updateConfig()">
Blur
</label>
</div>
<div class="demo-preview">
<ui-sticky-layout
[position]="selectedPosition"
[variant]="selectedVariant"
[shadow]="enableShadow"
[border]="enableBorder"
[blur]="enableBlur"
background="surface">
<div class="demo-content">
Dynamic Sticky Layout
</div>
</ui-sticky-layout>
</div>
</section>
</div>
`,
styleUrl: './sticky-layout-demo.component.scss'
})
export class StickyLayoutDemoComponent {
positions = ['top', 'bottom', 'left', 'right'] as const;
offsets = ['xs', 'sm', 'md', 'lg', 'xl'] as const;
sizes = ['auto', 'full', 'content'] as const;
variantOptions = ['default', 'header', 'sidebar', 'footer', 'floating', 'toolbar'] as const;
variants = [
{ name: 'Header', variant: 'header' as const, position: 'top' as const, shadow: true, background: 'surface' as const },
{ name: 'Sidebar', variant: 'sidebar' as const, position: 'left' as const, shadow: false, background: 'surface-secondary' as const },
{ name: 'Footer', variant: 'footer' as const, position: 'bottom' as const, shadow: true, background: 'surface' as const },
{ name: 'Floating', variant: 'floating' as const, position: 'top' as const, shadow: true, background: 'surface-elevated' as const },
{ name: 'Toolbar', variant: 'toolbar' as const, position: 'top' as const, shadow: true, background: 'surface-elevated' as const }
];
// Interactive controls
selectedPosition: typeof this.positions[number] = 'top';
selectedVariant: typeof this.variantOptions[number] = 'header';
enableShadow = true;
enableBorder = false;
enableBlur = false;
updateConfig(): void {
console.log('Sticky layout config updated:', {
position: this.selectedPosition,
variant: this.selectedVariant,
shadow: this.enableShadow,
border: this.enableBorder,
blur: this.enableBlur
});
}
}

View File

@@ -0,0 +1,591 @@
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { TextareaComponent } from '../../../../../ui-essentials/src/lib/components/forms/input/textarea.component';
import { faComment, faEdit, faEnvelope } from '@fortawesome/free-solid-svg-icons';
@Component({
selector: 'ui-textarea-demo',
standalone: true,
imports: [
CommonModule,
FormsModule,
TextareaComponent
],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div style="padding: 2rem;">
<h2>Textarea Component Showcase</h2>
<!-- Basic Textarea -->
<section style="margin-bottom: 3rem;">
<h3>Basic Textarea</h3>
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem;">
<ui-textarea
label="Basic textarea"
placeholder="Enter your message here..."
[(ngModel)]="basicTextarea1"
(textareaChange)="onTextareaChange('basic-1', $event)"
/>
<ui-textarea
label="Textarea with helper text"
placeholder="Share your thoughts..."
helperText="Your feedback is important to us"
[(ngModel)]="basicTextarea2"
(textareaChange)="onTextareaChange('basic-2', $event)"
/>
<ui-textarea
label="Pre-filled textarea"
placeholder="Enter description..."
[(ngModel)]="basicTextarea3"
(textareaChange)="onTextareaChange('basic-3', $event)"
/>
</div>
</section>
<!-- Size Variants -->
<section style="margin-bottom: 3rem;">
<h3>Size Variants</h3>
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem;">
<ui-textarea
label="Small textarea"
size="sm"
placeholder="Small size textarea..."
[(ngModel)]="sizeSmall"
(textareaChange)="onTextareaChange('size-small', $event)"
/>
<ui-textarea
label="Medium textarea (default)"
size="md"
placeholder="Medium size textarea..."
[(ngModel)]="sizeMedium"
(textareaChange)="onTextareaChange('size-medium', $event)"
/>
<ui-textarea
label="Large textarea"
size="lg"
placeholder="Large size textarea..."
[(ngModel)]="sizeLarge"
(textareaChange)="onTextareaChange('size-large', $event)"
/>
</div>
</section>
<!-- Variant Styles -->
<section style="margin-bottom: 3rem;">
<h3>Style Variants</h3>
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem;">
<ui-textarea
label="Outlined variant (default)"
variant="outlined"
placeholder="Outlined style textarea..."
[(ngModel)]="variantOutlined"
(textareaChange)="onTextareaChange('variant-outlined', $event)"
/>
<ui-textarea
label="Filled variant"
variant="filled"
placeholder="Filled style textarea..."
[(ngModel)]="variantFilled"
(textareaChange)="onTextareaChange('variant-filled', $event)"
/>
<ui-textarea
label="Underlined variant"
variant="underlined"
placeholder="Underlined style textarea..."
[(ngModel)]="variantUnderlined"
(textareaChange)="onTextareaChange('variant-underlined', $event)"
/>
</div>
</section>
<!-- States -->
<section style="margin-bottom: 3rem;">
<h3>States and Validation</h3>
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem;">
<ui-textarea
label="Default state"
state="default"
placeholder="Normal textarea..."
[(ngModel)]="stateDefault"
(textareaChange)="onTextareaChange('state-default', $event)"
/>
<ui-textarea
label="Success state"
state="success"
placeholder="Successful input..."
helperText="This looks good!"
[(ngModel)]="stateSuccess"
(textareaChange)="onTextareaChange('state-success', $event)"
/>
<ui-textarea
label="Warning state"
state="warning"
placeholder="Input with warning..."
helperText="Please double-check this content"
[(ngModel)]="stateWarning"
(textareaChange)="onTextareaChange('state-warning', $event)"
/>
<ui-textarea
label="Error state"
state="error"
placeholder="Input with error..."
errorMessage="This field contains an error"
[(ngModel)]="stateError"
(textareaChange)="onTextareaChange('state-error', $event)"
/>
</div>
</section>
<!-- Interactive States -->
<section style="margin-bottom: 3rem;">
<h3>Interactive States</h3>
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem;">
<ui-textarea
label="Disabled textarea"
[disabled]="true"
placeholder="This textarea is disabled"
[(ngModel)]="stateDisabled"
/>
<ui-textarea
label="Readonly textarea"
[readonly]="true"
placeholder="This textarea is readonly"
[(ngModel)]="stateReadonly"
/>
<ui-textarea
label="Required field"
[required]="true"
placeholder="This field is required"
[(ngModel)]="stateRequired"
(textareaChange)="onTextareaChange('state-required', $event)"
/>
<ui-textarea
label="Loading state"
[loading]="true"
placeholder="This textarea is in loading state"
[(ngModel)]="stateLoading"
(textareaChange)="onTextareaChange('state-loading', $event)"
/>
</div>
</section>
<!-- Character Limits and Counters -->
<section style="margin-bottom: 3rem;">
<h3>Character Limits and Counters</h3>
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem;">
<ui-textarea
label="Textarea with character limit"
placeholder="You have 100 characters max..."
[maxLength]="100"
[showCharacterCount]="true"
helperText="Please keep your message concise"
[(ngModel)]="charLimit1"
(textareaChange)="onTextareaChange('char-limit-1', $event)"
/>
<ui-textarea
label="Tweet-style limit"
placeholder="What's happening?"
[maxLength]="280"
[showCharacterCount]="true"
[(ngModel)]="charLimit2"
(textareaChange)="onTextareaChange('char-limit-2', $event)"
/>
<ui-textarea
label="Long form content"
placeholder="Write your article here..."
[maxLength]="1000"
[showCharacterCount]="true"
[rows]="8"
[(ngModel)]="charLimit3"
(textareaChange)="onTextareaChange('char-limit-3', $event)"
/>
</div>
</section>
<!-- Row and Resize Options -->
<section style="margin-bottom: 3rem;">
<h3>Rows and Resize Options</h3>
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem;">
<ui-textarea
label="2 rows (compact)"
placeholder="Short input area..."
[rows]="2"
resize="none"
[(ngModel)]="rowsCompact"
(textareaChange)="onTextareaChange('rows-compact', $event)"
/>
<ui-textarea
label="6 rows (expanded)"
placeholder="Larger input area..."
[rows]="6"
resize="vertical"
[(ngModel)]="rowsExpanded"
(textareaChange)="onTextareaChange('rows-expanded', $event)"
/>
<ui-textarea
label="Auto-resize textarea"
placeholder="This textarea will grow as you type..."
[autoResize]="true"
[rows]="3"
[(ngModel)]="autoResizeTextarea"
(textareaChange)="onTextareaChange('auto-resize', $event)"
/>
<ui-textarea
label="Horizontal resize"
placeholder="You can resize this horizontally..."
resize="horizontal"
[(ngModel)]="horizontalResize"
(textareaChange)="onTextareaChange('horizontal-resize', $event)"
/>
<ui-textarea
label="Both directions resize"
placeholder="Resize in any direction..."
resize="both"
[(ngModel)]="bothResize"
(textareaChange)="onTextareaChange('both-resize', $event)"
/>
</div>
</section>
<!-- Icons and Clearable -->
<section style="margin-bottom: 3rem;">
<h3>Icons and Clearable Features</h3>
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem;">
<ui-textarea
label="With prefix icon"
placeholder="Type your comment..."
[prefixIcon]="faComment"
[(ngModel)]="iconPrefix"
(textareaChange)="onTextareaChange('icon-prefix', $event)"
/>
<ui-textarea
label="With suffix icon"
placeholder="Write your email content..."
[suffixIcon]="faEnvelope"
[(ngModel)]="iconSuffix"
(textareaChange)="onTextareaChange('icon-suffix', $event)"
/>
<ui-textarea
label="Clearable textarea"
placeholder="You can clear this easily..."
[clearable]="true"
[prefixIcon]="faEdit"
[(ngModel)]="clearableTextarea"
(textareaChange)="onTextareaChange('clearable', $event)"
(clear)="onClear('clearable')"
/>
</div>
</section>
<!-- Interactive Controls -->
<section style="margin-bottom: 3rem;">
<h3>Interactive Controls</h3>
<div style="display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
<button
style="padding: 0.5rem 1rem; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;"
(click)="fillSampleText()"
>
Fill Sample Text
</button>
<button
style="padding: 0.5rem 1rem; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer;"
(click)="clearAllTextareas()"
>
Clear All
</button>
<button
style="padding: 0.5rem 1rem; background: #6f42c1; color: white; border: none; border-radius: 4px; cursor: pointer;"
(click)="toggleDisabled()"
>
{{ globalDisabled() ? 'Enable All' : 'Disable All' }}
</button>
</div>
@if (lastChange()) {
<div style="margin-top: 1rem; padding: 1rem; background: #e3f2fd; border-radius: 4px;">
<strong>Last change:</strong> {{ lastChange() }}
</div>
}
</section>
<!-- Code Examples -->
<section style="margin-bottom: 3rem;">
<h3>Code Examples</h3>
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; margin-bottom: 1rem;">
<h4>Basic Usage:</h4>
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>&lt;ui-textarea
label="Message"
placeholder="Enter your message..."
[(ngModel)]="message"
(textareaChange)="onMessageChange($event)"&gt;
&lt;/ui-textarea&gt;</code></pre>
<h4>With Character Limit:</h4>
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>&lt;ui-textarea
label="Tweet"
placeholder="What's happening?"
[maxLength]="280"
[showCharacterCount]="true"
size="lg"
[(ngModel)]="tweetText"&gt;
&lt;/ui-textarea&gt;</code></pre>
<h4>Auto-resize with Validation:</h4>
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>&lt;ui-textarea
label="Feedback"
[required]="true"
[autoResize]="true"
[clearable]="true"
state="error"
errorMessage="Please provide feedback"
[prefixIcon]="faComment"
[(ngModel)]="feedback"&gt;
&lt;/ui-textarea&gt;</code></pre>
<h4>Resize Options:</h4>
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>// Resize options:
resize="none" // No resize
resize="vertical" // Vertical only (default)
resize="horizontal" // Horizontal only
resize="both" // Both directions</code></pre>
</div>
</section>
<!-- Current Values -->
<section style="margin-bottom: 3rem;">
<h3>Current Values</h3>
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; max-height: 400px; overflow-y: auto;">
<pre>{{ getCurrentValues() }}</pre>
</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(279, 14%, 35%);
font-size: 1.2rem;
margin-bottom: 0.5rem;
}
pre {
margin: 0;
font-family: 'Courier New', monospace;
font-size: 0.9rem;
line-height: 1.4;
}
code {
color: hsl(279, 14%, 15%);
}
section {
border: 1px solid #e9ecef;
padding: 1.5rem;
border-radius: 8px;
background: #ffffff;
}
button:hover {
opacity: 0.9;
transform: translateY(-1px);
}
`]
})
export class TextareaDemoComponent {
// FontAwesome icons
readonly faComment = faComment;
readonly faEdit = faEdit;
readonly faEnvelope = faEnvelope;
// Basic textareas
basicTextarea1 = signal<string>('');
basicTextarea2 = signal<string>('');
basicTextarea3 = signal<string>('This is pre-filled content that demonstrates how the textarea looks with existing text.');
// Size variants
sizeSmall = signal<string>('');
sizeMedium = signal<string>('');
sizeLarge = signal<string>('');
// Style variants
variantOutlined = signal<string>('');
variantFilled = signal<string>('');
variantUnderlined = signal<string>('');
// States
stateDefault = signal<string>('');
stateSuccess = signal<string>('Great job on completing this form!');
stateWarning = signal<string>('Please review this content carefully.');
stateError = signal<string>('This content has some issues.');
// Interactive states
stateDisabled = signal<string>('This content cannot be edited');
stateReadonly = signal<string>('This is read-only information that you can view but not modify');
stateRequired = signal<string>('');
stateLoading = signal<string>('');
// Character limits
charLimit1 = signal<string>('');
charLimit2 = signal<string>('');
charLimit3 = signal<string>('');
// Rows and resize
rowsCompact = signal<string>('');
rowsExpanded = signal<string>('');
autoResizeTextarea = signal<string>('Start typing and watch this textarea grow automatically as you add more content...');
horizontalResize = signal<string>('');
bothResize = signal<string>('');
// Icons and clearable
iconPrefix = signal<string>('');
iconSuffix = signal<string>('');
clearableTextarea = signal<string>('You can clear this text by clicking the X button');
// Control states
globalDisabled = signal<boolean>(false);
lastChange = signal<string>('');
onTextareaChange(textareaId: string, value: string): void {
const preview = value.length > 50 ? value.substring(0, 50) + '...' : value;
this.lastChange.set(`${textareaId}: "${preview}" (${value.length} chars)`);
console.log(`Textarea ${textareaId} changed:`, value);
}
onClear(textareaId: string): void {
this.lastChange.set(`${textareaId}: cleared`);
console.log(`Textarea ${textareaId} cleared`);
}
fillSampleText(): void {
const sampleTexts = [
'This is sample text for demonstration purposes.',
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
'Here is some example content to show how the textarea handles different amounts of text.',
'Sample content with multiple lines.\nSecond line of content.\nThird line for testing.',
'Short text',
'A longer piece of sample text that demonstrates how the textarea component handles various lengths of content and provides a good example for testing purposes.'
];
this.basicTextarea1.set(sampleTexts[0]);
this.basicTextarea2.set(sampleTexts[1]);
this.sizeSmall.set(sampleTexts[4]);
this.sizeMedium.set(sampleTexts[2]);
this.sizeLarge.set(sampleTexts[5]);
this.variantOutlined.set(sampleTexts[0]);
this.variantFilled.set(sampleTexts[1]);
this.variantUnderlined.set(sampleTexts[2]);
this.stateDefault.set(sampleTexts[3]);
this.stateRequired.set(sampleTexts[0]);
this.stateLoading.set(sampleTexts[1]);
this.charLimit1.set('Sample text within limit');
this.charLimit2.set('This is a tweet-style message that fits within the character limit.');
this.charLimit3.set(sampleTexts[5]);
this.rowsCompact.set('Compact');
this.rowsExpanded.set(sampleTexts[3]);
this.iconPrefix.set('Comment with icon');
this.iconSuffix.set('Email content here');
this.clearableTextarea.set('This text can be cleared');
this.lastChange.set('All textareas filled with sample text');
}
clearAllTextareas(): void {
this.basicTextarea1.set('');
this.basicTextarea2.set('');
this.sizeSmall.set('');
this.sizeMedium.set('');
this.sizeLarge.set('');
this.variantOutlined.set('');
this.variantFilled.set('');
this.variantUnderlined.set('');
this.stateDefault.set('');
this.stateRequired.set('');
this.stateLoading.set('');
this.charLimit1.set('');
this.charLimit2.set('');
this.charLimit3.set('');
this.rowsCompact.set('');
this.rowsExpanded.set('');
this.autoResizeTextarea.set('');
this.horizontalResize.set('');
this.bothResize.set('');
this.iconPrefix.set('');
this.iconSuffix.set('');
this.clearableTextarea.set('');
this.lastChange.set('All textareas cleared');
}
toggleDisabled(): void {
this.globalDisabled.update(disabled => !disabled);
this.lastChange.set(`Global disabled: ${this.globalDisabled() ? 'enabled' : 'disabled'}`);
}
getCurrentValues(): string {
const values = {
basic: {
textarea1: this.basicTextarea1(),
textarea2: this.basicTextarea2(),
textarea3: this.basicTextarea3()
},
sizes: {
small: this.sizeSmall(),
medium: this.sizeMedium(),
large: this.sizeLarge()
},
variants: {
outlined: this.variantOutlined(),
filled: this.variantFilled(),
underlined: this.variantUnderlined()
},
states: {
default: this.stateDefault(),
success: this.stateSuccess(),
warning: this.stateWarning(),
error: this.stateError(),
disabled: this.stateDisabled(),
readonly: this.stateReadonly(),
required: this.stateRequired(),
loading: this.stateLoading()
},
characterLimits: {
limit100: this.charLimit1(),
limit280: this.charLimit2(),
limit1000: this.charLimit3()
},
rowsAndResize: {
compact: this.rowsCompact(),
expanded: this.rowsExpanded(),
autoResize: this.autoResizeTextarea(),
horizontal: this.horizontalResize(),
both: this.bothResize()
},
iconsAndClearable: {
prefix: this.iconPrefix(),
suffix: this.iconSuffix(),
clearable: this.clearableTextarea()
},
controls: {
globalDisabled: this.globalDisabled(),
lastChange: this.lastChange()
}
};
return JSON.stringify(values, null, 2);
}
}

View File

@@ -11,7 +11,7 @@ import {
faCloudUploadAlt, faFileText, faListAlt, faCircle, faExpandArrowsAlt, faCircleNotch, faSliders,
faMinus, faInfoCircle, faChevronDown, faCaretUp, faExclamationCircle, faSitemap, faStream,
faBell, faRoute, faChevronUp, faEllipsisV, faCut, faPalette, faExchangeAlt, faTools, faHeart, faCircleDot, faColumns,
faFolderOpen, faDesktop, faListUl
faFolderOpen, faDesktop, faListUl, faArrowDown, faThumbtack
} from '@fortawesome/free-solid-svg-icons';
import { DemoRoutes } from '../../demos';
import { ScrollContainerComponent } from "../../../../../ui-essentials/src/public-api";
@@ -115,6 +115,8 @@ export class DashboardComponent {
faDesktop = faDesktop;
faNewspaper = faNewspaper;
faListUl = faListUl;
faArrowDown = faArrowDown;
faThumbtack = faThumbtack;
menuItems: any = []
@@ -235,15 +237,21 @@ export class DashboardComponent {
this.createChildItem("container", "Container", this.faBoxOpen),
this.createChildItem("feed-layout", "Feed Layout", this.faNewspaper),
this.createChildItem("flex", "Flex (Flexbox Container)", this.faArrowsAlt),
this.createChildItem("gallery-grid", "Gallery Grid", this.faImages),
this.createChildItem("grid-system", "Grid System", this.faGripVertical),
this.createChildItem("grid-container", "Grid Container", this.faThLarge),
this.createChildItem("infinite-scroll-container", "Infinite Scroll Container", this.faArrowDown),
this.createChildItem("kanban-board", "Kanban Board Layout", this.faColumns),
this.createChildItem("list-detail-layout", "List Detail Layout", this.faListUl),
this.createChildItem("masonry", "Masonry Layout", this.faThLarge),
this.createChildItem("scroll-container", "Scroll Container", this.faListAlt),
this.createChildItem("supporting-pane-layout", "Supporting Pane Layout", this.faWindowMaximize),
this.createChildItem("section", "Section (Semantic Wrapper)", this.faLayerGroup),
this.createChildItem("sidebar-layout", "Sidebar Layout", this.faBars),
this.createChildItem("spacer", "Spacer", this.faArrowsAlt),
this.createChildItem("split-view", "Split View / Resizable Panels", this.faColumns),
this.createChildItem("stack", "Stack (VStack/HStack)", this.faStream),
this.createChildItem("sticky-layout", "Sticky Layout", this.faThumbtack),
this.createChildItem("tabs-container", "Tabs Container", this.faFolderOpen),
this.createChildItem("dashboard-shell", "Dashboard Shell", this.faDesktop)
];
@@ -263,8 +271,18 @@ export class DashboardComponent {
// Utilities (standalone)
this.addMenuItem("fontawesome", "FontAwesome Icons", this.faCheckSquare);
this.addMenuItem("accessibility", "Accessability", this.faCheckSquare);
this.addMenuItem("animations", "Animations", this.faCheckSquare);
this.addMenuItem("hcl-studio", "HCL Themes", this.faCheckSquare);
this.addMenuItem("font-manager", "Font Manager", this.faCheckSquare);
this.addMenuItem("data-utils", "Data Utils", this.faCheckSquare);
this.addMenuItem("code-display", "Code Display", this.faCheckSquare);
}
menuClick(id: string) {
console.log(JSON.stringify(id))

View File

@@ -47,7 +47,6 @@
.children-container {
margin-left: 1rem;
padding-left: 1rem;
border-left: 2px solid semantic.$semantic-color-border-secondary;
margin-top: 0.25rem;
::ng-deep .child-menu-item {

View File

@@ -11,9 +11,19 @@ $comfortaa-font-path: "/fonts/comfortaa/" !default;
// Import project variables (which now has semantic tokens available)
@import 'scss/_variables';
// Import UI Animations
@import '../../ui-animations/src/styles/index';
html {
height: 100%;
font-size: 16px; // Base font size for rem calculations
// Define default CSS custom properties for fonts
// These can be overridden by the font manager
--font-family-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--font-family-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-family-mono: ui-monospace, SFMono-Regular, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--font-family-display: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
body {
@@ -23,8 +33,8 @@ body {
background-color: $semantic-color-surface-primary;
color: $semantic-color-text-primary;
// Apply base typography using semantic tokens
font-family: map-get($semantic-typography-body-medium, font-family);
// Apply base typography using CSS custom properties first, then fallback to semantic tokens
font-family: var(--font-family-sans, 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);
@@ -32,4 +42,37 @@ body {
// Improve text rendering
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
// Support font transitions
transition: font-family 0.3s ease-in-out;
}
// Global typography classes that respect font variables
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-family-display, var(--font-family-sans));
transition: font-family 0.3s ease-in-out;
}
// Serif elements
blockquote, .serif {
font-family: var(--font-family-serif);
transition: font-family 0.3s ease-in-out;
}
// Monospace elements
code, pre, kbd, samp, .mono {
font-family: var(--font-family-mono);
transition: font-family 0.3s ease-in-out;
}
// Sans-serif elements
.sans {
font-family: var(--font-family-sans);
transition: font-family 0.3s ease-in-out;
}
// Display elements
.display {
font-family: var(--font-family-display);
transition: font-family 0.3s ease-in-out;
}