diff --git a/projects/demo-ui-essentials/src/app/demos/aspect-ratio-demo/aspect-ratio-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/aspect-ratio-demo/aspect-ratio-demo.component.scss new file mode 100644 index 0000000..dbe92c9 --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/aspect-ratio-demo/aspect-ratio-demo.component.scss @@ -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; + } +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/aspect-ratio-demo/aspect-ratio-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/aspect-ratio-demo/aspect-ratio-demo.component.ts new file mode 100644 index 0000000..6c2cfdf --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/aspect-ratio-demo/aspect-ratio-demo.component.ts @@ -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: ` +
+

Aspect Ratio Demo

+

Components for maintaining consistent aspect ratios across responsive content.

+ + +
+

Aspect Ratio Presets

+
+ @for (preset of presets; track preset.ratio) { +
+

{{ preset.label }}

+ +
+ {{ preset.ratio }} ({{ preset.description }}) +
+
+
+ } +
+
+ + +
+

Custom Aspect Ratios

+
+ @for (custom of customRatios; track custom.ratio) { +
+

{{ custom.label }}

+ +
+ {{ custom.ratio }} +
+
+
+ } +
+
+ + +
+

Size Variants

+
+ @for (size of sizes; track size) { +
+

Size: {{ size }}

+ +
+ {{ size }} size +
+
+
+ } +
+
+ + +
+

Style Variants

+
+ @for (variant of variants; track variant) { +
+

{{ variant | titlecase }}

+ +
+ {{ variant }} variant + @if (variant === 'interactive') { + Click me! + } +
+
+
+ } +
+
+ + +
+

Image Examples

+
+
+

Square Image

+ + Square demo image + +
+ +
+

Video Aspect

+ + Video aspect demo image + +
+ +
+

Portrait Image

+ + Portrait demo image + +
+
+
+ + +
+

Loading State

+
+
+

Loading

+ + + +
+ +
+ + +
+ Dynamic Loading +
+
+
+
+
+ + +
+

Content Centering

+
+
+

Fill Container

+ +
+ Fills entire container +
+
+
+ +
+

Centered Content

+ +
+ I'm centered! +
+
+
+
+
+ + +
+

Interactive Examples

+
+
+

Click Counter

+ +
+
+
{{ clickCount }}
+
Click to increment
+
+
+
+
+
+
+
+ `, + 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; + } +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/bento-grid-demo/bento-grid-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/bento-grid-demo/bento-grid-demo.component.scss new file mode 100644 index 0000000..fefa99e --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/bento-grid-demo/bento-grid-demo.component.scss @@ -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; + } +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/bento-grid-demo/bento-grid-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/bento-grid-demo/bento-grid-demo.component.ts new file mode 100644 index 0000000..6938d06 --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/bento-grid-demo/bento-grid-demo.component.ts @@ -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: ` +
+

Bento Grid Demo

+ + +
+

Basic Bento Grid

+ + + Basic grid item with regular content. + + + + This is a featured item that spans 2 columns and 2 rows. + + + + Another regular item without header. + + + + This item spans 2 columns wide. + + + + This item spans 2 rows tall. + + + Regular item + Regular item + + + Large featured item spanning 3 columns and 2 rows. + + + Regular item + Regular item + +
+ + +
+

Fixed Column Grids

+ +

4 Columns

+ + Item 1 + Item 2 (2 cols) + Item 3 + Item 4 + Item 5 (3 cols) + Item 6 + + +

6 Columns

+ + @for (item of sixColumnItems; track item.id) { + + {{ item.content }} + + } + +
+ + +
+

Gap Sizes

+
+ @for (gap of gaps; track gap) { +
+

Gap: {{ gap }}

+ + Item A + Item B + Item C + Item D (2 cols) + Item E + +
+ } +
+
+ + +
+

Featured Item Sizes

+ + + 2×1 featured item + + + Regular + Regular + + + 2×2 featured item + + + Regular + Regular + Regular + + + 3×2 featured item + + + Regular + Regular + +
+ + +
+

Interactive Items

+ + + Interactive primary item + + + + Wide interactive item + + + + Featured interactive item + + + Static item + Static item + + + @if (lastClicked) { +

Last clicked: {{ lastClicked }}

+ } +
+ + +
+

Row Heights

+
+ @for (rowHeight of rowHeights; track rowHeight) { +
+

Rows: {{ rowHeight }}

+ + Small content + + Longer content that might need more space depending on the row height setting. + + Tall item + Wide item + +
+ } +
+
+
+ `, + 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); + } +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/box-demo/box-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/box-demo/box-demo.component.scss new file mode 100644 index 0000000..1ce2432 --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/box-demo/box-demo.component.scss @@ -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; +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/box-demo/box-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/box-demo/box-demo.component.ts new file mode 100644 index 0000000..6b9f8e2 --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/box-demo/box-demo.component.ts @@ -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: ` +
+

Box Component Demo

+ + +
+

Padding Utilities

+
+ @for (spacing of spacings; track spacing) { +
+

p="{{ spacing }}"

+ +
Content with {{ spacing }} padding
+
+
+ } +
+
+ + +
+

Directional Padding

+
+
+

px="lg" (horizontal)

+ +
Horizontal padding
+
+
+ +
+

py="lg" (vertical)

+ +
Vertical padding
+
+
+ +
+

pt="xl" pb="sm"

+ +
Mixed padding
+
+
+
+
+ + +
+

Margin Utilities

+
+ @for (spacing of marginSpacings; track spacing) { + + m="{{ spacing }}" + + } +
+
+ + +
+

Display Options

+
+

display="flex"

+ +
Item 1
+
Item 2
+
Item 3
+
+ +

display="grid"

+ +
A
+
B
+
C
+
D
+
+ +

display="inline-block"

+
+ Box 1 + Box 2 + Box 3 +
+
+
+ + +
+

Visual Styles

+
+
+

Basic

+ +
Basic box
+
+
+ +
+

With Border

+ +
Bordered box
+
+
+ +
+

Rounded

+ +
Rounded box
+
+
+ +
+

With Shadow

+ +
Shadow box
+
+
+ +
+

All Styles

+ +
Full styled
+
+
+
+
+ + +
+

Complex Layout Example

+ + +

Card Header

+
+ + + +
Left column content with some text that wraps nicely.
+
+ + +
Right column with border and padding.
+
+
+ + +
Footer with top border only
+
+
+
+
+ `, + styleUrl: './box-demo.component.scss' +}) +export class BoxDemoComponent { + spacings = ['xs', 'sm', 'md', 'lg', 'xl'] as const; + marginSpacings = ['xs', 'sm', 'md', 'lg'] as const; +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/breakpoint-container-demo/breakpoint-container-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/breakpoint-container-demo/breakpoint-container-demo.component.scss new file mode 100644 index 0000000..55ff40f --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/breakpoint-container-demo/breakpoint-container-demo.component.scss @@ -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; + } +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/breakpoint-container-demo/breakpoint-container-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/breakpoint-container-demo/breakpoint-container-demo.component.ts new file mode 100644 index 0000000..b9a392f --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/breakpoint-container-demo/breakpoint-container-demo.component.ts @@ -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: ` +
+

Breakpoint Container Demo

+

This component shows/hides content based on screen size. Resize your browser to see the effects.

+ + +
+

Basic Visibility Controls

+ +
+
+

Always Visible

+ +
+ Always visible content +
+
+
+ +
+

Mobile Only (< 768px)

+ +
+ Only visible on mobile devices +
+
+
+ +
+

Tablet Only (768px - 1024px)

+ +
+ Only visible on tablets +
+
+
+ +
+

Desktop Only (≥ 1024px)

+ + + +
+
+
+ + +
+

Hidden Variants

+ +
+
+

Mobile Hidden

+ +
+ Hidden on mobile (< 768px) +
+
+
+ +
+

Tablet Hidden

+ +
+ Hidden on tablets (768px - 1024px) +
+
+
+ +
+

Desktop Hidden

+ +
+ Hidden on desktop (≥ 1024px) +
+
+
+
+
+ + +
+

Screen Size Based

+ +
+
+

Mobile Screen (< 640px)

+ +
+ Mobile screen size only +
+
+
+ +
+

Tablet Screen (640px - 1024px)

+ +
+ Tablet screen size only +
+
+
+ +
+

Desktop Screen (≥ 1024px)

+ + + +
+
+
+ + +
+

Range Based Visibility

+ +
+
+

Small to Medium (640px - 768px)

+ +
+ Visible between small and medium breakpoints +
+
+
+ +
+

Medium to Large (768px - 1024px)

+ +
+ Visible between medium and large breakpoints +
+
+
+
+
+ + +
+

Custom Breakpoint Rules

+ +
+
+

Show from Small (≥ 640px)

+ +
+ Visible from small breakpoint and up +
+
+
+ +
+

Hide from Medium (≥ 768px)

+ +
+ Hidden from medium breakpoint and up +
+
+
+ +
+

Show from Large (≥ 1024px)

+ + + +
+
+
+ + +
+

Display Types

+ +
+
+

Block Display (Default)

+ +
+ Block display (default) +
+
+
+ +
+

Inline Display

+

+ This is inline text with + + desktop-only inline content + + continuing after. +

+
+ +
+

Flex Display

+ +
+ Flex Item 1 +
+
+ Flex Item 2 +
+
+
+
+
+ + +
+

Print Media

+ +
+
+

Print Hidden

+ +
+ This content is hidden when printing +
+
+
+ +
+

Print Only

+ +
+ This content only appears when printing +
+
+
+
+
+ + +
+

Interactive Test

+

Current viewport info: Resize your browser window to test different breakpoints.

+ +
+ +
+ 📱 Mobile Screen (< 640px) +
+
+ + +
+ 📟 Tablet Screen (640px - 1024px) +
+
+ + + + +
+ +

Test Instructions:

+ +
+
+ `, + styleUrl: './breakpoint-container-demo.component.scss' +}) +export class BreakpointContainerDemoComponent {} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/center-demo/center-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/center-demo/center-demo.component.scss new file mode 100644 index 0000000..1a1fe7a --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/center-demo/center-demo.component.scss @@ -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; + } +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/center-demo/center-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/center-demo/center-demo.component.ts new file mode 100644 index 0000000..5d1e8f4 --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/center-demo/center-demo.component.ts @@ -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: ` +
+

Center Demo

+ + +
+

Center Axis

+
+
+

Both Axes (Default)

+ +
Centered Both Ways
+
+
+ +
+

Horizontal Only

+ +
Centered Horizontally
+
+
+ +
+

Vertical Only

+ +
Centered Vertically
+
+
+
+
+ + +
+

Max Width Constraints

+
+
+

Small (640px)

+ +
+ 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. +
+
+
+ +
+

Medium (768px)

+ +
+ 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. +
+
+
+ +
+

Large (1024px)

+ +
+ 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. +
+
+
+
+
+ + +
+

Spacing

+
+
+

Small Padding

+ +
Small Padding
+
+
+ +
+

Medium Padding

+ +
Medium Padding
+
+
+ +
+

Large Padding

+ +
Large Padding
+
+
+
+
+ + +
+

Inline Center

+
+

+ Here is some text with an + + inline centered element + + in the middle of the paragraph. +

+
+
+ + +
+

Practical Examples

+ +
+

Card Layout

+ +
+

Welcome Message

+

This card is centered with constrained width for better readability.

+ +
+
+
+ +
+

Loading State

+ +
+
+

Loading...

+
+
+
+ +
+

Hero Section

+ +
+

Hero Title

+

A compelling hero message that's perfectly centered and constrained for optimal reading.

+ +
+
+
+
+
+ `, + styleUrl: './center-demo.component.scss' +}) +export class CenterDemoComponent {} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/column-demo/column-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/column-demo/column-demo.component.scss new file mode 100644 index 0000000..bd0fc5a --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/column-demo/column-demo.component.scss @@ -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; + } + } + } +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/column-demo/column-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/column-demo/column-demo.component.ts new file mode 100644 index 0000000..d687842 --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/column-demo/column-demo.component.ts @@ -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: ` +
+

Column Layout Demo

+ + +
+

Column Counts

+
+ @for (count of columnCounts; track count) { +
+

{{ count }} Column{{ count !== '1' ? 's' : '' }}

+ +

{{ sampleText }}

+
+
+ } +
+
+ + +
+

Gap Sizes (2 Columns)

+
+ @for (gap of gaps; track gap) { +
+

Gap: {{ gap }}

+ +

{{ shortText }}

+
+
+ } +
+
+ + +
+

Column Rules (3 Columns)

+
+ @for (rule of rules; track rule) { +
+

Rule: {{ rule }}

+ +

{{ mediumText }}

+
+
+ } +
+
+ + +
+

Fill Types (3 Columns)

+
+ @for (fill of fills; track fill) { +
+

Fill: {{ fill }}

+ +

{{ variableText }}

+
+
+ } +
+
+ + +
+

Responsive Layout

+

Resize your browser to see responsive column behavior

+
+

Responsive 4-Column Layout

+ +
Section 1
+

{{ sampleText }}

+
Section 2
+

{{ shortText }}

+
Section 3
+

{{ mediumText }}

+
Section 4
+

{{ variableText }}

+
+
+
+ + +
+

Article Layout Example

+
+ +

The Future of Web Development

+

{{ longText }}

+

{{ mediumText }}

+
Key Benefits
+
    +
  • Improved readability through better text flow
  • +
  • Enhanced visual hierarchy with column structures
  • +
  • Responsive design that adapts to screen sizes
  • +
  • Better content organization for long-form text
  • +
+

{{ shortText }}

+
+
+
+ + +
+

Interactive Example

+
+ + + + +
+
+ +

Interactive Column Layout

+

{{ sampleText }}

+

{{ mediumText }}

+

{{ shortText }}

+
+
+
+
+ `, + 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."; +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/dashboard-shell-demo/dashboard-shell-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/dashboard-shell-demo/dashboard-shell-demo.component.scss new file mode 100644 index 0000000..565df89 --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/dashboard-shell-demo/dashboard-shell-demo.component.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/dashboard-shell-demo/dashboard-shell-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/dashboard-shell-demo/dashboard-shell-demo.component.ts new file mode 100644 index 0000000..149a8b7 --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/dashboard-shell-demo/dashboard-shell-demo.component.ts @@ -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: ` +
+

Dashboard Shell Demo

+ + +
+

Visual Variants

+
+ @for (variant of variants; track variant) { +
+

{{ variant | titlecase }}

+
+ + +
+

{{ variant | titlecase }} App

+
+ +
+ 🌙 + 🔔 + 👤 +
+ + + + + +
+

📢 Welcome to {{ variant }} variant!

+
+ +
+

Main Content

+

This is the main content area for {{ variant }} dashboard variant.

+
+ +
+ © 2024 Dashboard App +
+
+
+
+ } +
+
+ + +
+

Size Configurations

+
+ @for (config of sizeConfigs; track config.name) { +
+

{{ config.name }}

+
+ + +
+

{{ config.name }} Dashboard

+
+ +
+ 🔍 + 📱 +
+ + + + + +
+

{{ config.name }} Content

+

Sidebar: {{ config.sidebarWidth }}, Header: {{ config.headerHeight }}, Footer: {{ config.footerVariant }}

+
+ +
+ {{ config.name }} Footer + v1.0.0 +
+
+
+
+ } +
+
+ + +
+

Layout Options

+
+
+

No Footer

+
+ + +
+

Clean Dashboard

+
+ +
+ 🎨 +
+ + + +
+

Clean Layout

+

Dashboard without footer for cleaner appearance.

+
+
+
+
+ +
+

Minimal UI

+
+ + +
+

Minimal App

+
+ +
+ +
+ + + +
+

Minimal Interface

+

Streamlined dashboard without breadcrumbs and notifications.

+
+ +
+ Minimal +
+
+
+
+
+
+ + +
+

Interactive Dashboard

+
+ + + + + + + + + + + + + + + + + +
+ +
+ + +
+

Interactive Dashboard

+
+ +
+ + + +
+ + + + + +
+ @if (hasNewNotifications) { +
+ 🎉 New feature available! Click to explore the {{ activeSection }} section. + +
+ } +
+ +
+

{{ activeSection | titlecase }} Section

+

Current configuration:

+
    +
  • Variant: {{ interactiveVariant }}
  • +
  • Sidebar Width: {{ interactiveSidebarWidth }}
  • +
  • Header Height: {{ interactiveHeaderHeight }}
  • +
  • Footer Variant: {{ interactiveFooterVariant }}
  • +
  • Sidebar Collapsed: {{ interactiveSidebarCollapsed ? 'Yes' : 'No' }}
  • +
  • Theme: {{ isDarkTheme ? 'Dark' : 'Light' }}
  • +
+ +
+
{{ activeSection | titlecase }} Content
+ @switch (activeSection) { + @case ('dashboard') { +

📈 Welcome to your dashboard overview. Here you can see key metrics and quick actions.

+
+
Total Users: 1,234
+
Active Projects: 42
+
Revenue: $12,345
+
+ } + @case ('analytics') { +

📊 Analytics and reporting section. View detailed insights about your application usage.

+
📈 Chart Placeholder
+ } + @case ('users') { +

👥 User management section. Manage user accounts, permissions, and profiles.

+
+
John Doe - Admin
+
Jane Smith - User
+
Bob Johnson - Manager
+
+ } + @case ('projects') { +

💼 Project management section. Track and manage your ongoing projects.

+
+
🚀 Website Redesign - In Progress
+
📱 Mobile App - Planning
+
🔧 API Integration - Completed
+
+ } + @case ('settings') { +

⚙️ Application settings and configuration options.

+
+
+
Appearance
+ +
+
+
Notifications
+ +
+
+ } + @default { +

Select a section from the sidebar to view its content.

+ } + } +
+ +
+ + + +
+
+ +
+ @if (interactiveFooterVariant === 'detailed') { + + } @else if (interactiveFooterVariant === 'standard') { + © 2024 Interactive Dashboard - v1.2.3 + Status: {{ connectionStatus }} + } @else { + © 2024 Dashboard + } +
+
+
+
+ + +
+

Usage Information

+
+

Key Features:

+
    +
  • 🏗️ Complete application shell structure
  • +
  • 📱 Mobile-responsive with hamburger menu
  • +
  • 🎨 Multiple visual variants (default, bordered, elevated)
  • +
  • 📏 Configurable sizing for header, sidebar, and footer
  • +
  • 🧭 Integrated breadcrumbs navigation
  • +
  • 🔔 Notifications area with live region support
  • +
  • ♿ Full accessibility support with ARIA labels
  • +
  • 🎛️ Flexible content projection slots
  • +
  • ⚡ Event emitters for user interactions
  • +
+ +

Content Projection Slots:

+
    +
  • slot="header" - Header title and branding
  • +
  • slot="header-actions" - Theme switcher, notifications, user menu
  • +
  • slot="sidebar" - Main navigation content
  • +
  • slot="breadcrumbs" - Breadcrumb navigation
  • +
  • slot="notifications" - System notifications and alerts
  • +
  • slot="footer" - Footer content and links
  • +
  • Default slot - Main page content
  • +
