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 { BadgeDemoComponent } from './badge-demo/badge-demo.component';
|
||||||
import { MenuDemoComponent } from './menu-demo/menu-demo.component';
|
import { MenuDemoComponent } from './menu-demo/menu-demo.component';
|
||||||
import { InputDemoComponent } from './input-demo/input-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 { RadioDemoComponent } from './radio-demo/radio-demo.component';
|
||||||
import { CheckboxDemoComponent } from './checkbox-demo/checkbox-demo.component';
|
import { CheckboxDemoComponent } from './checkbox-demo/checkbox-demo.component';
|
||||||
import { SearchDemoComponent } from './search-demo/search-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 { FloatingToolbarDemoComponent } from './floating-toolbar-demo/floating-toolbar-demo.component';
|
||||||
import { TagInputDemoComponent } from './tag-input-demo/tag-input-demo.component';
|
import { TagInputDemoComponent } from './tag-input-demo/tag-input-demo.component';
|
||||||
import { IconButtonDemoComponent } from './icon-button-demo/icon-button-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({
|
@Component({
|
||||||
@@ -111,9 +127,6 @@ import { IconButtonDemoComponent } from './icon-button-demo/icon-button-demo.com
|
|||||||
|
|
||||||
@case ("input") {
|
@case ("input") {
|
||||||
<ui-input-demo></ui-input-demo>
|
<ui-input-demo></ui-input-demo>
|
||||||
}
|
|
||||||
@case ("layout") {
|
|
||||||
<ui-layout-demo></ui-layout-demo>
|
|
||||||
}
|
}
|
||||||
@case ("radio") {
|
@case ("radio") {
|
||||||
<ui-radio-demo></ui-radio-demo>
|
<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>
|
<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,
|
imports: [AvatarDemoComponent, ButtonDemoComponent, IconButtonDemoComponent, CardDemoComponent,
|
||||||
ChipDemoComponent, TableDemoComponent, BadgeDemoComponent,
|
ChipDemoComponent, TableDemoComponent, BadgeDemoComponent,
|
||||||
MenuDemoComponent, InputDemoComponent,
|
MenuDemoComponent, InputDemoComponent,
|
||||||
LayoutDemoComponent, RadioDemoComponent, CheckboxDemoComponent,
|
RadioDemoComponent, CheckboxDemoComponent,
|
||||||
SearchDemoComponent, SwitchDemoComponent, ProgressDemoComponent,
|
SearchDemoComponent, SwitchDemoComponent, ProgressDemoComponent,
|
||||||
AppbarDemoComponent, BottomNavigationDemoComponent, FontAwesomeDemoComponent, ImageContainerDemoComponent,
|
AppbarDemoComponent, BottomNavigationDemoComponent, FontAwesomeDemoComponent, ImageContainerDemoComponent,
|
||||||
CarouselDemoComponent, VideoPlayerDemoComponent, ListDemoComponent,
|
CarouselDemoComponent, VideoPlayerDemoComponent, ListDemoComponent,
|
||||||
@@ -318,7 +399,7 @@ import { IconButtonDemoComponent } from './icon-button-demo/icon-button-demo.com
|
|||||||
SkeletonLoaderDemoComponent, EmptyStateDemoComponent, FileUploadDemoComponent, FormFieldDemoComponent,
|
SkeletonLoaderDemoComponent, EmptyStateDemoComponent, FileUploadDemoComponent, FormFieldDemoComponent,
|
||||||
AutocompleteDemoComponent, BackdropDemoComponent, OverlayContainerDemoComponent, LoadingSpinnerDemoComponent,
|
AutocompleteDemoComponent, BackdropDemoComponent, OverlayContainerDemoComponent, LoadingSpinnerDemoComponent,
|
||||||
ProgressCircleDemoComponent, RangeSliderDemoComponent, ColorPickerDemoComponent, DividerDemoComponent, TooltipDemoComponent, AccordionDemoComponent,
|
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 { Component } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { DividerComponent } from '../../../../../ui-essentials/src/public-api';
|
import { DividerComponent } from '../../../../../ui-essentials/src/lib/components/layout';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ui-divider-demo',
|
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 './chip-demo/chip-demo.component';
|
||||||
export * from './fontawesome-demo/fontawesome-demo.component';
|
export * from './fontawesome-demo/fontawesome-demo.component';
|
||||||
export * from './input-demo/input-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 './menu-demo/menu-demo.component';
|
||||||
export * from './progress-demo/progress-demo.component';
|
export * from './progress-demo/progress-demo.component';
|
||||||
export * from './radio-demo/radio-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 {
|
.demo-container {
|
||||||
padding: $semantic-spacing-component-lg;
|
padding: $semantic-spacing-component-lg;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { TreeViewComponent } from "../../../../../ui-essentials/src/public-api";
|
import { TreeNode, TreeViewComponent } from "../../../../../ui-essentials/src/public-api";
|
||||||
import { TreeNode } from '../../../../../../dist/ui-essentials/lib/components/data-display/tree-view/tree-view.component';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ui-tree-view-demo',
|
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 { LayoutContainerComponent } from "../../shared/layout-containers/layout-container.component";
|
||||||
import {
|
import {
|
||||||
faWindowMaximize, faUserCircle, faCertificate, faHandPointer, faIdCard, faTags,
|
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,
|
faTable, faImage, faImages, faPlay, faBars, faEdit, faEye, faCompass,
|
||||||
faVideo, faComment, faMousePointer, faLayerGroup, faSquare, faCalendarDays, faClock,
|
faVideo, faComment, faMousePointer, faLayerGroup, faSquare, faCalendarDays, faClock,
|
||||||
faGripVertical, faArrowsAlt, faBoxOpen, faChevronLeft, faSpinner, faExclamationTriangle,
|
faGripVertical, faArrowsAlt, faBoxOpen, faChevronLeft, faSpinner, faExclamationTriangle,
|
||||||
faCloudUploadAlt, faFileText, faListAlt, faCircle, faExpandArrowsAlt, faCircleNotch, faSliders,
|
faCloudUploadAlt, faFileText, faListAlt, faCircle, faExpandArrowsAlt, faCircleNotch, faSliders,
|
||||||
faMinus, faInfoCircle, faChevronDown, faCaretUp, faExclamationCircle, faSitemap, faStream,
|
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';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
import { DemoRoutes } from '../../demos';
|
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";
|
// import { DemoRoutes } from "../../../../../ui-essentials/src/public-api";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -23,8 +24,8 @@ import { ScrollContainerComponent } from '../../../../../ui-essentials/src/lib/l
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
DashboardSidebarComponent,
|
DashboardSidebarComponent,
|
||||||
LayoutContainerComponent,
|
LayoutContainerComponent,
|
||||||
ScrollContainerComponent,
|
DemoRoutes,
|
||||||
DemoRoutes
|
ScrollContainerComponent
|
||||||
],
|
],
|
||||||
template: `
|
template: `
|
||||||
|
|
||||||
@@ -108,6 +109,12 @@ export class DashboardComponent {
|
|||||||
faExchangeAlt = faExchangeAlt;
|
faExchangeAlt = faExchangeAlt;
|
||||||
faTools = faTools;
|
faTools = faTools;
|
||||||
faHeart = faHeart;
|
faHeart = faHeart;
|
||||||
|
faCircleDot = faCircleDot;
|
||||||
|
faColumns = faColumns;
|
||||||
|
faFolderOpen = faFolderOpen;
|
||||||
|
faDesktop = faDesktop;
|
||||||
|
faNewspaper = faNewspaper;
|
||||||
|
faListUl = faListUl;
|
||||||
|
|
||||||
menuItems: any = []
|
menuItems: any = []
|
||||||
|
|
||||||
@@ -219,10 +226,26 @@ export class DashboardComponent {
|
|||||||
|
|
||||||
// Layout category
|
// Layout category
|
||||||
const layoutChildren = [
|
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-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("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);
|
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-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;
|
$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
|
// These overrides will cascade through all semantic typography tokens
|
||||||
// that use $base-typography-font-family-sans and $base-typography-font-family-display
|
// 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;
|
$comfortaa-font-path: "/fonts/comfortaa/" !default;
|
||||||
|
|
||||||
// Import specific fonts we need for the demo
|
// Import specific fonts we need for the demo
|
||||||
@import '../../shared-ui/src/styles/fontfaces/inter';
|
@import '../../ui-design-system/src/styles/fontfaces/inter';
|
||||||
@import '../../shared-ui/src/styles/fontfaces/comfortaa';
|
@import '../../ui-design-system/src/styles/fontfaces/comfortaa';
|
||||||
|
|
||||||
// Import project variables (which now has semantic tokens available)
|
// Import project variables (which now has semantic tokens available)
|
||||||
@import 'scss/_variables';
|
@import 'scss/_variables';
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ export * from './badge';
|
|||||||
export * from './card';
|
export * from './card';
|
||||||
export * from './carousel';
|
export * from './carousel';
|
||||||
export * from './chip';
|
export * from './chip';
|
||||||
export * from './divider';
|
|
||||||
export * from './image-container';
|
export * from './image-container';
|
||||||
export * from './list';
|
export * from './list';
|
||||||
export * from './progress';
|
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 './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 './grid-system';
|
||||||
export * from './spacer';
|
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