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

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