Restructure layout components architecture
- Move layout components from layouts/ directory to components/layout/ - Reorganize divider component from data-display to layout category - Add comprehensive layout component collection including aspect-ratio, bento-grid, box, breakpoint-container, center, column, dashboard-shell, feed-layout, flex, grid-container, hstack, list-detail-layout, scroll-container, section, sidebar-layout, stack, supporting-pane-layout, tabs-container, and vstack - Update all demo components to match new layout structure - Refactor routing and index exports to reflect reorganized component architecture 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
h2 {
|
||||
font-family: map-get($semantic-typography-heading-h2, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h2, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h2, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h2, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: map-get($semantic-typography-body-large, font-family);
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
font-weight: map-get($semantic-typography-body-large, font-weight);
|
||||
line-height: map-get($semantic-typography-body-large, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-layout-section-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-lg;
|
||||
|
||||
h3 {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
padding-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
button {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border: none;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
|
||||
font-family: map-get($semantic-typography-button-medium, font-family);
|
||||
font-size: map-get($semantic-typography-button-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-button-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-button-medium, line-height);
|
||||
cursor: pointer;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
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);
|
||||
opacity: $semantic-opacity-subtle;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { AspectRatioComponent } from '../../../../../ui-essentials/src/lib/components/layout/aspect-ratio';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-aspect-ratio-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, AspectRatioComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Aspect Ratio Demo</h2>
|
||||
<p>Components for maintaining consistent aspect ratios across responsive content.</p>
|
||||
|
||||
<!-- Common Presets -->
|
||||
<section class="demo-section">
|
||||
<h3>Aspect Ratio Presets</h3>
|
||||
<div class="demo-grid">
|
||||
@for (preset of presets; track preset.ratio) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ preset.label }}</h4>
|
||||
<ui-aspect-ratio [ratio]="preset.ratio">
|
||||
<div class="demo-content" [style.background]="getRandomColor()">
|
||||
<span>{{ preset.ratio }} ({{ preset.description }})</span>
|
||||
</div>
|
||||
</ui-aspect-ratio>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Custom Ratios -->
|
||||
<section class="demo-section">
|
||||
<h3>Custom Aspect Ratios</h3>
|
||||
<div class="demo-grid">
|
||||
@for (custom of customRatios; track custom.ratio) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ custom.label }}</h4>
|
||||
<ui-aspect-ratio [customRatio]="custom.ratio">
|
||||
<div class="demo-content" [style.background]="getRandomColor()">
|
||||
<span>{{ custom.ratio }}</span>
|
||||
</div>
|
||||
</ui-aspect-ratio>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Size Variants</h3>
|
||||
<div class="demo-row">
|
||||
@for (size of sizes; track size) {
|
||||
<div class="demo-item" style="flex: 1;">
|
||||
<h4>Size: {{ size }}</h4>
|
||||
<ui-aspect-ratio ratio="video" [size]="size">
|
||||
<div class="demo-content" style="background: linear-gradient(45deg, #667eea, #764ba2)">
|
||||
<span>{{ size }} size</span>
|
||||
</div>
|
||||
</ui-aspect-ratio>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Style Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Style Variants</h3>
|
||||
<div class="demo-grid">
|
||||
@for (variant of variants; track variant) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ variant | titlecase }}</h4>
|
||||
<ui-aspect-ratio
|
||||
ratio="photo"
|
||||
[variant]="variant"
|
||||
(clicked)="variant === 'interactive' ? handleInteractiveClick($event) : null">
|
||||
<div class="demo-content" [style.background]="getRandomColor()">
|
||||
<span>{{ variant }} variant</span>
|
||||
@if (variant === 'interactive') {
|
||||
<small>Click me!</small>
|
||||
}
|
||||
</div>
|
||||
</ui-aspect-ratio>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- With Images -->
|
||||
<section class="demo-section">
|
||||
<h3>Image Examples</h3>
|
||||
<div class="demo-grid">
|
||||
<div class="demo-item">
|
||||
<h4>Square Image</h4>
|
||||
<ui-aspect-ratio ratio="square" size="lg">
|
||||
<img
|
||||
src="https://picsum.photos/400/400"
|
||||
alt="Square demo image"
|
||||
style="object-fit: cover; width: 100%; height: 100%;">
|
||||
</ui-aspect-ratio>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Video Aspect</h4>
|
||||
<ui-aspect-ratio ratio="video" variant="elevated">
|
||||
<img
|
||||
src="https://picsum.photos/800/450"
|
||||
alt="Video aspect demo image"
|
||||
style="object-fit: cover; width: 100%; height: 100%;">
|
||||
</ui-aspect-ratio>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Portrait Image</h4>
|
||||
<ui-aspect-ratio ratio="portrait" variant="bordered">
|
||||
<img
|
||||
src="https://picsum.photos/400/533"
|
||||
alt="Portrait demo image"
|
||||
style="object-fit: cover; width: 100%; height: 100%;">
|
||||
</ui-aspect-ratio>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Loading State -->
|
||||
<section class="demo-section">
|
||||
<h3>Loading State</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item" style="max-width: 300px;">
|
||||
<h4>Loading</h4>
|
||||
<ui-aspect-ratio ratio="video" [loading]="true">
|
||||
<!-- Content is hidden when loading -->
|
||||
</ui-aspect-ratio>
|
||||
</div>
|
||||
|
||||
<div class="demo-item" style="max-width: 300px;">
|
||||
<button (click)="toggleLoading()">
|
||||
Toggle Loading: {{ isLoading ? 'ON' : 'OFF' }}
|
||||
</button>
|
||||
<ui-aspect-ratio ratio="square" [loading]="isLoading">
|
||||
<div class="demo-content" style="background: linear-gradient(135deg, #ff6b6b, #4ecdc4)">
|
||||
<span>Dynamic Loading</span>
|
||||
</div>
|
||||
</ui-aspect-ratio>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Centered Content -->
|
||||
<section class="demo-section">
|
||||
<h3>Content Centering</h3>
|
||||
<div class="demo-grid">
|
||||
<div class="demo-item">
|
||||
<h4>Fill Container</h4>
|
||||
<ui-aspect-ratio ratio="video" variant="bordered">
|
||||
<div class="demo-content" style="background: linear-gradient(45deg, #fa709a, #fee140)">
|
||||
<span>Fills entire container</span>
|
||||
</div>
|
||||
</ui-aspect-ratio>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Centered Content</h4>
|
||||
<ui-aspect-ratio ratio="video" variant="bordered" [centerContent]="true">
|
||||
<div style="background: #6c5ce7; color: white; padding: 20px; border-radius: 8px; text-align: center;">
|
||||
<span>I'm centered!</span>
|
||||
</div>
|
||||
</ui-aspect-ratio>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Examples</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<h4>Click Counter</h4>
|
||||
<ui-aspect-ratio
|
||||
ratio="square"
|
||||
variant="interactive"
|
||||
size="lg"
|
||||
(clicked)="incrementCounter()">
|
||||
<div class="demo-content" style="background: linear-gradient(135deg, #667eea, #764ba2); cursor: pointer;">
|
||||
<div style="text-align: center; color: white;">
|
||||
<div style="font-size: 2rem; font-weight: bold;">{{ clickCount }}</div>
|
||||
<div>Click to increment</div>
|
||||
</div>
|
||||
</div>
|
||||
</ui-aspect-ratio>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './aspect-ratio-demo.component.scss'
|
||||
})
|
||||
export class AspectRatioDemoComponent {
|
||||
presets = [
|
||||
{ ratio: 'square' as const, label: 'Square', description: '1:1' },
|
||||
{ ratio: 'video' as const, label: 'Video', description: '16:9' },
|
||||
{ ratio: 'cinema' as const, label: 'Cinema', description: '21:9' },
|
||||
{ ratio: 'photo' as const, label: 'Photo', description: '3:2' },
|
||||
{ ratio: 'portrait' as const, label: 'Portrait', description: '3:4' },
|
||||
{ ratio: 'golden' as const, label: 'Golden', description: '1.618:1' }
|
||||
];
|
||||
|
||||
customRatios = [
|
||||
{ ratio: '4/3', label: 'Custom 4:3' },
|
||||
{ ratio: '75%', label: 'Custom 75%' },
|
||||
{ ratio: '1.33', label: 'Custom 1.33' },
|
||||
{ ratio: '125%', label: 'Custom 125%' }
|
||||
];
|
||||
|
||||
sizes = ['sm', 'md', 'lg'] as const;
|
||||
variants = ['default', 'elevated', 'bordered', 'interactive'] as const;
|
||||
|
||||
clickCount = 0;
|
||||
isLoading = false;
|
||||
|
||||
private colors = [
|
||||
'linear-gradient(45deg, #667eea, #764ba2)',
|
||||
'linear-gradient(135deg, #f093fb, #f5576c)',
|
||||
'linear-gradient(45deg, #4facfe, #00f2fe)',
|
||||
'linear-gradient(135deg, #43e97b, #38f9d7)',
|
||||
'linear-gradient(45deg, #fa709a, #fee140)',
|
||||
'linear-gradient(135deg, #a8edea, #fed6e3)',
|
||||
'linear-gradient(45deg, #ffecd2, #fcb69f)',
|
||||
'linear-gradient(135deg, #ff9a9e, #fecfef)'
|
||||
];
|
||||
|
||||
getRandomColor(): string {
|
||||
return this.colors[Math.floor(Math.random() * this.colors.length)];
|
||||
}
|
||||
|
||||
handleInteractiveClick(event: MouseEvent): void {
|
||||
console.log('Interactive aspect ratio clicked', event);
|
||||
}
|
||||
|
||||
incrementCounter(): void {
|
||||
this.clickCount++;
|
||||
}
|
||||
|
||||
toggleLoading(): void {
|
||||
this.isLoading = !this.isLoading;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
h2 {
|
||||
font-family: map-get($semantic-typography-heading-h2, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h2, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h2, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h2, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-lg;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
margin-top: $semantic-spacing-content-paragraph;
|
||||
}
|
||||
|
||||
.demo-variant {
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-primary;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
// Override some demo-specific grid heights for better visualization
|
||||
.demo-section ui-bento-grid {
|
||||
min-height: 200px;
|
||||
|
||||
.demo-variant & {
|
||||
min-height: 150px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { BentoGridComponent, BentoGridItemComponent } from 'ui-essentials';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-bento-grid-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, BentoGridComponent, BentoGridItemComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Bento Grid Demo</h2>
|
||||
|
||||
<!-- Basic Grid -->
|
||||
<section class="demo-section">
|
||||
<h3>Basic Bento Grid</h3>
|
||||
<ui-bento-grid columns="auto-fit-md" gap="md">
|
||||
<ui-bento-grid-item header="Regular Item">
|
||||
Basic grid item with regular content.
|
||||
</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item featured="md" header="Featured Item" variant="primary">
|
||||
This is a featured item that spans 2 columns and 2 rows.
|
||||
</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item>
|
||||
Another regular item without header.
|
||||
</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item [colSpan]="2" variant="elevated" header="Wide Item">
|
||||
This item spans 2 columns wide.
|
||||
</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item [rowSpan]="2" variant="secondary" header="Tall Item">
|
||||
This item spans 2 rows tall.
|
||||
</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item>Regular item</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>Regular item</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item featured="lg" header="Large Featured" variant="elevated">
|
||||
Large featured item spanning 3 columns and 2 rows.
|
||||
</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item>Regular item</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>Regular item</ui-bento-grid-item>
|
||||
</ui-bento-grid>
|
||||
</section>
|
||||
|
||||
<!-- Fixed Columns -->
|
||||
<section class="demo-section">
|
||||
<h3>Fixed Column Grids</h3>
|
||||
|
||||
<h4>4 Columns</h4>
|
||||
<ui-bento-grid [columns]="4" gap="sm">
|
||||
<ui-bento-grid-item>Item 1</ui-bento-grid-item>
|
||||
<ui-bento-grid-item [colSpan]="2">Item 2 (2 cols)</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>Item 3</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>Item 4</ui-bento-grid-item>
|
||||
<ui-bento-grid-item [colSpan]="3">Item 5 (3 cols)</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>Item 6</ui-bento-grid-item>
|
||||
</ui-bento-grid>
|
||||
|
||||
<h4>6 Columns</h4>
|
||||
<ui-bento-grid [columns]="6" gap="xs">
|
||||
@for (item of sixColumnItems; track item.id) {
|
||||
<ui-bento-grid-item
|
||||
[colSpan]="item.colSpan"
|
||||
[rowSpan]="item.rowSpan"
|
||||
[variant]="item.variant"
|
||||
[header]="item.header">
|
||||
{{ item.content }}
|
||||
</ui-bento-grid-item>
|
||||
}
|
||||
</ui-bento-grid>
|
||||
</section>
|
||||
|
||||
<!-- Gap Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Gap Sizes</h3>
|
||||
<div class="demo-row">
|
||||
@for (gap of gaps; track gap) {
|
||||
<div class="demo-variant">
|
||||
<h4>Gap: {{ gap }}</h4>
|
||||
<ui-bento-grid [columns]="3" [gap]="gap">
|
||||
<ui-bento-grid-item>Item A</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>Item B</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>Item C</ui-bento-grid-item>
|
||||
<ui-bento-grid-item [colSpan]="2">Item D (2 cols)</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>Item E</ui-bento-grid-item>
|
||||
</ui-bento-grid>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Featured Sizes -->
|
||||
<section class="demo-section">
|
||||
<h3>Featured Item Sizes</h3>
|
||||
<ui-bento-grid columns="auto-fit-md" gap="md">
|
||||
<ui-bento-grid-item featured="sm" variant="primary" header="Small Featured">
|
||||
2×1 featured item
|
||||
</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item>Regular</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>Regular</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item featured="md" variant="secondary" header="Medium Featured">
|
||||
2×2 featured item
|
||||
</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item>Regular</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>Regular</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>Regular</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item featured="lg" variant="elevated" header="Large Featured">
|
||||
3×2 featured item
|
||||
</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item>Regular</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>Regular</ui-bento-grid-item>
|
||||
</ui-bento-grid>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Items -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Items</h3>
|
||||
<ui-bento-grid columns="auto-fit-md" gap="md">
|
||||
<ui-bento-grid-item
|
||||
[interactive]="true"
|
||||
variant="primary"
|
||||
header="Click Me!"
|
||||
(clicked)="handleItemClick('primary')">
|
||||
Interactive primary item
|
||||
</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item
|
||||
[interactive]="true"
|
||||
[colSpan]="2"
|
||||
variant="secondary"
|
||||
header="Wide Interactive"
|
||||
(clicked)="handleItemClick('secondary')">
|
||||
Wide interactive item
|
||||
</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item
|
||||
[interactive]="true"
|
||||
featured="md"
|
||||
variant="elevated"
|
||||
header="Featured Interactive"
|
||||
(clicked)="handleItemClick('featured')">
|
||||
Featured interactive item
|
||||
</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item>Static item</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>Static item</ui-bento-grid-item>
|
||||
</ui-bento-grid>
|
||||
|
||||
@if (lastClicked) {
|
||||
<p>Last clicked: <strong>{{ lastClicked }}</strong></p>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- Row Height Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Row Heights</h3>
|
||||
<div class="demo-row">
|
||||
@for (rowHeight of rowHeights; track rowHeight) {
|
||||
<div class="demo-variant">
|
||||
<h4>Rows: {{ rowHeight }}</h4>
|
||||
<ui-bento-grid [columns]="3" [rowHeight]="rowHeight" gap="sm">
|
||||
<ui-bento-grid-item>Small content</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>
|
||||
Longer content that might need more space depending on the row height setting.
|
||||
</ui-bento-grid-item>
|
||||
<ui-bento-grid-item [rowSpan]="2">Tall item</ui-bento-grid-item>
|
||||
<ui-bento-grid-item [colSpan]="2">Wide item</ui-bento-grid-item>
|
||||
</ui-bento-grid>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './bento-grid-demo.component.scss'
|
||||
})
|
||||
export class BentoGridDemoComponent {
|
||||
gaps = ['xs', 'sm', 'md', 'lg'] as const;
|
||||
rowHeights = ['sm', 'md', 'lg'] as const;
|
||||
lastClicked = '';
|
||||
|
||||
sixColumnItems = [
|
||||
{ id: 1, colSpan: 2, rowSpan: 1, variant: 'primary', header: 'Wide', content: '2×1 item' },
|
||||
{ id: 2, colSpan: 1, rowSpan: 2, variant: 'secondary', header: 'Tall', content: '1×2 item' },
|
||||
{ id: 3, colSpan: 1, rowSpan: 1, variant: 'default', header: '', content: 'Regular' },
|
||||
{ id: 4, colSpan: 2, rowSpan: 1, variant: 'elevated', header: 'Featured', content: '2×1 item' },
|
||||
{ id: 5, colSpan: 1, rowSpan: 1, variant: 'default', header: '', content: 'Regular' },
|
||||
{ id: 6, colSpan: 3, rowSpan: 1, variant: 'primary', header: 'Extra Wide', content: '3×1 item' },
|
||||
{ id: 7, colSpan: 1, rowSpan: 1, variant: 'default', header: '', content: 'Regular' },
|
||||
{ id: 8, colSpan: 2, rowSpan: 2, variant: 'elevated', header: 'Large', content: '2×2 featured' },
|
||||
{ id: 9, colSpan: 1, rowSpan: 1, variant: 'default', header: '', content: 'Regular' },
|
||||
{ id: 10, colSpan: 1, rowSpan: 1, variant: 'default', header: '', content: 'Regular' },
|
||||
] as const;
|
||||
|
||||
handleItemClick(type: string): void {
|
||||
this.lastClicked = type;
|
||||
console.log('Bento grid item clicked:', type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
h2 {
|
||||
font-family: map-get($semantic-typography-heading-h2, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h2, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h2, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h2, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-layout-section-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
|
||||
h3 {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-column {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.content-box {
|
||||
background: $semantic-color-surface-secondary;
|
||||
}
|
||||
|
||||
.inner-content {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
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-demo-container {
|
||||
background: $semantic-color-surface-elevated;
|
||||
padding: $semantic-spacing-component-xl;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
|
||||
.margin-box {
|
||||
background: $semantic-color-success;
|
||||
color: $semantic-color-on-success;
|
||||
padding: $semantic-spacing-component-md;
|
||||
text-align: center;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
.flex-demo {
|
||||
gap: $semantic-spacing-component-md;
|
||||
|
||||
.flex-item {
|
||||
background: $semantic-color-secondary;
|
||||
color: $semantic-color-on-secondary;
|
||||
padding: $semantic-spacing-component-md;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-demo {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: $semantic-spacing-component-md;
|
||||
|
||||
.grid-item {
|
||||
background: $semantic-color-warning;
|
||||
color: $semantic-color-on-warning;
|
||||
padding: $semantic-spacing-component-md;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
text-align: center;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
.inline-demo {
|
||||
background: $semantic-color-info;
|
||||
color: $semantic-color-on-info;
|
||||
margin-right: $semantic-spacing-component-sm;
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { BoxComponent } from '../../../../../ui-essentials/src/lib/components/layout/box';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-box-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, BoxComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Box Component Demo</h2>
|
||||
|
||||
<!-- Padding Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Padding Utilities</h3>
|
||||
<div class="demo-row">
|
||||
@for (spacing of spacings; track spacing) {
|
||||
<div class="demo-column">
|
||||
<h4>p="{{ spacing }}"</h4>
|
||||
<ui-box [p]="spacing" [border]="true" class="content-box">
|
||||
<div class="inner-content">Content with {{ spacing }} padding</div>
|
||||
</ui-box>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Directional Padding -->
|
||||
<section class="demo-section">
|
||||
<h3>Directional Padding</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-column">
|
||||
<h4>px="lg" (horizontal)</h4>
|
||||
<ui-box px="lg" [border]="true" class="content-box">
|
||||
<div class="inner-content">Horizontal padding</div>
|
||||
</ui-box>
|
||||
</div>
|
||||
|
||||
<div class="demo-column">
|
||||
<h4>py="lg" (vertical)</h4>
|
||||
<ui-box py="lg" [border]="true" class="content-box">
|
||||
<div class="inner-content">Vertical padding</div>
|
||||
</ui-box>
|
||||
</div>
|
||||
|
||||
<div class="demo-column">
|
||||
<h4>pt="xl" pb="sm"</h4>
|
||||
<ui-box pt="xl" pb="sm" [border]="true" class="content-box">
|
||||
<div class="inner-content">Mixed padding</div>
|
||||
</ui-box>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Margin Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Margin Utilities</h3>
|
||||
<div class="margin-demo-container">
|
||||
@for (spacing of marginSpacings; track spacing) {
|
||||
<ui-box [m]="spacing" [border]="true" [rounded]="true" class="margin-box">
|
||||
m="{{ spacing }}"
|
||||
</ui-box>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Display Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Display Options</h3>
|
||||
<div class="demo-column">
|
||||
<h4>display="flex"</h4>
|
||||
<ui-box display="flex" p="md" [border]="true" class="flex-demo">
|
||||
<div class="flex-item">Item 1</div>
|
||||
<div class="flex-item">Item 2</div>
|
||||
<div class="flex-item">Item 3</div>
|
||||
</ui-box>
|
||||
|
||||
<h4>display="grid"</h4>
|
||||
<ui-box display="grid" p="md" [border]="true" class="grid-demo">
|
||||
<div class="grid-item">A</div>
|
||||
<div class="grid-item">B</div>
|
||||
<div class="grid-item">C</div>
|
||||
<div class="grid-item">D</div>
|
||||
</ui-box>
|
||||
|
||||
<h4>display="inline-block"</h4>
|
||||
<div>
|
||||
<ui-box display="inline-block" p="sm" [border]="true" class="inline-demo">Box 1</ui-box>
|
||||
<ui-box display="inline-block" p="sm" [border]="true" class="inline-demo">Box 2</ui-box>
|
||||
<ui-box display="inline-block" p="sm" [border]="true" class="inline-demo">Box 3</ui-box>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Visual Styles -->
|
||||
<section class="demo-section">
|
||||
<h3>Visual Styles</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-column">
|
||||
<h4>Basic</h4>
|
||||
<ui-box p="md">
|
||||
<div class="inner-content">Basic box</div>
|
||||
</ui-box>
|
||||
</div>
|
||||
|
||||
<div class="demo-column">
|
||||
<h4>With Border</h4>
|
||||
<ui-box p="md" [border]="true">
|
||||
<div class="inner-content">Bordered box</div>
|
||||
</ui-box>
|
||||
</div>
|
||||
|
||||
<div class="demo-column">
|
||||
<h4>Rounded</h4>
|
||||
<ui-box p="md" [border]="true" [rounded]="true">
|
||||
<div class="inner-content">Rounded box</div>
|
||||
</ui-box>
|
||||
</div>
|
||||
|
||||
<div class="demo-column">
|
||||
<h4>With Shadow</h4>
|
||||
<ui-box p="md" [shadow]="true">
|
||||
<div class="inner-content">Shadow box</div>
|
||||
</ui-box>
|
||||
</div>
|
||||
|
||||
<div class="demo-column">
|
||||
<h4>All Styles</h4>
|
||||
<ui-box p="lg" [border]="true" [rounded]="true" [shadow]="true">
|
||||
<div class="inner-content">Full styled</div>
|
||||
</ui-box>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Complex Layout Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Complex Layout Example</h3>
|
||||
<ui-box p="lg" [border]="true" [rounded]="true" [shadow]="true">
|
||||
<ui-box mb="md">
|
||||
<h4 style="margin: 0;">Card Header</h4>
|
||||
</ui-box>
|
||||
|
||||
<ui-box display="flex" mb="md">
|
||||
<ui-box pr="md" style="flex: 1;">
|
||||
<div class="inner-content">Left column content with some text that wraps nicely.</div>
|
||||
</ui-box>
|
||||
|
||||
<ui-box pl="md" [border]="true" style="flex: 1;">
|
||||
<div class="inner-content">Right column with border and padding.</div>
|
||||
</ui-box>
|
||||
</ui-box>
|
||||
|
||||
<ui-box pt="md" [border]="true" style="border-left: none; border-right: none; border-bottom: none;">
|
||||
<div class="inner-content">Footer with top border only</div>
|
||||
</ui-box>
|
||||
</ui-box>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './box-demo.component.scss'
|
||||
})
|
||||
export class BoxDemoComponent {
|
||||
spacings = ['xs', 'sm', 'md', 'lg', 'xl'] as const;
|
||||
marginSpacings = ['xs', 'sm', 'md', 'lg'] as const;
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-lg;
|
||||
|
||||
h3 {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
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-content-paragraph;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: grid;
|
||||
gap: $semantic-spacing-grid-gap-md;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
margin-bottom: $semantic-spacing-layout-section-sm;
|
||||
|
||||
@media (max-width: $semantic-breakpoint-sm) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
padding: $semantic-spacing-component-md;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
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);
|
||||
text-align: center;
|
||||
border: $semantic-border-width-2 dashed;
|
||||
|
||||
&--primary {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border-color: $semantic-color-on-primary;
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
background: $semantic-color-secondary;
|
||||
color: $semantic-color-on-secondary;
|
||||
border-color: $semantic-color-on-secondary;
|
||||
}
|
||||
|
||||
&--success {
|
||||
background: $semantic-color-success;
|
||||
color: $semantic-color-on-success;
|
||||
border-color: $semantic-color-on-success;
|
||||
}
|
||||
|
||||
&--warning {
|
||||
background: $semantic-color-warning;
|
||||
color: $semantic-color-on-warning;
|
||||
border-color: $semantic-color-on-warning;
|
||||
}
|
||||
|
||||
&--danger {
|
||||
background: $semantic-color-danger;
|
||||
color: $semantic-color-on-danger;
|
||||
border-color: $semantic-color-on-danger;
|
||||
}
|
||||
|
||||
&--info {
|
||||
background: $semantic-color-info;
|
||||
color: $semantic-color-on-info;
|
||||
border-color: $semantic-color-on-info;
|
||||
}
|
||||
|
||||
&--dark {
|
||||
background: $semantic-color-text-primary;
|
||||
color: $semantic-color-surface-primary;
|
||||
border-color: $semantic-color-surface-primary;
|
||||
}
|
||||
|
||||
&--inline {
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border-color: $semantic-color-on-primary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
.viewport-info {
|
||||
margin: $semantic-spacing-layout-section-sm 0;
|
||||
min-height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.demo-content {
|
||||
font-weight: $semantic-typography-font-weight-bold;
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
padding: $semantic-spacing-component-lg;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
padding-left: $semantic-spacing-component-lg;
|
||||
|
||||
li {
|
||||
margin-bottom: $semantic-spacing-content-list-item;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { BreakpointContainerComponent } from '../../../../../ui-essentials/src/lib/components/layout/breakpoint-container';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-breakpoint-container-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, BreakpointContainerComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Breakpoint Container Demo</h2>
|
||||
<p>This component shows/hides content based on screen size. Resize your browser to see the effects.</p>
|
||||
|
||||
<!-- Basic Visibility Controls -->
|
||||
<section class="demo-section">
|
||||
<h3>Basic Visibility Controls</h3>
|
||||
|
||||
<div class="demo-row">
|
||||
<div class="demo-card">
|
||||
<h4>Always Visible</h4>
|
||||
<ui-breakpoint-container visibility="always">
|
||||
<div class="demo-content demo-content--primary">
|
||||
Always visible content
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Mobile Only (< 768px)</h4>
|
||||
<ui-breakpoint-container visibility="mobile-only">
|
||||
<div class="demo-content demo-content--success">
|
||||
Only visible on mobile devices
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Tablet Only (768px - 1024px)</h4>
|
||||
<ui-breakpoint-container visibility="tablet-only">
|
||||
<div class="demo-content demo-content--warning">
|
||||
Only visible on tablets
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Desktop Only (≥ 1024px)</h4>
|
||||
<ui-breakpoint-container visibility="desktop-only">
|
||||
<div class="demo-content demo-content--info">
|
||||
Only visible on desktop
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Hidden Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Hidden Variants</h3>
|
||||
|
||||
<div class="demo-row">
|
||||
<div class="demo-card">
|
||||
<h4>Mobile Hidden</h4>
|
||||
<ui-breakpoint-container visibility="mobile-hidden">
|
||||
<div class="demo-content demo-content--danger">
|
||||
Hidden on mobile (< 768px)
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Tablet Hidden</h4>
|
||||
<ui-breakpoint-container visibility="tablet-hidden">
|
||||
<div class="demo-content demo-content--secondary">
|
||||
Hidden on tablets (768px - 1024px)
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Desktop Hidden</h4>
|
||||
<ui-breakpoint-container visibility="desktop-hidden">
|
||||
<div class="demo-content demo-content--dark">
|
||||
Hidden on desktop (≥ 1024px)
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Screen Size Based -->
|
||||
<section class="demo-section">
|
||||
<h3>Screen Size Based</h3>
|
||||
|
||||
<div class="demo-row">
|
||||
<div class="demo-card">
|
||||
<h4>Mobile Screen (< 640px)</h4>
|
||||
<ui-breakpoint-container visibility="mobile-screen">
|
||||
<div class="demo-content demo-content--success">
|
||||
Mobile screen size only
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Tablet Screen (640px - 1024px)</h4>
|
||||
<ui-breakpoint-container visibility="tablet-screen">
|
||||
<div class="demo-content demo-content--warning">
|
||||
Tablet screen size only
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Desktop Screen (≥ 1024px)</h4>
|
||||
<ui-breakpoint-container visibility="desktop-screen">
|
||||
<div class="demo-content demo-content--info">
|
||||
Desktop screen size only
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Range Based -->
|
||||
<section class="demo-section">
|
||||
<h3>Range Based Visibility</h3>
|
||||
|
||||
<div class="demo-row">
|
||||
<div class="demo-card">
|
||||
<h4>Small to Medium (640px - 768px)</h4>
|
||||
<ui-breakpoint-container visibility="sm-to-md">
|
||||
<div class="demo-content demo-content--primary">
|
||||
Visible between small and medium breakpoints
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Medium to Large (768px - 1024px)</h4>
|
||||
<ui-breakpoint-container visibility="md-to-lg">
|
||||
<div class="demo-content demo-content--secondary">
|
||||
Visible between medium and large breakpoints
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Custom Breakpoint Rules -->
|
||||
<section class="demo-section">
|
||||
<h3>Custom Breakpoint Rules</h3>
|
||||
|
||||
<div class="demo-row">
|
||||
<div class="demo-card">
|
||||
<h4>Show from Small (≥ 640px)</h4>
|
||||
<ui-breakpoint-container [showSm]="true">
|
||||
<div class="demo-content demo-content--success">
|
||||
Visible from small breakpoint and up
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Hide from Medium (≥ 768px)</h4>
|
||||
<ui-breakpoint-container [hideMd]="true">
|
||||
<div class="demo-content demo-content--danger">
|
||||
Hidden from medium breakpoint and up
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Show from Large (≥ 1024px)</h4>
|
||||
<ui-breakpoint-container [showLg]="true">
|
||||
<div class="demo-content demo-content--info">
|
||||
Visible from large breakpoint and up
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Display Types -->
|
||||
<section class="demo-section">
|
||||
<h3>Display Types</h3>
|
||||
|
||||
<div class="demo-row">
|
||||
<div class="demo-card">
|
||||
<h4>Block Display (Default)</h4>
|
||||
<ui-breakpoint-container displayType="block" visibility="always">
|
||||
<div class="demo-content demo-content--primary">
|
||||
Block display (default)
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Inline Display</h4>
|
||||
<p>
|
||||
This is inline text with
|
||||
<ui-breakpoint-container displayType="inline" visibility="desktop-only">
|
||||
<span class="demo-content demo-content--inline">desktop-only inline content</span>
|
||||
</ui-breakpoint-container>
|
||||
continuing after.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Flex Display</h4>
|
||||
<ui-breakpoint-container displayType="flex" visibility="always">
|
||||
<div class="demo-content demo-content--success" style="flex: 1; margin-right: 8px;">
|
||||
Flex Item 1
|
||||
</div>
|
||||
<div class="demo-content demo-content--warning" style="flex: 1;">
|
||||
Flex Item 2
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Print Media -->
|
||||
<section class="demo-section">
|
||||
<h3>Print Media</h3>
|
||||
|
||||
<div class="demo-row">
|
||||
<div class="demo-card">
|
||||
<h4>Print Hidden</h4>
|
||||
<ui-breakpoint-container [printHidden]="true">
|
||||
<div class="demo-content demo-content--secondary">
|
||||
This content is hidden when printing
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Print Only</h4>
|
||||
<ui-breakpoint-container [printOnly]="true">
|
||||
<div class="demo-content demo-content--dark">
|
||||
This content only appears when printing
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Test</h3>
|
||||
<p>Current viewport info: Resize your browser window to test different breakpoints.</p>
|
||||
|
||||
<div class="viewport-info">
|
||||
<ui-breakpoint-container visibility="mobile-screen">
|
||||
<div class="demo-content demo-content--success">
|
||||
📱 Mobile Screen (< 640px)
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
|
||||
<ui-breakpoint-container visibility="tablet-screen">
|
||||
<div class="demo-content demo-content--warning">
|
||||
📟 Tablet Screen (640px - 1024px)
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
|
||||
<ui-breakpoint-container visibility="desktop-screen">
|
||||
<div class="demo-content demo-content--info">
|
||||
🖥️ Desktop Screen (≥ 1024px)
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
|
||||
<p><strong>Test Instructions:</strong></p>
|
||||
<ul>
|
||||
<li>Resize your browser window to see different content appear/disappear</li>
|
||||
<li>Try the print preview to see print-specific content</li>
|
||||
<li>Different display types maintain their layout characteristics</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './breakpoint-container-demo.component.scss'
|
||||
})
|
||||
export class BreakpointContainerDemoComponent {}
|
||||
@@ -0,0 +1,197 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-lg;
|
||||
|
||||
h3 {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-grid-gap-md;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.demo-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-stack-md;
|
||||
}
|
||||
|
||||
.demo-example {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-boundary {
|
||||
border: 2px dashed $semantic-color-border-secondary;
|
||||
background-color: rgba(128, 128, 128, 0.1);
|
||||
min-height: 100px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
background: $semantic-color-surface-elevated;
|
||||
padding: $semantic-spacing-component-md;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
border: 1px solid $semantic-color-border-primary;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
color: $semantic-color-text-primary;
|
||||
text-align: center;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
.demo-content-small {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
padding: $semantic-spacing-component-xs;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
font-size: $semantic-typography-font-size-sm;
|
||||
font-weight: $semantic-typography-font-weight-medium;
|
||||
}
|
||||
|
||||
.demo-inline {
|
||||
margin: 0 $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
background: $semantic-color-surface-primary;
|
||||
padding: $semantic-spacing-component-xl;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
border: 1px solid $semantic-color-border-subtle;
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
|
||||
h3 {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
}
|
||||
|
||||
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-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-loading {
|
||||
text-align: center;
|
||||
|
||||
.demo-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid $semantic-color-border-subtle;
|
||||
border-top: 4px solid $semantic-color-primary;
|
||||
border-radius: $semantic-border-radius-full;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto $semantic-spacing-component-md auto;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.demo-hero {
|
||||
text-align: center;
|
||||
|
||||
h2 {
|
||||
font-family: map-get($semantic-typography-heading-h2, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h2, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h2, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h2, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: map-get($semantic-typography-body-large, font-family);
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
font-weight: map-get($semantic-typography-body-large, font-weight);
|
||||
line-height: map-get($semantic-typography-body-large, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-component-xl;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
background: $semantic-color-surface-secondary;
|
||||
color: $semantic-color-text-primary;
|
||||
border: 1px solid $semantic-color-border-primary;
|
||||
padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
|
||||
font-family: map-get($semantic-typography-button-medium, font-family);
|
||||
font-size: map-get($semantic-typography-button-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-button-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-button-medium, line-height);
|
||||
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-elevated;
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border-color: $semantic-color-primary;
|
||||
|
||||
&:hover {
|
||||
opacity: $semantic-opacity-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.demo-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.demo-example {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CenterComponent } from '../../../../../ui-essentials/src/lib/components/layout/center';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-center-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, CenterComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Center Demo</h2>
|
||||
|
||||
<!-- Axis Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Center Axis</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-example">
|
||||
<h4>Both Axes (Default)</h4>
|
||||
<ui-center [customMinHeight]="'200px'" class="demo-boundary">
|
||||
<div class="demo-content">Centered Both Ways</div>
|
||||
</ui-center>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Horizontal Only</h4>
|
||||
<ui-center axis="horizontal" [customMinHeight]="'200px'" class="demo-boundary">
|
||||
<div class="demo-content">Centered Horizontally</div>
|
||||
</ui-center>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Vertical Only</h4>
|
||||
<ui-center axis="vertical" [customMinHeight]="'200px'" class="demo-boundary">
|
||||
<div class="demo-content">Centered Vertically</div>
|
||||
</ui-center>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Max Width Constraints -->
|
||||
<section class="demo-section">
|
||||
<h3>Max Width Constraints</h3>
|
||||
<div class="demo-column">
|
||||
<div class="demo-example">
|
||||
<h4>Small (640px)</h4>
|
||||
<ui-center maxWidth="sm" padding="md" class="demo-boundary">
|
||||
<div class="demo-content">
|
||||
This content is constrained to a maximum width of 640px and will be centered within its container.
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore.
|
||||
</div>
|
||||
</ui-center>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Medium (768px)</h4>
|
||||
<ui-center maxWidth="md" padding="md" class="demo-boundary">
|
||||
<div class="demo-content">
|
||||
This content is constrained to a maximum width of 768px and will be centered within its container.
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
</div>
|
||||
</ui-center>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Large (1024px)</h4>
|
||||
<ui-center maxWidth="lg" padding="md" class="demo-boundary">
|
||||
<div class="demo-content">
|
||||
This content is constrained to a maximum width of 1024px and will be centered within its container.
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.
|
||||
</div>
|
||||
</ui-center>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Spacing Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Spacing</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-example">
|
||||
<h4>Small Padding</h4>
|
||||
<ui-center padding="sm" class="demo-boundary">
|
||||
<div class="demo-content">Small Padding</div>
|
||||
</ui-center>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Medium Padding</h4>
|
||||
<ui-center padding="md" class="demo-boundary">
|
||||
<div class="demo-content">Medium Padding</div>
|
||||
</ui-center>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Large Padding</h4>
|
||||
<ui-center padding="lg" class="demo-boundary">
|
||||
<div class="demo-content">Large Padding</div>
|
||||
</ui-center>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Inline Variant -->
|
||||
<section class="demo-section">
|
||||
<h3>Inline Center</h3>
|
||||
<div class="demo-example">
|
||||
<p>
|
||||
Here is some text with an
|
||||
<ui-center [inline]="true" class="demo-inline">
|
||||
<span class="demo-content-small">inline centered element</span>
|
||||
</ui-center>
|
||||
in the middle of the paragraph.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Practical Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Practical Examples</h3>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Card Layout</h4>
|
||||
<ui-center maxWidth="md" padding="lg">
|
||||
<div class="demo-card">
|
||||
<h3>Welcome Message</h3>
|
||||
<p>This card is centered with constrained width for better readability.</p>
|
||||
<button class="demo-button">Get Started</button>
|
||||
</div>
|
||||
</ui-center>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Loading State</h4>
|
||||
<ui-center [customMinHeight]="'300px'">
|
||||
<div class="demo-loading">
|
||||
<div class="demo-spinner"></div>
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
</ui-center>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Hero Section</h4>
|
||||
<ui-center [customMinHeight]="'400px'" maxWidth="lg" padding="xl">
|
||||
<div class="demo-hero">
|
||||
<h2>Hero Title</h2>
|
||||
<p>A compelling hero message that's perfectly centered and constrained for optimal reading.</p>
|
||||
<button class="demo-button demo-button--primary">Call to Action</button>
|
||||
</div>
|
||||
</ui-center>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './center-demo.component.scss'
|
||||
})
|
||||
export class CenterDemoComponent {}
|
||||
@@ -0,0 +1,149 @@
|
||||
.demo-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 3rem;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 1.5rem;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
color: #555;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #666;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
border: 1px solid #f0f0f0;
|
||||
padding: 1rem;
|
||||
background: #fafafa;
|
||||
border-radius: 4px;
|
||||
min-height: 200px;
|
||||
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.6;
|
||||
text-align: justify;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 1rem 0;
|
||||
padding-left: 1.5rem;
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-note {
|
||||
background: #e3f2fd;
|
||||
border: 1px solid #90caf9;
|
||||
border-radius: 4px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #1565c0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1rem;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e0e0e0;
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
|
||||
&:has(input[type="checkbox"]) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-select {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
font-size: 0.9rem;
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid #2196f3;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.demo-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
flex-direction: column;
|
||||
|
||||
label {
|
||||
&:has(input[type="checkbox"]) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ColumnComponent } from '../../../../../ui-essentials/src/lib/components/layout/column';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-column-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, ColumnComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Column Layout Demo</h2>
|
||||
|
||||
<!-- Column Count Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Column Counts</h3>
|
||||
<div class="demo-row">
|
||||
@for (count of columnCounts; track count) {
|
||||
<div class="demo-card">
|
||||
<h4>{{ count }} Column{{ count !== '1' ? 's' : '' }}</h4>
|
||||
<ui-column [count]="count" [gap]="'md'" class="demo-content">
|
||||
<p>{{ sampleText }}</p>
|
||||
</ui-column>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Gap Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Gap Sizes (2 Columns)</h3>
|
||||
<div class="demo-row">
|
||||
@for (gap of gaps; track gap) {
|
||||
<div class="demo-card">
|
||||
<h4>Gap: {{ gap }}</h4>
|
||||
<ui-column [count]="'2'" [gap]="gap" class="demo-content">
|
||||
<p>{{ shortText }}</p>
|
||||
</ui-column>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Rule Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Column Rules (3 Columns)</h3>
|
||||
<div class="demo-row">
|
||||
@for (rule of rules; track rule) {
|
||||
<div class="demo-card">
|
||||
<h4>Rule: {{ rule }}</h4>
|
||||
<ui-column [count]="'3'" [gap]="'lg'" [rule]="rule" [ruleWidth]="'2'" class="demo-content">
|
||||
<p>{{ mediumText }}</p>
|
||||
</ui-column>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Fill Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Fill Types (3 Columns)</h3>
|
||||
<div class="demo-row">
|
||||
@for (fill of fills; track fill) {
|
||||
<div class="demo-card">
|
||||
<h4>Fill: {{ fill }}</h4>
|
||||
<ui-column [count]="'3'" [gap]="'md'" [fill]="fill" class="demo-content">
|
||||
<p>{{ variableText }}</p>
|
||||
</ui-column>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Responsive Behavior -->
|
||||
<section class="demo-section">
|
||||
<h3>Responsive Layout</h3>
|
||||
<p class="demo-note">Resize your browser to see responsive column behavior</p>
|
||||
<div class="demo-card">
|
||||
<h4>Responsive 4-Column Layout</h4>
|
||||
<ui-column [count]="'4'" [gap]="'md'" [responsive]="true" [rule]="'solid'" class="demo-content">
|
||||
<h5>Section 1</h5>
|
||||
<p>{{ sampleText }}</p>
|
||||
<h5>Section 2</h5>
|
||||
<p>{{ shortText }}</p>
|
||||
<h5>Section 3</h5>
|
||||
<p>{{ mediumText }}</p>
|
||||
<h5>Section 4</h5>
|
||||
<p>{{ variableText }}</p>
|
||||
</ui-column>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Real-world Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Article Layout Example</h3>
|
||||
<div class="demo-card">
|
||||
<ui-column [count]="'2'" [gap]="'xl'" [rule]="'solid'" [ruleWidth]="'1'" class="demo-content">
|
||||
<h4>The Future of Web Development</h4>
|
||||
<p>{{ longText }}</p>
|
||||
<p>{{ mediumText }}</p>
|
||||
<h5>Key Benefits</h5>
|
||||
<ul>
|
||||
<li>Improved readability through better text flow</li>
|
||||
<li>Enhanced visual hierarchy with column structures</li>
|
||||
<li>Responsive design that adapts to screen sizes</li>
|
||||
<li>Better content organization for long-form text</li>
|
||||
</ul>
|
||||
<p>{{ shortText }}</p>
|
||||
</ui-column>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Controls -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Example</h3>
|
||||
<div class="demo-controls">
|
||||
<label>
|
||||
Columns:
|
||||
<select [(ngModel)]="selectedCount" class="demo-select">
|
||||
@for (count of columnCounts; track count) {
|
||||
<option [value]="count">{{ count }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Gap:
|
||||
<select [(ngModel)]="selectedGap" class="demo-select">
|
||||
@for (gap of gaps; track gap) {
|
||||
<option [value]="gap">{{ gap }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Rule:
|
||||
<select [(ngModel)]="selectedRule" class="demo-select">
|
||||
@for (rule of rules; track rule) {
|
||||
<option [value]="rule">{{ rule }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="selectedResponsive">
|
||||
Responsive
|
||||
</label>
|
||||
</div>
|
||||
<div class="demo-card">
|
||||
<ui-column
|
||||
[count]="selectedCount"
|
||||
[gap]="selectedGap"
|
||||
[rule]="selectedRule"
|
||||
[ruleWidth]="'2'"
|
||||
[responsive]="selectedResponsive"
|
||||
class="demo-content">
|
||||
<h4>Interactive Column Layout</h4>
|
||||
<p>{{ sampleText }}</p>
|
||||
<p>{{ mediumText }}</p>
|
||||
<p>{{ shortText }}</p>
|
||||
</ui-column>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './column-demo.component.scss'
|
||||
})
|
||||
export class ColumnDemoComponent {
|
||||
columnCounts = ['1', '2', '3', '4', '5', '6'] as const;
|
||||
gaps = ['xs', 'sm', 'md', 'lg', 'xl'] as const;
|
||||
rules = ['none', 'solid', 'dashed', 'dotted'] as const;
|
||||
fills = ['auto', 'balance', 'balance-all'] as const;
|
||||
|
||||
// Interactive controls
|
||||
selectedCount = '3' as const;
|
||||
selectedGap = 'md' as const;
|
||||
selectedRule = 'solid' as const;
|
||||
selectedResponsive = true;
|
||||
|
||||
// Sample text content
|
||||
shortText = "This is a short paragraph to demonstrate column layout behavior with minimal text content.";
|
||||
|
||||
mediumText = "This is a medium-length paragraph that provides a good example of how text flows within column layouts. It contains enough content to show wrapping and distribution across multiple columns while remaining readable.";
|
||||
|
||||
sampleText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.";
|
||||
|
||||
longText = "Web development has evolved significantly over the past decade, with new frameworks, tools, and methodologies emerging to help developers create more efficient and maintainable applications. Modern web development practices emphasize component-based architecture, responsive design, and accessibility as core principles. The adoption of TypeScript has brought static typing to JavaScript, reducing runtime errors and improving developer productivity. CSS Grid and Flexbox have revolutionized layout design, making it easier to create complex, responsive layouts without relying on external frameworks.";
|
||||
|
||||
variableText = "This text demonstrates variable length content. Some paragraphs are longer, others shorter. This variation helps test how well the column layout handles different content lengths and maintains visual balance across columns.";
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
.demo-container {
|
||||
padding: 24px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 48px;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 24px;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
|
||||
h4 {
|
||||
background: #f7fafc;
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-demo-container {
|
||||
height: 400px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #f8f9fa;
|
||||
|
||||
&--interactive {
|
||||
height: 600px;
|
||||
border: 2px solid #4299e1;
|
||||
border-radius: 8px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #4a5568;
|
||||
|
||||
select,
|
||||
input[type="checkbox"] {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #d2d6dc;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid #4299e1;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: auto;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Demo content styling for dashboard shell slots
|
||||
.demo-content {
|
||||
padding: 20px;
|
||||
|
||||
h4, h5 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.6;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-bottom: 16px;
|
||||
|
||||
li {
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.5;
|
||||
color: #4a5568;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sidebar demo styling
|
||||
nav[slot="sidebar"] {
|
||||
h5 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
h6 {
|
||||
margin: 16px 0 8px 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #4a5568;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
margin-bottom: 8px;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: #4a5568;
|
||||
|
||||
&:hover {
|
||||
background: #edf2f7;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #4299e1;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-section {
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
}
|
||||
|
||||
// Demo buttons
|
||||
.demo-button {
|
||||
background: #4299e1;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&:hover {
|
||||
background: #3182ce;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid #4299e1;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&.primary {
|
||||
background: #38a169;
|
||||
|
||||
&:hover {
|
||||
background: #2f855a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-button-sm {
|
||||
background: transparent;
|
||||
color: #4a5568;
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f7fafc;
|
||||
border-color: #cbd5e0;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid #4299e1;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// Content area styling
|
||||
.content-area {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.metric-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: #ffffff;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
color: #2d3748;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.chart-placeholder {
|
||||
background: #ffffff;
|
||||
padding: 40px;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
color: #a0aec0;
|
||||
font-size: 1.125rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.user-list,
|
||||
.project-list {
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e2e8f0;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.user-item,
|
||||
.project-item {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
color: #4a5568;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-groups {
|
||||
background: #ffffff;
|
||||
padding: 20px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e2e8f0;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.setting-group {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h6 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #4a5568;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
// Notification styling
|
||||
.notification-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: #ebf8ff;
|
||||
border: 1px solid #bee3f8;
|
||||
border-radius: 6px;
|
||||
color: #2b6cb0;
|
||||
font-size: 0.875rem;
|
||||
|
||||
.dismiss-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #2b6cb0;
|
||||
cursor: pointer;
|
||||
font-size: 1.125rem;
|
||||
padding: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
background: #bee3f8;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Footer styling
|
||||
.footer-detailed {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
.footer-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage information
|
||||
.usage-info {
|
||||
background: #f8f9fa;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
color: #2d3748;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-bottom: 24px;
|
||||
|
||||
li {
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.6;
|
||||
color: #4a5568;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
background: #edf2f7;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Consolas', monospace;
|
||||
font-size: 0.875rem;
|
||||
color: #2d3748;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.demo-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-demo-container {
|
||||
&--interactive {
|
||||
height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
label {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.demo-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.dashboard-demo-container {
|
||||
height: 300px;
|
||||
|
||||
&--interactive {
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,557 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { DashboardShellComponent } from '../../../../../ui-essentials/src/lib/components/layout/dashboard-shell/dashboard-shell.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-dashboard-shell-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, DashboardShellComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Dashboard Shell Demo</h2>
|
||||
|
||||
<!-- Variant Demonstrations -->
|
||||
<section class="demo-section">
|
||||
<h3>Visual Variants</h3>
|
||||
<div class="demo-row">
|
||||
@for (variant of variants; track variant) {
|
||||
<div class="demo-card">
|
||||
<h4>{{ variant | titlecase }}</h4>
|
||||
<div class="dashboard-demo-container">
|
||||
<ui-dashboard-shell
|
||||
[variant]="variant"
|
||||
sidebarWidth="sm"
|
||||
headerHeight="sm"
|
||||
footerVariant="minimal">
|
||||
|
||||
<div slot="header">
|
||||
<h4>{{ variant | titlecase }} App</h4>
|
||||
</div>
|
||||
|
||||
<div slot="header-actions">
|
||||
<span>🌙</span>
|
||||
<span>🔔</span>
|
||||
<span>👤</span>
|
||||
</div>
|
||||
|
||||
<nav slot="sidebar">
|
||||
<ul>
|
||||
<li>🏠 Dashboard</li>
|
||||
<li>📊 Analytics</li>
|
||||
<li>⚙️ Settings</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<nav slot="breadcrumbs">
|
||||
<span>Home / Dashboard / {{ variant | titlecase }}</span>
|
||||
</nav>
|
||||
|
||||
<div slot="notifications">
|
||||
<p>📢 Welcome to {{ variant }} variant!</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<h4>Main Content</h4>
|
||||
<p>This is the main content area for {{ variant }} dashboard variant.</p>
|
||||
</div>
|
||||
|
||||
<div slot="footer">
|
||||
<span>© 2024 Dashboard App</span>
|
||||
</div>
|
||||
</ui-dashboard-shell>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Size Configurations</h3>
|
||||
<div class="demo-row">
|
||||
@for (config of sizeConfigs; track config.name) {
|
||||
<div class="demo-card">
|
||||
<h4>{{ config.name }}</h4>
|
||||
<div class="dashboard-demo-container">
|
||||
<ui-dashboard-shell
|
||||
[sidebarWidth]="config.sidebarWidth"
|
||||
[headerHeight]="config.headerHeight"
|
||||
[footerVariant]="config.footerVariant">
|
||||
|
||||
<div slot="header">
|
||||
<h4>{{ config.name }} Dashboard</h4>
|
||||
</div>
|
||||
|
||||
<div slot="header-actions">
|
||||
<span>🔍</span>
|
||||
<span>📱</span>
|
||||
</div>
|
||||
|
||||
<nav slot="sidebar">
|
||||
<h5>Navigation</h5>
|
||||
<ul>
|
||||
<li>📈 Overview</li>
|
||||
<li>📋 Tasks</li>
|
||||
<li>💼 Projects</li>
|
||||
<li>👥 Team</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<nav slot="breadcrumbs">
|
||||
<span>Dashboard / {{ config.name }} Layout</span>
|
||||
</nav>
|
||||
|
||||
<div class="demo-content">
|
||||
<h4>{{ config.name }} Content</h4>
|
||||
<p>Sidebar: {{ config.sidebarWidth }}, Header: {{ config.headerHeight }}, Footer: {{ config.footerVariant }}</p>
|
||||
</div>
|
||||
|
||||
<div slot="footer">
|
||||
<span>{{ config.name }} Footer</span>
|
||||
<span>v1.0.0</span>
|
||||
</div>
|
||||
</ui-dashboard-shell>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Layout Options -->
|
||||
<section class="demo-section">
|
||||
<h3>Layout Options</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-card">
|
||||
<h4>No Footer</h4>
|
||||
<div class="dashboard-demo-container">
|
||||
<ui-dashboard-shell
|
||||
[showFooter]="false"
|
||||
sidebarWidth="sm">
|
||||
|
||||
<div slot="header">
|
||||
<h4>Clean Dashboard</h4>
|
||||
</div>
|
||||
|
||||
<div slot="header-actions">
|
||||
<span>🎨</span>
|
||||
</div>
|
||||
|
||||
<nav slot="sidebar">
|
||||
<ul>
|
||||
<li>🖼️ Gallery</li>
|
||||
<li>🎵 Music</li>
|
||||
<li>📝 Notes</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="demo-content">
|
||||
<h4>Clean Layout</h4>
|
||||
<p>Dashboard without footer for cleaner appearance.</p>
|
||||
</div>
|
||||
</ui-dashboard-shell>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Minimal UI</h4>
|
||||
<div class="dashboard-demo-container">
|
||||
<ui-dashboard-shell
|
||||
[showBreadcrumbs]="false"
|
||||
[showNotifications]="false"
|
||||
sidebarWidth="sm"
|
||||
footerVariant="minimal">
|
||||
|
||||
<div slot="header">
|
||||
<h4>Minimal App</h4>
|
||||
</div>
|
||||
|
||||
<div slot="header-actions">
|
||||
<span>⚡</span>
|
||||
</div>
|
||||
|
||||
<nav slot="sidebar">
|
||||
<ul>
|
||||
<li>🚀 Launch</li>
|
||||
<li>📊 Stats</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="demo-content">
|
||||
<h4>Minimal Interface</h4>
|
||||
<p>Streamlined dashboard without breadcrumbs and notifications.</p>
|
||||
</div>
|
||||
|
||||
<div slot="footer">
|
||||
<span>Minimal</span>
|
||||
</div>
|
||||
</ui-dashboard-shell>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Dashboard</h3>
|
||||
<div class="demo-controls">
|
||||
<label>
|
||||
Variant:
|
||||
<select [(ngModel)]="interactiveVariant">
|
||||
<option value="default">Default</option>
|
||||
<option value="bordered">Bordered</option>
|
||||
<option value="elevated">Elevated</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Sidebar Width:
|
||||
<select [(ngModel)]="interactiveSidebarWidth">
|
||||
<option value="sm">Small</option>
|
||||
<option value="md">Medium</option>
|
||||
<option value="lg">Large</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Header Height:
|
||||
<select [(ngModel)]="interactiveHeaderHeight">
|
||||
<option value="sm">Small</option>
|
||||
<option value="md">Medium</option>
|
||||
<option value="lg">Large</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Footer Variant:
|
||||
<select [(ngModel)]="interactiveFooterVariant">
|
||||
<option value="minimal">Minimal</option>
|
||||
<option value="standard">Standard</option>
|
||||
<option value="detailed">Detailed</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="interactiveSidebarCollapsed">
|
||||
Sidebar Collapsed
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="interactiveMobileMenuOpen">
|
||||
Mobile Menu Open
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="interactiveShowFooter">
|
||||
Show Footer
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="interactiveShowBreadcrumbs">
|
||||
Show Breadcrumbs
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="interactiveShowNotifications">
|
||||
Show Notifications
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-demo-container dashboard-demo-container--interactive">
|
||||
<ui-dashboard-shell
|
||||
[variant]="interactiveVariant"
|
||||
[sidebarWidth]="interactiveSidebarWidth"
|
||||
[headerHeight]="interactiveHeaderHeight"
|
||||
[footerVariant]="interactiveFooterVariant"
|
||||
[sidebarCollapsed]="interactiveSidebarCollapsed"
|
||||
[mobileMenuOpen]="interactiveMobileMenuOpen"
|
||||
[showFooter]="interactiveShowFooter"
|
||||
[showBreadcrumbs]="interactiveShowBreadcrumbs"
|
||||
[showNotifications]="interactiveShowNotifications"
|
||||
(sidebarToggled)="handleSidebarToggle($event)"
|
||||
(mobileMenuToggled)="handleMobileMenuToggle($event)"
|
||||
(mobileBackdropClicked)="handleMobileBackdropClick()">
|
||||
|
||||
<div slot="header">
|
||||
<h4>Interactive Dashboard</h4>
|
||||
</div>
|
||||
|
||||
<div slot="header-actions">
|
||||
<button (click)="toggleTheme()" class="demo-button-sm">
|
||||
{{ isDarkTheme ? '☀️' : '🌙' }}
|
||||
</button>
|
||||
<button (click)="showNotification()" class="demo-button-sm">
|
||||
🔔 {{ notificationCount }}
|
||||
</button>
|
||||
<button (click)="toggleSidebar()" class="demo-button-sm">
|
||||
{{ interactiveSidebarCollapsed ? '➡️' : '⬅️' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav slot="sidebar">
|
||||
<h5>Main Navigation</h5>
|
||||
<ul>
|
||||
<li (click)="setActiveSection('dashboard')">
|
||||
<span [class.active]="activeSection === 'dashboard'">🏠 Dashboard</span>
|
||||
</li>
|
||||
<li (click)="setActiveSection('analytics')">
|
||||
<span [class.active]="activeSection === 'analytics'">📊 Analytics</span>
|
||||
</li>
|
||||
<li (click)="setActiveSection('users')">
|
||||
<span [class.active]="activeSection === 'users'">👥 Users</span>
|
||||
</li>
|
||||
<li (click)="setActiveSection('projects')">
|
||||
<span [class.active]="activeSection === 'projects'">💼 Projects</span>
|
||||
</li>
|
||||
<li (click)="setActiveSection('settings')">
|
||||
<span [class.active]="activeSection === 'settings'">⚙️ Settings</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@if (!interactiveSidebarCollapsed) {
|
||||
<div class="sidebar-section">
|
||||
<h6>Quick Actions</h6>
|
||||
<button (click)="performAction('new')" class="demo-button">New Project</button>
|
||||
<button (click)="performAction('export')" class="demo-button">Export Data</button>
|
||||
</div>
|
||||
}
|
||||
</nav>
|
||||
|
||||
<nav slot="breadcrumbs">
|
||||
<span>Home / {{ activeSection | titlecase }} / {{ interactiveVariant | titlecase }}</span>
|
||||
</nav>
|
||||
|
||||
<div slot="notifications">
|
||||
@if (hasNewNotifications) {
|
||||
<div class="notification-item">
|
||||
<span>🎉 New feature available! Click to explore the {{ activeSection }} section.</span>
|
||||
<button (click)="dismissNotification()" class="dismiss-btn">×</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<h4>{{ activeSection | titlecase }} Section</h4>
|
||||
<p>Current configuration:</p>
|
||||
<ul>
|
||||
<li><strong>Variant:</strong> {{ interactiveVariant }}</li>
|
||||
<li><strong>Sidebar Width:</strong> {{ interactiveSidebarWidth }}</li>
|
||||
<li><strong>Header Height:</strong> {{ interactiveHeaderHeight }}</li>
|
||||
<li><strong>Footer Variant:</strong> {{ interactiveFooterVariant }}</li>
|
||||
<li><strong>Sidebar Collapsed:</strong> {{ interactiveSidebarCollapsed ? 'Yes' : 'No' }}</li>
|
||||
<li><strong>Theme:</strong> {{ isDarkTheme ? 'Dark' : 'Light' }}</li>
|
||||
</ul>
|
||||
|
||||
<div class="content-area">
|
||||
<h5>{{ activeSection | titlecase }} Content</h5>
|
||||
@switch (activeSection) {
|
||||
@case ('dashboard') {
|
||||
<p>📈 Welcome to your dashboard overview. Here you can see key metrics and quick actions.</p>
|
||||
<div class="metric-cards">
|
||||
<div class="metric-card">Total Users: 1,234</div>
|
||||
<div class="metric-card">Active Projects: 42</div>
|
||||
<div class="metric-card">Revenue: $12,345</div>
|
||||
</div>
|
||||
}
|
||||
@case ('analytics') {
|
||||
<p>📊 Analytics and reporting section. View detailed insights about your application usage.</p>
|
||||
<div class="chart-placeholder">📈 Chart Placeholder</div>
|
||||
}
|
||||
@case ('users') {
|
||||
<p>👥 User management section. Manage user accounts, permissions, and profiles.</p>
|
||||
<div class="user-list">
|
||||
<div class="user-item">John Doe - Admin</div>
|
||||
<div class="user-item">Jane Smith - User</div>
|
||||
<div class="user-item">Bob Johnson - Manager</div>
|
||||
</div>
|
||||
}
|
||||
@case ('projects') {
|
||||
<p>💼 Project management section. Track and manage your ongoing projects.</p>
|
||||
<div class="project-list">
|
||||
<div class="project-item">🚀 Website Redesign - In Progress</div>
|
||||
<div class="project-item">📱 Mobile App - Planning</div>
|
||||
<div class="project-item">🔧 API Integration - Completed</div>
|
||||
</div>
|
||||
}
|
||||
@case ('settings') {
|
||||
<p>⚙️ Application settings and configuration options.</p>
|
||||
<div class="settings-groups">
|
||||
<div class="setting-group">
|
||||
<h6>Appearance</h6>
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="isDarkTheme">
|
||||
Dark Theme
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<h6>Notifications</h6>
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="emailNotifications">
|
||||
Email Notifications
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@default {
|
||||
<p>Select a section from the sidebar to view its content.</p>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button (click)="performAction('save')" class="demo-button primary">Save Changes</button>
|
||||
<button (click)="performAction('cancel')" class="demo-button">Cancel</button>
|
||||
<button (click)="performAction('refresh')" class="demo-button">Refresh Data</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="footer">
|
||||
@if (interactiveFooterVariant === 'detailed') {
|
||||
<div class="footer-detailed">
|
||||
<div class="footer-section">
|
||||
<span>© 2024 Interactive Dashboard</span>
|
||||
<span>Version 1.2.3</span>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<span>Last Updated: {{ lastUpdated }}</span>
|
||||
<span>Status: {{ connectionStatus }}</span>
|
||||
</div>
|
||||
</div>
|
||||
} @else if (interactiveFooterVariant === 'standard') {
|
||||
<span>© 2024 Interactive Dashboard - v1.2.3</span>
|
||||
<span>Status: {{ connectionStatus }}</span>
|
||||
} @else {
|
||||
<span>© 2024 Dashboard</span>
|
||||
}
|
||||
</div>
|
||||
</ui-dashboard-shell>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Usage Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Usage Information</h3>
|
||||
<div class="usage-info">
|
||||
<h4>Key Features:</h4>
|
||||
<ul>
|
||||
<li>🏗️ Complete application shell structure</li>
|
||||
<li>📱 Mobile-responsive with hamburger menu</li>
|
||||
<li>🎨 Multiple visual variants (default, bordered, elevated)</li>
|
||||
<li>📏 Configurable sizing for header, sidebar, and footer</li>
|
||||
<li>🧭 Integrated breadcrumbs navigation</li>
|
||||
<li>🔔 Notifications area with live region support</li>
|
||||
<li>♿ Full accessibility support with ARIA labels</li>
|
||||
<li>🎛️ Flexible content projection slots</li>
|
||||
<li>⚡ Event emitters for user interactions</li>
|
||||
</ul>
|
||||
|
||||
<h4>Content Projection Slots:</h4>
|
||||
<ul>
|
||||
<li><code>slot="header"</code> - Header title and branding</li>
|
||||
<li><code>slot="header-actions"</code> - Theme switcher, notifications, user menu</li>
|
||||
<li><code>slot="sidebar"</code> - Main navigation content</li>
|
||||
<li><code>slot="breadcrumbs"</code> - Breadcrumb navigation</li>
|
||||
<li><code>slot="notifications"</code> - System notifications and alerts</li>
|
||||
<li><code>slot="footer"</code> - Footer content and links</li>
|
||||
<li>Default slot - Main page content</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './dashboard-shell-demo.component.scss'
|
||||
})
|
||||
export class DashboardShellDemoComponent {
|
||||
variants = ['default', 'bordered', 'elevated'] as const;
|
||||
|
||||
sizeConfigs = [
|
||||
{ name: 'Compact', sidebarWidth: 'sm' as const, headerHeight: 'sm' as const, footerVariant: 'minimal' as const },
|
||||
{ name: 'Standard', sidebarWidth: 'md' as const, headerHeight: 'md' as const, footerVariant: 'standard' as const },
|
||||
{ name: 'Spacious', sidebarWidth: 'lg' as const, headerHeight: 'lg' as const, footerVariant: 'detailed' as const }
|
||||
];
|
||||
|
||||
// Interactive demo properties
|
||||
interactiveVariant: 'default' | 'bordered' | 'elevated' = 'default';
|
||||
interactiveSidebarWidth: 'sm' | 'md' | 'lg' = 'md';
|
||||
interactiveHeaderHeight: 'sm' | 'md' | 'lg' = 'md';
|
||||
interactiveFooterVariant: 'minimal' | 'standard' | 'detailed' = 'standard';
|
||||
interactiveSidebarCollapsed = false;
|
||||
interactiveMobileMenuOpen = false;
|
||||
interactiveShowFooter = true;
|
||||
interactiveShowBreadcrumbs = true;
|
||||
interactiveShowNotifications = true;
|
||||
|
||||
// Demo state
|
||||
activeSection = 'dashboard';
|
||||
isDarkTheme = false;
|
||||
notificationCount = 3;
|
||||
hasNewNotifications = true;
|
||||
emailNotifications = true;
|
||||
lastUpdated = new Date().toLocaleTimeString();
|
||||
connectionStatus = 'Connected';
|
||||
|
||||
handleSidebarToggle(collapsed: boolean): void {
|
||||
this.interactiveSidebarCollapsed = collapsed;
|
||||
console.log('Sidebar toggled:', collapsed);
|
||||
}
|
||||
|
||||
handleMobileMenuToggle(open: boolean): void {
|
||||
this.interactiveMobileMenuOpen = open;
|
||||
console.log('Mobile menu toggled:', open);
|
||||
}
|
||||
|
||||
handleMobileBackdropClick(): void {
|
||||
this.interactiveMobileMenuOpen = false;
|
||||
console.log('Mobile backdrop clicked - closing menu');
|
||||
}
|
||||
|
||||
toggleSidebar(): void {
|
||||
this.interactiveSidebarCollapsed = !this.interactiveSidebarCollapsed;
|
||||
}
|
||||
|
||||
toggleTheme(): void {
|
||||
this.isDarkTheme = !this.isDarkTheme;
|
||||
console.log('Theme toggled:', this.isDarkTheme ? 'dark' : 'light');
|
||||
}
|
||||
|
||||
showNotification(): void {
|
||||
this.notificationCount++;
|
||||
this.hasNewNotifications = true;
|
||||
console.log('New notification added');
|
||||
}
|
||||
|
||||
dismissNotification(): void {
|
||||
this.hasNewNotifications = false;
|
||||
console.log('Notification dismissed');
|
||||
}
|
||||
|
||||
setActiveSection(section: string): void {
|
||||
this.activeSection = section;
|
||||
this.lastUpdated = new Date().toLocaleTimeString();
|
||||
console.log('Active section changed to:', section);
|
||||
}
|
||||
|
||||
performAction(action: string): void {
|
||||
console.log('Action performed:', action);
|
||||
this.lastUpdated = new Date().toLocaleTimeString();
|
||||
|
||||
switch (action) {
|
||||
case 'save':
|
||||
this.connectionStatus = 'Saving...';
|
||||
setTimeout(() => {
|
||||
this.connectionStatus = 'Saved';
|
||||
setTimeout(() => this.connectionStatus = 'Connected', 2000);
|
||||
}, 1000);
|
||||
break;
|
||||
case 'refresh':
|
||||
this.connectionStatus = 'Refreshing...';
|
||||
setTimeout(() => this.connectionStatus = 'Connected', 1500);
|
||||
break;
|
||||
case 'new':
|
||||
case 'export':
|
||||
case 'cancel':
|
||||
// Handle other actions
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ 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 { LayoutDemoComponent } from './layout-demo/layout-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';
|
||||
@@ -61,6 +60,23 @@ import { TransferListDemoComponent } from './transfer-list-demo/transfer-list-de
|
||||
import { FloatingToolbarDemoComponent } from './floating-toolbar-demo/floating-toolbar-demo.component';
|
||||
import { TagInputDemoComponent } from './tag-input-demo/tag-input-demo.component';
|
||||
import { IconButtonDemoComponent } from './icon-button-demo/icon-button-demo.component';
|
||||
import { StackDemoComponent } from './stack-demo/stack-demo.component';
|
||||
import { BoxDemoComponent } from './box-demo/box-demo.component';
|
||||
import { CenterDemoComponent } from './center-demo/center-demo.component';
|
||||
import { AspectRatioDemoComponent } from './aspect-ratio-demo/aspect-ratio-demo.component';
|
||||
import { BentoGridDemoComponent } from './bento-grid-demo/bento-grid-demo.component';
|
||||
import { BreakpointContainerDemoComponent } from './breakpoint-container-demo/breakpoint-container-demo.component';
|
||||
import { SectionDemoComponent } from './section-demo/section-demo.component';
|
||||
import { FlexDemoComponent } from './flex-demo/flex-demo.component';
|
||||
import { ColumnDemoComponent } from './column-demo/column-demo.component';
|
||||
import { SidebarLayoutDemoComponent } from './sidebar-layout-demo/sidebar-layout-demo.component';
|
||||
import { ScrollContainerDemoComponent } from './scroll-container-demo/scroll-container-demo.component';
|
||||
import { TabsContainerDemoComponent } from './tabs-container-demo/tabs-container-demo.component';
|
||||
import { DashboardShellDemoComponent } from './dashboard-shell-demo/dashboard-shell-demo.component';
|
||||
import { GridContainerDemoComponent } from './grid-container-demo/grid-container-demo.component';
|
||||
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';
|
||||
|
||||
|
||||
@Component({
|
||||
@@ -111,9 +127,6 @@ import { IconButtonDemoComponent } from './icon-button-demo/icon-button-demo.com
|
||||
|
||||
@case ("input") {
|
||||
<ui-input-demo></ui-input-demo>
|
||||
}
|
||||
@case ("layout") {
|
||||
<ui-layout-demo></ui-layout-demo>
|
||||
}
|
||||
@case ("radio") {
|
||||
<ui-radio-demo></ui-radio-demo>
|
||||
@@ -303,13 +316,81 @@ import { IconButtonDemoComponent } from './icon-button-demo/icon-button-demo.com
|
||||
<ui-tag-input-demo></ui-tag-input-demo>
|
||||
}
|
||||
|
||||
@case ("stack") {
|
||||
<ui-stack-demo></ui-stack-demo>
|
||||
}
|
||||
|
||||
@case ("box") {
|
||||
<ui-box-demo></ui-box-demo>
|
||||
}
|
||||
|
||||
@case ("center") {
|
||||
<ui-center-demo></ui-center-demo>
|
||||
}
|
||||
|
||||
@case ("aspect-ratio") {
|
||||
<ui-aspect-ratio-demo></ui-aspect-ratio-demo>
|
||||
}
|
||||
|
||||
@case ("bento-grid") {
|
||||
<ui-bento-grid-demo></ui-bento-grid-demo>
|
||||
}
|
||||
|
||||
@case ("breakpoint-container") {
|
||||
<ui-breakpoint-container-demo></ui-breakpoint-container-demo>
|
||||
}
|
||||
|
||||
@case ("section") {
|
||||
<ui-section-demo></ui-section-demo>
|
||||
}
|
||||
|
||||
@case ("flex") {
|
||||
<ui-flex-demo></ui-flex-demo>
|
||||
}
|
||||
|
||||
@case ("column") {
|
||||
<ui-column-demo></ui-column-demo>
|
||||
}
|
||||
|
||||
@case ("sidebar-layout") {
|
||||
<ui-sidebar-layout-demo></ui-sidebar-layout-demo>
|
||||
}
|
||||
|
||||
@case ("scroll-container") {
|
||||
<ui-scroll-container-demo></ui-scroll-container-demo>
|
||||
}
|
||||
|
||||
@case ("tabs-container") {
|
||||
<ui-tabs-container-demo></ui-tabs-container-demo>
|
||||
}
|
||||
|
||||
@case ("dashboard-shell") {
|
||||
<ui-dashboard-shell-demo></ui-dashboard-shell-demo>
|
||||
}
|
||||
|
||||
@case ("grid-container") {
|
||||
<ui-grid-container-demo></ui-grid-container-demo>
|
||||
}
|
||||
|
||||
@case ("feed-layout") {
|
||||
<ui-feed-layout-demo></ui-feed-layout-demo>
|
||||
}
|
||||
|
||||
@case ("list-detail-layout") {
|
||||
<ui-list-detail-layout-demo></ui-list-detail-layout-demo>
|
||||
}
|
||||
|
||||
@case ("supporting-pane-layout") {
|
||||
<ui-supporting-pane-layout-demo></ui-supporting-pane-layout-demo>
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
`,
|
||||
imports: [AvatarDemoComponent, ButtonDemoComponent, IconButtonDemoComponent, CardDemoComponent,
|
||||
ChipDemoComponent, TableDemoComponent, BadgeDemoComponent,
|
||||
MenuDemoComponent, InputDemoComponent,
|
||||
LayoutDemoComponent, RadioDemoComponent, CheckboxDemoComponent,
|
||||
RadioDemoComponent, CheckboxDemoComponent,
|
||||
SearchDemoComponent, SwitchDemoComponent, ProgressDemoComponent,
|
||||
AppbarDemoComponent, BottomNavigationDemoComponent, FontAwesomeDemoComponent, ImageContainerDemoComponent,
|
||||
CarouselDemoComponent, VideoPlayerDemoComponent, ListDemoComponent,
|
||||
@@ -318,7 +399,7 @@ import { IconButtonDemoComponent } from './icon-button-demo/icon-button-demo.com
|
||||
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]
|
||||
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]
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { DividerComponent } from '../../../../../ui-essentials/src/public-api';
|
||||
import { DividerComponent } from '../../../../../ui-essentials/src/lib/components/layout';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-divider-demo',
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-lg;
|
||||
|
||||
h3 {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.demo-feed-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-feed {
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
background: $semantic-color-surface;
|
||||
|
||||
&--preview {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
&--interactive {
|
||||
height: 600px;
|
||||
margin: $semantic-spacing-component-lg 0;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-feed-item {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&--loading {
|
||||
opacity: $semantic-opacity-subtle;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
&__badge {
|
||||
padding: 2px $semantic-spacing-component-xs;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
font-family: map-get($semantic-typography-caption, font-family);
|
||||
font-size: map-get($semantic-typography-caption, font-size);
|
||||
font-weight: map-get($semantic-typography-caption, font-weight);
|
||||
line-height: map-get($semantic-typography-caption, line-height);
|
||||
text-transform: uppercase;
|
||||
|
||||
&--text {
|
||||
background: $semantic-color-info;
|
||||
color: $semantic-color-on-info;
|
||||
}
|
||||
|
||||
&--image {
|
||||
background: $semantic-color-success;
|
||||
color: $semantic-color-on-success;
|
||||
}
|
||||
|
||||
&--video {
|
||||
background: $semantic-color-warning;
|
||||
color: $semantic-color-on-warning;
|
||||
}
|
||||
}
|
||||
|
||||
&__timestamp {
|
||||
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;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h5, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h5, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h5, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h5, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin: 0 0 $semantic-spacing-content-paragraph 0;
|
||||
}
|
||||
|
||||
&__content {
|
||||
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-md 0;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&__action {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
cursor: pointer;
|
||||
|
||||
font-family: map-get($semantic-typography-button-small, font-family);
|
||||
font-size: map-get($semantic-typography-button-small, font-size);
|
||||
font-weight: map-get($semantic-typography-button-small, font-weight);
|
||||
line-height: map-get($semantic-typography-button-small, line-height);
|
||||
color: $semantic-color-text-tertiary;
|
||||
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-elevated;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-md;
|
||||
align-items: center;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
|
||||
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-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);
|
||||
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
opacity: $semantic-opacity-hover;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: $semantic-opacity-disabled;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
|
||||
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;
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-empty-state {
|
||||
text-align: center;
|
||||
padding: $semantic-spacing-layout-section-lg;
|
||||
|
||||
&--small {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin: 0 0 $semantic-spacing-component-sm 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-lg 0;
|
||||
}
|
||||
|
||||
&__button {
|
||||
padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
|
||||
background: $semantic-color-secondary;
|
||||
color: $semantic-color-on-secondary;
|
||||
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 {
|
||||
opacity: $semantic-opacity-hover;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: $semantic-spacing-grid-gap-md;
|
||||
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;
|
||||
}
|
||||
|
||||
.demo-stat {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&__label {
|
||||
font-family: map-get($semantic-typography-body-small, font-family);
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: map-get($semantic-typography-body-small, font-weight);
|
||||
line-height: map-get($semantic-typography-body-small, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
}
|
||||
|
||||
&__value {
|
||||
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-primary;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: 768px) {
|
||||
.demo-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
|
||||
label {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-feed {
|
||||
&--preview {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
&--interactive {
|
||||
height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FeedLayoutComponent, FeedItem } from 'ui-essentials';
|
||||
|
||||
interface DemoFeedItem extends FeedItem {
|
||||
title: string;
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
author: string;
|
||||
likes: number;
|
||||
type: 'text' | 'image' | 'video';
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ui-feed-layout-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, FeedLayoutComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Feed Layout Demo</h2>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Sizes</h3>
|
||||
<div class="demo-row">
|
||||
@for (size of sizes; track size) {
|
||||
<div class="demo-feed-wrapper">
|
||||
<h4>{{ size | titlecase }}</h4>
|
||||
<ui-feed-layout
|
||||
[size]="size"
|
||||
[loading]="false"
|
||||
[enableInfiniteScroll]="false"
|
||||
[enableRefresh]="false"
|
||||
class="demo-feed demo-feed--preview">
|
||||
@for (item of sampleItems.slice(0, 2); track item.id) {
|
||||
<div class="demo-feed-item">
|
||||
<div class="demo-feed-item__header">
|
||||
<strong>{{ item.author }}</strong>
|
||||
<span class="demo-feed-item__timestamp">{{ item.timestamp | date:'short' }}</span>
|
||||
</div>
|
||||
<h5 class="demo-feed-item__title">{{ item.title }}</h5>
|
||||
<p class="demo-feed-item__content">{{ item.content }}</p>
|
||||
<div class="demo-feed-item__actions">
|
||||
<button class="demo-feed-item__action">♡ {{ item.likes }}</button>
|
||||
<button class="demo-feed-item__action">💬 Comment</button>
|
||||
<button class="demo-feed-item__action">↗ Share</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</ui-feed-layout>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Feed -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Feed with Infinite Scroll</h3>
|
||||
<div class="demo-controls">
|
||||
<button (click)="resetFeed()" [disabled]="isLoading()">Reset Feed</button>
|
||||
<button (click)="toggleError()">
|
||||
{{ hasError() ? 'Clear Error' : 'Simulate Error' }}
|
||||
</button>
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="enableRefreshControl" />
|
||||
Enable Pull to Refresh
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<ui-feed-layout
|
||||
size="md"
|
||||
[loading]="isLoading()"
|
||||
[hasError]="hasError()"
|
||||
[errorMessage]="errorMessage"
|
||||
[isEmpty]="feedItems().length === 0 && !isLoading()"
|
||||
[enableInfiniteScroll]="true"
|
||||
[enableRefresh]="enableRefreshControl"
|
||||
(loadMore)="loadMoreItems()"
|
||||
(refresh)="refreshFeed()"
|
||||
(retry)="retryLoad()"
|
||||
class="demo-feed demo-feed--interactive"
|
||||
#interactiveFeed>
|
||||
|
||||
@for (item of feedItems(); track item.id) {
|
||||
<div class="demo-feed-item">
|
||||
<div class="demo-feed-item__header">
|
||||
<strong>{{ item.author }}</strong>
|
||||
<span class="demo-feed-item__badge demo-feed-item__badge--{{item.type}}">
|
||||
{{ item.type }}
|
||||
</span>
|
||||
<span class="demo-feed-item__timestamp">{{ item.timestamp | date:'short' }}</span>
|
||||
</div>
|
||||
<h5 class="demo-feed-item__title">{{ item.title }}</h5>
|
||||
<p class="demo-feed-item__content">{{ item.content }}</p>
|
||||
<div class="demo-feed-item__actions">
|
||||
<button
|
||||
class="demo-feed-item__action"
|
||||
(click)="toggleLike(item)">
|
||||
{{ item.likes > 0 ? '❤️' : '♡' }} {{ item.likes }}
|
||||
</button>
|
||||
<button class="demo-feed-item__action">💬 Comment</button>
|
||||
<button class="demo-feed-item__action">↗ Share</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Empty state slot -->
|
||||
<div slot="empty" class="demo-empty-state">
|
||||
<div class="demo-empty-state__icon">📝</div>
|
||||
<h4>No posts yet</h4>
|
||||
<p>Be the first to share something!</p>
|
||||
<button (click)="addSampleContent()" class="demo-empty-state__button">
|
||||
Add Sample Content
|
||||
</button>
|
||||
</div>
|
||||
</ui-feed-layout>
|
||||
</section>
|
||||
|
||||
<!-- States Demo -->
|
||||
<section class="demo-section">
|
||||
<h3>States</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-feed-wrapper">
|
||||
<h4>Loading State</h4>
|
||||
<ui-feed-layout
|
||||
size="sm"
|
||||
[loading]="true"
|
||||
[enableInfiniteScroll]="false"
|
||||
[enableRefresh]="false"
|
||||
class="demo-feed demo-feed--preview">
|
||||
@for (item of sampleItems.slice(0, 1); track item.id) {
|
||||
<div class="demo-feed-item demo-feed-item--loading">
|
||||
<div class="demo-feed-item__header">
|
||||
<strong>{{ item.author }}</strong>
|
||||
</div>
|
||||
<h5 class="demo-feed-item__title">{{ item.title }}</h5>
|
||||
<p class="demo-feed-item__content">{{ item.content }}</p>
|
||||
</div>
|
||||
}
|
||||
</ui-feed-layout>
|
||||
</div>
|
||||
|
||||
<div class="demo-feed-wrapper">
|
||||
<h4>Error State</h4>
|
||||
<ui-feed-layout
|
||||
size="sm"
|
||||
[loading]="false"
|
||||
[hasError]="true"
|
||||
errorMessage="Network connection failed"
|
||||
[enableInfiniteScroll]="false"
|
||||
[enableRefresh]="false"
|
||||
class="demo-feed demo-feed--preview">
|
||||
</ui-feed-layout>
|
||||
</div>
|
||||
|
||||
<div class="demo-feed-wrapper">
|
||||
<h4>Empty State</h4>
|
||||
<ui-feed-layout
|
||||
size="sm"
|
||||
[loading]="false"
|
||||
[isEmpty]="true"
|
||||
[enableInfiniteScroll]="false"
|
||||
[enableRefresh]="false"
|
||||
class="demo-feed demo-feed--preview">
|
||||
<div slot="empty" class="demo-empty-state demo-empty-state--small">
|
||||
<div class="demo-empty-state__icon">📭</div>
|
||||
<p>No content available</p>
|
||||
</div>
|
||||
</ui-feed-layout>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Usage Statistics -->
|
||||
<section class="demo-section">
|
||||
<h3>Usage Statistics</h3>
|
||||
<div class="demo-stats">
|
||||
<div class="demo-stat">
|
||||
<span class="demo-stat__label">Total Items:</span>
|
||||
<span class="demo-stat__value">{{ feedItems().length }}</span>
|
||||
</div>
|
||||
<div class="demo-stat">
|
||||
<span class="demo-stat__label">Load More Calls:</span>
|
||||
<span class="demo-stat__value">{{ loadMoreCount }}</span>
|
||||
</div>
|
||||
<div class="demo-stat">
|
||||
<span class="demo-stat__label">Refresh Calls:</span>
|
||||
<span class="demo-stat__value">{{ refreshCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './feed-layout-demo.component.scss'
|
||||
})
|
||||
export class FeedLayoutDemoComponent {
|
||||
sizes = ['sm', 'md', 'lg'] as const;
|
||||
|
||||
protected readonly feedItems = signal<DemoFeedItem[]>([]);
|
||||
protected readonly isLoading = signal(false);
|
||||
protected readonly hasError = signal(false);
|
||||
|
||||
protected errorMessage = '';
|
||||
protected enableRefreshControl = true;
|
||||
protected loadMoreCount = 0;
|
||||
protected refreshCount = 0;
|
||||
|
||||
protected sampleItems: DemoFeedItem[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Getting Started with Angular 19',
|
||||
content: 'Angular 19 brings exciting new features including improved SSR, better performance, and enhanced developer experience.',
|
||||
timestamp: new Date(Date.now() - 3600000),
|
||||
author: 'Sarah Chen',
|
||||
likes: 42,
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Building Responsive Layouts',
|
||||
content: 'Learn how to create flexible, mobile-first designs that work beautifully across all devices.',
|
||||
timestamp: new Date(Date.now() - 7200000),
|
||||
author: 'Mike Rodriguez',
|
||||
likes: 28,
|
||||
type: 'image'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'TypeScript Best Practices',
|
||||
content: 'Discover advanced TypeScript patterns and techniques to write more maintainable code.',
|
||||
timestamp: new Date(Date.now() - 10800000),
|
||||
author: 'Alex Kim',
|
||||
likes: 35,
|
||||
type: 'video'
|
||||
}
|
||||
];
|
||||
|
||||
constructor() {
|
||||
this.addSampleContent();
|
||||
}
|
||||
|
||||
loadMoreItems(): void {
|
||||
if (this.isLoading() || this.hasError()) return;
|
||||
|
||||
this.loadMoreCount++;
|
||||
this.isLoading.set(true);
|
||||
|
||||
// Simulate API call delay
|
||||
setTimeout(() => {
|
||||
const currentItems = this.feedItems();
|
||||
const nextBatch = this.generateItems(3, currentItems.length);
|
||||
this.feedItems.set([...currentItems, ...nextBatch]);
|
||||
this.isLoading.set(false);
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
refreshFeed(): void {
|
||||
this.refreshCount++;
|
||||
this.isLoading.set(true);
|
||||
this.hasError.set(false);
|
||||
|
||||
// Simulate refresh delay
|
||||
setTimeout(() => {
|
||||
const newItems = this.generateItems(5, 0);
|
||||
this.feedItems.set(newItems);
|
||||
this.isLoading.set(false);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
retryLoad(): void {
|
||||
this.hasError.set(false);
|
||||
this.loadMoreItems();
|
||||
}
|
||||
|
||||
resetFeed(): void {
|
||||
this.feedItems.set([]);
|
||||
this.hasError.set(false);
|
||||
this.isLoading.set(false);
|
||||
this.loadMoreCount = 0;
|
||||
this.refreshCount = 0;
|
||||
this.addSampleContent();
|
||||
}
|
||||
|
||||
toggleError(): void {
|
||||
if (this.hasError()) {
|
||||
this.hasError.set(false);
|
||||
this.errorMessage = '';
|
||||
} else {
|
||||
this.hasError.set(true);
|
||||
this.errorMessage = 'Failed to load more content. Please check your connection.';
|
||||
}
|
||||
}
|
||||
|
||||
toggleLike(item: DemoFeedItem): void {
|
||||
const items = this.feedItems();
|
||||
const index = items.findIndex(i => i.id === item.id);
|
||||
if (index !== -1) {
|
||||
const updatedItems = [...items];
|
||||
updatedItems[index] = { ...item, likes: item.likes > 0 ? 0 : 1 };
|
||||
this.feedItems.set(updatedItems);
|
||||
}
|
||||
}
|
||||
|
||||
addSampleContent(): void {
|
||||
const items = this.generateItems(5, 0);
|
||||
this.feedItems.set(items);
|
||||
}
|
||||
|
||||
private generateItems(count: number, startId: number): DemoFeedItem[] {
|
||||
const contentTemplates = [
|
||||
{
|
||||
title: 'Web Development Trends',
|
||||
content: 'Exploring the latest trends in modern web development and what they mean for developers.',
|
||||
author: 'Jane Doe',
|
||||
type: 'text' as const
|
||||
},
|
||||
{
|
||||
title: 'UI/UX Design Principles',
|
||||
content: 'Understanding the fundamental principles that make great user interfaces and experiences.',
|
||||
author: 'John Smith',
|
||||
type: 'image' as const
|
||||
},
|
||||
{
|
||||
title: 'Performance Optimization',
|
||||
content: 'Tips and techniques for optimizing web application performance and user experience.',
|
||||
author: 'Emma Wilson',
|
||||
type: 'video' as const
|
||||
},
|
||||
{
|
||||
title: 'Accessibility Matters',
|
||||
content: 'Why web accessibility is crucial and how to implement it in your projects.',
|
||||
author: 'David Brown',
|
||||
type: 'text' as const
|
||||
},
|
||||
{
|
||||
title: 'Mobile-First Design',
|
||||
content: 'Best practices for designing mobile-first responsive web applications.',
|
||||
author: 'Lisa Garcia',
|
||||
type: 'image' as const
|
||||
}
|
||||
];
|
||||
|
||||
return Array.from({ length: count }, (_, index) => {
|
||||
const template = contentTemplates[index % contentTemplates.length];
|
||||
const id = startId + index + 1;
|
||||
|
||||
return {
|
||||
id: id.toString(),
|
||||
title: `${template.title} #${id}`,
|
||||
content: template.content,
|
||||
timestamp: new Date(Date.now() - (index + 1) * 1800000),
|
||||
author: template.author,
|
||||
likes: Math.floor(Math.random() * 50),
|
||||
type: template.type
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
h2 {
|
||||
font-family: map-get($semantic-typography-heading-h2, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h2, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h2, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h2, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-layout-section-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
|
||||
h3 {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-family: map-get($semantic-typography-heading-h5, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h5, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h5, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h5, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
h6 {
|
||||
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-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-column {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
// Flex demo containers
|
||||
.flex-demo {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.flex-item {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
padding: $semantic-spacing-component-md;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
text-align: center;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
|
||||
&--small {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
&--medium {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&--large {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
&--fixed {
|
||||
min-width: 120px;
|
||||
background: $semantic-color-secondary;
|
||||
color: $semantic-color-on-secondary;
|
||||
}
|
||||
|
||||
&--inline {
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
background: $semantic-color-info;
|
||||
color: $semantic-color-on-info;
|
||||
}
|
||||
}
|
||||
|
||||
// Justify content demos
|
||||
.justify-example {
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.justify-demo {
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
// Align items demos
|
||||
.align-demo {
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
// Wrap demos
|
||||
.wrap-example {
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.wrap-demo {
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
// Gap demos
|
||||
.gap-demo {
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
// Complex examples
|
||||
.complex-example {
|
||||
margin-bottom: $semantic-spacing-layout-section-sm;
|
||||
}
|
||||
|
||||
.centered-container {
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
min-height: 200px;
|
||||
|
||||
.card {
|
||||
background: $semantic-color-surface-primary;
|
||||
padding: $semantic-spacing-component-xl;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
text-align: center;
|
||||
max-width: 300px;
|
||||
|
||||
h5 {
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-demo {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
|
||||
.nav-brand {
|
||||
font-weight: $semantic-typography-font-weight-bold;
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
transition: background-color $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
.grid-card {
|
||||
background: $semantic-color-surface-primary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
max-width: 350px;
|
||||
|
||||
h6 {
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-layout {
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
min-height: 200px;
|
||||
|
||||
.sidebar {
|
||||
background: $semantic-color-surface-primary;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
min-width: 200px;
|
||||
border-right: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
flex: 1;
|
||||
|
||||
h6 {
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Size examples
|
||||
.size-example {
|
||||
margin-bottom: $semantic-spacing-component-xl;
|
||||
}
|
||||
|
||||
.size-demo {
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.inline-flex-demo {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FlexComponent } from '../../../../../ui-essentials/src/lib/components/layout/flex';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-flex-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FlexComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Flex Component Demo</h2>
|
||||
|
||||
<!-- Direction Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Flex Direction</h3>
|
||||
<div class="demo-row">
|
||||
@for (direction of directions; track direction) {
|
||||
<div class="demo-column">
|
||||
<h4>direction="{{ direction }}"</h4>
|
||||
<ui-flex [direction]="direction" class="flex-demo">
|
||||
<div class="flex-item">1</div>
|
||||
<div class="flex-item">2</div>
|
||||
<div class="flex-item">3</div>
|
||||
</ui-flex>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Justify Content -->
|
||||
<section class="demo-section">
|
||||
<h3>Justify Content</h3>
|
||||
<div class="demo-column">
|
||||
@for (justify of justifyOptions; track justify) {
|
||||
<div class="justify-example">
|
||||
<h4>justify="{{ justify }}"</h4>
|
||||
<ui-flex [justify]="justify" class="justify-demo">
|
||||
<div class="flex-item">A</div>
|
||||
<div class="flex-item">B</div>
|
||||
<div class="flex-item">C</div>
|
||||
</ui-flex>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Align Items -->
|
||||
<section class="demo-section">
|
||||
<h3>Align Items</h3>
|
||||
<div class="demo-row">
|
||||
@for (align of alignOptions; track align) {
|
||||
<div class="demo-column">
|
||||
<h4>align="{{ align }}"</h4>
|
||||
<ui-flex [align]="align" class="align-demo">
|
||||
<div class="flex-item flex-item--small">Small</div>
|
||||
<div class="flex-item flex-item--medium">Medium</div>
|
||||
<div class="flex-item flex-item--large">Large item with more content</div>
|
||||
</ui-flex>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Wrap Options -->
|
||||
<section class="demo-section">
|
||||
<h3>Flex Wrap</h3>
|
||||
<div class="demo-column">
|
||||
@for (wrapOption of wrapOptions; track wrapOption) {
|
||||
<div class="wrap-example">
|
||||
<h4>wrap="{{ wrapOption }}"</h4>
|
||||
<ui-flex [wrap]="wrapOption" class="wrap-demo">
|
||||
@for (item of manyItems; track item) {
|
||||
<div class="flex-item flex-item--fixed">{{ item }}</div>
|
||||
}
|
||||
</ui-flex>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Gap Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Gap Spacing</h3>
|
||||
<div class="demo-row">
|
||||
@for (gap of gapSizes; track gap) {
|
||||
<div class="demo-column">
|
||||
<h4>gap="{{ gap }}"</h4>
|
||||
<ui-flex [gap]="gap" class="gap-demo">
|
||||
<div class="flex-item">1</div>
|
||||
<div class="flex-item">2</div>
|
||||
<div class="flex-item">3</div>
|
||||
</ui-flex>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Complex Layout Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Complex Layout Examples</h3>
|
||||
|
||||
<!-- Centered Card -->
|
||||
<div class="complex-example">
|
||||
<h4>Centered Card Layout</h4>
|
||||
<ui-flex justify="center" align="center" class="centered-container">
|
||||
<div class="card">
|
||||
<h5>Centered Card</h5>
|
||||
<p>This card is perfectly centered using flexbox.</p>
|
||||
</div>
|
||||
</ui-flex>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Layout -->
|
||||
<div class="complex-example">
|
||||
<h4>Navigation Layout</h4>
|
||||
<ui-flex justify="between" align="center" class="nav-demo">
|
||||
<div class="nav-brand">Brand</div>
|
||||
<ui-flex gap="md" align="center">
|
||||
<div class="nav-item">Home</div>
|
||||
<div class="nav-item">About</div>
|
||||
<div class="nav-item">Contact</div>
|
||||
</ui-flex>
|
||||
</ui-flex>
|
||||
</div>
|
||||
|
||||
<!-- Responsive Grid Alternative -->
|
||||
<div class="complex-example">
|
||||
<h4>Responsive Card Grid</h4>
|
||||
<ui-flex wrap="wrap" gap="lg" class="card-grid">
|
||||
@for (card of cards; track card.id) {
|
||||
<div class="grid-card">
|
||||
<h6>{{ card.title }}</h6>
|
||||
<p>{{ card.content }}</p>
|
||||
</div>
|
||||
}
|
||||
</ui-flex>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar Layout -->
|
||||
<div class="complex-example">
|
||||
<h4>Sidebar Layout</h4>
|
||||
<ui-flex class="sidebar-layout">
|
||||
<div class="sidebar">Sidebar</div>
|
||||
<div class="main-content">
|
||||
<h6>Main Content</h6>
|
||||
<p>This is the main content area that grows to fill the available space.</p>
|
||||
</div>
|
||||
</ui-flex>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Size Variants</h3>
|
||||
<div class="demo-column">
|
||||
<div class="size-example">
|
||||
<h4>Full Width</h4>
|
||||
<ui-flex [fullWidth]="true" class="size-demo">
|
||||
<div class="flex-item">Full Width Container</div>
|
||||
</ui-flex>
|
||||
</div>
|
||||
|
||||
<div class="size-example">
|
||||
<h4>Inline Flex</h4>
|
||||
<div>
|
||||
Before text
|
||||
<ui-flex [inline]="true" gap="sm" class="inline-flex-demo">
|
||||
<div class="flex-item flex-item--inline">A</div>
|
||||
<div class="flex-item flex-item--inline">B</div>
|
||||
</ui-flex>
|
||||
After text
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './flex-demo.component.scss'
|
||||
})
|
||||
export class FlexDemoComponent {
|
||||
directions = ['row', 'row-reverse', 'column', 'column-reverse'] as const;
|
||||
justifyOptions = ['start', 'end', 'center', 'between', 'around', 'evenly'] as const;
|
||||
alignOptions = ['start', 'end', 'center', 'baseline', 'stretch'] as const;
|
||||
wrapOptions = ['nowrap', 'wrap', 'wrap-reverse'] as const;
|
||||
gapSizes = ['xs', 'sm', 'md', 'lg', 'xl'] as const;
|
||||
|
||||
manyItems = Array.from({ length: 12 }, (_, i) => `Item ${i + 1}`);
|
||||
|
||||
cards = [
|
||||
{ id: 1, title: 'Card 1', content: 'This is the first card with some content.' },
|
||||
{ id: 2, title: 'Card 2', content: 'This is the second card with different content.' },
|
||||
{ id: 3, title: 'Card 3', content: 'This is the third card with more content.' },
|
||||
{ id: 4, title: 'Card 4', content: 'This is the fourth card with additional content.' },
|
||||
{ id: 5, title: 'Card 5', content: 'This is the fifth card with extra content.' },
|
||||
{ id: 6, title: 'Card 6', content: 'This is the sixth card with final content.' },
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-lg;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-description {
|
||||
font-family: map-get($semantic-typography-body-large, font-family);
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
font-weight: map-get($semantic-typography-body-large, font-weight);
|
||||
line-height: map-get($semantic-typography-body-large, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
|
||||
.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-content-heading;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-example {
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
}
|
||||
|
||||
.demo-grid-item {
|
||||
padding: $semantic-spacing-component-md;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
min-height: 80px;
|
||||
position: relative;
|
||||
|
||||
small {
|
||||
position: absolute;
|
||||
bottom: $semantic-spacing-component-xs;
|
||||
right: $semantic-spacing-component-xs;
|
||||
font-size: $semantic-typography-font-size-xs;
|
||||
opacity: $semantic-opacity-subtle;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border-color: $semantic-color-primary;
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
background: $semantic-color-secondary;
|
||||
color: $semantic-color-on-secondary;
|
||||
border-color: $semantic-color-secondary;
|
||||
}
|
||||
|
||||
&--success {
|
||||
background: $semantic-color-success;
|
||||
color: $semantic-color-on-success;
|
||||
border-color: $semantic-color-success;
|
||||
}
|
||||
|
||||
&--info {
|
||||
background: $semantic-color-info;
|
||||
color: $semantic-color-on-info;
|
||||
border-color: $semantic-color-info;
|
||||
}
|
||||
|
||||
&--warning {
|
||||
background: $semantic-color-warning;
|
||||
color: $semantic-color-on-warning;
|
||||
border-color: $semantic-color-warning;
|
||||
}
|
||||
|
||||
&--danger {
|
||||
background: $semantic-color-danger;
|
||||
color: $semantic-color-on-danger;
|
||||
border-color: $semantic-color-danger;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
}
|
||||
|
||||
.demo-control-group {
|
||||
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;
|
||||
}
|
||||
|
||||
select,
|
||||
input[type="checkbox"] {
|
||||
padding: $semantic-spacing-interactive-input-padding-y $semantic-spacing-interactive-input-padding-x;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-input-radius;
|
||||
background: $semantic-color-surface-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
font-family: map-get($semantic-typography-input, font-family);
|
||||
font-size: map-get($semantic-typography-input, font-size);
|
||||
font-weight: map-get($semantic-typography-input, font-weight);
|
||||
line-height: map-get($semantic-typography-input, line-height);
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
border-color: $semantic-color-border-focus;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: auto;
|
||||
height: $semantic-sizing-touch-minimum;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.demo-grid-item {
|
||||
min-height: 60px;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { GridContainerComponent } from 'ui-essentials';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-grid-container-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, GridContainerComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>GridContainer Demo</h2>
|
||||
<p class="demo-description">
|
||||
Advanced CSS Grid wrapper with template areas support, responsive column/row definitions, and auto-placement algorithms.
|
||||
</p>
|
||||
|
||||
<!-- Basic Grid Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Basic Grid Layouts</h3>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Auto-fit Grid (Responsive)</h4>
|
||||
<ui-grid-container columns="auto-fit" gap="md">
|
||||
@for (item of basicItems; track item.id) {
|
||||
<div class="demo-grid-item demo-grid-item--primary">{{ item.label }}</div>
|
||||
}
|
||||
</ui-grid-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Fixed 3-Column Grid</h4>
|
||||
<ui-grid-container [columns]="3" gap="lg">
|
||||
@for (item of basicItems; track item.id) {
|
||||
<div class="demo-grid-item demo-grid-item--secondary">{{ item.label }}</div>
|
||||
}
|
||||
</ui-grid-container>
|
||||
</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-example">
|
||||
<h4>{{ gap }} Gap</h4>
|
||||
<ui-grid-container [columns]="3" [gap]="gap">
|
||||
@for (item of shortItems; track item.id) {
|
||||
<div class="demo-grid-item demo-grid-item--success">{{ item.label }}</div>
|
||||
}
|
||||
</ui-grid-container>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Column Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Column Layouts</h3>
|
||||
@for (colCount of columnCounts; track colCount) {
|
||||
<div class="demo-example">
|
||||
<h4>{{ colCount }} Columns</h4>
|
||||
<ui-grid-container [columns]="colCount" gap="md">
|
||||
@for (item of getItemsForColumns(colCount); track item.id) {
|
||||
<div class="demo-grid-item demo-grid-item--info">{{ item.label }}</div>
|
||||
}
|
||||
</ui-grid-container>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- Auto Grid Types -->
|
||||
<section class="demo-section">
|
||||
<h3>Auto Grid Types</h3>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Auto-fit (Items stretch to fill space)</h4>
|
||||
<ui-grid-container columns="auto-fit" gap="md">
|
||||
@for (item of autoItems; track item.id) {
|
||||
<div class="demo-grid-item demo-grid-item--warning">{{ item.label }}</div>
|
||||
}
|
||||
</ui-grid-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Auto-fill (Empty columns maintained)</h4>
|
||||
<ui-grid-container columns="auto-fill" gap="md">
|
||||
@for (item of autoItems; track item.id) {
|
||||
<div class="demo-grid-item demo-grid-item--danger">{{ item.label }}</div>
|
||||
}
|
||||
</ui-grid-container>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Advanced Features -->
|
||||
<section class="demo-section">
|
||||
<h3>Advanced Features</h3>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Dense Grid (Auto-placement fills gaps)</h4>
|
||||
<ui-grid-container [columns]="4" gap="md" [dense]="true">
|
||||
<div class="demo-grid-item demo-grid-item--primary ui-grid-item--span-2">Wide Item (2 cols)</div>
|
||||
<div class="demo-grid-item demo-grid-item--secondary">Item 1</div>
|
||||
<div class="demo-grid-item demo-grid-item--success ui-grid-item--row-span-2">Tall Item</div>
|
||||
<div class="demo-grid-item demo-grid-item--info">Item 2</div>
|
||||
<div class="demo-grid-item demo-grid-item--warning">Item 3</div>
|
||||
<div class="demo-grid-item demo-grid-item--danger ui-grid-item--span-3">Extra Wide (3 cols)</div>
|
||||
<div class="demo-grid-item demo-grid-item--primary">Item 4</div>
|
||||
<div class="demo-grid-item demo-grid-item--secondary">Item 5</div>
|
||||
</ui-grid-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Grid with Template Areas</h4>
|
||||
<ui-grid-container
|
||||
customColumns="1fr 2fr 1fr"
|
||||
customRows="auto auto 1fr auto"
|
||||
templateAreas="'header header header' 'sidebar main aside' 'sidebar main aside' 'footer footer footer'"
|
||||
gap="md"
|
||||
padding="lg">
|
||||
<div class="demo-grid-item demo-grid-item--primary" style="grid-area: header;">Header</div>
|
||||
<div class="demo-grid-item demo-grid-item--secondary" style="grid-area: sidebar;">Sidebar</div>
|
||||
<div class="demo-grid-item demo-grid-item--info" style="grid-area: main;">Main Content</div>
|
||||
<div class="demo-grid-item demo-grid-item--success" style="grid-area: aside;">Aside</div>
|
||||
<div class="demo-grid-item demo-grid-item--warning" style="grid-area: footer;">Footer</div>
|
||||
</ui-grid-container>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Alignment Options -->
|
||||
<section class="demo-section">
|
||||
<h3>Alignment Options</h3>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Justify Content: Center</h4>
|
||||
<ui-grid-container [columns]="2" gap="md" justifyContent="center" style="height: 200px;">
|
||||
<div class="demo-grid-item demo-grid-item--primary">Item 1</div>
|
||||
<div class="demo-grid-item demo-grid-item--secondary">Item 2</div>
|
||||
</ui-grid-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Align Items: Center</h4>
|
||||
<ui-grid-container [columns]="3" gap="md" alignItems="center" style="height: 150px;">
|
||||
<div class="demo-grid-item demo-grid-item--success">Centered</div>
|
||||
<div class="demo-grid-item demo-grid-item--info">Items</div>
|
||||
<div class="demo-grid-item demo-grid-item--warning">Vertically</div>
|
||||
</ui-grid-container>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Row Modes -->
|
||||
<section class="demo-section">
|
||||
<h3>Row Modes</h3>
|
||||
@for (rowMode of rowModes; track rowMode) {
|
||||
<div class="demo-example">
|
||||
<h4>{{ rowMode }} Rows</h4>
|
||||
<ui-grid-container [columns]="3" gap="md" [rowMode]="rowMode" style="height: 200px;">
|
||||
<div class="demo-grid-item demo-grid-item--primary">
|
||||
@if (rowMode === 'min-content') {
|
||||
Short
|
||||
} @else if (rowMode === 'max-content') {
|
||||
This is longer content to demonstrate max-content behavior
|
||||
} @else {
|
||||
Content for {{ rowMode }}
|
||||
}
|
||||
</div>
|
||||
<div class="demo-grid-item demo-grid-item--secondary">Item 2</div>
|
||||
<div class="demo-grid-item demo-grid-item--success">Item 3</div>
|
||||
</ui-grid-container>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- Interactive Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Grid Builder</h3>
|
||||
|
||||
<div class="demo-controls">
|
||||
<div class="demo-control-group">
|
||||
<label>Columns:</label>
|
||||
<select [(ngModel)]="interactiveConfig.columns">
|
||||
@for (col of allColumns; track col) {
|
||||
<option [value]="col">{{ col }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="demo-control-group">
|
||||
<label>Gap:</label>
|
||||
<select [(ngModel)]="interactiveConfig.gap">
|
||||
@for (gap of gaps; track gap) {
|
||||
<option [value]="gap">{{ gap }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="demo-control-group">
|
||||
<label>Dense:</label>
|
||||
<input type="checkbox" [(ngModel)]="interactiveConfig.dense">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ui-grid-container
|
||||
[columns]="interactiveConfig.columns"
|
||||
[gap]="interactiveConfig.gap"
|
||||
[dense]="interactiveConfig.dense"
|
||||
style="border: 2px dashed #ccc; min-height: 200px;">
|
||||
@for (item of interactiveItems; track item.id) {
|
||||
<div
|
||||
class="demo-grid-item"
|
||||
[class]="'demo-grid-item--' + item.variant"
|
||||
[class.ui-grid-item--span-2]="item.span === 2"
|
||||
[class.ui-grid-item--span-3]="item.span === 3"
|
||||
[class.ui-grid-item--row-span-2]="item.rowSpan === 2">
|
||||
{{ item.label }}
|
||||
@if (item.span > 1) {
|
||||
<small>({{ item.span }} cols)</small>
|
||||
}
|
||||
@if (item.rowSpan > 1) {
|
||||
<small>({{ item.rowSpan }} rows)</small>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</ui-grid-container>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './grid-container-demo.component.scss'
|
||||
})
|
||||
export class GridContainerDemoComponent {
|
||||
gaps = ['sm', 'md', 'lg'] as const;
|
||||
columnCounts = [1, 2, 3, 4, 5, 6] as const;
|
||||
allColumns = [1, 2, 3, 4, 5, 6, 'auto-fit', 'auto-fill'] as const;
|
||||
rowModes = ['auto', 'equal', 'min-content', 'max-content'] as const;
|
||||
|
||||
basicItems = [
|
||||
{ id: 1, label: 'Item 1' },
|
||||
{ id: 2, label: 'Item 2' },
|
||||
{ id: 3, label: 'Item 3' },
|
||||
{ id: 4, label: 'Item 4' },
|
||||
{ id: 5, label: 'Item 5' },
|
||||
{ id: 6, label: 'Item 6' },
|
||||
{ id: 7, label: 'Item 7' },
|
||||
{ id: 8, label: 'Item 8' }
|
||||
];
|
||||
|
||||
shortItems = [
|
||||
{ id: 1, label: 'A' },
|
||||
{ id: 2, label: 'B' },
|
||||
{ id: 3, label: 'C' }
|
||||
];
|
||||
|
||||
autoItems = [
|
||||
{ id: 1, label: 'Auto Item 1' },
|
||||
{ id: 2, label: 'Auto Item 2' },
|
||||
{ id: 3, label: 'Auto Item 3' },
|
||||
{ id: 4, label: 'Auto Item 4' }
|
||||
];
|
||||
|
||||
interactiveConfig = {
|
||||
columns: 'auto-fit' as const,
|
||||
gap: 'md' as const,
|
||||
dense: false
|
||||
};
|
||||
|
||||
interactiveItems = [
|
||||
{ id: 1, label: 'Regular', variant: 'primary', span: 1, rowSpan: 1 },
|
||||
{ id: 2, label: 'Wide', variant: 'secondary', span: 2, rowSpan: 1 },
|
||||
{ id: 3, label: 'Tall', variant: 'success', span: 1, rowSpan: 2 },
|
||||
{ id: 4, label: 'Extra Wide', variant: 'info', span: 3, rowSpan: 1 },
|
||||
{ id: 5, label: 'Normal', variant: 'warning', span: 1, rowSpan: 1 },
|
||||
{ id: 6, label: 'Regular', variant: 'danger', span: 1, rowSpan: 1 },
|
||||
{ id: 7, label: 'Wide', variant: 'primary', span: 2, rowSpan: 1 },
|
||||
{ id: 8, label: 'Normal', variant: 'secondary', span: 1, rowSpan: 1 }
|
||||
];
|
||||
|
||||
getItemsForColumns(colCount: number) {
|
||||
const count = Math.min(colCount * 2, this.basicItems.length);
|
||||
return this.basicItems.slice(0, count);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ export * from './checkbox-demo/checkbox-demo.component';
|
||||
export * from './chip-demo/chip-demo.component';
|
||||
export * from './fontawesome-demo/fontawesome-demo.component';
|
||||
export * from './input-demo/input-demo.component';
|
||||
export * from './layout-demo/layout-demo.component';
|
||||
export * from './menu-demo/menu-demo.component';
|
||||
export * from './progress-demo/progress-demo.component';
|
||||
export * from './radio-demo/radio-demo.component';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,468 @@
|
||||
@use "../../../../../ui-design-system/src/styles/semantic/index" as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
|
||||
h2 {
|
||||
font-family: map-get($semantic-typography-heading-h2, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h2, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h2, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h2, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-lg;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-variant-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
|
||||
@media (min-width: $semantic-breakpoint-lg) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.demo-variant {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-layout-container {
|
||||
height: 400px;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
overflow: hidden;
|
||||
|
||||
&.demo-layout--sm {
|
||||
height: 350px;
|
||||
}
|
||||
|
||||
&.demo-layout--lg {
|
||||
height: 450px;
|
||||
}
|
||||
|
||||
&.demo-layout--variant {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
&.demo-layout--interactive {
|
||||
height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
// Demo List Styles
|
||||
.demo-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: $semantic-color-surface-secondary;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-list-item {
|
||||
padding: $semantic-spacing-component-md;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
cursor: pointer;
|
||||
transition: background $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-container;
|
||||
}
|
||||
|
||||
&--selected {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-left: 3px solid $semantic-color-primary;
|
||||
}
|
||||
|
||||
&__content {
|
||||
h5 {
|
||||
font-family: map-get($semantic-typography-heading-h5, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h5, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h5, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h5, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin: 0 0 $semantic-spacing-component-xs 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-component-xs 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__status {
|
||||
display: inline-block;
|
||||
padding: 2px $semantic-spacing-component-xs;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
font-family: map-get($semantic-typography-caption, font-family);
|
||||
font-size: map-get($semantic-typography-caption, font-size);
|
||||
font-weight: map-get($semantic-typography-caption, font-weight);
|
||||
line-height: map-get($semantic-typography-caption, line-height);
|
||||
text-transform: uppercase;
|
||||
|
||||
&--active {
|
||||
background: $semantic-color-success;
|
||||
color: $semantic-color-on-success;
|
||||
}
|
||||
|
||||
&--pending {
|
||||
background: $semantic-color-warning;
|
||||
color: $semantic-color-on-warning;
|
||||
}
|
||||
|
||||
&--completed {
|
||||
background: $semantic-color-info;
|
||||
color: $semantic-color-on-info;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Demo Detail Styles
|
||||
.demo-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: $semantic-color-surface-primary;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
|
||||
h3 {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__category {
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
font-family: map-get($semantic-typography-caption, font-family);
|
||||
font-size: map-get($semantic-typography-caption, font-size);
|
||||
font-weight: map-get($semantic-typography-caption, font-weight);
|
||||
line-height: map-get($semantic-typography-caption, line-height);
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
overflow-y: auto;
|
||||
|
||||
p {
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
}
|
||||
|
||||
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: $semantic-spacing-component-lg 0 $semantic-spacing-component-sm 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__meta {
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
|
||||
p {
|
||||
margin: $semantic-spacing-component-xs 0;
|
||||
|
||||
strong {
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__stats {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
flex-wrap: wrap;
|
||||
margin-top: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-stat {
|
||||
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-secondary;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
span {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
// Empty State
|
||||
.demo-empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
text-align: center;
|
||||
|
||||
&__icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin: 0 0 $semantic-spacing-component-sm 0;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $semantic-color-text-secondary;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Interactive Controls
|
||||
.demo-interactive {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
flex-wrap: wrap;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
cursor: pointer;
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Buttons
|
||||
.demo-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
|
||||
border: $semantic-border-width-1 solid transparent;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
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);
|
||||
|
||||
&--primary {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border-color: $semantic-color-primary;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
background: $semantic-color-surface-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
border-color: $semantic-color-border-primary;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-elevated;
|
||||
}
|
||||
}
|
||||
|
||||
&--small {
|
||||
padding: calc($semantic-spacing-interactive-button-padding-y / 2) $semantic-spacing-interactive-button-padding-x;
|
||||
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);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// Event Log
|
||||
.demo-event-log {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
padding: $semantic-spacing-component-xs 0;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
font-family: map-get($semantic-typography-body-small, font-family);
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: map-get($semantic-typography-body-small, font-weight);
|
||||
line-height: map-get($semantic-typography-body-small, line-height);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__timestamp {
|
||||
color: $semantic-color-text-tertiary;
|
||||
font-family: $semantic-typography-font-family-mono;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
&__type {
|
||||
color: $semantic-color-primary;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
&__data {
|
||||
color: $semantic-color-text-secondary;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: calc($semantic-breakpoint-md - 1px)) {
|
||||
.demo-variant-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.demo-layout-container {
|
||||
height: 300px;
|
||||
|
||||
&.demo-layout--interactive {
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
import { Component, ViewChild, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ListDetailLayoutComponent } from '../../../../../ui-essentials/src/lib/components/layout/list-detail-layout/list-detail-layout.component';
|
||||
|
||||
interface DemoItem {
|
||||
id: number;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
description: string;
|
||||
status: 'active' | 'pending' | 'completed';
|
||||
category: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ui-list-detail-layout-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ListDetailLayoutComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>List Detail Layout Demo</h2>
|
||||
<p>Master-detail pattern with resizable panes and mobile-adaptive behavior</p>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Size Variants</h3>
|
||||
<div class="demo-variant-container">
|
||||
@for (size of sizes; track size) {
|
||||
<div class="demo-variant">
|
||||
<h4>{{ size | titlecase }} Size</h4>
|
||||
<div class="demo-layout-container" [class]="'demo-layout--' + size">
|
||||
<ui-list-detail-layout
|
||||
[size]="size"
|
||||
[selectedItem]="selectedItems[size]"
|
||||
(selectionChanged)="handleSelectionChange(size, $event)">
|
||||
|
||||
<div slot="list" class="demo-list">
|
||||
<div class="demo-list__header">
|
||||
<h4>Items ({{ size }})</h4>
|
||||
</div>
|
||||
<div class="demo-list__content">
|
||||
@for (item of demoItems(); track item.id) {
|
||||
<div
|
||||
class="demo-list-item"
|
||||
[class.demo-list-item--selected]="selectedItems[size]?.id === item.id"
|
||||
(click)="selectItem(size, item)"
|
||||
[attr.aria-selected]="selectedItems[size]?.id === item.id"
|
||||
role="option">
|
||||
<div class="demo-list-item__content">
|
||||
<h5>{{ item.title }}</h5>
|
||||
<p>{{ item.subtitle }}</p>
|
||||
<span class="demo-list-item__status demo-list-item__status--{{ item.status }}">
|
||||
{{ item.status }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="detail" class="demo-detail">
|
||||
@if (selectedItems[size]) {
|
||||
<div class="demo-detail__header">
|
||||
<h3>{{ selectedItems[size].title }}</h3>
|
||||
<span class="demo-detail__category">{{ selectedItems[size].category }}</span>
|
||||
</div>
|
||||
<div class="demo-detail__content">
|
||||
<div class="demo-detail__meta">
|
||||
<p><strong>Subtitle:</strong> {{ selectedItems[size].subtitle }}</p>
|
||||
<p><strong>Status:</strong> {{ selectedItems[size].status }}</p>
|
||||
<p><strong>Date:</strong> {{ selectedItems[size].date }}</p>
|
||||
</div>
|
||||
<div class="demo-detail__description">
|
||||
<h4>Description</h4>
|
||||
<p>{{ selectedItems[size].description }}</p>
|
||||
</div>
|
||||
<div class="demo-detail__actions">
|
||||
<button
|
||||
type="button"
|
||||
class="demo-button demo-button--primary"
|
||||
(click)="handleAction('edit', selectedItems[size])">
|
||||
Edit Item
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="demo-button demo-button--secondary"
|
||||
(click)="handleAction('delete', selectedItems[size])">
|
||||
Delete Item
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div slot="empty" class="demo-empty-state">
|
||||
<div class="demo-empty-state__icon">📋</div>
|
||||
<h4>No Item Selected</h4>
|
||||
<p>Choose an item from the list to view its details</p>
|
||||
</div>
|
||||
</ui-list-detail-layout>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Variant Styles -->
|
||||
<section class="demo-section">
|
||||
<h3>Style Variants</h3>
|
||||
<div class="demo-variant-container">
|
||||
@for (variant of variants; track variant) {
|
||||
<div class="demo-variant">
|
||||
<h4>{{ variant | titlecase }} Style</h4>
|
||||
<div class="demo-layout-container demo-layout--variant">
|
||||
<ui-list-detail-layout
|
||||
[variant]="variant"
|
||||
[selectedItem]="selectedItems[variant]"
|
||||
(selectionChanged)="handleSelectionChange(variant, $event)">
|
||||
|
||||
<div slot="list" class="demo-list">
|
||||
<div class="demo-list__header">
|
||||
<h4>{{ variant | titlecase }} List</h4>
|
||||
</div>
|
||||
<div class="demo-list__content">
|
||||
@for (item of demoItems().slice(0, 3); track item.id) {
|
||||
<div
|
||||
class="demo-list-item"
|
||||
[class.demo-list-item--selected]="selectedItems[variant]?.id === item.id"
|
||||
(click)="selectItem(variant, item)"
|
||||
role="option">
|
||||
<div class="demo-list-item__content">
|
||||
<h5>{{ item.title }}</h5>
|
||||
<p>{{ item.subtitle }}</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="detail" class="demo-detail">
|
||||
@if (selectedItems[variant]) {
|
||||
<div class="demo-detail__header">
|
||||
<h3>{{ selectedItems[variant].title }}</h3>
|
||||
</div>
|
||||
<div class="demo-detail__content">
|
||||
<p>{{ selectedItems[variant].description }}</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ui-list-detail-layout>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Features -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Features</h3>
|
||||
<div class="demo-interactive">
|
||||
<div class="demo-controls">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="interactiveFeatures.resizable"
|
||||
(change)="toggleFeature('resizable', $event)">
|
||||
Resizable Panels
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="interactiveFeatures.showEmptyState"
|
||||
(change)="toggleFeature('showEmptyState', $event)">
|
||||
Show Empty State
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="interactiveFeatures.showMobileNavigation"
|
||||
(change)="toggleFeature('showMobileNavigation', $event)">
|
||||
Mobile Navigation
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="interactiveFeatures.loading"
|
||||
(change)="toggleFeature('loading', $event)">
|
||||
Loading State
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="demo-layout-container demo-layout--interactive">
|
||||
<ui-list-detail-layout
|
||||
#interactiveLayout
|
||||
[resizable]="interactiveFeatures.resizable"
|
||||
[showEmptyState]="interactiveFeatures.showEmptyState"
|
||||
[showMobileNavigation]="interactiveFeatures.showMobileNavigation"
|
||||
[loading]="interactiveFeatures.loading"
|
||||
[selectedItem]="selectedItems['interactive']"
|
||||
(selectionChanged)="handleSelectionChange('interactive', $event)"
|
||||
(mobileNavigated)="handleMobileNavigation($event)"
|
||||
(panelResized)="handlePanelResize($event)">
|
||||
|
||||
<div slot="list" class="demo-list">
|
||||
<div class="demo-list__header">
|
||||
<h4>Interactive List</h4>
|
||||
<button
|
||||
type="button"
|
||||
class="demo-button demo-button--small"
|
||||
(click)="clearSelection('interactive')">
|
||||
Clear Selection
|
||||
</button>
|
||||
</div>
|
||||
<div class="demo-list__content">
|
||||
@for (item of demoItems(); track item.id) {
|
||||
<div
|
||||
class="demo-list-item"
|
||||
[class.demo-list-item--selected]="selectedItems['interactive']?.id === item.id"
|
||||
(click)="selectItem('interactive', item)"
|
||||
role="option">
|
||||
<div class="demo-list-item__content">
|
||||
<h5>{{ item.title }}</h5>
|
||||
<p>{{ item.subtitle }}</p>
|
||||
<span class="demo-list-item__status demo-list-item__status--{{ item.status }}">
|
||||
{{ item.status }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="detail" class="demo-detail">
|
||||
@if (selectedItems['interactive']) {
|
||||
<div class="demo-detail__header">
|
||||
<h3>{{ selectedItems['interactive'].title }}</h3>
|
||||
<span class="demo-detail__category">{{ selectedItems['interactive'].category }}</span>
|
||||
</div>
|
||||
<div class="demo-detail__content">
|
||||
<p>{{ selectedItems['interactive'].description }}</p>
|
||||
<div class="demo-detail__stats">
|
||||
<div class="demo-stat">
|
||||
<label>Status:</label>
|
||||
<span>{{ selectedItems['interactive'].status }}</span>
|
||||
</div>
|
||||
<div class="demo-stat">
|
||||
<label>Category:</label>
|
||||
<span>{{ selectedItems['interactive'].category }}</span>
|
||||
</div>
|
||||
<div class="demo-stat">
|
||||
<label>Date:</label>
|
||||
<span>{{ selectedItems['interactive'].date }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div slot="empty" class="demo-empty-state">
|
||||
<div class="demo-empty-state__icon">🎯</div>
|
||||
<h4>Ready to Explore</h4>
|
||||
<p>Select any item to see the interactive features in action</p>
|
||||
</div>
|
||||
</ui-list-detail-layout>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Event Log -->
|
||||
<section class="demo-section">
|
||||
<h3>Event Log</h3>
|
||||
<div class="demo-event-log">
|
||||
@for (event of eventLog(); track $index) {
|
||||
<div class="demo-event-log__item">
|
||||
<span class="demo-event-log__timestamp">{{ event.timestamp }}</span>
|
||||
<span class="demo-event-log__type">{{ event.type }}</span>
|
||||
<span class="demo-event-log__data">{{ event.data }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './list-detail-layout-demo.component.scss'
|
||||
})
|
||||
export class ListDetailLayoutDemoComponent {
|
||||
@ViewChild('interactiveLayout') interactiveLayout?: ListDetailLayoutComponent;
|
||||
|
||||
sizes = ['sm', 'md', 'lg'] as const;
|
||||
variants = ['default', 'bordered', 'elevated'] as const;
|
||||
|
||||
selectedItems: Record<string, DemoItem | null> = {
|
||||
sm: null,
|
||||
md: null,
|
||||
lg: null,
|
||||
default: null,
|
||||
bordered: null,
|
||||
elevated: null,
|
||||
interactive: null
|
||||
};
|
||||
|
||||
interactiveFeatures = {
|
||||
resizable: true,
|
||||
showEmptyState: true,
|
||||
showMobileNavigation: true,
|
||||
loading: false
|
||||
};
|
||||
|
||||
demoItems = signal<DemoItem[]>([
|
||||
{
|
||||
id: 1,
|
||||
title: 'Project Alpha',
|
||||
subtitle: 'High-priority initiative',
|
||||
description: 'A comprehensive project focused on improving user experience and system performance. This initiative involves multiple teams and requires careful coordination across different departments.',
|
||||
status: 'active',
|
||||
category: 'Development',
|
||||
date: '2024-01-15'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Database Migration',
|
||||
subtitle: 'Critical infrastructure update',
|
||||
description: 'Migration of legacy database systems to modern cloud infrastructure. This project includes data validation, performance optimization, and minimal downtime deployment strategies.',
|
||||
status: 'pending',
|
||||
category: 'Infrastructure',
|
||||
date: '2024-02-01'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'UI Component Library',
|
||||
subtitle: 'Design system implementation',
|
||||
description: 'Development of a comprehensive UI component library to ensure consistency across all applications. Includes documentation, testing, and integration guidelines.',
|
||||
status: 'completed',
|
||||
category: 'Design',
|
||||
date: '2024-01-28'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'Security Audit',
|
||||
subtitle: 'Annual security review',
|
||||
description: 'Comprehensive security audit covering all systems, applications, and infrastructure. Includes penetration testing, vulnerability assessment, and compliance verification.',
|
||||
status: 'active',
|
||||
category: 'Security',
|
||||
date: '2024-02-10'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: 'Mobile App Redesign',
|
||||
subtitle: 'User interface overhaul',
|
||||
description: 'Complete redesign of the mobile application focusing on improved usability, modern design patterns, and enhanced accessibility features.',
|
||||
status: 'pending',
|
||||
category: 'Mobile',
|
||||
date: '2024-03-01'
|
||||
}
|
||||
]);
|
||||
|
||||
eventLog = signal<Array<{ timestamp: string; type: string; data: string }>>([]);
|
||||
|
||||
selectItem(context: string, item: DemoItem): void {
|
||||
this.selectedItems[context] = item;
|
||||
this.logEvent('selection', `Selected: ${item.title} (${context})`);
|
||||
}
|
||||
|
||||
clearSelection(context: string): void {
|
||||
this.selectedItems[context] = null;
|
||||
this.logEvent('selection', `Cleared selection (${context})`);
|
||||
}
|
||||
|
||||
handleSelectionChange(context: string, item: DemoItem | null): void {
|
||||
this.selectedItems[context] = item;
|
||||
this.logEvent('selectionChanged',
|
||||
item ? `Item selected: ${item.title}` : 'Selection cleared');
|
||||
}
|
||||
|
||||
handleMobileNavigation(event: any): void {
|
||||
this.logEvent('mobileNavigation',
|
||||
`Navigated to: ${event.view} (has selection: ${event.hasSelection})`);
|
||||
}
|
||||
|
||||
handlePanelResize(event: any): void {
|
||||
this.logEvent('panelResize', `List width: ${event.listWidth}px`);
|
||||
}
|
||||
|
||||
handleAction(action: string, item: DemoItem): void {
|
||||
this.logEvent('action', `${action} action on: ${item.title}`);
|
||||
}
|
||||
|
||||
toggleFeature(feature: keyof typeof this.interactiveFeatures, event: Event): void {
|
||||
const target = event.target as HTMLInputElement;
|
||||
this.interactiveFeatures[feature] = target.checked;
|
||||
this.logEvent('feature', `${feature}: ${target.checked ? 'enabled' : 'disabled'}`);
|
||||
}
|
||||
|
||||
private logEvent(type: string, data: string): void {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const events = this.eventLog();
|
||||
events.unshift({ timestamp, type, data });
|
||||
// Keep only last 10 events
|
||||
if (events.length > 10) {
|
||||
events.splice(10);
|
||||
}
|
||||
this.eventLog.set([...events]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-lg;
|
||||
|
||||
h3 {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
p {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-horizontal-content {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-horizontal-item {
|
||||
min-width: 150px;
|
||||
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;
|
||||
white-space: nowrap;
|
||||
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
.demo-large-content {
|
||||
width: 800px;
|
||||
height: 600px;
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
td {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
white-space: nowrap;
|
||||
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-md;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
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: background-color $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-interactive-primary;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-info,
|
||||
.demo-stats {
|
||||
margin-top: $semantic-spacing-component-md;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
margin-bottom: $semantic-spacing-component-xs;
|
||||
|
||||
font-family: map-get($semantic-typography-body-small, font-family);
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: map-get($semantic-typography-body-small, font-weight);
|
||||
line-height: map-get($semantic-typography-body-small, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: $semantic-spacing-grid-gap-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
width: 1000px;
|
||||
}
|
||||
|
||||
.demo-grid-item {
|
||||
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;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
|
||||
font-family: map-get($semantic-typography-body-small, font-family);
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: map-get($semantic-typography-body-small, font-weight);
|
||||
line-height: map-get($semantic-typography-body-small, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.virtual-item {
|
||||
padding: $semantic-spacing-component-md;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
strong {
|
||||
color: $semantic-color-text-primary;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
margin-right: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: $semantic-breakpoint-lg - 1) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
|
||||
.demo-large-content {
|
||||
width: 400px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
width: 600px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: $semantic-spacing-grid-gap-sm;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
import { Component, TemplateRef, ViewChild } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ScrollContainerComponent, ScrollVirtualConfig } from '../../../../../ui-essentials/src/lib/components/layout/scroll-container/scroll-container.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-scroll-container-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ScrollContainerComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>ScrollContainer Demo</h2>
|
||||
|
||||
<!-- Basic Scrolling -->
|
||||
<section class="demo-section">
|
||||
<h3>Basic Vertical Scrolling</h3>
|
||||
<ui-scroll-container
|
||||
[style.height.px]="200"
|
||||
[style.width.px]="400"
|
||||
direction="vertical"
|
||||
scrollbarVisibility="auto"
|
||||
[showScrollIndicators]="true"
|
||||
(scrolled)="onScroll($event)"
|
||||
(reachedTop)="onReachedTop()"
|
||||
(reachedBottom)="onReachedBottom()">
|
||||
<div class="demo-content">
|
||||
@for (item of longContent; track item; let i = $index) {
|
||||
<p>{{ i + 1 }}. {{ item }}</p>
|
||||
}
|
||||
</div>
|
||||
</ui-scroll-container>
|
||||
<p class="demo-info">Scroll events: Top reached {{ topCount }} times, Bottom reached {{ bottomCount }} times</p>
|
||||
</section>
|
||||
|
||||
<!-- Direction Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Direction Variants</h3>
|
||||
<div class="demo-row">
|
||||
<!-- Vertical -->
|
||||
<div class="demo-item">
|
||||
<h4>Vertical</h4>
|
||||
<ui-scroll-container
|
||||
[style.height.px]="150"
|
||||
[style.width.px]="200"
|
||||
direction="vertical">
|
||||
<div class="demo-content">
|
||||
@for (item of shortContent; track item; let i = $index) {
|
||||
<p>{{ i + 1 }}. {{ item }}</p>
|
||||
}
|
||||
</div>
|
||||
</ui-scroll-container>
|
||||
</div>
|
||||
|
||||
<!-- Horizontal -->
|
||||
<div class="demo-item">
|
||||
<h4>Horizontal</h4>
|
||||
<ui-scroll-container
|
||||
[style.height.px]="100"
|
||||
[style.width.px]="200"
|
||||
direction="horizontal">
|
||||
<div class="demo-horizontal-content">
|
||||
@for (item of horizontalItems; track item) {
|
||||
<div class="demo-horizontal-item">{{ item }}</div>
|
||||
}
|
||||
</div>
|
||||
</ui-scroll-container>
|
||||
</div>
|
||||
|
||||
<!-- Both -->
|
||||
<div class="demo-item">
|
||||
<h4>Both Directions</h4>
|
||||
<ui-scroll-container
|
||||
[style.height.px]="150"
|
||||
[style.width.px]="200"
|
||||
direction="both">
|
||||
<div class="demo-large-content">
|
||||
<table class="demo-table">
|
||||
@for (row of tableData; track row.id) {
|
||||
<tr>
|
||||
<td>{{ row.id }}</td>
|
||||
<td>{{ row.name }}</td>
|
||||
<td>{{ row.description }}</td>
|
||||
<td>{{ row.value }}</td>
|
||||
<td>{{ row.status }}</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
</div>
|
||||
</ui-scroll-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Scrollbar Visibility -->
|
||||
<section class="demo-section">
|
||||
<h3>Scrollbar Visibility</h3>
|
||||
<div class="demo-row">
|
||||
@for (visibility of scrollbarOptions; track visibility) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ visibility | titlecase }}</h4>
|
||||
<ui-scroll-container
|
||||
[style.height.px]="120"
|
||||
[style.width.px]="200"
|
||||
[scrollbarVisibility]="visibility">
|
||||
<div class="demo-content">
|
||||
@for (item of shortContent; track item; let i = $index) {
|
||||
<p>{{ i + 1 }}. {{ item }}</p>
|
||||
}
|
||||
</div>
|
||||
</ui-scroll-container>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Virtual Scrolling -->
|
||||
<section class="demo-section">
|
||||
<h3>Virtual Scrolling</h3>
|
||||
<div class="demo-item">
|
||||
<p>Virtual scrolling with {{ largeDataset.length }} items (only visible items are rendered)</p>
|
||||
<ui-scroll-container
|
||||
[style.height.px]="300"
|
||||
[style.width.px]="400"
|
||||
[virtualScrollConfig]="virtualConfig"
|
||||
[items]="largeDataset"
|
||||
[itemTemplate]="itemTemplate"
|
||||
[trackByFn]="trackByItem">
|
||||
</ui-scroll-container>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Smooth Scrolling & Controls -->
|
||||
<section class="demo-section">
|
||||
<h3>Scroll Controls & Smooth Scrolling</h3>
|
||||
<div class="demo-controls">
|
||||
<button (click)="scrollToTop()">Scroll to Top</button>
|
||||
<button (click)="scrollToBottom()">Scroll to Bottom</button>
|
||||
<button (click)="scrollToMiddle()">Scroll to Middle</button>
|
||||
<button (click)="toggleSmoothScroll()">
|
||||
{{ smoothScrollEnabled ? 'Disable' : 'Enable' }} Smooth Scroll
|
||||
</button>
|
||||
</div>
|
||||
<ui-scroll-container
|
||||
#controllableScroll
|
||||
[style.height.px]="250"
|
||||
[style.width.px]="400"
|
||||
[scrollBehavior]="smoothScrollEnabled ? 'smooth' : 'auto'"
|
||||
[showScrollIndicators]="true">
|
||||
<div class="demo-content">
|
||||
@for (item of longContent; track item; let i = $index) {
|
||||
<p [id]="'item-' + i">{{ i + 1 }}. {{ item }}</p>
|
||||
}
|
||||
</div>
|
||||
</ui-scroll-container>
|
||||
</section>
|
||||
|
||||
<!-- Position Restoration -->
|
||||
<section class="demo-section">
|
||||
<h3>Scroll Position Restoration</h3>
|
||||
<p>Scroll position is saved and restored automatically (uses sessionStorage)</p>
|
||||
<ui-scroll-container
|
||||
[style.height.px]="200"
|
||||
[style.width.px]="400"
|
||||
[restoreScrollPosition]="true"
|
||||
scrollPositionKey="demo-scroll">
|
||||
<div class="demo-content">
|
||||
@for (item of longContent; track item; let i = $index) {
|
||||
<p>{{ i + 1 }}. {{ item }}</p>
|
||||
}
|
||||
</div>
|
||||
</ui-scroll-container>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Example</h3>
|
||||
<div class="demo-stats">
|
||||
<p>Current scroll position: {{ currentScrollPosition.top }}px (top), {{ currentScrollPosition.left }}px (left)</p>
|
||||
<p>Is scrolling: {{ isScrolling ? 'Yes' : 'No' }}</p>
|
||||
</div>
|
||||
<ui-scroll-container
|
||||
[style.height.px]="200"
|
||||
[style.width.px]="400"
|
||||
direction="both"
|
||||
(scrolled)="updateScrollPosition($event)"
|
||||
(scrollStart)="onScrollStart()"
|
||||
(scrollEnd)="onScrollEnd()">
|
||||
<div class="demo-large-content">
|
||||
<div class="demo-grid">
|
||||
@for (item of gridData; track item.id) {
|
||||
<div class="demo-grid-item">
|
||||
<h4>Item {{ item.id }}</h4>
|
||||
<p>{{ item.content }}</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ui-scroll-container>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Virtual Scrolling Item Template -->
|
||||
<ng-template #itemTemplate let-item="item" let-index="index">
|
||||
<div class="virtual-item">
|
||||
<strong>Item {{ index + 1 }}:</strong> {{ item.name }} - {{ item.description }}
|
||||
</div>
|
||||
</ng-template>
|
||||
`,
|
||||
styleUrl: './scroll-container-demo.component.scss'
|
||||
})
|
||||
export class ScrollContainerDemoComponent {
|
||||
@ViewChild('controllableScroll') controllableScroll!: ScrollContainerComponent;
|
||||
@ViewChild('itemTemplate') itemTemplate!: TemplateRef<any>;
|
||||
|
||||
// Content data
|
||||
longContent = [
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
|
||||
'Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
|
||||
'Ut enim ad minim veniam, quis nostrud exercitation ullamco.',
|
||||
'Duis aute irure dolor in reprehenderit in voluptate velit esse.',
|
||||
'Excepteur sint occaecat cupidatat non proident, sunt in culpa.',
|
||||
'Qui officia deserunt mollit anim id est laborum.',
|
||||
'At vero eos et accusamus et iusto odio dignissimos ducimus.',
|
||||
'Et harum quidem rerum facilis est et expedita distinctio.',
|
||||
'Nam libero tempore, cum soluta nobis est eligendi optio.',
|
||||
'Temporibus autem quibusdam et aut officiis debitis aut rerum.',
|
||||
'Itaque earum rerum hic tenetur a sapiente delectus.',
|
||||
'Ut aut reiciendis voluptatibus maiores alias consequatur.',
|
||||
'Sed ut perspiciatis unde omnis iste natus error sit.',
|
||||
'Voluptatem accusantium doloremque laudantium, totam rem.',
|
||||
'Aperiam, eaque ipsa quae ab illo inventore veritatis.',
|
||||
'Quasi architecto beatae vitae dicta sunt explicabo.',
|
||||
'Nemo enim ipsam voluptatem quia voluptas sit aspernatur.',
|
||||
'Neque porro quisquam est, qui dolorem ipsum quia dolor.',
|
||||
'Consectetur, adipisci velit, sed quia non numquam eius.',
|
||||
'Modi tempora incidunt ut labore et dolore magnam.'
|
||||
];
|
||||
|
||||
shortContent = this.longContent.slice(0, 8);
|
||||
|
||||
horizontalItems = [
|
||||
'Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5',
|
||||
'Item 6', 'Item 7', 'Item 8', 'Item 9', 'Item 10'
|
||||
];
|
||||
|
||||
tableData = Array.from({ length: 20 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: `Row ${i + 1}`,
|
||||
description: `Description for row ${i + 1} with some longer text`,
|
||||
value: Math.floor(Math.random() * 1000),
|
||||
status: i % 3 === 0 ? 'Active' : i % 3 === 1 ? 'Pending' : 'Inactive'
|
||||
}));
|
||||
|
||||
// Virtual scrolling data
|
||||
largeDataset = Array.from({ length: 10000 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: `Item ${i + 1}`,
|
||||
description: `This is a description for item number ${i + 1}. It contains some sample text to demonstrate virtual scrolling.`
|
||||
}));
|
||||
|
||||
virtualConfig: ScrollVirtualConfig = {
|
||||
itemHeight: 60,
|
||||
bufferSize: 5,
|
||||
enabled: true
|
||||
};
|
||||
|
||||
gridData = Array.from({ length: 50 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
content: `Grid item content ${i + 1} with some descriptive text that makes it interesting.`
|
||||
}));
|
||||
|
||||
// Options
|
||||
scrollbarOptions = ['auto', 'always', 'never'] as const;
|
||||
|
||||
// State
|
||||
topCount = 0;
|
||||
bottomCount = 0;
|
||||
smoothScrollEnabled = false;
|
||||
currentScrollPosition = { top: 0, left: 0 };
|
||||
isScrolling = false;
|
||||
|
||||
// Event handlers
|
||||
onScroll(position: any): void {
|
||||
// Handle scroll event if needed
|
||||
}
|
||||
|
||||
onReachedTop(): void {
|
||||
this.topCount++;
|
||||
}
|
||||
|
||||
onReachedBottom(): void {
|
||||
this.bottomCount++;
|
||||
}
|
||||
|
||||
updateScrollPosition(position: any): void {
|
||||
this.currentScrollPosition = position;
|
||||
}
|
||||
|
||||
onScrollStart(): void {
|
||||
this.isScrolling = true;
|
||||
}
|
||||
|
||||
onScrollEnd(): void {
|
||||
this.isScrolling = false;
|
||||
}
|
||||
|
||||
// Control methods
|
||||
scrollToTop(): void {
|
||||
this.controllableScroll?.scrollToTop();
|
||||
}
|
||||
|
||||
scrollToBottom(): void {
|
||||
this.controllableScroll?.scrollToBottom();
|
||||
}
|
||||
|
||||
scrollToMiddle(): void {
|
||||
this.controllableScroll?.scrollTo({ top: 1000 });
|
||||
}
|
||||
|
||||
toggleSmoothScroll(): void {
|
||||
this.smoothScrollEnabled = !this.smoothScrollEnabled;
|
||||
}
|
||||
|
||||
// Virtual scrolling
|
||||
trackByItem(item: any): any {
|
||||
return item.id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' 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-content-heading;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-example {
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
border-left: 3px solid $semantic-color-primary;
|
||||
padding-left: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.section-content {
|
||||
h2, h3, h4, h5 {
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-family: map-get($semantic-typography-heading-h5, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h5, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h5, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h5, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
padding-left: $semantic-spacing-component-md;
|
||||
|
||||
li {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-list-item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
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;
|
||||
font-family: map-get($semantic-typography-button-medium, font-family);
|
||||
font-size: map-get($semantic-typography-button-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-button-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-button-medium, line-height);
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SectionComponent } from '../../../../../ui-essentials/src/lib/components/layout/section/section.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-section-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, SectionComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Section Component Demo</h2>
|
||||
|
||||
<!-- Spacing Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Spacing Variants</h3>
|
||||
@for (spacing of spacings; track spacing) {
|
||||
<div class="demo-example">
|
||||
<h4>{{ spacing }} spacing</h4>
|
||||
<ui-section [spacing]="spacing" background="surface" [contained]=false>
|
||||
<div class="section-content">
|
||||
<h5>Section with {{ spacing }} spacing</h5>
|
||||
<p>This section demonstrates the {{ spacing }} spacing variant with consistent vertical padding.</p>
|
||||
</div>
|
||||
</ui-section>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- Background Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Background Variants</h3>
|
||||
@for (background of backgrounds; track background) {
|
||||
<div class="demo-example">
|
||||
<h4>{{ background }} background</h4>
|
||||
<ui-section spacing="md" [background]="background" [contained]="true">
|
||||
<div class="section-content">
|
||||
<h5>{{ background | titlecase }} Background Section</h5>
|
||||
<p>This section demonstrates the {{ background }} background variant.</p>
|
||||
</div>
|
||||
</ui-section>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- Alignment Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Text Alignment</h3>
|
||||
@for (align of alignments; track align) {
|
||||
<div class="demo-example">
|
||||
<h4>{{ align }} aligned</h4>
|
||||
<ui-section spacing="md" background="surface-secondary" [align]="align" [contained]="true">
|
||||
<div class="section-content">
|
||||
<h5>{{ align | titlecase }} Aligned Content</h5>
|
||||
<p>This section content is aligned to the {{ align }}.</p>
|
||||
</div>
|
||||
</ui-section>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- Width Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Width Variants</h3>
|
||||
<div class="demo-example">
|
||||
<h4>Contained (default)</h4>
|
||||
<ui-section spacing="md" background="surface" [contained]="true">
|
||||
<div class="section-content">
|
||||
<h5>Contained Section</h5>
|
||||
<p>This section is contained with max-width and centered margins.</p>
|
||||
</div>
|
||||
</ui-section>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Full Width</h4>
|
||||
<ui-section spacing="md" background="surface-elevated" [fullWidth]="true" [contained]="false">
|
||||
<div class="section-content">
|
||||
<h5>Full Width Section</h5>
|
||||
<p>This section spans the full width of its container without constraints.</p>
|
||||
</div>
|
||||
</ui-section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Accessibility Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Accessibility Features</h3>
|
||||
<div class="demo-example">
|
||||
<h4>With ARIA label</h4>
|
||||
<ui-section spacing="md" background="surface" ariaLabel="Main content area" [contained]="true">
|
||||
<div class="section-content">
|
||||
<h5>Accessible Section</h5>
|
||||
<p>This section includes proper ARIA labeling for screen readers.</p>
|
||||
</div>
|
||||
</ui-section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Real-world Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Real-world Examples</h3>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Hero Section</h4>
|
||||
<ui-section spacing="xl" background="surface-elevated" align="center" [contained]="true">
|
||||
<div class="section-content">
|
||||
<h2>Welcome to Our Platform</h2>
|
||||
<p>This is a hero section with extra large spacing and centered content.</p>
|
||||
<button class="demo-button">Get Started</button>
|
||||
</div>
|
||||
</ui-section>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Content Section</h4>
|
||||
<ui-section spacing="lg" background="transparent" [contained]="true">
|
||||
<div class="section-content">
|
||||
<h3>About Our Services</h3>
|
||||
<p>This is a standard content section with large spacing and transparent background.</p>
|
||||
<ul>
|
||||
<li>Feature one with detailed description</li>
|
||||
<li>Feature two with comprehensive details</li>
|
||||
<li>Feature three with full specifications</li>
|
||||
</ul>
|
||||
</div>
|
||||
</ui-section>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Footer Section</h4>
|
||||
<ui-section spacing="md" background="surface-secondary" [fullWidth]="true" [contained]="false">
|
||||
<div class="section-content" style="max-width: 1200px; margin: 0 auto; padding: 0 16px;">
|
||||
<h4>Contact Information</h4>
|
||||
<p>This footer section spans full width with contained inner content.</p>
|
||||
</div>
|
||||
</ui-section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './section-demo.component.scss'
|
||||
})
|
||||
export class SectionDemoComponent {
|
||||
spacings = ['xs', 'sm', 'md', 'lg', 'xl'] as const;
|
||||
backgrounds = ['transparent', 'surface', 'surface-secondary', 'surface-elevated'] as const;
|
||||
alignments = ['start', 'center', 'end'] as const;
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface;
|
||||
min-height: 100vh;
|
||||
|
||||
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;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
padding-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.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-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: $semantic-spacing-grid-gap-md;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
background: $semantic-color-surface-secondary;
|
||||
padding: $semantic-spacing-component-md;
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-demo-container {
|
||||
height: 300px;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
&--interactive {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
padding: $semantic-spacing-component-md;
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
h4, h5 {
|
||||
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-bottom: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
padding: $semantic-spacing-content-line-tight;
|
||||
color: $semantic-color-text-secondary;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sidebar content styling
|
||||
[slot="sidebar"] {
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
h5 {
|
||||
font-family: map-get($semantic-typography-heading-h5, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h5, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h5, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h5, line-height);
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
padding: $semantic-spacing-component-xs;
|
||||
margin-bottom: $semantic-spacing-component-xs;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
color: $semantic-color-text-secondary;
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-elevated;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nav ul li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
}
|
||||
}
|
||||
|
||||
.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-elevated;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
font-family: map-get($semantic-typography-label, font-family);
|
||||
font-size: map-get($semantic-typography-label, font-size);
|
||||
font-weight: map-get($semantic-typography-label, font-weight);
|
||||
line-height: map-get($semantic-typography-label, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
select, input {
|
||||
padding: $semantic-spacing-interactive-input-padding-y $semantic-spacing-interactive-input-padding-x;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-input-radius;
|
||||
background: $semantic-color-surface;
|
||||
color: $semantic-color-text-primary;
|
||||
font-family: map-get($semantic-typography-input, font-family);
|
||||
font-size: map-get($semantic-typography-input, font-size);
|
||||
transition: border-color $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
border-color: $semantic-color-border-focus;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: auto;
|
||||
margin-right: $semantic-spacing-component-xs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border: none;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
font-family: map-get($semantic-typography-button-medium, font-family);
|
||||
font-size: map-get($semantic-typography-button-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-button-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-button-medium, line-height);
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: $semantic-shadow-button-rest;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.demo-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
flex-direction: column;
|
||||
|
||||
label {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-demo-container {
|
||||
height: 250px;
|
||||
|
||||
&--interactive {
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { SidebarLayoutComponent } from '../../../../../ui-essentials/src/lib/components/layout/sidebar-layout/sidebar-layout.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-sidebar-layout-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, SidebarLayoutComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Sidebar Layout Demo</h2>
|
||||
|
||||
<!-- Position Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Position Variants</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-card">
|
||||
<h4>Left Sidebar (Default)</h4>
|
||||
<div class="sidebar-demo-container">
|
||||
<ui-sidebar-layout position="left" sidebarWidth="sm">
|
||||
<div slot="sidebar">
|
||||
<h5>Left Navigation</h5>
|
||||
<ul>
|
||||
<li>Dashboard</li>
|
||||
<li>Analytics</li>
|
||||
<li>Settings</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
<h4>Main Content Area</h4>
|
||||
<p>This is the main content area with left sidebar navigation.</p>
|
||||
</div>
|
||||
</ui-sidebar-layout>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Right Sidebar</h4>
|
||||
<div class="sidebar-demo-container">
|
||||
<ui-sidebar-layout position="right" sidebarWidth="sm">
|
||||
<div slot="sidebar">
|
||||
<h5>Right Panel</h5>
|
||||
<ul>
|
||||
<li>Quick Actions</li>
|
||||
<li>Recent Items</li>
|
||||
<li>Help</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
<h4>Main Content Area</h4>
|
||||
<p>This is the main content area with right sidebar panel.</p>
|
||||
</div>
|
||||
</ui-sidebar-layout>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Width Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Sidebar Widths</h3>
|
||||
<div class="demo-row">
|
||||
@for (width of widths; track width) {
|
||||
<div class="demo-card">
|
||||
<h4>{{ width.toUpperCase() }} Width</h4>
|
||||
<div class="sidebar-demo-container">
|
||||
<ui-sidebar-layout [sidebarWidth]="width">
|
||||
<div slot="sidebar">
|
||||
<h5>{{ width.toUpperCase() }} Sidebar</h5>
|
||||
<p>Width: {{ width }}</p>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
<h4>Content</h4>
|
||||
<p>Main content with {{ width }} sidebar.</p>
|
||||
</div>
|
||||
</ui-sidebar-layout>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Visual Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Visual Variants</h3>
|
||||
<div class="demo-row">
|
||||
@for (variant of variants; track variant) {
|
||||
<div class="demo-card">
|
||||
<h4>{{ variant | titlecase }}</h4>
|
||||
<div class="sidebar-demo-container">
|
||||
<ui-sidebar-layout [variant]="variant" sidebarWidth="sm">
|
||||
<div slot="sidebar">
|
||||
<h5>{{ variant | titlecase }} Style</h5>
|
||||
<ul>
|
||||
<li>Item One</li>
|
||||
<li>Item Two</li>
|
||||
<li>Item Three</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
<h4>Content</h4>
|
||||
<p>Content area with {{ variant }} sidebar variant.</p>
|
||||
</div>
|
||||
</ui-sidebar-layout>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Collapse States -->
|
||||
<section class="demo-section">
|
||||
<h3>Collapse States</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-card">
|
||||
<h4>Collapsed Sidebar</h4>
|
||||
<div class="sidebar-demo-container">
|
||||
<ui-sidebar-layout [collapsed]="true" collapseMode="collapsed">
|
||||
<div slot="sidebar">
|
||||
<h5>Nav</h5>
|
||||
<ul>
|
||||
<li>🏠</li>
|
||||
<li>📊</li>
|
||||
<li>⚙️</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
<h4>Collapsed Mode</h4>
|
||||
<p>Sidebar is collapsed showing minimal width.</p>
|
||||
</div>
|
||||
</ui-sidebar-layout>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Hidden Sidebar</h4>
|
||||
<div class="sidebar-demo-container">
|
||||
<ui-sidebar-layout [collapsed]="true" collapseMode="hidden">
|
||||
<div slot="sidebar">
|
||||
<h5>Hidden Sidebar</h5>
|
||||
<p>This sidebar is hidden when collapsed.</p>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
<h4>Hidden Mode</h4>
|
||||
<p>Sidebar is completely hidden, content takes full width.</p>
|
||||
</div>
|
||||
</ui-sidebar-layout>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Controls</h3>
|
||||
<div class="demo-controls">
|
||||
<label>
|
||||
Position:
|
||||
<select [(ngModel)]="interactivePosition">
|
||||
<option value="left">Left</option>
|
||||
<option value="right">Right</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Width:
|
||||
<select [(ngModel)]="interactiveWidth">
|
||||
<option value="sm">Small</option>
|
||||
<option value="md">Medium</option>
|
||||
<option value="lg">Large</option>
|
||||
<option value="xl">Extra Large</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Variant:
|
||||
<select [(ngModel)]="interactiveVariant">
|
||||
<option value="default">Default</option>
|
||||
<option value="bordered">Bordered</option>
|
||||
<option value="elevated">Elevated</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="interactiveCollapsed">
|
||||
Collapsed
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Collapse Mode:
|
||||
<select [(ngModel)]="interactiveCollapseMode">
|
||||
<option value="collapsed">Collapsed</option>
|
||||
<option value="hidden">Hidden</option>
|
||||
<option value="overlay">Overlay</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-demo-container sidebar-demo-container--interactive">
|
||||
<ui-sidebar-layout
|
||||
[position]="interactivePosition"
|
||||
[sidebarWidth]="interactiveWidth"
|
||||
[variant]="interactiveVariant"
|
||||
[collapsed]="interactiveCollapsed"
|
||||
[collapseMode]="interactiveCollapseMode">
|
||||
|
||||
<div slot="sidebar">
|
||||
<h5>Interactive Sidebar</h5>
|
||||
<nav>
|
||||
<ul>
|
||||
<li>📱 Dashboard</li>
|
||||
<li>📊 Analytics</li>
|
||||
<li>👥 Users</li>
|
||||
<li>⚙️ Settings</li>
|
||||
<li>❓ Help</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<h4>Interactive Demo</h4>
|
||||
<p>Use the controls above to test different sidebar configurations:</p>
|
||||
<ul>
|
||||
<li><strong>Position:</strong> {{ interactivePosition }}</li>
|
||||
<li><strong>Width:</strong> {{ interactiveWidth }}</li>
|
||||
<li><strong>Variant:</strong> {{ interactiveVariant }}</li>
|
||||
<li><strong>Collapsed:</strong> {{ interactiveCollapsed }}</li>
|
||||
<li><strong>Collapse Mode:</strong> {{ interactiveCollapseMode }}</li>
|
||||
</ul>
|
||||
|
||||
<button
|
||||
(click)="toggleCollapse()"
|
||||
class="demo-button">
|
||||
{{ interactiveCollapsed ? 'Expand' : 'Collapse' }} Sidebar
|
||||
</button>
|
||||
</div>
|
||||
</ui-sidebar-layout>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './sidebar-layout-demo.component.scss'
|
||||
})
|
||||
export class SidebarLayoutDemoComponent {
|
||||
widths = ['sm', 'md', 'lg', 'xl'] as const;
|
||||
variants = ['default', 'bordered', 'elevated'] as const;
|
||||
|
||||
// Interactive demo properties
|
||||
interactivePosition: 'left' | 'right' = 'left';
|
||||
interactiveWidth: 'sm' | 'md' | 'lg' | 'xl' = 'md';
|
||||
interactiveVariant: 'default' | 'bordered' | 'elevated' = 'default';
|
||||
interactiveCollapsed = false;
|
||||
interactiveCollapseMode: 'hidden' | 'collapsed' | 'overlay' = 'collapsed';
|
||||
|
||||
toggleCollapse(): void {
|
||||
this.interactiveCollapsed = !this.interactiveCollapsed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
h2 {
|
||||
font-family: map-get($semantic-typography-heading-h2, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h2, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h2, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h2, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-layout-section-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
|
||||
h3 {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-family: map-get($semantic-typography-heading-h5, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h5, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h5, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h5, line-height);
|
||||
color: $semantic-color-text-tertiary;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-example {
|
||||
margin-bottom: $semantic-spacing-layout-section-sm;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-column {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
text-align: center;
|
||||
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);
|
||||
|
||||
&.small {
|
||||
background: $semantic-color-success;
|
||||
color: $semantic-color-on-success;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
background: $semantic-color-warning;
|
||||
color: $semantic-color-on-warning;
|
||||
}
|
||||
|
||||
&.large {
|
||||
background: $semantic-color-danger;
|
||||
color: $semantic-color-on-danger;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section-header,
|
||||
.demo-section-footer {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
text-align: center;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
.alignment-demo {
|
||||
border: $semantic-border-width-2 dashed $semantic-color-border-secondary;
|
||||
|
||||
.alignment-container {
|
||||
background: $semantic-color-surface;
|
||||
min-height: 120px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.alignment-container-horizontal {
|
||||
background: $semantic-color-surface;
|
||||
min-height: 80px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.justify-container {
|
||||
background: $semantic-color-surface;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
min-height: 60px;
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HStackComponent, StackComponent, VStackComponent } from '../../../../../ui-essentials/src/lib/components/layout';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-stack-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, StackComponent, VStackComponent, HStackComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Stack Components Demo</h2>
|
||||
|
||||
<!-- Basic Stack (column) -->
|
||||
<section class="demo-section">
|
||||
<h3>Basic Stack (Column)</h3>
|
||||
<div class="demo-example">
|
||||
<ui-stack spacing="md">
|
||||
<div class="demo-item">Item 1</div>
|
||||
<div class="demo-item">Item 2</div>
|
||||
<div class="demo-item">Item 3</div>
|
||||
</ui-stack>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stack Row -->
|
||||
<section class="demo-section">
|
||||
<h3>Stack (Row)</h3>
|
||||
<div class="demo-example">
|
||||
<ui-stack direction="row" spacing="md">
|
||||
<div class="demo-item">Item 1</div>
|
||||
<div class="demo-item">Item 2</div>
|
||||
<div class="demo-item">Item 3</div>
|
||||
</ui-stack>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- VStack Variations -->
|
||||
<section class="demo-section">
|
||||
<h3>VStack (Vertical Stack)</h3>
|
||||
<div class="demo-row">
|
||||
@for (spacing of spacings; track spacing) {
|
||||
<div class="demo-column">
|
||||
<h4>{{ spacing }} spacing</h4>
|
||||
<ui-vstack [spacing]="spacing">
|
||||
<div class="demo-item">Item A</div>
|
||||
<div class="demo-item">Item B</div>
|
||||
<div class="demo-item">Item C</div>
|
||||
</ui-vstack>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- HStack Variations -->
|
||||
<section class="demo-section">
|
||||
<h3>HStack (Horizontal Stack)</h3>
|
||||
<div class="demo-column">
|
||||
@for (spacing of spacings; track spacing) {
|
||||
<div class="demo-example">
|
||||
<h4>{{ spacing }} spacing</h4>
|
||||
<ui-hstack [spacing]="spacing">
|
||||
<div class="demo-item">Item A</div>
|
||||
<div class="demo-item">Item B</div>
|
||||
<div class="demo-item">Item C</div>
|
||||
</ui-hstack>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Alignment Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Alignment Options</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-column">
|
||||
<h4>VStack Alignment</h4>
|
||||
@for (align of alignments; track align) {
|
||||
<div class="demo-example alignment-demo">
|
||||
<h5>align="{{ align }}"</h5>
|
||||
<ui-vstack [align]="align" spacing="sm" class="alignment-container">
|
||||
<div class="demo-item small">Small</div>
|
||||
<div class="demo-item medium">Medium Item</div>
|
||||
<div class="demo-item large">Large Item Here</div>
|
||||
</ui-vstack>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="demo-column">
|
||||
<h4>HStack Alignment</h4>
|
||||
@for (align of alignments; track align) {
|
||||
<div class="demo-example alignment-demo">
|
||||
<h5>align="{{ align }}"</h5>
|
||||
<ui-hstack [align]="align" spacing="sm" class="alignment-container-horizontal">
|
||||
<div class="demo-item small">Small</div>
|
||||
<div class="demo-item medium">Medium<br>Two lines</div>
|
||||
<div class="demo-item large">Large<br>Item<br>Three lines</div>
|
||||
</ui-hstack>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Justify Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Justify Options</h3>
|
||||
<div class="demo-column">
|
||||
<h4>HStack Justify (full width)</h4>
|
||||
@for (justify of justifyOptions; track justify) {
|
||||
<div class="demo-example">
|
||||
<h5>justify="{{ justify }}"</h5>
|
||||
<ui-hstack [justify]="justify" spacing="sm" [fullWidth]="true" class="justify-container">
|
||||
<div class="demo-item">A</div>
|
||||
<div class="demo-item">B</div>
|
||||
<div class="demo-item">C</div>
|
||||
</ui-hstack>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Divider Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>With Dividers</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-column">
|
||||
<h4>VStack with Dividers</h4>
|
||||
<ui-vstack [divider]="true">
|
||||
<div class="demo-item">Section One</div>
|
||||
<div class="demo-item">Section Two</div>
|
||||
<div class="demo-item">Section Three</div>
|
||||
</ui-vstack>
|
||||
</div>
|
||||
|
||||
<div class="demo-column">
|
||||
<h4>HStack with Dividers</h4>
|
||||
<ui-hstack [divider]="true">
|
||||
<div class="demo-item">Left</div>
|
||||
<div class="demo-item">Center</div>
|
||||
<div class="demo-item">Right</div>
|
||||
</ui-hstack>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Responsive Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Responsive Behavior</h3>
|
||||
<p>The HStack below converts to a VStack on small screens:</p>
|
||||
<ui-hstack spacing="lg" [responsive]="true" align="center">
|
||||
<div class="demo-item">Item 1</div>
|
||||
<div class="demo-item">Item 2</div>
|
||||
<div class="demo-item">Item 3</div>
|
||||
<div class="demo-item">Item 4</div>
|
||||
</ui-hstack>
|
||||
</section>
|
||||
|
||||
<!-- Nested Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Nested Stacks</h3>
|
||||
<ui-vstack spacing="lg">
|
||||
<div class="demo-section-header">Header Section</div>
|
||||
<ui-hstack spacing="md" align="start">
|
||||
<ui-vstack spacing="sm">
|
||||
<div class="demo-item">Left Column</div>
|
||||
<div class="demo-item">Item 2</div>
|
||||
<div class="demo-item">Item 3</div>
|
||||
</ui-vstack>
|
||||
<ui-vstack spacing="sm">
|
||||
<div class="demo-item">Right Column</div>
|
||||
<div class="demo-item">Item B</div>
|
||||
<div class="demo-item">Item C</div>
|
||||
</ui-vstack>
|
||||
</ui-hstack>
|
||||
<div class="demo-section-footer">Footer Section</div>
|
||||
</ui-vstack>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './stack-demo.component.scss'
|
||||
})
|
||||
export class StackDemoComponent {
|
||||
spacings = ['xs', 'sm', 'md', 'lg', 'xl'] as const;
|
||||
alignments = ['start', 'center', 'end', 'stretch'] as const;
|
||||
justifyOptions = ['start', 'center', 'end', 'between', 'around', 'evenly'] as const;
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
max-width: 100%;
|
||||
|
||||
h2 {
|
||||
font-family: map-get($semantic-typography-heading-h2, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h2, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h2, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h2, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-lg;
|
||||
|
||||
h3 {
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
padding-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
|
||||
gap: $semantic-spacing-component-lg;
|
||||
margin-top: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-layout-preview {
|
||||
height: 400px;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
overflow: hidden;
|
||||
margin-top: $semantic-spacing-component-sm;
|
||||
|
||||
.ui-supporting-pane-layout {
|
||||
min-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
margin: $semantic-spacing-component-md 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.demo-btn {
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
background: $semantic-color-surface;
|
||||
color: $semantic-color-text-primary;
|
||||
font-family: map-get($semantic-typography-button-medium, font-family);
|
||||
font-size: map-get($semantic-typography-button-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-button-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-button-medium, line-height);
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-interactive-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border-color: $semantic-color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-md;
|
||||
margin: $semantic-spacing-component-md 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.demo-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
cursor: pointer;
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
// Demo content styles
|
||||
.demo-main-content {
|
||||
padding: $semantic-spacing-component-md;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
margin-top: 0;
|
||||
|
||||
fa-icon {
|
||||
color: $semantic-color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: $semantic-spacing-component-sm 0;
|
||||
padding-left: $semantic-spacing-component-md;
|
||||
|
||||
li {
|
||||
margin-bottom: $semantic-spacing-component-xs;
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-content-blocks {
|
||||
display: grid;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
margin-top: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-content-block {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
|
||||
h4 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
margin: 0 0 $semantic-spacing-component-xs 0;
|
||||
|
||||
fa-icon {
|
||||
color: $semantic-color-primary;
|
||||
font-size: $semantic-sizing-icon-inline;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-family: map-get($semantic-typography-body-small, font-family);
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: map-get($semantic-typography-body-small, font-weight);
|
||||
line-height: map-get($semantic-typography-body-small, line-height);
|
||||
}
|
||||
}
|
||||
|
||||
.demo-pane-content {
|
||||
margin-top: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
.demo-setting-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
padding: $semantic-spacing-component-xs 0;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
cursor: pointer;
|
||||
transition: color $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $semantic-color-primary;
|
||||
}
|
||||
|
||||
fa-icon {
|
||||
color: $semantic-color-text-secondary;
|
||||
font-size: $semantic-sizing-icon-inline;
|
||||
transition: color $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
}
|
||||
|
||||
&:hover fa-icon {
|
||||
color: $semantic-color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-tag-item {
|
||||
display: inline-block;
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
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-medium;
|
||||
line-height: map-get($semantic-typography-body-small, line-height);
|
||||
margin: $semantic-spacing-component-xs $semantic-spacing-component-xs $semantic-spacing-component-xs 0;
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-secondary;
|
||||
color: $semantic-color-on-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-info-item,
|
||||
.demo-config-item {
|
||||
padding: $semantic-spacing-component-xs 0;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
font-family: map-get($semantic-typography-body-small, font-family);
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: map-get($semantic-typography-body-small, font-weight);
|
||||
line-height: map-get($semantic-typography-body-small, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: $semantic-color-text-primary;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-variant-info {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
text-align: 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-medium;
|
||||
line-height: map-get($semantic-typography-body-small, line-height);
|
||||
color: $semantic-color-primary;
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 1200px) {
|
||||
.demo-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.demo-layout-preview {
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
.demo-layout-preview {
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { SupportingPaneLayoutComponent } from 'ui-essentials';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import { faHome, faCog, faUser, faChartBar, faFileText, faBell, faBookmark, faTag, faCalendar, faComment } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-supporting-pane-layout-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, SupportingPaneLayoutComponent, FontAwesomeModule],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Supporting Pane Layout Demo</h2>
|
||||
<p>A layout component with main content and a collapsible supporting pane for additional information, tools, or navigation.</p>
|
||||
|
||||
<!-- Position Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Position Variants</h3>
|
||||
<div class="demo-grid">
|
||||
<!-- Right Position (Default) -->
|
||||
<div class="demo-card">
|
||||
<h4>Right Position (Default)</h4>
|
||||
<div class="demo-layout-preview">
|
||||
<ui-supporting-pane-layout
|
||||
position="right"
|
||||
[collapsed]="rightPaneCollapsed"
|
||||
(paneToggled)="rightPaneCollapsed = $event">
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="demo-main-content">
|
||||
<h3><fa-icon [icon]="faHome"></fa-icon> Main Content Area</h3>
|
||||
<p>This is the primary content area where your main application content would be displayed.</p>
|
||||
<div class="demo-content-blocks">
|
||||
<div class="demo-content-block">
|
||||
<h4><fa-icon [icon]="faChartBar"></fa-icon> Analytics Dashboard</h4>
|
||||
<p>Key metrics and performance indicators would be shown here.</p>
|
||||
</div>
|
||||
<div class="demo-content-block">
|
||||
<h4><fa-icon [icon]="faFileText"></fa-icon> Recent Documents</h4>
|
||||
<p>A list of recently accessed or modified documents.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Supporting Pane -->
|
||||
<div slot="supporting-pane">
|
||||
<h4><fa-icon [icon]="faCog"></fa-icon> Quick Settings</h4>
|
||||
<div class="demo-pane-content">
|
||||
<div class="demo-setting-item">
|
||||
<fa-icon [icon]="faBell"></fa-icon>
|
||||
<span>Notifications</span>
|
||||
</div>
|
||||
<div class="demo-setting-item">
|
||||
<fa-icon [icon]="faUser"></fa-icon>
|
||||
<span>Profile Settings</span>
|
||||
</div>
|
||||
<div class="demo-setting-item">
|
||||
<fa-icon [icon]="faBookmark"></fa-icon>
|
||||
<span>Saved Items</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pane Header -->
|
||||
<div slot="pane-header">
|
||||
<span><fa-icon [icon]="faCog"></fa-icon> Tools</span>
|
||||
</div>
|
||||
</ui-supporting-pane-layout>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Left Position -->
|
||||
<div class="demo-card">
|
||||
<h4>Left Position</h4>
|
||||
<div class="demo-layout-preview">
|
||||
<ui-supporting-pane-layout
|
||||
position="left"
|
||||
[collapsed]="leftPaneCollapsed"
|
||||
(paneToggled)="leftPaneCollapsed = $event">
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="demo-main-content">
|
||||
<h3><fa-icon [icon]="faHome"></fa-icon> Main Content Area</h3>
|
||||
<p>Content area with supporting pane on the left side.</p>
|
||||
<div class="demo-content-blocks">
|
||||
<div class="demo-content-block">
|
||||
<h4><fa-icon [icon]="faComment"></fa-icon> Messages</h4>
|
||||
<p>Recent messages and communications.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Supporting Pane -->
|
||||
<div slot="supporting-pane">
|
||||
<h4><fa-icon [icon]="faTag"></fa-icon> Tags & Categories</h4>
|
||||
<div class="demo-pane-content">
|
||||
<div class="demo-tag-item">Design</div>
|
||||
<div class="demo-tag-item">Development</div>
|
||||
<div class="demo-tag-item">Marketing</div>
|
||||
<div class="demo-tag-item">Research</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="pane-header">
|
||||
<span><fa-icon [icon]="faTag"></fa-icon> Categories</span>
|
||||
</div>
|
||||
</ui-supporting-pane-layout>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Size Variants</h3>
|
||||
<div class="demo-row">
|
||||
<button class="demo-btn" [class.active]="selectedSize === size"
|
||||
*ngFor="let size of sizes" (click)="selectedSize = size">
|
||||
{{ size.toUpperCase() }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="demo-layout-preview">
|
||||
<ui-supporting-pane-layout
|
||||
[paneSize]="selectedSize"
|
||||
[collapsed]="sizePaneCollapsed"
|
||||
(paneToggled)="sizePaneCollapsed = $event">
|
||||
|
||||
<div class="demo-main-content">
|
||||
<h3>Size: {{ selectedSize.toUpperCase() }}</h3>
|
||||
<p>The supporting pane width changes based on the selected size variant.</p>
|
||||
</div>
|
||||
|
||||
<div slot="supporting-pane">
|
||||
<h4>Supporting Information</h4>
|
||||
<p>Pane size: {{ selectedSize }}</p>
|
||||
<div class="demo-pane-content">
|
||||
<div class="demo-info-item">Current size: {{ selectedSize.toUpperCase() }}</div>
|
||||
<div class="demo-info-item">Responsive: Yes</div>
|
||||
<div class="demo-info-item">Collapsible: Yes</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="pane-header">
|
||||
<span>Size: {{ selectedSize.toUpperCase() }}</span>
|
||||
</div>
|
||||
</ui-supporting-pane-layout>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Visual Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Visual Variants</h3>
|
||||
<div class="demo-row">
|
||||
<button class="demo-btn" [class.active]="selectedVariant === variant"
|
||||
*ngFor="let variant of variants" (click)="selectedVariant = variant">
|
||||
{{ variant | titlecase }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="demo-layout-preview">
|
||||
<ui-supporting-pane-layout
|
||||
[variant]="selectedVariant"
|
||||
[collapsed]="variantPaneCollapsed"
|
||||
(paneToggled)="variantPaneCollapsed = $event">
|
||||
|
||||
<div class="demo-main-content">
|
||||
<h3>Variant: {{ selectedVariant | titlecase }}</h3>
|
||||
<p>Different visual styles for the supporting pane.</p>
|
||||
<div class="demo-content-blocks">
|
||||
<div class="demo-content-block">
|
||||
<h4>Visual Style</h4>
|
||||
<p>Current variant: <strong>{{ selectedVariant }}</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="supporting-pane">
|
||||
<h4>Variant Demo</h4>
|
||||
<div class="demo-pane-content">
|
||||
<div class="demo-variant-info">
|
||||
<strong>{{ selectedVariant | titlecase }}</strong> variant applied
|
||||
</div>
|
||||
<p>This demonstrates the {{ selectedVariant }} visual styling.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="pane-header">
|
||||
<span>{{ selectedVariant | titlecase }} Style</span>
|
||||
</div>
|
||||
</ui-supporting-pane-layout>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features Demo -->
|
||||
<section class="demo-section">
|
||||
<h3>Features & Options</h3>
|
||||
|
||||
<div class="demo-controls">
|
||||
<label class="demo-checkbox">
|
||||
<input type="checkbox" [(ngModel)]="stickyPane">
|
||||
Sticky Pane
|
||||
</label>
|
||||
<label class="demo-checkbox">
|
||||
<input type="checkbox" [(ngModel)]="alwaysVisible">
|
||||
Always Visible (No responsive collapse)
|
||||
</label>
|
||||
<label class="demo-checkbox">
|
||||
<input type="checkbox" [(ngModel)]="showFooter">
|
||||
Show Pane Footer
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="demo-layout-preview">
|
||||
<ui-supporting-pane-layout
|
||||
[stickyPane]="stickyPane"
|
||||
[alwaysVisible]="alwaysVisible"
|
||||
[showPaneFooter]="showFooter"
|
||||
[collapsed]="featuresPaneCollapsed"
|
||||
(paneToggled)="featuresPaneCollapsed = $event"
|
||||
(paneCollapsed)="onPaneCollapsed()"
|
||||
(paneExpanded)="onPaneExpanded()">
|
||||
|
||||
<div class="demo-main-content">
|
||||
<h3><fa-icon [icon]="faCalendar"></fa-icon> Features Demo</h3>
|
||||
<p>This demo shows various configuration options:</p>
|
||||
<ul>
|
||||
<li><strong>Sticky Pane:</strong> {{ stickyPane ? 'Enabled' : 'Disabled' }}</li>
|
||||
<li><strong>Always Visible:</strong> {{ alwaysVisible ? 'Enabled' : 'Disabled' }}</li>
|
||||
<li><strong>Show Footer:</strong> {{ showFooter ? 'Enabled' : 'Disabled' }}</li>
|
||||
</ul>
|
||||
|
||||
<div class="demo-content-blocks">
|
||||
<div class="demo-content-block" *ngFor="let i of [1,2,3,4,5,6,7,8]">
|
||||
<h4>Content Block {{ i }}</h4>
|
||||
<p>This is example content to demonstrate scrolling behavior when sticky pane is enabled.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="supporting-pane">
|
||||
<h4><fa-icon [icon]="faCog"></fa-icon> Configuration</h4>
|
||||
<div class="demo-pane-content">
|
||||
<div class="demo-config-item">
|
||||
<strong>Sticky:</strong> {{ stickyPane ? 'Yes' : 'No' }}
|
||||
</div>
|
||||
<div class="demo-config-item">
|
||||
<strong>Always Visible:</strong> {{ alwaysVisible ? 'Yes' : 'No' }}
|
||||
</div>
|
||||
<div class="demo-config-item">
|
||||
<strong>Footer:</strong> {{ showFooter ? 'Shown' : 'Hidden' }}
|
||||
</div>
|
||||
<div class="demo-config-item">
|
||||
<strong>Events:</strong> {{ eventCount }} triggered
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="pane-header">
|
||||
<span><fa-icon [icon]="faCog"></fa-icon> Options</span>
|
||||
</div>
|
||||
|
||||
<div slot="pane-footer" *ngIf="showFooter">
|
||||
<small>Last event: {{ lastEvent }}</small>
|
||||
</div>
|
||||
</ui-supporting-pane-layout>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './supporting-pane-layout-demo.component.scss'
|
||||
})
|
||||
export class SupportingPaneLayoutDemoComponent {
|
||||
// FontAwesome icons
|
||||
faHome = faHome;
|
||||
faCog = faCog;
|
||||
faUser = faUser;
|
||||
faChartBar = faChartBar;
|
||||
faFileText = faFileText;
|
||||
faBell = faBell;
|
||||
faBookmark = faBookmark;
|
||||
faTag = faTag;
|
||||
faCalendar = faCalendar;
|
||||
faComment = faComment;
|
||||
|
||||
// Demo state
|
||||
sizes = ['sm', 'md', 'lg', 'xl'] as const;
|
||||
variants = ['default', 'bordered', 'elevated', 'subtle'] as const;
|
||||
|
||||
selectedSize: 'sm' | 'md' | 'lg' | 'xl' = 'md';
|
||||
selectedVariant: 'default' | 'bordered' | 'elevated' | 'subtle' = 'default';
|
||||
|
||||
// Pane collapsed states
|
||||
rightPaneCollapsed = false;
|
||||
leftPaneCollapsed = false;
|
||||
sizePaneCollapsed = false;
|
||||
variantPaneCollapsed = false;
|
||||
featuresPaneCollapsed = false;
|
||||
|
||||
// Feature options
|
||||
stickyPane = false;
|
||||
alwaysVisible = false;
|
||||
showFooter = false;
|
||||
|
||||
// Event tracking
|
||||
eventCount = 0;
|
||||
lastEvent = 'None';
|
||||
|
||||
onPaneCollapsed(): void {
|
||||
this.eventCount++;
|
||||
this.lastEvent = 'Pane Collapsed';
|
||||
console.log('Supporting pane collapsed');
|
||||
}
|
||||
|
||||
onPaneExpanded(): void {
|
||||
this.eventCount++;
|
||||
this.lastEvent = 'Pane Expanded';
|
||||
console.log('Supporting pane expanded');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-lg;
|
||||
|
||||
h3 {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface;
|
||||
|
||||
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-heading 0;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-info {
|
||||
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-top: $semantic-spacing-component-md;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
border-left: 3px solid $semantic-color-info;
|
||||
}
|
||||
|
||||
.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-sm;
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
font-family: map-get($semantic-typography-label, font-family);
|
||||
font-size: map-get($semantic-typography-label, font-size);
|
||||
font-weight: map-get($semantic-typography-label, font-weight);
|
||||
line-height: map-get($semantic-typography-label, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
select {
|
||||
padding: $semantic-spacing-interactive-input-padding-y $semantic-spacing-interactive-input-padding-x;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-input-radius;
|
||||
background: $semantic-color-surface;
|
||||
color: $semantic-color-text-primary;
|
||||
font-family: map-get($semantic-typography-input, font-family);
|
||||
font-size: map-get($semantic-typography-input, font-size);
|
||||
min-width: 120px;
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
border-color: $semantic-color-focus;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
align-self: flex-start;
|
||||
margin-top: $semantic-spacing-component-xs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-status {
|
||||
margin-top: $semantic-spacing-component-lg;
|
||||
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;
|
||||
|
||||
p {
|
||||
margin: 0 0 $semantic-spacing-component-xs 0;
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: $semantic-color-text-primary;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: $semantic-breakpoint-lg - 1) {
|
||||
.demo-row {
|
||||
gap: $semantic-spacing-layout-section-sm;
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-sm;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
flex-direction: column;
|
||||
|
||||
label {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
select {
|
||||
min-width: auto;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { TabsContainerComponent, Tab } from '../../../../../ui-essentials/src/lib/components/layout/tabs-container/tabs-container.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-tabs-container-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, TabsContainerComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Tabs Container Demo</h2>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Sizes</h3>
|
||||
<div class="demo-row">
|
||||
@for (size of sizes; track size) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ size | titlecase }} Size</h4>
|
||||
<ui-tabs-container
|
||||
[tabs]="basicTabs()"
|
||||
[size]="size"
|
||||
(tabSelected)="handleTabSelect('size-' + size, $event)">
|
||||
<div>Content for {{ size }} tabs. Use tab selection to see different content.</div>
|
||||
</ui-tabs-container>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Variant Styles -->
|
||||
<section class="demo-section">
|
||||
<h3>Variants</h3>
|
||||
<div class="demo-row">
|
||||
@for (variant of variants; track variant) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ variant | titlecase }} Variant</h4>
|
||||
<ui-tabs-container
|
||||
[tabs]="basicTabs()"
|
||||
[variant]="variant"
|
||||
(tabSelected)="handleTabSelect('variant-' + variant, $event)">
|
||||
<div>{{ variant | titlecase }} variant tabs with different styling approaches.</div>
|
||||
</ui-tabs-container>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Position Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Tab Positions</h3>
|
||||
<div class="demo-row">
|
||||
@for (position of positions; track position) {
|
||||
<div class="demo-item" [style.height]="position === 'left' || position === 'right' ? '300px' : 'auto'">
|
||||
<h4>{{ position | titlecase }} Position</h4>
|
||||
<ui-tabs-container
|
||||
[tabs]="basicTabs()"
|
||||
[position]="position"
|
||||
(tabSelected)="handleTabSelect('position-' + position, $event)">
|
||||
<div>Tabs positioned {{ position }} with appropriate navigation layout.</div>
|
||||
</ui-tabs-container>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Closeable Tabs -->
|
||||
<section class="demo-section">
|
||||
<h3>Closeable Tabs</h3>
|
||||
<div class="demo-item">
|
||||
<ui-tabs-container
|
||||
[tabs]="closeableTabs()"
|
||||
(tabSelected)="handleTabSelect('closeable', $event)"
|
||||
(tabClosed)="handleTabClose($event)">
|
||||
<div>Closeable tabs demo - try closing tabs with the × button. Some tabs cannot be closed.</div>
|
||||
</ui-tabs-container>
|
||||
<p class="demo-info">Close tabs by clicking the × button. Some tabs cannot be closed.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tabs with Icons -->
|
||||
<section class="demo-section">
|
||||
<h3>Tabs with Icons</h3>
|
||||
<div class="demo-item">
|
||||
<ui-tabs-container
|
||||
[tabs]="iconTabs()"
|
||||
(tabSelected)="handleTabSelect('icons', $event)">
|
||||
<div>Tabs with icons provide visual context and better user experience.</div>
|
||||
</ui-tabs-container>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Scrollable Tabs -->
|
||||
<section class="demo-section">
|
||||
<h3>Scrollable Tabs</h3>
|
||||
<div class="demo-item">
|
||||
<ui-tabs-container
|
||||
[tabs]="manyTabs()"
|
||||
[scrollable]="true"
|
||||
(tabSelected)="handleTabSelect('scrollable', $event)">
|
||||
<div>Scrollable tabs for when you have many tabs that exceed container width.</div>
|
||||
</ui-tabs-container>
|
||||
<p class="demo-info">Use scroll controls when tabs overflow the container width.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Reorderable Tabs -->
|
||||
<section class="demo-section">
|
||||
<h3>Reorderable Tabs</h3>
|
||||
<div class="demo-item">
|
||||
<ui-tabs-container
|
||||
[tabs]="reorderableTabs()"
|
||||
[reorderable]="true"
|
||||
(tabSelected)="handleTabSelect('reorderable', $event)"
|
||||
(tabsReordered)="handleTabsReorder($event)">
|
||||
<div>Drag and drop reorderable tabs - try dragging tabs to rearrange them.</div>
|
||||
</ui-tabs-container>
|
||||
<p class="demo-info">Drag tabs to reorder them. Order changes: {{ reorderCount() }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Lazy Loading -->
|
||||
<section class="demo-section">
|
||||
<h3>Lazy Loading</h3>
|
||||
<div class="demo-item">
|
||||
<ui-tabs-container
|
||||
[tabs]="lazyTabs()"
|
||||
[lazyLoad]="true"
|
||||
(tabSelected)="handleTabSelect('lazy', $event)">
|
||||
<div>Lazy loading tabs optimize performance by loading content only when needed.</div>
|
||||
</ui-tabs-container>
|
||||
<p class="demo-info">Lazy tabs only load content when first selected.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Disabled Tabs -->
|
||||
<section class="demo-section">
|
||||
<h3>Disabled States</h3>
|
||||
<div class="demo-item">
|
||||
<ui-tabs-container
|
||||
[tabs]="disabledTabs()"
|
||||
(tabSelected)="handleTabSelect('disabled', $event)">
|
||||
<div>Disabled tabs prevent user interaction while maintaining visual hierarchy.</div>
|
||||
</ui-tabs-container>
|
||||
<p class="demo-info">Disabled tabs cannot be selected or interacted with.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Example</h3>
|
||||
<div class="demo-item">
|
||||
<div class="demo-controls">
|
||||
<label>
|
||||
Size:
|
||||
<select [(ngModel)]="interactiveSize" (change)="updateInteractiveConfig()">
|
||||
@for (size of sizes; track size) {
|
||||
<option [value]="size">{{ size | titlecase }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Variant:
|
||||
<select [(ngModel)]="interactiveVariant" (change)="updateInteractiveConfig()">
|
||||
@for (variant of variants; track variant) {
|
||||
<option [value]="variant">{{ variant | titlecase }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Position:
|
||||
<select [(ngModel)]="interactivePosition" (change)="updateInteractiveConfig()">
|
||||
@for (position of positions; track position) {
|
||||
<option [value]="position">{{ position | titlecase }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="interactiveScrollable" (change)="updateInteractiveConfig()">
|
||||
Scrollable
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="interactiveReorderable" (change)="updateInteractiveConfig()">
|
||||
Reorderable
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<ui-tabs-container
|
||||
[tabs]="interactiveTabs()"
|
||||
[size]="interactiveSize"
|
||||
[variant]="interactiveVariant"
|
||||
[position]="interactivePosition"
|
||||
[scrollable]="interactiveScrollable"
|
||||
[reorderable]="interactiveReorderable"
|
||||
(tabSelected)="handleTabSelect('interactive', $event)"
|
||||
(tabClosed)="handleInteractiveTabClose($event)"
|
||||
(tabsReordered)="handleInteractiveTabsReorder($event)">
|
||||
<div>Interactive demo with configurable options - use controls above to customize behavior.</div>
|
||||
</ui-tabs-container>
|
||||
|
||||
<div class="demo-status">
|
||||
<p><strong>Current tab:</strong> {{ activeTab() || 'None' }}</p>
|
||||
<p><strong>Tab selections:</strong> {{ tabSelectCount() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './tabs-container-demo.component.scss'
|
||||
})
|
||||
export class TabsContainerDemoComponent {
|
||||
sizes = ['sm', 'md', 'lg'] as const;
|
||||
variants = ['default', 'filled', 'pills', 'underlined'] as const;
|
||||
positions = ['top', 'bottom', 'left', 'right'] as const;
|
||||
|
||||
// Demo state
|
||||
activeTab = signal<string>('');
|
||||
tabSelectCount = signal(0);
|
||||
reorderCount = signal(0);
|
||||
|
||||
// Interactive demo controls
|
||||
interactiveSize: 'sm' | 'md' | 'lg' = 'md';
|
||||
interactiveVariant: 'default' | 'filled' | 'pills' | 'underlined' = 'default';
|
||||
interactivePosition: 'top' | 'bottom' | 'left' | 'right' = 'top';
|
||||
interactiveScrollable = false;
|
||||
interactiveReorderable = false;
|
||||
|
||||
// Tab collections
|
||||
private _closeableTabs = signal<Tab[]>([
|
||||
{ id: 'doc1', label: 'Document 1', closeable: true },
|
||||
{ id: 'doc2', label: 'Document 2', closeable: true },
|
||||
{ id: 'doc3', label: 'Important Doc', closeable: false },
|
||||
{ id: 'doc4', label: 'Temp File', closeable: true }
|
||||
]);
|
||||
|
||||
private _reorderableTabs = signal<Tab[]>([
|
||||
{ id: 'order1', label: 'First' },
|
||||
{ id: 'order2', label: 'Second' },
|
||||
{ id: 'order3', label: 'Third' },
|
||||
{ id: 'order4', label: 'Fourth' }
|
||||
]);
|
||||
|
||||
private _interactiveTabs = signal<Tab[]>([
|
||||
{ id: 'int1', label: 'Tab One', closeable: true, icon: '1️⃣' },
|
||||
{ id: 'int2', label: 'Tab Two', closeable: true, icon: '2️⃣' },
|
||||
{ id: 'int3', label: 'Tab Three', closeable: false, icon: '3️⃣' },
|
||||
{ id: 'int4', label: 'Tab Four', closeable: true, icon: '4️⃣' }
|
||||
]);
|
||||
|
||||
basicTabs(): Tab[] {
|
||||
return [
|
||||
{ id: 'tab1', label: 'Tab 1' },
|
||||
{ id: 'tab2', label: 'Tab 2' },
|
||||
{ id: 'tab3', label: 'Tab 3' }
|
||||
];
|
||||
}
|
||||
|
||||
iconTabs(): Tab[] {
|
||||
return [
|
||||
{ id: 'home', label: 'Home', icon: '🏠' },
|
||||
{ id: 'settings', label: 'Settings', icon: '⚙️' },
|
||||
{ id: 'profile', label: 'Profile', icon: '👤' },
|
||||
{ id: 'notifications', label: 'Notifications', icon: '🔔' }
|
||||
];
|
||||
}
|
||||
|
||||
manyTabs(): Tab[] {
|
||||
return Array.from({ length: 12 }, (_, i) => ({
|
||||
id: `many${i + 1}`,
|
||||
label: `Tab ${i + 1}`,
|
||||
closeable: i > 2
|
||||
}));
|
||||
}
|
||||
|
||||
lazyTabs(): Tab[] {
|
||||
return [
|
||||
{ id: 'eager', label: 'Eager Load', lazyLoad: false },
|
||||
{ id: 'lazy1', label: 'Lazy Tab 1', lazyLoad: true },
|
||||
{ id: 'lazy2', label: 'Lazy Tab 2', lazyLoad: true }
|
||||
];
|
||||
}
|
||||
|
||||
disabledTabs(): Tab[] {
|
||||
return [
|
||||
{ id: 'enabled1', label: 'Enabled' },
|
||||
{ id: 'disabled', label: 'Disabled', disabled: true },
|
||||
{ id: 'enabled2', label: 'Also Enabled' }
|
||||
];
|
||||
}
|
||||
|
||||
closeableTabs = this._closeableTabs.asReadonly();
|
||||
reorderableTabs = this._reorderableTabs.asReadonly();
|
||||
interactiveTabs = this._interactiveTabs.asReadonly();
|
||||
|
||||
handleTabSelect(context: string, tabId: string): void {
|
||||
console.log(`Tab selected in ${context}:`, tabId);
|
||||
this.activeTab.set(tabId);
|
||||
this.tabSelectCount.update(count => count + 1);
|
||||
}
|
||||
|
||||
handleTabClose(tabId: string): void {
|
||||
console.log('Tab closed:', tabId);
|
||||
this._closeableTabs.update(tabs => tabs.filter(tab => tab.id !== tabId));
|
||||
}
|
||||
|
||||
handleTabsReorder(newTabs: Tab[]): void {
|
||||
console.log('Tabs reordered:', newTabs);
|
||||
this._reorderableTabs.set(newTabs);
|
||||
this.reorderCount.update(count => count + 1);
|
||||
}
|
||||
|
||||
handleInteractiveTabClose(tabId: string): void {
|
||||
console.log('Interactive tab closed:', tabId);
|
||||
this._interactiveTabs.update(tabs => tabs.filter(tab => tab.id !== tabId));
|
||||
}
|
||||
|
||||
handleInteractiveTabsReorder(newTabs: Tab[]): void {
|
||||
console.log('Interactive tabs reordered:', newTabs);
|
||||
this._interactiveTabs.set(newTabs);
|
||||
}
|
||||
|
||||
updateInteractiveConfig(): void {
|
||||
// Trigger change detection for interactive demo
|
||||
console.log('Interactive config updated:', {
|
||||
size: this.interactiveSize,
|
||||
variant: this.interactiveVariant,
|
||||
position: this.interactivePosition,
|
||||
scrollable: this.interactiveScrollable,
|
||||
reorderable: this.interactiveReorderable
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
@use '../../../../../ui-design-system/src/styles/semantic' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { TreeViewComponent } from "../../../../../ui-essentials/src/public-api";
|
||||
import { TreeNode } from '../../../../../../dist/ui-essentials/lib/components/data-display/tree-view/tree-view.component';
|
||||
import { TreeNode, TreeViewComponent } from "../../../../../ui-essentials/src/public-api";
|
||||
|
||||
@Component({
|
||||
selector: 'ui-tree-view-demo',
|
||||
|
||||
@@ -4,16 +4,17 @@ import { DashboardSidebarComponent, SidebarMenuItem } from "./dashboard.sidebar.
|
||||
import { LayoutContainerComponent } from "../../shared/layout-containers/layout-container.component";
|
||||
import {
|
||||
faWindowMaximize, faUserCircle, faCertificate, faHandPointer, faIdCard, faTags,
|
||||
faCheckSquare, faKeyboard, faThLarge, faList, faDotCircle, faSearch, faToggleOn,
|
||||
faCheckSquare, faKeyboard, faThLarge, faList, faDotCircle, faSearch, faToggleOn, faNewspaper,
|
||||
faTable, faImage, faImages, faPlay, faBars, faEdit, faEye, faCompass,
|
||||
faVideo, faComment, faMousePointer, faLayerGroup, faSquare, faCalendarDays, faClock,
|
||||
faGripVertical, faArrowsAlt, faBoxOpen, faChevronLeft, faSpinner, faExclamationTriangle,
|
||||
faCloudUploadAlt, faFileText, faListAlt, faCircle, faExpandArrowsAlt, faCircleNotch, faSliders,
|
||||
faMinus, faInfoCircle, faChevronDown, faCaretUp, faExclamationCircle, faSitemap, faStream,
|
||||
faBell, faRoute, faChevronUp, faEllipsisV, faCut, faPalette, faExchangeAlt, faTools, faHeart
|
||||
faBell, faRoute, faChevronUp, faEllipsisV, faCut, faPalette, faExchangeAlt, faTools, faHeart, faCircleDot, faColumns,
|
||||
faFolderOpen, faDesktop, faListUl
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { DemoRoutes } from '../../demos';
|
||||
import { ScrollContainerComponent } from '../../../../../ui-essentials/src/lib/layouts/scroll-container.component';
|
||||
import { ScrollContainerComponent } from "../../../../../ui-essentials/src/public-api";
|
||||
// import { DemoRoutes } from "../../../../../ui-essentials/src/public-api";
|
||||
|
||||
@Component({
|
||||
@@ -23,8 +24,8 @@ import { ScrollContainerComponent } from '../../../../../ui-essentials/src/lib/l
|
||||
CommonModule,
|
||||
DashboardSidebarComponent,
|
||||
LayoutContainerComponent,
|
||||
ScrollContainerComponent,
|
||||
DemoRoutes
|
||||
DemoRoutes,
|
||||
ScrollContainerComponent
|
||||
],
|
||||
template: `
|
||||
|
||||
@@ -108,6 +109,12 @@ export class DashboardComponent {
|
||||
faExchangeAlt = faExchangeAlt;
|
||||
faTools = faTools;
|
||||
faHeart = faHeart;
|
||||
faCircleDot = faCircleDot;
|
||||
faColumns = faColumns;
|
||||
faFolderOpen = faFolderOpen;
|
||||
faDesktop = faDesktop;
|
||||
faNewspaper = faNewspaper;
|
||||
faListUl = faListUl;
|
||||
|
||||
menuItems: any = []
|
||||
|
||||
@@ -219,10 +226,26 @@ export class DashboardComponent {
|
||||
|
||||
// Layout category
|
||||
const layoutChildren = [
|
||||
this.createChildItem("layout", "Layout Demos", this.faThLarge),
|
||||
this.createChildItem("aspect-ratio", "Aspect Ratio", this.faExpandArrowsAlt),
|
||||
this.createChildItem("bento-grid", "Bento Grid", this.faThLarge),
|
||||
this.createChildItem("box", "Box (Padding/Margin)", this.faSquare),
|
||||
this.createChildItem("breakpoint-container", "Breakpoint Container", this.faEye),
|
||||
this.createChildItem("center", "Center (Content Alignment)", this.faCircleDot),
|
||||
this.createChildItem("column", "Column Layout", this.faColumns),
|
||||
this.createChildItem("container", "Container", this.faBoxOpen),
|
||||
this.createChildItem("feed-layout", "Feed Layout", this.faNewspaper),
|
||||
this.createChildItem("flex", "Flex (Flexbox Container)", this.faArrowsAlt),
|
||||
this.createChildItem("grid-system", "Grid System", this.faGripVertical),
|
||||
this.createChildItem("grid-container", "Grid Container", this.faThLarge),
|
||||
this.createChildItem("list-detail-layout", "List Detail Layout", this.faListUl),
|
||||
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("container", "Container", this.faBoxOpen)
|
||||
this.createChildItem("stack", "Stack (VStack/HStack)", this.faStream),
|
||||
this.createChildItem("tabs-container", "Tabs Container", this.faFolderOpen),
|
||||
this.createChildItem("dashboard-shell", "Dashboard Shell", this.faDesktop)
|
||||
];
|
||||
this.addMenuItem("layouts", "Layouts", this.faLayerGroup, layoutChildren);
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
$base-typography-font-family-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
|
||||
$base-typography-font-family-display: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
|
||||
|
||||
@import '../../../shared-ui/src/styles/semantic';
|
||||
@import '../../../ui-design-system/src/styles/semantic';
|
||||
|
||||
// These overrides will cascade through all semantic typography tokens
|
||||
// that use $base-typography-font-family-sans and $base-typography-font-family-display
|
||||
|
||||
@@ -5,8 +5,8 @@ $inter-font-path: "/fonts/inter/" !default;
|
||||
$comfortaa-font-path: "/fonts/comfortaa/" !default;
|
||||
|
||||
// Import specific fonts we need for the demo
|
||||
@import '../../shared-ui/src/styles/fontfaces/inter';
|
||||
@import '../../shared-ui/src/styles/fontfaces/comfortaa';
|
||||
@import '../../ui-design-system/src/styles/fontfaces/inter';
|
||||
@import '../../ui-design-system/src/styles/fontfaces/comfortaa';
|
||||
|
||||
// Import project variables (which now has semantic tokens available)
|
||||
@import 'scss/_variables';
|
||||
|
||||
@@ -4,7 +4,6 @@ export * from './badge';
|
||||
export * from './card';
|
||||
export * from './carousel';
|
||||
export * from './chip';
|
||||
export * from './divider';
|
||||
export * from './image-container';
|
||||
export * from './list';
|
||||
export * from './progress';
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
@use '../../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-aspect-ratio {
|
||||
// Core Structure
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
// Visual Design
|
||||
background: $semantic-color-surface-primary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
|
||||
// Default aspect ratio container
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
padding-bottom: 56.25%; // 16:9 default
|
||||
}
|
||||
|
||||
// Content wrapper
|
||||
&__content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
// Ensure content fills the container properly
|
||||
& > * {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
// For images and videos
|
||||
& > img,
|
||||
& > video {
|
||||
object-fit: cover;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
// For custom content that needs centering
|
||||
&--center {
|
||||
& > * {
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Common Aspect Ratio Variants
|
||||
&--square {
|
||||
&::before {
|
||||
padding-bottom: 100%; // 1:1
|
||||
}
|
||||
}
|
||||
|
||||
&--video {
|
||||
&::before {
|
||||
padding-bottom: 56.25%; // 16:9
|
||||
}
|
||||
}
|
||||
|
||||
&--cinema {
|
||||
&::before {
|
||||
padding-bottom: 42.86%; // 21:9
|
||||
}
|
||||
}
|
||||
|
||||
&--photo {
|
||||
&::before {
|
||||
padding-bottom: 66.67%; // 3:2
|
||||
}
|
||||
}
|
||||
|
||||
&--portrait {
|
||||
&::before {
|
||||
padding-bottom: 133.33%; // 3:4
|
||||
}
|
||||
}
|
||||
|
||||
&--golden {
|
||||
&::before {
|
||||
padding-bottom: 61.8%; // Golden ratio ~1.618:1
|
||||
}
|
||||
}
|
||||
|
||||
// Size variants for different contexts
|
||||
&--sm {
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
|
||||
.ui-aspect-ratio__content {
|
||||
& > img,
|
||||
& > video {
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--lg {
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
|
||||
.ui-aspect-ratio__content {
|
||||
& > img,
|
||||
& > video {
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Surface variants
|
||||
&--elevated {
|
||||
background: $semantic-color-surface-elevated;
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
}
|
||||
|
||||
&--bordered {
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
}
|
||||
|
||||
// Interactive state for clickable aspect ratios
|
||||
&--interactive {
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $semantic-shadow-elevation-3;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
}
|
||||
}
|
||||
|
||||
// Loading state
|
||||
&--loading {
|
||||
background: $semantic-color-surface-secondary;
|
||||
|
||||
.ui-aspect-ratio__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid $semantic-color-border-subtle;
|
||||
border-top: 3px solid $semantic-color-primary;
|
||||
border-radius: 50%;
|
||||
animation: spin $semantic-motion-duration-slow linear infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: calc($semantic-breakpoint-md - 1px)) {
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
|
||||
&--sm {
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
}
|
||||
|
||||
&--lg {
|
||||
border-radius: $semantic-border-radius-md;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loading animation
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
type AspectRatioPreset = 'square' | 'video' | 'cinema' | 'photo' | 'portrait' | 'golden';
|
||||
type AspectRatioSize = 'sm' | 'md' | 'lg';
|
||||
type AspectRatioVariant = 'default' | 'elevated' | 'bordered' | 'interactive';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-aspect-ratio',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<div
|
||||
[ngClass]="getClasses()"
|
||||
[style.--aspect-ratio]="customRatio"
|
||||
[attr.role]="role"
|
||||
[tabindex]="interactive && !loading ? tabIndex : -1"
|
||||
(click)="handleClick($event)"
|
||||
(keydown)="handleKeydown($event)">
|
||||
|
||||
<div class="ui-aspect-ratio__content" [class.ui-aspect-ratio__content--center]="centerContent">
|
||||
@if (loading) {
|
||||
<!-- Loading indicator is handled by CSS -->
|
||||
} @else {
|
||||
<ng-content></ng-content>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './aspect-ratio.component.scss'
|
||||
})
|
||||
export class AspectRatioComponent {
|
||||
@Input() ratio: AspectRatioPreset = 'video';
|
||||
@Input() customRatio?: string; // e.g., "4/3", "1.5", "75%"
|
||||
@Input() size: AspectRatioSize = 'md';
|
||||
@Input() variant: AspectRatioVariant = 'default';
|
||||
@Input() loading = false;
|
||||
@Input() centerContent = false;
|
||||
@Input() role?: string;
|
||||
@Input() tabIndex = 0;
|
||||
|
||||
@Output() clicked = new EventEmitter<MouseEvent>();
|
||||
|
||||
get interactive(): boolean {
|
||||
return this.variant === 'interactive';
|
||||
}
|
||||
|
||||
getClasses(): Record<string, boolean> {
|
||||
const classes: Record<string, boolean> = {
|
||||
'ui-aspect-ratio': true,
|
||||
[`ui-aspect-ratio--${this.ratio}`]: !this.customRatio,
|
||||
[`ui-aspect-ratio--${this.size}`]: this.size !== 'md',
|
||||
'ui-aspect-ratio--loading': this.loading
|
||||
};
|
||||
|
||||
if (this.variant !== 'default') {
|
||||
classes[`ui-aspect-ratio--${this.variant}`] = true;
|
||||
}
|
||||
|
||||
return classes;
|
||||
}
|
||||
|
||||
handleClick(event: MouseEvent): void {
|
||||
if (this.interactive && !this.loading) {
|
||||
this.clicked.emit(event);
|
||||
}
|
||||
}
|
||||
|
||||
handleKeydown(event: KeyboardEvent): void {
|
||||
if (this.interactive && !this.loading && (event.key === 'Enter' || event.key === ' ')) {
|
||||
event.preventDefault();
|
||||
this.handleClick(event as any);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './aspect-ratio.component';
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
type BentoGridItemSpan = 1 | 2 | 3 | 4 | 'full' | "1" | "2" | "3" | "4";
|
||||
type BentoGridItemRowSpan = 1 | 2 | 3 | 4 | "1" | "2" | "3" | "4";
|
||||
type BentoGridItemFeatured = 'sm' | 'md' | 'lg' | 'xl';
|
||||
type BentoGridItemVariant = 'default' | 'primary' | 'secondary' | 'elevated';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-bento-grid-item',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<div
|
||||
class="ui-bento-grid-item"
|
||||
[class.ui-bento-grid-item--span-{{colSpan}}]="colSpan"
|
||||
[class.ui-bento-grid-item--row-span-{{rowSpan}}]="rowSpan"
|
||||
[class.ui-bento-grid-item--featured-{{featured}}]="featured"
|
||||
[class.ui-bento-grid-item--{{variant}}]="variant !== 'default'"
|
||||
[class.ui-bento-grid-item--interactive]="interactive"
|
||||
[attr.role]="role"
|
||||
[attr.aria-label]="ariaLabel"
|
||||
[attr.tabindex]="interactive ? tabIndex : -1"
|
||||
(click)="handleClick($event)"
|
||||
(keydown)="handleKeydown($event)">
|
||||
|
||||
@if (header) {
|
||||
<div class="ui-bento-grid-item__header">
|
||||
{{ header }}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="ui-bento-grid-item__content">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './bento-grid.component.scss'
|
||||
})
|
||||
export class BentoGridItemComponent {
|
||||
@Input() colSpan: BentoGridItemSpan = 1;
|
||||
@Input() rowSpan: BentoGridItemRowSpan = 1;
|
||||
@Input() featured?: BentoGridItemFeatured;
|
||||
@Input() variant: BentoGridItemVariant = 'default';
|
||||
@Input() interactive = false;
|
||||
@Input() header?: string;
|
||||
@Input() role = 'gridcell';
|
||||
@Input() ariaLabel?: string;
|
||||
@Input() tabIndex = 0;
|
||||
|
||||
@Output() clicked = new EventEmitter<MouseEvent>();
|
||||
|
||||
handleClick(event: MouseEvent): void {
|
||||
if (this.interactive) {
|
||||
this.clicked.emit(event);
|
||||
}
|
||||
}
|
||||
|
||||
handleKeydown(event: KeyboardEvent): void {
|
||||
if (this.interactive && (event.key === 'Enter' || event.key === ' ')) {
|
||||
event.preventDefault();
|
||||
this.handleClick(event as any);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
@use '../../../../../../ui-design-system/src/styles/semantic' as *;
|
||||
|
||||
.ui-bento-grid {
|
||||
display: grid;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
// Base grid layout with masonry-like behavior
|
||||
gap: $semantic-spacing-grid-gap-md;
|
||||
grid-auto-rows: minmax(120px, auto);
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
|
||||
// Size variants for gap
|
||||
&--gap-xs {
|
||||
gap: $semantic-spacing-grid-gap-sm;
|
||||
}
|
||||
|
||||
&--gap-sm {
|
||||
gap: $semantic-spacing-grid-gap-sm;
|
||||
}
|
||||
|
||||
&--gap-md {
|
||||
gap: $semantic-spacing-grid-gap-md;
|
||||
}
|
||||
|
||||
&--gap-lg {
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
}
|
||||
|
||||
// Column variants for different breakpoints
|
||||
&--cols-2 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
&--cols-3 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
&--cols-4 {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
&--cols-5 {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
}
|
||||
|
||||
&--cols-6 {
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
}
|
||||
|
||||
// Auto-fit responsive columns with different min widths
|
||||
&--auto-fit-sm {
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
}
|
||||
|
||||
&--auto-fit-md {
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
}
|
||||
|
||||
&--auto-fit-lg {
|
||||
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
||||
}
|
||||
|
||||
// Row height variants
|
||||
&--rows-sm {
|
||||
grid-auto-rows: minmax(80px, auto);
|
||||
}
|
||||
|
||||
&--rows-md {
|
||||
grid-auto-rows: minmax(120px, auto);
|
||||
}
|
||||
|
||||
&--rows-lg {
|
||||
grid-auto-rows: minmax(160px, auto);
|
||||
}
|
||||
|
||||
// Dense packing for masonry effect
|
||||
&--dense {
|
||||
grid-auto-flow: row dense;
|
||||
}
|
||||
|
||||
// Responsive behavior
|
||||
@media (max-width: 1024px) {
|
||||
&--cols-6,
|
||||
&--cols-5 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
&--cols-4 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
&--auto-fit-lg {
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
gap: $semantic-spacing-grid-gap-sm;
|
||||
|
||||
&--cols-6,
|
||||
&--cols-5,
|
||||
&--cols-4,
|
||||
&--cols-3 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
&--auto-fit-md,
|
||||
&--auto-fit-lg {
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
gap: $semantic-spacing-grid-gap-sm;
|
||||
|
||||
&--cols-6,
|
||||
&--cols-5,
|
||||
&--cols-4,
|
||||
&--cols-3,
|
||||
&--cols-2 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
&--auto-fit-sm,
|
||||
&--auto-fit-md,
|
||||
&--auto-fit-lg {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bento Grid Item - for individual grid cells with specific spans
|
||||
.ui-bento-grid-item {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: $semantic-color-surface-primary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
padding: $semantic-spacing-component-md;
|
||||
|
||||
// Transitions
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
// Size variants - column spans
|
||||
&--span-1 {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
&--span-2 {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
&--span-3 {
|
||||
grid-column: span 3;
|
||||
}
|
||||
|
||||
&--span-4 {
|
||||
grid-column: span 4;
|
||||
}
|
||||
|
||||
&--span-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
// Row spans for featured items
|
||||
&--row-span-1 {
|
||||
grid-row: span 1;
|
||||
}
|
||||
|
||||
&--row-span-2 {
|
||||
grid-row: span 2;
|
||||
}
|
||||
|
||||
&--row-span-3 {
|
||||
grid-row: span 3;
|
||||
}
|
||||
|
||||
&--row-span-4 {
|
||||
grid-row: span 4;
|
||||
}
|
||||
|
||||
// Combined featured sizes
|
||||
&--featured-sm {
|
||||
grid-column: span 2;
|
||||
grid-row: span 1;
|
||||
}
|
||||
|
||||
&--featured-md {
|
||||
grid-column: span 2;
|
||||
grid-row: span 2;
|
||||
}
|
||||
|
||||
&--featured-lg {
|
||||
grid-column: span 3;
|
||||
grid-row: span 2;
|
||||
}
|
||||
|
||||
&--featured-xl {
|
||||
grid-column: span 4;
|
||||
grid-row: span 3;
|
||||
}
|
||||
|
||||
// Color variants
|
||||
&--primary {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border-color: $semantic-color-primary;
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
background: $semantic-color-secondary;
|
||||
color: $semantic-color-on-secondary;
|
||||
border-color: $semantic-color-secondary;
|
||||
}
|
||||
|
||||
&--elevated {
|
||||
background: $semantic-color-surface-elevated;
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
}
|
||||
|
||||
// Interactive states
|
||||
&--interactive {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $semantic-shadow-elevation-3;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
}
|
||||
}
|
||||
|
||||
// Content styling
|
||||
&__header {
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
font-family: map-get($semantic-typography-heading-h5, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h5, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h5, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h5, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
&__content {
|
||||
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;
|
||||
}
|
||||
|
||||
// Responsive item behavior
|
||||
@media (max-width: 1024px) {
|
||||
&--featured-xl,
|
||||
&--featured-lg {
|
||||
grid-column: span 2;
|
||||
grid-row: span 2;
|
||||
}
|
||||
|
||||
&--span-4 {
|
||||
grid-column: span 3;
|
||||
}
|
||||
|
||||
&--span-3 {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
|
||||
&--featured-xl,
|
||||
&--featured-lg,
|
||||
&--featured-md,
|
||||
&--featured-sm {
|
||||
grid-column: span 2;
|
||||
grid-row: span 1;
|
||||
}
|
||||
|
||||
&--span-4,
|
||||
&--span-3,
|
||||
&--span-2 {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
&--row-span-4,
|
||||
&--row-span-3,
|
||||
&--row-span-2 {
|
||||
grid-row: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
padding: $semantic-spacing-component-xs;
|
||||
|
||||
&--featured-xl,
|
||||
&--featured-lg,
|
||||
&--featured-md,
|
||||
&--featured-sm,
|
||||
&--span-4,
|
||||
&--span-3,
|
||||
&--span-2,
|
||||
&--span-full {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
&--row-span-4,
|
||||
&--row-span-3,
|
||||
&--row-span-2 {
|
||||
grid-row: span 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
type BentoGridColumns = 2 | 3 | 4 | 5 | 6 | 'auto-fit-sm' | 'auto-fit-md' | 'auto-fit-lg' | "2" | "3" | "4" | "5" | "6";
|
||||
type BentoGridGap = 'xs' | 'sm' | 'md' | 'lg';
|
||||
type BentoGridRows = 'sm' | 'md' | 'lg';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-bento-grid',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<div
|
||||
class="ui-bento-grid"
|
||||
[class.ui-bento-grid--cols-{{columns}}]="typeof columns === 'number'"
|
||||
[class.ui-bento-grid--{{columns}}]="typeof columns === 'string'"
|
||||
[class.ui-bento-grid--gap-{{gap}}]="gap"
|
||||
[class.ui-bento-grid--rows-{{rowHeight}}]="rowHeight"
|
||||
[class.ui-bento-grid--dense]="dense"
|
||||
[attr.role]="role"
|
||||
[attr.aria-label]="ariaLabel">
|
||||
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './bento-grid.component.scss'
|
||||
})
|
||||
export class BentoGridComponent {
|
||||
@Input() columns: BentoGridColumns = 'auto-fit-md';
|
||||
@Input() gap: BentoGridGap = 'md';
|
||||
@Input() rowHeight: BentoGridRows = 'md';
|
||||
@Input() dense = true;
|
||||
@Input() role = 'grid';
|
||||
@Input() ariaLabel = 'Bento grid layout';
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './bento-grid.component';
|
||||
export * from './bento-grid-item.component';
|
||||
@@ -0,0 +1,454 @@
|
||||
@use '../../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-box {
|
||||
// Base box behavior
|
||||
box-sizing: border-box;
|
||||
|
||||
// Display variants
|
||||
&--display-block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&--display-inline {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
&--display-inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&--display-flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&--display-inline-flex {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
&--display-grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
&--display-inline-grid {
|
||||
display: inline-grid;
|
||||
}
|
||||
|
||||
// Position variants
|
||||
&--position-relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&--position-absolute {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&--position-fixed {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
&--position-sticky {
|
||||
position: sticky;
|
||||
}
|
||||
|
||||
// Overflow variants
|
||||
&--overflow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&--overflow-scroll {
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
&--overflow-auto {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
// Padding utilities - All sides
|
||||
&--p-none {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&--p-xs {
|
||||
padding: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
&--p-sm {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
&--p-md {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&--p-lg {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
&--p-xl {
|
||||
padding: $semantic-spacing-component-xl;
|
||||
}
|
||||
|
||||
&--p-2xl {
|
||||
padding: $semantic-spacing-2xl;
|
||||
}
|
||||
|
||||
&--p-3xl {
|
||||
padding: $semantic-spacing-3xl;
|
||||
}
|
||||
|
||||
&--p-4xl {
|
||||
padding: $semantic-spacing-4xl;
|
||||
}
|
||||
|
||||
&--p-5xl {
|
||||
padding: $semantic-spacing-5xl;
|
||||
}
|
||||
|
||||
// Padding utilities - Horizontal (left + right)
|
||||
&--px-none {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
&--px-xs {
|
||||
padding-left: $semantic-spacing-component-xs;
|
||||
padding-right: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
&--px-sm {
|
||||
padding-left: $semantic-spacing-component-sm;
|
||||
padding-right: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
&--px-md {
|
||||
padding-left: $semantic-spacing-component-md;
|
||||
padding-right: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&--px-lg {
|
||||
padding-left: $semantic-spacing-component-lg;
|
||||
padding-right: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
&--px-xl {
|
||||
padding-left: $semantic-spacing-component-xl;
|
||||
padding-right: $semantic-spacing-component-xl;
|
||||
}
|
||||
|
||||
&--px-2xl {
|
||||
padding-left: $semantic-spacing-2xl;
|
||||
padding-right: $semantic-spacing-2xl;
|
||||
}
|
||||
|
||||
&--px-3xl {
|
||||
padding-left: $semantic-spacing-3xl;
|
||||
padding-right: $semantic-spacing-3xl;
|
||||
}
|
||||
|
||||
&--px-4xl {
|
||||
padding-left: $semantic-spacing-4xl;
|
||||
padding-right: $semantic-spacing-4xl;
|
||||
}
|
||||
|
||||
&--px-5xl {
|
||||
padding-left: $semantic-spacing-5xl;
|
||||
padding-right: $semantic-spacing-5xl;
|
||||
}
|
||||
|
||||
// Padding utilities - Vertical (top + bottom)
|
||||
&--py-none {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&--py-xs {
|
||||
padding-top: $semantic-spacing-component-xs;
|
||||
padding-bottom: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
&--py-sm {
|
||||
padding-top: $semantic-spacing-component-sm;
|
||||
padding-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
&--py-md {
|
||||
padding-top: $semantic-spacing-component-md;
|
||||
padding-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&--py-lg {
|
||||
padding-top: $semantic-spacing-component-lg;
|
||||
padding-bottom: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
&--py-xl {
|
||||
padding-top: $semantic-spacing-component-xl;
|
||||
padding-bottom: $semantic-spacing-component-xl;
|
||||
}
|
||||
|
||||
&--py-2xl {
|
||||
padding-top: $semantic-spacing-2xl;
|
||||
padding-bottom: $semantic-spacing-2xl;
|
||||
}
|
||||
|
||||
&--py-3xl {
|
||||
padding-top: $semantic-spacing-3xl;
|
||||
padding-bottom: $semantic-spacing-3xl;
|
||||
}
|
||||
|
||||
&--py-4xl {
|
||||
padding-top: $semantic-spacing-4xl;
|
||||
padding-bottom: $semantic-spacing-4xl;
|
||||
}
|
||||
|
||||
&--py-5xl {
|
||||
padding-top: $semantic-spacing-5xl;
|
||||
padding-bottom: $semantic-spacing-5xl;
|
||||
}
|
||||
|
||||
// Individual padding sides (pt, pr, pb, pl)
|
||||
&--pt-none { padding-top: 0; }
|
||||
&--pt-xs { padding-top: $semantic-spacing-component-xs; }
|
||||
&--pt-sm { padding-top: $semantic-spacing-component-sm; }
|
||||
&--pt-md { padding-top: $semantic-spacing-component-md; }
|
||||
&--pt-lg { padding-top: $semantic-spacing-component-lg; }
|
||||
&--pt-xl { padding-top: $semantic-spacing-component-xl; }
|
||||
&--pt-2xl { padding-top: $semantic-spacing-2xl; }
|
||||
&--pt-3xl { padding-top: $semantic-spacing-3xl; }
|
||||
&--pt-4xl { padding-top: $semantic-spacing-4xl; }
|
||||
&--pt-5xl { padding-top: $semantic-spacing-5xl; }
|
||||
|
||||
&--pr-none { padding-right: 0; }
|
||||
&--pr-xs { padding-right: $semantic-spacing-component-xs; }
|
||||
&--pr-sm { padding-right: $semantic-spacing-component-sm; }
|
||||
&--pr-md { padding-right: $semantic-spacing-component-md; }
|
||||
&--pr-lg { padding-right: $semantic-spacing-component-lg; }
|
||||
&--pr-xl { padding-right: $semantic-spacing-component-xl; }
|
||||
&--pr-2xl { padding-right: $semantic-spacing-2xl; }
|
||||
&--pr-3xl { padding-right: $semantic-spacing-3xl; }
|
||||
&--pr-4xl { padding-right: $semantic-spacing-4xl; }
|
||||
&--pr-5xl { padding-right: $semantic-spacing-5xl; }
|
||||
|
||||
&--pb-none { padding-bottom: 0; }
|
||||
&--pb-xs { padding-bottom: $semantic-spacing-component-xs; }
|
||||
&--pb-sm { padding-bottom: $semantic-spacing-component-sm; }
|
||||
&--pb-md { padding-bottom: $semantic-spacing-component-md; }
|
||||
&--pb-lg { padding-bottom: $semantic-spacing-component-lg; }
|
||||
&--pb-xl { padding-bottom: $semantic-spacing-component-xl; }
|
||||
&--pb-2xl { padding-bottom: $semantic-spacing-2xl; }
|
||||
&--pb-3xl { padding-bottom: $semantic-spacing-3xl; }
|
||||
&--pb-4xl { padding-bottom: $semantic-spacing-4xl; }
|
||||
&--pb-5xl { padding-bottom: $semantic-spacing-5xl; }
|
||||
|
||||
&--pl-none { padding-left: 0; }
|
||||
&--pl-xs { padding-left: $semantic-spacing-component-xs; }
|
||||
&--pl-sm { padding-left: $semantic-spacing-component-sm; }
|
||||
&--pl-md { padding-left: $semantic-spacing-component-md; }
|
||||
&--pl-lg { padding-left: $semantic-spacing-component-lg; }
|
||||
&--pl-xl { padding-left: $semantic-spacing-component-xl; }
|
||||
&--pl-2xl { padding-left: $semantic-spacing-2xl; }
|
||||
&--pl-3xl { padding-left: $semantic-spacing-3xl; }
|
||||
&--pl-4xl { padding-left: $semantic-spacing-4xl; }
|
||||
&--pl-5xl { padding-left: $semantic-spacing-5xl; }
|
||||
|
||||
// Margin utilities - All sides
|
||||
&--m-none {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&--m-xs {
|
||||
margin: $semantic-spacing-layout-xs;
|
||||
}
|
||||
|
||||
&--m-sm {
|
||||
margin: $semantic-spacing-layout-sm;
|
||||
}
|
||||
|
||||
&--m-md {
|
||||
margin: $semantic-spacing-layout-md;
|
||||
}
|
||||
|
||||
&--m-lg {
|
||||
margin: $semantic-spacing-layout-lg;
|
||||
}
|
||||
|
||||
&--m-xl {
|
||||
margin: $semantic-spacing-layout-xl;
|
||||
}
|
||||
|
||||
&--m-2xl {
|
||||
margin: $semantic-spacing-2xl;
|
||||
}
|
||||
|
||||
&--m-3xl {
|
||||
margin: $semantic-spacing-3xl;
|
||||
}
|
||||
|
||||
&--m-4xl {
|
||||
margin: $semantic-spacing-4xl;
|
||||
}
|
||||
|
||||
&--m-5xl {
|
||||
margin: $semantic-spacing-5xl;
|
||||
}
|
||||
|
||||
// Margin utilities - Horizontal (left + right)
|
||||
&--mx-none {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&--mx-xs {
|
||||
margin-left: $semantic-spacing-layout-xs;
|
||||
margin-right: $semantic-spacing-layout-xs;
|
||||
}
|
||||
|
||||
&--mx-sm {
|
||||
margin-left: $semantic-spacing-layout-sm;
|
||||
margin-right: $semantic-spacing-layout-sm;
|
||||
}
|
||||
|
||||
&--mx-md {
|
||||
margin-left: $semantic-spacing-layout-md;
|
||||
margin-right: $semantic-spacing-layout-md;
|
||||
}
|
||||
|
||||
&--mx-lg {
|
||||
margin-left: $semantic-spacing-layout-lg;
|
||||
margin-right: $semantic-spacing-layout-lg;
|
||||
}
|
||||
|
||||
&--mx-xl {
|
||||
margin-left: $semantic-spacing-layout-xl;
|
||||
margin-right: $semantic-spacing-layout-xl;
|
||||
}
|
||||
|
||||
&--mx-2xl {
|
||||
margin-left: $semantic-spacing-2xl;
|
||||
margin-right: $semantic-spacing-2xl;
|
||||
}
|
||||
|
||||
&--mx-3xl {
|
||||
margin-left: $semantic-spacing-3xl;
|
||||
margin-right: $semantic-spacing-3xl;
|
||||
}
|
||||
|
||||
&--mx-4xl {
|
||||
margin-left: $semantic-spacing-4xl;
|
||||
margin-right: $semantic-spacing-4xl;
|
||||
}
|
||||
|
||||
&--mx-5xl {
|
||||
margin-left: $semantic-spacing-5xl;
|
||||
margin-right: $semantic-spacing-5xl;
|
||||
}
|
||||
|
||||
// Margin utilities - Vertical (top + bottom)
|
||||
&--my-none {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&--my-xs {
|
||||
margin-top: $semantic-spacing-layout-xs;
|
||||
margin-bottom: $semantic-spacing-layout-xs;
|
||||
}
|
||||
|
||||
&--my-sm {
|
||||
margin-top: $semantic-spacing-layout-sm;
|
||||
margin-bottom: $semantic-spacing-layout-sm;
|
||||
}
|
||||
|
||||
&--my-md {
|
||||
margin-top: $semantic-spacing-layout-md;
|
||||
margin-bottom: $semantic-spacing-layout-md;
|
||||
}
|
||||
|
||||
&--my-lg {
|
||||
margin-top: $semantic-spacing-layout-lg;
|
||||
margin-bottom: $semantic-spacing-layout-lg;
|
||||
}
|
||||
|
||||
&--my-xl {
|
||||
margin-top: $semantic-spacing-layout-xl;
|
||||
margin-bottom: $semantic-spacing-layout-xl;
|
||||
}
|
||||
|
||||
&--my-2xl {
|
||||
margin-top: $semantic-spacing-2xl;
|
||||
margin-bottom: $semantic-spacing-2xl;
|
||||
}
|
||||
|
||||
&--my-3xl {
|
||||
margin-top: $semantic-spacing-3xl;
|
||||
margin-bottom: $semantic-spacing-3xl;
|
||||
}
|
||||
|
||||
&--my-4xl {
|
||||
margin-top: $semantic-spacing-4xl;
|
||||
margin-bottom: $semantic-spacing-4xl;
|
||||
}
|
||||
|
||||
&--my-5xl {
|
||||
margin-top: $semantic-spacing-5xl;
|
||||
margin-bottom: $semantic-spacing-5xl;
|
||||
}
|
||||
|
||||
// Individual margin sides (mt, mr, mb, ml)
|
||||
&--mt-none { margin-top: 0; }
|
||||
&--mt-xs { margin-top: $semantic-spacing-layout-xs; }
|
||||
&--mt-sm { margin-top: $semantic-spacing-layout-sm; }
|
||||
&--mt-md { margin-top: $semantic-spacing-layout-md; }
|
||||
&--mt-lg { margin-top: $semantic-spacing-layout-lg; }
|
||||
&--mt-xl { margin-top: $semantic-spacing-layout-xl; }
|
||||
&--mt-2xl { margin-top: $semantic-spacing-2xl; }
|
||||
&--mt-3xl { margin-top: $semantic-spacing-3xl; }
|
||||
&--mt-4xl { margin-top: $semantic-spacing-4xl; }
|
||||
&--mt-5xl { margin-top: $semantic-spacing-5xl; }
|
||||
|
||||
&--mr-none { margin-right: 0; }
|
||||
&--mr-xs { margin-right: $semantic-spacing-layout-xs; }
|
||||
&--mr-sm { margin-right: $semantic-spacing-layout-sm; }
|
||||
&--mr-md { margin-right: $semantic-spacing-layout-md; }
|
||||
&--mr-lg { margin-right: $semantic-spacing-layout-lg; }
|
||||
&--mr-xl { margin-right: $semantic-spacing-layout-xl; }
|
||||
&--mr-2xl { margin-right: $semantic-spacing-2xl; }
|
||||
&--mr-3xl { margin-right: $semantic-spacing-3xl; }
|
||||
&--mr-4xl { margin-right: $semantic-spacing-4xl; }
|
||||
&--mr-5xl { margin-right: $semantic-spacing-5xl; }
|
||||
|
||||
&--mb-none { margin-bottom: 0; }
|
||||
&--mb-xs { margin-bottom: $semantic-spacing-layout-xs; }
|
||||
&--mb-sm { margin-bottom: $semantic-spacing-layout-sm; }
|
||||
&--mb-md { margin-bottom: $semantic-spacing-layout-md; }
|
||||
&--mb-lg { margin-bottom: $semantic-spacing-layout-lg; }
|
||||
&--mb-xl { margin-bottom: $semantic-spacing-layout-xl; }
|
||||
&--mb-2xl { margin-bottom: $semantic-spacing-2xl; }
|
||||
&--mb-3xl { margin-bottom: $semantic-spacing-3xl; }
|
||||
&--mb-4xl { margin-bottom: $semantic-spacing-4xl; }
|
||||
&--mb-5xl { margin-bottom: $semantic-spacing-5xl; }
|
||||
|
||||
&--ml-none { margin-left: 0; }
|
||||
&--ml-xs { margin-left: $semantic-spacing-layout-xs; }
|
||||
&--ml-sm { margin-left: $semantic-spacing-layout-sm; }
|
||||
&--ml-md { margin-left: $semantic-spacing-layout-md; }
|
||||
&--ml-lg { margin-left: $semantic-spacing-layout-lg; }
|
||||
&--ml-xl { margin-left: $semantic-spacing-layout-xl; }
|
||||
&--ml-2xl { margin-left: $semantic-spacing-2xl; }
|
||||
&--ml-3xl { margin-left: $semantic-spacing-3xl; }
|
||||
&--ml-4xl { margin-left: $semantic-spacing-4xl; }
|
||||
&--ml-5xl { margin-left: $semantic-spacing-5xl; }
|
||||
|
||||
// Visual utilities
|
||||
&--rounded {
|
||||
border-radius: $semantic-border-radius-md;
|
||||
}
|
||||
|
||||
&--shadow {
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
}
|
||||
|
||||
&--border {
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
type BoxSpacing = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl';
|
||||
type BoxDisplay = 'block' | 'inline' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'inline-grid';
|
||||
type BoxPosition = 'static' | 'relative' | 'absolute' | 'fixed' | 'sticky';
|
||||
type BoxOverflow = 'visible' | 'hidden' | 'scroll' | 'auto';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-box',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<div
|
||||
[ngClass]="getClasses()"
|
||||
[style.width]="width"
|
||||
[style.height]="height"
|
||||
[style.min-width]="minWidth"
|
||||
[style.min-height]="minHeight"
|
||||
[style.max-width]="maxWidth"
|
||||
[style.max-height]="maxHeight"
|
||||
[attr.role]="role">
|
||||
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './box.component.scss'
|
||||
})
|
||||
export class BoxComponent {
|
||||
// Padding variants
|
||||
@Input() p?: BoxSpacing; // all sides
|
||||
@Input() px?: BoxSpacing; // horizontal (left + right)
|
||||
@Input() py?: BoxSpacing; // vertical (top + bottom)
|
||||
@Input() pt?: BoxSpacing; // top
|
||||
@Input() pr?: BoxSpacing; // right
|
||||
@Input() pb?: BoxSpacing; // bottom
|
||||
@Input() pl?: BoxSpacing; // left
|
||||
|
||||
// Margin variants
|
||||
@Input() m?: BoxSpacing; // all sides
|
||||
@Input() mx?: BoxSpacing; // horizontal (left + right)
|
||||
@Input() my?: BoxSpacing; // vertical (top + bottom)
|
||||
@Input() mt?: BoxSpacing; // top
|
||||
@Input() mr?: BoxSpacing; // right
|
||||
@Input() mb?: BoxSpacing; // bottom
|
||||
@Input() ml?: BoxSpacing; // left
|
||||
|
||||
// Display and layout
|
||||
@Input() display?: BoxDisplay;
|
||||
@Input() position?: BoxPosition;
|
||||
@Input() overflow?: BoxOverflow;
|
||||
|
||||
// Dimensions
|
||||
@Input() width?: string;
|
||||
@Input() height?: string;
|
||||
@Input() minWidth?: string;
|
||||
@Input() minHeight?: string;
|
||||
@Input() maxWidth?: string;
|
||||
@Input() maxHeight?: string;
|
||||
|
||||
// Visual
|
||||
@Input() rounded = false;
|
||||
@Input() shadow = false;
|
||||
@Input() border = false;
|
||||
|
||||
// Accessibility
|
||||
@Input() role?: string;
|
||||
|
||||
getClasses(): Record<string, boolean> {
|
||||
const classes: Record<string, boolean> = {
|
||||
'ui-box': true
|
||||
};
|
||||
|
||||
// Display
|
||||
if (this.display) {
|
||||
classes[`ui-box--display-${this.display}`] = true;
|
||||
}
|
||||
|
||||
// Position
|
||||
if (this.position && this.position !== 'static') {
|
||||
classes[`ui-box--position-${this.position}`] = true;
|
||||
}
|
||||
|
||||
// Overflow
|
||||
if (this.overflow && this.overflow !== 'visible') {
|
||||
classes[`ui-box--overflow-${this.overflow}`] = true;
|
||||
}
|
||||
|
||||
// Padding classes
|
||||
if (this.p) classes[`ui-box--p-${this.p}`] = true;
|
||||
if (this.px) classes[`ui-box--px-${this.px}`] = true;
|
||||
if (this.py) classes[`ui-box--py-${this.py}`] = true;
|
||||
if (this.pt) classes[`ui-box--pt-${this.pt}`] = true;
|
||||
if (this.pr) classes[`ui-box--pr-${this.pr}`] = true;
|
||||
if (this.pb) classes[`ui-box--pb-${this.pb}`] = true;
|
||||
if (this.pl) classes[`ui-box--pl-${this.pl}`] = true;
|
||||
|
||||
// Margin classes
|
||||
if (this.m) classes[`ui-box--m-${this.m}`] = true;
|
||||
if (this.mx) classes[`ui-box--mx-${this.mx}`] = true;
|
||||
if (this.my) classes[`ui-box--my-${this.my}`] = true;
|
||||
if (this.mt) classes[`ui-box--mt-${this.mt}`] = true;
|
||||
if (this.mr) classes[`ui-box--mr-${this.mr}`] = true;
|
||||
if (this.mb) classes[`ui-box--mb-${this.mb}`] = true;
|
||||
if (this.ml) classes[`ui-box--ml-${this.ml}`] = true;
|
||||
|
||||
// Visual styles
|
||||
if (this.rounded) classes['ui-box--rounded'] = true;
|
||||
if (this.shadow) classes['ui-box--shadow'] = true;
|
||||
if (this.border) classes['ui-box--border'] = true;
|
||||
|
||||
return classes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './box.component';
|
||||
@@ -0,0 +1,227 @@
|
||||
@use '../../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-breakpoint-container {
|
||||
position: relative;
|
||||
display: block;
|
||||
|
||||
// Base visibility - show by default
|
||||
&:not(.ui-breakpoint-container--hidden) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&--hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
// Show only on mobile (below tablet)
|
||||
&--mobile-only {
|
||||
display: block;
|
||||
|
||||
@media (min-width: $semantic-breakpoint-md) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Hide on mobile
|
||||
&--mobile-hidden {
|
||||
display: none;
|
||||
|
||||
@media (min-width: $semantic-breakpoint-md) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
// Show only on tablet (between mobile and desktop)
|
||||
&--tablet-only {
|
||||
display: none;
|
||||
|
||||
@media (min-width: $semantic-breakpoint-md) and (max-width: $semantic-breakpoint-lg - 1px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
// Hide on tablet
|
||||
&--tablet-hidden {
|
||||
display: block;
|
||||
|
||||
@media (min-width: $semantic-breakpoint-md) and (max-width: $semantic-breakpoint-lg - 1px) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Show only on desktop and up
|
||||
&--desktop-only {
|
||||
display: none;
|
||||
|
||||
@media (min-width: $semantic-breakpoint-lg) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
// Hide on desktop and up
|
||||
&--desktop-hidden {
|
||||
display: block;
|
||||
|
||||
@media (min-width: $semantic-breakpoint-lg) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Custom breakpoint utilities
|
||||
&--show-sm {
|
||||
display: none;
|
||||
|
||||
@media (min-width: $semantic-breakpoint-sm) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&--hide-sm {
|
||||
display: block;
|
||||
|
||||
@media (min-width: $semantic-breakpoint-sm) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&--show-md {
|
||||
display: none;
|
||||
|
||||
@media (min-width: $semantic-breakpoint-md) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&--hide-md {
|
||||
display: block;
|
||||
|
||||
@media (min-width: $semantic-breakpoint-md) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&--show-lg {
|
||||
display: none;
|
||||
|
||||
@media (min-width: $semantic-breakpoint-lg) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&--hide-lg {
|
||||
display: block;
|
||||
|
||||
@media (min-width: $semantic-breakpoint-lg) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Screen size based visibility using semantic sizing breakpoints
|
||||
&--mobile-screen {
|
||||
display: block;
|
||||
|
||||
@media (min-width: $semantic-sizing-breakpoint-tablet) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&--tablet-screen {
|
||||
display: none;
|
||||
|
||||
@media (min-width: $semantic-sizing-breakpoint-tablet) and (max-width: $semantic-sizing-breakpoint-desktop - 1px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&--desktop-screen {
|
||||
display: none;
|
||||
|
||||
@media (min-width: $semantic-sizing-breakpoint-desktop) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
// Combination utilities - show/hide between specific ranges
|
||||
&--sm-to-md {
|
||||
display: none;
|
||||
|
||||
@media (min-width: $semantic-breakpoint-sm) and (max-width: $semantic-breakpoint-md - 1px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&--md-to-lg {
|
||||
display: none;
|
||||
|
||||
@media (min-width: $semantic-breakpoint-md) and (max-width: $semantic-breakpoint-lg - 1px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
// Inline display variant
|
||||
&--inline {
|
||||
display: inline;
|
||||
|
||||
&.ui-breakpoint-container--hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
&.ui-breakpoint-container--mobile-only {
|
||||
display: inline;
|
||||
|
||||
@media (min-width: $semantic-breakpoint-md) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.ui-breakpoint-container--desktop-only {
|
||||
display: none;
|
||||
|
||||
@media (min-width: $semantic-breakpoint-lg) {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flex display variant
|
||||
&--flex {
|
||||
display: flex;
|
||||
|
||||
&.ui-breakpoint-container--hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
&.ui-breakpoint-container--mobile-only {
|
||||
display: flex;
|
||||
|
||||
@media (min-width: $semantic-breakpoint-md) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.ui-breakpoint-container--desktop-only {
|
||||
display: none;
|
||||
|
||||
@media (min-width: $semantic-breakpoint-lg) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print media query
|
||||
@media print {
|
||||
&--print-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
&--print-only {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&:not(.ui-breakpoint-container--print-only) {
|
||||
&.ui-breakpoint-container--print-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
type BreakpointVisibility =
|
||||
| 'always'
|
||||
| 'mobile-only'
|
||||
| 'tablet-only'
|
||||
| 'desktop-only'
|
||||
| 'mobile-hidden'
|
||||
| 'tablet-hidden'
|
||||
| 'desktop-hidden'
|
||||
| 'mobile-screen'
|
||||
| 'tablet-screen'
|
||||
| 'desktop-screen'
|
||||
| 'sm-to-md'
|
||||
| 'md-to-lg';
|
||||
|
||||
type DisplayType = 'block' | 'inline' | 'flex';
|
||||
|
||||
type BreakpointRule = 'show-sm' | 'hide-sm' | 'show-md' | 'hide-md' | 'show-lg' | 'hide-lg';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-breakpoint-container',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<div
|
||||
class="ui-breakpoint-container"
|
||||
[class.ui-breakpoint-container--{{visibility}}]="visibility !== 'always'"
|
||||
[class.ui-breakpoint-container--{{displayType}}]="displayType !== 'block'"
|
||||
[class.ui-breakpoint-container--print-hidden]="printHidden"
|
||||
[class.ui-breakpoint-container--print-only]="printOnly"
|
||||
[ngClass]="getBreakpointRuleClasses()"
|
||||
[attr.role]="role"
|
||||
[attr.aria-hidden]="getAriaHidden()">
|
||||
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './breakpoint-container.component.scss'
|
||||
})
|
||||
export class BreakpointContainerComponent {
|
||||
@Input() visibility: BreakpointVisibility = 'always';
|
||||
@Input() displayType: DisplayType = 'block';
|
||||
@Input() showSm = false;
|
||||
@Input() hideSm = false;
|
||||
@Input() showMd = false;
|
||||
@Input() hideMd = false;
|
||||
@Input() showLg = false;
|
||||
@Input() hideLg = false;
|
||||
@Input() printHidden = false;
|
||||
@Input() printOnly = false;
|
||||
@Input() role?: string;
|
||||
|
||||
getBreakpointRuleClasses(): Record<string, boolean> {
|
||||
return {
|
||||
'ui-breakpoint-container--show-sm': this.showSm,
|
||||
'ui-breakpoint-container--hide-sm': this.hideSm,
|
||||
'ui-breakpoint-container--show-md': this.showMd,
|
||||
'ui-breakpoint-container--hide-md': this.hideMd,
|
||||
'ui-breakpoint-container--show-lg': this.showLg,
|
||||
'ui-breakpoint-container--hide-lg': this.hideLg
|
||||
};
|
||||
}
|
||||
|
||||
getAriaHidden(): string | null {
|
||||
// Don't set aria-hidden for content that might be visible
|
||||
if (this.visibility === 'always') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For screen readers, we generally want to keep content accessible
|
||||
// unless it's explicitly meant to be hidden for print or specific contexts
|
||||
if (this.printOnly) {
|
||||
return 'true';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './breakpoint-container.component';
|
||||
@@ -0,0 +1,139 @@
|
||||
@use '../../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-center {
|
||||
// Base structure - centering container
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
|
||||
// Both axis centering (default)
|
||||
&--both {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// Horizontal centering only
|
||||
&--horizontal {
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
// Vertical centering only
|
||||
&--vertical {
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// Inline variant
|
||||
&--inline {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
// Max width constraints
|
||||
&--max-width-xs {
|
||||
max-width: 475px;
|
||||
}
|
||||
|
||||
&--max-width-sm {
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
&--max-width-md {
|
||||
max-width: 768px;
|
||||
}
|
||||
|
||||
&--max-width-lg {
|
||||
max-width: 1024px;
|
||||
}
|
||||
|
||||
&--max-width-xl {
|
||||
max-width: 1280px;
|
||||
}
|
||||
|
||||
&--max-width-2xl {
|
||||
max-width: 1536px;
|
||||
}
|
||||
|
||||
&--max-width-3xl {
|
||||
max-width: 1792px;
|
||||
}
|
||||
|
||||
&--max-width-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// Padding variants
|
||||
&--padding-xs {
|
||||
padding: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
&--padding-sm {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
&--padding-md {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&--padding-lg {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
&--padding-xl {
|
||||
padding: $semantic-spacing-component-xl;
|
||||
}
|
||||
|
||||
&--padding-none {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
// Margin variants
|
||||
&--margin-xs {
|
||||
margin: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
&--margin-sm {
|
||||
margin: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
&--margin-md {
|
||||
margin: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&--margin-lg {
|
||||
margin: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
&--margin-xl {
|
||||
margin: $semantic-spacing-component-xl;
|
||||
}
|
||||
|
||||
&--margin-none {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// Responsive adjustments - ensure centering works on all screen sizes
|
||||
@media (max-width: 768px) {
|
||||
&--max-width-xl,
|
||||
&--max-width-lg {
|
||||
max-width: 100%;
|
||||
padding-left: $semantic-spacing-component-sm;
|
||||
padding-right: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
// Stack content vertically on smaller screens for horizontal centering
|
||||
&--horizontal {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
&--max-width-md,
|
||||
&--max-width-sm {
|
||||
max-width: 100%;
|
||||
padding-left: $semantic-spacing-component-xs;
|
||||
padding-right: $semantic-spacing-component-xs;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
type CenterAxis = 'both' | 'horizontal' | 'vertical';
|
||||
type CenterMaxWidth = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full' | 'none';
|
||||
type CenterSpacing = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-center',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<div
|
||||
[ngClass]="getClasses()"
|
||||
[style.max-width]="customMaxWidth"
|
||||
[style.min-height]="customMinHeight"
|
||||
[attr.role]="role">
|
||||
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './center.component.scss'
|
||||
})
|
||||
export class CenterComponent {
|
||||
@Input() axis: CenterAxis = 'both';
|
||||
@Input() maxWidth: CenterMaxWidth = 'none';
|
||||
@Input() padding?: CenterSpacing;
|
||||
@Input() margin?: CenterSpacing;
|
||||
@Input() inline = false;
|
||||
@Input() customMaxWidth?: string;
|
||||
@Input() customMinHeight?: string;
|
||||
@Input() role?: string;
|
||||
|
||||
getClasses(): Record<string, boolean> {
|
||||
const classes: Record<string, boolean> = {
|
||||
'ui-center': true,
|
||||
[`ui-center--${this.axis}`]: true
|
||||
};
|
||||
|
||||
if (this.maxWidth !== 'none') {
|
||||
classes[`ui-center--max-width-${this.maxWidth}`] = true;
|
||||
}
|
||||
|
||||
if (this.padding) {
|
||||
classes[`ui-center--padding-${this.padding}`] = true;
|
||||
}
|
||||
|
||||
if (this.margin) {
|
||||
classes[`ui-center--margin-${this.margin}`] = true;
|
||||
}
|
||||
|
||||
if (this.inline) {
|
||||
classes['ui-center--inline'] = true;
|
||||
}
|
||||
|
||||
return classes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './center.component';
|
||||
@@ -0,0 +1,149 @@
|
||||
@use '../../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-column {
|
||||
// Core column container
|
||||
box-sizing: border-box;
|
||||
|
||||
// Column count variants
|
||||
&--count-1 {
|
||||
column-count: 1;
|
||||
}
|
||||
|
||||
&--count-2 {
|
||||
column-count: 2;
|
||||
}
|
||||
|
||||
&--count-3 {
|
||||
column-count: 3;
|
||||
}
|
||||
|
||||
&--count-4 {
|
||||
column-count: 4;
|
||||
}
|
||||
|
||||
&--count-5 {
|
||||
column-count: 5;
|
||||
}
|
||||
|
||||
&--count-6 {
|
||||
column-count: 6;
|
||||
}
|
||||
|
||||
&--count-auto {
|
||||
column-count: auto;
|
||||
}
|
||||
|
||||
// Gap variants using semantic spacing tokens
|
||||
&--gap-xs {
|
||||
column-gap: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
&--gap-sm {
|
||||
column-gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
&--gap-md {
|
||||
column-gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&--gap-lg {
|
||||
column-gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
&--gap-xl {
|
||||
column-gap: $semantic-spacing-component-xl;
|
||||
}
|
||||
|
||||
// Column fill variants
|
||||
&--fill-auto {
|
||||
column-fill: auto;
|
||||
}
|
||||
|
||||
&--fill-balance {
|
||||
column-fill: balance;
|
||||
}
|
||||
|
||||
&--fill-balance-all {
|
||||
column-fill: balance-all;
|
||||
}
|
||||
|
||||
// Column rule variants
|
||||
&--rule-solid {
|
||||
column-rule-style: solid;
|
||||
column-rule-color: $semantic-color-border-secondary;
|
||||
}
|
||||
|
||||
&--rule-dashed {
|
||||
column-rule-style: dashed;
|
||||
column-rule-color: $semantic-color-border-secondary;
|
||||
}
|
||||
|
||||
&--rule-dotted {
|
||||
column-rule-style: dotted;
|
||||
column-rule-color: $semantic-color-border-secondary;
|
||||
}
|
||||
|
||||
// Rule width variants
|
||||
&--rule-width-1 {
|
||||
column-rule-width: $semantic-border-width-1;
|
||||
}
|
||||
|
||||
&--rule-width-2 {
|
||||
column-rule-width: $semantic-border-width-2;
|
||||
}
|
||||
|
||||
&--rule-width-3 {
|
||||
column-rule-width: 3px; // No semantic token for 3px, using fallback
|
||||
}
|
||||
|
||||
// Column span control
|
||||
&--span-all {
|
||||
* {
|
||||
column-span: all;
|
||||
}
|
||||
}
|
||||
|
||||
// Size variants
|
||||
&--full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&--full-height {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// Responsive behavior
|
||||
&--responsive {
|
||||
// Single column on small screens
|
||||
@media (max-width: calc(768px - 1px)) {
|
||||
column-count: 1 !important;
|
||||
}
|
||||
|
||||
// Reduce column count on medium screens
|
||||
@media (max-width: calc(1024px - 1px)) and (min-width: 768px) {
|
||||
&.ui-column--count-6 {
|
||||
column-count: 3;
|
||||
}
|
||||
|
||||
&.ui-column--count-5 {
|
||||
column-count: 3;
|
||||
}
|
||||
|
||||
&.ui-column--count-4 {
|
||||
column-count: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent orphans and widows for better text flow
|
||||
p, li, div {
|
||||
break-inside: avoid-column;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
// Headings should not be orphaned
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
break-after: avoid-column;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
type ColumnCount = '1' | '2' | '3' | '4' | '5' | '6' | 'auto';
|
||||
type ColumnGap = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
type ColumnFill = 'auto' | 'balance' | 'balance-all';
|
||||
type ColumnRule = 'none' | 'solid' | 'dashed' | 'dotted';
|
||||
type ColumnRuleWidth = '1' | '2' | '3';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-column',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<div
|
||||
[ngClass]="getClasses()"
|
||||
[attr.role]="role">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './column.component.scss'
|
||||
})
|
||||
export class ColumnComponent {
|
||||
// Column layout properties
|
||||
@Input() count: ColumnCount = '2';
|
||||
@Input() gap: ColumnGap = 'md';
|
||||
@Input() fill: ColumnFill = 'balance';
|
||||
|
||||
// Column rule (divider) properties
|
||||
@Input() rule: ColumnRule = 'none';
|
||||
@Input() ruleWidth: ColumnRuleWidth = '1';
|
||||
|
||||
// Column span control
|
||||
@Input() spanAll = false;
|
||||
|
||||
// Size variants
|
||||
@Input() fullWidth = false;
|
||||
@Input() fullHeight = false;
|
||||
|
||||
// Responsive breakpoint control
|
||||
@Input() responsive = true;
|
||||
|
||||
// Accessibility
|
||||
@Input() role?: string;
|
||||
|
||||
getClasses(): Record<string, boolean> {
|
||||
const classes: Record<string, boolean> = {
|
||||
'ui-column': true
|
||||
};
|
||||
|
||||
// Column count
|
||||
if (this.count) {
|
||||
classes[`ui-column--count-${this.count}`] = true;
|
||||
}
|
||||
|
||||
// Gap
|
||||
if (this.gap) {
|
||||
classes[`ui-column--gap-${this.gap}`] = true;
|
||||
}
|
||||
|
||||
// Fill
|
||||
if (this.fill) {
|
||||
classes[`ui-column--fill-${this.fill}`] = true;
|
||||
}
|
||||
|
||||
// Rule
|
||||
if (this.rule && this.rule !== 'none') {
|
||||
classes[`ui-column--rule-${this.rule}`] = true;
|
||||
|
||||
if (this.ruleWidth) {
|
||||
classes[`ui-column--rule-width-${this.ruleWidth}`] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Span all
|
||||
if (this.spanAll) {
|
||||
classes['ui-column--span-all'] = true;
|
||||
}
|
||||
|
||||
// Size variants
|
||||
if (this.fullWidth) {
|
||||
classes['ui-column--full-width'] = true;
|
||||
}
|
||||
|
||||
if (this.fullHeight) {
|
||||
classes['ui-column--full-height'] = true;
|
||||
}
|
||||
|
||||
// Responsive
|
||||
if (this.responsive) {
|
||||
classes['ui-column--responsive'] = true;
|
||||
}
|
||||
|
||||
return classes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './column.component';
|
||||
@@ -0,0 +1,428 @@
|
||||
@use '../../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-dashboard-shell {
|
||||
// Core Structure
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"header header"
|
||||
"sidebar main"
|
||||
"footer footer";
|
||||
grid-template-rows: auto 1fr auto;
|
||||
grid-template-columns: auto 1fr;
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
|
||||
// Visual Design
|
||||
background: $semantic-color-surface;
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
// Transitions
|
||||
transition: all $semantic-motion-duration-normal $semantic-motion-easing-ease;
|
||||
|
||||
// Variant: Bordered
|
||||
&--bordered {
|
||||
.ui-dashboard-shell__header {
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
}
|
||||
|
||||
.ui-dashboard-shell__sidebar {
|
||||
border-right: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
}
|
||||
|
||||
.ui-dashboard-shell__footer {
|
||||
border-top: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
}
|
||||
}
|
||||
|
||||
// Variant: Elevated
|
||||
&--elevated {
|
||||
.ui-dashboard-shell__header {
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
z-index: $semantic-z-index-dropdown;
|
||||
}
|
||||
|
||||
.ui-dashboard-shell__sidebar {
|
||||
box-shadow: $semantic-shadow-elevation-3;
|
||||
background: $semantic-color-surface-elevated;
|
||||
}
|
||||
|
||||
.ui-dashboard-shell__footer {
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
}
|
||||
}
|
||||
|
||||
// Sidebar collapsed state
|
||||
&--sidebar-collapsed {
|
||||
.ui-dashboard-shell__sidebar {
|
||||
&--sm, &--md, &--lg {
|
||||
width: 60px;
|
||||
min-width: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No footer variant
|
||||
&--no-footer {
|
||||
grid-template-areas:
|
||||
"header header"
|
||||
"sidebar main";
|
||||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
|
||||
// Mobile menu open state
|
||||
&--mobile-menu-open {
|
||||
.ui-dashboard-shell__sidebar {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Header Element
|
||||
&__header {
|
||||
grid-area: header;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: $semantic-z-index-dropdown;
|
||||
|
||||
// Layout & Spacing
|
||||
padding: 0 $semantic-spacing-component-md;
|
||||
gap: $semantic-spacing-component-md;
|
||||
|
||||
// Visual Design
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
|
||||
// Typography
|
||||
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);
|
||||
|
||||
// Height variants
|
||||
&--sm {
|
||||
min-height: $semantic-sizing-input-height-sm;
|
||||
padding: 0 $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
&--md {
|
||||
min-height: $semantic-sizing-input-height-md;
|
||||
padding: 0 $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&--lg {
|
||||
min-height: $semantic-sizing-input-height-lg;
|
||||
padding: 0 $semantic-spacing-component-lg;
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile Toggle Button
|
||||
&__mobile-toggle {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
// Layout & Spacing
|
||||
width: $semantic-sizing-touch-target;
|
||||
height: $semantic-sizing-touch-target;
|
||||
padding: $semantic-spacing-component-xs;
|
||||
margin-right: $semantic-spacing-component-sm;
|
||||
|
||||
// Visual Design
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
color: $semantic-color-text-primary;
|
||||
cursor: pointer;
|
||||
|
||||
// Typography
|
||||
font-family: map-get($semantic-typography-button-medium, font-family);
|
||||
font-size: map-get($semantic-typography-button-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-button-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-button-medium, line-height);
|
||||
|
||||
// Transitions
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
// Interactive States
|
||||
&:hover {
|
||||
background: $semantic-color-surface-elevated;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: $semantic-color-surface-container;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
display: block;
|
||||
font-size: $semantic-typography-font-size-lg;
|
||||
}
|
||||
}
|
||||
|
||||
// Header Content
|
||||
&__header-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
// Header Actions
|
||||
&__header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
// Container for Sidebar and Main
|
||||
&__container {
|
||||
grid-area: sidebar / sidebar / main / main;
|
||||
display: contents;
|
||||
}
|
||||
|
||||
// Sidebar Element
|
||||
&__sidebar {
|
||||
grid-area: sidebar;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
z-index: $semantic-z-index-dropdown;
|
||||
|
||||
// Layout & Spacing
|
||||
padding: $semantic-spacing-component-md;
|
||||
|
||||
// Visual Design
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-right: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
|
||||
// Typography
|
||||
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);
|
||||
|
||||
// Transitions
|
||||
transition: all $semantic-motion-duration-normal $semantic-motion-easing-ease;
|
||||
|
||||
// Width variants
|
||||
&--sm {
|
||||
width: 200px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
&--md {
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
&--lg {
|
||||
width: 360px;
|
||||
min-width: 360px;
|
||||
}
|
||||
|
||||
// Collapsed state
|
||||
&--collapsed {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile Backdrop
|
||||
&__mobile-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: $semantic-z-index-overlay;
|
||||
|
||||
// Visual Design
|
||||
background: $semantic-color-backdrop;
|
||||
opacity: $semantic-opacity-backdrop;
|
||||
|
||||
// Transitions
|
||||
transition: opacity $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
// Interactive
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// Main Content Element
|
||||
&__main {
|
||||
grid-area: main;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
|
||||
// Layout & Spacing
|
||||
padding: $semantic-spacing-component-md;
|
||||
|
||||
// Visual Design
|
||||
background: $semantic-color-surface;
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
// Typography
|
||||
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);
|
||||
}
|
||||
|
||||
// Breadcrumbs Element
|
||||
&__breadcrumbs {
|
||||
padding-bottom: $semantic-spacing-component-sm;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
// Typography
|
||||
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;
|
||||
}
|
||||
|
||||
// Notifications Element
|
||||
&__notifications {
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
background: $semantic-color-surface-container;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
}
|
||||
|
||||
// Content Element
|
||||
&__content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
// Footer Element
|
||||
&__footer {
|
||||
grid-area: footer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
// Layout & Spacing
|
||||
padding: $semantic-spacing-component-md;
|
||||
|
||||
// Visual Design
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-top: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
|
||||
// Typography
|
||||
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;
|
||||
|
||||
// Footer variants
|
||||
&--minimal {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
font-family: map-get($semantic-typography-caption, font-family);
|
||||
font-size: map-get($semantic-typography-caption, font-size);
|
||||
font-weight: map-get($semantic-typography-caption, font-weight);
|
||||
line-height: map-get($semantic-typography-caption, line-height);
|
||||
}
|
||||
|
||||
&--standard {
|
||||
// Default styling already applied above
|
||||
}
|
||||
|
||||
&--detailed {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Design - Mobile First
|
||||
@media (max-width: 768px) {
|
||||
grid-template-areas:
|
||||
"header"
|
||||
"main"
|
||||
"footer";
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
.ui-dashboard-shell__mobile-toggle {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ui-dashboard-shell__sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
z-index: $semantic-z-index-modal;
|
||||
transform: translateX(-100%);
|
||||
|
||||
&--sm, &--md, &--lg {
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-dashboard-shell__main {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
.ui-dashboard-shell__header {
|
||||
padding: 0 $semantic-spacing-component-sm;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
|
||||
&--sm, &--md, &--lg {
|
||||
min-height: $semantic-sizing-input-height-md;
|
||||
padding: 0 $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-dashboard-shell__footer {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
|
||||
&--detailed {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.ui-dashboard-shell__main {
|
||||
padding: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
.ui-dashboard-shell__header {
|
||||
padding: 0 $semantic-spacing-component-xs;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
|
||||
&--sm, &--md, &--lg {
|
||||
padding: 0 $semantic-spacing-component-xs;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-dashboard-shell__footer {
|
||||
padding: $semantic-spacing-component-xs;
|
||||
|
||||
&--detailed {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-dashboard-shell__sidebar {
|
||||
&--sm, &--md, &--lg {
|
||||
width: 100vw;
|
||||
min-width: 100vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
type DashboardVariant = 'default' | 'bordered' | 'elevated';
|
||||
type SidebarWidth = 'sm' | 'md' | 'lg';
|
||||
type HeaderHeight = 'sm' | 'md' | 'lg';
|
||||
type FooterVariant = 'minimal' | 'standard' | 'detailed';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-dashboard-shell',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<div
|
||||
class="ui-dashboard-shell"
|
||||
[class.ui-dashboard-shell--sidebar-collapsed]="sidebarCollapsed"
|
||||
[class.ui-dashboard-shell--mobile-menu-open]="mobileMenuOpen"
|
||||
[class.ui-dashboard-shell--bordered]="variant === 'bordered'"
|
||||
[class.ui-dashboard-shell--elevated]="variant === 'elevated'"
|
||||
[class.ui-dashboard-shell--no-footer]="!showFooter"
|
||||
[attr.role]="role">
|
||||
|
||||
<!-- Mobile Backdrop -->
|
||||
@if (mobileMenuOpen) {
|
||||
<div
|
||||
class="ui-dashboard-shell__mobile-backdrop"
|
||||
(click)="handleMobileBackdropClick()"
|
||||
role="presentation"
|
||||
aria-hidden="true">
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Header -->
|
||||
<header
|
||||
class="ui-dashboard-shell__header"
|
||||
[class.ui-dashboard-shell__header--sm]="headerHeight === 'sm'"
|
||||
[class.ui-dashboard-shell__header--md]="headerHeight === 'md'"
|
||||
[class.ui-dashboard-shell__header--lg]="headerHeight === 'lg'"
|
||||
role="banner">
|
||||
|
||||
<!-- Mobile Menu Toggle -->
|
||||
<button
|
||||
class="ui-dashboard-shell__mobile-toggle"
|
||||
(click)="handleMobileToggle()"
|
||||
[attr.aria-expanded]="mobileMenuOpen"
|
||||
[attr.aria-label]="mobileMenuOpen ? 'Close menu' : 'Open menu'"
|
||||
type="button">
|
||||
<span class="ui-dashboard-shell__mobile-toggle-icon" aria-hidden="true">
|
||||
@if (mobileMenuOpen) {
|
||||
✕
|
||||
} @else {
|
||||
☰
|
||||
}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!-- Header Content -->
|
||||
<div class="ui-dashboard-shell__header-content">
|
||||
<ng-content select="[slot='header']"></ng-content>
|
||||
</div>
|
||||
|
||||
<!-- Header Actions (Theme Switcher, Notifications, etc.) -->
|
||||
<div class="ui-dashboard-shell__header-actions">
|
||||
<ng-content select="[slot='header-actions']"></ng-content>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Container -->
|
||||
<div class="ui-dashboard-shell__container">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside
|
||||
class="ui-dashboard-shell__sidebar"
|
||||
[class.ui-dashboard-shell__sidebar--sm]="sidebarWidth === 'sm'"
|
||||
[class.ui-dashboard-shell__sidebar--md]="sidebarWidth === 'md'"
|
||||
[class.ui-dashboard-shell__sidebar--lg]="sidebarWidth === 'lg'"
|
||||
[class.ui-dashboard-shell__sidebar--collapsed]="sidebarCollapsed"
|
||||
[attr.aria-hidden]="sidebarCollapsed && !mobileMenuOpen"
|
||||
[attr.aria-expanded]="!sidebarCollapsed || mobileMenuOpen"
|
||||
role="navigation"
|
||||
[attr.aria-label]="sidebarLabel">
|
||||
|
||||
<ng-content select="[slot='sidebar']"></ng-content>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main
|
||||
class="ui-dashboard-shell__main"
|
||||
role="main"
|
||||
[attr.aria-label]="mainContentLabel">
|
||||
|
||||
<!-- Breadcrumbs Area -->
|
||||
@if (showBreadcrumbs) {
|
||||
<nav
|
||||
class="ui-dashboard-shell__breadcrumbs"
|
||||
[attr.aria-label]="breadcrumbsLabel"
|
||||
role="navigation">
|
||||
<ng-content select="[slot='breadcrumbs']"></ng-content>
|
||||
</nav>
|
||||
}
|
||||
|
||||
<!-- Notifications Area -->
|
||||
@if (showNotifications) {
|
||||
<div
|
||||
class="ui-dashboard-shell__notifications"
|
||||
[attr.aria-live]="notificationsLive ? 'polite' : null"
|
||||
role="status">
|
||||
<ng-content select="[slot='notifications']"></ng-content>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Main Content -->
|
||||
<div
|
||||
class="ui-dashboard-shell__content"
|
||||
[attr.aria-label]="contentLabel">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
@if (showFooter) {
|
||||
<footer
|
||||
class="ui-dashboard-shell__footer"
|
||||
[class.ui-dashboard-shell__footer--minimal]="footerVariant === 'minimal'"
|
||||
[class.ui-dashboard-shell__footer--standard]="footerVariant === 'standard'"
|
||||
[class.ui-dashboard-shell__footer--detailed]="footerVariant === 'detailed'"
|
||||
role="contentinfo"
|
||||
[attr.aria-label]="footerLabel">
|
||||
|
||||
<ng-content select="[slot='footer']"></ng-content>
|
||||
</footer>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './dashboard-shell.component.scss'
|
||||
})
|
||||
export class DashboardShellComponent {
|
||||
@Input() variant: DashboardVariant = 'default';
|
||||
@Input() sidebarWidth: SidebarWidth = 'md';
|
||||
@Input() headerHeight: HeaderHeight = 'md';
|
||||
@Input() footerVariant: FooterVariant = 'standard';
|
||||
@Input() sidebarCollapsed = false;
|
||||
@Input() mobileMenuOpen = false;
|
||||
@Input() showFooter = true;
|
||||
@Input() showBreadcrumbs = true;
|
||||
@Input() showNotifications = true;
|
||||
@Input() notificationsLive = true;
|
||||
@Input() enableMobileBackdropClose = true;
|
||||
@Input() role = 'application';
|
||||
|
||||
// ARIA Labels
|
||||
@Input() sidebarLabel = 'Main navigation';
|
||||
@Input() mainContentLabel = 'Main content';
|
||||
@Input() breadcrumbsLabel = 'Breadcrumb navigation';
|
||||
@Input() contentLabel = 'Page content';
|
||||
@Input() footerLabel = 'Site footer';
|
||||
|
||||
// Events
|
||||
@Output() sidebarToggled = new EventEmitter<boolean>();
|
||||
@Output() mobileMenuToggled = new EventEmitter<boolean>();
|
||||
@Output() mobileBackdropClicked = new EventEmitter<void>();
|
||||
|
||||
handleMobileToggle(): void {
|
||||
const newState = !this.mobileMenuOpen;
|
||||
this.mobileMenuToggled.emit(newState);
|
||||
}
|
||||
|
||||
handleMobileBackdropClick(): void {
|
||||
if (this.enableMobileBackdropClose) {
|
||||
this.mobileBackdropClicked.emit();
|
||||
}
|
||||
}
|
||||
|
||||
handleSidebarToggle(): void {
|
||||
const newState = !this.sidebarCollapsed;
|
||||
this.sidebarToggled.emit(newState);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { DashboardShellComponent } from './dashboard-shell.component';
|
||||
@@ -0,0 +1,260 @@
|
||||
@use "../../../../../../ui-design-system/src/styles/semantic/index" as *;
|
||||
|
||||
.ui-feed-layout {
|
||||
// Core Structure
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
// Layout & Spacing
|
||||
background: $semantic-color-surface;
|
||||
|
||||
// Size Variants
|
||||
&--sm {
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
&--md {
|
||||
max-width: 768px;
|
||||
}
|
||||
|
||||
&--lg {
|
||||
max-width: 1024px;
|
||||
}
|
||||
|
||||
// Loading State
|
||||
&--loading {
|
||||
.ui-feed-layout__container {
|
||||
opacity: $semantic-opacity-subtle;
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh Enabled
|
||||
&--refresh-enabled {
|
||||
.ui-feed-layout__container {
|
||||
touch-action: pan-y;
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh Indicator
|
||||
&__refresh-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
font-family: map-get($semantic-typography-body-small, font-family);
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: map-get($semantic-typography-body-small, font-weight);
|
||||
line-height: map-get($semantic-typography-body-small, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
}
|
||||
|
||||
&__refresh-spinner {
|
||||
width: $semantic-sizing-icon-inline;
|
||||
height: $semantic-sizing-icon-inline;
|
||||
border: 2px solid $semantic-color-border-subtle;
|
||||
border-top-color: $semantic-color-primary;
|
||||
border-radius: $semantic-border-radius-full;
|
||||
animation: ui-feed-layout-spin $semantic-motion-duration-slow linear infinite;
|
||||
}
|
||||
|
||||
// Container
|
||||
&__container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scroll-behavior: smooth;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
// Custom scrollbar
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: $semantic-color-surface-secondary;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loader
|
||||
&__loader {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
|
||||
border-top: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
background: $semantic-color-surface-elevated;
|
||||
}
|
||||
|
||||
&__loader-spinner {
|
||||
width: $semantic-sizing-icon-button;
|
||||
height: $semantic-sizing-icon-button;
|
||||
border: 2px solid $semantic-color-border-subtle;
|
||||
border-top-color: $semantic-color-primary;
|
||||
border-radius: $semantic-border-radius-full;
|
||||
animation: ui-feed-layout-spin $semantic-motion-duration-slow linear infinite;
|
||||
}
|
||||
|
||||
&__loader-text {
|
||||
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;
|
||||
}
|
||||
|
||||
// Error State
|
||||
&__error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $semantic-spacing-component-md;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
|
||||
background: $semantic-color-surface-elevated;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-error;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
margin: $semantic-spacing-component-md;
|
||||
|
||||
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-danger;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__retry-button {
|
||||
padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
|
||||
background: $semantic-color-danger;
|
||||
color: $semantic-color-on-danger;
|
||||
border: none;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
cursor: pointer;
|
||||
|
||||
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);
|
||||
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
opacity: $semantic-opacity-hover;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: $semantic-opacity-disabled;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
// Empty State
|
||||
&__empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $semantic-spacing-layout-section-lg;
|
||||
min-height: 200px;
|
||||
|
||||
font-family: map-get($semantic-typography-body-large, font-family);
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
font-weight: map-get($semantic-typography-body-large, font-weight);
|
||||
line-height: map-get($semantic-typography-body-large, line-height);
|
||||
color: $semantic-color-text-tertiary;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// Refresh Zone (for pull-to-refresh)
|
||||
&__refresh-zone {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 100px;
|
||||
z-index: -1;
|
||||
|
||||
&--active {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-bottom: 2px solid $semantic-color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
// Animation Keyframes
|
||||
@keyframes ui-feed-layout-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: 768px) {
|
||||
&__loader,
|
||||
&__error,
|
||||
&__empty {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&__refresh-indicator {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
|
||||
font-family: map-get($semantic-typography-caption, font-family);
|
||||
font-size: map-get($semantic-typography-caption, font-size);
|
||||
font-weight: map-get($semantic-typography-caption, font-weight);
|
||||
line-height: map-get($semantic-typography-caption, line-height);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
&--sm,
|
||||
&--md,
|
||||
&--lg {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
&__empty {
|
||||
min-height: 150px;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Focus management for accessibility
|
||||
.ui-feed-layout:focus-within {
|
||||
.ui-feed-layout__container::-webkit-scrollbar-thumb {
|
||||
background: $semantic-color-focus;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, ElementRef, ViewChild, OnInit, OnDestroy, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
type FeedLayoutSize = 'sm' | 'md' | 'lg';
|
||||
|
||||
export interface FeedItem {
|
||||
id: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ui-feed-layout',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<div
|
||||
class="ui-feed-layout"
|
||||
[class.ui-feed-layout--{{size}}]="size"
|
||||
[class.ui-feed-layout--loading]="loading"
|
||||
[class.ui-feed-layout--refresh-enabled]="enableRefresh"
|
||||
[attr.aria-busy]="loading"
|
||||
[attr.aria-label]="ariaLabel"
|
||||
role="feed">
|
||||
|
||||
@if (enableRefresh && isRefreshing()) {
|
||||
<div class="ui-feed-layout__refresh-indicator" aria-hidden="true">
|
||||
<div class="ui-feed-layout__refresh-spinner"></div>
|
||||
<span>Refreshing...</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div
|
||||
class="ui-feed-layout__container"
|
||||
#scrollContainer
|
||||
(scroll)="handleScroll($event)"
|
||||
role="log"
|
||||
aria-live="polite"
|
||||
aria-atomic="false">
|
||||
|
||||
<ng-content></ng-content>
|
||||
|
||||
@if (loading && !isRefreshing()) {
|
||||
<div class="ui-feed-layout__loader" role="status" aria-live="polite">
|
||||
<div class="ui-feed-layout__loader-spinner" aria-hidden="true"></div>
|
||||
<span class="ui-feed-layout__loader-text">Loading more content...</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (hasError) {
|
||||
<div class="ui-feed-layout__error" role="alert">
|
||||
<span>{{ errorMessage || 'Failed to load content' }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="ui-feed-layout__retry-button"
|
||||
(click)="handleRetry()"
|
||||
[disabled]="loading">
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isEmpty && !loading) {
|
||||
<div class="ui-feed-layout__empty" role="status">
|
||||
<ng-content select="[slot=empty]">
|
||||
<span>No content available</span>
|
||||
</ng-content>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (enableRefresh) {
|
||||
<div
|
||||
class="ui-feed-layout__refresh-zone"
|
||||
[class.ui-feed-layout__refresh-zone--active]="isPullingToRefresh()"
|
||||
(touchstart)="handleTouchStart($event)"
|
||||
(touchmove)="handleTouchMove($event)"
|
||||
(touchend)="handleTouchEnd($event)">
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './feed-layout.component.scss'
|
||||
})
|
||||
export class FeedLayoutComponent implements OnInit, OnDestroy {
|
||||
@Input() size: FeedLayoutSize = 'md';
|
||||
@Input() loading = false;
|
||||
@Input() hasError = false;
|
||||
@Input() errorMessage = '';
|
||||
@Input() isEmpty = false;
|
||||
@Input() enableInfiniteScroll = true;
|
||||
@Input() enableRefresh = true;
|
||||
@Input() scrollThreshold = 200;
|
||||
@Input() refreshThreshold = 80;
|
||||
@Input() ariaLabel = 'Content feed';
|
||||
|
||||
@Output() loadMore = new EventEmitter<void>();
|
||||
@Output() refresh = new EventEmitter<void>();
|
||||
@Output() retry = new EventEmitter<void>();
|
||||
@Output() scrolled = new EventEmitter<Event>();
|
||||
|
||||
@ViewChild('scrollContainer', { static: true }) scrollContainer!: ElementRef<HTMLDivElement>;
|
||||
|
||||
protected readonly isRefreshing = signal(false);
|
||||
protected readonly isPullingToRefresh = signal(false);
|
||||
|
||||
private touchStartY = 0;
|
||||
private touchCurrentY = 0;
|
||||
private scrollThrottleTimer: number | null = null;
|
||||
private resizeObserver?: ResizeObserver;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.setupResizeObserver();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.scrollThrottleTimer) {
|
||||
clearTimeout(this.scrollThrottleTimer);
|
||||
}
|
||||
this.resizeObserver?.disconnect();
|
||||
}
|
||||
|
||||
private setupResizeObserver(): void {
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
this.checkScrollPosition();
|
||||
});
|
||||
this.resizeObserver.observe(this.scrollContainer.nativeElement);
|
||||
}
|
||||
}
|
||||
|
||||
handleScroll(event: Event): void {
|
||||
this.scrolled.emit(event);
|
||||
|
||||
if (!this.enableInfiniteScroll || this.loading || this.hasError) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.scrollThrottleTimer) {
|
||||
clearTimeout(this.scrollThrottleTimer);
|
||||
}
|
||||
|
||||
this.scrollThrottleTimer = window.setTimeout(() => {
|
||||
this.checkScrollPosition();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
private checkScrollPosition(): void {
|
||||
const container = this.scrollContainer.nativeElement;
|
||||
const scrollTop = container.scrollTop;
|
||||
const scrollHeight = container.scrollHeight;
|
||||
const clientHeight = container.clientHeight;
|
||||
|
||||
if (scrollTop + clientHeight >= scrollHeight - this.scrollThreshold) {
|
||||
this.loadMore.emit();
|
||||
}
|
||||
}
|
||||
|
||||
handleTouchStart(event: TouchEvent): void {
|
||||
if (!this.enableRefresh) return;
|
||||
|
||||
this.touchStartY = event.touches[0].clientY;
|
||||
}
|
||||
|
||||
handleTouchMove(event: TouchEvent): void {
|
||||
if (!this.enableRefresh || this.scrollContainer.nativeElement.scrollTop > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.touchCurrentY = event.touches[0].clientY;
|
||||
const pullDistance = this.touchCurrentY - this.touchStartY;
|
||||
|
||||
if (pullDistance > 0) {
|
||||
this.isPullingToRefresh.set(pullDistance > this.refreshThreshold);
|
||||
|
||||
if (pullDistance > this.refreshThreshold) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleTouchEnd(event: TouchEvent): void {
|
||||
if (!this.enableRefresh) return;
|
||||
|
||||
const pullDistance = this.touchCurrentY - this.touchStartY;
|
||||
|
||||
if (pullDistance > this.refreshThreshold && !this.loading) {
|
||||
this.triggerRefresh();
|
||||
}
|
||||
|
||||
this.isPullingToRefresh.set(false);
|
||||
this.touchStartY = 0;
|
||||
this.touchCurrentY = 0;
|
||||
}
|
||||
|
||||
private triggerRefresh(): void {
|
||||
this.isRefreshing.set(true);
|
||||
this.refresh.emit();
|
||||
|
||||
// Auto-reset refreshing state after 2 seconds if not manually reset
|
||||
setTimeout(() => {
|
||||
if (this.isRefreshing()) {
|
||||
this.isRefreshing.set(false);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
handleRetry(): void {
|
||||
this.retry.emit();
|
||||
}
|
||||
|
||||
// Public method to reset refresh state
|
||||
resetRefresh(): void {
|
||||
this.isRefreshing.set(false);
|
||||
}
|
||||
|
||||
// Public method to scroll to top
|
||||
scrollToTop(): void {
|
||||
this.scrollContainer.nativeElement.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './feed-layout.component';
|
||||
@@ -0,0 +1,190 @@
|
||||
@use '../../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-flex {
|
||||
// Core flexbox container
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
|
||||
// Direction variants
|
||||
&--row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
&--row-reverse {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
&--column {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&--column-reverse {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
// Wrap variants
|
||||
&--nowrap {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
&--wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&--wrap-reverse {
|
||||
flex-wrap: wrap-reverse;
|
||||
}
|
||||
|
||||
// Justify content variants
|
||||
&--justify-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&--justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&--justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&--justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&--justify-around {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
&--justify-evenly {
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
// Align items variants
|
||||
&--align-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&--align-end {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
&--align-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&--align-baseline {
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
&--align-stretch {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
// Align content variants (for wrapped flex containers)
|
||||
&--align-content-start {
|
||||
align-content: flex-start;
|
||||
}
|
||||
|
||||
&--align-content-end {
|
||||
align-content: flex-end;
|
||||
}
|
||||
|
||||
&--align-content-center {
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
&--align-content-between {
|
||||
align-content: space-between;
|
||||
}
|
||||
|
||||
&--align-content-around {
|
||||
align-content: space-around;
|
||||
}
|
||||
|
||||
&--align-content-stretch {
|
||||
align-content: stretch;
|
||||
}
|
||||
|
||||
// Gap variants using semantic spacing tokens
|
||||
&--gap-xs {
|
||||
gap: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
&--gap-sm {
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
&--gap-md {
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&--gap-lg {
|
||||
gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
&--gap-xl {
|
||||
gap: $semantic-spacing-component-xl;
|
||||
}
|
||||
|
||||
// Row gap variants
|
||||
&--row-gap-xs {
|
||||
row-gap: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
&--row-gap-sm {
|
||||
row-gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
&--row-gap-md {
|
||||
row-gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&--row-gap-lg {
|
||||
row-gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
&--row-gap-xl {
|
||||
row-gap: $semantic-spacing-component-xl;
|
||||
}
|
||||
|
||||
// Column gap variants
|
||||
&--column-gap-xs {
|
||||
column-gap: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
&--column-gap-sm {
|
||||
column-gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
&--column-gap-md {
|
||||
column-gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&--column-gap-lg {
|
||||
column-gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
&--column-gap-xl {
|
||||
column-gap: $semantic-spacing-component-xl;
|
||||
}
|
||||
|
||||
// Inline flex variant
|
||||
&--inline {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
// Full width/height variants
|
||||
&--full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&--full-height {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&--full {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
type FlexDirection = 'row' | 'row-reverse' | 'column' | 'column-reverse';
|
||||
type FlexWrap = 'nowrap' | 'wrap' | 'wrap-reverse';
|
||||
type FlexJustify = 'start' | 'end' | 'center' | 'between' | 'around' | 'evenly';
|
||||
type FlexAlign = 'start' | 'end' | 'center' | 'baseline' | 'stretch';
|
||||
type FlexAlignContent = 'start' | 'end' | 'center' | 'between' | 'around' | 'stretch';
|
||||
type FlexGap = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-flex',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<div
|
||||
[ngClass]="getClasses()"
|
||||
[attr.role]="role">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './flex.component.scss'
|
||||
})
|
||||
export class FlexComponent {
|
||||
// Flexbox properties
|
||||
@Input() direction: FlexDirection = 'row';
|
||||
@Input() wrap: FlexWrap = 'nowrap';
|
||||
@Input() justify: FlexJustify = 'start';
|
||||
@Input() align: FlexAlign = 'stretch';
|
||||
@Input() alignContent?: FlexAlignContent;
|
||||
|
||||
// Gap properties
|
||||
@Input() gap?: FlexGap;
|
||||
@Input() rowGap?: FlexGap;
|
||||
@Input() columnGap?: FlexGap;
|
||||
|
||||
// Display variants
|
||||
@Input() inline = false;
|
||||
|
||||
// Size variants
|
||||
@Input() fullWidth = false;
|
||||
@Input() fullHeight = false;
|
||||
@Input() full = false;
|
||||
|
||||
// Accessibility
|
||||
@Input() role?: string;
|
||||
|
||||
getClasses(): Record<string, boolean> {
|
||||
const classes: Record<string, boolean> = {
|
||||
'ui-flex': true
|
||||
};
|
||||
|
||||
// Direction
|
||||
if (this.direction) {
|
||||
classes[`ui-flex--${this.direction}`] = true;
|
||||
}
|
||||
|
||||
// Wrap
|
||||
if (this.wrap) {
|
||||
classes[`ui-flex--${this.wrap}`] = true;
|
||||
}
|
||||
|
||||
// Justify content
|
||||
if (this.justify) {
|
||||
classes[`ui-flex--justify-${this.justify}`] = true;
|
||||
}
|
||||
|
||||
// Align items
|
||||
if (this.align) {
|
||||
classes[`ui-flex--align-${this.align}`] = true;
|
||||
}
|
||||
|
||||
// Align content (for wrapped containers)
|
||||
if (this.alignContent) {
|
||||
classes[`ui-flex--align-content-${this.alignContent}`] = true;
|
||||
}
|
||||
|
||||
// Gap properties
|
||||
if (this.gap) {
|
||||
classes[`ui-flex--gap-${this.gap}`] = true;
|
||||
}
|
||||
|
||||
if (this.rowGap) {
|
||||
classes[`ui-flex--row-gap-${this.rowGap}`] = true;
|
||||
}
|
||||
|
||||
if (this.columnGap) {
|
||||
classes[`ui-flex--column-gap-${this.columnGap}`] = true;
|
||||
}
|
||||
|
||||
// Display variant
|
||||
if (this.inline) {
|
||||
classes['ui-flex--inline'] = true;
|
||||
}
|
||||
|
||||
// Size variants
|
||||
if (this.full) {
|
||||
classes['ui-flex--full'] = true;
|
||||
} else {
|
||||
if (this.fullWidth) {
|
||||
classes['ui-flex--full-width'] = true;
|
||||
}
|
||||
if (this.fullHeight) {
|
||||
classes['ui-flex--full-height'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return classes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './flex.component';
|
||||
@@ -0,0 +1,315 @@
|
||||
@use '../../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-grid-container {
|
||||
// Core Structure
|
||||
display: grid;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
// Layout & Spacing
|
||||
gap: $semantic-spacing-grid-gap-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
|
||||
// Visual Design
|
||||
background: $semantic-color-surface-primary;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
|
||||
// Sizing Variants
|
||||
&--gap-sm {
|
||||
gap: $semantic-spacing-grid-gap-sm;
|
||||
}
|
||||
|
||||
&--gap-md {
|
||||
gap: $semantic-spacing-grid-gap-md;
|
||||
}
|
||||
|
||||
&--gap-lg {
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
}
|
||||
|
||||
// Padding Variants
|
||||
&--padding-none {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&--padding-sm {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
&--padding-md {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&--padding-lg {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
// Column Variants - Auto Grid
|
||||
&--auto-fit {
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
}
|
||||
|
||||
&--auto-fill {
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
}
|
||||
|
||||
// Responsive Column Templates
|
||||
&--cols-1 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
&--cols-2 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
&--cols-3 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
&--cols-4 {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
&--cols-5 {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
}
|
||||
|
||||
&--cols-6 {
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
}
|
||||
|
||||
// Row Variants
|
||||
&--rows-auto {
|
||||
grid-auto-rows: auto;
|
||||
}
|
||||
|
||||
&--rows-equal {
|
||||
grid-auto-rows: 1fr;
|
||||
}
|
||||
|
||||
&--rows-min-content {
|
||||
grid-auto-rows: min-content;
|
||||
}
|
||||
|
||||
&--rows-max-content {
|
||||
grid-auto-rows: max-content;
|
||||
}
|
||||
|
||||
// Dense Grid for Auto Placement
|
||||
&--dense {
|
||||
grid-auto-flow: dense;
|
||||
}
|
||||
|
||||
// Alignment Options
|
||||
&--justify-start {
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
&--justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&--justify-end {
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
&--justify-space-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&--justify-space-around {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
&--justify-space-evenly {
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
&--align-start {
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
&--align-center {
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
&--align-end {
|
||||
align-content: end;
|
||||
}
|
||||
|
||||
&--align-space-between {
|
||||
align-content: space-between;
|
||||
}
|
||||
|
||||
&--align-space-around {
|
||||
align-content: space-around;
|
||||
}
|
||||
|
||||
&--align-space-evenly {
|
||||
align-content: space-evenly;
|
||||
}
|
||||
|
||||
// Item Alignment
|
||||
&--items-start {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
&--items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&--items-end {
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
&--items-stretch {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
// Responsive Behavior
|
||||
@media (max-width: 1024px) {
|
||||
&--cols-6 {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
&--cols-5 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
&--cols-4 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
gap: $semantic-spacing-grid-gap-sm;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
|
||||
&--cols-6,
|
||||
&--cols-5,
|
||||
&--cols-4,
|
||||
&--cols-3 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
gap: $semantic-spacing-component-xs;
|
||||
padding: $semantic-spacing-component-xs;
|
||||
|
||||
&--cols-6,
|
||||
&--cols-5,
|
||||
&--cols-4,
|
||||
&--cols-3,
|
||||
&--cols-2 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
&--auto-fit,
|
||||
&--auto-fill {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Grid Item Utilities
|
||||
.ui-grid-item {
|
||||
// Span Utilities
|
||||
&--span-1 {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
&--span-2 {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
&--span-3 {
|
||||
grid-column: span 3;
|
||||
}
|
||||
|
||||
&--span-4 {
|
||||
grid-column: span 4;
|
||||
}
|
||||
|
||||
&--span-5 {
|
||||
grid-column: span 5;
|
||||
}
|
||||
|
||||
&--span-6 {
|
||||
grid-column: span 6;
|
||||
}
|
||||
|
||||
&--span-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
// Row Span Utilities
|
||||
&--row-span-1 {
|
||||
grid-row: span 1;
|
||||
}
|
||||
|
||||
&--row-span-2 {
|
||||
grid-row: span 2;
|
||||
}
|
||||
|
||||
&--row-span-3 {
|
||||
grid-row: span 3;
|
||||
}
|
||||
|
||||
&--row-span-4 {
|
||||
grid-row: span 4;
|
||||
}
|
||||
|
||||
// Item Alignment
|
||||
&--justify-self-start {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
&--justify-self-center {
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
&--justify-self-end {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
&--justify-self-stretch {
|
||||
justify-self: stretch;
|
||||
}
|
||||
|
||||
&--align-self-start {
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
&--align-self-center {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
&--align-self-end {
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
&--align-self-stretch {
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
// Responsive Item Spans
|
||||
@media (max-width: 768px) {
|
||||
&--span-6,
|
||||
&--span-5,
|
||||
&--span-4,
|
||||
&--span-3 {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
&--span-6,
|
||||
&--span-5,
|
||||
&--span-4,
|
||||
&--span-3,
|
||||
&--span-2 {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
type GridColumns = 1 | 2 | 3 | 4 | 5 | 6 | 'auto-fit' | 'auto-fill';
|
||||
type GridGap = 'sm' | 'md' | 'lg';
|
||||
type GridPadding = 'none' | 'sm' | 'md' | 'lg';
|
||||
type GridRowMode = 'auto' | 'equal' | 'min-content' | 'max-content';
|
||||
type GridJustifyContent = 'start' | 'center' | 'end' | 'space-between' | 'space-around' | 'space-evenly';
|
||||
type GridAlignContent = 'start' | 'center' | 'end' | 'space-between' | 'space-around' | 'space-evenly';
|
||||
type GridAlignItems = 'start' | 'center' | 'end' | 'stretch';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-grid-container',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<div
|
||||
class="ui-grid-container"
|
||||
[class.ui-grid-container--gap-sm]="gap === 'sm'"
|
||||
[class.ui-grid-container--gap-md]="gap === 'md'"
|
||||
[class.ui-grid-container--gap-lg]="gap === 'lg'"
|
||||
[class.ui-grid-container--padding-none]="padding === 'none'"
|
||||
[class.ui-grid-container--padding-sm]="padding === 'sm'"
|
||||
[class.ui-grid-container--padding-md]="padding === 'md'"
|
||||
[class.ui-grid-container--padding-lg]="padding === 'lg'"
|
||||
[class.ui-grid-container--cols-1]="columns === 1"
|
||||
[class.ui-grid-container--cols-2]="columns === 2"
|
||||
[class.ui-grid-container--cols-3]="columns === 3"
|
||||
[class.ui-grid-container--cols-4]="columns === 4"
|
||||
[class.ui-grid-container--cols-5]="columns === 5"
|
||||
[class.ui-grid-container--cols-6]="columns === 6"
|
||||
[class.ui-grid-container--auto-fit]="columns === 'auto-fit'"
|
||||
[class.ui-grid-container--auto-fill]="columns === 'auto-fill'"
|
||||
[class.ui-grid-container--rows-auto]="rowMode === 'auto'"
|
||||
[class.ui-grid-container--rows-equal]="rowMode === 'equal'"
|
||||
[class.ui-grid-container--rows-min-content]="rowMode === 'min-content'"
|
||||
[class.ui-grid-container--rows-max-content]="rowMode === 'max-content'"
|
||||
[class.ui-grid-container--dense]="dense"
|
||||
[class.ui-grid-container--justify-start]="justifyContent === 'start'"
|
||||
[class.ui-grid-container--justify-center]="justifyContent === 'center'"
|
||||
[class.ui-grid-container--justify-end]="justifyContent === 'end'"
|
||||
[class.ui-grid-container--justify-space-between]="justifyContent === 'space-between'"
|
||||
[class.ui-grid-container--justify-space-around]="justifyContent === 'space-around'"
|
||||
[class.ui-grid-container--justify-space-evenly]="justifyContent === 'space-evenly'"
|
||||
[class.ui-grid-container--align-start]="alignContent === 'start'"
|
||||
[class.ui-grid-container--align-center]="alignContent === 'center'"
|
||||
[class.ui-grid-container--align-end]="alignContent === 'end'"
|
||||
[class.ui-grid-container--align-space-between]="alignContent === 'space-between'"
|
||||
[class.ui-grid-container--align-space-around]="alignContent === 'space-around'"
|
||||
[class.ui-grid-container--align-space-evenly]="alignContent === 'space-evenly'"
|
||||
[class.ui-grid-container--items-start]="alignItems === 'start'"
|
||||
[class.ui-grid-container--items-center]="alignItems === 'center'"
|
||||
[class.ui-grid-container--items-end]="alignItems === 'end'"
|
||||
[class.ui-grid-container--items-stretch]="alignItems === 'stretch'"
|
||||
[style.grid-template-columns]="customColumns || null"
|
||||
[style.grid-template-rows]="customRows || null"
|
||||
[style.grid-template-areas]="templateAreas || null"
|
||||
[attr.role]="role"
|
||||
[attr.aria-label]="ariaLabel">
|
||||
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './grid-container.component.scss'
|
||||
})
|
||||
export class GridContainerComponent {
|
||||
@Input() columns: GridColumns = 'auto-fit';
|
||||
@Input() gap: GridGap = 'md';
|
||||
@Input() padding: GridPadding = 'md';
|
||||
@Input() rowMode: GridRowMode = 'auto';
|
||||
@Input() dense = false;
|
||||
@Input() justifyContent: GridJustifyContent | null = null;
|
||||
@Input() alignContent: GridAlignContent | null = null;
|
||||
@Input() alignItems: GridAlignItems = 'stretch';
|
||||
|
||||
// Advanced CSS Grid Properties
|
||||
@Input() customColumns: string | null = null;
|
||||
@Input() customRows: string | null = null;
|
||||
@Input() templateAreas: string | null = null;
|
||||
|
||||
// Accessibility
|
||||
@Input() role: string = 'grid';
|
||||
@Input() ariaLabel: string | null = null;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './grid-container.component';
|
||||
@@ -0,0 +1,165 @@
|
||||
@use '../../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-hstack {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
|
||||
// Inline variant
|
||||
&--inline {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
// Full width variant
|
||||
&--full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// Spacing variants - using semantic stack spacing tokens
|
||||
&--spacing-xs {
|
||||
gap: $semantic-spacing-stack-xs;
|
||||
}
|
||||
|
||||
&--spacing-sm {
|
||||
gap: $semantic-spacing-stack-sm;
|
||||
}
|
||||
|
||||
&--spacing-md {
|
||||
gap: $semantic-spacing-stack-md;
|
||||
}
|
||||
|
||||
&--spacing-lg {
|
||||
gap: $semantic-spacing-stack-lg;
|
||||
}
|
||||
|
||||
&--spacing-xl {
|
||||
gap: $semantic-spacing-stack-xl;
|
||||
}
|
||||
|
||||
&--spacing-2xl {
|
||||
gap: $semantic-spacing-2xl;
|
||||
}
|
||||
|
||||
&--spacing-3xl {
|
||||
gap: $semantic-spacing-3xl;
|
||||
}
|
||||
|
||||
&--spacing-4xl {
|
||||
gap: $semantic-spacing-4xl;
|
||||
}
|
||||
|
||||
&--spacing-5xl {
|
||||
gap: $semantic-spacing-5xl;
|
||||
}
|
||||
|
||||
// Vertical alignment (cross-axis for row)
|
||||
&--align-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&--align-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&--align-end {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
&--align-stretch {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
&--align-baseline {
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
// Horizontal justify (main-axis for row)
|
||||
&--justify-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&--justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&--justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&--justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&--justify-around {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
&--justify-evenly {
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
// Wrap variants
|
||||
&--wrap-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&--wrap-nowrap {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
&--wrap-wrap-reverse {
|
||||
flex-wrap: wrap-reverse;
|
||||
}
|
||||
|
||||
// Divider variant - adds vertical borders between children
|
||||
&--divider {
|
||||
> :not(:last-child) {
|
||||
border-right: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
padding-right: $semantic-spacing-component-sm;
|
||||
margin-right: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
// Remove gap when using dividers to avoid double spacing
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
// Responsive behavior
|
||||
&--responsive {
|
||||
@media (max-width: 640px) {
|
||||
// Convert to vertical stack on small screens for better usability
|
||||
flex-direction: column;
|
||||
|
||||
// Adjust dividers for responsive layout
|
||||
&.ui-hstack--divider {
|
||||
> :not(:last-child) {
|
||||
border-right: none;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
padding-right: 0;
|
||||
margin-right: 0;
|
||||
padding-bottom: $semantic-spacing-component-sm;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
// Reduce spacing on small screens
|
||||
&.ui-hstack--spacing-xl,
|
||||
&.ui-hstack--spacing-2xl,
|
||||
&.ui-hstack--spacing-3xl,
|
||||
&.ui-hstack--spacing-4xl,
|
||||
&.ui-hstack--spacing-5xl {
|
||||
gap: $semantic-spacing-stack-lg;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
&.ui-hstack--spacing-lg,
|
||||
&.ui-hstack--spacing-xl,
|
||||
&.ui-hstack--spacing-2xl,
|
||||
&.ui-hstack--spacing-3xl,
|
||||
&.ui-hstack--spacing-4xl,
|
||||
&.ui-hstack--spacing-5xl {
|
||||
gap: $semantic-spacing-stack-md;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
type StackSpacing = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl';
|
||||
type VerticalAlignment = 'start' | 'center' | 'end' | 'stretch' | 'baseline';
|
||||
type HorizontalJustify = 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly';
|
||||
type StackWrap = 'nowrap' | 'wrap' | 'wrap-reverse';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-hstack',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<div
|
||||
[ngClass]="getClasses()"
|
||||
[attr.role]="role"
|
||||
[style.gap]="customGap">
|
||||
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './hstack.component.scss'
|
||||
})
|
||||
export class HStackComponent {
|
||||
@Input() spacing: StackSpacing = 'md';
|
||||
@Input() align?: VerticalAlignment;
|
||||
@Input() justify?: HorizontalJustify;
|
||||
@Input() wrap?: StackWrap;
|
||||
@Input() inline = false;
|
||||
@Input() responsive = true;
|
||||
@Input() divider = false;
|
||||
@Input() fullWidth = false;
|
||||
@Input() customGap?: string;
|
||||
@Input() role?: string;
|
||||
|
||||
getClasses(): Record<string, boolean> {
|
||||
const classes: Record<string, boolean> = {
|
||||
'ui-hstack': true,
|
||||
[`ui-hstack--spacing-${this.spacing}`]: true,
|
||||
'ui-hstack--inline': this.inline,
|
||||
'ui-hstack--responsive': this.responsive,
|
||||
'ui-hstack--divider': this.divider,
|
||||
'ui-hstack--full-width': this.fullWidth
|
||||
};
|
||||
|
||||
if (this.align) {
|
||||
classes[`ui-hstack--align-${this.align}`] = true;
|
||||
}
|
||||
|
||||
if (this.justify) {
|
||||
classes[`ui-hstack--justify-${this.justify}`] = true;
|
||||
}
|
||||
|
||||
if (this.wrap) {
|
||||
classes[`ui-hstack--wrap-${this.wrap}`] = true;
|
||||
}
|
||||
|
||||
return classes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './hstack.component';
|
||||
@@ -1,3 +1,23 @@
|
||||
export * from './aspect-ratio';
|
||||
export * from './bento-grid';
|
||||
export * from './box';
|
||||
export * from './breakpoint-container';
|
||||
export * from './center';
|
||||
export * from './column';
|
||||
export * from './container';
|
||||
export * from './dashboard-shell';
|
||||
export * from './divider';
|
||||
export * from './feed-layout';
|
||||
export * from './flex';
|
||||
export * from './grid-container';
|
||||
export * from './grid-system';
|
||||
export * from './hstack';
|
||||
export * from './list-detail-layout';
|
||||
export * from './scroll-container';
|
||||
export * from './section';
|
||||
export * from './sidebar-layout';
|
||||
export * from './spacer';
|
||||
export * from './stack';
|
||||
export * from './supporting-pane-layout';
|
||||
export * from './tabs-container';
|
||||
export * from './vstack';
|
||||
@@ -0,0 +1 @@
|
||||
export * from './list-detail-layout.component';
|
||||
@@ -0,0 +1,247 @@
|
||||
@use '../../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-list-detail-layout {
|
||||
// Core structure
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
|
||||
// Visual design
|
||||
background: $semantic-color-surface-primary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
|
||||
// Typography
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
// Mobile-first orientation
|
||||
flex-direction: column;
|
||||
|
||||
// Desktop orientation
|
||||
@media (min-width: $semantic-breakpoint-md) {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
// List panel
|
||||
&__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-right: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
overflow: hidden;
|
||||
|
||||
// Mobile: full height, collapsed when detail shown
|
||||
flex: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
// Desktop: fixed width, full height
|
||||
@media (min-width: $semantic-breakpoint-md) {
|
||||
flex: none;
|
||||
width: 320px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
// Resizable on desktop
|
||||
@media (min-width: $semantic-breakpoint-md) {
|
||||
resize: horizontal;
|
||||
min-width: 280px;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
// Mobile: hide when detail is shown
|
||||
&--hidden-mobile {
|
||||
@media (max-width: calc($semantic-breakpoint-md - 1px)) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Detail panel
|
||||
&__detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
background: $semantic-color-surface-primary;
|
||||
overflow: hidden;
|
||||
|
||||
// Mobile: full height, shown only when item selected
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
// Desktop: flexible width
|
||||
@media (min-width: $semantic-breakpoint-md) {
|
||||
flex: 1;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
// Mobile: hide when no selection
|
||||
&--hidden-mobile {
|
||||
@media (max-width: calc($semantic-breakpoint-md - 1px)) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state
|
||||
&--empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $semantic-spacing-layout-section-lg;
|
||||
color: $semantic-color-text-secondary;
|
||||
|
||||
font-family: map-get($semantic-typography-body-large, font-family);
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
font-weight: map-get($semantic-typography-body-large, font-weight);
|
||||
line-height: map-get($semantic-typography-body-large, line-height);
|
||||
}
|
||||
}
|
||||
|
||||
// Resize handle for desktop
|
||||
&__resize-handle {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -2px;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
background: transparent;
|
||||
z-index: 1;
|
||||
|
||||
@media (min-width: $semantic-breakpoint-md) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
background: $semantic-color-primary;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile navigation controls
|
||||
&__mobile-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
|
||||
@media (min-width: $semantic-breakpoint-md) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__mobile-nav-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
|
||||
background: $semantic-color-surface-primary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
color: $semantic-color-text-primary;
|
||||
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-surface-elevated;
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: $semantic-shadow-button-rest;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
opacity: $semantic-opacity-disabled;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Size variants
|
||||
&--sm {
|
||||
.ui-list-detail-layout__list {
|
||||
@media (min-width: $semantic-breakpoint-md) {
|
||||
width: 280px;
|
||||
min-width: 240px;
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--lg {
|
||||
.ui-list-detail-layout__list {
|
||||
@media (min-width: $semantic-breakpoint-md) {
|
||||
width: 400px;
|
||||
min-width: 320px;
|
||||
max-width: 800px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Variant styles
|
||||
&--bordered {
|
||||
border: $semantic-border-width-2 solid $semantic-color-border-primary;
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
}
|
||||
|
||||
&--elevated {
|
||||
border: none;
|
||||
box-shadow: $semantic-shadow-elevation-3;
|
||||
}
|
||||
|
||||
// Selection states for items (to be applied via content projection)
|
||||
::ng-deep .ui-list-item {
|
||||
&--selected {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-left: 3px solid $semantic-color-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
&--hover {
|
||||
background: $semantic-color-surface-container;
|
||||
}
|
||||
}
|
||||
|
||||
// Loading state
|
||||
&--loading {
|
||||
pointer-events: none;
|
||||
opacity: $semantic-opacity-subtle;
|
||||
}
|
||||
|
||||
// Responsive adjustments for small screens
|
||||
@media (max-width: calc($semantic-breakpoint-sm - 1px)) {
|
||||
border-radius: 0;
|
||||
height: 100vh;
|
||||
|
||||
.ui-list-detail-layout__mobile-nav {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
type ListDetailSize = 'sm' | 'md' | 'lg';
|
||||
type ListDetailVariant = 'default' | 'bordered' | 'elevated';
|
||||
type MobileView = 'list' | 'detail';
|
||||
|
||||
export interface ListDetailNavigationEvent {
|
||||
view: MobileView;
|
||||
hasSelection: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ui-list-detail-layout',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<div
|
||||
class="ui-list-detail-layout"
|
||||
[class.ui-list-detail-layout--sm]="size === 'sm'"
|
||||
[class.ui-list-detail-layout--lg]="size === 'lg'"
|
||||
[class.ui-list-detail-layout--bordered]="variant === 'bordered'"
|
||||
[class.ui-list-detail-layout--elevated]="variant === 'elevated'"
|
||||
[class.ui-list-detail-layout--loading]="loading"
|
||||
[attr.role]="role"
|
||||
[attr.aria-label]="ariaLabel">
|
||||
|
||||
<!-- Mobile Navigation Header -->
|
||||
<div class="ui-list-detail-layout__mobile-nav" *ngIf="showMobileNavigation">
|
||||
<button
|
||||
type="button"
|
||||
class="ui-list-detail-layout__mobile-nav-button"
|
||||
[disabled]="!hasSelection()"
|
||||
(click)="handleMobileNavigation('list')"
|
||||
[attr.aria-label]="backToListLabel">
|
||||
← {{ backToListText }}
|
||||
</button>
|
||||
|
||||
<span class="ui-list-detail-layout__mobile-nav-title">
|
||||
{{ currentView() === 'list' ? listTitle : detailTitle }}
|
||||
</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="ui-list-detail-layout__mobile-nav-button"
|
||||
[disabled]="!hasSelection()"
|
||||
(click)="handleMobileNavigation('detail')"
|
||||
[attr.aria-label]="viewDetailLabel">
|
||||
{{ viewDetailText }} →
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- List Panel -->
|
||||
<div
|
||||
class="ui-list-detail-layout__list"
|
||||
[class.ui-list-detail-layout__list--hidden-mobile]="currentView() === 'detail'"
|
||||
[attr.aria-label]="listAriaLabel"
|
||||
role="region">
|
||||
|
||||
<div
|
||||
class="ui-list-detail-layout__resize-handle"
|
||||
[attr.tabindex]="resizable ? 0 : -1"
|
||||
[attr.aria-label]="resizeHandleLabel"
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
(keydown)="handleResizeKeydown($event)">
|
||||
</div>
|
||||
|
||||
<ng-content select="[slot='list']"></ng-content>
|
||||
</div>
|
||||
|
||||
<!-- Detail Panel -->
|
||||
<div
|
||||
class="ui-list-detail-layout__detail"
|
||||
[class.ui-list-detail-layout__detail--hidden-mobile]="currentView() === 'list'"
|
||||
[class.ui-list-detail-layout__detail--empty]="!hasSelection() && showEmptyState"
|
||||
[attr.aria-label]="detailAriaLabel"
|
||||
role="region">
|
||||
|
||||
@if (!hasSelection() && showEmptyState) {
|
||||
<div class="ui-list-detail-layout__empty-state">
|
||||
<ng-content select="[slot='empty']">
|
||||
{{ emptyStateText }}
|
||||
</ng-content>
|
||||
</div>
|
||||
} @else {
|
||||
<ng-content select="[slot='detail']"></ng-content>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './list-detail-layout.component.scss'
|
||||
})
|
||||
export class ListDetailLayoutComponent {
|
||||
// Appearance
|
||||
@Input() size: ListDetailSize = 'md';
|
||||
@Input() variant: ListDetailVariant = 'default';
|
||||
@Input() loading = false;
|
||||
|
||||
// Functionality
|
||||
@Input() resizable = true;
|
||||
@Input() showEmptyState = true;
|
||||
@Input() showMobileNavigation = true;
|
||||
|
||||
// Selection state
|
||||
@Input()
|
||||
set selectedItem(value: any) {
|
||||
this._selectedItem.set(value);
|
||||
}
|
||||
get selectedItem() {
|
||||
return this._selectedItem();
|
||||
}
|
||||
private _selectedItem = signal<any>(null);
|
||||
|
||||
// Mobile view state
|
||||
@Input()
|
||||
set mobileView(value: MobileView) {
|
||||
this._currentView.set(value);
|
||||
}
|
||||
get mobileView() {
|
||||
return this._currentView();
|
||||
}
|
||||
private _currentView = signal<MobileView>('list');
|
||||
|
||||
// Text customization
|
||||
@Input() listTitle = 'List';
|
||||
@Input() detailTitle = 'Details';
|
||||
@Input() backToListText = 'Back to List';
|
||||
@Input() viewDetailText = 'View Details';
|
||||
@Input() emptyStateText = 'Select an item to view details';
|
||||
|
||||
// Accessibility
|
||||
@Input() role = 'application';
|
||||
@Input() ariaLabel = 'List and detail view';
|
||||
@Input() listAriaLabel = 'Item list';
|
||||
@Input() detailAriaLabel = 'Item details';
|
||||
@Input() backToListLabel = 'Return to item list';
|
||||
@Input() viewDetailLabel = 'View item details';
|
||||
@Input() resizeHandleLabel = 'Resize panels';
|
||||
|
||||
// Events
|
||||
@Output() selectionChanged = new EventEmitter<any>();
|
||||
@Output() mobileNavigated = new EventEmitter<ListDetailNavigationEvent>();
|
||||
@Output() panelResized = new EventEmitter<{ listWidth: number }>();
|
||||
|
||||
// Computed properties
|
||||
currentView = this._currentView.asReadonly();
|
||||
|
||||
hasSelection(): boolean {
|
||||
return this._selectedItem() != null;
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
handleMobileNavigation(view: MobileView): void {
|
||||
if (view === 'detail' && !this.hasSelection()) {
|
||||
return; // Can't navigate to detail without selection
|
||||
}
|
||||
|
||||
this._currentView.set(view);
|
||||
this.mobileNavigated.emit({
|
||||
view,
|
||||
hasSelection: this.hasSelection()
|
||||
});
|
||||
}
|
||||
|
||||
handleResizeKeydown(event: KeyboardEvent): void {
|
||||
if (!this.resizable) return;
|
||||
|
||||
// Keyboard control for resize handle
|
||||
const step = 20; // pixels
|
||||
let deltaX = 0;
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowLeft':
|
||||
deltaX = -step;
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
deltaX = step;
|
||||
break;
|
||||
case 'Home':
|
||||
// Reset to default size
|
||||
this.panelResized.emit({ listWidth: 320 });
|
||||
event.preventDefault();
|
||||
return;
|
||||
case 'End':
|
||||
// Maximize list panel
|
||||
this.panelResized.emit({ listWidth: 600 });
|
||||
event.preventDefault();
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (deltaX !== 0) {
|
||||
event.preventDefault();
|
||||
// In a real implementation, you'd calculate the new width
|
||||
// For demo purposes, we'll just emit the event
|
||||
this.panelResized.emit({ listWidth: 320 + deltaX });
|
||||
}
|
||||
}
|
||||
|
||||
// Public methods for external control
|
||||
selectItem(item: any): void {
|
||||
this._selectedItem.set(item);
|
||||
this.selectionChanged.emit(item);
|
||||
|
||||
// Auto-navigate to detail on mobile when item is selected
|
||||
if (window.innerWidth < 768) { // md breakpoint
|
||||
this._currentView.set('detail');
|
||||
this.mobileNavigated.emit({
|
||||
view: 'detail',
|
||||
hasSelection: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
clearSelection(): void {
|
||||
this._selectedItem.set(null);
|
||||
this.selectionChanged.emit(null);
|
||||
|
||||
// Auto-navigate to list on mobile when selection is cleared
|
||||
if (window.innerWidth < 768) { // md breakpoint
|
||||
this._currentView.set('list');
|
||||
this.mobileNavigated.emit({
|
||||
view: 'list',
|
||||
hasSelection: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
showList(): void {
|
||||
this._currentView.set('list');
|
||||
}
|
||||
|
||||
showDetail(): void {
|
||||
if (this.hasSelection()) {
|
||||
this._currentView.set('detail');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './scroll-container.component';
|
||||
@@ -0,0 +1,245 @@
|
||||
@use '../../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-scroll-container {
|
||||
// Core Structure
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
|
||||
// Layout & Spacing
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
// Visual Design
|
||||
background: $semantic-color-surface-primary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
|
||||
// Base overflow behavior
|
||||
overflow: auto;
|
||||
|
||||
// Smooth scrolling when enabled
|
||||
&--smooth {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
// Direction Variants
|
||||
&--vertical {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&--horizontal {
|
||||
overflow-y: hidden;
|
||||
overflow-x: auto;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
&--both {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
// Scrollbar Visibility Variants
|
||||
&--scrollbar-auto {
|
||||
// Default behavior - scrollbars appear when needed
|
||||
}
|
||||
|
||||
&--scrollbar-always {
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
&--scrollbar-never {
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none; /* WebKit */
|
||||
}
|
||||
}
|
||||
|
||||
// Custom Scrollbar Styling
|
||||
&:not(&--scrollbar-never) {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: $semantic-color-border-secondary $semantic-color-surface-secondary;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
transition: background-color $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-border-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-corner {
|
||||
background: $semantic-color-surface-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
// Content Container
|
||||
&__content {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
padding: $semantic-spacing-component-md;
|
||||
|
||||
// Typography
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
// Virtual Scrolling Components
|
||||
&__virtual-spacer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__virtual-content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
&__virtual-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
// Typography
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll Indicators
|
||||
&__indicators {
|
||||
position: absolute;
|
||||
top: $semantic-spacing-component-sm;
|
||||
right: $semantic-spacing-component-sm;
|
||||
z-index: $semantic-z-index-tooltip;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&__indicator {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: $semantic-border-radius-full;
|
||||
background: $semantic-color-surface-elevated;
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
margin-bottom: $semantic-spacing-component-xs;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: $semantic-opacity-subtle;
|
||||
transition: opacity $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&--up::before {
|
||||
content: '↑';
|
||||
color: $semantic-color-text-primary;
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: $semantic-typography-font-weight-bold;
|
||||
}
|
||||
|
||||
&--down::before {
|
||||
content: '↓';
|
||||
color: $semantic-color-text-primary;
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: $semantic-typography-font-weight-bold;
|
||||
}
|
||||
|
||||
.ui-scroll-container:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Focus States
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
&__content {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
&__virtual-item {
|
||||
padding: $semantic-spacing-component-xs;
|
||||
|
||||
// Smaller typography on mobile
|
||||
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);
|
||||
}
|
||||
|
||||
&__indicators {
|
||||
top: $semantic-spacing-component-xs;
|
||||
right: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
&__indicator {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
&--up::before,
|
||||
&--down::before {
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
&__content {
|
||||
padding: $semantic-spacing-component-xs;
|
||||
}
|
||||
}
|
||||
|
||||
// Reduced Motion Support
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
&--smooth {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb,
|
||||
&__indicator {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
// High Contrast Mode Support
|
||||
@media (prefers-contrast: high) {
|
||||
border-width: $semantic-border-width-2;
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: $semantic-color-border-primary;
|
||||
}
|
||||
|
||||
&__indicator {
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, ElementRef, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
type ScrollbarVisibility = 'auto' | 'always' | 'never';
|
||||
type ScrollDirection = 'vertical' | 'horizontal' | 'both';
|
||||
type ScrollBehavior = 'smooth' | 'auto';
|
||||
|
||||
export interface ScrollVirtualConfig {
|
||||
itemHeight: number;
|
||||
bufferSize?: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface ScrollPosition {
|
||||
top: number;
|
||||
left: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ui-scroll-container',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<div
|
||||
#scrollContainer
|
||||
class="ui-scroll-container"
|
||||
[class.ui-scroll-container--vertical]="direction === 'vertical'"
|
||||
[class.ui-scroll-container--horizontal]="direction === 'horizontal'"
|
||||
[class.ui-scroll-container--both]="direction === 'both'"
|
||||
[class.ui-scroll-container--scrollbar-auto]="scrollbarVisibility === 'auto'"
|
||||
[class.ui-scroll-container--scrollbar-always]="scrollbarVisibility === 'always'"
|
||||
[class.ui-scroll-container--scrollbar-never]="scrollbarVisibility === 'never'"
|
||||
[class.ui-scroll-container--virtual]="virtualScrollConfig?.enabled"
|
||||
[class.ui-scroll-container--smooth]="scrollBehavior === 'smooth'"
|
||||
[attr.role]="role"
|
||||
[attr.aria-label]="ariaLabel"
|
||||
[tabindex]="tabIndex"
|
||||
(scroll)="onScroll($event)"
|
||||
(keydown)="onKeyDown($event)">
|
||||
|
||||
@if (virtualScrollConfig?.enabled && items && items.length > 0) {
|
||||
<!-- Virtual Scrolling Content -->
|
||||
<div class="ui-scroll-container__virtual-spacer" [style.height.px]="totalHeight">
|
||||
<div class="ui-scroll-container__virtual-content" [style.transform]="'translateY(' + offsetY + 'px)'">
|
||||
@for (item of visibleItems; track trackByFn ? trackByFn(item) : item) {
|
||||
<div
|
||||
class="ui-scroll-container__virtual-item"
|
||||
[style.height.px]="virtualScrollConfig?.itemHeight">
|
||||
<ng-content [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ item: item, index: getItemIndex(item) }"></ng-content>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- Regular Content -->
|
||||
<div class="ui-scroll-container__content">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (showScrollIndicators) {
|
||||
<div class="ui-scroll-container__indicators">
|
||||
@if (canScrollUp) {
|
||||
<div class="ui-scroll-container__indicator ui-scroll-container__indicator--up" aria-hidden="true"></div>
|
||||
}
|
||||
@if (canScrollDown) {
|
||||
<div class="ui-scroll-container__indicator ui-scroll-container__indicator--down" aria-hidden="true"></div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './scroll-container.component.scss'
|
||||
})
|
||||
export class ScrollContainerComponent implements AfterViewInit, OnDestroy {
|
||||
@ViewChild('scrollContainer', { static: true }) scrollContainer!: ElementRef<HTMLDivElement>;
|
||||
|
||||
// Basic Configuration
|
||||
@Input() direction: ScrollDirection = 'vertical';
|
||||
@Input() scrollbarVisibility: ScrollbarVisibility = 'auto';
|
||||
@Input() scrollBehavior: ScrollBehavior = 'auto';
|
||||
@Input() showScrollIndicators = false;
|
||||
@Input() role = 'region';
|
||||
@Input() ariaLabel = 'Scrollable content';
|
||||
@Input() tabIndex = 0;
|
||||
|
||||
// Virtual Scrolling
|
||||
@Input() virtualScrollConfig?: ScrollVirtualConfig;
|
||||
@Input() items?: any[];
|
||||
@Input() itemTemplate?: any;
|
||||
@Input() trackByFn?: (item: any) => any;
|
||||
|
||||
// Scroll Position Restoration
|
||||
@Input() restoreScrollPosition = false;
|
||||
@Input() scrollPositionKey?: string;
|
||||
|
||||
// Events
|
||||
@Output() scrolled = new EventEmitter<ScrollPosition>();
|
||||
@Output() scrollStart = new EventEmitter<void>();
|
||||
@Output() scrollEnd = new EventEmitter<void>();
|
||||
@Output() reachedTop = new EventEmitter<void>();
|
||||
@Output() reachedBottom = new EventEmitter<void>();
|
||||
|
||||
// Virtual Scrolling State
|
||||
visibleItems: any[] = [];
|
||||
totalHeight = 0;
|
||||
offsetY = 0;
|
||||
startIndex = 0;
|
||||
endIndex = 0;
|
||||
|
||||
// Scroll State
|
||||
canScrollUp = false;
|
||||
canScrollDown = false;
|
||||
private scrollTimeout?: number;
|
||||
private isScrolling = false;
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
if (this.virtualScrollConfig?.enabled && this.items) {
|
||||
this.setupVirtualScrolling();
|
||||
}
|
||||
|
||||
if (this.restoreScrollPosition) {
|
||||
this.restorePosition();
|
||||
}
|
||||
|
||||
this.updateScrollIndicators();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.scrollTimeout) {
|
||||
clearTimeout(this.scrollTimeout);
|
||||
}
|
||||
|
||||
if (this.restoreScrollPosition) {
|
||||
this.savePosition();
|
||||
}
|
||||
}
|
||||
|
||||
onScroll(event: Event): void {
|
||||
const element = event.target as HTMLElement;
|
||||
const scrollPosition: ScrollPosition = {
|
||||
top: element.scrollTop,
|
||||
left: element.scrollLeft
|
||||
};
|
||||
|
||||
if (!this.isScrolling) {
|
||||
this.isScrolling = true;
|
||||
this.scrollStart.emit();
|
||||
}
|
||||
|
||||
this.scrolled.emit(scrollPosition);
|
||||
|
||||
// Check if reached boundaries
|
||||
if (element.scrollTop === 0) {
|
||||
this.reachedTop.emit();
|
||||
}
|
||||
|
||||
if (element.scrollTop + element.clientHeight >= element.scrollHeight - 1) {
|
||||
this.reachedBottom.emit();
|
||||
}
|
||||
|
||||
// Update virtual scrolling
|
||||
if (this.virtualScrollConfig?.enabled) {
|
||||
this.updateVirtualScrolling(element.scrollTop);
|
||||
}
|
||||
|
||||
this.updateScrollIndicators();
|
||||
|
||||
// Debounce scroll end event
|
||||
if (this.scrollTimeout) {
|
||||
clearTimeout(this.scrollTimeout);
|
||||
}
|
||||
|
||||
this.scrollTimeout = window.setTimeout(() => {
|
||||
this.isScrolling = false;
|
||||
this.scrollEnd.emit();
|
||||
}, 150);
|
||||
}
|
||||
|
||||
onKeyDown(event: KeyboardEvent): void {
|
||||
const element = this.scrollContainer.nativeElement;
|
||||
let handled = false;
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowUp':
|
||||
this.scrollBy(0, -40);
|
||||
handled = true;
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
this.scrollBy(0, 40);
|
||||
handled = true;
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
if (this.direction === 'horizontal' || this.direction === 'both') {
|
||||
this.scrollBy(-40, 0);
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
if (this.direction === 'horizontal' || this.direction === 'both') {
|
||||
this.scrollBy(40, 0);
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
case 'PageUp':
|
||||
this.scrollBy(0, -element.clientHeight * 0.8);
|
||||
handled = true;
|
||||
break;
|
||||
case 'PageDown':
|
||||
this.scrollBy(0, element.clientHeight * 0.8);
|
||||
handled = true;
|
||||
break;
|
||||
case 'Home':
|
||||
this.scrollTo({ top: 0, left: 0 });
|
||||
handled = true;
|
||||
break;
|
||||
case 'End':
|
||||
this.scrollTo({ top: element.scrollHeight, left: 0 });
|
||||
handled = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
// Public API Methods
|
||||
scrollTo(position: Partial<ScrollPosition>): void {
|
||||
const element = this.scrollContainer.nativeElement;
|
||||
const options: ScrollToOptions = {
|
||||
behavior: this.scrollBehavior
|
||||
};
|
||||
|
||||
if (position.top !== undefined) {
|
||||
options.top = position.top;
|
||||
}
|
||||
if (position.left !== undefined) {
|
||||
options.left = position.left;
|
||||
}
|
||||
|
||||
element.scrollTo(options);
|
||||
}
|
||||
|
||||
scrollBy(deltaX: number, deltaY: number): void {
|
||||
const element = this.scrollContainer.nativeElement;
|
||||
element.scrollBy({
|
||||
left: deltaX,
|
||||
top: deltaY,
|
||||
behavior: this.scrollBehavior
|
||||
});
|
||||
}
|
||||
|
||||
scrollToTop(): void {
|
||||
this.scrollTo({ top: 0 });
|
||||
}
|
||||
|
||||
scrollToBottom(): void {
|
||||
const element = this.scrollContainer.nativeElement;
|
||||
this.scrollTo({ top: element.scrollHeight });
|
||||
}
|
||||
|
||||
getScrollPosition(): ScrollPosition {
|
||||
const element = this.scrollContainer.nativeElement;
|
||||
return {
|
||||
top: element.scrollTop,
|
||||
left: element.scrollLeft
|
||||
};
|
||||
}
|
||||
|
||||
// Virtual Scrolling Implementation
|
||||
private setupVirtualScrolling(): void {
|
||||
if (!this.virtualScrollConfig || !this.items) return;
|
||||
|
||||
const bufferSize = this.virtualScrollConfig.bufferSize || 10;
|
||||
const itemHeight = this.virtualScrollConfig.itemHeight;
|
||||
|
||||
this.totalHeight = this.items.length * itemHeight;
|
||||
|
||||
const containerHeight = this.scrollContainer.nativeElement.clientHeight;
|
||||
const visibleCount = Math.ceil(containerHeight / itemHeight) + bufferSize * 2;
|
||||
|
||||
this.endIndex = Math.min(visibleCount, this.items.length);
|
||||
this.visibleItems = this.items.slice(this.startIndex, this.endIndex);
|
||||
}
|
||||
|
||||
private updateVirtualScrolling(scrollTop: number): void {
|
||||
if (!this.virtualScrollConfig || !this.items) return;
|
||||
|
||||
const itemHeight = this.virtualScrollConfig.itemHeight;
|
||||
const bufferSize = this.virtualScrollConfig.bufferSize || 10;
|
||||
const containerHeight = this.scrollContainer.nativeElement.clientHeight;
|
||||
|
||||
this.startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - bufferSize);
|
||||
const visibleCount = Math.ceil(containerHeight / itemHeight) + bufferSize * 2;
|
||||
this.endIndex = Math.min(this.startIndex + visibleCount, this.items.length);
|
||||
|
||||
this.visibleItems = this.items.slice(this.startIndex, this.endIndex);
|
||||
this.offsetY = this.startIndex * itemHeight;
|
||||
}
|
||||
|
||||
getItemIndex(item: any): number {
|
||||
if (!this.items) return -1;
|
||||
return this.items.indexOf(item);
|
||||
}
|
||||
|
||||
// Scroll Position Restoration
|
||||
private savePosition(): void {
|
||||
if (!this.scrollPositionKey) return;
|
||||
|
||||
const position = this.getScrollPosition();
|
||||
const key = `scroll-position-${this.scrollPositionKey}`;
|
||||
sessionStorage.setItem(key, JSON.stringify(position));
|
||||
}
|
||||
|
||||
private restorePosition(): void {
|
||||
if (!this.scrollPositionKey) return;
|
||||
|
||||
const key = `scroll-position-${this.scrollPositionKey}`;
|
||||
const saved = sessionStorage.getItem(key);
|
||||
|
||||
if (saved) {
|
||||
try {
|
||||
const position = JSON.parse(saved) as ScrollPosition;
|
||||
setTimeout(() => this.scrollTo(position), 0);
|
||||
} catch (e) {
|
||||
console.warn('Failed to restore scroll position:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll Indicators
|
||||
private updateScrollIndicators(): void {
|
||||
if (!this.showScrollIndicators) return;
|
||||
|
||||
const element = this.scrollContainer.nativeElement;
|
||||
this.canScrollUp = element.scrollTop > 0;
|
||||
this.canScrollDown = element.scrollTop + element.clientHeight < element.scrollHeight - 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './section.component';
|
||||
@@ -0,0 +1,114 @@
|
||||
@use '../../../../../../ui-design-system/src/styles/semantic' as *;
|
||||
|
||||
.ui-section {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
// Base spacing - using semantic layout section tokens
|
||||
&--spacing-xs {
|
||||
padding-top: $semantic-spacing-layout-section-xs;
|
||||
padding-bottom: $semantic-spacing-layout-section-xs;
|
||||
}
|
||||
|
||||
&--spacing-sm {
|
||||
padding-top: $semantic-spacing-layout-section-sm;
|
||||
padding-bottom: $semantic-spacing-layout-section-sm;
|
||||
}
|
||||
|
||||
&--spacing-md {
|
||||
padding-top: $semantic-spacing-layout-section-md;
|
||||
padding-bottom: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
|
||||
&--spacing-lg {
|
||||
padding-top: $semantic-spacing-layout-section-lg;
|
||||
padding-bottom: $semantic-spacing-layout-section-lg;
|
||||
}
|
||||
|
||||
&--spacing-xl {
|
||||
padding-top: $semantic-spacing-layout-section-xl;
|
||||
padding-bottom: $semantic-spacing-layout-section-xl;
|
||||
}
|
||||
|
||||
// Background variants
|
||||
&--surface {
|
||||
background: $semantic-color-surface-primary;
|
||||
}
|
||||
|
||||
&--surface-secondary {
|
||||
background: $semantic-color-surface-secondary;
|
||||
}
|
||||
|
||||
&--surface-elevated {
|
||||
background: $semantic-color-surface-elevated;
|
||||
}
|
||||
|
||||
// Content alignment
|
||||
&--align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&--align-end {
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
// Width variants
|
||||
&--full-width {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
&--contained {
|
||||
max-width: 1200px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: $semantic-spacing-component-md;
|
||||
padding-right: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
// Typography styling for section content
|
||||
color: $semantic-color-text-primary;
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
|
||||
// Responsive behavior
|
||||
@media (max-width: 768px) {
|
||||
&--contained {
|
||||
padding-left: $semantic-spacing-component-sm;
|
||||
padding-right: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
// Reduce section spacing on mobile
|
||||
&--spacing-xl {
|
||||
padding-top: $semantic-spacing-layout-section-lg;
|
||||
padding-bottom: $semantic-spacing-layout-section-lg;
|
||||
}
|
||||
|
||||
&--spacing-lg {
|
||||
padding-top: $semantic-spacing-layout-section-md;
|
||||
padding-bottom: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
&--contained {
|
||||
padding-left: $semantic-spacing-component-xs;
|
||||
padding-right: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
// Further reduce section spacing on very small screens
|
||||
&--spacing-xl,
|
||||
&--spacing-lg {
|
||||
padding-top: $semantic-spacing-layout-section-sm;
|
||||
padding-bottom: $semantic-spacing-layout-section-sm;
|
||||
}
|
||||
|
||||
&--spacing-md {
|
||||
padding-top: $semantic-spacing-layout-section-xs;
|
||||
padding-bottom: $semantic-spacing-layout-section-xs;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
type SectionSpacing = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
type SectionBackground = 'transparent' | 'surface' | 'surface-secondary' | 'surface-elevated';
|
||||
type SectionAlign = 'start' | 'center' | 'end';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-section',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<section
|
||||
[ngClass]="getClasses()"
|
||||
[attr.aria-label]="ariaLabel"
|
||||
[attr.role]="role">
|
||||
|
||||
<ng-content></ng-content>
|
||||
</section>
|
||||
`,
|
||||
styleUrl: './section.component.scss'
|
||||
})
|
||||
export class SectionComponent {
|
||||
@Input() spacing: SectionSpacing = 'md';
|
||||
@Input() background: SectionBackground = 'transparent';
|
||||
@Input() align: SectionAlign = 'start';
|
||||
@Input() fullWidth = false;
|
||||
@Input() contained = true;
|
||||
@Input() ariaLabel?: string;
|
||||
@Input() role?: string;
|
||||
|
||||
getClasses(): Record<string, boolean> {
|
||||
const classes: Record<string, boolean> = {
|
||||
'ui-section': true,
|
||||
[`ui-section--spacing-${this.spacing}`]: true,
|
||||
[`ui-section--${this.background}`]: this.background !== 'transparent',
|
||||
[`ui-section--align-${this.align}`]: this.align !== 'start',
|
||||
'ui-section--full-width': this.fullWidth,
|
||||
'ui-section--contained': this.contained
|
||||
};
|
||||
|
||||
return classes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './sidebar-layout.component';
|
||||
@@ -0,0 +1,217 @@
|
||||
@use '../../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-sidebar-layout {
|
||||
// Core Structure
|
||||
display: flex;
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
|
||||
// Layout & Spacing
|
||||
gap: 0;
|
||||
|
||||
// Visual Design
|
||||
background: $semantic-color-surface;
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
// Transitions
|
||||
transition: all $semantic-motion-duration-normal $semantic-motion-easing-ease;
|
||||
|
||||
// Right position variant
|
||||
&--position-right {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.ui-sidebar-layout__sidebar {
|
||||
border-left: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Collapsed state
|
||||
&--collapsed {
|
||||
.ui-sidebar-layout__content {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&.ui-sidebar-layout--position-right .ui-sidebar-layout__content {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Hidden variant
|
||||
&--hidden {
|
||||
.ui-sidebar-layout__sidebar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Overlay mode
|
||||
&--overlay {
|
||||
.ui-sidebar-layout__sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
z-index: $semantic-z-index-modal;
|
||||
|
||||
&--collapsed {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
||||
|
||||
&.ui-sidebar-layout--position-right .ui-sidebar-layout__sidebar {
|
||||
left: auto;
|
||||
right: 0;
|
||||
|
||||
&--collapsed {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-sidebar-layout__content {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Visual variants
|
||||
&--bordered {
|
||||
.ui-sidebar-layout__sidebar {
|
||||
border-right: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&--elevated {
|
||||
.ui-sidebar-layout__sidebar {
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
background: $semantic-color-surface-elevated;
|
||||
}
|
||||
}
|
||||
|
||||
// Sidebar Element
|
||||
&__sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
|
||||
// Layout & Spacing
|
||||
padding: $semantic-spacing-component-md;
|
||||
|
||||
// Visual Design
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-right: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
|
||||
// Typography
|
||||
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);
|
||||
|
||||
// Transitions
|
||||
transition: all $semantic-motion-duration-normal $semantic-motion-easing-ease;
|
||||
|
||||
// Width variants
|
||||
&--sm {
|
||||
width: 200px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
&--md {
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
&--lg {
|
||||
width: 360px;
|
||||
min-width: 360px;
|
||||
}
|
||||
|
||||
&--xl {
|
||||
width: 440px;
|
||||
min-width: 440px;
|
||||
}
|
||||
|
||||
// Collapsed state
|
||||
&--collapsed {
|
||||
width: 60px;
|
||||
min-width: 60px;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
// Backdrop Element
|
||||
&__backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: $semantic-z-index-overlay;
|
||||
|
||||
// Visual Design
|
||||
background: $semantic-color-backdrop;
|
||||
opacity: $semantic-opacity-backdrop;
|
||||
|
||||
// Transitions
|
||||
transition: opacity $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
// Interactive
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// Content Element
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
|
||||
// Layout & Spacing
|
||||
padding: $semantic-spacing-component-md;
|
||||
|
||||
// Visual Design
|
||||
background: $semantic-color-surface;
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
// Typography
|
||||
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);
|
||||
|
||||
// Transitions
|
||||
transition: margin $semantic-motion-duration-normal $semantic-motion-easing-ease;
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: 768px) {
|
||||
&:not(.ui-sidebar-layout--overlay) {
|
||||
.ui-sidebar-layout__sidebar {
|
||||
&--sm {
|
||||
width: 60px;
|
||||
min-width: 60px;
|
||||
padding: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
&--md, &--lg, &--xl {
|
||||
width: 60px;
|
||||
min-width: 60px;
|
||||
padding: $semantic-spacing-component-xs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ui-sidebar-layout__content {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.ui-sidebar-layout__content {
|
||||
padding: $semantic-spacing-component-xs;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
type SidebarPosition = 'left' | 'right';
|
||||
type SidebarWidth = 'sm' | 'md' | 'lg' | 'xl';
|
||||
type SidebarVariant = 'default' | 'bordered' | 'elevated';
|
||||
type CollapseMode = 'hidden' | 'collapsed' | 'overlay';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-sidebar-layout',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<div
|
||||
class="ui-sidebar-layout"
|
||||
[class.ui-sidebar-layout--position-right]="position === 'right'"
|
||||
[class.ui-sidebar-layout--collapsed]="collapsed"
|
||||
[class.ui-sidebar-layout--overlay]="collapseMode === 'overlay'"
|
||||
[class.ui-sidebar-layout--hidden]="collapseMode === 'hidden' && collapsed"
|
||||
[class.ui-sidebar-layout--bordered]="variant === 'bordered'"
|
||||
[class.ui-sidebar-layout--elevated]="variant === 'elevated'"
|
||||
[attr.role]="role">
|
||||
|
||||
<aside
|
||||
class="ui-sidebar-layout__sidebar"
|
||||
[class.ui-sidebar-layout__sidebar--sm]="sidebarWidth === 'sm'"
|
||||
[class.ui-sidebar-layout__sidebar--md]="sidebarWidth === 'md'"
|
||||
[class.ui-sidebar-layout__sidebar--lg]="sidebarWidth === 'lg'"
|
||||
[class.ui-sidebar-layout__sidebar--xl]="sidebarWidth === 'xl'"
|
||||
[class.ui-sidebar-layout__sidebar--collapsed]="collapsed"
|
||||
[attr.aria-hidden]="collapsed && collapseMode === 'hidden'"
|
||||
[attr.aria-expanded]="!collapsed"
|
||||
role="complementary">
|
||||
|
||||
<ng-content select="[slot='sidebar']"></ng-content>
|
||||
</aside>
|
||||
|
||||
@if (collapseMode === 'overlay' && !collapsed) {
|
||||
<div
|
||||
class="ui-sidebar-layout__backdrop"
|
||||
(click)="handleBackdropClick()"
|
||||
role="presentation"
|
||||
aria-hidden="true">
|
||||
</div>
|
||||
}
|
||||
|
||||
<main
|
||||
class="ui-sidebar-layout__content"
|
||||
[attr.aria-label]="contentLabel"
|
||||
role="main">
|
||||
|
||||
<ng-content></ng-content>
|
||||
</main>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './sidebar-layout.component.scss'
|
||||
})
|
||||
export class SidebarLayoutComponent {
|
||||
@Input() position: SidebarPosition = 'left';
|
||||
@Input() sidebarWidth: SidebarWidth = 'md';
|
||||
@Input() variant: SidebarVariant = 'default';
|
||||
@Input() collapsed = false;
|
||||
@Input() collapseMode: CollapseMode = 'collapsed';
|
||||
@Input() role = 'application';
|
||||
@Input() contentLabel = 'Main content';
|
||||
@Input() enableBackdropClose = true;
|
||||
|
||||
handleBackdropClick(): void {
|
||||
if (this.enableBackdropClose && this.collapseMode === 'overlay') {
|
||||
// In a real implementation, this would emit an event
|
||||
// For demo purposes, we'll just log
|
||||
console.log('Backdrop clicked - sidebar should close');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './stack.component';
|
||||
@@ -0,0 +1,176 @@
|
||||
@use '../../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-stack {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
// Base structure
|
||||
&--column {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&--row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
// Inline variant
|
||||
&--inline {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
// Spacing variants - using semantic stack spacing tokens
|
||||
&--spacing-xs {
|
||||
gap: $semantic-spacing-stack-xs;
|
||||
}
|
||||
|
||||
&--spacing-sm {
|
||||
gap: $semantic-spacing-stack-sm;
|
||||
}
|
||||
|
||||
&--spacing-md {
|
||||
gap: $semantic-spacing-stack-md;
|
||||
}
|
||||
|
||||
&--spacing-lg {
|
||||
gap: $semantic-spacing-stack-lg;
|
||||
}
|
||||
|
||||
&--spacing-xl {
|
||||
gap: $semantic-spacing-stack-xl;
|
||||
}
|
||||
|
||||
&--spacing-2xl {
|
||||
gap: $semantic-spacing-2xl;
|
||||
}
|
||||
|
||||
&--spacing-3xl {
|
||||
gap: $semantic-spacing-3xl;
|
||||
}
|
||||
|
||||
&--spacing-4xl {
|
||||
gap: $semantic-spacing-4xl;
|
||||
}
|
||||
|
||||
&--spacing-5xl {
|
||||
gap: $semantic-spacing-5xl;
|
||||
}
|
||||
|
||||
// Alignment variants
|
||||
&--align-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&--align-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&--align-end {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
&--align-stretch {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
&--align-baseline {
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
// Justify variants
|
||||
&--justify-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&--justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&--justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&--justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&--justify-around {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
&--justify-evenly {
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
// Wrap variants
|
||||
&--wrap-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&--wrap-nowrap {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
&--wrap-wrap-reverse {
|
||||
flex-wrap: wrap-reverse;
|
||||
}
|
||||
|
||||
// Divider variant - adds borders between children
|
||||
&--divider {
|
||||
&.ui-stack--column > :not(:last-child) {
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
padding-bottom: $semantic-spacing-component-sm;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
&.ui-stack--row > :not(:last-child) {
|
||||
border-right: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
padding-right: $semantic-spacing-component-sm;
|
||||
margin-right: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
// Remove gap when using dividers to avoid double spacing
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
// Responsive behavior
|
||||
&--responsive {
|
||||
// On small screens, convert horizontal stacks to vertical for better usability
|
||||
@media (max-width: 640px) {
|
||||
&.ui-stack--row {
|
||||
flex-direction: column;
|
||||
|
||||
// Adjust dividers for responsive layout
|
||||
&.ui-stack--divider {
|
||||
> :not(:last-child) {
|
||||
border-right: none;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
padding-right: 0;
|
||||
margin-right: 0;
|
||||
padding-bottom: $semantic-spacing-component-sm;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reduce spacing on small screens
|
||||
&.ui-stack--spacing-xl,
|
||||
&.ui-stack--spacing-2xl,
|
||||
&.ui-stack--spacing-3xl,
|
||||
&.ui-stack--spacing-4xl,
|
||||
&.ui-stack--spacing-5xl {
|
||||
gap: $semantic-spacing-stack-lg;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
&.ui-stack--spacing-lg,
|
||||
&.ui-stack--spacing-xl,
|
||||
&.ui-stack--spacing-2xl,
|
||||
&.ui-stack--spacing-3xl,
|
||||
&.ui-stack--spacing-4xl,
|
||||
&.ui-stack--spacing-5xl {
|
||||
gap: $semantic-spacing-stack-md;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
type StackDirection = 'column' | 'row';
|
||||
type StackSpacing = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl';
|
||||
type StackAlignment = 'start' | 'center' | 'end' | 'stretch' | 'baseline';
|
||||
type StackJustify = 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly';
|
||||
type StackWrap = 'nowrap' | 'wrap' | 'wrap-reverse';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-stack',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<div
|
||||
[ngClass]="getClasses()"
|
||||
[attr.role]="role"
|
||||
[style.gap]="customGap">
|
||||
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './stack.component.scss'
|
||||
})
|
||||
export class StackComponent {
|
||||
@Input() direction: StackDirection = 'column';
|
||||
@Input() spacing: StackSpacing = 'md';
|
||||
@Input() align?: StackAlignment;
|
||||
@Input() justify?: StackJustify;
|
||||
@Input() wrap?: StackWrap;
|
||||
@Input() inline = false;
|
||||
@Input() responsive = true;
|
||||
@Input() divider = false;
|
||||
@Input() customGap?: string;
|
||||
@Input() role?: string;
|
||||
|
||||
getClasses(): Record<string, boolean> {
|
||||
const classes: Record<string, boolean> = {
|
||||
'ui-stack': true,
|
||||
[`ui-stack--${this.direction}`]: true,
|
||||
[`ui-stack--spacing-${this.spacing}`]: true,
|
||||
'ui-stack--inline': this.inline,
|
||||
'ui-stack--responsive': this.responsive,
|
||||
'ui-stack--divider': this.divider
|
||||
};
|
||||
|
||||
if (this.align) {
|
||||
classes[`ui-stack--align-${this.align}`] = true;
|
||||
}
|
||||
|
||||
if (this.justify) {
|
||||
classes[`ui-stack--justify-${this.justify}`] = true;
|
||||
}
|
||||
|
||||
if (this.wrap) {
|
||||
classes[`ui-stack--wrap-${this.wrap}`] = true;
|
||||
}
|
||||
|
||||
return classes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './supporting-pane-layout.component';
|
||||
@@ -0,0 +1,369 @@
|
||||
@use '../../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-supporting-pane-layout {
|
||||
// Core Structure
|
||||
display: flex;
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
|
||||
// Layout & Spacing
|
||||
gap: 0;
|
||||
|
||||
// Visual Design
|
||||
background: $semantic-color-surface;
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
// Transitions
|
||||
transition: all $semantic-motion-duration-normal $semantic-motion-easing-ease;
|
||||
|
||||
// Right position variant
|
||||
&--position-right {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.ui-supporting-pane-layout__pane {
|
||||
border-left: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Collapsed state
|
||||
&--collapsed {
|
||||
.ui-supporting-pane-layout__pane {
|
||||
width: 60px;
|
||||
min-width: 60px;
|
||||
|
||||
.ui-supporting-pane-layout__pane-content {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.ui-supporting-pane-layout__pane-toggle {
|
||||
padding: $semantic-spacing-component-xs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sticky positioning
|
||||
&--sticky-pane {
|
||||
.ui-supporting-pane-layout__pane {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Visual variants
|
||||
&--bordered {
|
||||
.ui-supporting-pane-layout__pane {
|
||||
border-right: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&--elevated {
|
||||
.ui-supporting-pane-layout__pane {
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
background: $semantic-color-surface-elevated;
|
||||
}
|
||||
}
|
||||
|
||||
&--subtle {
|
||||
.ui-supporting-pane-layout__pane {
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-right: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
}
|
||||
}
|
||||
|
||||
// Size variants
|
||||
&--sm {
|
||||
.ui-supporting-pane-layout__pane:not(.ui-supporting-pane-layout__pane--collapsed) {
|
||||
width: 240px;
|
||||
min-width: 240px;
|
||||
}
|
||||
}
|
||||
|
||||
&--md {
|
||||
.ui-supporting-pane-layout__pane:not(.ui-supporting-pane-layout__pane--collapsed) {
|
||||
width: 320px;
|
||||
min-width: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
&--lg {
|
||||
.ui-supporting-pane-layout__pane:not(.ui-supporting-pane-layout__pane--collapsed) {
|
||||
width: 400px;
|
||||
min-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
&--xl {
|
||||
.ui-supporting-pane-layout__pane:not(.ui-supporting-pane-layout__pane--collapsed) {
|
||||
width: 480px;
|
||||
min-width: 480px;
|
||||
}
|
||||
}
|
||||
|
||||
// Main Content Element
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
|
||||
// Layout & Spacing
|
||||
padding: $semantic-spacing-component-md;
|
||||
|
||||
// Visual Design
|
||||
background: $semantic-color-surface;
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
// Typography
|
||||
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);
|
||||
|
||||
// Transitions
|
||||
transition: margin $semantic-motion-duration-normal $semantic-motion-easing-ease;
|
||||
}
|
||||
|
||||
// Supporting Pane Element
|
||||
&__pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
width: 320px;
|
||||
min-width: 320px;
|
||||
|
||||
// Layout & Spacing
|
||||
padding: 0;
|
||||
|
||||
// Visual Design
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-right: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
|
||||
// Typography
|
||||
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);
|
||||
|
||||
// Transitions
|
||||
transition: all $semantic-motion-duration-normal $semantic-motion-easing-ease;
|
||||
|
||||
&--collapsed {
|
||||
width: 60px;
|
||||
min-width: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
// Pane Header Element
|
||||
&__pane-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
|
||||
// Layout & Spacing
|
||||
padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
// Visual Design
|
||||
background: $semantic-color-surface-elevated;
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
// Typography
|
||||
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);
|
||||
}
|
||||
|
||||
// Pane Content Element
|
||||
&__pane-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
// Layout & Spacing
|
||||
padding: $semantic-spacing-component-md;
|
||||
|
||||
// Visual Design
|
||||
background: inherit;
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
// Transitions
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
// When collapsed
|
||||
.ui-supporting-pane-layout--collapsed & {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Pane Toggle Button
|
||||
&__pane-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
// Layout & Spacing
|
||||
width: $semantic-sizing-touch-target;
|
||||
height: $semantic-sizing-touch-target;
|
||||
padding: $semantic-spacing-component-xs;
|
||||
|
||||
// Visual Design
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
color: $semantic-color-text-secondary;
|
||||
|
||||
// Typography
|
||||
font-size: $semantic-sizing-icon-button;
|
||||
line-height: 1;
|
||||
|
||||
// Transitions
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
cursor: pointer;
|
||||
|
||||
// Interactive States
|
||||
&:hover {
|
||||
background: $semantic-color-interactive-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: $semantic-color-interactive-primary;
|
||||
opacity: $semantic-opacity-hover;
|
||||
}
|
||||
|
||||
// Icon styling
|
||||
.fa-icon {
|
||||
font-size: inherit;
|
||||
transition: transform $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
}
|
||||
|
||||
// Collapsed state rotation
|
||||
.ui-supporting-pane-layout--collapsed & .fa-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.ui-supporting-pane-layout--position-right & .fa-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.ui-supporting-pane-layout--position-right.ui-supporting-pane-layout--collapsed & .fa-icon {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
// Pane Footer Element (optional)
|
||||
&__pane-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
|
||||
// Layout & Spacing
|
||||
padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
|
||||
border-top: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
// Visual Design
|
||||
background: $semantic-color-surface-elevated;
|
||||
color: $semantic-color-text-secondary;
|
||||
|
||||
// Typography
|
||||
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);
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: 1024px) {
|
||||
&:not(.ui-supporting-pane-layout--always-visible) {
|
||||
.ui-supporting-pane-layout__pane {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 100vh;
|
||||
z-index: $semantic-z-index-modal;
|
||||
box-shadow: $semantic-shadow-elevation-4;
|
||||
transform: translateX(100%);
|
||||
|
||||
&:not(.ui-supporting-pane-layout__pane--collapsed) {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
&.ui-supporting-pane-layout--position-left .ui-supporting-pane-layout__pane {
|
||||
left: 0;
|
||||
right: auto;
|
||||
transform: translateX(-100%);
|
||||
|
||||
&:not(.ui-supporting-pane-layout__pane--collapsed) {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-supporting-pane-layout__content {
|
||||
width: 100%;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ui-supporting-pane-layout__content {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
.ui-supporting-pane-layout__pane {
|
||||
width: 280px !important;
|
||||
min-width: 280px !important;
|
||||
|
||||
&--collapsed {
|
||||
width: 60px !important;
|
||||
min-width: 60px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-supporting-pane-layout__pane-content {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.ui-supporting-pane-layout__content {
|
||||
padding: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
.ui-supporting-pane-layout__pane {
|
||||
width: 240px !important;
|
||||
min-width: 240px !important;
|
||||
}
|
||||
|
||||
.ui-supporting-pane-layout__pane-content {
|
||||
padding: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
.ui-supporting-pane-layout__pane-header {
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
.ui-supporting-pane-layout__pane-footer {
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import { faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
type PanePosition = 'left' | 'right';
|
||||
type PaneSize = 'sm' | 'md' | 'lg' | 'xl';
|
||||
type PaneVariant = 'default' | 'bordered' | 'elevated' | 'subtle';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-supporting-pane-layout',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FontAwesomeModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<div
|
||||
class="ui-supporting-pane-layout"
|
||||
[class.ui-supporting-pane-layout--position-right]="position === 'right'"
|
||||
[class.ui-supporting-pane-layout--collapsed]="collapsed"
|
||||
[class.ui-supporting-pane-layout--sticky-pane]="stickyPane"
|
||||
[class.ui-supporting-pane-layout--always-visible]="alwaysVisible"
|
||||
[class.ui-supporting-pane-layout--bordered]="variant === 'bordered'"
|
||||
[class.ui-supporting-pane-layout--elevated]="variant === 'elevated'"
|
||||
[class.ui-supporting-pane-layout--subtle]="variant === 'subtle'"
|
||||
[class.ui-supporting-pane-layout--sm]="paneSize === 'sm'"
|
||||
[class.ui-supporting-pane-layout--md]="paneSize === 'md'"
|
||||
[class.ui-supporting-pane-layout--lg]="paneSize === 'lg'"
|
||||
[class.ui-supporting-pane-layout--xl]="paneSize === 'xl'"
|
||||
[attr.role]="role">
|
||||
|
||||
<!-- Main Content -->
|
||||
<main
|
||||
class="ui-supporting-pane-layout__content"
|
||||
[attr.aria-label]="contentLabel"
|
||||
role="main">
|
||||
|
||||
<ng-content></ng-content>
|
||||
</main>
|
||||
|
||||
<!-- Supporting Pane -->
|
||||
<aside
|
||||
class="ui-supporting-pane-layout__pane"
|
||||
[class.ui-supporting-pane-layout__pane--collapsed]="collapsed"
|
||||
[attr.aria-hidden]="collapsed"
|
||||
[attr.aria-expanded]="!collapsed"
|
||||
[attr.aria-label]="paneLabel"
|
||||
role="complementary">
|
||||
|
||||
<!-- Pane Header (optional) -->
|
||||
@if (showPaneHeader) {
|
||||
<header class="ui-supporting-pane-layout__pane-header">
|
||||
@if (!collapsed) {
|
||||
<ng-content select="[slot='pane-header']"></ng-content>
|
||||
}
|
||||
|
||||
@if (collapsible) {
|
||||
<button
|
||||
class="ui-supporting-pane-layout__pane-toggle"
|
||||
type="button"
|
||||
[attr.aria-label]="collapsed ? expandAriaLabel : collapseAriaLabel"
|
||||
[attr.aria-expanded]="!collapsed"
|
||||
(click)="handleTogglePane()"
|
||||
(keydown)="handleToggleKeydown($event)">
|
||||
|
||||
<fa-icon
|
||||
[icon]="position === 'right' ? faChevronRight : faChevronLeft"
|
||||
class="fa-icon"
|
||||
aria-hidden="true">
|
||||
</fa-icon>
|
||||
</button>
|
||||
}
|
||||
</header>
|
||||
}
|
||||
|
||||
<!-- Pane Content -->
|
||||
<div class="ui-supporting-pane-layout__pane-content">
|
||||
<ng-content select="[slot='supporting-pane']"></ng-content>
|
||||
</div>
|
||||
|
||||
<!-- Pane Footer (optional) -->
|
||||
@if (showPaneFooter && !collapsed) {
|
||||
<footer class="ui-supporting-pane-layout__pane-footer">
|
||||
<ng-content select="[slot='pane-footer']"></ng-content>
|
||||
</footer>
|
||||
}
|
||||
</aside>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './supporting-pane-layout.component.scss'
|
||||
})
|
||||
export class SupportingPaneLayoutComponent {
|
||||
@Input() position: PanePosition = 'right';
|
||||
@Input() paneSize: PaneSize = 'md';
|
||||
@Input() variant: PaneVariant = 'default';
|
||||
@Input() collapsed = false;
|
||||
@Input() collapsible = true;
|
||||
@Input() stickyPane = false;
|
||||
@Input() alwaysVisible = false;
|
||||
@Input() showPaneHeader = true;
|
||||
@Input() showPaneFooter = false;
|
||||
@Input() role = 'application';
|
||||
@Input() contentLabel = 'Main content';
|
||||
@Input() paneLabel = 'Supporting information';
|
||||
@Input() collapseAriaLabel = 'Collapse supporting pane';
|
||||
@Input() expandAriaLabel = 'Expand supporting pane';
|
||||
|
||||
@Output() paneToggled = new EventEmitter<boolean>();
|
||||
@Output() paneCollapsed = new EventEmitter<void>();
|
||||
@Output() paneExpanded = new EventEmitter<void>();
|
||||
|
||||
// FontAwesome icons
|
||||
faChevronLeft = faChevronLeft;
|
||||
faChevronRight = faChevronRight;
|
||||
|
||||
handleTogglePane(): void {
|
||||
if (!this.collapsible) return;
|
||||
|
||||
const newCollapsedState = !this.collapsed;
|
||||
this.collapsed = newCollapsedState;
|
||||
|
||||
// Emit events
|
||||
this.paneToggled.emit(newCollapsedState);
|
||||
|
||||
if (newCollapsedState) {
|
||||
this.paneCollapsed.emit();
|
||||
} else {
|
||||
this.paneExpanded.emit();
|
||||
}
|
||||
}
|
||||
|
||||
handleToggleKeydown(event: KeyboardEvent): void {
|
||||
// Support Enter and Space keys for accessibility
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
this.handleTogglePane();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Programmatically collapse the pane
|
||||
*/
|
||||
collapsePane(): void {
|
||||
if (!this.collapsed && this.collapsible) {
|
||||
this.handleTogglePane();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Programmatically expand the pane
|
||||
*/
|
||||
expandPane(): void {
|
||||
if (this.collapsed && this.collapsible) {
|
||||
this.handleTogglePane();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current pane state
|
||||
*/
|
||||
isPaneCollapsed(): boolean {
|
||||
return this.collapsed;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user