+
+
+
+ `, + 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; + } + } +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/demos.routes.ts b/projects/demo-ui-essentials/src/app/demos/demos.routes.ts index 36433a9..aa69dbe 100644 --- a/projects/demo-ui-essentials/src/app/demos/demos.routes.ts +++ b/projects/demo-ui-essentials/src/app/demos/demos.routes.ts @@ -9,7 +9,6 @@ import { TableDemoComponent } from './table-demo/table-demo.component'; import { BadgeDemoComponent } from './badge-demo/badge-demo.component'; import { MenuDemoComponent } from './menu-demo/menu-demo.component'; import { InputDemoComponent } from './input-demo/input-demo.component'; -import { LayoutDemoComponent } from './layout-demo/layout-demo.component'; import { RadioDemoComponent } from './radio-demo/radio-demo.component'; import { CheckboxDemoComponent } from './checkbox-demo/checkbox-demo.component'; import { SearchDemoComponent } from './search-demo/search-demo.component'; @@ -61,6 +60,23 @@ import { TransferListDemoComponent } from './transfer-list-demo/transfer-list-de import { FloatingToolbarDemoComponent } from './floating-toolbar-demo/floating-toolbar-demo.component'; import { TagInputDemoComponent } from './tag-input-demo/tag-input-demo.component'; import { IconButtonDemoComponent } from './icon-button-demo/icon-button-demo.component'; +import { StackDemoComponent } from './stack-demo/stack-demo.component'; +import { BoxDemoComponent } from './box-demo/box-demo.component'; +import { CenterDemoComponent } from './center-demo/center-demo.component'; +import { AspectRatioDemoComponent } from './aspect-ratio-demo/aspect-ratio-demo.component'; +import { BentoGridDemoComponent } from './bento-grid-demo/bento-grid-demo.component'; +import { BreakpointContainerDemoComponent } from './breakpoint-container-demo/breakpoint-container-demo.component'; +import { SectionDemoComponent } from './section-demo/section-demo.component'; +import { FlexDemoComponent } from './flex-demo/flex-demo.component'; +import { ColumnDemoComponent } from './column-demo/column-demo.component'; +import { SidebarLayoutDemoComponent } from './sidebar-layout-demo/sidebar-layout-demo.component'; +import { ScrollContainerDemoComponent } from './scroll-container-demo/scroll-container-demo.component'; +import { TabsContainerDemoComponent } from './tabs-container-demo/tabs-container-demo.component'; +import { DashboardShellDemoComponent } from './dashboard-shell-demo/dashboard-shell-demo.component'; +import { GridContainerDemoComponent } from './grid-container-demo/grid-container-demo.component'; +import { FeedLayoutDemoComponent } from './feed-layout-demo/feed-layout-demo.component'; +import { ListDetailLayoutDemoComponent } from './list-detail-layout-demo/list-detail-layout-demo.component'; +import { SupportingPaneLayoutDemoComponent } from './supporting-pane-layout-demo/supporting-pane-layout-demo.component'; @Component({ @@ -111,9 +127,6 @@ import { IconButtonDemoComponent } from './icon-button-demo/icon-button-demo.com @case ("input") { - } - @case ("layout") { - } @case ("radio") { @@ -303,13 +316,81 @@ import { IconButtonDemoComponent } from './icon-button-demo/icon-button-demo.com } + @case ("stack") { + + } + + @case ("box") { + + } + + @case ("center") { + + } + + @case ("aspect-ratio") { + + } + + @case ("bento-grid") { + + } + + @case ("breakpoint-container") { + + } + + @case ("section") { + + } + + @case ("flex") { + + } + + @case ("column") { + + } + + @case ("sidebar-layout") { + + } + + @case ("scroll-container") { + + } + + @case ("tabs-container") { + + } + + @case ("dashboard-shell") { + + } + + @case ("grid-container") { + + } + + @case ("feed-layout") { + + } + + @case ("list-detail-layout") { + + } + + @case ("supporting-pane-layout") { + + } + } `, imports: [AvatarDemoComponent, ButtonDemoComponent, IconButtonDemoComponent, CardDemoComponent, ChipDemoComponent, TableDemoComponent, BadgeDemoComponent, MenuDemoComponent, InputDemoComponent, - LayoutDemoComponent, RadioDemoComponent, CheckboxDemoComponent, + RadioDemoComponent, CheckboxDemoComponent, SearchDemoComponent, SwitchDemoComponent, ProgressDemoComponent, AppbarDemoComponent, BottomNavigationDemoComponent, FontAwesomeDemoComponent, ImageContainerDemoComponent, CarouselDemoComponent, VideoPlayerDemoComponent, ListDemoComponent, @@ -318,7 +399,7 @@ import { IconButtonDemoComponent } from './icon-button-demo/icon-button-demo.com SkeletonLoaderDemoComponent, EmptyStateDemoComponent, FileUploadDemoComponent, FormFieldDemoComponent, AutocompleteDemoComponent, BackdropDemoComponent, OverlayContainerDemoComponent, LoadingSpinnerDemoComponent, ProgressCircleDemoComponent, RangeSliderDemoComponent, ColorPickerDemoComponent, DividerDemoComponent, TooltipDemoComponent, AccordionDemoComponent, - PopoverDemoComponent, AlertDemoComponent, SnackbarDemoComponent, ToastDemoComponent, TreeViewDemoComponent, TimelineDemoComponent, StepperDemoComponent, FabMenuDemoComponent, EnhancedTableDemoComponent, SplitButtonDemoComponent, CommandPaletteDemoComponent, FloatingToolbarDemoComponent, TransferListDemoComponent, TagInputDemoComponent] + PopoverDemoComponent, AlertDemoComponent, SnackbarDemoComponent, ToastDemoComponent, TreeViewDemoComponent, TimelineDemoComponent, StepperDemoComponent, FabMenuDemoComponent, EnhancedTableDemoComponent, SplitButtonDemoComponent, CommandPaletteDemoComponent, FloatingToolbarDemoComponent, TransferListDemoComponent, TagInputDemoComponent, StackDemoComponent, BoxDemoComponent, CenterDemoComponent, AspectRatioDemoComponent, BentoGridDemoComponent, BreakpointContainerDemoComponent, SectionDemoComponent, FlexDemoComponent, ColumnDemoComponent, SidebarLayoutDemoComponent, ScrollContainerDemoComponent, TabsContainerDemoComponent, DashboardShellDemoComponent, GridContainerDemoComponent, FeedLayoutDemoComponent, ListDetailLayoutDemoComponent, SupportingPaneLayoutDemoComponent] }) diff --git a/projects/demo-ui-essentials/src/app/demos/divider-demo/divider-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/divider-demo/divider-demo.component.ts index 688b332..aa57dab 100644 --- a/projects/demo-ui-essentials/src/app/demos/divider-demo/divider-demo.component.ts +++ b/projects/demo-ui-essentials/src/app/demos/divider-demo/divider-demo.component.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { DividerComponent } from '../../../../../ui-essentials/src/public-api'; +import { DividerComponent } from '../../../../../ui-essentials/src/lib/components/layout'; @Component({ selector: 'ui-divider-demo', diff --git a/projects/demo-ui-essentials/src/app/demos/feed-layout-demo/feed-layout-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/feed-layout-demo/feed-layout-demo.component.scss new file mode 100644 index 0000000..2d3d47c --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/feed-layout-demo/feed-layout-demo.component.scss @@ -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; + } +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/feed-layout-demo/feed-layout-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/feed-layout-demo/feed-layout-demo.component.ts new file mode 100644 index 0000000..d90863a --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/feed-layout-demo/feed-layout-demo.component.ts @@ -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: ` +
+

Feed Layout Demo

+ + +
+

Sizes

+
+ @for (size of sizes; track size) { +
+

{{ size | titlecase }}

+ + @for (item of sampleItems.slice(0, 2); track item.id) { +
+
+ {{ item.author }} + {{ item.timestamp | date:'short' }} +
+
{{ item.title }}
+

{{ item.content }}

+
+ + + +
+
+ } +
+
+ } +
+
+ + +
+

Interactive Feed with Infinite Scroll

+
+ + + +
+ + + + @for (item of feedItems(); track item.id) { +
+
+ {{ item.author }} + + {{ item.type }} + + {{ item.timestamp | date:'short' }} +
+
{{ item.title }}
+

{{ item.content }}

+
+ + + +
+
+ } + + +
+
📝
+

No posts yet

+

Be the first to share something!

+ +
+
+
+ + +
+

States

+
+
+

Loading State

+ + @for (item of sampleItems.slice(0, 1); track item.id) { +
+
+ {{ item.author }} +
+
{{ item.title }}
+

{{ item.content }}

+
+ } +
+
+ +
+

Error State

+ + +
+ +
+

Empty State

+ +
+
📭
+

No content available

+
+
+
+
+
+ + +
+

Usage Statistics

+
+
+ Total Items: + {{ feedItems().length }} +
+
+ Load More Calls: + {{ loadMoreCount }} +
+
+ Refresh Calls: + {{ refreshCount }} +
+
+
+
+ `, + styleUrl: './feed-layout-demo.component.scss' +}) +export class FeedLayoutDemoComponent { + sizes = ['sm', 'md', 'lg'] as const; + + protected readonly feedItems = signal([]); + 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 + }; + }); + } +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/flex-demo/flex-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/flex-demo/flex-demo.component.scss new file mode 100644 index 0000000..ee2e248 --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/flex-demo/flex-demo.component.scss @@ -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; +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/flex-demo/flex-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/flex-demo/flex-demo.component.ts new file mode 100644 index 0000000..3153149 --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/flex-demo/flex-demo.component.ts @@ -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: ` +
+

Flex Component Demo

+ + +
+

Flex Direction

+
+ @for (direction of directions; track direction) { +
+

direction="{{ direction }}"

+ +
1
+
2
+
3
+
+
+ } +
+
+ + +
+

Justify Content

+
+ @for (justify of justifyOptions; track justify) { +
+

justify="{{ justify }}"

+ +
A
+
B
+
C
+
+
+ } +
+
+ + +
+

Align Items

+
+ @for (align of alignOptions; track align) { +
+

align="{{ align }}"

+ +
Small
+
Medium
+
Large item with more content
+
+
+ } +
+
+ + +
+

Flex Wrap

+
+ @for (wrapOption of wrapOptions; track wrapOption) { +
+

wrap="{{ wrapOption }}"

+ + @for (item of manyItems; track item) { +
{{ item }}
+ } +
+
+ } +
+
+ + +
+

Gap Spacing

+
+ @for (gap of gapSizes; track gap) { +
+

gap="{{ gap }}"

+ +
1
+
2
+
3
+
+
+ } +
+
+ + +
+

Complex Layout Examples

+ + +
+

Centered Card Layout

+ +
+
Centered Card
+

This card is perfectly centered using flexbox.

+
+
+
+ + +
+

Navigation Layout

+ + + + + + + + +
+ + +
+

Responsive Card Grid

+ + @for (card of cards; track card.id) { +
+
{{ card.title }}
+

{{ card.content }}

+
+ } +
+
+ + +
+

Sidebar Layout

+ + +
+
Main Content
+

This is the main content area that grows to fill the available space.

+
+
+
+
+ + +
+

Size Variants

+
+
+

Full Width

+ +
Full Width Container
+
+
+ +
+

Inline Flex

+
+ Before text + +
A
+
B
+
+ After text +
+
+
+
+
+ `, + 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.' }, + ]; +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/grid-container-demo/grid-container-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/grid-container-demo/grid-container-demo.component.scss new file mode 100644 index 0000000..7065a35 --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/grid-container-demo/grid-container-demo.component.scss @@ -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; + } +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/grid-container-demo/grid-container-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/grid-container-demo/grid-container-demo.component.ts new file mode 100644 index 0000000..77c21cf --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/grid-container-demo/grid-container-demo.component.ts @@ -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: ` +
+

GridContainer Demo

+

+ Advanced CSS Grid wrapper with template areas support, responsive column/row definitions, and auto-placement algorithms. +

+ + +
+

Basic Grid Layouts

+ +
+

Auto-fit Grid (Responsive)

+ + @for (item of basicItems; track item.id) { +
{{ item.label }}
+ } +
+
+ +
+

Fixed 3-Column Grid

+ + @for (item of basicItems; track item.id) { +
{{ item.label }}
+ } +
+
+
+ + +
+

Gap Sizes

+
+ @for (gap of gaps; track gap) { +
+

{{ gap }} Gap

+ + @for (item of shortItems; track item.id) { +
{{ item.label }}
+ } +
+
+ } +
+
+ + +
+

Column Layouts

+ @for (colCount of columnCounts; track colCount) { +
+

{{ colCount }} Columns

+ + @for (item of getItemsForColumns(colCount); track item.id) { +
{{ item.label }}
+ } +
+
+ } +
+ + +
+

Auto Grid Types

+ +
+

Auto-fit (Items stretch to fill space)

+ + @for (item of autoItems; track item.id) { +
{{ item.label }}
+ } +
+
+ +
+

Auto-fill (Empty columns maintained)

+ + @for (item of autoItems; track item.id) { +
{{ item.label }}
+ } +
+
+
+ + +
+

Advanced Features

+ +
+

Dense Grid (Auto-placement fills gaps)

+ +
Wide Item (2 cols)
+
Item 1
+
Tall Item
+
Item 2
+
Item 3
+
Extra Wide (3 cols)
+
Item 4
+
Item 5
+
+
+ +
+

Grid with Template Areas

+ +
Header
+
Sidebar
+
Main Content
+
Aside
+
Footer
+
+
+
+ + +
+

Alignment Options

+ +
+

Justify Content: Center

+ +
Item 1
+
Item 2
+
+
+ +
+

Align Items: Center

+ +
Centered
+
Items
+
Vertically
+
+
+
+ + +
+

Row Modes

+ @for (rowMode of rowModes; track rowMode) { +
+

{{ rowMode }} Rows

+ +
+ @if (rowMode === 'min-content') { + Short + } @else if (rowMode === 'max-content') { + This is longer content to demonstrate max-content behavior + } @else { + Content for {{ rowMode }} + } +
+
Item 2
+
Item 3
+
+
+ } +
+ + +
+

Interactive Grid Builder

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ + + @for (item of interactiveItems; track item.id) { +
+ {{ item.label }} + @if (item.span > 1) { + ({{ item.span }} cols) + } + @if (item.rowSpan > 1) { + ({{ item.rowSpan }} rows) + } +
+ } +
+
+
+ `, + 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); + } +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/index.ts b/projects/demo-ui-essentials/src/app/demos/index.ts index 5b40d5f..6368344 100644 --- a/projects/demo-ui-essentials/src/app/demos/index.ts +++ b/projects/demo-ui-essentials/src/app/demos/index.ts @@ -7,7 +7,6 @@ export * from './checkbox-demo/checkbox-demo.component'; export * from './chip-demo/chip-demo.component'; export * from './fontawesome-demo/fontawesome-demo.component'; export * from './input-demo/input-demo.component'; -export * from './layout-demo/layout-demo.component'; export * from './menu-demo/menu-demo.component'; export * from './progress-demo/progress-demo.component'; export * from './radio-demo/radio-demo.component'; diff --git a/projects/demo-ui-essentials/src/app/demos/layout-demo/layout-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/layout-demo/layout-demo.component.ts deleted file mode 100644 index f7cd6c9..0000000 --- a/projects/demo-ui-essentials/src/app/demos/layout-demo/layout-demo.component.ts +++ /dev/null @@ -1,1199 +0,0 @@ -import { Component, ChangeDetectionStrategy, signal } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { BentoGridLayoutComponent, DashboardShellLayoutComponent, ErrorState, FeedLayoutComponent, GridContainerComponent, GridResponsiveConfig, KpiCardLayoutComponent, ListDetailLayoutComponent, LoadingState, LoadingStateContainerComponent, ScrollContainerComponent, SupportingPaneLayoutComponent, TabContainerComponent, TabItem, VirtualScrollConfig, WidgetContainerComponent, WidgetGridLayoutComponent } from '../../../../../ui-essentials/src/public-api'; - -// Note: Local layout components are available but not used in this demo -// They can be imported if needed for specific layout demonstrations - -@Component({ - selector: 'ui-layout-demo', - standalone: true, - imports: [ - CommonModule, - DashboardShellLayoutComponent, - WidgetGridLayoutComponent, - WidgetContainerComponent, - BentoGridLayoutComponent, - KpiCardLayoutComponent, - ListDetailLayoutComponent, - FeedLayoutComponent, - SupportingPaneLayoutComponent, - GridContainerComponent, - TabContainerComponent, - ScrollContainerComponent, - LoadingStateContainerComponent -], - changeDetection: ChangeDetectionStrategy.OnPush, - template: ` -
-

Layout Component Showcase

- - -
-

Dashboard Shell Layout

-
-

Standard Dashboard Shell

-
- -
-
Dashboard Header
- - -
- - - -
-
Main Content Area
-

This is the main content area of the dashboard. Content can scroll independently.

-
Widget 1
-
Widget 2
-
Widget 3
-
- -
- © 2024 Dashboard Footer -
-
-
-
-
- - -
-

Widget Grid Layout

- - -
-

Small Grid (sm)

- - -
-
Widget 1
-

Small grid widget

-
-
- -
-
Widget 2
-

Analytics data

-
-
- -
-
Widget 3
-

Metrics overview

-
-
-
-
- - -
-

Medium Grid (md)

- - -
-
Revenue
-
$24,300
-
↗️ 12% increase
-
-
- -
-
Users
-
8,423
-
👥 Active users
-
-
- -
-
Orders
-
1,234
-
📦 This month
-
-
- -
-
Conversion
-
3.2%
-
📈 Conversion rate
-
-
-
-
- - -
-

Large Grid (lg)

- - -
-
Performance Dashboard
-
-
-
89%
-
CPU Usage
-
-
-
67%
-
Memory
-
-
-
45%
-
Disk I/O
-
-
-
-
System Status: All services operational
-
-
-
-
-
-
- - -
-

Bento Grid Layout

- - -
-

Balanced Bento

-
- -
-
Featured
-

Main highlight content

-
-
-
Analytics
-

Data insights

-
-
-
Metrics
-

Performance data

-
-
-
Reports
-

Generated reports

-
-
-
-
- - -
-

Asymmetric Bento

-
- -
-
-
Primary Widget
-
42
-
-
↗️ 8% this week
-
-
-
Secondary
-
$1,234
-
-
-
Users
-
892
-
-
-
Status
-
✅ Online
-
-
-
-
-
- - -
-

KPI Card Layout

- -
-
-
-
Total Revenue
-
$48,392
-
-
- 💰 -
-
-
- ↗️ - 12.5% increase - vs last month -
-
- -
-
-
-
Active Users
-
2,847
-
-
- 👥 -
-
-
- ↗️ - 8.2% increase - vs last month -
-
- -
-
-
-
Conversion Rate
-
3.7%
-
-
- 📈 -
-
-
- ↘️ - 2.1% decrease - vs last month -
-
-
-
- - -
-

List Detail Layout

-
- -
-
Items List
-
-
-
Project Alpha
-
Web Application
-
-
-
Project Beta
-
Mobile App
-
-
-
Project Gamma
-
API Service
-
-
-
- -
- @if (selectedItem() === 1) { -
Project Alpha Details
-

A comprehensive web application built with Angular and TypeScript.

-
-
Status
- Active -
-
-
Team Members
-
John Doe, Jane Smith, Mike Johnson
-
- } - @if (selectedItem() === 2) { -
Project Beta Details
-

Native mobile application for iOS and Android platforms.

-
-
Status
- In Progress -
-
-
Team Members
-
Sarah Wilson, Tom Brown
-
- } - @if (selectedItem() === 3) { -
Project Gamma Details
-

RESTful API service built with Node.js and Express.

-
-
Status
- Planning -
-
-
Team Members
-
Alex Chen, Maria Garcia
-
- } - @if (selectedItem() === 0) { -
-
📋
-
Select an item from the list
-

Choose a project from the left panel to view its details here.

-
- } -
-
-
-
- - -
-

Feed Layout

-
- -
-
-
- 👤 -
-
-
John Doe
-
2 hours ago
-
-
-
- Just launched our new feature! 🚀 Really excited to see how the community responds to this update. -
-
- - - -
-
- -
-
-
- 👩 -
-
-
Jane Smith
-
5 hours ago
-
-
-
- Working on some exciting new layouts for our dashboard. The bento grid approach is really promising for organizing complex data visualizations. -
-
-
📎 Attachment
-
dashboard-mockup.png
-
-
- - - -
-
- -
-
-
- 👨 -
-
-
Mike Johnson
-
1 day ago
-
-
-
- Great team meeting today! We've outlined the roadmap for Q2 and everyone is aligned on the priorities. Looking forward to shipping these features. -
-
- - - -
-
-
-
-
- - -
-

Supporting Pane Layout

-
- -
-
Main Content Area
-

- This is the primary content area where the main application content is displayed. - It takes up most of the available space and can scroll independently from the supporting pane. -

- -
-
Article Content
-

- 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. -

-
- -
-
Section 2
-

- Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. - Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. -

-
-
- -
-
Supporting Information
- -
-
Quick Stats
-
142
-
Total Items
-
- -
-
Recent Activity
-
• Item updated 2 min ago
-
• New comment added
-
• Status changed
-
- -
-
Related Links
- Documentation - API Reference - Support -
-
-
-
-
- - - - -
-

Grid Container (New)

- - -
-

Responsive Grid

- -
-
Span 6 Columns
-

Responsive: mobile 12, tablet 6, desktop 6

-
-
-
Span 3
-

Quarter width

-
-
-
Span 3
-

Quarter width

-
-
-
Span 4
-

One third width

-
-
-
Span 8
-

Two thirds width

-
-
-
- - -
-

Auto-Fit Grid

- - @for (item of gridItems(); track item.id) { -
-
{{ item.title }}
-

{{ item.description }}

-
- } -
-
- -
- - - -
-
- - -
-

Tab Container (New)

- -
-

Horizontal Tabs

-
- - -
-
Dashboard Content
-

This is the dashboard tab content with some sample data and metrics.

-
-
-
123
-
Users
-
-
-
$4,567
-
Revenue
-
-
-
- -
-
Analytics Content
-

Analytics data and charts would be displayed here.

-
-
📊
-
Charts and graphs
-
-
- -
-
Settings Content
-

Application settings and preferences.

-
-
- Enable notifications - -
-
- Dark mode - -
-
-
-
-
-
- - -
-

Vertical Pills Tabs

-
- - -
-
Profile Information
-
-
- 👤 -
-
-
John Doe
-
john.doe@example.com
-
-
-
- -
-
Account Settings
-
-
- - -
-
- - -
-
-
- -
-
Security Settings
-
-
- Two-factor authentication - Enabled -
-
- Login alerts - -
-
-
-
-
-
-
- - -
-

Scroll Container (New)

- - -
-

Virtual Scrolling (Large Dataset)

-
- - - -
-
- {{ item.avatar }} -
-
-
{{ item.name }}
-
{{ item.email }}
-
-
-
#{{ index + 1 }}
-
-
-
-
-
-
- - -
-

Custom Scrollbars

-
- -
-
Large Content Area
-

This content area is larger than the container, so it will show custom scrollbars.

-

You can scroll both horizontally and vertically to see all the content.

-
- @for (item of Array(8).fill(0); track $index) { -
-
Item {{ $index + 1 }}
-
Sample data
-
- } -
-
-
-
-
-
- - -
-

Loading State Container (New)

- -
- - - - - -
- -
- - - -
-
Sample Content
-

- This content can be in various loading states. Use the buttons above to simulate different states. -

-
-
-
Data Item 1
-

Sample data content

-
-
-
Data Item 2
-

More sample content

-
-
-
- - - -
-
-
-
-
-
-
-
-
-
-
-
- - -
-

Usage Examples

-
-

Dashboard Shell Layout:

-
<ui-dashboard-shell-layout 
-  [sidebarCollapsed]="collapsed"
-  [showFooter]="true">
-  <div slot="header">Header Content</div>
-  <nav slot="sidebar">Navigation</nav>
-  <div slot="main">Main Content</div>
-  <div slot="footer">Footer Content</div>
-</ui-dashboard-shell-layout>
- -

Widget Grid Layout:

-
<ui-widget-grid-layout 
-  gridSize="md" 
-  [autoFit]="true">
-  <ui-widget-container>
-    <div>Widget Content</div>
-  </ui-widget-container>
-</ui-widget-grid-layout>
- -

Bento Grid Layout:

-
<ui-bento-grid-layout 
-  variant="balanced" 
-  [adaptive]="true">
-  <div>Grid Item 1</div>
-  <div>Grid Item 2</div>
-  <div>Grid Item 3</div>
-</ui-bento-grid-layout>
- -

List Detail Layout:

-
<ui-list-detail-layout>
-  <div slot="list">
-    List of items
-  </div>
-  <div slot="detail">
-    Selected item details
-  </div>
-</ui-list-detail-layout>
-
-
- - -
-

Interactive Controls

-
- - - -
- - @if (lastAction()) { -
- Last action: {{ lastAction() }} -
- } -
-
- `, - styles: [` - h2 { - color: hsl(279, 14%, 11%); - font-size: 2rem; - margin-bottom: 2rem; - border-bottom: 2px solid hsl(258, 100%, 47%); - padding-bottom: 0.5rem; - } - - h3 { - color: hsl(279, 14%, 25%); - font-size: 1.5rem; - margin-bottom: 1rem; - } - - h4 { - color: hsl(287, 12%, 35%); - font-size: 1.125rem; - margin-bottom: 0.75rem; - } - - section { - border: 1px solid hsl(289, 14%, 90%); - border-radius: 8px; - padding: 1.5rem; - background: hsl(286, 20%, 99%); - } - - pre { - font-size: 0.875rem; - line-height: 1.5; - margin: 0.5rem 0; - } - - code { - font-family: 'JetBrains Mono', monospace; - color: #d63384; - } - - button { - transition: all 0.2s ease-in-out; - } - - button:hover { - opacity: 0.9; - transform: translateY(-1px); - } - `] -}) -export class LayoutDemoComponent { - // Original properties - sidebarCollapsed = signal(false); - showFooter = signal(false); - selectedItem = signal(0); - lastAction = signal(''); - - // Grid Container properties - showGridDebug = signal(false); - responsiveConfig = signal({ - mobile: 1, - tablet: 2, - desktop: 3, - wide: 4 - }); - - gridItems = signal([ - { id: 1, title: 'Dynamic Item 1', description: 'Auto-fit grid item', color: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }, - { id: 2, title: 'Dynamic Item 2', description: 'Responsive layout', color: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' }, - { id: 3, title: 'Dynamic Item 3', description: 'Grid container', color: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' }, - { id: 4, title: 'Dynamic Item 4', description: 'Flexible grid', color: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)' } - ]); - - // Tab Container properties - tabItems = signal([ - { id: 'dashboard', label: 'Dashboard', icon: '📊', badge: '3' }, - { id: 'analytics', label: 'Analytics', icon: '📈', closeable: true }, - { id: 'settings', label: 'Settings', icon: '⚙️', closeable: true } - ]); - - activeTabId = signal('dashboard'); - - verticalTabItems = signal([ - { id: 'profile', label: 'Profile', icon: '👤' }, - { id: 'account', label: 'Account', icon: '🔧' }, - { id: 'security', label: 'Security', icon: '🔒' } - ]); - - activeVerticalTabId = signal('profile'); - - // Scroll Container properties - virtualConfig = signal({ - itemHeight: 60, - bufferSize: 10 - }); - - largeDataset = signal(this.generateLargeDataset(1000)); - - // Loading State Container properties - loadingState = signal('idle'); - loadingConfig = signal({ - variant: 'spinner' as const, - size: 'md' as const, - message: 'Loading data...', - showProgress: false, - progress: 0 - }); - - errorState = signal(null); - - // Utility property - Array = Array; - - toggleSidebar(): void { - this.sidebarCollapsed.set(!this.sidebarCollapsed()); - this.lastAction.set(`Sidebar ${this.sidebarCollapsed() ? 'collapsed' : 'expanded'}`); - console.log(`Sidebar ${this.sidebarCollapsed() ? 'collapsed' : 'expanded'}`); - } - - toggleFooter(): void { - this.showFooter.set(!this.showFooter()); - this.lastAction.set(`Footer ${this.showFooter() ? 'shown' : 'hidden'}`); - console.log(`Footer ${this.showFooter() ? 'shown' : 'hidden'}`); - } - - selectItem(itemId: number): void { - this.selectedItem.set(itemId); - this.lastAction.set(`Selected item ${itemId}`); - console.log(`Selected item ${itemId}`); - } - - resetSelection(): void { - this.selectedItem.set(0); - this.lastAction.set('Selection reset'); - console.log('Selection reset'); - } - - // New container methods - - // Grid Container methods - toggleGridDebug(): void { - this.showGridDebug.set(!this.showGridDebug()); - this.lastAction.set(`Grid debug ${this.showGridDebug() ? 'enabled' : 'disabled'}`); - } - - addGridItem(): void { - const colors = [ - 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', - 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)', - 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)', - 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)', - 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)', - 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)' - ]; - - const currentItems = this.gridItems(); - const newId = Math.max(...currentItems.map(i => i.id)) + 1; - const newItem = { - id: newId, - title: `Dynamic Item ${newId}`, - description: `Added item #${newId}`, - color: colors[newId % colors.length] - }; - - this.gridItems.set([...currentItems, newItem]); - this.lastAction.set(`Added grid item ${newId}`); - } - - removeGridItem(): void { - const currentItems = this.gridItems(); - if (currentItems.length > 1) { - this.gridItems.set(currentItems.slice(0, -1)); - this.lastAction.set('Removed last grid item'); - } - } - - // Tab Container methods - onTabChange(event: any): void { - this.activeTabId.set(event.activeTab.id); - this.lastAction.set(`Switched to tab: ${event.activeTab.label}`); - console.log('Tab changed:', event); - } - - onTabClose(event: any): void { - const tabs = this.tabItems(); - const updatedTabs = tabs.filter(tab => tab.id !== event.tab.id); - this.tabItems.set(updatedTabs); - - // Switch to first available tab if current active tab was closed - if (event.tab.id === this.activeTabId() && updatedTabs.length > 0) { - this.activeTabId.set(updatedTabs[0].id); - } - - this.lastAction.set(`Closed tab: ${event.tab.label}`); - console.log('Tab closed:', event); - } - - onTabAdd(): void { - const currentTabs = this.tabItems(); - const newTabId = `tab-${Date.now()}`; - const newTab: TabItem = { - id: newTabId, - label: `New Tab ${currentTabs.length + 1}`, - icon: '📄', - closeable: true - }; - - this.tabItems.set([...currentTabs, newTab]); - this.activeTabId.set(newTabId); - this.lastAction.set(`Added new tab: ${newTab.label}`); - console.log('Tab added:', newTab); - } - - onVerticalTabChange(event: any): void { - this.activeVerticalTabId.set(event.activeTab.id); - this.lastAction.set(`Switched to vertical tab: ${event.activeTab.label}`); - console.log('Vertical tab changed:', event); - } - - // Scroll Container methods - onScroll(event: any): void { - // Only log occasionally to avoid spam - if (Math.random() < 0.1) { - console.log('Scroll event:', event); - } - } - - generateLargeDataset(size: number): any[] { - const names = ['John Doe', 'Jane Smith', 'Mike Johnson', 'Sarah Wilson', 'Tom Brown', 'Lisa Garcia', 'David Miller', 'Emily Davis']; - const colors = ['#007bff', '#28a745', '#ffc107', '#dc3545', '#6f42c1', '#20c997', '#fd7e14', '#17a2b8']; - const avatars = ['👨', '👩', '🧑', '👴', '👵', '👦', '👧', '🧒']; - - return Array.from({ length: size }, (_, i) => ({ - id: i + 1, - name: `${names[i % names.length]} ${Math.floor(i / names.length) + 1}`, - email: `user${i + 1}@example.com`, - color: colors[i % colors.length], - avatar: avatars[i % avatars.length] - })); - } - - // Loading State Container methods - setLoadingState(state: LoadingState): void { - this.loadingState.set(state); - - if (state === 'error') { - this.errorState.set({ - message: 'Failed to load data from the server.', - code: 'NETWORK_ERROR', - details: { timestamp: new Date().toISOString(), endpoint: '/api/data' }, - retryable: true - }); - } else { - this.errorState.set(null); - } - - this.lastAction.set(`Loading state changed to: ${state}`); - console.log('Loading state changed:', state); - } - - changeLoadingVariant(event: Event): void { - const target = event.target as HTMLSelectElement; - const variant = target.value as any; - - this.loadingConfig.update(config => ({ - ...config, - variant, - message: `Loading with ${variant} variant...` - })); - - this.lastAction.set(`Loading variant changed to: ${variant}`); - console.log('Loading variant changed:', variant); - } - - onLoadingRetry(): void { - this.lastAction.set('Retry button clicked'); - this.setLoadingState('loading'); - - // Simulate retry process - setTimeout(() => { - this.setLoadingState('success'); - }, 2000); - - console.log('Loading retry triggered'); - } - - onLoadingCancel(): void { - this.lastAction.set('Loading cancelled'); - this.setLoadingState('idle'); - console.log('Loading cancelled'); - } -} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/list-detail-layout-demo/list-detail-layout-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/list-detail-layout-demo/list-detail-layout-demo.component.scss new file mode 100644 index 0000000..f1c3cd5 --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/list-detail-layout-demo/list-detail-layout-demo.component.scss @@ -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; + } +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/list-detail-layout-demo/list-detail-layout-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/list-detail-layout-demo/list-detail-layout-demo.component.ts new file mode 100644 index 0000000..92c6c14 --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/list-detail-layout-demo/list-detail-layout-demo.component.ts @@ -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: ` +
+

List Detail Layout Demo

+

Master-detail pattern with resizable panes and mobile-adaptive behavior

+ + +
+

Size Variants

+
+ @for (size of sizes; track size) { +
+

{{ size | titlecase }} Size

+
+ + +
+
+

Items ({{ size }})

+
+
+ @for (item of demoItems(); track item.id) { +
+
+
{{ item.title }}
+

{{ item.subtitle }}

+ + {{ item.status }} + +
+
+ } +
+
+ +
+ @if (selectedItems[size]) { +
+

{{ selectedItems[size].title }}

+ {{ selectedItems[size].category }} +
+
+
+

Subtitle: {{ selectedItems[size].subtitle }}

+

Status: {{ selectedItems[size].status }}

+

Date: {{ selectedItems[size].date }}

+
+
+

Description

+

{{ selectedItems[size].description }}

+
+
+ + +
+
+ } +
+ +
+
📋
+

No Item Selected

+

Choose an item from the list to view its details

+
+
+
+
+ } +
+
+ + +
+

Style Variants

+
+ @for (variant of variants; track variant) { +
+

{{ variant | titlecase }} Style

+
+ + +
+
+

{{ variant | titlecase }} List

+
+
+ @for (item of demoItems().slice(0, 3); track item.id) { +
+
+
{{ item.title }}
+

{{ item.subtitle }}

+
+
+ } +
+
+ +
+ @if (selectedItems[variant]) { +
+

{{ selectedItems[variant].title }}

+
+
+

{{ selectedItems[variant].description }}

+
+ } +
+
+
+
+ } +
+
+ + +
+

Interactive Features

+
+
+ + + + +
+ +
+ + +
+
+

Interactive List

+ +
+
+ @for (item of demoItems(); track item.id) { +
+
+
{{ item.title }}
+

{{ item.subtitle }}

+ + {{ item.status }} + +
+
+ } +
+
+ +
+ @if (selectedItems['interactive']) { +
+

{{ selectedItems['interactive'].title }}

+ {{ selectedItems['interactive'].category }} +
+
+

{{ selectedItems['interactive'].description }}

+
+
+ + {{ selectedItems['interactive'].status }} +
+
+ + {{ selectedItems['interactive'].category }} +
+
+ + {{ selectedItems['interactive'].date }} +
+
+
+ } +
+ +
+
🎯
+

Ready to Explore

+

Select any item to see the interactive features in action

+
+
+
+
+
+ + +
+

Event Log

+
+ @for (event of eventLog(); track $index) { +
+ {{ event.timestamp }} + {{ event.type }} + {{ event.data }} +
+ } +
+
+
+ `, + 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 = { + 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([ + { + 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>([]); + + 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]); + } +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/scroll-container-demo/scroll-container-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/scroll-container-demo/scroll-container-demo.component.scss new file mode 100644 index 0000000..7c2a276 --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/scroll-container-demo/scroll-container-demo.component.scss @@ -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; + } +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/scroll-container-demo/scroll-container-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/scroll-container-demo/scroll-container-demo.component.ts new file mode 100644 index 0000000..ae3e918 --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/scroll-container-demo/scroll-container-demo.component.ts @@ -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: ` +
+

ScrollContainer Demo

+ + +
+

Basic Vertical Scrolling

+ +
+ @for (item of longContent; track item; let i = $index) { +

{{ i + 1 }}. {{ item }}

+ } +
+
+

Scroll events: Top reached {{ topCount }} times, Bottom reached {{ bottomCount }} times

+
+ + +
+

Direction Variants

+
+ +
+

Vertical

+ +
+ @for (item of shortContent; track item; let i = $index) { +

{{ i + 1 }}. {{ item }}

+ } +
+
+
+ + +
+

Horizontal

+ +
+ @for (item of horizontalItems; track item) { +
{{ item }}
+ } +
+
+
+ + +
+

Both Directions

+ +
+ + @for (row of tableData; track row.id) { + + + + + + + + } +
{{ row.id }}{{ row.name }}{{ row.description }}{{ row.value }}{{ row.status }}
+
+
+
+
+
+ + +
+

Scrollbar Visibility

+
+ @for (visibility of scrollbarOptions; track visibility) { +
+

{{ visibility | titlecase }}

+ +
+ @for (item of shortContent; track item; let i = $index) { +

{{ i + 1 }}. {{ item }}

+ } +
+
+
+ } +
+
+ + +
+

Virtual Scrolling

+
+

Virtual scrolling with {{ largeDataset.length }} items (only visible items are rendered)

+ + +
+
+ + +
+

Scroll Controls & Smooth Scrolling

+
+ + + + +
+ +
+ @for (item of longContent; track item; let i = $index) { +

{{ i + 1 }}. {{ item }}

+ } +
+
+
+ + +
+

Scroll Position Restoration

+

Scroll position is saved and restored automatically (uses sessionStorage)

+ +
+ @for (item of longContent; track item; let i = $index) { +

{{ i + 1 }}. {{ item }}

+ } +
+
+
+ + +
+

Interactive Example

+
+

Current scroll position: {{ currentScrollPosition.top }}px (top), {{ currentScrollPosition.left }}px (left)

+

Is scrolling: {{ isScrolling ? 'Yes' : 'No' }}

+
+ +
+
+ @for (item of gridData; track item.id) { +
+

Item {{ item.id }}

+

{{ item.content }}

+
+ } +
+
+
+
+
+ + + +
+ Item {{ index + 1 }}: {{ item.name }} - {{ item.description }} +
+
+ `, + styleUrl: './scroll-container-demo.component.scss' +}) +export class ScrollContainerDemoComponent { + @ViewChild('controllableScroll') controllableScroll!: ScrollContainerComponent; + @ViewChild('itemTemplate') itemTemplate!: TemplateRef; + + // 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; + } +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/section-demo/section-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/section-demo/section-demo.component.scss new file mode 100644 index 0000000..5e97cce --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/section-demo/section-demo.component.scss @@ -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; + } +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/section-demo/section-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/section-demo/section-demo.component.ts new file mode 100644 index 0000000..180a144 --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/section-demo/section-demo.component.ts @@ -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: ` +
+

Section Component Demo

+ + +
+

Spacing Variants

+ @for (spacing of spacings; track spacing) { +
+

{{ spacing }} spacing

+ +
+
Section with {{ spacing }} spacing
+

This section demonstrates the {{ spacing }} spacing variant with consistent vertical padding.

+
+
+
+ } +
+ + +
+

Background Variants

+ @for (background of backgrounds; track background) { +
+

{{ background }} background

+ +
+
{{ background | titlecase }} Background Section
+

This section demonstrates the {{ background }} background variant.

+
+
+
+ } +
+ + +
+

Text Alignment

+ @for (align of alignments; track align) { +
+

{{ align }} aligned

+ +
+
{{ align | titlecase }} Aligned Content
+

This section content is aligned to the {{ align }}.

+
+
+
+ } +
+ + +
+

Width Variants

+
+

Contained (default)

+ +
+
Contained Section
+

This section is contained with max-width and centered margins.

+
+
+
+ +
+

Full Width

+ +
+
Full Width Section
+

This section spans the full width of its container without constraints.

+
+
+
+
+ + +
+

Accessibility Features

+
+

With ARIA label

+ +
+
Accessible Section
+

This section includes proper ARIA labeling for screen readers.

+
+
+
+
+ + +
+

Real-world Examples

+ +
+

Hero Section

+ +
+

Welcome to Our Platform

+

This is a hero section with extra large spacing and centered content.

+ +
+
+
+ +
+

Content Section

+ +
+

About Our Services

+

This is a standard content section with large spacing and transparent background.

+
    +
  • Feature one with detailed description
  • +
  • Feature two with comprehensive details
  • +
  • Feature three with full specifications
  • +
+
+
+
+ +
+

Footer Section

+ +
+

Contact Information

+

This footer section spans full width with contained inner content.

+
+
+
+
+
+ `, + 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; +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/sidebar-layout-demo/sidebar-layout-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/sidebar-layout-demo/sidebar-layout-demo.component.scss new file mode 100644 index 0000000..b2ba8a9 --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/sidebar-layout-demo/sidebar-layout-demo.component.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/sidebar-layout-demo/sidebar-layout-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/sidebar-layout-demo/sidebar-layout-demo.component.ts new file mode 100644 index 0000000..aecbb78 --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/sidebar-layout-demo/sidebar-layout-demo.component.ts @@ -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: ` +
+

Sidebar Layout Demo

+ + +
+

Position Variants

+
+
+

Left Sidebar (Default)

+ +
+ +
+

Right Sidebar

+ +
+
+
+ + +
+

Sidebar Widths

+
+ @for (width of widths; track width) { +
+

{{ width.toUpperCase() }} Width

+ +
+ } +
+
+ + +
+

Visual Variants

+
+ @for (variant of variants; track variant) { +
+

{{ variant | titlecase }}

+ +
+ } +
+
+ + +
+

Collapse States

+
+
+

Collapsed Sidebar

+ +
+ +
+

Hidden Sidebar

+ +
+
+
+ + +
+

Interactive Controls

+
+ + + + + + + + + +
+ + +
+
+ `, + 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; + } +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/stack-demo/stack-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/stack-demo/stack-demo.component.scss new file mode 100644 index 0000000..766394d --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/stack-demo/stack-demo.component.scss @@ -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; +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/stack-demo/stack-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/stack-demo/stack-demo.component.ts new file mode 100644 index 0000000..4862ad9 --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/stack-demo/stack-demo.component.ts @@ -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: ` +
+

Stack Components Demo

+ + +
+

Basic Stack (Column)

+
+ +
Item 1
+
Item 2
+
Item 3
+
+
+
+ + +
+

Stack (Row)

+
+ +
Item 1
+
Item 2
+
Item 3
+
+
+
+ + +
+

VStack (Vertical Stack)

+
+ @for (spacing of spacings; track spacing) { +
+

{{ spacing }} spacing

+ +
Item A
+
Item B
+
Item C
+
+
+ } +
+
+ + +
+

HStack (Horizontal Stack)

+
+ @for (spacing of spacings; track spacing) { +
+

{{ spacing }} spacing

+ +
Item A
+
Item B
+
Item C
+
+
+ } +
+
+ + +
+

Alignment Options

+
+
+

VStack Alignment

+ @for (align of alignments; track align) { +
+
align="{{ align }}"
+ +
Small
+
Medium Item
+
Large Item Here
+
+
+ } +
+ +
+

HStack Alignment

+ @for (align of alignments; track align) { +
+
align="{{ align }}"
+ +
Small
+
Medium
Two lines
+
Large
Item
Three lines
+
+
+ } +
+
+
+ + +
+

Justify Options

+
+

HStack Justify (full width)

+ @for (justify of justifyOptions; track justify) { +
+
justify="{{ justify }}"
+ +
A
+
B
+
C
+
+
+ } +
+
+ + +
+

With Dividers

+
+
+

VStack with Dividers

+ +
Section One
+
Section Two
+
Section Three
+
+
+ +
+

HStack with Dividers

+ +
Left
+
Center
+
Right
+
+
+
+
+ + +
+

Responsive Behavior

+

The HStack below converts to a VStack on small screens:

+ +
Item 1
+
Item 2
+
Item 3
+
Item 4
+
+
+ + +
+

Nested Stacks

+ +
Header Section
+ + +
Left Column
+
Item 2
+
Item 3
+
+ +
Right Column
+
Item B
+
Item C
+
+
+ +
+
+
+ `, + 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; +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/supporting-pane-layout-demo/supporting-pane-layout-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/supporting-pane-layout-demo/supporting-pane-layout-demo.component.scss new file mode 100644 index 0000000..a110da8 --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/supporting-pane-layout-demo/supporting-pane-layout-demo.component.scss @@ -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; + } +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/supporting-pane-layout-demo/supporting-pane-layout-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/supporting-pane-layout-demo/supporting-pane-layout-demo.component.ts new file mode 100644 index 0000000..1ac1b2a --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/supporting-pane-layout-demo/supporting-pane-layout-demo.component.ts @@ -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: ` +
+

Supporting Pane Layout Demo

+

A layout component with main content and a collapsible supporting pane for additional information, tools, or navigation.

+ + +
+

Position Variants

+
+ +
+

Right Position (Default)

+
+ + + +
+

Main Content Area

+

This is the primary content area where your main application content would be displayed.

+
+
+

Analytics Dashboard

+

Key metrics and performance indicators would be shown here.

+
+
+

Recent Documents

+

A list of recently accessed or modified documents.

+
+
+
+ + +
+

Quick Settings

+
+
+ + Notifications +
+
+ + Profile Settings +
+
+ + Saved Items +
+
+
+ + +
+ Tools +
+
+
+
+ + +
+

Left Position

+
+ + + +
+

Main Content Area

+

Content area with supporting pane on the left side.

+
+
+

Messages

+

Recent messages and communications.

+
+
+
+ + +
+

Tags & Categories

+
+
Design
+
Development
+
Marketing
+
Research
+
+
+ +
+ Categories +
+
+
+
+
+
+ + +
+

Size Variants

+
+ +
+ +
+ + +
+

Size: {{ selectedSize.toUpperCase() }}

+

The supporting pane width changes based on the selected size variant.

+
+ +
+

Supporting Information

+

Pane size: {{ selectedSize }}

+
+
Current size: {{ selectedSize.toUpperCase() }}
+
Responsive: Yes
+
Collapsible: Yes
+
+
+ +
+ Size: {{ selectedSize.toUpperCase() }} +
+
+
+
+ + +
+

Visual Variants

+
+ +
+ +
+ + +
+

Variant: {{ selectedVariant | titlecase }}

+

Different visual styles for the supporting pane.

+
+
+

Visual Style

+

Current variant: {{ selectedVariant }}

+
+
+
+ +
+

Variant Demo

+
+
+ {{ selectedVariant | titlecase }} variant applied +
+

This demonstrates the {{ selectedVariant }} visual styling.

+
+
+ +
+ {{ selectedVariant | titlecase }} Style +
+
+
+
+ + +
+

Features & Options

+ +
+ + + +
+ +
+ + +
+

Features Demo

+

This demo shows various configuration options:

+
    +
  • Sticky Pane: {{ stickyPane ? 'Enabled' : 'Disabled' }}
  • +
  • Always Visible: {{ alwaysVisible ? 'Enabled' : 'Disabled' }}
  • +
  • Show Footer: {{ showFooter ? 'Enabled' : 'Disabled' }}
  • +
+ +
+
+

Content Block {{ i }}

+

This is example content to demonstrate scrolling behavior when sticky pane is enabled.

+
+
+
+ +
+

Configuration

+
+
+ Sticky: {{ stickyPane ? 'Yes' : 'No' }} +
+
+ Always Visible: {{ alwaysVisible ? 'Yes' : 'No' }} +
+
+ Footer: {{ showFooter ? 'Shown' : 'Hidden' }} +
+
+ Events: {{ eventCount }} triggered +
+
+
+ +
+ Options +
+ +
+ Last event: {{ lastEvent }} +
+
+
+
+
+ `, + 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'); + } +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/tabs-container-demo/tabs-container-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/tabs-container-demo/tabs-container-demo.component.scss new file mode 100644 index 0000000..b175ad0 --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/tabs-container-demo/tabs-container-demo.component.scss @@ -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; + } + } + } +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/tabs-container-demo/tabs-container-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/tabs-container-demo/tabs-container-demo.component.ts new file mode 100644 index 0000000..239fe49 --- /dev/null +++ b/projects/demo-ui-essentials/src/app/demos/tabs-container-demo/tabs-container-demo.component.ts @@ -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: ` +
+

Tabs Container Demo

+ + +
+

Sizes

+
+ @for (size of sizes; track size) { +
+

{{ size | titlecase }} Size

+ +
Content for {{ size }} tabs. Use tab selection to see different content.
+
+
+ } +
+
+ + +
+

Variants

+
+ @for (variant of variants; track variant) { +
+

{{ variant | titlecase }} Variant

+ +
{{ variant | titlecase }} variant tabs with different styling approaches.
+
+
+ } +
+
+ + +
+

Tab Positions

+
+ @for (position of positions; track position) { +
+

{{ position | titlecase }} Position

+ +
Tabs positioned {{ position }} with appropriate navigation layout.
+
+
+ } +
+
+ + +
+

Closeable Tabs

+
+ +
Closeable tabs demo - try closing tabs with the × button. Some tabs cannot be closed.
+
+

Close tabs by clicking the × button. Some tabs cannot be closed.

+
+
+ + +
+

Tabs with Icons

+
+ +
Tabs with icons provide visual context and better user experience.
+
+
+
+ + +
+

Scrollable Tabs

+
+ +
Scrollable tabs for when you have many tabs that exceed container width.
+
+

Use scroll controls when tabs overflow the container width.

+
+
+ + +
+

Reorderable Tabs

+
+ +
Drag and drop reorderable tabs - try dragging tabs to rearrange them.
+
+

Drag tabs to reorder them. Order changes: {{ reorderCount() }}

+
+
+ + +
+

Lazy Loading

+
+ +
Lazy loading tabs optimize performance by loading content only when needed.
+
+

Lazy tabs only load content when first selected.

+
+
+ + +
+

Disabled States

+
+ +
Disabled tabs prevent user interaction while maintaining visual hierarchy.
+
+

Disabled tabs cannot be selected or interacted with.

+
+
+ + +
+

Interactive Example

+
+
+ + + + + +
+ + +
Interactive demo with configurable options - use controls above to customize behavior.
+
+ +
+

Current tab: {{ activeTab() || 'None' }}

+

Tab selections: {{ tabSelectCount() }}

+
+
+
+
+ `, + 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(''); + 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([ + { 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([ + { id: 'order1', label: 'First' }, + { id: 'order2', label: 'Second' }, + { id: 'order3', label: 'Third' }, + { id: 'order4', label: 'Fourth' } + ]); + + private _interactiveTabs = signal([ + { 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 + }); + } +} \ No newline at end of file diff --git a/projects/demo-ui-essentials/src/app/demos/tree-view-demo/tree-view-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/tree-view-demo/tree-view-demo.component.scss index 44252da..cb4a360 100644 --- a/projects/demo-ui-essentials/src/app/demos/tree-view-demo/tree-view-demo.component.scss +++ b/projects/demo-ui-essentials/src/app/demos/tree-view-demo/tree-view-demo.component.scss @@ -1,4 +1,4 @@ -@use '../../../../../ui-design-system/src/styles/semantic/index' as *; +@use '../../../../../ui-design-system/src/styles/semantic' as *; .demo-container { padding: $semantic-spacing-component-lg; diff --git a/projects/demo-ui-essentials/src/app/demos/tree-view-demo/tree-view-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/tree-view-demo/tree-view-demo.component.ts index 64a6e52..6663933 100644 --- a/projects/demo-ui-essentials/src/app/demos/tree-view-demo/tree-view-demo.component.ts +++ b/projects/demo-ui-essentials/src/app/demos/tree-view-demo/tree-view-demo.component.ts @@ -1,7 +1,6 @@ import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { TreeViewComponent } from "../../../../../ui-essentials/src/public-api"; -import { TreeNode } from '../../../../../../dist/ui-essentials/lib/components/data-display/tree-view/tree-view.component'; +import { TreeNode, TreeViewComponent } from "../../../../../ui-essentials/src/public-api"; @Component({ selector: 'ui-tree-view-demo', diff --git a/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.component.ts b/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.component.ts index 035e616..d1efb3e 100644 --- a/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.component.ts +++ b/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.component.ts @@ -4,16 +4,17 @@ import { DashboardSidebarComponent, SidebarMenuItem } from "./dashboard.sidebar. import { LayoutContainerComponent } from "../../shared/layout-containers/layout-container.component"; import { faWindowMaximize, faUserCircle, faCertificate, faHandPointer, faIdCard, faTags, - faCheckSquare, faKeyboard, faThLarge, faList, faDotCircle, faSearch, faToggleOn, + faCheckSquare, faKeyboard, faThLarge, faList, faDotCircle, faSearch, faToggleOn, faNewspaper, faTable, faImage, faImages, faPlay, faBars, faEdit, faEye, faCompass, faVideo, faComment, faMousePointer, faLayerGroup, faSquare, faCalendarDays, faClock, faGripVertical, faArrowsAlt, faBoxOpen, faChevronLeft, faSpinner, faExclamationTriangle, faCloudUploadAlt, faFileText, faListAlt, faCircle, faExpandArrowsAlt, faCircleNotch, faSliders, faMinus, faInfoCircle, faChevronDown, faCaretUp, faExclamationCircle, faSitemap, faStream, - faBell, faRoute, faChevronUp, faEllipsisV, faCut, faPalette, faExchangeAlt, faTools, faHeart + faBell, faRoute, faChevronUp, faEllipsisV, faCut, faPalette, faExchangeAlt, faTools, faHeart, faCircleDot, faColumns, + faFolderOpen, faDesktop, faListUl } from '@fortawesome/free-solid-svg-icons'; import { DemoRoutes } from '../../demos'; -import { ScrollContainerComponent } from '../../../../../ui-essentials/src/lib/layouts/scroll-container.component'; +import { ScrollContainerComponent } from "../../../../../ui-essentials/src/public-api"; // import { DemoRoutes } from "../../../../../ui-essentials/src/public-api"; @Component({ @@ -23,8 +24,8 @@ import { ScrollContainerComponent } from '../../../../../ui-essentials/src/lib/l CommonModule, DashboardSidebarComponent, LayoutContainerComponent, - ScrollContainerComponent, - DemoRoutes + DemoRoutes, + ScrollContainerComponent ], template: ` @@ -108,6 +109,12 @@ export class DashboardComponent { faExchangeAlt = faExchangeAlt; faTools = faTools; faHeart = faHeart; + faCircleDot = faCircleDot; + faColumns = faColumns; + faFolderOpen = faFolderOpen; + faDesktop = faDesktop; + faNewspaper = faNewspaper; + faListUl = faListUl; menuItems: any = [] @@ -219,10 +226,26 @@ export class DashboardComponent { // Layout category const layoutChildren = [ - this.createChildItem("layout", "Layout Demos", this.faThLarge), + this.createChildItem("aspect-ratio", "Aspect Ratio", this.faExpandArrowsAlt), + this.createChildItem("bento-grid", "Bento Grid", this.faThLarge), + this.createChildItem("box", "Box (Padding/Margin)", this.faSquare), + this.createChildItem("breakpoint-container", "Breakpoint Container", this.faEye), + this.createChildItem("center", "Center (Content Alignment)", this.faCircleDot), + this.createChildItem("column", "Column Layout", this.faColumns), + this.createChildItem("container", "Container", this.faBoxOpen), + this.createChildItem("feed-layout", "Feed Layout", this.faNewspaper), + this.createChildItem("flex", "Flex (Flexbox Container)", this.faArrowsAlt), this.createChildItem("grid-system", "Grid System", this.faGripVertical), + this.createChildItem("grid-container", "Grid Container", this.faThLarge), + this.createChildItem("list-detail-layout", "List Detail Layout", this.faListUl), + this.createChildItem("scroll-container", "Scroll Container", this.faListAlt), + this.createChildItem("supporting-pane-layout", "Supporting Pane Layout", this.faWindowMaximize), + this.createChildItem("section", "Section (Semantic Wrapper)", this.faLayerGroup), + this.createChildItem("sidebar-layout", "Sidebar Layout", this.faBars), this.createChildItem("spacer", "Spacer", this.faArrowsAlt), - this.createChildItem("container", "Container", this.faBoxOpen) + this.createChildItem("stack", "Stack (VStack/HStack)", this.faStream), + this.createChildItem("tabs-container", "Tabs Container", this.faFolderOpen), + this.createChildItem("dashboard-shell", "Dashboard Shell", this.faDesktop) ]; this.addMenuItem("layouts", "Layouts", this.faLayerGroup, layoutChildren); diff --git a/projects/demo-ui-essentials/src/scss/_variables.scss b/projects/demo-ui-essentials/src/scss/_variables.scss index e731d61..819fae2 100644 --- a/projects/demo-ui-essentials/src/scss/_variables.scss +++ b/projects/demo-ui-essentials/src/scss/_variables.scss @@ -13,7 +13,7 @@ $base-typography-font-family-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif; $base-typography-font-family-display: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif; -@import '../../../shared-ui/src/styles/semantic'; +@import '../../../ui-design-system/src/styles/semantic'; // These overrides will cascade through all semantic typography tokens // that use $base-typography-font-family-sans and $base-typography-font-family-display diff --git a/projects/demo-ui-essentials/src/styles.scss b/projects/demo-ui-essentials/src/styles.scss index 5f2f657..3df943c 100644 --- a/projects/demo-ui-essentials/src/styles.scss +++ b/projects/demo-ui-essentials/src/styles.scss @@ -5,8 +5,8 @@ $inter-font-path: "/fonts/inter/" !default; $comfortaa-font-path: "/fonts/comfortaa/" !default; // Import specific fonts we need for the demo -@import '../../shared-ui/src/styles/fontfaces/inter'; -@import '../../shared-ui/src/styles/fontfaces/comfortaa'; +@import '../../ui-design-system/src/styles/fontfaces/inter'; +@import '../../ui-design-system/src/styles/fontfaces/comfortaa'; // Import project variables (which now has semantic tokens available) @import 'scss/_variables'; diff --git a/projects/ui-essentials/src/lib/components/data-display/index.ts b/projects/ui-essentials/src/lib/components/data-display/index.ts index 89834f2..0512996 100644 --- a/projects/ui-essentials/src/lib/components/data-display/index.ts +++ b/projects/ui-essentials/src/lib/components/data-display/index.ts @@ -4,7 +4,6 @@ export * from './badge'; export * from './card'; export * from './carousel'; export * from './chip'; -export * from './divider'; export * from './image-container'; export * from './list'; export * from './progress'; diff --git a/projects/ui-essentials/src/lib/components/layout/aspect-ratio/aspect-ratio.component.scss b/projects/ui-essentials/src/lib/components/layout/aspect-ratio/aspect-ratio.component.scss new file mode 100644 index 0000000..bac1348 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/aspect-ratio/aspect-ratio.component.scss @@ -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); } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/aspect-ratio/aspect-ratio.component.ts b/projects/ui-essentials/src/lib/components/layout/aspect-ratio/aspect-ratio.component.ts new file mode 100644 index 0000000..ac5ed7d --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/aspect-ratio/aspect-ratio.component.ts @@ -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: ` +
+ +
+ @if (loading) { + + } @else { + + } +
+
+ `, + 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(); + + get interactive(): boolean { + return this.variant === 'interactive'; + } + + getClasses(): Record { + const classes: Record = { + '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); + } + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/aspect-ratio/index.ts b/projects/ui-essentials/src/lib/components/layout/aspect-ratio/index.ts new file mode 100644 index 0000000..b4cbf1e --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/aspect-ratio/index.ts @@ -0,0 +1 @@ +export * from './aspect-ratio.component'; \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/bento-grid/bento-grid-item.component.ts b/projects/ui-essentials/src/lib/components/layout/bento-grid/bento-grid-item.component.ts new file mode 100644 index 0000000..7e74bc7 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/bento-grid/bento-grid-item.component.ts @@ -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: ` +
+ + @if (header) { +
+ {{ header }} +
+ } + +
+ +
+
+ `, + 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(); + + 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); + } + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/bento-grid/bento-grid.component.scss b/projects/ui-essentials/src/lib/components/layout/bento-grid/bento-grid.component.scss new file mode 100644 index 0000000..ccd6fdf --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/bento-grid/bento-grid.component.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/bento-grid/bento-grid.component.ts b/projects/ui-essentials/src/lib/components/layout/bento-grid/bento-grid.component.ts new file mode 100644 index 0000000..f8454a2 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/bento-grid/bento-grid.component.ts @@ -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: ` +
+ + +
+ `, + 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'; +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/bento-grid/index.ts b/projects/ui-essentials/src/lib/components/layout/bento-grid/index.ts new file mode 100644 index 0000000..24ba511 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/bento-grid/index.ts @@ -0,0 +1,2 @@ +export * from './bento-grid.component'; +export * from './bento-grid-item.component'; \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/box/box.component.scss b/projects/ui-essentials/src/lib/components/layout/box/box.component.scss new file mode 100644 index 0000000..9699d18 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/box/box.component.scss @@ -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; + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/box/box.component.ts b/projects/ui-essentials/src/lib/components/layout/box/box.component.ts new file mode 100644 index 0000000..8d6d9ba --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/box/box.component.ts @@ -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: ` +
+ + +
+ `, + 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 { + const classes: Record = { + '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; + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/box/index.ts b/projects/ui-essentials/src/lib/components/layout/box/index.ts new file mode 100644 index 0000000..bff941c --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/box/index.ts @@ -0,0 +1 @@ +export * from './box.component'; \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/breakpoint-container/breakpoint-container.component.scss b/projects/ui-essentials/src/lib/components/layout/breakpoint-container/breakpoint-container.component.scss new file mode 100644 index 0000000..93507b7 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/breakpoint-container/breakpoint-container.component.scss @@ -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; + } + } + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/breakpoint-container/breakpoint-container.component.ts b/projects/ui-essentials/src/lib/components/layout/breakpoint-container/breakpoint-container.component.ts new file mode 100644 index 0000000..d7a5374 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/breakpoint-container/breakpoint-container.component.ts @@ -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: ` +
+ + +
+ `, + 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 { + 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; + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/breakpoint-container/index.ts b/projects/ui-essentials/src/lib/components/layout/breakpoint-container/index.ts new file mode 100644 index 0000000..c4721c7 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/breakpoint-container/index.ts @@ -0,0 +1 @@ +export * from './breakpoint-container.component'; \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/center/center.component.scss b/projects/ui-essentials/src/lib/components/layout/center/center.component.scss new file mode 100644 index 0000000..d572152 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/center/center.component.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/center/center.component.ts b/projects/ui-essentials/src/lib/components/layout/center/center.component.ts new file mode 100644 index 0000000..fafdc00 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/center/center.component.ts @@ -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: ` +
+ + +
+ `, + 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 { + const classes: Record = { + '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; + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/center/index.ts b/projects/ui-essentials/src/lib/components/layout/center/index.ts new file mode 100644 index 0000000..4bbea30 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/center/index.ts @@ -0,0 +1 @@ +export * from './center.component'; \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/column/column.component.scss b/projects/ui-essentials/src/lib/components/layout/column/column.component.scss new file mode 100644 index 0000000..78f0835 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/column/column.component.scss @@ -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; + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/column/column.component.ts b/projects/ui-essentials/src/lib/components/layout/column/column.component.ts new file mode 100644 index 0000000..40bacf7 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/column/column.component.ts @@ -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: ` +
+ +
+ `, + 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 { + const classes: Record = { + '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; + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/column/index.ts b/projects/ui-essentials/src/lib/components/layout/column/index.ts new file mode 100644 index 0000000..21e2b69 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/column/index.ts @@ -0,0 +1 @@ +export * from './column.component'; \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/dashboard-shell/dashboard-shell.component.scss b/projects/ui-essentials/src/lib/components/layout/dashboard-shell/dashboard-shell.component.scss new file mode 100644 index 0000000..3769752 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/dashboard-shell/dashboard-shell.component.scss @@ -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; + } + } + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/dashboard-shell/dashboard-shell.component.ts b/projects/ui-essentials/src/lib/components/layout/dashboard-shell/dashboard-shell.component.ts new file mode 100644 index 0000000..88702dc --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/dashboard-shell/dashboard-shell.component.ts @@ -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: ` +
+ + + @if (mobileMenuOpen) { + + } + + + + + +
+ + + + + +
+ + + @if (showBreadcrumbs) { + + } + + + @if (showNotifications) { +
+ +
+ } + + +
+ +
+
+
+ + + @if (showFooter) { +
+ + +
+ } +
+ `, + 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(); + @Output() mobileMenuToggled = new EventEmitter(); + @Output() mobileBackdropClicked = new EventEmitter(); + + 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); + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/dashboard-shell/index.ts b/projects/ui-essentials/src/lib/components/layout/dashboard-shell/index.ts new file mode 100644 index 0000000..8f52f9f --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/dashboard-shell/index.ts @@ -0,0 +1 @@ +export { DashboardShellComponent } from './dashboard-shell.component'; \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/data-display/divider/divider.component.scss b/projects/ui-essentials/src/lib/components/layout/divider/divider.component.scss similarity index 100% rename from projects/ui-essentials/src/lib/components/data-display/divider/divider.component.scss rename to projects/ui-essentials/src/lib/components/layout/divider/divider.component.scss diff --git a/projects/ui-essentials/src/lib/components/data-display/divider/divider.component.ts b/projects/ui-essentials/src/lib/components/layout/divider/divider.component.ts similarity index 100% rename from projects/ui-essentials/src/lib/components/data-display/divider/divider.component.ts rename to projects/ui-essentials/src/lib/components/layout/divider/divider.component.ts diff --git a/projects/ui-essentials/src/lib/components/data-display/divider/index.ts b/projects/ui-essentials/src/lib/components/layout/divider/index.ts similarity index 100% rename from projects/ui-essentials/src/lib/components/data-display/divider/index.ts rename to projects/ui-essentials/src/lib/components/layout/divider/index.ts diff --git a/projects/ui-essentials/src/lib/components/layout/feed-layout/feed-layout.component.scss b/projects/ui-essentials/src/lib/components/layout/feed-layout/feed-layout.component.scss new file mode 100644 index 0000000..a68be06 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/feed-layout/feed-layout.component.scss @@ -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; + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/feed-layout/feed-layout.component.ts b/projects/ui-essentials/src/lib/components/layout/feed-layout/feed-layout.component.ts new file mode 100644 index 0000000..edb932f --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/feed-layout/feed-layout.component.ts @@ -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: ` +
+ + @if (enableRefresh && isRefreshing()) { + + } + +
+ + + + @if (loading && !isRefreshing()) { +
+ + Loading more content... +
+ } + + @if (hasError) { + + } + + @if (isEmpty && !loading) { +
+ + No content available + +
+ } +
+ + @if (enableRefresh) { +
+
+ } +
+ `, + 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(); + @Output() refresh = new EventEmitter(); + @Output() retry = new EventEmitter(); + @Output() scrolled = new EventEmitter(); + + @ViewChild('scrollContainer', { static: true }) scrollContainer!: ElementRef; + + 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' + }); + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/feed-layout/index.ts b/projects/ui-essentials/src/lib/components/layout/feed-layout/index.ts new file mode 100644 index 0000000..d5c4306 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/feed-layout/index.ts @@ -0,0 +1 @@ +export * from './feed-layout.component'; \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/flex/flex.component.scss b/projects/ui-essentials/src/lib/components/layout/flex/flex.component.scss new file mode 100644 index 0000000..e5f838f --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/flex/flex.component.scss @@ -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%; + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/flex/flex.component.ts b/projects/ui-essentials/src/lib/components/layout/flex/flex.component.ts new file mode 100644 index 0000000..b88a187 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/flex/flex.component.ts @@ -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: ` +
+ +
+ `, + 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 { + const classes: Record = { + '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; + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/flex/index.ts b/projects/ui-essentials/src/lib/components/layout/flex/index.ts new file mode 100644 index 0000000..25ae054 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/flex/index.ts @@ -0,0 +1 @@ +export * from './flex.component'; \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/grid-container/grid-container.component.scss b/projects/ui-essentials/src/lib/components/layout/grid-container/grid-container.component.scss new file mode 100644 index 0000000..7afa28c --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/grid-container/grid-container.component.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/grid-container/grid-container.component.ts b/projects/ui-essentials/src/lib/components/layout/grid-container/grid-container.component.ts new file mode 100644 index 0000000..1daed06 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/grid-container/grid-container.component.ts @@ -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: ` +
+ + +
+ `, + 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; +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/grid-container/index.ts b/projects/ui-essentials/src/lib/components/layout/grid-container/index.ts new file mode 100644 index 0000000..774633f --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/grid-container/index.ts @@ -0,0 +1 @@ +export * from './grid-container.component'; \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/hstack/hstack.component.scss b/projects/ui-essentials/src/lib/components/layout/hstack/hstack.component.scss new file mode 100644 index 0000000..124a950 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/hstack/hstack.component.scss @@ -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; + } + } + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/hstack/hstack.component.ts b/projects/ui-essentials/src/lib/components/layout/hstack/hstack.component.ts new file mode 100644 index 0000000..e7a47a1 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/hstack/hstack.component.ts @@ -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: ` +
+ + +
+ `, + 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 { + const classes: Record = { + '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; + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/hstack/index.ts b/projects/ui-essentials/src/lib/components/layout/hstack/index.ts new file mode 100644 index 0000000..348bab5 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/hstack/index.ts @@ -0,0 +1 @@ +export * from './hstack.component'; \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/index.ts b/projects/ui-essentials/src/lib/components/layout/index.ts index 8c71929..93996cc 100644 --- a/projects/ui-essentials/src/lib/components/layout/index.ts +++ b/projects/ui-essentials/src/lib/components/layout/index.ts @@ -1,3 +1,23 @@ +export * from './aspect-ratio'; +export * from './bento-grid'; +export * from './box'; +export * from './breakpoint-container'; +export * from './center'; +export * from './column'; export * from './container'; +export * from './dashboard-shell'; +export * from './divider'; +export * from './feed-layout'; +export * from './flex'; +export * from './grid-container'; export * from './grid-system'; -export * from './spacer'; \ No newline at end of file +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'; \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/list-detail-layout/index.ts b/projects/ui-essentials/src/lib/components/layout/list-detail-layout/index.ts new file mode 100644 index 0000000..6f37786 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/list-detail-layout/index.ts @@ -0,0 +1 @@ +export * from './list-detail-layout.component'; \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/list-detail-layout/list-detail-layout.component.scss b/projects/ui-essentials/src/lib/components/layout/list-detail-layout/list-detail-layout.component.scss new file mode 100644 index 0000000..d18bd13 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/list-detail-layout/list-detail-layout.component.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/list-detail-layout/list-detail-layout.component.ts b/projects/ui-essentials/src/lib/components/layout/list-detail-layout/list-detail-layout.component.ts new file mode 100644 index 0000000..1ceea04 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/list-detail-layout/list-detail-layout.component.ts @@ -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: ` +
+ + +
+ + + + {{ currentView() === 'list' ? listTitle : detailTitle }} + + + +
+ + +
+ + + + +
+ + +
+ + @if (!hasSelection() && showEmptyState) { +
+ + {{ emptyStateText }} + +
+ } @else { + + } +
+
+ `, + 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(null); + + // Mobile view state + @Input() + set mobileView(value: MobileView) { + this._currentView.set(value); + } + get mobileView() { + return this._currentView(); + } + private _currentView = signal('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(); + @Output() mobileNavigated = new EventEmitter(); + @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'); + } + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/scroll-container/index.ts b/projects/ui-essentials/src/lib/components/layout/scroll-container/index.ts new file mode 100644 index 0000000..8418f2d --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/scroll-container/index.ts @@ -0,0 +1 @@ +export * from './scroll-container.component'; \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/scroll-container/scroll-container.component.scss b/projects/ui-essentials/src/lib/components/layout/scroll-container/scroll-container.component.scss new file mode 100644 index 0000000..a71feac --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/scroll-container/scroll-container.component.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/scroll-container/scroll-container.component.ts b/projects/ui-essentials/src/lib/components/layout/scroll-container/scroll-container.component.ts new file mode 100644 index 0000000..58d7f95 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/scroll-container/scroll-container.component.ts @@ -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: ` +
+ + @if (virtualScrollConfig?.enabled && items && items.length > 0) { + +
+
+ @for (item of visibleItems; track trackByFn ? trackByFn(item) : item) { +
+ +
+ } +
+
+ } @else { + +
+ +
+ } + + @if (showScrollIndicators) { +
+ @if (canScrollUp) { + + } + @if (canScrollDown) { + + } +
+ } +
+ `, + styleUrl: './scroll-container.component.scss' +}) +export class ScrollContainerComponent implements AfterViewInit, OnDestroy { + @ViewChild('scrollContainer', { static: true }) scrollContainer!: ElementRef; + + // 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(); + @Output() scrollStart = new EventEmitter(); + @Output() scrollEnd = new EventEmitter(); + @Output() reachedTop = new EventEmitter(); + @Output() reachedBottom = new EventEmitter(); + + // 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): 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; + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/section/index.ts b/projects/ui-essentials/src/lib/components/layout/section/index.ts new file mode 100644 index 0000000..cf19dc4 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/section/index.ts @@ -0,0 +1 @@ +export * from './section.component'; \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/section/section.component.scss b/projects/ui-essentials/src/lib/components/layout/section/section.component.scss new file mode 100644 index 0000000..cd0aa6c --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/section/section.component.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/section/section.component.ts b/projects/ui-essentials/src/lib/components/layout/section/section.component.ts new file mode 100644 index 0000000..1745dd3 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/section/section.component.ts @@ -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: ` +
+ + +
+ `, + 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 { + const classes: Record = { + '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; + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/sidebar-layout/index.ts b/projects/ui-essentials/src/lib/components/layout/sidebar-layout/index.ts new file mode 100644 index 0000000..622d712 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/sidebar-layout/index.ts @@ -0,0 +1 @@ +export * from './sidebar-layout.component'; \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/sidebar-layout/sidebar-layout.component.scss b/projects/ui-essentials/src/lib/components/layout/sidebar-layout/sidebar-layout.component.scss new file mode 100644 index 0000000..efc38df --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/sidebar-layout/sidebar-layout.component.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/sidebar-layout/sidebar-layout.component.ts b/projects/ui-essentials/src/lib/components/layout/sidebar-layout/sidebar-layout.component.ts new file mode 100644 index 0000000..a997304 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/sidebar-layout/sidebar-layout.component.ts @@ -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: ` +
+ + + + @if (collapseMode === 'overlay' && !collapsed) { + + } + +
+ + +
+
+ `, + 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'); + } + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/stack/index.ts b/projects/ui-essentials/src/lib/components/layout/stack/index.ts new file mode 100644 index 0000000..d8c4b26 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/stack/index.ts @@ -0,0 +1 @@ +export * from './stack.component'; \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/stack/stack.component.scss b/projects/ui-essentials/src/lib/components/layout/stack/stack.component.scss new file mode 100644 index 0000000..5305ffd --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/stack/stack.component.scss @@ -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; + } + } + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/stack/stack.component.ts b/projects/ui-essentials/src/lib/components/layout/stack/stack.component.ts new file mode 100644 index 0000000..095d1ea --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/stack/stack.component.ts @@ -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: ` +
+ + +
+ `, + 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 { + const classes: Record = { + '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; + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/supporting-pane-layout/index.ts b/projects/ui-essentials/src/lib/components/layout/supporting-pane-layout/index.ts new file mode 100644 index 0000000..566e68f --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/supporting-pane-layout/index.ts @@ -0,0 +1 @@ +export * from './supporting-pane-layout.component'; \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/supporting-pane-layout/supporting-pane-layout.component.scss b/projects/ui-essentials/src/lib/components/layout/supporting-pane-layout/supporting-pane-layout.component.scss new file mode 100644 index 0000000..d0c5222 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/supporting-pane-layout/supporting-pane-layout.component.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/supporting-pane-layout/supporting-pane-layout.component.ts b/projects/ui-essentials/src/lib/components/layout/supporting-pane-layout/supporting-pane-layout.component.ts new file mode 100644 index 0000000..9aa6830 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/supporting-pane-layout/supporting-pane-layout.component.ts @@ -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: ` +
+ + +
+ + +
+ + + +
+ `, + 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(); + @Output() paneCollapsed = new EventEmitter(); + @Output() paneExpanded = new EventEmitter(); + + // 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; + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/tabs-container/index.ts b/projects/ui-essentials/src/lib/components/layout/tabs-container/index.ts new file mode 100644 index 0000000..eee69b4 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/tabs-container/index.ts @@ -0,0 +1 @@ +export * from './tabs-container.component'; \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/tabs-container/tabs-container.component.scss b/projects/ui-essentials/src/lib/components/layout/tabs-container/tabs-container.component.scss new file mode 100644 index 0000000..6183ac7 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/tabs-container/tabs-container.component.scss @@ -0,0 +1,404 @@ +@use '../../../../../../ui-design-system/src/styles/semantic/index' as *; + +.ui-tabs-container { + display: flex; + position: relative; + width: 100%; + + // Layout variants based on position + &--position-top, + &--position-bottom { + flex-direction: column; + } + + &--position-left, + &--position-right { + flex-direction: row; + } + + &--position-bottom { + flex-direction: column-reverse; + } + + &--position-right { + flex-direction: row-reverse; + } + + // Tab Navigation + &__nav { + position: relative; + display: flex; + align-items: center; + background: $semantic-color-surface; + border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle; + + .ui-tabs-container--position-bottom & { + border-bottom: none; + border-top: $semantic-border-width-1 solid $semantic-color-border-subtle; + } + + .ui-tabs-container--position-left & { + border-bottom: none; + border-right: $semantic-border-width-1 solid $semantic-color-border-subtle; + flex-direction: column; + align-items: stretch; + } + + .ui-tabs-container--position-right & { + border-bottom: none; + border-left: $semantic-border-width-1 solid $semantic-color-border-subtle; + flex-direction: column; + align-items: stretch; + } + } + + &__nav-content { + display: flex; + flex: 1; + overflow-x: auto; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } + + .ui-tabs-container--position-left &, + .ui-tabs-container--position-right & { + flex-direction: column; + overflow-x: visible; + overflow-y: auto; + } + + .ui-tabs-container--scrollable & { + scroll-behavior: smooth; + } + + &--dragging { + pointer-events: none; + + .ui-tabs-container__tab { + pointer-events: auto; + } + } + } + + // Individual Tab Button + &__tab { + display: flex; + align-items: center; + gap: $semantic-spacing-component-xs; + position: relative; + background: transparent; + border: none; + 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); + color: $semantic-color-text-secondary; + transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease; + white-space: nowrap; + user-select: none; + + // Size variants + .ui-tabs-container--size-sm & { + padding: $semantic-spacing-component-xs $semantic-spacing-component-sm; + min-height: $semantic-sizing-button-height-sm; + font-family: map-get($semantic-typography-button-small, font-family); + font-size: map-get($semantic-typography-button-small, font-size); + font-weight: map-get($semantic-typography-button-small, font-weight); + line-height: map-get($semantic-typography-button-small, line-height); + } + + .ui-tabs-container--size-md & { + padding: $semantic-spacing-component-sm $semantic-spacing-component-md; + min-height: $semantic-sizing-button-height-md; + } + + .ui-tabs-container--size-lg & { + padding: $semantic-spacing-component-md $semantic-spacing-component-lg; + min-height: $semantic-sizing-button-height-lg; + font-family: map-get($semantic-typography-button-large, font-family); + font-size: map-get($semantic-typography-button-large, font-size); + font-weight: map-get($semantic-typography-button-large, font-weight); + line-height: map-get($semantic-typography-button-large, line-height); + } + + // Variant styles + .ui-tabs-container--variant-default & { + border-bottom: 2px solid transparent; + + &--active { + color: $semantic-color-primary; + border-bottom-color: $semantic-color-primary; + } + } + + .ui-tabs-container--variant-filled & { + border-radius: $semantic-border-radius-sm; + margin: 0 $semantic-spacing-component-xs; + + &--active { + background: $semantic-color-primary; + color: $semantic-color-on-primary; + } + } + + .ui-tabs-container--variant-pills & { + border-radius: $semantic-border-radius-full; + margin: 0 $semantic-spacing-component-xs; + + &--active { + background: $semantic-color-primary; + color: $semantic-color-on-primary; + } + } + + .ui-tabs-container--variant-underlined & { + border-radius: 0; + border-bottom: 2px solid transparent; + + &--active { + color: $semantic-color-primary; + border-bottom-color: $semantic-color-primary; + background: $semantic-color-surface-elevated; + } + } + + // Position-specific styles + .ui-tabs-container--position-left &, + .ui-tabs-container--position-right & { + justify-content: flex-start; + width: 100%; + text-align: left; + + .ui-tabs-container--variant-default & { + border-bottom: none; + border-right: 2px solid transparent; + + &--active { + border-right-color: $semantic-color-primary; + } + } + + .ui-tabs-container--position-right & { + .ui-tabs-container--variant-default & { + border-right: none; + border-left: 2px solid transparent; + + &--active { + border-left-color: $semantic-color-primary; + } + } + } + } + + // Interactive states + &:hover:not(&--disabled):not(&--active) { + color: $semantic-color-text-primary; + background: $semantic-color-surface-secondary; + } + + &:focus-visible { + outline: 2px solid $semantic-color-focus; + outline-offset: 2px; + } + + &--active { + color: $semantic-color-primary; + font-weight: $semantic-typography-font-weight-semibold; + + .ui-tabs-container--variant-filled &, + .ui-tabs-container--variant-pills & { + background: $semantic-color-primary; + color: $semantic-color-on-primary; + + &:hover { + background: $semantic-color-primary; + } + } + } + + &--disabled { + color: $semantic-color-text-disabled; + cursor: not-allowed; + opacity: $semantic-opacity-disabled; + + &:hover { + background: transparent; + color: $semantic-color-text-disabled; + } + } + + // Draggable state + &[draggable="true"]:not(&--disabled) { + cursor: grab; + + &:active { + cursor: grabbing; + } + } + } + + // Tab elements + &__tab-icon { + display: flex; + align-items: center; + font-size: $semantic-sizing-icon-inline; + } + + &__tab-label { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + } + + &__tab-close { + display: flex; + align-items: center; + justify-content: center; + width: $semantic-sizing-touch-minimum; + height: $semantic-sizing-touch-minimum; + border: none; + background: transparent; + border-radius: $semantic-border-radius-sm; + color: inherit; + cursor: pointer; + opacity: $semantic-opacity-subtle; + transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease; + font-size: $semantic-typography-font-size-lg; + line-height: 1; + + &:hover { + opacity: 1; + background: $semantic-color-surface-secondary; + color: $semantic-color-danger; + } + + &:focus-visible { + outline: 2px solid $semantic-color-focus; + outline-offset: 1px; + } + } + + // Scroll Controls + &__scroll-controls { + display: flex; + gap: $semantic-spacing-component-xs; + padding: 0 $semantic-spacing-component-sm; + border-left: $semantic-border-width-1 solid $semantic-color-border-subtle; + + .ui-tabs-container--position-left &, + .ui-tabs-container--position-right & { + flex-direction: column; + border-left: none; + border-top: $semantic-border-width-1 solid $semantic-color-border-subtle; + padding: $semantic-spacing-component-sm 0; + } + } + + &__scroll-button { + display: flex; + align-items: center; + justify-content: center; + width: $semantic-sizing-button-height-sm; + height: $semantic-sizing-button-height-sm; + border: $semantic-border-width-1 solid $semantic-color-border-primary; + background: $semantic-color-surface; + border-radius: $semantic-border-radius-sm; + color: $semantic-color-text-primary; + cursor: pointer; + font-size: $semantic-typography-font-size-md; + line-height: 1; + transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease; + + &:hover:not(:disabled) { + background: $semantic-color-surface-secondary; + border-color: $semantic-color-border-secondary; + } + + &:focus-visible { + outline: 2px solid $semantic-color-focus; + outline-offset: 2px; + } + + &:disabled { + color: $semantic-color-text-disabled; + border-color: $semantic-color-border-subtle; + cursor: not-allowed; + opacity: $semantic-opacity-disabled; + } + } + + // Content Area + &__content { + flex: 1; + position: relative; + min-height: 0; + } + + &__panel { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + padding: $semantic-spacing-component-lg; + background: $semantic-color-surface; + opacity: 0; + visibility: hidden; + transition: opacity $semantic-motion-duration-fast $semantic-motion-easing-ease; + overflow: auto; + + &--active { + opacity: 1; + visibility: visible; + position: relative; + top: auto; + left: auto; + right: auto; + bottom: auto; + } + } + + // Responsive adjustments + @media (max-width: $semantic-breakpoint-md - 1) { + &__tab { + .ui-tabs-container--size-lg & { + padding: $semantic-spacing-component-sm $semantic-spacing-component-md; + min-height: $semantic-sizing-button-height-md; + 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); + } + } + + &__panel { + padding: $semantic-spacing-component-md; + } + } + + @media (max-width: $semantic-breakpoint-sm - 1) { + &__tab { + .ui-tabs-container--size-md &, + .ui-tabs-container--size-lg & { + padding: $semantic-spacing-component-xs $semantic-spacing-component-sm; + min-height: $semantic-sizing-button-height-sm; + font-family: map-get($semantic-typography-button-small, font-family); + font-size: map-get($semantic-typography-button-small, font-size); + font-weight: map-get($semantic-typography-button-small, font-weight); + line-height: map-get($semantic-typography-button-small, line-height); + } + } + + &__panel { + padding: $semantic-spacing-component-sm; + } + + &__scroll-controls { + padding: 0 $semantic-spacing-component-xs; + } + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/tabs-container/tabs-container.component.ts b/projects/ui-essentials/src/lib/components/layout/tabs-container/tabs-container.component.ts new file mode 100644 index 0000000..85ac006 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/tabs-container/tabs-container.component.ts @@ -0,0 +1,337 @@ +import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, signal, computed } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +export interface Tab { + id: string; + label: string; + content?: any; + closeable?: boolean; + disabled?: boolean; + lazyLoad?: boolean; + icon?: string; +} + +type TabSize = 'sm' | 'md' | 'lg'; +type TabVariant = 'default' | 'filled' | 'pills' | 'underlined'; +type TabPosition = 'top' | 'bottom' | 'left' | 'right'; + +@Component({ + selector: 'ui-tabs-container', + standalone: true, + imports: [CommonModule], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + template: ` +
+ + +
+
+ @for (tab of tabs; track tab.id; let index = $index) { + + } + + } +
+ + @if (scrollable && showScrollControls()) { +
+ + +
+ } +
+ + +
+ +
+
+ `, + styleUrl: './tabs-container.component.scss' +}) +export class TabsContainerComponent { + @Input() tabs: Tab[] = []; + @Input() size: TabSize = 'md'; + @Input() variant: TabVariant = 'default'; + @Input() position: TabPosition = 'top'; + @Input() scrollable = false; + @Input() reorderable = false; + @Input() lazyLoad = true; + + // Active tab management + private _activeTabId = signal(null); + activeTabId = computed(() => this._activeTabId()); + + // Drag and drop state + isDragging = false; + draggedIndex: number | null = null; + + @Output() tabSelected = new EventEmitter(); + @Output() tabClosed = new EventEmitter(); + @Output() tabsReordered = new EventEmitter(); + + ngOnInit() { + // Set initial active tab if none is set + if (!this.activeTabId() && this.tabs.length > 0) { + const firstEnabledTab = this.tabs.find(tab => !tab.disabled); + if (firstEnabledTab) { + this._activeTabId.set(firstEnabledTab.id); + } + } + } + + getContainerClasses(): Record { + return { + 'ui-tabs-container': true, + [`ui-tabs-container--size-${this.size}`]: true, + [`ui-tabs-container--variant-${this.variant}`]: true, + [`ui-tabs-container--position-${this.position}`]: true, + 'ui-tabs-container--scrollable': this.scrollable + }; + } + + selectTab(tabId: string): void { + const tab = this.tabs.find(t => t.id === tabId); + if (!tab || tab.disabled) return; + + this._activeTabId.set(tabId); + this.tabSelected.emit(tabId); + } + + closeTab(event: Event, tabId: string): void { + event.stopPropagation(); + + const tabIndex = this.tabs.findIndex(t => t.id === tabId); + if (tabIndex === -1) return; + + // If closing active tab, select adjacent tab + if (this.activeTabId() === tabId) { + const remainingTabs = this.tabs.filter(t => t.id !== tabId && !t.disabled); + if (remainingTabs.length > 0) { + // Try to select next tab, or previous if no next + const nextTab = this.tabs[tabIndex + 1]; + const prevTab = this.tabs[tabIndex - 1]; + + if (nextTab && !nextTab.disabled) { + this._activeTabId.set(nextTab.id); + } else if (prevTab && !prevTab.disabled) { + this._activeTabId.set(prevTab.id); + } else { + this._activeTabId.set(remainingTabs[0].id); + } + } else { + this._activeTabId.set(null); + } + } + + this.tabClosed.emit(tabId); + } + + handleTabKeydown(event: KeyboardEvent, index: number): void { + const isHorizontal = this.position === 'top' || this.position === 'bottom'; + let newIndex = index; + + switch (event.key) { + case 'ArrowRight': + if (isHorizontal) { + event.preventDefault(); + newIndex = this.getNextEnabledTabIndex(index, 1); + } + break; + case 'ArrowLeft': + if (isHorizontal) { + event.preventDefault(); + newIndex = this.getNextEnabledTabIndex(index, -1); + } + break; + case 'ArrowDown': + if (!isHorizontal) { + event.preventDefault(); + newIndex = this.getNextEnabledTabIndex(index, 1); + } + break; + case 'ArrowUp': + if (!isHorizontal) { + event.preventDefault(); + newIndex = this.getNextEnabledTabIndex(index, -1); + } + break; + case 'Home': + event.preventDefault(); + newIndex = this.getNextEnabledTabIndex(-1, 1); + break; + case 'End': + event.preventDefault(); + newIndex = this.getNextEnabledTabIndex(this.tabs.length, -1); + break; + case 'Delete': + if (this.tabs[index].closeable) { + event.preventDefault(); + this.closeTab(event, this.tabs[index].id); + } + break; + } + + if (newIndex !== index && newIndex >= 0 && newIndex < this.tabs.length) { + this.selectTab(this.tabs[newIndex].id); + // Focus the new tab + setTimeout(() => { + const newTabElement = document.getElementById('tab-' + this.tabs[newIndex].id); + newTabElement?.focus(); + }); + } + } + + private getNextEnabledTabIndex(startIndex: number, direction: number): number { + let index = startIndex + direction; + + while (index >= 0 && index < this.tabs.length) { + if (!this.tabs[index].disabled) { + return index; + } + index += direction; + } + + // If we didn't find an enabled tab, wrap around + if (direction > 0) { + // Going forward, start from beginning + for (let i = 0; i < startIndex; i++) { + if (!this.tabs[i].disabled) { + return i; + } + } + } else { + // Going backward, start from end + for (let i = this.tabs.length - 1; i > startIndex; i--) { + if (!this.tabs[i].disabled) { + return i; + } + } + } + + return startIndex; // Return original index if no other enabled tab found + } + + // Drag and drop methods + handleDragStart(event: DragEvent, index: number): void { + if (!this.reorderable) return; + + this.isDragging = true; + this.draggedIndex = index; + + if (event.dataTransfer) { + event.dataTransfer.effectAllowed = 'move'; + event.dataTransfer.setData('text/plain', index.toString()); + } + } + + handleDragOver(event: DragEvent): void { + if (!this.reorderable || !this.isDragging) return; + + event.preventDefault(); + event.dataTransfer!.dropEffect = 'move'; + } + + handleDrop(event: DragEvent, dropIndex: number): void { + if (!this.reorderable || this.draggedIndex === null) return; + + event.preventDefault(); + + if (this.draggedIndex !== dropIndex) { + const newTabs = [...this.tabs]; + const draggedTab = newTabs.splice(this.draggedIndex, 1)[0]; + newTabs.splice(dropIndex, 0, draggedTab); + + this.tabsReordered.emit(newTabs); + } + + this.handleDragEnd(); + } + + handleDragEnd(): void { + this.isDragging = false; + this.draggedIndex = null; + } + + // Scrolling methods + showScrollControls(): boolean { + // This would need actual element measurement in a real implementation + return this.scrollable; + } + + canScrollPrev(): boolean { + // Implementation would check actual scroll position + return true; + } + + canScrollNext(): boolean { + // Implementation would check actual scroll position + return true; + } + + scrollTabs(direction: 'prev' | 'next'): void { + // Implementation would handle actual scrolling + const navContent = document.querySelector('.ui-tabs-container__nav-content') as HTMLElement; + if (navContent) { + const scrollAmount = 200; + navContent.scrollBy({ + left: direction === 'next' ? scrollAmount : -scrollAmount, + behavior: 'smooth' + }); + } + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/vstack/index.ts b/projects/ui-essentials/src/lib/components/layout/vstack/index.ts new file mode 100644 index 0000000..ac918e6 --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/vstack/index.ts @@ -0,0 +1 @@ +export * from './vstack.component'; \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/vstack/vstack.component.scss b/projects/ui-essentials/src/lib/components/layout/vstack/vstack.component.scss new file mode 100644 index 0000000..b3596ce --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/vstack/vstack.component.scss @@ -0,0 +1,137 @@ +@use '../../../../../../ui-design-system/src/styles/semantic/index' as *; + +.ui-vstack { + display: flex; + flex-direction: column; + position: relative; + + // Inline variant + &--inline { + display: inline-flex; + } + + // Full width/height variants + &--full-width { + width: 100%; + } + + &--full-height { + height: 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; + } + + // Horizontal alignment (cross-axis for column) + &--align-start { + align-items: flex-start; + } + + &--align-center { + align-items: center; + } + + &--align-end { + align-items: flex-end; + } + + &--align-stretch { + align-items: stretch; + } + + // Vertical justify (main-axis for column) + &--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; + } + + // Divider variant - adds horizontal borders between children + &--divider { + > :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; + } + + // Remove gap when using dividers to avoid double spacing + gap: 0; + } + + // Responsive behavior + &--responsive { + @media (max-width: 640px) { + // Reduce spacing on small screens + &.ui-vstack--spacing-xl, + &.ui-vstack--spacing-2xl, + &.ui-vstack--spacing-3xl, + &.ui-vstack--spacing-4xl, + &.ui-vstack--spacing-5xl { + gap: $semantic-spacing-stack-lg; + } + } + + @media (max-width: 480px) { + &.ui-vstack--spacing-lg, + &.ui-vstack--spacing-xl, + &.ui-vstack--spacing-2xl, + &.ui-vstack--spacing-3xl, + &.ui-vstack--spacing-4xl, + &.ui-vstack--spacing-5xl { + gap: $semantic-spacing-stack-md; + } + } + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/layout/vstack/vstack.component.ts b/projects/ui-essentials/src/lib/components/layout/vstack/vstack.component.ts new file mode 100644 index 0000000..b9fae5c --- /dev/null +++ b/projects/ui-essentials/src/lib/components/layout/vstack/vstack.component.ts @@ -0,0 +1,58 @@ +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 HorizontalAlignment = 'start' | 'center' | 'end' | 'stretch'; +type VerticalJustify = 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly'; + +@Component({ + selector: 'ui-vstack', + standalone: true, + imports: [CommonModule], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + template: ` +
+ + +
+ `, + styleUrl: './vstack.component.scss' +}) +export class VStackComponent { + @Input() spacing: StackSpacing = 'md'; + @Input() align?: HorizontalAlignment; + @Input() justify?: VerticalJustify; + @Input() inline = false; + @Input() responsive = true; + @Input() divider = false; + @Input() fullWidth = false; + @Input() fullHeight = false; + @Input() customGap?: string; + @Input() role?: string; + + getClasses(): Record { + const classes: Record = { + 'ui-vstack': true, + [`ui-vstack--spacing-${this.spacing}`]: true, + 'ui-vstack--inline': this.inline, + 'ui-vstack--responsive': this.responsive, + 'ui-vstack--divider': this.divider, + 'ui-vstack--full-width': this.fullWidth, + 'ui-vstack--full-height': this.fullHeight + }; + + if (this.align) { + classes[`ui-vstack--align-${this.align}`] = true; + } + + if (this.justify) { + classes[`ui-vstack--justify-${this.justify}`] = true; + } + + return classes; + } +} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/components/overlays/command-palette/command-palette-item.component.scss b/projects/ui-essentials/src/lib/components/overlays/command-palette/command-palette-item.component.scss index 0f2068c..55ce44a 100644 --- a/projects/ui-essentials/src/lib/components/overlays/command-palette/command-palette-item.component.scss +++ b/projects/ui-essentials/src/lib/components/overlays/command-palette/command-palette-item.component.scss @@ -1,4 +1,4 @@ -@use '../../../../../../ui-design-system/src/styles/semantic' as *; +@use '../../../../../../ui-design-system/src/styles/semantic/index' as *; .ui-command-palette-item { display: flex; diff --git a/projects/ui-essentials/src/lib/components/overlays/command-palette/command-palette.component.scss b/projects/ui-essentials/src/lib/components/overlays/command-palette/command-palette.component.scss index 0758da2..a16248f 100644 --- a/projects/ui-essentials/src/lib/components/overlays/command-palette/command-palette.component.scss +++ b/projects/ui-essentials/src/lib/components/overlays/command-palette/command-palette.component.scss @@ -1,4 +1,4 @@ -@use '../../../../../../ui-design-system/src/styles/semantic' as *; +@use '../../../../../../ui-design-system/src/styles/semantic/index' as *; .ui-command-palette { position: fixed; diff --git a/projects/ui-essentials/src/lib/components/overlays/floating-toolbar/floating-toolbar.component.scss b/projects/ui-essentials/src/lib/components/overlays/floating-toolbar/floating-toolbar.component.scss index efec38c..2f37104 100644 --- a/projects/ui-essentials/src/lib/components/overlays/floating-toolbar/floating-toolbar.component.scss +++ b/projects/ui-essentials/src/lib/components/overlays/floating-toolbar/floating-toolbar.component.scss @@ -1,4 +1,4 @@ -@use '../../../../../../ui-design-system/src/styles/semantic' as *; +@use '../../../../../../ui-design-system/src/styles/semantic/index' as *; .ui-floating-toolbar { // Core Structure diff --git a/projects/ui-essentials/src/lib/layouts/bento-grid-layout.component.scss b/projects/ui-essentials/src/lib/layouts/bento-grid-layout.component.scss deleted file mode 100644 index 66115af..0000000 --- a/projects/ui-essentials/src/lib/layouts/bento-grid-layout.component.scss +++ /dev/null @@ -1,251 +0,0 @@ -@use "../../../../ui-design-system/src/styles/semantic/index" as *; - -.bento-grid { - display: grid; - gap: $semantic-spacing-4; - padding: $semantic-spacing-4; - transition: - grid-template-columns $semantic-motion-duration-normal $semantic-motion-easing-ease-in-out, - grid-template-rows $semantic-motion-duration-normal $semantic-motion-easing-ease-in-out, - gap $semantic-motion-duration-fast $semantic-motion-easing-ease-out; - - // Mobile: Single column for all variants - grid-template-columns: 1fr; - grid-auto-rows: minmax(200px, auto); - - // Tablet: 2x2 base grid - @media (min-width: $semantic-breakpoint-sm) { - grid-template-columns: repeat(2, 1fr); - gap: $semantic-spacing-5; - padding: $semantic-spacing-5; - } - - // Desktop: Full bento layouts - @media (min-width: $semantic-breakpoint-lg) { - gap: $semantic-spacing-6; - padding: $semantic-spacing-6; - grid-auto-rows: minmax(240px, auto); - } - - @media (min-width: $semantic-sizing-breakpoint-wide) { - gap: $semantic-spacing-8; - padding: $semantic-spacing-8; - } -} - -// Balanced variant: Even distribution -.bento-grid--balanced { - @media (min-width: $semantic-breakpoint-lg) { - grid-template-columns: repeat(4, 1fr); - grid-template-rows: repeat(3, 1fr); - } - - @media (min-width: $semantic-sizing-breakpoint-wide) { - grid-template-columns: repeat(6, 1fr); - grid-template-rows: repeat(4, 1fr); - } -} - -// Asymmetric variant: Varied cell sizes -.bento-grid--asymmetric { - @media (min-width: $semantic-breakpoint-lg) { - grid-template-columns: repeat(12, 1fr); - grid-template-rows: repeat(6, minmax(120px, 1fr)); - } -} - -// Masonry variant: Pinterest-style -.bento-grid--masonry { - @media (min-width: $semantic-breakpoint-lg) { - grid-template-columns: repeat(4, 1fr); - grid-auto-rows: 120px; - } - - @media (min-width: $semantic-sizing-breakpoint-wide) { - grid-template-columns: repeat(5, 1fr); - } -} - -// Grid item base styles -.bento-grid > * { - background-color: var(--color-background-surface, #fff); - border: 1px solid var(--color-border-subtle, #e5e7eb); - border-radius: $semantic-border-radius-lg; - overflow: hidden; - transition: - transform $semantic-motion-duration-fast $semantic-motion-easing-ease-out, - box-shadow $semantic-motion-duration-fast $semantic-motion-easing-ease-out, - border-color $semantic-motion-duration-fast $semantic-motion-easing-ease-out; - - &:hover { - transform: $semantic-motion-hover-transform-lift-sm; - box-shadow: $semantic-shadow-lg; - border-color: var(--color-border-primary, #d1d5db); - } - - // Stagger animation on load - @for $i from 1 through 20 { - &:nth-child(#{$i}) { - animation: bentoSlideIn $semantic-motion-duration-normal $semantic-motion-easing-ease-out; - animation-delay: calc(#{$semantic-motion-transition-delay-100} * #{$i}); - animation-fill-mode: both; - } - } -} - -// Span utility classes for grid positioning -.bento-grid { - // Column spans - ::ng-deep [data-bento-span-col="2"] { - @media (min-width: $semantic-breakpoint-sm) { - grid-column: span 2; - } - } - - ::ng-deep [data-bento-span-col="3"] { - @media (min-width: $semantic-breakpoint-lg) { - grid-column: span 3; - } - } - - ::ng-deep [data-bento-span-col="4"] { - @media (min-width: $semantic-breakpoint-lg) { - grid-column: span 4; - } - } - - // Row spans - ::ng-deep [data-bento-span-row="2"] { - @media (min-width: $semantic-breakpoint-sm) { - grid-row: span 2; - } - } - - ::ng-deep [data-bento-span-row="3"] { - @media (min-width: $semantic-breakpoint-lg) { - grid-row: span 3; - } - } - - // Featured item (large) - ::ng-deep [data-bento-featured="true"] { - @media (min-width: $semantic-breakpoint-sm) { - grid-column: span 2; - grid-row: span 2; - } - - @media (min-width: $semantic-breakpoint-lg) { - grid-column: span 3; - grid-row: span 2; - } - } - - // Wide item - ::ng-deep [data-bento-wide="true"] { - @media (min-width: $semantic-breakpoint-sm) { - grid-column: span 2; - } - - @media (min-width: $semantic-breakpoint-lg) { - grid-column: span 4; - } - } - - // Tall item - ::ng-deep [data-bento-tall="true"] { - @media (min-width: $semantic-breakpoint-sm) { - grid-row: span 2; - } - - @media (min-width: $semantic-breakpoint-lg) { - grid-row: span 3; - } - } -} - -// Asymmetric preset layouts -.bento-grid--asymmetric { - ::ng-deep [data-bento-area="hero"] { - @media (min-width: $semantic-breakpoint-lg) { - grid-area: 1 / 1 / 3 / 7; - } - } - - ::ng-deep [data-bento-area="sidebar"] { - @media (min-width: $semantic-breakpoint-lg) { - grid-area: 1 / 7 / 4 / 13; - } - } - - ::ng-deep [data-bento-area="feature-1"] { - @media (min-width: $semantic-breakpoint-lg) { - grid-area: 3 / 1 / 5 / 5; - } - } - - ::ng-deep [data-bento-area="feature-2"] { - @media (min-width: $semantic-breakpoint-lg) { - grid-area: 3 / 5 / 5 / 7; - } - } - - ::ng-deep [data-bento-area="bottom-left"] { - @media (min-width: $semantic-breakpoint-lg) { - grid-area: 5 / 1 / 7 / 4; - } - } - - ::ng-deep [data-bento-area="bottom-center"] { - @media (min-width: $semantic-breakpoint-lg) { - grid-area: 5 / 4 / 6 / 7; - } - } - - ::ng-deep [data-bento-area="bottom-right"] { - @media (min-width: $semantic-breakpoint-lg) { - grid-area: 4 / 7 / 7 / 13; - } - } -} - -// Adaptive sizing for content -.bento-grid[data-adaptive="true"] > * { - // Ensure minimum content area - min-height: 160px; - padding: $semantic-spacing-4; - - @media (min-width: $semantic-breakpoint-lg) { - min-height: 200px; - padding: $semantic-spacing-6; - } - - // Scale content based on grid area size - &[data-bento-featured="true"], - &[data-bento-span-col="2"][data-bento-span-row="2"] { - padding: $semantic-spacing-6; - - @media (min-width: $semantic-breakpoint-lg) { - padding: $semantic-spacing-8; - } - } -} - -// Animation keyframes -@keyframes bentoSlideIn { - from { - opacity: 0; - transform: translateY($semantic-spacing-6) scale(0.95); - } - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -// Responsive gap adjustments -@media (max-width: $semantic-breakpoint-sm) { - .bento-grid { - gap: $semantic-spacing-3; - padding: $semantic-spacing-3; - } -} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/layouts/bento-grid-layout.component.ts b/projects/ui-essentials/src/lib/layouts/bento-grid-layout.component.ts deleted file mode 100644 index 5adeca6..0000000 --- a/projects/ui-essentials/src/lib/layouts/bento-grid-layout.component.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -@Component({ - selector: 'ui-bento-grid-layout', - standalone: true, - imports: [CommonModule], - template: ` -
- -
- `, - styleUrl: './bento-grid-layout.component.scss' -}) -export class BentoGridLayoutComponent { - @Input() variant: 'balanced' | 'asymmetric' | 'masonry' = 'balanced'; - @Input() adaptive: boolean = true; -} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/layouts/dashboard-shell-layout.component.scss b/projects/ui-essentials/src/lib/layouts/dashboard-shell-layout.component.scss deleted file mode 100644 index 13483c6..0000000 --- a/projects/ui-essentials/src/lib/layouts/dashboard-shell-layout.component.scss +++ /dev/null @@ -1,132 +0,0 @@ -@use "../../../../ui-design-system/src/styles/semantic/index" as *; - -.dashboard-shell { - display: flex; - flex-direction: column; - min-height: 100vh; - background-color: var(--color-background-primary, #fff); -} - -.dashboard-header { - position: sticky; - top: 0; - z-index: $semantic-z-index-fixed; - padding: $semantic-spacing-4; - background-color: var(--color-background-surface, #fff); - border-bottom: 1px solid var(--color-border-subtle, #e5e7eb); - transition: - box-shadow $semantic-motion-duration-fast $semantic-motion-easing-ease-out; - - @media (min-width: $semantic-breakpoint-md) { - padding: $semantic-spacing-4 $semantic-spacing-6; - } -} - -.dashboard-content { - display: flex; - flex: 1; - overflow: hidden; -} - -.dashboard-sidebar { - width: 100%; - max-width: $semantic-spacing-64; - background-color: var(--color-background-surface, #fff); - border-right: 1px solid var(--color-border-subtle, #e5e7eb); - transition: - width $semantic-motion-duration-normal $semantic-motion-easing-ease-in-out, - transform $semantic-motion-duration-normal $semantic-motion-easing-ease-out, - box-shadow $semantic-motion-duration-fast $semantic-motion-easing-ease-out; - - // Mobile: Hidden by default, slide in when needed - @media (max-width: $semantic-breakpoint-md - 1) { - position: fixed; - top: 0; - left: 0; - height: 100vh; - z-index: $semantic-z-index-sticky; - transform: translateX(-100%); - box-shadow: 0 0 0 rgba(0, 0, 0, 0); - - &:not(.collapsed) { - transform: translateX(0); - box-shadow: $semantic-shadow-lg; - } - } - - // Tablet and up: Always visible unless collapsed - @media (min-width: $semantic-breakpoint-md) { - position: relative; - transform: translateX(0); - - &.collapsed { - width: $semantic-spacing-16; - overflow: hidden; - } - } - - // Desktop: Full sidebar width - @media (min-width: $semantic-breakpoint-lg) { - max-width: $semantic-spacing-72; - } -} - -.dashboard-main { - flex: 1; - overflow: auto; - padding: $semantic-spacing-4; - background-color: var(--color-background-primary, #fafafa); - transition: - margin-left $semantic-motion-duration-normal $semantic-motion-easing-ease-in-out; - - @media (min-width: $semantic-breakpoint-md) { - padding: $semantic-spacing-6; - } - - @media (min-width: $semantic-breakpoint-lg) { - padding: $semantic-spacing-8; - } -} - -.dashboard-footer { - padding: $semantic-spacing-4; - background-color: var(--color-background-surface, #fff); - border-top: 1px solid var(--color-border-subtle, #e5e7eb); - transition: - transform $semantic-motion-duration-normal $semantic-motion-easing-ease-out; - - @media (min-width: $semantic-breakpoint-md) { - padding: $semantic-spacing-4 $semantic-spacing-6; - } -} - -// Sidebar collapsed state adjustments -.dashboard-shell.sidebar-collapsed { - .dashboard-main { - @media (min-width: $semantic-breakpoint-md) { - margin-left: 0; - } - } -} - -// Mobile overlay when sidebar is open -@media (max-width: $semantic-breakpoint-md - 1) { - .dashboard-shell:not(.sidebar-collapsed)::before { - content: ''; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.5); - z-index: $semantic-z-index-overlay; - opacity: 0; - animation: fadeIn $semantic-motion-duration-fast $semantic-motion-easing-ease-out forwards; - } -} - -@keyframes fadeIn { - to { - opacity: 1; - } -} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/layouts/dashboard-shell-layout.component.ts b/projects/ui-essentials/src/lib/layouts/dashboard-shell-layout.component.ts deleted file mode 100644 index 78640c9..0000000 --- a/projects/ui-essentials/src/lib/layouts/dashboard-shell-layout.component.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -@Component({ - selector: 'ui-dashboard-shell-layout', - standalone: true, - imports: [CommonModule], - template: ` -
-
- -
- -
- - -
- - -
-
- -
- -
-
- `, - styleUrl: './dashboard-shell-layout.component.scss' -}) -export class DashboardShellLayoutComponent { - @Input() sidebarCollapsed: boolean = false; - @Input() showFooter: boolean = false; -} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/layouts/feed-layout.component.scss b/projects/ui-essentials/src/lib/layouts/feed-layout.component.scss deleted file mode 100644 index c39e608..0000000 --- a/projects/ui-essentials/src/lib/layouts/feed-layout.component.scss +++ /dev/null @@ -1,297 +0,0 @@ -@use "../../../../ui-design-system/src/styles/semantic/index" as *; - -.feed-layout { - display: flex; - flex-direction: column; - height: 100%; - overflow: hidden; - background-color: var(--color-background-primary, #fafafa); -} - -.feed-layout__header { - display: flex; - align-items: center; - justify-content: space-between; - gap: $semantic-spacing-4; - padding: $semantic-spacing-4; - background-color: var(--color-background-surface, #fff); - border-bottom: 1px solid var(--color-border-subtle, #e5e7eb); - z-index: $semantic-z-index-sticky; - - @media (min-width: $semantic-breakpoint-md) { - padding: $semantic-spacing-6; - } -} - -.feed-layout__title { - flex: 1; - min-width: 0; // Prevent flex item from overflowing -} - -.feed-layout__header-actions { - display: flex; - align-items: center; - gap: $semantic-spacing-2; - flex-shrink: 0; -} - -.feed-layout__filters { - padding: $semantic-spacing-4; - background-color: var(--color-background-subtle, #f9fafb); - border-bottom: 1px solid var(--color-border-subtle, #f3f4f6); - transition: - background-color $semantic-motion-duration-fast $semantic-motion-easing-ease-out; - - @media (min-width: $semantic-breakpoint-md) { - padding: $semantic-spacing-4 $semantic-spacing-6; - } -} - -.feed-layout__content { - flex: 1; - overflow-y: auto; - overflow-x: hidden; - position: relative; - - // Smooth scrolling - scroll-behavior: smooth; - - // Custom scrollbar - &::-webkit-scrollbar { - width: 8px; - } - - &::-webkit-scrollbar-track { - background: var(--color-background-subtle, #f3f4f6); - } - - &::-webkit-scrollbar-thumb { - background: var(--color-border-primary, #d1d5db); - border-radius: 4px; - - &:hover { - background: var(--color-border-strong, #9ca3af); - } - } -} - -.feed-layout__items { - padding: $semantic-spacing-4; - - @media (min-width: $semantic-breakpoint-md) { - padding: $semantic-spacing-6; - } -} - -// Standard feed variant -.feed-layout--standard { - .feed-layout__items { - display: flex; - flex-direction: column; - gap: $semantic-spacing-4; - - @media (min-width: $semantic-breakpoint-md) { - gap: $semantic-spacing-6; - } - } -} - -// Card-based feed variant -.feed-layout--card-based { - .feed-layout__items { - display: grid; - gap: $semantic-spacing-4; - - // Mobile: Single column - grid-template-columns: 1fr; - - // Tablet: Two columns - @media (min-width: $semantic-breakpoint-sm) { - grid-template-columns: repeat(2, 1fr); - gap: $semantic-spacing-5; - } - - // Desktop: Three columns - @media (min-width: $semantic-breakpoint-lg) { - grid-template-columns: repeat(3, 1fr); - gap: $semantic-spacing-6; - } - - // Large desktop: Four columns - @media (min-width: $semantic-sizing-breakpoint-wide) { - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - } - } -} - -// Compact feed variant -.feed-layout--compact { - .feed-layout__items { - display: flex; - flex-direction: column; - gap: $semantic-spacing-2; - - @media (min-width: $semantic-breakpoint-md) { - gap: $semantic-spacing-3; - } - } -} - -// Loading state -.feed-layout__loading { - display: flex; - align-items: center; - justify-content: center; - padding: $semantic-spacing-8; - - // Animate loading appearance - animation: fadeIn $semantic-motion-duration-fast $semantic-motion-easing-ease-out; -} - -// Load more section -.feed-layout__load-more { - display: flex; - justify-content: center; - padding: $semantic-spacing-6; - border-top: 1px solid var(--color-border-subtle, #f3f4f6); - background-color: var(--color-background-surface, #fff); - - transition: - background-color $semantic-motion-duration-fast $semantic-motion-easing-ease-out, - border-color $semantic-motion-duration-fast $semantic-motion-easing-ease-out; - - &:hover { - background-color: var(--color-background-subtle, #f9fafb); - } -} - -// Empty state -.feed-layout__empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: $semantic-spacing-12; - text-align: center; - min-height: 400px; - - animation: fadeInUp $semantic-motion-duration-normal $semantic-motion-easing-ease-out; -} - -// Footer -.feed-layout__footer { - padding: $semantic-spacing-4; - background-color: var(--color-background-surface, #fff); - border-top: 1px solid var(--color-border-subtle, #e5e7eb); - - @media (min-width: $semantic-breakpoint-md) { - padding: $semantic-spacing-4 $semantic-spacing-6; - } -} - -// Feed items animation (staggered appearance) -.feed-layout__items > * { - animation: feedItemSlideIn $semantic-motion-duration-normal $semantic-motion-easing-ease-out; - animation-fill-mode: both; - - @for $i from 1 through 20 { - &:nth-child(#{$i}) { - animation-delay: calc(#{$semantic-motion-transition-delay-75} * #{$i}); - } - } -} - -// Infinite scroll loading indicator -.feed-layout__content.infinite-scroll { - .feed-layout__loading { - position: sticky; - bottom: 0; - @include glass-effect('frosted', $css-glass-blur-md, true, true, true); - - &::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 1px; - background: linear-gradient( - 90deg, - transparent, - var(--color-primary, #3b82f6), - transparent - ); - animation: shimmer $semantic-motion-duration-slower linear infinite; - } - } -} - -// Responsive adjustments for small screens -@media (max-width: $semantic-breakpoint-sm) { - .feed-layout__header { - flex-direction: column; - align-items: stretch; - gap: $semantic-spacing-3; - padding: $semantic-spacing-3; - } - - .feed-layout__header-actions { - justify-content: center; - } - - .feed-layout__items { - padding: $semantic-spacing-3; - } - - .feed-layout__empty { - padding: $semantic-spacing-8; - min-height: 300px; - } -} - -// Animation keyframes -@keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY($semantic-spacing-6); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes feedItemSlideIn { - from { - opacity: 0; - transform: translateY($semantic-spacing-4); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes shimmer { - 0% { - transform: translateX(-100%); - } - 100% { - transform: translateX(100%); - } -} - -// Focus states for accessibility -.feed-layout__content:focus-visible { - outline: 2px solid var(--color-primary, #3b82f6); - outline-offset: -2px; -} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/layouts/feed-layout.component.ts b/projects/ui-essentials/src/lib/layouts/feed-layout.component.ts deleted file mode 100644 index 9c7cb6e..0000000 --- a/projects/ui-essentials/src/lib/layouts/feed-layout.component.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -@Component({ - selector: 'ui-feed-layout', - standalone: true, - imports: [CommonModule], - template: ` -
-
-
- -
-
- -
-
- -
- -
- -
-
- - -
- -
- -
- -
- -
- -
- -
-
- - -
- `, - styleUrl: './feed-layout.component.scss' -}) -export class FeedLayoutComponent { - @Input() variant: 'standard' | 'card-based' | 'compact' = 'standard'; - @Input() showHeader: boolean = true; - @Input() showFilters: boolean = false; - @Input() showLoadMore: boolean = true; - @Input() showFooter: boolean = false; - @Input() infiniteScroll: boolean = false; - @Input() loading: boolean = false; - @Input() isEmpty: boolean = false; -} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/layouts/grid-container.component.ts b/projects/ui-essentials/src/lib/layouts/grid-container.component.ts deleted file mode 100644 index fe61884..0000000 --- a/projects/ui-essentials/src/lib/layouts/grid-container.component.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { Component, Input, OnInit, signal, computed } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -export type GridColumns = 1 | 2 | 3 | 4 | 5 | 6 | 8 | 12; -export type GridGap = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'; -export type GridJustify = 'start' | 'end' | 'center' | 'between' | 'around' | 'evenly'; -export type GridAlign = 'start' | 'end' | 'center' | 'stretch' | 'baseline'; - -export interface GridResponsiveConfig { - mobile?: GridColumns; - tablet?: GridColumns; - desktop?: GridColumns; - wide?: GridColumns; -} - -@Component({ - selector: 'ui-grid-container', - standalone: true, - imports: [CommonModule], - template: ` -
- - - - @if (showDebugGrid()) { -
- @for (item of debugGridItems(); track $index) { -
{{ $index + 1 }}
- } -
- } -
- `, - styles: [` - .grid-container { - display: grid; - width: 100%; - gap: 1rem; - } - - .grid-container.debug-mode { - border: 2px dashed #007bff; - } - - .debug-grid-cell { - background: rgba(0, 123, 255, 0.1); - border: 1px solid #007bff; - display: flex; - align-items: center; - justify-content: center; - } - `] -}) -export class GridContainerComponent implements OnInit { - // Grid configuration inputs - @Input() columns: GridColumns = 12; - @Input() responsive: GridResponsiveConfig | null = null; - @Input() gap: GridGap = 'md'; - @Input() justify: GridJustify = 'start'; - @Input() align: GridAlign = 'stretch'; - - // Behavior inputs - @Input() autoFit = false; // Auto-fit columns based on min-width - @Input() autoFill = false; // Auto-fill with available space - @Input() minItemWidth = '200px'; // Minimum width for auto-fit/fill - @Input() maxItemWidth = '1fr'; // Maximum width for auto-fit/fill - - // Debug and accessibility - @Input() debugMode = false; - @Input() label: string | null = null; - @Input() description: string | null = null; - - // Reactive state - private currentBreakpoint = signal<'mobile' | 'tablet' | 'desktop' | 'wide'>('desktop'); - - // Computed properties - protected effectiveColumns = computed(() => { - if (this.autoFit || this.autoFill) { - return 'auto'; - } - - if (this.responsive) { - const breakpoint = this.currentBreakpoint(); - return this.responsive[breakpoint] ?? this.columns; - } - - return this.columns; - }); - - protected gridClasses = computed(() => { - const classes = ['grid-container']; - - if (this.autoFit) classes.push('grid-auto-fit'); - if (this.autoFill) classes.push('grid-auto-fill'); - if (this.debugMode) classes.push('debug-mode'); - - classes.push(`gap-${this.gap}`); - classes.push(`justify-${this.justify}`); - classes.push(`align-${this.align}`); - - return classes.join(' '); - }); - - protected showDebugGrid = computed(() => this.debugMode); - protected debugGridItems = computed(() => { - const cols = this.effectiveColumns(); - if (cols === 'auto') return Array(12).fill(0); // Show 12 for auto - return Array(typeof cols === 'number' ? cols : 12).fill(0); - }); - - ngOnInit(): void { - this.setupBreakpointDetection(); - } - - /** - * Sets up responsive breakpoint detection using ResizeObserver - */ - private setupBreakpointDetection(): void { - if (typeof window === 'undefined' || !this.responsive) return; - - const mediaQueries = { - mobile: '(max-width: 767px)', - tablet: '(min-width: 768px) and (max-width: 1023px)', - desktop: '(min-width: 1024px) and (max-width: 1439px)', - wide: '(min-width: 1440px)' - }; - - // Set initial breakpoint - this.updateBreakpoint(mediaQueries); - - // Listen for changes - Object.entries(mediaQueries).forEach(([, query]) => { - const mql = window.matchMedia(query); - mql.addEventListener('change', () => this.updateBreakpoint(mediaQueries)); - }); - } - - /** - * Updates the current breakpoint based on media queries - */ - private updateBreakpoint(queries: Record): void { - for (const [breakpoint, query] of Object.entries(queries)) { - if (window.matchMedia(query).matches) { - this.currentBreakpoint.set(breakpoint as any); - break; - } - } - } -} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/layouts/index.ts b/projects/ui-essentials/src/lib/layouts/index.ts index 02a526b..ee262f9 100644 --- a/projects/ui-essentials/src/lib/layouts/index.ts +++ b/projects/ui-essentials/src/lib/layouts/index.ts @@ -1,15 +1,5 @@ // Layout Components Barrel Export -export * from './dashboard-shell-layout.component'; -export * from './widget-grid-layout.component'; -export * from './kpi-card-layout.component'; -export * from './bento-grid-layout.component'; -export * from './list-detail-layout.component'; -export * from './feed-layout.component'; -export * from './supporting-pane-layout.component'; -export * from './widget-container.component'; +// Components removed for recreation -// New Content Container Components -export * from './grid-container.component'; -export * from './scroll-container.component'; -export * from './tab-container.component'; +// Remaining container components export * from './loading-state-container.component'; \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/layouts/kpi-card-layout.component.scss b/projects/ui-essentials/src/lib/layouts/kpi-card-layout.component.scss deleted file mode 100644 index b50dcfb..0000000 --- a/projects/ui-essentials/src/lib/layouts/kpi-card-layout.component.scss +++ /dev/null @@ -1,222 +0,0 @@ -@use "../../../../ui-design-system/src/styles/semantic/index" as *; - -.kpi-card { - background-color: var(--color-background-surface, #fff); - border: 1px solid var(--color-border-subtle, #e5e7eb); - border-radius: $semantic-border-radius-md; - overflow: hidden; - transition: - transform $semantic-motion-duration-fast $semantic-motion-easing-ease-out, - box-shadow $semantic-motion-duration-fast $semantic-motion-easing-ease-out, - border-color $semantic-motion-duration-fast $semantic-motion-easing-ease-out; - - &:hover { - transform: $semantic-motion-hover-transform-lift-sm; - box-shadow: $semantic-shadow-md; - border-color: var(--color-border-primary, #d1d5db); - } -} - -.kpi-card__header { - display: flex; - align-items: center; - justify-content: space-between; - padding: $semantic-spacing-4; - border-bottom: 1px solid var(--color-border-subtle, #f3f4f6); - background-color: var(--color-background-subtle, #fafafa); - - .kpi-card--sm & { - padding: $semantic-spacing-3; - } - - .kpi-card--lg & { - padding: $semantic-spacing-5; - } -} - -.kpi-card__actions { - display: flex; - align-items: center; - gap: $semantic-spacing-2; -} - -.kpi-card__content { - padding: $semantic-spacing-6; - - .kpi-card--sm & { - padding: $semantic-spacing-4; - } - - .kpi-card--lg & { - padding: $semantic-spacing-8; - } - - // Mobile: Stack primary and secondary vertically - display: flex; - flex-direction: column; - gap: $semantic-spacing-4; - - // Tablet and up: Side by side if there's secondary content - @media (min-width: $semantic-breakpoint-md) { - flex-direction: row; - align-items: center; - justify-content: space-between; - gap: $semantic-spacing-6; - } -} - -.kpi-card__primary { - flex: 1; - - // Center align on mobile - text-align: center; - - @media (min-width: $semantic-breakpoint-md) { - text-align: left; - } -} - -.kpi-card__value { - font-size: $semantic-typography-font-size-3xl; - font-weight: $semantic-typography-font-weight-bold; - line-height: $semantic-typography-line-height-tight; - color: var(--color-text-primary, #111827); - margin-bottom: $semantic-spacing-2; - - .kpi-card--sm & { - font-size: $semantic-typography-font-size-2xl; - } - - .kpi-card--lg & { - font-size: $semantic-typography-font-size-4xl; - margin-bottom: $semantic-spacing-3; - } - - // Animate number changes - transition: - transform $semantic-motion-duration-fast $semantic-motion-easing-spring, - color $semantic-motion-duration-fast $semantic-motion-easing-ease-out; - - &:hover { - transform: $semantic-motion-hover-transform-scale-sm; - } -} - -.kpi-card__label { - font-size: $semantic-typography-font-size-sm; - font-weight: $semantic-typography-font-weight-medium; - color: var(--color-text-secondary, #6b7280); - text-transform: uppercase; - letter-spacing: 0.05em; - - .kpi-card--lg & { - font-size: $semantic-typography-font-size-md; - } -} - -.kpi-card__secondary { - display: flex; - flex-direction: column; - align-items: center; - gap: $semantic-spacing-3; - - @media (min-width: $semantic-breakpoint-md) { - flex-shrink: 0; - align-items: flex-end; - } -} - -.kpi-card__trend { - display: flex; - align-items: center; - gap: $semantic-spacing-2; - padding: $semantic-spacing-2 $semantic-spacing-3; - background-color: var(--color-background-subtle, #f9fafb); - border-radius: $semantic-border-radius-full; - font-size: $semantic-typography-font-size-sm; - font-weight: $semantic-typography-font-weight-medium; - - transition: - background-color $semantic-motion-duration-fast $semantic-motion-easing-ease-out, - transform $semantic-motion-duration-fast $semantic-motion-easing-ease-out; - - &:hover { - background-color: var(--color-background-secondary, #f3f4f6); - transform: $semantic-motion-hover-transform-scale-sm; - } -} - -.kpi-card__chart { - width: 100%; - max-width: 120px; - height: 60px; - - .kpi-card--sm & { - max-width: 80px; - height: 40px; - } - - .kpi-card--lg & { - max-width: 160px; - height: 80px; - } -} - -.kpi-card__footer { - padding: $semantic-spacing-4; - border-top: 1px solid var(--color-border-subtle, #f3f4f6); - background-color: var(--color-background-subtle, #fafafa); - - .kpi-card--sm & { - padding: $semantic-spacing-3; - } - - .kpi-card--lg & { - padding: $semantic-spacing-5; - } -} - -// Responsive adjustments for very small screens -@media (max-width: $semantic-breakpoint-sm) { - .kpi-card__content { - padding: $semantic-spacing-4; - - .kpi-card--sm & { - padding: $semantic-spacing-3; - } - } - - .kpi-card__value { - font-size: $semantic-typography-font-size-2xl; - - .kpi-card--sm & { - font-size: $semantic-typography-font-size-xl; - } - } -} - -// Animation for appearing content -.kpi-card__value, -.kpi-card__trend, -.kpi-card__chart { - animation: fadeInUp $semantic-motion-duration-normal $semantic-motion-easing-ease-out; -} - -.kpi-card__trend { - animation-delay: $semantic-motion-transition-delay-100; -} - -.kpi-card__chart { - animation-delay: $semantic-motion-transition-delay-200; -} - -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY($semantic-spacing-2); - } - to { - opacity: 1; - transform: translateY(0); - } -} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/layouts/kpi-card-layout.component.ts b/projects/ui-essentials/src/lib/layouts/kpi-card-layout.component.ts deleted file mode 100644 index 6d2bda2..0000000 --- a/projects/ui-essentials/src/lib/layouts/kpi-card-layout.component.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -@Component({ - selector: 'ui-kpi-card-layout', - standalone: true, - imports: [CommonModule], - template: ` -
-
- -
- -
-
- -
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
- - -
- `, - styleUrl: './kpi-card-layout.component.scss' -}) -export class KpiCardLayoutComponent { - @Input() size: 'sm' | 'md' | 'lg' = 'md'; - @Input() showHeader: boolean = true; - @Input() showTrend: boolean = true; - @Input() showChart: boolean = false; - @Input() showFooter: boolean = false; -} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/layouts/list-detail-layout.component.scss b/projects/ui-essentials/src/lib/layouts/list-detail-layout.component.scss deleted file mode 100644 index 6ca47ee..0000000 --- a/projects/ui-essentials/src/lib/layouts/list-detail-layout.component.scss +++ /dev/null @@ -1,256 +0,0 @@ -@use "../../../../ui-design-system/src/styles/semantic/index" as *; - -.list-detail-layout { - display: flex; - flex-direction: column; - height: 100%; - overflow: hidden; - background-color: var(--color-background-primary, #fafafa); -} - -.list-detail-layout__toolbar { - padding: $semantic-spacing-4; - background-color: var(--color-background-surface, #fff); - border-bottom: 1px solid var(--color-border-subtle, #e5e7eb); - z-index: $semantic-z-index-fixed; - - @media (min-width: $semantic-breakpoint-md) { - padding: $semantic-spacing-4 $semantic-spacing-6; - } -} - -.list-detail-layout__content { - flex: 1; - display: flex; - overflow: hidden; - - // Mobile: Stack views, show one at a time - flex-direction: column; - - // Tablet and up: Side by side - @media (min-width: $semantic-breakpoint-md) { - flex-direction: row; - } -} - -.list-detail-layout__list { - background-color: var(--color-background-surface, #fff); - border-right: 1px solid var(--color-border-subtle, #e5e7eb); - overflow: hidden; - display: flex; - flex-direction: column; - transition: - width $semantic-motion-duration-normal $semantic-motion-easing-ease-in-out, - flex $semantic-motion-duration-normal $semantic-motion-easing-ease-in-out, - transform $semantic-motion-duration-normal $semantic-motion-easing-ease-out; - - // Mobile: Full width when visible - width: 100%; - - // Tablet and up: Split based on ratio - @media (min-width: $semantic-breakpoint-md) { - flex-shrink: 0; - } - - &.hidden-on-mobile { - @media (max-width: $semantic-breakpoint-md - 1) { - transform: translateX(-100%); - position: absolute; - z-index: -1; - } - } -} - -.list-detail-layout__detail { - flex: 1; - background-color: var(--color-background-primary, #fff); - overflow: hidden; - display: flex; - flex-direction: column; - transition: - transform $semantic-motion-duration-normal $semantic-motion-easing-ease-out, - opacity $semantic-motion-duration-normal $semantic-motion-easing-ease-out; - - &.hidden-on-mobile { - @media (max-width: $semantic-breakpoint-md - 1) { - transform: translateX(100%); - position: absolute; - z-index: -1; - opacity: 0; - } - } -} - -// Split ratio variations -.list-detail-layout--30-70 { - .list-detail-layout__list { - @media (min-width: $semantic-breakpoint-md) { - width: 30%; - } - } -} - -.list-detail-layout--25-75 { - .list-detail-layout__list { - @media (min-width: $semantic-breakpoint-md) { - width: 25%; - } - } -} - -.list-detail-layout--40-60 { - .list-detail-layout__list { - @media (min-width: $semantic-breakpoint-md) { - width: 40%; - } - } -} - -// List section -.list-detail-layout__list-header { - padding: $semantic-spacing-4; - border-bottom: 1px solid var(--color-border-subtle, #f3f4f6); - background-color: var(--color-background-subtle, #fafafa); - - @media (min-width: $semantic-breakpoint-md) { - padding: $semantic-spacing-4 $semantic-spacing-5; - } -} - -.list-detail-layout__list-content { - flex: 1; - overflow-y: auto; - overflow-x: hidden; -} - -// Detail section -.list-detail-layout__detail-header { - display: flex; - align-items: center; - gap: $semantic-spacing-3; - padding: $semantic-spacing-4; - border-bottom: 1px solid var(--color-border-subtle, #e5e7eb); - background-color: var(--color-background-surface, #fff); - - @media (min-width: $semantic-breakpoint-md) { - padding: $semantic-spacing-4 $semantic-spacing-6; - } -} - -.list-detail-layout__back-button { - display: flex; - align-items: center; - justify-content: center; - padding: $semantic-spacing-2; - border-radius: $semantic-border-radius-md; - transition: - background-color $semantic-motion-duration-fast $semantic-motion-easing-ease-out, - transform $semantic-motion-duration-fast $semantic-motion-easing-ease-out; - - &:hover { - background-color: var(--color-background-subtle, #f3f4f6); - transform: $semantic-motion-hover-transform-scale-sm; - } - - // Hide back button on desktop - @media (min-width: $semantic-breakpoint-md) { - display: none; - } -} - -.list-detail-layout__detail-content { - flex: 1; - overflow-y: auto; - overflow-x: hidden; - padding: $semantic-spacing-6; - - @media (min-width: $semantic-breakpoint-md) { - padding: $semantic-spacing-8; - } - - @media (min-width: $semantic-breakpoint-lg) { - padding: $semantic-spacing-10; - } -} - -// Mobile detail view animations -.list-detail-layout.mobile-detail-view { - @media (max-width: $semantic-breakpoint-md - 1) { - .list-detail-layout__detail { - animation: slideInRight $semantic-motion-duration-normal $semantic-motion-easing-ease-out; - } - } -} - -.list-detail-layout:not(.mobile-detail-view) { - @media (max-width: $semantic-breakpoint-md - 1) { - .list-detail-layout__list { - animation: slideInLeft $semantic-motion-duration-normal $semantic-motion-easing-ease-out; - } - } -} - -// Responsive adjustments for small screens -@media (max-width: $semantic-breakpoint-sm) { - .list-detail-layout__list-header, - .list-detail-layout__detail-header { - padding: $semantic-spacing-3; - } - - .list-detail-layout__detail-content { - padding: $semantic-spacing-4; - } -} - -// Smooth scrolling for content areas -.list-detail-layout__list-content, -.list-detail-layout__detail-content { - scroll-behavior: smooth; - - // Custom scrollbar styling - &::-webkit-scrollbar { - width: 6px; - } - - &::-webkit-scrollbar-track { - background: var(--color-background-subtle, #f3f4f6); - } - - &::-webkit-scrollbar-thumb { - background: var(--color-border-primary, #d1d5db); - border-radius: 3px; - - &:hover { - background: var(--color-border-strong, #9ca3af); - } - } -} - -// Animation keyframes -@keyframes slideInRight { - from { - transform: translateX(100%); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } -} - -@keyframes slideInLeft { - from { - transform: translateX(-100%); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } -} - -// Enhanced focus states for accessibility -.list-detail-layout__back-button:focus-visible { - outline: 2px solid var(--color-primary, #3b82f6); - outline-offset: 2px; -} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/layouts/list-detail-layout.component.ts b/projects/ui-essentials/src/lib/layouts/list-detail-layout.component.ts deleted file mode 100644 index 2a2a019..0000000 --- a/projects/ui-essentials/src/lib/layouts/list-detail-layout.component.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -@Component({ - selector: 'ui-list-detail-layout', - standalone: true, - imports: [CommonModule], - template: ` -
-
- -
- -
-
-
- -
-
- -
-
- -
-
-
- -
- -
-
- - -
-
-
-
- `, - styleUrl: './list-detail-layout.component.scss' -}) -export class ListDetailLayoutComponent { - @Input() splitRatio: '30-70' | '25-75' | '40-60' = '30-70'; - @Input() mobileDetailView: boolean = false; - @Input() showToolbar: boolean = false; - @Input() showListHeader: boolean = true; - @Input() showDetailHeader: boolean = true; -} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/layouts/scroll-container.component.ts b/projects/ui-essentials/src/lib/layouts/scroll-container.component.ts deleted file mode 100644 index 43d024c..0000000 --- a/projects/ui-essentials/src/lib/layouts/scroll-container.component.ts +++ /dev/null @@ -1,462 +0,0 @@ -import { - Component, - Input, - OnInit, - OnDestroy, - AfterViewInit, - ElementRef, - ViewChild, - signal, - computed, - Output, - EventEmitter, - TemplateRef, - ContentChild -} from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { Subject } from 'rxjs'; -import { takeUntil, throttleTime } from 'rxjs/operators'; -import { fromEvent } from 'rxjs'; - -export type ScrollDirection = 'vertical' | 'horizontal' | 'both'; -export type ScrollbarVisibility = 'auto' | 'always' | 'never' | 'hover'; - -export interface VirtualScrollConfig { - itemHeight: number; - bufferSize?: number; - enableDynamicHeight?: boolean; -} - -export interface ScrollPosition { - top: number; - left: number; -} - -export interface ScrollEvent { - position: ScrollPosition; - percentage: { x: number; y: number }; - isAtTop: boolean; - isAtBottom: boolean; - isAtLeft: boolean; - isAtRight: boolean; -} - -@Component({ - selector: 'ui-scroll-container', - standalone: true, - imports: [CommonModule], - template: ` -
- - @if (virtualScrolling && virtualConfig) { - -
- -
- - @for (item of visibleItems(); track trackBy ? trackBy($index, item) : $index) { -
- - @if (itemTemplate) { - - - } @else { - - } -
- } -
-
- - } @else { - -
- -
- } - - - @if (customScrollbars) { -
-
-
-
-
-
- -
-
-
-
-
-
- } - - - @if (showScrollIndicators) { -
- @if (!isAtTop() && direction !== 'horizontal') { - - } - @if (!isAtBottom() && direction !== 'horizontal') { - - } - @if (!isAtLeft() && direction !== 'vertical') { - - } - @if (!isAtRight() && direction !== 'vertical') { - - } -
- } -
- `, - styles: [` - .scroll-container { - position: relative; - overflow: auto; - width: 100%; - height: 100%; - } - - .scroll-container[data-direction="vertical"] { - overflow-x: hidden; - overflow-y: auto; - } - - .scroll-container[data-direction="horizontal"] { - overflow-x: auto; - overflow-y: hidden; - } - - .scroll-container[data-scrollbar="never"] { - scrollbar-width: none; - -ms-overflow-style: none; - } - - .scroll-container[data-scrollbar="never"]::-webkit-scrollbar { - width: 0px; - background: transparent; - } - - - .scroll-container[data-scrollbar="hover"]::-webkit-scrollbar { - opacity: 0; - transition: opacity 0.3s ease; - } - - .scroll-container[data-scrollbar="hover"]:hover::-webkit-scrollbar { - opacity: 1; - } - `] -}) -export class ScrollContainerComponent implements OnInit, AfterViewInit, OnDestroy { - @ViewChild('scrollContainer', { static: true }) scrollContainer!: ElementRef; - @ContentChild('itemTemplate') itemTemplate?: TemplateRef; - - // Configuration inputs - @Input() direction: ScrollDirection = 'vertical'; - @Input() scrollbarVisibility: ScrollbarVisibility = 'auto'; - @Input() customScrollbars = false; - @Input() showScrollIndicators = false; - @Input() smoothScrolling = true; - - // Virtual scrolling - @Input() virtualScrolling = false; - @Input() virtualConfig: VirtualScrollConfig | null = null; - @Input() items: any[] = []; - @Input() trackBy?: (index: number, item: any) => any; - - // Accessibility - @Input() role: string | null = null; - @Input() ariaLabel: string | null = null; - @Input() tabIndex = -1; - - // Styling - @Input() maxHeight: string | null = null; - @Input() maxWidth: string | null = null; - - // Events - @Output() scroll = new EventEmitter(); - @Output() scrollEnd = new EventEmitter(); - @Output() reachStart = new EventEmitter(); - @Output() reachEnd = new EventEmitter(); - - private destroy$ = new Subject(); - - // State signals - private scrollPosition = signal({ top: 0, left: 0 }); - private containerSize = signal<{ width: number; height: number }>({ width: 0, height: 0 }); - private contentSize = signal<{ width: number; height: number }>({ width: 0, height: 0 }); - private firstVisibleIndex = signal(0); - private lastVisibleIndex = signal(0); - - // Computed properties - protected containerClasses = computed(() => { - const classes = ['scroll-container']; - - if (this.customScrollbars) classes.push('custom-scrollbars'); - if (this.smoothScrolling) classes.push('smooth-scrolling'); - if (this.virtualScrolling) classes.push('virtual-scrolling'); - - return classes.join(' '); - }); - - protected isAtTop = computed(() => this.scrollPosition().top <= 1); - protected isAtBottom = computed(() => { - const { height } = this.containerSize(); - const contentHeight = this.virtualScrolling ? this.virtualTotalHeight() : this.contentSize().height; - return (this.scrollPosition().top + height) >= (contentHeight - 1); - }); - - protected isAtLeft = computed(() => this.scrollPosition().left <= 1); - protected isAtRight = computed(() => { - const { width } = this.containerSize(); - const contentWidth = this.virtualScrolling ? this.virtualTotalWidth() : this.contentSize().width; - return (this.scrollPosition().left + width) >= (contentWidth - 1); - }); - - protected showVerticalScrollbar = computed(() => { - const contentHeight = this.virtualScrolling ? this.virtualTotalHeight() : this.contentSize().height; - return contentHeight > this.containerSize().height; - }); - - protected showHorizontalScrollbar = computed(() => { - const contentWidth = this.virtualScrolling ? this.virtualTotalWidth() : this.contentSize().width; - return contentWidth > this.containerSize().width; - }); - - protected verticalThumbSize = computed(() => { - const containerHeight = this.containerSize().height; - const contentHeight = this.virtualScrolling ? this.virtualTotalHeight() : this.contentSize().height; - return Math.max((containerHeight / contentHeight) * 100, 5); - }); - - protected verticalThumbPosition = computed(() => { - const scrollTop = this.scrollPosition().top; - const contentHeight = this.virtualScrolling ? this.virtualTotalHeight() : this.contentSize().height; - const containerHeight = this.containerSize().height; - const maxScroll = contentHeight - containerHeight; - return maxScroll > 0 ? (scrollTop / maxScroll) * (100 - this.verticalThumbSize()) : 0; - }); - - protected horizontalThumbSize = computed(() => { - const containerWidth = this.containerSize().width; - const contentWidth = this.virtualScrolling ? this.virtualTotalWidth() : this.contentSize().width; - return Math.max((containerWidth / contentWidth) * 100, 5); - }); - - protected horizontalThumbPosition = computed(() => { - const scrollLeft = this.scrollPosition().left; - const contentWidth = this.virtualScrolling ? this.virtualTotalWidth() : this.contentSize().width; - const containerWidth = this.containerSize().width; - const maxScroll = contentWidth - containerWidth; - return maxScroll > 0 ? (scrollLeft / maxScroll) * (100 - this.horizontalThumbSize()) : 0; - }); - - // Virtual scrolling computed properties - protected virtualTotalHeight = computed(() => { - if (!this.virtualConfig) return 0; - return this.items.length * this.virtualConfig.itemHeight; - }); - - protected virtualTotalWidth = computed(() => { - return this.containerSize().width; // For now, assume full width - }); - - protected visibleItems = computed(() => { - if (!this.virtualScrolling || !this.virtualConfig) return []; - - const startIndex = this.firstVisibleIndex(); - const endIndex = this.lastVisibleIndex(); - - return this.items.slice(startIndex, endIndex + 1).map((item, i) => ({ - data: item, - index: startIndex + i - })); - }); - - protected virtualTransform = computed(() => { - if (!this.virtualConfig) return ''; - const startIndex = this.firstVisibleIndex(); - const offsetY = startIndex * this.virtualConfig.itemHeight; - return `translateY(${offsetY}px)`; - }); - - ngOnInit(): void { - this.setupResizeObserver(); - } - - ngAfterViewInit(): void { - this.setupScrollListener(); - this.updateContainerSize(); - - if (this.virtualScrolling) { - this.updateVisibleRange(); - } - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - /** - * Scrolls to a specific position - */ - scrollTo(position: Partial, smooth = true): void { - const element = this.scrollContainer.nativeElement; - const options: ScrollToOptions = { - behavior: smooth && this.smoothScrolling ? 'smooth' : 'auto' - }; - - if (position.top !== undefined) options.top = position.top; - if (position.left !== undefined) options.left = position.left; - - element.scrollTo(options); - } - - /** - * Scrolls to a specific item (virtual scrolling only) - */ - scrollToItem(index: number, smooth = true): void { - if (!this.virtualScrolling || !this.virtualConfig) return; - - const position = index * this.virtualConfig.itemHeight; - this.scrollTo({ top: position }, smooth); - } - - /** - * Gets current scroll position - */ - getScrollPosition(): ScrollPosition { - return this.scrollPosition(); - } - - private setupScrollListener(): void { - fromEvent(this.scrollContainer.nativeElement, 'scroll') - .pipe( - throttleTime(16), // ~60fps - takeUntil(this.destroy$) - ) - .subscribe(() => { - this.updateScrollPosition(); - this.emitScrollEvent(); - - if (this.virtualScrolling) { - this.updateVisibleRange(); - } - }); - } - - private setupResizeObserver(): void { - if (typeof ResizeObserver === 'undefined') return; - - const resizeObserver = new ResizeObserver(() => { - this.updateContainerSize(); - this.updateContentSize(); - - if (this.virtualScrolling) { - this.updateVisibleRange(); - } - }); - - resizeObserver.observe(this.scrollContainer.nativeElement); - } - - private updateScrollPosition(): void { - const element = this.scrollContainer.nativeElement; - this.scrollPosition.set({ - top: element.scrollTop, - left: element.scrollLeft - }); - } - - private updateContainerSize(): void { - const element = this.scrollContainer.nativeElement; - this.containerSize.set({ - width: element.clientWidth, - height: element.clientHeight - }); - } - - private updateContentSize(): void { - const element = this.scrollContainer.nativeElement; - this.contentSize.set({ - width: element.scrollWidth, - height: element.scrollHeight - }); - } - - private updateVisibleRange(): void { - if (!this.virtualConfig) return; - - const containerHeight = this.containerSize().height; - const scrollTop = this.scrollPosition().top; - const itemHeight = this.virtualConfig.itemHeight; - const bufferSize = this.virtualConfig.bufferSize ?? 5; - - const visibleStart = Math.floor(scrollTop / itemHeight); - const visibleEnd = Math.min( - visibleStart + Math.ceil(containerHeight / itemHeight), - this.items.length - 1 - ); - - this.firstVisibleIndex.set(Math.max(0, visibleStart - bufferSize)); - this.lastVisibleIndex.set(Math.min(this.items.length - 1, visibleEnd + bufferSize)); - } - - private emitScrollEvent(): void { - const position = this.scrollPosition(); - const containerSize = this.containerSize(); - const contentSize = this.virtualScrolling ? - { width: this.virtualTotalWidth(), height: this.virtualTotalHeight() } : - this.contentSize(); - - const scrollEvent: ScrollEvent = { - position, - percentage: { - x: contentSize.width > containerSize.width ? - position.left / (contentSize.width - containerSize.width) : 0, - y: contentSize.height > containerSize.height ? - position.top / (contentSize.height - containerSize.height) : 0 - }, - isAtTop: this.isAtTop(), - isAtBottom: this.isAtBottom(), - isAtLeft: this.isAtLeft(), - isAtRight: this.isAtRight() - }; - - this.scroll.emit(scrollEvent); - - // Emit boundary events - if (scrollEvent.isAtTop) this.reachStart.emit(); - if (scrollEvent.isAtBottom) this.reachEnd.emit(); - } -} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/layouts/supporting-pane-layout.component.scss b/projects/ui-essentials/src/lib/layouts/supporting-pane-layout.component.scss deleted file mode 100644 index 73b1a86..0000000 --- a/projects/ui-essentials/src/lib/layouts/supporting-pane-layout.component.scss +++ /dev/null @@ -1,398 +0,0 @@ -@use "../../../../ui-design-system/src/styles/semantic/index" as *; - -.supporting-pane-layout { - display: flex; - height: 100%; - overflow: hidden; - background-color: var(--color-background-primary, #fafafa); - position: relative; - - // Default: Right pane layout - flex-direction: row; - - // Bottom pane layout - &.supporting-pane-layout--bottom { - flex-direction: column; - } - - // Left pane layout (reverse the order) - &.supporting-pane-layout--left { - flex-direction: row-reverse; - } -} - -.supporting-pane-layout__main { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; - background-color: var(--color-background-surface, #fff); - - // Add border based on pane position - .supporting-pane-layout--right & { - border-right: 1px solid var(--color-border-subtle, #e5e7eb); - } - - .supporting-pane-layout--left & { - border-left: 1px solid var(--color-border-subtle, #e5e7eb); - } - - .supporting-pane-layout--bottom & { - border-bottom: 1px solid var(--color-border-subtle, #e5e7eb); - } - - transition: - flex $semantic-motion-duration-normal $semantic-motion-easing-ease-in-out, - border-color $semantic-motion-duration-fast $semantic-motion-easing-ease-out; -} - -.supporting-pane-layout__main-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: $semantic-spacing-4; - padding: $semantic-spacing-4; - border-bottom: 1px solid var(--color-border-subtle, #f3f4f6); - background-color: var(--color-background-subtle, #fafafa); - - @media (min-width: $semantic-breakpoint-md) { - padding: $semantic-spacing-4 $semantic-spacing-6; - } -} - -.supporting-pane-layout__main-actions { - display: flex; - align-items: center; - gap: $semantic-spacing-2; - flex-shrink: 0; -} - -.supporting-pane-layout__main-content { - flex: 1; - overflow: auto; - padding: $semantic-spacing-6; - - @media (min-width: $semantic-breakpoint-md) { - padding: $semantic-spacing-8; - } - - // Custom scrollbar - &::-webkit-scrollbar { - width: 8px; - height: 8px; - } - - &::-webkit-scrollbar-track { - background: var(--color-background-subtle, #f3f4f6); - } - - &::-webkit-scrollbar-thumb { - background: var(--color-border-primary, #d1d5db); - border-radius: 4px; - - &:hover { - background: var(--color-border-strong, #9ca3af); - } - } -} - -.supporting-pane-layout__pane { - background-color: var(--color-background-surface, #fff); - display: flex; - flex-direction: column; - overflow: hidden; - z-index: $semantic-z-index-sticky; - - transition: - width $semantic-motion-duration-normal $semantic-motion-easing-ease-in-out, - height $semantic-motion-duration-normal $semantic-motion-easing-ease-in-out, - transform $semantic-motion-duration-normal $semantic-motion-easing-ease-out, - opacity $semantic-motion-duration-normal $semantic-motion-easing-ease-out; - - // Right/Left pane sizing - .supporting-pane-layout--right &, - .supporting-pane-layout--left & { - width: 320px; - min-width: 280px; - max-width: 50%; - - @media (min-width: $semantic-breakpoint-lg) { - width: 400px; - min-width: 350px; - } - - @media (min-width: $semantic-sizing-breakpoint-wide) { - width: 480px; - min-width: 400px; - } - - // Mobile: Full screen overlay - @media (max-width: $semantic-breakpoint-md - 1) { - position: fixed; - top: 0; - bottom: 0; - width: 100%; - max-width: none; - box-shadow: $semantic-shadow-xl; - - &.collapsed { - transform: translateX(100%); - opacity: 0; - } - } - - // Desktop: Collapsible - @media (min-width: $semantic-breakpoint-md) { - &.collapsed { - width: 0; - min-width: 0; - opacity: 0; - } - } - } - - // Left pane positioning on mobile - .supporting-pane-layout--left & { - @media (max-width: $semantic-breakpoint-md - 1) { - &.collapsed { - transform: translateX(-100%); - } - } - } - - // Bottom pane sizing - .supporting-pane-layout--bottom & { - height: 300px; - min-height: 200px; - max-height: 50%; - - @media (min-width: $semantic-breakpoint-lg) { - height: 400px; - min-height: 250px; - } - - // Mobile: Slide up from bottom - @media (max-width: $semantic-breakpoint-md - 1) { - position: fixed; - left: 0; - right: 0; - bottom: 0; - height: 60%; - max-height: none; - box-shadow: $semantic-shadow-xl; - border-top-left-radius: $semantic-border-radius-lg; - border-top-right-radius: $semantic-border-radius-lg; - - &.collapsed { - transform: translateY(100%); - opacity: 0; - } - } - - // Desktop: Collapsible - @media (min-width: $semantic-breakpoint-md) { - &.collapsed { - height: 0; - min-height: 0; - opacity: 0; - } - } - } -} - -.supporting-pane-layout__pane-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: $semantic-spacing-3; - padding: $semantic-spacing-4; - border-bottom: 1px solid var(--color-border-subtle, #f3f4f6); - background-color: var(--color-background-subtle, #fafafa); - flex-shrink: 0; - - @media (min-width: $semantic-breakpoint-md) { - padding: $semantic-spacing-4 $semantic-spacing-5; - } -} - -.supporting-pane-layout__pane-title { - flex: 1; - min-width: 0; -} - -.supporting-pane-layout__pane-actions { - display: flex; - align-items: center; - gap: $semantic-spacing-2; - flex-shrink: 0; -} - -.supporting-pane-layout__pane-content { - flex: 1; - overflow: auto; - padding: $semantic-spacing-4; - - @media (min-width: $semantic-breakpoint-md) { - padding: $semantic-spacing-5; - } - - // Custom scrollbar - &::-webkit-scrollbar { - width: 6px; - } - - &::-webkit-scrollbar-track { - background: var(--color-background-subtle, #f3f4f6); - } - - &::-webkit-scrollbar-thumb { - background: var(--color-border-primary, #d1d5db); - border-radius: 3px; - - &:hover { - background: var(--color-border-strong, #9ca3af); - } - } -} - -.supporting-pane-layout__pane-footer { - padding: $semantic-spacing-4; - border-top: 1px solid var(--color-border-subtle, #f3f4f6); - background-color: var(--color-background-subtle, #fafafa); - flex-shrink: 0; - - @media (min-width: $semantic-breakpoint-md) { - padding: $semantic-spacing-4 $semantic-spacing-5; - } -} - -// Toggle button positioning -.supporting-pane-layout__toggle { - position: absolute; - z-index: $semantic-z-index-sticky; - transition: - transform $semantic-motion-duration-fast $semantic-motion-easing-ease-out, - opacity $semantic-motion-duration-fast $semantic-motion-easing-ease-out; - - .supporting-pane-layout--right & { - right: $semantic-spacing-4; - top: 50%; - transform: translateY(-50%); - - .supporting-pane-layout.pane-collapsed & { - right: $semantic-spacing-4; - } - } - - .supporting-pane-layout--left & { - left: $semantic-spacing-4; - top: 50%; - transform: translateY(-50%); - - .supporting-pane-layout.pane-collapsed & { - left: $semantic-spacing-4; - } - } - - .supporting-pane-layout--bottom & { - bottom: $semantic-spacing-4; - right: 50%; - transform: translateX(50%); - - .supporting-pane-layout.pane-collapsed & { - bottom: $semantic-spacing-4; - } - } -} - -// Mobile overlay backdrop when pane is open -@media (max-width: $semantic-breakpoint-md - 1) { - .supporting-pane-layout:not(.pane-collapsed)::before { - content: ''; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.5); - z-index: $semantic-z-index-overlay; - animation: backdropFadeIn $semantic-motion-duration-fast $semantic-motion-easing-ease-out; - } -} - -// Responsive adjustments for small screens -@media (max-width: $semantic-breakpoint-sm) { - .supporting-pane-layout__main-header, - .supporting-pane-layout__pane-header { - padding: $semantic-spacing-3; - } - - .supporting-pane-layout__main-content, - .supporting-pane-layout__pane-content { - padding: $semantic-spacing-3; - } -} - -// Animation keyframes -@keyframes backdropFadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -// Focus states for accessibility -.supporting-pane-layout__toggle:focus-visible { - outline: 2px solid var(--color-primary, #3b82f6); - outline-offset: 2px; -} - -// Resize handle for desktop -.supporting-pane-layout__pane { - @media (min-width: $semantic-breakpoint-md) { - position: relative; - - &::before { - content: ''; - position: absolute; - background-color: transparent; - transition: background-color $semantic-motion-duration-fast $semantic-motion-easing-ease-out; - - &:hover { - background-color: var(--color-primary, #3b82f6); - } - } - - .supporting-pane-layout--right & { - &::before { - left: 0; - top: 0; - bottom: 0; - width: 3px; - cursor: col-resize; - } - } - - .supporting-pane-layout--left & { - &::before { - right: 0; - top: 0; - bottom: 0; - width: 3px; - cursor: col-resize; - } - } - - .supporting-pane-layout--bottom & { - &::before { - top: 0; - left: 0; - right: 0; - height: 3px; - cursor: row-resize; - } - } - } -} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/layouts/supporting-pane-layout.component.ts b/projects/ui-essentials/src/lib/layouts/supporting-pane-layout.component.ts deleted file mode 100644 index 58175c2..0000000 --- a/projects/ui-essentials/src/lib/layouts/supporting-pane-layout.component.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -@Component({ - selector: 'ui-supporting-pane-layout', - standalone: true, - imports: [CommonModule], - template: ` -
-
-
- -
- -
-
- -
- - -
-
- -
-
-
- -
-
- -
-
- -
- -
- - -
- -
- -
-
- `, - styleUrl: './supporting-pane-layout.component.scss' -}) -export class SupportingPaneLayoutComponent { - @Input() panePosition: 'right' | 'left' | 'bottom' = 'right'; - @Input() paneCollapsed: boolean = false; - @Input() showMainHeader: boolean = true; - @Input() showPaneFooter: boolean = false; - @Input() showToggle: boolean = true; -} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/layouts/tab-container.component.ts b/projects/ui-essentials/src/lib/layouts/tab-container.component.ts deleted file mode 100644 index c93c368..0000000 --- a/projects/ui-essentials/src/lib/layouts/tab-container.component.ts +++ /dev/null @@ -1,517 +0,0 @@ -import { - Component, - Input, - Output, - EventEmitter, - OnInit, - OnDestroy, - ChangeDetectionStrategy, - signal, - computed, - ContentChildren, - QueryList, - AfterContentInit, - ElementRef, - ViewChild, - effect -} from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; -import { IconDefinition } from '@fortawesome/fontawesome-svg-core'; - -export interface TabItem { - id: string; - label: string; - icon?: string | IconDefinition; - disabled?: boolean; - closeable?: boolean; - badge?: string | number; - tooltip?: string; - data?: any; -} - -export interface TabChangeEvent { - activeTab: TabItem; - previousTab: TabItem | null; - activeIndex: number; - previousIndex: number; -} - -export interface TabCloseEvent { - tab: TabItem; - index: number; -} - -export type TabOrientation = 'horizontal' | 'vertical'; -export type TabVariant = 'default' | 'pills' | 'underline' | 'boxed'; -export type TabSize = 'sm' | 'md' | 'lg'; - -@Component({ - selector: 'ui-tab-container', - standalone: true, - imports: [CommonModule], - changeDetection: ChangeDetectionStrategy.OnPush, - template: ` -
- - -
- - @if (scrollableTabs && showScrollButtons()) { - - } - -
- - @for (tab of tabs(); track tab.id) { - - } - - } - - @if (showAddButton) { - - } -
- - @if (scrollableTabs && showScrollButtons()) { - - } -
- - -
- @for (tab of tabs(); track tab.id) { -
- - @if (lazyLoading) { - @if (tab.id === activeTabId() || loadedTabs().has(tab.id)) { - - } - } @else { - - } -
- } - - @if (tabs().length === 0 && emptyStateTemplate) { -
- -
- } -
-
- `, - styles: [` - .tab-container { - display: flex; - width: 100%; - height: 100%; - } - - .tab-container[data-orientation="horizontal"] { - flex-direction: column; - } - - .tab-container[data-orientation="vertical"] { - flex-direction: row; - } - - .tab-nav { - display: flex; - border-bottom: 1px solid #e9ecef; - } - - .tab-button { - padding: 0.5rem 1rem; - border: none; - background: transparent; - cursor: pointer; - border-bottom: 2px solid transparent; - } - - .tab-button.active { - border-bottom-color: #007bff; - color: #007bff; - } - - .tab-content { - flex: 1; - padding: 1rem; - } - - .tab-panel { - display: none; - } - - .tab-panel.active { - display: block; - } - `] -}) -export class TabContainerComponent implements OnInit, AfterContentInit, OnDestroy { - @ViewChild('tabNavContent') tabNavContent!: ElementRef; - - // Configuration - @Input() tabs = signal([]); - @Input() activeTabId = signal(''); - @Input() orientation: TabOrientation = 'horizontal'; - @Input() variant: TabVariant = 'default'; - @Input() size: TabSize = 'md'; - - // Behavior - @Input() allowTabClose = false; - @Input() showAddButton = false; - @Input() scrollableTabs = false; - @Input() animateTransitions = true; - @Input() lazyLoading = false; - @Input() keepAliveInactive = false; - @Input() emptyStateTemplate = false; - - // Keyboard navigation - @Input() keyboardNavigation = true; - @Input() focusOnSelect = true; - - // Events - @Output() tabChange = new EventEmitter(); - @Output() tabClose = new EventEmitter(); - @Output() tabAdd = new EventEmitter(); - @Output() tabReorder = new EventEmitter<{ from: number; to: number }>(); - - private destroy$ = new Subject(); - private previousActiveTabId = signal(''); - protected loadedTabs = signal>(new Set()); - private scrollPosition = signal(0); - private tabNavWidth = signal(0); - private tabNavScrollWidth = signal(0); - - // Computed properties - protected containerClasses = computed(() => { - const classes = ['tab-container']; - - if (this.scrollableTabs) classes.push('scrollable'); - if (this.animateTransitions) classes.push('animated'); - if (this.allowTabClose) classes.push('closeable'); - - return classes.join(' '); - }); - - protected showScrollButtons = computed(() => { - return this.scrollableTabs && this.tabNavScrollWidth() > this.tabNavWidth(); - }); - - protected canScrollPrev = computed(() => this.scrollPosition() > 0); - - protected canScrollNext = computed(() => { - const maxScroll = this.tabNavScrollWidth() - this.tabNavWidth(); - return this.scrollPosition() < maxScroll; - }); - - constructor() { - // Effect to handle tab changes - effect(() => { - const currentId = this.activeTabId(); - const previousId = this.previousActiveTabId(); - - if (currentId && currentId !== previousId) { - this.handleTabChange(currentId, previousId); - } - }); - - // Effect to mark tabs as loaded for lazy loading - effect(() => { - const activeId = this.activeTabId(); - if (activeId && this.lazyLoading) { - this.loadedTabs.update(loaded => new Set([...loaded, activeId])); - } - }); - } - - ngOnInit(): void { - // Set initial active tab if not specified - if (!this.activeTabId() && this.tabs().length > 0) { - const firstEnabledTab = this.tabs().find(tab => !tab.disabled); - if (firstEnabledTab) { - this.activeTabId.set(firstEnabledTab.id); - } - } - } - - ngAfterContentInit(): void { - if (this.scrollableTabs) { - this.setupScrollObserver(); - } - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - /** - * Selects a tab by ID or tab object - */ - selectTab(tabOrId: TabItem | string): void { - const tab = typeof tabOrId === 'string' - ? this.tabs().find(t => t.id === tabOrId) - : tabOrId; - - if (!tab || tab.disabled || tab.id === this.activeTabId()) return; - - this.previousActiveTabId.set(this.activeTabId()); - this.activeTabId.set(tab.id); - - if (this.focusOnSelect) { - setTimeout(() => this.focusActiveTab(), 0); - } - } - - /** - * Closes a tab - */ - closeTab(event: Event, tab: TabItem): void { - event.stopPropagation(); - - const index = this.tabs().findIndex(t => t.id === tab.id); - if (index === -1) return; - - const closeEvent: TabCloseEvent = { tab, index }; - this.tabClose.emit(closeEvent); - - // If this was the active tab, select another one - if (tab.id === this.activeTabId()) { - const newTabs = this.tabs().filter(t => t.id !== tab.id); - if (newTabs.length > 0) { - const nextIndex = Math.min(index, newTabs.length - 1); - this.selectTab(newTabs[nextIndex]); - } else { - this.activeTabId.set(''); - } - } - - // Remove from tabs - this.tabs.update(tabs => tabs.filter(t => t.id !== tab.id)); - } - - /** - * Adds a new tab (emits event for parent to handle) - */ - addTab(): void { - this.tabAdd.emit(); - } - - /** - * Scrolls the tab navigation - */ - scrollTabs(direction: 'prev' | 'next'): void { - if (!this.tabNavContent) return; - - const scrollAmount = 200; - const currentScroll = this.scrollPosition(); - const newScroll = direction === 'prev' - ? Math.max(0, currentScroll - scrollAmount) - : Math.min(this.tabNavScrollWidth() - this.tabNavWidth(), currentScroll + scrollAmount); - - this.tabNavContent.nativeElement.scrollTo({ - left: newScroll, - behavior: 'smooth' - }); - } - - /** - * Handles keyboard navigation - */ - handleTabKeyDown(event: KeyboardEvent, tab: TabItem): void { - if (!this.keyboardNavigation) return; - - const tabs = this.tabs().filter(t => !t.disabled); - const currentIndex = tabs.findIndex(t => t.id === tab.id); - - let targetIndex = currentIndex; - - switch (event.key) { - case 'ArrowLeft': - case 'ArrowUp': - event.preventDefault(); - targetIndex = currentIndex > 0 ? currentIndex - 1 : tabs.length - 1; - break; - - case 'ArrowRight': - case 'ArrowDown': - event.preventDefault(); - targetIndex = currentIndex < tabs.length - 1 ? currentIndex + 1 : 0; - break; - - case 'Home': - event.preventDefault(); - targetIndex = 0; - break; - - case 'End': - event.preventDefault(); - targetIndex = tabs.length - 1; - break; - - case 'Enter': - case ' ': - event.preventDefault(); - this.selectTab(tab); - return; - - case 'Delete': - case 'Backspace': - if (tab.closeable && this.allowTabClose) { - event.preventDefault(); - this.closeTab(event, tab); - } - return; - } - - if (targetIndex !== currentIndex) { - this.focusTab(tabs[targetIndex].id); - } - } - - /** - * Focuses a specific tab - */ - private focusTab(tabId: string): void { - const tabElement = document.getElementById(`tab-${tabId}`); - if (tabElement) { - tabElement.focus(); - } - } - - /** - * Focuses the currently active tab - */ - private focusActiveTab(): void { - const activeId = this.activeTabId(); - if (activeId) { - this.focusTab(activeId); - } - } - - /** - * Handles tab change events - */ - private handleTabChange(currentId: string, previousId: string): void { - const currentTab = this.tabs().find(t => t.id === currentId); - const previousTab = this.tabs().find(t => t.id === previousId); - - if (currentTab) { - const currentIndex = this.tabs().findIndex(t => t.id === currentId); - const previousIndex = previousId ? this.tabs().findIndex(t => t.id === previousId) : -1; - - const changeEvent: TabChangeEvent = { - activeTab: currentTab, - previousTab: previousTab || null, - activeIndex: currentIndex, - previousIndex - }; - - this.tabChange.emit(changeEvent); - } - } - - /** - * Sets up scroll observer for scrollable tabs - */ - private setupScrollObserver(): void { - if (!this.tabNavContent) return; - - const element = this.tabNavContent.nativeElement; - - // Update dimensions - const updateDimensions = () => { - this.tabNavWidth.set(element.clientWidth); - this.tabNavScrollWidth.set(element.scrollWidth); - }; - - updateDimensions(); - - // Listen for scroll events - element.addEventListener('scroll', () => { - this.scrollPosition.set(element.scrollLeft); - }); - - // Listen for resize - if (typeof ResizeObserver !== 'undefined') { - const resizeObserver = new ResizeObserver(updateDimensions); - resizeObserver.observe(element); - } - } -} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/layouts/widget-container.component.scss b/projects/ui-essentials/src/lib/layouts/widget-container.component.scss deleted file mode 100644 index 0e14f07..0000000 --- a/projects/ui-essentials/src/lib/layouts/widget-container.component.scss +++ /dev/null @@ -1,331 +0,0 @@ -@use "../../../../ui-design-system/src/styles/semantic/index" as *; - -.widget-container { - display: flex; - flex-direction: column; - overflow: hidden; - position: relative; - transition: - transform $semantic-motion-duration-fast $semantic-motion-easing-ease-out, - box-shadow $semantic-motion-duration-fast $semantic-motion-easing-ease-out, - border-color $semantic-motion-duration-fast $semantic-motion-easing-ease-out, - background-color $semantic-motion-duration-fast $semantic-motion-easing-ease-out; - - // Base styling - background-color: var(--color-background-surface, #fff); - border-radius: $semantic-border-radius-lg; - - // Minimum height for consistent appearance - min-height: 120px; -} - -// Variant styles -.widget-container--default { - background-color: var(--color-background-surface, #fff); - border: 1px solid var(--color-border-subtle, #e5e7eb); - box-shadow: $semantic-shadow-sm; -} - -.widget-container--elevated { - background-color: var(--color-background-surface, #fff); - border: 1px solid var(--color-border-subtle, #f3f4f6); - box-shadow: $semantic-shadow-md; -} - -.widget-container--outlined { - background-color: var(--color-background-surface, #fff); - border: 2px solid var(--color-border-primary, #d1d5db); - box-shadow: none; -} - -.widget-container--flat { - background-color: var(--color-background-subtle, #f9fafb); - border: none; - box-shadow: none; -} - -// Interactive states -.widget-container.interactive { - cursor: pointer; - - &:hover { - transform: $semantic-motion-hover-transform-lift-sm; - - &.widget-container--default { - box-shadow: $semantic-shadow-md; - border-color: var(--color-border-primary, #d1d5db); - } - - &.widget-container--elevated { - box-shadow: $semantic-shadow-lg; - border-color: var(--color-border-primary, #d1d5db); - } - - &.widget-container--outlined { - border-color: var(--color-primary, #3b82f6); - background-color: var(--color-background-subtle, #f9fafb); - } - - &.widget-container--flat { - background-color: var(--color-background-surface, #fff); - box-shadow: $semantic-shadow-sm; - } - } - - &:active { - transform: $semantic-motion-hover-transform-lift-sm scale(0.98); - } - - &:focus-visible { - outline: 2px solid var(--color-primary, #3b82f6); - outline-offset: 2px; - } -} - -// Loading state -.widget-container.loading { - pointer-events: none; - - .widget-container__content { - opacity: 0.6; - filter: blur(1px); - transition: - opacity $semantic-motion-duration-normal $semantic-motion-easing-ease-out, - filter $semantic-motion-duration-normal $semantic-motion-easing-ease-out; - } -} - -// Header section -.widget-container__header { - padding: $semantic-spacing-4; - border-bottom: 1px solid var(--color-border-subtle, #f3f4f6); - background-color: var(--color-background-subtle, #fafafa); - border-top-left-radius: $semantic-border-radius-lg; - border-top-right-radius: $semantic-border-radius-lg; - flex-shrink: 0; - - // Responsive padding - @media (min-width: $semantic-breakpoint-md) { - padding: $semantic-spacing-5; - } - - // Layout for title and actions - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: $semantic-spacing-3; -} - -.widget-container__title { - flex: 1; - min-width: 0; -} - -.widget-container__subtitle { - margin-top: $semantic-spacing-1; - opacity: 0.8; -} - -.widget-container__actions { - display: flex; - align-items: center; - gap: $semantic-spacing-2; - flex-shrink: 0; -} - -// Content section -.widget-container__content { - flex: 1; - position: relative; - overflow: hidden; - padding: $semantic-spacing-4; - - @media (min-width: $semantic-breakpoint-md) { - padding: $semantic-spacing-5; - } - - // Auto-scroll for overflow content - overflow-y: auto; - - // Custom scrollbar - &::-webkit-scrollbar { - width: 6px; - } - - &::-webkit-scrollbar-track { - background: var(--color-background-subtle, #f3f4f6); - border-radius: 3px; - } - - &::-webkit-scrollbar-thumb { - background: var(--color-border-primary, #d1d5db); - border-radius: 3px; - - &:hover { - background: var(--color-border-strong, #9ca3af); - } - } -} - -// Loading overlay -.widget-container__loading-overlay { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - display: flex; - align-items: center; - justify-content: center; - @include glass-effect('frosted', $css-glass-blur-xs, false, false); - z-index: $semantic-z-index-overlay; - - animation: fadeIn $semantic-motion-duration-fast $semantic-motion-easing-ease-out; -} - -// Footer section -.widget-container__footer { - padding: $semantic-spacing-4; - border-top: 1px solid var(--color-border-subtle, #f3f4f6); - background-color: var(--color-background-subtle, #fafafa); - border-bottom-left-radius: $semantic-border-radius-lg; - border-bottom-right-radius: $semantic-border-radius-lg; - flex-shrink: 0; - - @media (min-width: $semantic-breakpoint-md) { - padding: $semantic-spacing-4 $semantic-spacing-5; - } -} - -// Special styling for header-only widgets -.widget-container:not(:has(.widget-container__content)) { - .widget-container__header { - border-bottom: none; - border-radius: $semantic-border-radius-lg; - } -} - -// Special styling for content-only widgets -.widget-container:not(:has(.widget-container__header)):not(:has(.widget-container__footer)) { - .widget-container__content { - border-radius: $semantic-border-radius-lg; - } -} - -// Responsive adjustments for small screens -@media (max-width: $semantic-breakpoint-sm) { - .widget-container { - min-height: 100px; - } - - .widget-container__header, - .widget-container__content, - .widget-container__footer { - padding: $semantic-spacing-3; - } - - .widget-container__header { - flex-direction: column; - align-items: stretch; - gap: $semantic-spacing-2; - } - - .widget-container__actions { - justify-content: flex-end; - } -} - -// Animation for content changes -.widget-container__content > * { - animation: contentFadeIn $semantic-motion-duration-normal $semantic-motion-easing-ease-out; -} - -// Skeleton loading state -.widget-container.loading { - .widget-container__header::after, - .widget-container__content::after { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: linear-gradient( - 90deg, - transparent, - rgba(255, 255, 255, 0.4), - transparent - ); - animation: shimmer $semantic-motion-duration-slower linear infinite; - } - - .widget-container__header { - position: relative; - overflow: hidden; - } - - .widget-container__content { - position: relative; - overflow: hidden; - } -} - -// Error state (can be toggled with additional class) -.widget-container.error { - border-color: var(--color-error, #ef4444); - - .widget-container__header { - background-color: var(--color-error-subtle, #fef2f2); - border-color: var(--color-error, #ef4444); - } -} - -// Success state (can be toggled with additional class) -.widget-container.success { - border-color: var(--color-success, #10b981); - - .widget-container__header { - background-color: var(--color-success-subtle, #f0fdf4); - border-color: var(--color-success, #10b981); - } -} - -// Warning state (can be toggled with additional class) -.widget-container.warning { - border-color: var(--color-warning, #f59e0b); - - .widget-container__header { - background-color: var(--color-warning-subtle, #fffbeb); - border-color: var(--color-warning, #f59e0b); - } -} - -// Animation keyframes -@keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -@keyframes contentFadeIn { - from { - opacity: 0; - transform: translateY($semantic-spacing-2); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes shimmer { - 0% { - transform: translateX(-100%); - } - 100% { - transform: translateX(100%); - } -} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/layouts/widget-container.component.ts b/projects/ui-essentials/src/lib/layouts/widget-container.component.ts deleted file mode 100644 index c806446..0000000 --- a/projects/ui-essentials/src/lib/layouts/widget-container.component.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -@Component({ - selector: 'ui-widget-container', - standalone: true, - imports: [CommonModule], - template: ` -
-
-
- -
-
- -
-
- -
-
- -
-
- -
- - -
- - -
- `, - styleUrl: './widget-container.component.scss' -}) -export class WidgetContainerComponent { - @Input() variant: 'default' | 'elevated' | 'outlined' | 'flat' = 'default'; - @Input() interactive: boolean = false; - @Input() loading: boolean = false; - @Input() showHeader: boolean = true; - @Input() showSubtitle: boolean = false; - @Input() showFooter: boolean = false; -} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/layouts/widget-grid-layout.component.scss b/projects/ui-essentials/src/lib/layouts/widget-grid-layout.component.scss deleted file mode 100644 index e88f2fb..0000000 --- a/projects/ui-essentials/src/lib/layouts/widget-grid-layout.component.scss +++ /dev/null @@ -1,124 +0,0 @@ -@use "../../../../ui-design-system/src/styles/semantic/index" as *; - -.widget-grid { - display: grid; - gap: $semantic-spacing-4; - padding: $semantic-spacing-4; - transition: - grid-template-columns $semantic-motion-duration-normal $semantic-motion-easing-ease-in-out, - gap $semantic-motion-duration-fast $semantic-motion-easing-ease-out; - - // Mobile: Single column - grid-template-columns: 1fr; - - // Tablet: 2 columns - @media (min-width: $semantic-breakpoint-sm) { - grid-template-columns: repeat(2, 1fr); - gap: $semantic-spacing-5; - padding: $semantic-spacing-5; - } - - // Desktop: Auto-fit or fixed columns based on size - @media (min-width: $semantic-breakpoint-lg) { - gap: $semantic-spacing-6; - padding: $semantic-spacing-6; - } - - // Auto-fit behavior (default) - &[data-auto-fit="true"] { - @media (min-width: $semantic-breakpoint-lg) { - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - } - - @media (min-width: $semantic-sizing-breakpoint-wide) { - grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); - } - } - - // Small grid size (more columns, smaller widgets) - &.widget-grid--sm { - @media (min-width: $semantic-breakpoint-md) { - grid-template-columns: repeat(3, 1fr); - } - - @media (min-width: $semantic-breakpoint-lg) { - grid-template-columns: repeat(4, 1fr); - } - - @media (min-width: $semantic-sizing-breakpoint-wide) { - grid-template-columns: repeat(5, 1fr); - } - - &[data-auto-fit="true"] { - @media (min-width: $semantic-breakpoint-lg) { - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - } - } - } - - // Medium grid size (balanced) - &.widget-grid--md { - @media (min-width: $semantic-breakpoint-lg) { - grid-template-columns: repeat(3, 1fr); - } - - @media (min-width: $semantic-sizing-breakpoint-wide) { - grid-template-columns: repeat(4, 1fr); - } - - &[data-auto-fit="true"] { - @media (min-width: $semantic-breakpoint-lg) { - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - } - } - } - - // Large grid size (fewer, larger widgets) - &.widget-grid--lg { - @media (min-width: $semantic-breakpoint-lg) { - grid-template-columns: repeat(2, 1fr); - } - - @media (min-width: $semantic-sizing-breakpoint-wide) { - grid-template-columns: repeat(3, 1fr); - } - - &[data-auto-fit="true"] { - @media (min-width: $semantic-breakpoint-lg) { - grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); - } - } - } -} - -// Widget item styling (for direct children) -.widget-grid > * { - transition: - transform $semantic-motion-duration-fast $semantic-motion-easing-ease-out, - box-shadow $semantic-motion-duration-fast $semantic-motion-easing-ease-out; - - &:hover { - transform: $semantic-motion-hover-transform-lift-sm; - } - - // Stagger animation on load - @for $i from 1 through 20 { - &:nth-child(#{$i}) { - animation: slideInUp $semantic-motion-duration-normal $semantic-motion-easing-ease-out; - animation-delay: calc(#{$semantic-motion-transition-delay-75} * #{$i}); - animation-fill-mode: both; - } - } -} - -// Slide in animation for widgets -@keyframes slideInUp { - from { - opacity: 0; - transform: translateY($semantic-spacing-4); - } - to { - opacity: 1; - transform: translateY(0); - } -} \ No newline at end of file diff --git a/projects/ui-essentials/src/lib/layouts/widget-grid-layout.component.ts b/projects/ui-essentials/src/lib/layouts/widget-grid-layout.component.ts deleted file mode 100644 index b4c639c..0000000 --- a/projects/ui-essentials/src/lib/layouts/widget-grid-layout.component.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -@Component({ - selector: 'ui-widget-grid-layout', - standalone: true, - imports: [CommonModule], - template: ` -
- -
- `, - styleUrl: './widget-grid-layout.component.scss' -}) -export class WidgetGridLayoutComponent { - @Input() gridSize: 'sm' | 'md' | 'lg' = 'md'; - @Input() autoFit: boolean = true; -} \ No newline at end of file diff --git a/projects/ui-essentials/src/public-api.ts b/projects/ui-essentials/src/public-api.ts index f410e8d..25e1fad 100644 --- a/projects/ui-essentials/src/public-api.ts +++ b/projects/ui-essentials/src/public-api.ts @@ -12,21 +12,6 @@ export * from './lib/components/overlays/index'; export * from './lib/components/layout/index'; // Layout Components (avoiding conflicts with navigation tab components) -export { DashboardShellLayoutComponent } from './lib/layouts/dashboard-shell-layout.component'; -export { WidgetGridLayoutComponent } from './lib/layouts/widget-grid-layout.component'; -export { KpiCardLayoutComponent } from './lib/layouts/kpi-card-layout.component'; -export { BentoGridLayoutComponent } from './lib/layouts/bento-grid-layout.component'; -export { ListDetailLayoutComponent } from './lib/layouts/list-detail-layout.component'; -export { FeedLayoutComponent } from './lib/layouts/feed-layout.component'; -export { SupportingPaneLayoutComponent } from './lib/layouts/supporting-pane-layout.component'; -export { WidgetContainerComponent } from './lib/layouts/widget-container.component'; -export { GridContainerComponent } from './lib/layouts/grid-container.component'; -export { ScrollContainerComponent } from './lib/layouts/scroll-container.component'; +// Layout components removed for recreation export { LoadingStateContainerComponent } from './lib/layouts/loading-state-container.component'; - -// Layout component types -export { TabContainerComponent } from './lib/layouts/tab-container.component'; -export type { TabItem as LayoutTabItem, TabChangeEvent as LayoutTabChangeEvent, TabCloseEvent as LayoutTabCloseEvent } from './lib/layouts/tab-container.component'; -export type { VirtualScrollConfig } from './lib/layouts/scroll-container.component'; -export type { GridResponsiveConfig } from './lib/layouts/grid-container.component'; export type { LoadingState, ErrorState } from './lib/layouts/loading-state-container.component';