diff --git a/COMPONENT_CREATION_TEMPLATE.md b/COMPONENT_CREATION_TEMPLATE.md
new file mode 100644
index 0000000..f208fd5
--- /dev/null
+++ b/COMPONENT_CREATION_TEMPLATE.md
@@ -0,0 +1,884 @@
+# Component Creation Template
+
+## ⚠️ CRITICAL RESTRICTIONS ⚠️
+
+### DO NOT MODIFY EXISTING COMPONENTS
+- **NEVER** alter Angular or SCSS code of already built components
+- **NEVER** change existing component files unless explicitly instructed
+- **ONLY** create NEW components as specified in the task
+- **ONLY** update integration files (routes, menu, public API) to add references to new components
+- If asked to modify an existing component, REQUEST CLARIFICATION before proceeding
+- Existing components are PRODUCTION CODE and must remain untouched
+
+### Protected Files and Directories
+The following should NEVER be modified without explicit permission:
+- Any existing component files in `@projects/ui-essentials/src/lib/components/`
+- Any existing demo files in `@projects/demo-ui-essentials/src/app/demos/`
+- Only ADD new entries to:
+ - `demos.routes.ts` (add new cases, don't modify existing)
+ - `dashboard.component.ts` (add new menu items, don't modify existing)
+ - `public-api.ts` (add new exports, don't modify existing)
+
+---
+
+## Component Creation Task Template
+
+### 1. Component Selection
+
+ - Alert/Notification
+
+
+### 2. Pre-Development Checklist
+- [ ] Check if similar component exists in codebase
+- [ ] Review existing components in same category for patterns
+- [ ] Identify required design tokens from semantic layer
+- [ ] Plan component API (inputs, outputs, content projection)
+- [ ] **CONFIRM: This is a NEW component, not a modification of an existing one**
+
+### 3. Implementation Steps
+
+#### Step 1: Create Component Structure
+**Location:** `@projects/ui-essentials/src/lib/components/[category]/[component-name]/`
+
+Create these NEW files (do not overwrite existing):
+- `[component-name].component.ts`
+- `[component-name].component.scss`
+- `index.ts` (barrel export)
+
+#### Step 2: SCSS Implementation
+Follow the **Design Token Guidelines** below to create component styles.
+
+#### Step 3: Angular Component Implementation
+Follow the **Angular Component Guidelines** below.
+
+#### Step 4: Create Demo Component
+**Location:** `@projects/demo-ui-essentials/src/app/demos/[component-name]-demo/`
+- Create NEW demo component showing all variants, sizes, and states
+- Include interactive examples and code snippets
+- Follow existing demo patterns
+
+#### Step 5: Integration Updates (ADDITIVE ONLY)
+
+##### A. Update Routes (ADD ONLY - DO NOT MODIFY EXISTING)
+**File:** `@projects/demo-ui-essentials/src/app/demos/demos.routes.ts`
+```typescript
+// Add import - DO NOT MODIFY EXISTING IMPORTS
+import { [ComponentName]DemoComponent } from './[component-name]-demo/[component-name]-demo.component';
+
+// Add to template switch case - DO NOT MODIFY EXISTING CASES
+@case ("[component-name]") {
+
+}
+
+// Add to imports array - DO NOT REMOVE OR MODIFY EXISTING ENTRIES
+imports: [...existing, [ComponentName]DemoComponent]
+```
+
+##### B. Update Dashboard Menu (ADD ONLY - DO NOT MODIFY EXISTING)
+**File:** `@projects/demo-ui-essentials/src/app/features/dashboard/dashboard.component.ts`
+```typescript
+// Import appropriate FontAwesome icon - DO NOT MODIFY EXISTING IMPORTS
+import { faIconName } from '@fortawesome/free-solid-svg-icons';
+
+// Add in ngOnInit() - DO NOT MODIFY EXISTING MENU ITEMS
+this.addMenuItem("[component-id]", "[Component Label]", this.faIconName);
+```
+
+##### C. Update Public API (ADD ONLY - DO NOT MODIFY EXISTING)
+**File:** `@projects/ui-essentials/src/public-api.ts`
+```typescript
+// Add export in appropriate section - DO NOT MODIFY EXISTING EXPORTS
+export * from './lib/components/[category]/[component-name]';
+```
+
+### ⚠️ INTEGRATION WARNING ⚠️
+When updating integration files:
+- **ONLY ADD** new entries
+- **NEVER DELETE** existing entries
+- **NEVER MODIFY** existing entries
+- **PRESERVE** all existing formatting and structure
+- **TEST** that existing components still work after changes
+
+---
+
+## Design Token Guidelines ⚠️ CRITICAL: ONLY USE EXISTING TOKENS ⚠️
+
+### MANDATORY: Import and Use Semantic Tokens
+```scss
+@use '@projects/shared-ui/src/styles/semantic' as *;
+```
+
+### 🚨 TOKEN VALIDATION RULES 🚨
+- ❌ **NEVER** invent or guess token names
+- ❌ **NEVER** use tokens not listed below
+- ❌ **NEVER** hardcode values (px, rem, #hex colors, etc.)
+- ✅ **ALWAYS** use semantic tokens for ALL values
+- ✅ **ALWAYS** check this reference before using any token
+- ✅ **ASK FOR GUIDANCE** if needed token doesn't exist
+
+### Available Token Categories (VERIFIED ACTUAL TOKENS):
+
+## 1. **COLORS** (Single Values - Use Directly)
+```scss
+// Text Colors
+$semantic-color-text-primary // Primary text
+$semantic-color-text-secondary // Secondary text
+$semantic-color-text-tertiary // Tertiary text
+$semantic-color-text-disabled // Disabled text
+$semantic-color-text-inverse // Inverse text
+
+// Surface Colors
+$semantic-color-surface-primary // Primary surface
+$semantic-color-surface-secondary // Secondary surface
+$semantic-color-surface-elevated // Elevated surface
+$semantic-color-surface // Base surface
+$semantic-color-surface-container // Container surface
+
+// Border Colors
+$semantic-color-border-primary // Primary border
+$semantic-color-border-secondary // Secondary border
+$semantic-color-border-subtle // Subtle border
+$semantic-color-border-focus // Focus border
+$semantic-color-border-error // Error border
+
+// Brand Colors
+$semantic-color-primary // Primary brand
+$semantic-color-secondary // Secondary brand
+$semantic-color-on-primary // Text on primary
+$semantic-color-on-secondary // Text on secondary
+
+// Feedback Colors
+$semantic-color-success // Success state
+$semantic-color-warning // Warning state
+$semantic-color-danger // Danger/error state
+$semantic-color-info // Info state
+$semantic-color-on-success // Text on success
+$semantic-color-on-warning // Text on warning
+$semantic-color-on-danger // Text on danger
+$semantic-color-on-info // Text on info
+
+// Focus and Interactive
+$semantic-color-focus // Focus indicator
+$semantic-color-interactive-primary // Interactive primary
+$semantic-color-backdrop // Backdrop/overlay
+```
+
+## 2. **TYPOGRAPHY** (Maps and Single Values)
+
+### Typography Maps (Use with map-get())
+```scss
+// Headings (Maps - Extract individual properties)
+$semantic-typography-heading-h1 // h1 heading map
+$semantic-typography-heading-h2 // h2 heading map
+$semantic-typography-heading-h3 // h3 heading map
+$semantic-typography-heading-h4 // h4 heading map
+$semantic-typography-heading-h5 // h5 heading map
+
+// Body Text (Maps)
+$semantic-typography-body-large // Large body text map
+$semantic-typography-body-medium // Medium body text map
+$semantic-typography-body-small // Small body text map
+
+// UI Components (Maps)
+$semantic-typography-button-large // Large button text map
+$semantic-typography-button-medium // Medium button text map
+$semantic-typography-button-small // Small button text map
+$semantic-typography-label // Form label map
+$semantic-typography-input // Input text map
+$semantic-typography-caption // Caption text map
+
+// Usage Example for Maps:
+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);
+```
+
+### Typography Single Values (Use Directly)
+```scss
+// Font Weights
+$semantic-typography-font-weight-normal
+$semantic-typography-font-weight-medium
+$semantic-typography-font-weight-semibold
+$semantic-typography-font-weight-bold
+
+// Font Sizes
+$semantic-typography-font-size-xs
+$semantic-typography-font-size-sm
+$semantic-typography-font-size-md
+$semantic-typography-font-size-lg
+$semantic-typography-font-size-xl
+$semantic-typography-font-size-2xl
+$semantic-typography-font-size-3xl
+
+// Line Heights
+$semantic-typography-line-height-tight
+$semantic-typography-line-height-normal
+$semantic-typography-line-height-relaxed
+
+// Font Families
+$semantic-typography-font-family-sans
+$semantic-typography-font-family-serif
+$semantic-typography-font-family-mono
+```
+
+## 3. **SPACING** (Single Values - Use Directly)
+```scss
+// Component Spacing
+$semantic-spacing-component-xs // Extra small component spacing
+$semantic-spacing-component-sm // Small component spacing
+$semantic-spacing-component-md // Medium component spacing
+$semantic-spacing-component-lg // Large component spacing
+$semantic-spacing-component-xl // Extra large component spacing
+
+// Layout Spacing
+$semantic-spacing-layout-section-xs // Extra small section spacing
+$semantic-spacing-layout-section-sm // Small section spacing
+$semantic-spacing-layout-section-md // Medium section spacing
+$semantic-spacing-layout-section-lg // Large section spacing
+$semantic-spacing-layout-section-xl // Extra large section spacing
+
+// Content Spacing
+$semantic-spacing-content-paragraph // Paragraph spacing
+$semantic-spacing-content-heading // Heading spacing
+$semantic-spacing-content-list-item // List item spacing
+$semantic-spacing-content-line-tight // Tight line spacing
+
+// Interactive Spacing
+$semantic-spacing-interactive-button-padding-x // Button horizontal padding
+$semantic-spacing-interactive-button-padding-y // Button vertical padding
+$semantic-spacing-interactive-input-padding-x // Input horizontal padding
+$semantic-spacing-interactive-input-padding-y // Input vertical padding
+
+// Grid and Stack Spacing
+$semantic-spacing-grid-gap-sm // Small grid gap
+$semantic-spacing-grid-gap-md // Medium grid gap
+$semantic-spacing-grid-gap-lg // Large grid gap
+$semantic-spacing-stack-sm // Small stack spacing
+$semantic-spacing-stack-md // Medium stack spacing
+$semantic-spacing-stack-lg // Large stack spacing
+
+// Form Spacing
+$semantic-spacing-form-field-gap // Form field gap
+$semantic-spacing-form-group-gap // Form group gap
+```
+
+## 4. **BORDERS** (Single Values - Use Directly)
+```scss
+// Border Widths
+$semantic-border-width-1 // 1px border
+$semantic-border-width-2 // 2px border
+$semantic-border-card-width // Standard card border width
+
+// Border Radius
+$semantic-border-radius-sm // Small radius
+$semantic-border-radius-md // Medium radius
+$semantic-border-radius-lg // Large radius
+$semantic-border-radius-xl // Extra large radius
+$semantic-border-radius-full // Full/circle radius
+
+// Component Borders
+$semantic-border-button-radius // Button border radius
+$semantic-border-input-radius // Input border radius
+$semantic-border-card-radius // Card border radius
+```
+
+## 5. **SHADOWS** (Single Values - Use Directly)
+```scss
+// Elevation Shadows
+$semantic-shadow-elevation-0 // No shadow
+$semantic-shadow-elevation-1 // Level 1 elevation
+$semantic-shadow-elevation-2 // Level 2 elevation
+$semantic-shadow-elevation-3 // Level 3 elevation
+$semantic-shadow-elevation-4 // Level 4 elevation
+$semantic-shadow-elevation-5 // Level 5 elevation
+
+// Component Shadows
+$semantic-shadow-card-rest // Card default shadow
+$semantic-shadow-card-hover // Card hover shadow
+$semantic-shadow-button-rest // Button default shadow
+$semantic-shadow-button-hover // Button hover shadow
+$semantic-shadow-dropdown // Dropdown shadow
+$semantic-shadow-modal // Modal shadow
+```
+
+## 6. **MOTION** (Single Values and Maps)
+
+### Motion Single Values (Use Directly)
+```scss
+// Duration
+$semantic-motion-duration-fast // Fast duration
+$semantic-motion-duration-normal // Normal duration
+$semantic-motion-duration-slow // Slow duration
+
+// Easing
+$semantic-motion-easing-ease // Standard easing
+$semantic-motion-easing-ease-in-out // Ease in-out
+$semantic-motion-easing-spring // Spring easing
+```
+
+## 7. **SIZING** (Single Values - Use Directly)
+```scss
+// Button Heights
+$semantic-sizing-button-height-sm // Small button height
+$semantic-sizing-button-height-md // Medium button height
+$semantic-sizing-button-height-lg // Large button height
+
+// Input Heights
+$semantic-sizing-input-height-sm // Small input height
+$semantic-sizing-input-height-md // Medium input height
+$semantic-sizing-input-height-lg // Large input height
+
+// Icon Sizes
+$semantic-sizing-icon-inline // Inline icon size
+$semantic-sizing-icon-button // Button icon size
+$semantic-sizing-icon-navigation // Navigation icon size
+
+// Touch Targets
+$semantic-sizing-touch-minimum // Minimum touch target
+$semantic-sizing-touch-target // Standard touch target
+```
+
+## 8. **Z-INDEX** (Single Values - Use Directly)
+```scss
+$semantic-z-index-dropdown // Dropdown layer
+$semantic-z-index-modal // Modal layer
+$semantic-z-index-tooltip // Tooltip layer
+$semantic-z-index-overlay // Overlay layer
+```
+
+## 9. **OPACITY** (Single Values - Use Directly)
+```scss
+$semantic-opacity-disabled // Disabled opacity
+$semantic-opacity-hover // Hover opacity
+$semantic-opacity-backdrop // Backdrop opacity
+$semantic-opacity-subtle // Subtle opacity
+```
+
+### Strict Rules:
+- ❌ **NEVER** hardcode values (px, rem, #hex colors, etc.)
+- ❌ **NEVER** use base tokens directly (from `/base/` directory)
+- ❌ **NEVER** use magic numbers
+- ✅ **ALWAYS** use semantic tokens for ALL values
+- ✅ **ALWAYS** follow BEM naming convention for classes
+- ✅ **ALWAYS** use SCSS nesting appropriately (max 3 levels)
+- ✅ **ALWAYS** include responsive breakpoints using `$semantic-breakpoint-*` tokens
+- ✅ **ALWAYS** maintain consistency with existing components
+
+### Component SCSS Template (CORRECTED)
+```scss
+@use '@projects/shared-ui/src/styles/semantic' as *;
+
+.ui-[component-name] {
+ // Core Structure
+ display: flex;
+ position: relative;
+
+ // Layout & Spacing - ONLY USE ACTUAL TOKENS
+ padding: $semantic-spacing-component-md;
+ margin: $semantic-spacing-layout-section-sm;
+
+ // Visual Design - ONLY USE ACTUAL TOKENS
+ background: $semantic-color-surface-primary;
+ border: $semantic-border-card-width solid $semantic-color-border-primary;
+ border-radius: $semantic-border-card-radius;
+ box-shadow: $semantic-shadow-elevation-1;
+
+ // Typography - USE MAP-GET FOR MAPS, SINGLE VALUES DIRECTLY
+ 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;
+
+ // Transitions - ONLY USE ACTUAL TOKENS
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
+
+ // Sizing Variants - ONLY USE ACTUAL TOKENS
+ &--sm {
+ min-height: $semantic-sizing-button-height-sm;
+ padding: $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);
+ }
+
+ &--md {
+ min-height: $semantic-sizing-button-height-md;
+ padding: $semantic-spacing-component-sm;
+ // Typography already set above for medium
+ }
+
+ &--lg {
+ min-height: $semantic-sizing-button-height-lg;
+ padding: $semantic-spacing-component-lg;
+ 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 Variants - ONLY USE ACTUAL TOKENS
+ &--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;
+ }
+
+ &--danger {
+ background: $semantic-color-danger;
+ color: $semantic-color-on-danger;
+ border-color: $semantic-color-danger;
+ }
+
+ // State Variants - ONLY USE ACTUAL TOKENS
+ &--disabled {
+ opacity: $semantic-opacity-disabled;
+ cursor: not-allowed;
+ pointer-events: none;
+ }
+
+ // BEM Element - ONLY USE ACTUAL TOKENS
+ &__element {
+ padding: $semantic-spacing-content-line-tight;
+ color: $semantic-color-text-secondary;
+
+ // Element modifier
+ &--active {
+ color: $semantic-color-text-primary;
+ font-weight: $semantic-typography-font-weight-semibold;
+ }
+ }
+
+ // Interactive States - ONLY USE ACTUAL TOKENS
+ &:not(.ui-[component-name]--disabled) {
+ &:hover {
+ box-shadow: $semantic-shadow-card-hover;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+
+ &:active {
+ box-shadow: $semantic-shadow-card-rest;
+ }
+ }
+
+ // Responsive Design - USE ACTUAL BREAKPOINT TOKENS
+ @media (max-width: $semantic-breakpoint-md - 1) {
+ padding: $semantic-spacing-component-sm;
+ }
+
+ @media (max-width: $semantic-breakpoint-sm - 1) {
+ padding: $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);
+ }
+}
+```
+
+### 🚨 CRITICAL TOKEN VALIDATION 🚨
+
+#### ❌ COMMON MISTAKES TO AVOID:
+```scss
+// WRONG - These tokens DON'T EXIST:
+font-size: $semantic-typography-heading-h3; // This is a MAP, not a single value!
+padding: $semantic-spacing-layout-xs; // DOESN'T EXIST
+box-shadow: $semantic-shadow-card-featured; // DOESN'T EXIST
+border-radius: $semantic-border-radius-xs; // DOESN'T EXIST
+transition: $semantic-motion-duration-medium; // DOESN'T EXIST
+background: $semantic-color-background-primary; // DOESN'T EXIST
+
+// WRONG - Hardcoded values:
+padding: 16px; // Use $semantic-spacing-component-md
+color: #333333; // Use $semantic-color-text-primary
+border-radius: 8px; // Use $semantic-border-radius-md
+```
+
+#### ✅ CORRECT USAGE:
+```scss
+// RIGHT - Use map-get for typography maps:
+font-size: map-get($semantic-typography-heading-h3, font-size);
+font-weight: map-get($semantic-typography-heading-h3, font-weight);
+
+// RIGHT - Use single value tokens directly:
+padding: $semantic-spacing-component-md;
+color: $semantic-color-text-primary;
+border-radius: $semantic-border-card-radius;
+box-shadow: $semantic-shadow-elevation-2;
+```
+
+### ⚠️ MAP USAGE WARNING ⚠️
+These tokens are **MAPS** and require `map-get()`:
+- `$semantic-typography-heading-*` (h1, h2, h3, h4, h5)
+- `$semantic-typography-body-*` (large, medium, small)
+- `$semantic-typography-button-*` (large, medium, small)
+- `$semantic-typography-label`
+- `$semantic-typography-input`
+- `$semantic-typography-caption`
+
+**Always extract individual properties:**
+```scss
+// CORRECT MAP USAGE:
+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);
+```
+
+### Expected Deliverables:
+1. **SCSS file** properly structured with ONLY existing semantic tokens
+2. **BEM methodology** consistently applied throughout
+3. **Responsive design** using actual breakpoint tokens
+4. **Interactive states** (hover, focus, active) defined with real tokens
+5. **Zero hardcoded values** - everything uses verified design tokens
+6. **Proper map usage** for typography tokens
+7. **Comments** explaining major sections if complex
+
+### Quality Checklist:
+- [ ] All colors use actual `$semantic-color-*` tokens
+- [ ] All spacing uses actual `$semantic-spacing-*` tokens
+- [ ] All typography uses proper map-get() for maps or single tokens
+- [ ] All shadows use actual `$semantic-shadow-*` tokens
+- [ ] All borders use actual `$semantic-border-*` tokens
+- [ ] All motion uses actual `$semantic-motion-*` tokens
+- [ ] **No hardcoded values anywhere**
+- [ ] **No invented token names**
+- [ ] BEM naming convention followed
+- [ ] Responsive breakpoints implemented with actual tokens
+- [ ] File saved in correct directory
+
+---
+
+## Angular Component Guidelines (Enhanced)
+
+### Angular 19+ Development Standards:
+
+#### TEMPLATES:
+- Use inline templates in @Component decorator
+- Use new control flow syntax: @if, @for, @switch (NOT *ngIf, *ngFor)
+- Use @defer for lazy loading where appropriate
+- Put stylesheets in a separate scss file to the component
+
+#### STYLING:
+- Import design tokens: @use '@projects/shared-ui/src/styles/semantic' as *;
+- **CRITICAL:** Ensure correct semantic tokens are used. Do not invent new ones.
+- **If a semantic variable is required but doesn't exist, ASK FOR INSTRUCTION**
+- NO hardcoded values - use only design tokens for colors, spacing, typography, breakpoints
+- Follow BEM naming convention for class names
+- Use SCSS nesting judiciously (max 3 levels)
+- Prefer mixins and functions from the design system
+
+#### TYPESCRIPT:
+- Use standalone components (no NgModules)
+- Prefer signals over observables for state management
+- Use inject() function over constructor injection
+- Strong typing everywhere - avoid 'any'
+- Use readonly where applicable
+- Implement OnPush change detection by default
+
+#### STRUCTURE:
+- Feature-based folder structure
+- Barrel exports for clean imports
+- Smart/presentational component pattern
+- Services should be provided in root unless scoped needed
+
+### Component Template
+```typescript
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+type ComponentSize = 'sm' | 'md' | 'lg';
+type ComponentVariant = 'primary' | 'secondary' | 'success' | 'danger';
+
+@Component({
+ selector: 'ui-[component-name]',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ template: `
+
+
+ @if (loading) {
+
+ }
+
+
+
+ `,
+ styleUrl: './[component-name].component.scss'
+})
+export class [ComponentName]Component {
+ @Input() size: ComponentSize = 'md';
+ @Input() variant: ComponentVariant = 'primary';
+ @Input() disabled = false;
+ @Input() loading = false;
+ @Input() role = '[appropriate-aria-role]';
+ @Input() tabIndex = 0;
+
+ @Output() clicked = new EventEmitter();
+
+ handleClick(event: MouseEvent): void {
+ if (!this.disabled && !this.loading) {
+ this.clicked.emit(event);
+ }
+ }
+
+ handleKeydown(event: KeyboardEvent): void {
+ // Implement keyboard navigation
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ this.handleClick(event as any);
+ }
+ }
+}
+```
+
+### Demo Component Template
+```typescript
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { [ComponentName]Component } from '../[component-name].component';
+
+@Component({
+ selector: 'ui-[component-name]-demo',
+ standalone: true,
+ imports: [CommonModule, [ComponentName]Component],
+ template: `
+
+
[Component Name] Demo
+
+
+
+ Sizes
+
+ @for (size of sizes; track size) {
+
+ {{ size }} size
+
+ }
+
+
+
+
+
+ Variants
+
+ @for (variant of variants; track variant) {
+
+ {{ variant }}
+
+ }
+
+
+
+
+
+ States
+
+ Disabled
+ Loading
+
+
+
+
+
+ Interactive
+
+ Click me
+
+ Clicked: {{ clickCount }} times
+
+
+ `,
+ styleUrl: './[component-name]-demo.component.scss'
+})
+export class [ComponentName]DemoComponent {
+ sizes = ['sm', 'md', 'lg'] as const;
+ variants = ['primary', 'secondary', 'success', 'danger'] as const;
+ clickCount = 0;
+
+ handleDemoClick(event: MouseEvent): void {
+ this.clickCount++;
+ console.log('Component clicked', event);
+ }
+}
+```
+
+### Component Requirements
+- **Design Tokens**: Use ONLY verified semantic tokens throughout
+- **Variants**: Include common UI system variants (colors, styles)
+- **Sizes**: sm, md, lg sizing options
+- **States**: hover, focus, active, disabled
+- **Accessibility**: ARIA support, keyboard navigation
+
+---
+
+## Testing Requirements
+
+### Unit Test Template
+```typescript
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { [ComponentName]Component } from './[component-name].component';
+
+describe('[ComponentName]Component', () => {
+ let component: [ComponentName]Component;
+ let fixture: ComponentFixture<[ComponentName]Component>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [[ComponentName]Component]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent([ComponentName]Component);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should apply size classes correctly', () => {
+ component.size = 'lg';
+ fixture.detectChanges();
+
+ const element = fixture.nativeElement.querySelector('.ui-[component-name]');
+ expect(element).toHaveClass('ui-[component-name]--lg');
+ });
+
+ it('should handle disabled state', () => {
+ component.disabled = true;
+ fixture.detectChanges();
+
+ const element = fixture.nativeElement.querySelector('.ui-[component-name]');
+ expect(element).toHaveClass('ui-[component-name]--disabled');
+ expect(element.getAttribute('aria-disabled')).toBe('true');
+ });
+
+ it('should emit click events when not disabled', () => {
+ spyOn(component.clicked, 'emit');
+ const element = fixture.nativeElement.querySelector('.ui-[component-name]');
+
+ element.click();
+
+ expect(component.clicked.emit).toHaveBeenCalled();
+ });
+
+ it('should not emit click events when disabled', () => {
+ component.disabled = true;
+ spyOn(component.clicked, 'emit');
+ const element = fixture.nativeElement.querySelector('.ui-[component-name]');
+
+ element.click();
+
+ expect(component.clicked.emit).not.toHaveBeenCalled();
+ });
+
+ it('should support keyboard navigation', () => {
+ spyOn(component, 'handleClick');
+ const element = fixture.nativeElement.querySelector('.ui-[component-name]');
+
+ const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' });
+ element.dispatchEvent(enterEvent);
+
+ expect(component.handleClick).toHaveBeenCalled();
+ });
+});
+```
+
+---
+
+## Component Quality Checklist
+
+### ⚠️ PRE-FLIGHT CHECK ⚠️
+- [ ] **CONFIRMED: Creating NEW component, not modifying existing**
+- [ ] **VERIFIED: No existing component files will be altered**
+- [ ] **CHECKED: Only adding to integration files, not modifying**
+
+### Functionality
+- [ ] All size variants work correctly (sm, md, lg)
+- [ ] All color variants apply properly
+- [ ] Disabled state prevents interaction
+- [ ] Loading state shows indicator
+- [ ] Events emit correctly
+- [ ] Content projection works
+
+### Styling ⚠️ CRITICAL VALIDATION ⚠️
+- [ ] **Uses ONLY verified existing semantic tokens**
+- [ ] **NO HARDCODED VALUES** (px, rem, hex colors)
+- [ ] **NO INVENTED TOKEN NAMES**
+- [ ] Responsive on all breakpoints
+- [ ] Follows BEM naming convention
+- [ ] Proper state transitions
+- [ ] Map tokens used with map-get()
+
+### Accessibility
+- [ ] Proper ARIA attributes
+- [ ] Keyboard navigable
+- [ ] Focus indicators visible
+- [ ] Screen reader friendly
+- [ ] Meets WCAG 2.1 AA standards
+- [ ] Color contrast sufficient
+
+### Code Quality
+- [ ] TypeScript strictly typed
+- [ ] OnPush change detection
+- [ ] Standalone component
+- [ ] Proper imports/exports
+- [ ] Demo shows all features
+- [ ] Unit tests pass
+
+### Integration
+- [ ] Added to public API (additive only)
+- [ ] Demo route configured (additive only)
+- [ ] Menu item added to dashboard (additive only)
+- [ ] Build passes without errors
+- [ ] Follows project conventions
+- [ ] **Existing components still work**
+
+---
+
+## 🚨 FINAL TOKEN VALIDATION 🚨
+
+### Before Implementation:
+1. **VERIFY** every token used exists in the reference above
+2. **ASK FOR HELP** if you need a token that doesn't exist
+3. **TEST** build process to catch any invalid tokens
+4. **NEVER GUESS** or invent token names
+
+### If you need a token that doesn't exist:
+- **STOP** implementation
+- **ASK** "This component needs [specific token type] but I don't see one available. Should I [alternative approach] or do you have guidance?"
+- **WAIT** for direction rather than inventing tokens
+
+This template provides a comprehensive guide for creating consistent, high-quality NEW components that integrate seamlessly with the existing design system without affecting any existing code, **and most importantly, without using non-existent tokens that would cause build failures**.
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/accordion-demo/accordion-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/accordion-demo/accordion-demo.component.scss
new file mode 100644
index 0000000..0926090
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/accordion-demo/accordion-demo.component.scss
@@ -0,0 +1,194 @@
+@use '../../../../../shared-ui/src/styles/semantic/index' as *;
+
+.demo-container {
+ padding: $semantic-spacing-layout-lg;
+ max-width: 1200px;
+ margin: 0 auto;
+
+ h2 {
+ color: $semantic-color-text-primary;
+ font-size: $semantic-typography-heading-h2-size;
+ margin-bottom: $semantic-spacing-layout-lg;
+ border-bottom: 1px solid $semantic-color-border-subtle;
+ padding-bottom: $semantic-spacing-content-paragraph;
+ }
+
+ h3 {
+ color: $semantic-color-text-secondary;
+ font-size: $semantic-typography-heading-h3-size;
+ margin-bottom: $semantic-spacing-component-lg;
+ margin-top: $semantic-spacing-layout-lg;
+ }
+
+ h4 {
+ color: $semantic-color-text-primary;
+ font-size: $semantic-typography-heading-h4-size;
+ margin-bottom: $semantic-spacing-component-md;
+ }
+}
+
+.demo-section {
+ margin-bottom: $semantic-spacing-layout-xl;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+}
+
+.demo-row {
+ display: flex;
+ gap: $semantic-spacing-component-lg;
+ flex-wrap: wrap;
+ align-items: start;
+}
+
+.demo-column {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-component-lg;
+ flex: 1;
+ min-width: 300px;
+}
+
+.demo-accordion-container {
+ max-width: 600px;
+}
+
+.demo-controls {
+ display: flex;
+ gap: $semantic-spacing-component-md;
+ flex-wrap: wrap;
+ margin-bottom: $semantic-spacing-component-lg;
+ padding: $semantic-spacing-component-lg;
+ background: $semantic-color-surface-secondary;
+ border-radius: $semantic-border-radius-lg;
+ border: 1px solid $semantic-color-border-subtle;
+
+ button {
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
+ border: 1px solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-sm;
+ background: $semantic-color-surface-primary;
+ color: $semantic-color-text-primary;
+ font-size: $semantic-typography-font-size-sm;
+ cursor: pointer;
+ transition: all $semantic-motion-duration-fast $semantic-easing-standard;
+
+ &:hover {
+ background: $semantic-color-surface-secondary;
+ border-color: $semantic-color-border-primary;
+ }
+
+ &:focus {
+ outline: 2px solid $semantic-color-primary;
+ outline-offset: 2px;
+ }
+
+ &--primary {
+ background: $semantic-color-primary;
+ color: $semantic-color-on-primary;
+ border-color: $semantic-color-primary;
+
+ &:hover {
+ background: $semantic-color-primary;
+ opacity: 0.9;
+ }
+ }
+ }
+}
+
+.demo-item {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-component-sm;
+ padding: $semantic-spacing-component-md;
+ border: 1px solid $semantic-color-border-subtle;
+ border-radius: $semantic-border-radius-md;
+ background: $semantic-color-surface-primary;
+
+ h5 {
+ margin: 0 0 $semantic-spacing-component-xs 0;
+ font-size: $semantic-typography-font-size-sm;
+ font-weight: $semantic-typography-font-weight-medium;
+ color: $semantic-color-text-secondary;
+ }
+}
+
+.code-example {
+ background: $semantic-color-surface-secondary;
+ border: 1px solid $semantic-color-border-subtle;
+ border-radius: $semantic-border-radius-sm;
+ padding: $semantic-spacing-component-md;
+ font-family: $semantic-typography-font-family-mono;
+ font-size: $semantic-typography-font-size-xs;
+ color: $semantic-color-text-primary;
+ overflow-x: auto;
+ white-space: pre-wrap;
+ margin-top: $semantic-spacing-component-sm;
+}
+
+.sample-content {
+ line-height: $semantic-typography-line-height-relaxed;
+
+ p {
+ margin: 0 0 $semantic-spacing-component-sm 0;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ ul {
+ margin: 0 0 $semantic-spacing-component-sm $semantic-spacing-component-lg;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ li {
+ margin-bottom: $semantic-spacing-component-xs;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+}
+
+.icon-demo {
+ display: inline-flex;
+ align-items: center;
+ gap: $semantic-spacing-component-xs;
+ color: $semantic-color-primary;
+ font-weight: $semantic-typography-font-weight-medium;
+}
+
+// Responsive design
+@media (max-width: $semantic-breakpoint-md - 1) {
+ .demo-container {
+ padding: $semantic-spacing-layout-md;
+ }
+
+ .demo-row {
+ flex-direction: column;
+ }
+
+ .demo-column {
+ min-width: auto;
+ }
+
+ .demo-controls {
+ flex-direction: column;
+ gap: $semantic-spacing-component-sm;
+ }
+}
+
+@media (max-width: $semantic-breakpoint-sm - 1) {
+ .demo-container {
+ padding: $semantic-spacing-layout-sm;
+ }
+
+ .demo-item {
+ padding: $semantic-spacing-component-sm;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/accordion-demo/accordion-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/accordion-demo/accordion-demo.component.ts
new file mode 100644
index 0000000..4e16f57
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/accordion-demo/accordion-demo.component.ts
@@ -0,0 +1,355 @@
+import { Component, ViewChild } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { AccordionComponent, AccordionItemComponent, AccordionItem, AccordionExpandEvent } from '../../../../../ui-essentials/src/public-api';
+
+@Component({
+ selector: 'ui-accordion-demo',
+ standalone: true,
+ imports: [CommonModule, AccordionComponent, AccordionItemComponent],
+ template: `
+
+
Accordion Demo
+
+
+
+ Basic Usage
+
+
+
+
Single Expand (Default)
+
+
+
+
+
Welcome to our comprehensive guide on getting started with our platform.
+
This section covers the essential steps you need to know to begin your journey.
+
+
+
+
+
Explore our advanced features designed for power users.
+
+ Advanced analytics and reporting
+ Custom integrations and APIs
+ Enterprise-grade security features
+
+
+
+
+
+
Common issues and their solutions:
+
+ Login and authentication problems
+ Performance optimization tips
+ Data import and export issues
+
+
+
+
+
+
+
+
+
+
+
Multiple Expand
+
+
+
+
+
Manage your account preferences and personal information.
+
Update your profile, change password, and configure notification settings.
+
+
+
+
+
Control how your data is used and shared:
+
+ Data sharing preferences
+ Visibility settings
+ Third-party integrations
+
+
+
+
+
+
Customize your notification preferences to stay informed about what matters most to you.
+
+
+
+
+
+
+
+
+
+
+
+ Size Variants
+
+ @for (size of sizes; track size) {
+
+
+
Size: {{ size | titlecase }}
+
+
+
+
+
This is {{ size }} sized accordion content. The padding and font sizes adjust based on the size variant.
+
+
+
+
+
More content to demonstrate the {{ size }} size variant styling.
+
+
+
+
+
+
+ }
+
+
+
+
+
+ Style Variants
+
+ @for (variant of variants; track variant) {
+
+
+
{{ variant | titlecase }}
+
+
+
+
+
This {{ variant }} variant shows different visual styling approaches for the accordion component.
+
+
+
+
+
Each variant provides a different visual weight and emphasis level.
+
+
+
+
+
+
+ }
+
+
+
+
+
+ Programmatic Control
+
+
+ Expand Item 1
+ Expand Item 2
+ Expand Item 3
+ Expand All
+ Collapse All
+
+
+
+
+
+
+
+
+Current expanded items: {{ expandedItemIds | json }}
+Last toggled: {{ lastToggledItem | json }}
+
+
+
+
+
+
+ States
+
+
+
+
Disabled Accordion
+
+
+
+
+
This entire accordion is disabled and cannot be interacted with.
+
+
+
+
+
All items in this accordion are disabled.
+
+
+
+
+
+
+
+
+
+
Individual Disabled Items
+
+
+
+
+
This section is active and can be expanded/collapsed normally.
+
+
+
+
+
This individual section is disabled.
+
+
+
+
+
This section is also active and functional.
+
+
+
+
+
+
+
+
+
+
+
+ Custom Content
+
+
+
+
+
+
+
+
+ Custom Header with Icon
+
+
+
This accordion item uses custom header content with an icon.
+
The header slot allows for complete customization of the trigger area.
+
+
+
+
+
+
+ Rich Header Content
+
+ New
+
+
+
+
+
Complex header layouts are possible with custom slot content.
+
This example shows a header with a badge indicator.
+
+
+
+
+
+
+
+
+
+ Interactive Example
+
+
Event Handling
+
+
+
+
+
This accordion tracks all toggle events. Check the event log below.
+
+
+
+
+
Each interaction will be logged with detailed event information.
+
+
+
+
+
+
+Event Log ({{ eventLog.length }} events):
+{{ eventLog.length > 0 ? (eventLog | json) : 'No events yet...' }}
+
+
+
+
+ `,
+ styleUrl: './accordion-demo.component.scss'
+})
+export class AccordionDemoComponent {
+ @ViewChild('programmaticAccordion') programmaticAccordion!: AccordionComponent;
+
+ sizes = ['sm', 'md', 'lg'] as const;
+ variants = ['elevated', 'outlined', 'filled'] as const;
+
+ programmaticItems: AccordionItem[] = [
+ {
+ id: 'api-1',
+ title: 'API Documentation',
+ content: 'Complete API reference with examples and best practices for integration.',
+ expanded: false
+ },
+ {
+ id: 'api-2',
+ title: 'SDK Downloads',
+ content: 'Download our SDKs for various programming languages and frameworks.',
+ expanded: false
+ },
+ {
+ id: 'api-3',
+ title: 'Code Examples',
+ content: 'Real-world code examples and implementation patterns.',
+ expanded: false
+ }
+ ];
+
+ expandedItemIds: string[] = [];
+ lastToggledItem: AccordionExpandEvent | null = null;
+ eventLog: AccordionExpandEvent[] = [];
+
+ handleItemToggle(event: AccordionExpandEvent): void {
+ this.lastToggledItem = event;
+ console.log('Accordion item toggled:', event);
+ }
+
+ handleExpandedItemsChange(expandedIds: string[]): void {
+ this.expandedItemIds = expandedIds;
+ console.log('Expanded items changed:', expandedIds);
+ }
+
+ handleInteractiveToggle(event: AccordionExpandEvent): void {
+ this.eventLog.unshift({
+ ...event,
+ // Add timestamp for demonstration
+ timestamp: new Date().toLocaleTimeString()
+ } as any);
+
+ // Keep only last 5 events
+ if (this.eventLog.length > 5) {
+ this.eventLog = this.eventLog.slice(0, 5);
+ }
+ }
+
+ expandItem(itemId: string): void {
+ this.programmaticAccordion?.expandItem(itemId);
+ }
+
+ expandAll(): void {
+ this.programmaticAccordion?.expandAll();
+ }
+
+ collapseAll(): void {
+ this.programmaticAccordion?.collapseAll();
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/alert-demo/alert-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/alert-demo/alert-demo.component.scss
new file mode 100644
index 0000000..eb7d89b
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/alert-demo/alert-demo.component.scss
@@ -0,0 +1,167 @@
+@use '../../../../../shared-ui/src/styles/semantic/index' as *;
+
+.demo-container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: $semantic-spacing-layout-section-md;
+}
+
+.demo-section {
+ margin-bottom: $semantic-spacing-layout-section-lg;
+
+ h2, h3, h4 {
+ margin: 0 0 $semantic-spacing-component-lg 0;
+ color: $semantic-color-text-primary;
+ }
+
+ 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);
+ border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ padding-bottom: $semantic-spacing-component-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-secondary;
+ 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);
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+}
+
+.demo-column {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-component-md;
+ max-width: 800px;
+}
+
+.demo-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: $semantic-spacing-layout-section-md;
+}
+
+.demo-variant-group {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-component-sm;
+}
+
+.demo-message {
+ padding: $semantic-spacing-component-md;
+ background: $semantic-color-surface-secondary;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ border-radius: $semantic-border-card-radius;
+ font-family: map-get($semantic-typography-body-medium, font-family);
+ font-size: map-get($semantic-typography-body-medium, font-size);
+ font-weight: map-get($semantic-typography-body-medium, font-weight);
+ line-height: map-get($semantic-typography-body-medium, line-height);
+ color: $semantic-color-text-secondary;
+ margin: 0;
+}
+
+.demo-button {
+ background: $semantic-color-primary;
+ color: $semantic-color-on-primary;
+ border: none;
+ border-radius: $semantic-border-button-radius;
+ 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;
+ 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;
+ }
+}
+
+.demo-info {
+ background: $semantic-color-surface-elevated;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ border-radius: $semantic-border-card-radius;
+ padding: $semantic-spacing-component-lg;
+
+ ul {
+ margin: 0;
+ 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-secondary;
+ margin-bottom: $semantic-spacing-component-xs;
+
+ strong {
+ color: $semantic-color-text-primary;
+ font-weight: $semantic-typography-font-weight-semibold;
+ }
+ }
+ }
+}
+
+// Responsive adjustments
+@media (max-width: $semantic-breakpoint-md - 1) {
+ .demo-container {
+ padding: $semantic-spacing-layout-section-sm;
+ }
+
+ .demo-grid {
+ grid-template-columns: 1fr;
+ gap: $semantic-spacing-layout-section-sm;
+ }
+
+ .demo-column {
+ gap: $semantic-spacing-component-sm;
+ }
+}
+
+@media (max-width: $semantic-breakpoint-sm - 1) {
+ .demo-container {
+ padding: $semantic-spacing-component-md;
+ }
+
+ .demo-section {
+ margin-bottom: $semantic-spacing-layout-section-md;
+
+ h2 {
+ 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);
+ }
+
+ h3 {
+ 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/alert-demo/alert-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/alert-demo/alert-demo.component.ts
new file mode 100644
index 0000000..126dc4d
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/alert-demo/alert-demo.component.ts
@@ -0,0 +1,212 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { AlertComponent } from "../../../../../ui-essentials/src/public-api";
+
+@Component({
+ selector: 'ui-alert-demo',
+ standalone: true,
+ imports: [CommonModule, FontAwesomeModule, AlertComponent],
+ template: `
+
+
Alert Demo
+
+
+
+ Sizes
+
+ @for (size of sizes; track size) {
+
+ This is a {{ size }} size alert component demonstration.
+
+ }
+
+
+
+
+
+ Variants
+
+
+ This is a primary alert for general information or neutral messages.
+
+
+
+ Your action was completed successfully! Everything worked as expected.
+
+
+
+ Please review this information carefully before proceeding.
+
+
+
+ Something went wrong. Please check your input and try again.
+
+
+
+ Here's some helpful information you might want to know.
+
+
+
+
+
+
+ Without Icons
+
+
+ This alert doesn't show an icon for a cleaner look.
+
+
+
+ This warning alert has no title and no icon.
+
+
+
+
+
+
+ Dismissible Alerts
+
+ @for (dismissibleAlert of dismissibleAlerts; track dismissibleAlert.id) {
+
+ {{ dismissibleAlert.message }}
+
+ }
+
+ @if (dismissibleAlerts.length === 0) {
+
All dismissible alerts have been dismissed. Reset Alerts
+ }
+
+
+
+
+
+ Without Titles
+
+
+ This is an informational alert without a title. The message stands on its own.
+
+
+
+ This is a dismissible error alert without a title.
+
+
+
+
+
+
+ Bold Titles
+
+
+ This alert has a bold title to emphasize importance.
+
+
+
+ Bold titles help draw attention to critical messages.
+
+
+
+
+
+
+ Interactive Examples
+
+
+ Your form has been submitted and will be processed within 24 hours.
+
+
+
+ Don't forget to confirm your email address to complete your subscription.
+
+
+
+
+
+
+ Size & Variant Combinations
+
+ @for (variant of variants; track variant) {
+
+
{{ variant | titlecase }}
+ @for (size of sizes; track size) {
+
+ {{ variant | titlecase }} {{ size }} alert
+
+ }
+
+ }
+
+
+
+
+
+ Accessibility Features
+
+
+ Screen Reader Support: Proper ARIA labels and live regions
+ Keyboard Navigation: Dismissible alerts can be closed with Enter or Space
+ Focus Management: Clear focus indicators on dismiss button
+ Semantic HTML: Proper heading hierarchy and content structure
+
+
+
+
+ `,
+ styleUrl: './alert-demo.component.scss'
+})
+export class AlertDemoComponent {
+ sizes = ['sm', 'md', 'lg'] as const;
+ variants = ['primary', 'success', 'warning', 'danger', 'info'] as const;
+
+ dismissibleAlerts = [
+ {
+ id: 1,
+ variant: 'success' as const,
+ title: 'Task Completed',
+ message: 'Your task has been completed successfully and saved to your dashboard.'
+ },
+ {
+ id: 2,
+ variant: 'warning' as const,
+ title: 'Session Expiring',
+ message: 'Your session will expire in 5 minutes. Please save your work.'
+ },
+ {
+ id: 3,
+ variant: 'info' as const,
+ title: 'New Feature Available',
+ message: 'Check out our new dashboard analytics feature in the sidebar menu.'
+ }
+ ];
+
+ private originalDismissibleAlerts = [...this.dismissibleAlerts];
+
+ dismissAlert(id: number): void {
+ this.dismissibleAlerts = this.dismissibleAlerts.filter(alert => alert.id !== id);
+ console.log(`Alert with ID ${id} dismissed`);
+ }
+
+ resetDismissibleAlerts(): void {
+ this.dismissibleAlerts = [...this.originalDismissibleAlerts];
+ }
+
+ handleAlertDismissed(type: string): void {
+ console.log(`${type} alert dismissed`);
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/popover-demo/popover-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/popover-demo/popover-demo.component.scss
new file mode 100644
index 0000000..5d76646
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/popover-demo/popover-demo.component.scss
@@ -0,0 +1,367 @@
+@use "../../../../../shared-ui/src/styles/semantic/index" as *;
+
+.popover-demo {
+ padding: $semantic-spacing-layout-section-xs;
+
+ &__section {
+ margin-bottom: $semantic-spacing-layout-section-sm;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ &__title {
+ font-size: map-get($semantic-typography-heading-h3, font-size);
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-content-heading;
+ padding-bottom: $semantic-spacing-content-list-item;
+ border-bottom: $semantic-border-card-width solid $semantic-color-border-subtle;
+ }
+
+ &__subtitle {
+ font-size: map-get($semantic-typography-heading-h4, font-size);
+ color: $semantic-color-text-secondary;
+ margin-bottom: $semantic-spacing-content-paragraph;
+ }
+
+ &__grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: $semantic-spacing-layout-section-xs;
+ margin-bottom: $semantic-spacing-layout-section-xs;
+ }
+
+ &__row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: $semantic-spacing-layout-section-xs;
+ margin-bottom: $semantic-spacing-layout-section-xs;
+ align-items: center;
+
+ &--center {
+ justify-content: center;
+ min-height: 200px;
+ padding: $semantic-spacing-layout-section-xs;
+ border: 2px dashed $semantic-color-border-subtle;
+ border-radius: $semantic-border-radius-lg;
+ }
+
+ &--positions {
+ display: grid;
+ grid-template-columns: 1fr auto 1fr;
+ grid-template-rows: auto auto auto;
+ gap: $semantic-spacing-component-lg;
+ align-items: center;
+ justify-items: center;
+ min-height: 300px;
+ padding: $semantic-spacing-layout-section-xs;
+ border: 2px dashed $semantic-color-border-subtle;
+ border-radius: $semantic-border-radius-lg;
+ }
+ }
+
+ &__trigger-button {
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
+ border: $semantic-border-card-width solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-md;
+ background: $semantic-color-surface-primary;
+ color: $semantic-color-text-primary;
+ font-size: $semantic-typography-font-size-md;
+ cursor: pointer;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
+ min-width: 120px;
+
+ &:hover {
+ background: $semantic-color-surface-secondary;
+ border-color: $semantic-color-border-focus;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+
+ &--primary {
+ background: $semantic-color-primary;
+ color: $semantic-color-on-primary;
+ border-color: $semantic-color-primary;
+
+ &:hover {
+ background: $semantic-color-primary-hover;
+ border-color: $semantic-color-primary-hover;
+ }
+ }
+
+ &--small {
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ font-size: $semantic-typography-font-size-sm;
+ min-width: 100px;
+ }
+
+ &--large {
+ padding: $semantic-spacing-component-md $semantic-spacing-component-lg;
+ font-size: $semantic-typography-font-size-lg;
+ min-width: 140px;
+ }
+ }
+
+ &__stats {
+ display: flex;
+ gap: $semantic-spacing-content-paragraph;
+ margin-top: $semantic-spacing-content-paragraph;
+ font-size: $semantic-typography-font-size-sm;
+ color: $semantic-color-text-secondary;
+ }
+
+ &__code-example {
+ margin-top: $semantic-spacing-content-heading;
+ padding: $semantic-spacing-component-md;
+ background: $semantic-color-surface-secondary;
+ border: $semantic-border-card-width solid $semantic-color-border-subtle;
+ border-radius: $semantic-border-radius-md;
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ font-size: $semantic-typography-font-size-sm;
+ color: $semantic-color-text-secondary;
+ overflow-x: auto;
+ }
+
+ // Position grid layout
+ &__position-top-start {
+ grid-column: 1;
+ grid-row: 1;
+ }
+
+ &__position-top {
+ grid-column: 2;
+ grid-row: 1;
+ }
+
+ &__position-top-end {
+ grid-column: 3;
+ grid-row: 1;
+ }
+
+ &__position-left {
+ grid-column: 1;
+ grid-row: 2;
+ }
+
+ &__position-center {
+ grid-column: 2;
+ grid-row: 2;
+ }
+
+ &__position-right {
+ grid-column: 3;
+ grid-row: 2;
+ }
+
+ &__position-bottom-start {
+ grid-column: 1;
+ grid-row: 3;
+ }
+
+ &__position-bottom {
+ grid-column: 2;
+ grid-row: 3;
+ }
+
+ &__position-bottom-end {
+ grid-column: 3;
+ grid-row: 3;
+ }
+
+ // Custom popover content styles
+ .popover-content {
+ &__menu {
+ min-width: 200px;
+
+ .menu-item {
+ display: block;
+ width: 100%;
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
+ border: none;
+ background: transparent;
+ color: $semantic-color-text-primary;
+ font-size: $semantic-typography-font-size-sm;
+ text-align: left;
+ cursor: pointer;
+ transition: background-color $semantic-motion-duration-fast $semantic-motion-easing-ease;
+
+ &:hover {
+ background: $semantic-color-surface-secondary;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: -2px;
+ }
+
+ &--divider {
+ border-bottom: $semantic-border-card-width solid $semantic-color-border-subtle;
+ margin-bottom: $semantic-spacing-content-line-tight;
+ padding-bottom: $semantic-spacing-content-line-tight;
+ }
+ }
+ }
+
+ &__form {
+ min-width: 280px;
+
+ .form-group {
+ margin-bottom: $semantic-spacing-content-paragraph;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ label {
+ display: block;
+ font-size: $semantic-typography-font-size-sm;
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-content-line-tight;
+ font-weight: $semantic-typography-font-weight-medium;
+ }
+
+ input, textarea {
+ width: 100%;
+ padding: $semantic-spacing-component-sm;
+ border: $semantic-border-card-width solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-sm;
+ background: $semantic-color-surface-primary;
+ color: $semantic-color-text-primary;
+ font-size: $semantic-typography-font-size-sm;
+ transition: border-color $semantic-motion-duration-fast $semantic-motion-easing-ease;
+
+ &:focus {
+ outline: none;
+ border-color: $semantic-color-focus;
+ box-shadow: 0 0 0 1px $semantic-color-focus;
+ }
+
+ &::placeholder {
+ color: $semantic-color-text-tertiary;
+ }
+ }
+
+ textarea {
+ resize: vertical;
+ min-height: 80px;
+ }
+ }
+
+ &__card {
+ min-width: 300px;
+ max-width: 400px;
+
+ .card-header {
+ display: flex;
+ align-items: center;
+ gap: $semantic-spacing-content-list-item;
+ margin-bottom: $semantic-spacing-content-list-item;
+
+ .avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: $semantic-border-radius-full;
+ background: $semantic-color-primary;
+ color: $semantic-color-on-primary;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: $semantic-typography-font-weight-semibold;
+ }
+
+ .user-info {
+ .name {
+ font-weight: $semantic-typography-font-weight-medium;
+ font-size: $semantic-typography-font-size-md;
+ color: $semantic-color-text-primary;
+ }
+
+ .role {
+ font-size: $semantic-typography-font-size-xs;
+ color: $semantic-color-text-secondary;
+ }
+ }
+ }
+
+ .card-content {
+ font-size: $semantic-typography-font-size-sm;
+ color: $semantic-color-text-secondary;
+ line-height: $semantic-typography-line-height-relaxed;
+ margin-bottom: $semantic-spacing-content-paragraph;
+ }
+
+ .card-actions {
+ display: flex;
+ gap: $semantic-spacing-content-list-item;
+
+ button {
+ flex: 1;
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ border: $semantic-border-card-width solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-sm;
+ background: $semantic-color-surface-primary;
+ color: $semantic-color-text-primary;
+ font-size: $semantic-typography-font-size-xs;
+ cursor: pointer;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
+
+ &:hover {
+ background: $semantic-color-surface-secondary;
+ }
+
+ &.primary {
+ background: $semantic-color-primary;
+ color: $semantic-color-on-primary;
+ border-color: $semantic-color-primary;
+
+ &:hover {
+ background: $semantic-color-primary-hover;
+ }
+ }
+ }
+ }
+ }
+
+ &__tooltip {
+ max-width: 200px;
+ font-size: $semantic-typography-font-size-xs;
+ line-height: $semantic-typography-line-height-normal;
+ }
+ }
+
+ // Responsive design
+ @media (max-width: $semantic-breakpoint-md - 1) {
+ &__grid {
+ grid-template-columns: 1fr;
+ }
+
+ &__row {
+ flex-direction: column;
+ align-items: stretch;
+
+ &--positions {
+ grid-template-columns: 1fr;
+ grid-template-rows: repeat(9, auto);
+ gap: $semantic-spacing-component-sm;
+ min-height: auto;
+ }
+ }
+
+ &__position-top-start,
+ &__position-top,
+ &__position-top-end,
+ &__position-left,
+ &__position-center,
+ &__position-right,
+ &__position-bottom-start,
+ &__position-bottom,
+ &__position-bottom-end {
+ grid-column: 1;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/popover-demo/popover-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/popover-demo/popover-demo.component.ts
new file mode 100644
index 0000000..40e68f2
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/popover-demo/popover-demo.component.ts
@@ -0,0 +1,534 @@
+import { Component, ViewChild, ElementRef, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { PopoverComponent } from '../../../../../ui-essentials/src/lib/components/overlays/popover';
+
+type PopoverPosition = 'top' | 'bottom' | 'left' | 'right' | 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end';
+type PopoverSize = 'sm' | 'md' | 'lg';
+type PopoverVariant = 'default' | 'elevated' | 'floating' | 'menu' | 'tooltip';
+type PopoverTrigger = 'click' | 'hover' | 'focus' | 'manual';
+
+@Component({
+ selector: 'ui-popover-demo',
+ standalone: true,
+ imports: [CommonModule, PopoverComponent],
+ template: `
+
+
+
+
Popover Component
+
A flexible popover component with multiple positioning options, trigger types, and variants. Perfect for dropdowns, tooltips, context menus, and floating content.
+
+
+
+
+
Sizes
+
+ @for (size of sizes; track size) {
+
+ {{ size.toUpperCase() }} Size
+
+
+
+ This is a {{ size }} sized popover with sample content to demonstrate the different size options available.
+
+
+ }
+
+
+
+
+
+
Variants
+
+ @for (variant of variants; track variant) {
+
+ {{ variant | titlecase }}
+
+
+
+ @if (variant === 'tooltip') {
+
Quick tooltip content
+ } @else {
+
+
{{ variant | titlecase }} Variant
+
This demonstrates the {{ variant }} styling variant of the popover component.
+
+ }
+
+
+ }
+
+
+
+
+
+
Positions
+
+
+
+ Top Start
+
+
+ Top
+
+
+ Top End
+
+
+
+
+ Left
+
+
+
+ Reference Element
+
+
+
+ Right
+
+
+
+
+ Bottom Start
+
+
+ Bottom
+
+
+ Bottom End
+
+
+
+
+ @for (position of positions; track position.key) {
+
+
+
{{ position.value | titlecase }}
+
Positioned {{ position.value }} relative to trigger.
+
+
+ }
+
+
+
+
+
Trigger Types
+
+
+
+ Click Trigger
+
+
+
+
+
+
+
+ Hover Trigger
+
+
+
+ This popover appears on hover with a 500ms delay.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Advanced Examples
+
+
+
+
+ Menu Dropdown
+
+
+
+
+
+
+
+ Quick Form
+
+
+
+
+ Name
+
+
+
+ Email
+
+
+
+ Message
+
+
+
+
+ Submit
+ Cancel
+
+
+
+
+
+ User Profile
+
+
+
+
+
+ Full-stack developer with 5+ years of experience in Angular, Node.js, and cloud technologies. Passionate about creating user-friendly interfaces and scalable applications.
+
+
+ Message
+ Connect
+
+
+
+
+
+
+
+
+
Configuration Options
+
+
+
+
+ Auto Position (try at edge)
+
+
+
+
Auto-positioning Enabled
+
This popover automatically adjusts its position to stay within the viewport.
+
Current Position: {{ currentAutoPosition() || 'right' }}
+
+
+
+
+
+ With Backdrop
+
+
+
+
+
Modal-like Popover
+
This popover has a backdrop and prevents body scroll. Click the backdrop or press ESC to close.
+
Close
+
+
+
+
+
+
+
+
+ Total Interactions: {{ totalInteractions() }}
+ Backdrop Clicks: {{ backdropClicks() }}
+ Auto-repositions: {{ autoRepositions() }}
+
+
+
+
+
<!-- Basic Usage -->
+<button #trigger>Click me</button>
+<ui-popover [triggerElement]="trigger" position="bottom" trigger="click">
+ <p>Popover content goes here</p>
+</ui-popover>
+
+<!-- Advanced Configuration -->
+<ui-popover
+ [triggerElement]="myTrigger"
+ position="bottom-start"
+ variant="menu"
+ trigger="click"
+ [autoPosition]="true"
+ [showArrow]="true"
+ (visibleChange)="onPopoverToggle($event)">
+ <div class="custom-menu">...</div>
+</ui-popover>
+
+
+ `,
+ styleUrl: './popover-demo.component.scss'
+})
+export class PopoverDemoComponent {
+ // Component references
+ @ViewChild('triggerTopStart', { read: ElementRef }) triggerTopStart?: ElementRef;
+ @ViewChild('triggerTop', { read: ElementRef }) triggerTop?: ElementRef;
+ @ViewChild('triggerTopEnd', { read: ElementRef }) triggerTopEnd?: ElementRef;
+ @ViewChild('triggerLeft', { read: ElementRef }) triggerLeft?: ElementRef;
+ @ViewChild('triggerRight', { read: ElementRef }) triggerRight?: ElementRef;
+ @ViewChild('triggerBottomStart', { read: ElementRef }) triggerBottomStart?: ElementRef;
+ @ViewChild('triggerBottom', { read: ElementRef }) triggerBottom?: ElementRef;
+ @ViewChild('triggerBottomEnd', { read: ElementRef }) triggerBottomEnd?: ElementRef;
+
+ // Demo data
+ readonly sizes: PopoverSize[] = ['sm', 'md', 'lg'];
+ readonly variants: PopoverVariant[] = ['default', 'elevated', 'floating', 'menu', 'tooltip'];
+ readonly triggers: PopoverTrigger[] = ['click', 'hover', 'focus', 'manual'];
+
+ readonly positions = [
+ { key: 'top-start', value: 'top-start' as PopoverPosition },
+ { key: 'top', value: 'top' as PopoverPosition },
+ { key: 'top-end', value: 'top-end' as PopoverPosition },
+ { key: 'left', value: 'left' as PopoverPosition },
+ { key: 'right', value: 'right' as PopoverPosition },
+ { key: 'bottom-start', value: 'bottom-start' as PopoverPosition },
+ { key: 'bottom', value: 'bottom' as PopoverPosition },
+ { key: 'bottom-end', value: 'bottom-end' as PopoverPosition }
+ ];
+
+ // State management
+ private popoverStates = new Map();
+
+ // Statistics
+ private _totalInteractions = signal(0);
+ private _backdropClicks = signal(0);
+ private _autoRepositions = signal(0);
+ private _currentAutoPosition = signal(null);
+
+ readonly totalInteractions = this._totalInteractions.asReadonly();
+ readonly backdropClicks = this._backdropClicks.asReadonly();
+ readonly autoRepositions = this._autoRepositions.asReadonly();
+ readonly currentAutoPosition = this._currentAutoPosition.asReadonly();
+
+ /**
+ * Gets the state of a popover by key
+ */
+ getPopoverState(key: string): boolean {
+ return this.popoverStates.get(key) || false;
+ }
+
+ /**
+ * Sets the state of a popover by key
+ */
+ setPopoverState(key: string, visible: boolean): void {
+ this.popoverStates.set(key, visible);
+ if (visible) {
+ this._totalInteractions.update(count => count + 1);
+ }
+ }
+
+ /**
+ * Toggles a popover state
+ */
+ togglePopover(key: string): void {
+ const currentState = this.getPopoverState(key);
+ this.setPopoverState(key, !currentState);
+ }
+
+ /**
+ * Gets the trigger element for position demos
+ */
+ getPositionTrigger(position: string): HTMLElement | undefined {
+ switch (position) {
+ case 'top-start': return this.triggerTopStart?.nativeElement;
+ case 'top': return this.triggerTop?.nativeElement;
+ case 'top-end': return this.triggerTopEnd?.nativeElement;
+ case 'left': return this.triggerLeft?.nativeElement;
+ case 'right': return this.triggerRight?.nativeElement;
+ case 'bottom-start': return this.triggerBottomStart?.nativeElement;
+ case 'bottom': return this.triggerBottom?.nativeElement;
+ case 'bottom-end': return this.triggerBottomEnd?.nativeElement;
+ default: return undefined;
+ }
+ }
+
+ /**
+ * Handles menu actions
+ */
+ handleMenuAction(action: string): void {
+ console.log('Menu action:', action);
+ this.setPopoverState('menu-dropdown', false);
+ // You could implement actual menu logic here
+ }
+
+ /**
+ * Handles form submission
+ */
+ handleFormSubmit(): void {
+ console.log('Form submitted');
+ this.setPopoverState('form-popover', false);
+ // You could implement actual form logic here
+ }
+
+ /**
+ * Handles user profile actions
+ */
+ handleUserAction(action: string): void {
+ console.log('User action:', action);
+ this.setPopoverState('user-card', false);
+ // You could implement actual user interaction logic here
+ }
+
+ /**
+ * Handles position changes from auto-positioning
+ */
+ handlePositionChange(newPosition: PopoverPosition): void {
+ console.log('Position changed to:', newPosition);
+ this._currentAutoPosition.set(newPosition);
+ this._autoRepositions.update(count => count + 1);
+ }
+
+ /**
+ * Handles backdrop clicks
+ */
+ handleBackdropClick(): void {
+ console.log('Backdrop clicked');
+ this._backdropClicks.update(count => count + 1);
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/timeline-demo/timeline-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/timeline-demo/timeline-demo.component.scss
new file mode 100644
index 0000000..79fca66
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/timeline-demo/timeline-demo.component.scss
@@ -0,0 +1,121 @@
+@use '../../../../../shared-ui/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-layout-section-lg;
+ }
+}
+
+.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-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;
+ }
+}
+
+.demo-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: $semantic-spacing-grid-gap-lg;
+}
+
+.demo-column {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-layout-section-md;
+}
+
+.demo-item {
+ padding: $semantic-spacing-component-lg;
+ border: $semantic-border-width-1 solid $semantic-color-border-secondary;
+ border-radius: $semantic-border-card-radius;
+ background: $semantic-color-surface-primary;
+}
+
+.demo-controls {
+ display: flex;
+ gap: $semantic-spacing-component-sm;
+ margin-bottom: $semantic-spacing-component-md;
+ flex-wrap: wrap;
+}
+
+.demo-button {
+ padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-button-radius;
+ background: $semantic-color-surface-primary;
+ 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;
+ min-height: $semantic-sizing-button-height-md;
+
+ &:hover {
+ background: $semantic-color-primary;
+ color: $semantic-color-on-primary;
+ border-color: $semantic-color-primary;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+}
+
+// Responsive Design
+@media (max-width: ($semantic-breakpoint-lg - 1)) {
+ .demo-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: ($semantic-breakpoint-md - 1)) {
+ .demo-container {
+ padding: $semantic-spacing-layout-section-sm;
+ }
+
+ .demo-section {
+ margin-bottom: $semantic-spacing-layout-section-lg;
+ }
+
+ .demo-item {
+ padding: $semantic-spacing-component-md;
+ }
+}
+
+@media (max-width: ($semantic-breakpoint-sm - 1)) {
+ .demo-controls {
+ flex-direction: column;
+ }
+
+ .demo-button {
+ width: 100%;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/timeline-demo/timeline-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/timeline-demo/timeline-demo.component.ts
new file mode 100644
index 0000000..62a33d6
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/timeline-demo/timeline-demo.component.ts
@@ -0,0 +1,274 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { TimelineComponent, TimelineItem } from "../../../../../ui-essentials/src/public-api";
+
+@Component({
+ selector: 'ui-timeline-demo',
+ standalone: true,
+ imports: [CommonModule, TimelineComponent],
+ template: `
+
+
Timeline Demo
+
+
+
+ Sizes
+
+ @for (size of sizes; track size) {
+
+
{{ size }} size
+
+
+
+ }
+
+
+
+
+
+ Variants
+
+ @for (variant of variants; track variant) {
+
+
{{ variant }}
+
+
+
+ }
+
+
+
+
+
+ Orientation
+
+
+
Vertical (Default)
+
+
+
+
+
+
Horizontal
+
+
+
+
+
+
+
+
+ Status Examples
+
+
Various Status States
+
+
+
+
+
+
+ Complete Project Timeline
+
+
+
+
+
+
+
+
+ Interactive
+
+
+ Add Item
+
+
+ Toggle Orientation
+
+
+ Cycle Size
+
+
+
+
+
Dynamic Timeline ({{ currentOrientation }} / {{ currentSize }})
+
+
+
+
+ Items: {{ dynamicItems.length }}
+
+
+ `,
+ styleUrl: './timeline-demo.component.scss'
+})
+export class TimelineDemoComponent {
+ sizes = ['sm', 'md', 'lg'] as const;
+ variants = ['primary', 'secondary', 'success'] as const;
+
+ currentSize: 'sm' | 'md' | 'lg' = 'md';
+ currentOrientation: 'vertical' | 'horizontal' = 'vertical';
+ itemCounter = 1;
+
+ basicItems: TimelineItem[] = [
+ {
+ id: '1',
+ title: 'Project Started',
+ description: 'Initial setup and planning phase',
+ timestamp: '2 hours ago',
+ status: 'completed'
+ },
+ {
+ id: '2',
+ title: 'Development Phase',
+ description: 'Building core features and functionality',
+ timestamp: '1 hour ago',
+ status: 'active'
+ },
+ {
+ id: '3',
+ title: 'Testing',
+ description: 'Quality assurance and bug fixes',
+ timestamp: 'In 30 minutes',
+ status: 'pending'
+ }
+ ];
+
+ shortItems: TimelineItem[] = [
+ {
+ id: '1',
+ title: 'Plan',
+ status: 'completed'
+ },
+ {
+ id: '2',
+ title: 'Build',
+ status: 'active'
+ },
+ {
+ id: '3',
+ title: 'Test',
+ status: 'pending'
+ },
+ {
+ id: '4',
+ title: 'Deploy',
+ status: 'pending'
+ }
+ ];
+
+ statusItems: TimelineItem[] = [
+ {
+ id: '1',
+ title: 'Completed Task',
+ description: 'This task has been successfully finished',
+ timestamp: '2 days ago',
+ status: 'completed'
+ },
+ {
+ id: '2',
+ title: 'Active Task',
+ description: 'This task is currently in progress',
+ timestamp: 'Now',
+ status: 'active'
+ },
+ {
+ id: '3',
+ title: 'Failed Task',
+ description: 'This task encountered an error',
+ timestamp: '1 hour ago',
+ status: 'error'
+ },
+ {
+ id: '4',
+ title: 'Pending Task',
+ description: 'This task is waiting to be started',
+ timestamp: 'Tomorrow',
+ status: 'pending'
+ }
+ ];
+
+ projectItems: TimelineItem[] = [
+ {
+ id: '1',
+ title: 'Requirements Gathering',
+ description: 'Collected and analyzed project requirements from stakeholders',
+ timestamp: 'March 1, 2024',
+ status: 'completed'
+ },
+ {
+ id: '2',
+ title: 'Design Phase',
+ description: 'Created wireframes, mockups, and technical specifications',
+ timestamp: 'March 15, 2024',
+ status: 'completed'
+ },
+ {
+ id: '3',
+ title: 'Development Sprint 1',
+ description: 'Implemented core functionality and user authentication',
+ timestamp: 'April 1, 2024',
+ status: 'completed'
+ },
+ {
+ id: '4',
+ title: 'Development Sprint 2',
+ description: 'Building dashboard and data visualization features',
+ timestamp: 'April 15, 2024',
+ status: 'active'
+ },
+ {
+ id: '5',
+ title: 'Testing & QA',
+ description: 'Comprehensive testing and bug fixing phase',
+ timestamp: 'May 1, 2024',
+ status: 'pending'
+ },
+ {
+ id: '6',
+ title: 'Deployment',
+ description: 'Production deployment and go-live activities',
+ timestamp: 'May 15, 2024',
+ status: 'pending'
+ }
+ ];
+
+ dynamicItems: TimelineItem[] = [...this.basicItems];
+
+ addTimelineItem(): void {
+ const newItem: TimelineItem = {
+ id: `dynamic-${this.itemCounter}`,
+ title: `Dynamic Item ${this.itemCounter}`,
+ description: `This is a dynamically added item #${this.itemCounter}`,
+ timestamp: `${this.itemCounter} minutes ago`,
+ status: 'active'
+ };
+
+ this.dynamicItems = [...this.dynamicItems, newItem];
+ this.itemCounter++;
+ }
+
+ toggleOrientation(): void {
+ this.currentOrientation = this.currentOrientation === 'vertical' ? 'horizontal' : 'vertical';
+ }
+
+ cycleSize(): void {
+ const currentIndex = this.sizes.indexOf(this.currentSize);
+ const nextIndex = (currentIndex + 1) % this.sizes.length;
+ this.currentSize = this.sizes[nextIndex];
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/toast-demo/toast-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/toast-demo/toast-demo.component.scss
index 194602e..125da0a 100644
--- a/projects/demo-ui-essentials/src/app/demos/toast-demo/toast-demo.component.scss
+++ b/projects/demo-ui-essentials/src/app/demos/toast-demo/toast-demo.component.scss
@@ -1,4 +1,4 @@
-@use '../../../../../../../../shared-ui/src/styles/semantic' as *;
+@use '../../../../../shared-ui/src/styles/semantic/index' as *;
.demo-container {
padding: $semantic-spacing-layout-section-md;
diff --git a/projects/demo-ui-essentials/src/app/demos/toast-demo/toast-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/toast-demo/toast-demo.component.ts
index bfa30ab..adacfcb 100644
--- a/projects/demo-ui-essentials/src/app/demos/toast-demo/toast-demo.component.ts
+++ b/projects/demo-ui-essentials/src/app/demos/toast-demo/toast-demo.component.ts
@@ -1,8 +1,8 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
-import { ToastComponent } from '../../../../../../../ui-essentials/src/lib/components/feedback/toast/toast.component';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { faPlay, faStop, faRedo } from '@fortawesome/free-solid-svg-icons';
+import { ToastComponent } from '../../../../../../dist/ui-essentials/lib/components/feedback/toast/toast.component';
@Component({
selector: 'ui-toast-demo',
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
new file mode 100644
index 0000000..5134076
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/tree-view-demo/tree-view-demo.component.scss
@@ -0,0 +1,116 @@
+@use '../../../../../shared-ui/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;
+ 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-primary;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+}
+
+.demo-row {
+ display: flex;
+ gap: $semantic-spacing-layout-section-md;
+ flex-wrap: wrap;
+}
+
+.demo-column {
+ flex: 1;
+ min-width: 300px;
+
+ .ui-tree-view {
+ max-height: 300px;
+ overflow-y: auto;
+ }
+}
+
+.demo-controls {
+ display: flex;
+ gap: $semantic-spacing-component-sm;
+ margin-bottom: $semantic-spacing-component-md;
+ flex-wrap: wrap;
+}
+
+.demo-button {
+ padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-button-radius;
+ background: $semantic-color-surface-primary;
+ color: $semantic-color-text-primary;
+ cursor: pointer;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
+
+ font-family: map-get($semantic-typography-button-medium, font-family);
+ font-size: map-get($semantic-typography-button-medium, font-size);
+ font-weight: map-get($semantic-typography-button-medium, font-weight);
+ line-height: map-get($semantic-typography-button-medium, line-height);
+
+ &:hover {
+ background: $semantic-color-surface-secondary;
+ box-shadow: $semantic-shadow-button-hover;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+
+ &:active {
+ box-shadow: $semantic-shadow-button-rest;
+ }
+}
+
+.demo-info {
+ margin-top: $semantic-spacing-component-md;
+ padding: $semantic-spacing-component-sm;
+ background: $semantic-color-surface-secondary;
+ border-radius: $semantic-border-radius-sm;
+ border-left: 4px solid $semantic-color-primary;
+
+ font-family: map-get($semantic-typography-body-small, font-family);
+ font-size: map-get($semantic-typography-body-small, font-size);
+ font-weight: map-get($semantic-typography-body-small, font-weight);
+ line-height: map-get($semantic-typography-body-small, line-height);
+ color: $semantic-color-text-secondary;
+
+ strong {
+ color: $semantic-color-text-primary;
+ font-weight: $semantic-typography-font-weight-semibold;
+ }
+}
+
+// Responsive Design
+@media (max-width: $semantic-breakpoint-md - 1) {
+ .demo-row {
+ flex-direction: column;
+ gap: $semantic-spacing-component-md;
+ }
+
+ .demo-column {
+ min-width: unset;
+ }
+
+ .demo-controls {
+ justify-content: center;
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..64a6e52
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/tree-view-demo/tree-view-demo.component.ts
@@ -0,0 +1,366 @@
+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';
+
+@Component({
+ selector: 'ui-tree-view-demo',
+ standalone: true,
+ imports: [CommonModule, TreeViewComponent],
+ template: `
+
+
Tree View Demo
+
+
+
+ Sizes
+
+ @for (size of sizes; track size) {
+
+
{{ size | titlecase }} Size
+
+
+
+ }
+
+
+
+
+
+ Variants
+
+ @for (variant of variants; track variant) {
+
+
{{ variant | titlecase }} Variant
+
+
+
+ }
+
+
+
+
+
+ Features
+
+
+
+
Multi-Select
+
+
+
+
+
+
+
With Icons
+
+
+
+
+
+
+
+
+ States
+
+
+
+
Loading
+
+
+
+
+
+
Empty
+
+
+
+
+
+
+
Disabled Nodes
+
+
+
+
+
+
+
+
+ Interactive Controls
+
+ Expand All
+ Collapse All
+ Get Selected
+ Clear Selection
+
+
+
+
+
+ @if (selectedInfo) {
+
+ Selected: {{ selectedInfo }}
+
+ }
+
+ @if (lastAction) {
+
+ Last Action: {{ lastAction }}
+
+ }
+
+
+
+
+ File System Example
+
+
+
+ @if (selectedFile) {
+
+ Selected File: {{ selectedFile }}
+
+ }
+
+
+ `,
+ styleUrl: './tree-view-demo.component.scss'
+})
+export class TreeViewDemoComponent {
+ sizes = ['sm', 'md', 'lg'] as const;
+ variants = ['primary', 'secondary'] as const;
+
+ selectedInfo = '';
+ lastAction = '';
+ selectedFile = '';
+
+ basicNodes: TreeNode[] = [
+ {
+ id: 'item1',
+ label: 'Item 1',
+ children: [
+ { id: 'item1-1', label: 'Sub Item 1.1' },
+ { id: 'item1-2', label: 'Sub Item 1.2' }
+ ]
+ },
+ {
+ id: 'item2',
+ label: 'Item 2',
+ children: [
+ {
+ id: 'item2-1',
+ label: 'Sub Item 2.1',
+ children: [
+ { id: 'item2-1-1', label: 'Sub Sub Item 2.1.1' }
+ ]
+ }
+ ]
+ },
+ { id: 'item3', label: 'Item 3' }
+ ];
+
+ featureNodes: TreeNode[] = [
+ {
+ id: 'feature1',
+ label: 'Authentication',
+ expanded: true,
+ children: [
+ { id: 'feature1-1', label: 'Login', selected: true },
+ { id: 'feature1-2', label: 'Registration' },
+ { id: 'feature1-3', label: 'Password Reset' }
+ ]
+ },
+ {
+ id: 'feature2',
+ label: 'User Management',
+ children: [
+ { id: 'feature2-1', label: 'User Profile' },
+ { id: 'feature2-2', label: 'User Settings', selected: true }
+ ]
+ }
+ ];
+
+ iconNodes: TreeNode[] = [
+ {
+ id: 'folder1',
+ label: 'Documents',
+ icon: '📁',
+ expanded: true,
+ children: [
+ { id: 'file1', label: 'Report.pdf', icon: '📄' },
+ { id: 'file2', label: 'Presentation.pptx', icon: '📊' }
+ ]
+ },
+ {
+ id: 'folder2',
+ label: 'Images',
+ icon: '📁',
+ children: [
+ { id: 'file3', label: 'Photo1.jpg', icon: '🖼️' },
+ { id: 'file4', label: 'Logo.png', icon: '🖼️' }
+ ]
+ }
+ ];
+
+ disabledNodes: TreeNode[] = [
+ {
+ id: 'enabled1',
+ label: 'Enabled Item',
+ children: [
+ { id: 'disabled1', label: 'Disabled Sub Item', disabled: true },
+ { id: 'enabled2', label: 'Enabled Sub Item' }
+ ]
+ },
+ { id: 'disabled2', label: 'Disabled Item', disabled: true }
+ ];
+
+ interactiveNodes: TreeNode[] = JSON.parse(JSON.stringify(this.basicNodes));
+
+ fileSystemNodes: TreeNode[] = [
+ {
+ id: 'src',
+ label: 'src',
+ icon: '📁',
+ expanded: true,
+ children: [
+ {
+ id: 'components',
+ label: 'components',
+ icon: '📁',
+ children: [
+ { id: 'button.ts', label: 'button.component.ts', icon: '📄' },
+ { id: 'input.ts', label: 'input.component.ts', icon: '📄' }
+ ]
+ },
+ {
+ id: 'services',
+ label: 'services',
+ icon: '📁',
+ children: [
+ { id: 'api.ts', label: 'api.service.ts', icon: '📄' },
+ { id: 'auth.ts', label: 'auth.service.ts', icon: '📄' }
+ ]
+ },
+ { id: 'main.ts', label: 'main.ts', icon: '📄' }
+ ]
+ },
+ {
+ id: 'assets',
+ label: 'assets',
+ icon: '📁',
+ children: [
+ { id: 'logo.png', label: 'logo.png', icon: '🖼️' },
+ { id: 'styles.css', label: 'styles.css', icon: '📄' }
+ ]
+ },
+ { id: 'package.json', label: 'package.json', icon: '📄' },
+ { id: 'readme.md', label: 'README.md', icon: '📄' }
+ ];
+
+ handleNodeSelected(event: { node: TreeNode; selected: boolean }): void {
+ this.lastAction = `${event.selected ? 'Selected' : 'Deselected'}: ${event.node.label}`;
+ console.log('Node selected:', event);
+ }
+
+ handleNodeToggled(event: { node: TreeNode; expanded: boolean }): void {
+ this.lastAction = `${event.expanded ? 'Expanded' : 'Collapsed'}: ${event.node.label}`;
+ console.log('Node toggled:', event);
+ }
+
+ handleNodesChanged(nodes: TreeNode[]): void {
+ this.interactiveNodes = nodes;
+ const selected = this.getSelectedNodes(nodes);
+ this.selectedInfo = selected.map(n => n.label).join(', ') || 'None';
+ }
+
+ handleFileSystemSelect(event: { node: TreeNode; selected: boolean }): void {
+ this.selectedFile = event.selected ? event.node.label : '';
+ }
+
+ expandAll(): void {
+ this.setAllExpanded(this.interactiveNodes, true);
+ this.lastAction = 'Expanded all nodes';
+ }
+
+ collapseAll(): void {
+ this.setAllExpanded(this.interactiveNodes, false);
+ this.lastAction = 'Collapsed all nodes';
+ }
+
+ getSelected(): void {
+ const selected = this.getSelectedNodes(this.interactiveNodes);
+ this.selectedInfo = selected.map(n => n.label).join(', ') || 'None';
+ this.lastAction = `Found ${selected.length} selected nodes`;
+ }
+
+ clearSelection(): void {
+ this.clearAllSelections(this.interactiveNodes);
+ this.selectedInfo = 'None';
+ this.lastAction = 'Cleared all selections';
+ }
+
+ private setAllExpanded(nodes: TreeNode[], expanded: boolean): void {
+ for (const node of nodes) {
+ if (node.children && node.children.length > 0) {
+ node.expanded = expanded;
+ this.setAllExpanded(node.children, expanded);
+ }
+ }
+ }
+
+ private getSelectedNodes(nodes: TreeNode[]): TreeNode[] {
+ const selected: TreeNode[] = [];
+ for (const node of nodes) {
+ if (node.selected) {
+ selected.push(node);
+ }
+ if (node.children) {
+ selected.push(...this.getSelectedNodes(node.children));
+ }
+ }
+ return selected;
+ }
+
+ private clearAllSelections(nodes: TreeNode[]): void {
+ for (const node of nodes) {
+ node.selected = false;
+ if (node.children) {
+ this.clearAllSelections(node.children);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/accordion/accordion.component.scss b/projects/ui-essentials/src/lib/components/data-display/accordion/accordion.component.scss
new file mode 100644
index 0000000..69ad3d8
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/accordion/accordion.component.scss
@@ -0,0 +1,268 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+.ui-accordion {
+ // Core Structure
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ width: 100%;
+ box-sizing: border-box;
+
+ // Base styles
+ font-family: $semantic-typography-font-family-sans;
+
+ // Size variants
+ &--sm {
+ .ui-accordion__header {
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ font-size: $semantic-typography-font-size-sm;
+ }
+
+ .ui-accordion__content {
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ font-size: $semantic-typography-font-size-sm;
+ }
+ }
+
+ &--md {
+ .ui-accordion__header {
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
+ font-size: $semantic-typography-font-size-md;
+ }
+
+ .ui-accordion__content {
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
+ font-size: $semantic-typography-font-size-md;
+ }
+ }
+
+ &--lg {
+ .ui-accordion__header {
+ padding: $semantic-spacing-component-md $semantic-spacing-component-lg;
+ font-size: $semantic-typography-font-size-lg;
+ }
+
+ .ui-accordion__content {
+ padding: $semantic-spacing-component-md $semantic-spacing-component-lg;
+ font-size: $semantic-typography-font-size-lg;
+ }
+ }
+
+ // Variant styles
+ &--elevated {
+ .ui-accordion__item {
+ background: $semantic-color-surface-primary;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ border-radius: $semantic-border-radius-md;
+ box-shadow: $semantic-shadow-elevation-1;
+ margin-bottom: $semantic-spacing-component-xs;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+
+ &--outlined {
+ .ui-accordion__item {
+ background: $semantic-color-surface-primary;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-md;
+ margin-bottom: $semantic-spacing-component-xs;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+
+ &--filled {
+ .ui-accordion__item {
+ background: $semantic-color-surface-secondary;
+ border: $semantic-border-width-1 solid $semantic-color-surface-secondary;
+ border-radius: $semantic-border-radius-md;
+ margin-bottom: $semantic-spacing-component-xs;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+
+ // State variants
+ &--disabled {
+ opacity: 0.38;
+ cursor: not-allowed;
+ pointer-events: none;
+ }
+
+ // Individual accordion item
+ &__item {
+ position: relative;
+ overflow: hidden;
+ transition: all $semantic-motion-duration-fast $semantic-easing-standard;
+
+ &--expanded {
+ .ui-accordion__header {
+ .ui-accordion__icon {
+ transform: rotate(180deg);
+ }
+ }
+ }
+
+ &--disabled {
+ opacity: 0.38;
+ cursor: not-allowed;
+
+ .ui-accordion__header {
+ cursor: not-allowed;
+ pointer-events: none;
+ }
+ }
+ }
+
+ // Header/trigger area
+ &__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ color: $semantic-color-text-primary;
+ font-weight: $semantic-typography-font-weight-medium;
+ text-align: left;
+ transition: all $semantic-motion-duration-fast $semantic-easing-standard;
+
+ &:hover:not(:disabled) {
+ background: $semantic-color-surface-secondary;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-primary;
+ outline-offset: 2px;
+ }
+
+ &:active:not(:disabled) {
+ background: $semantic-color-surface-elevated;
+ }
+
+ &:disabled {
+ cursor: not-allowed;
+ opacity: 0.38;
+ }
+ }
+
+ // Header content area
+ &__header-content {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ gap: $semantic-spacing-component-sm;
+ }
+
+ // Icon container
+ &__icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: $semantic-sizing-icon-inline;
+ height: $semantic-sizing-icon-inline;
+ color: $semantic-color-text-secondary;
+ transition: transform $semantic-motion-duration-fast $semantic-easing-standard;
+ flex-shrink: 0;
+ }
+
+ // Content area
+ &__content {
+ overflow: hidden;
+ background: $semantic-color-surface-primary;
+ color: $semantic-color-text-primary;
+ line-height: $semantic-typography-line-height-relaxed;
+
+ // Animation states
+ &--collapsed {
+ max-height: 0;
+ padding-top: 0;
+ padding-bottom: 0;
+ transition:
+ max-height $semantic-motion-duration-normal $semantic-easing-standard,
+ padding $semantic-motion-duration-normal $semantic-easing-standard;
+ }
+
+ &--expanded {
+ max-height: 1000px; // Large enough for most content
+ transition: max-height $semantic-motion-duration-normal $semantic-easing-standard;
+ }
+
+ &--expanding {
+ transition:
+ max-height $semantic-motion-duration-normal $semantic-easing-standard,
+ padding $semantic-motion-duration-normal $semantic-easing-standard;
+ }
+ }
+
+ // Content inner wrapper for proper padding animation
+ &__content-inner {
+ padding-top: $semantic-spacing-component-xs;
+ }
+
+ // Multiple expand mode styling
+ &--multiple {
+ .ui-accordion__item:not(:last-child) {
+ border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ }
+ }
+
+ // Dark mode support
+ :host-context(.dark-theme) & {
+ .ui-accordion__item {
+ background: $semantic-color-surface-elevated;
+ border-color: $semantic-color-border-secondary;
+ }
+
+ .ui-accordion__header {
+ color: $semantic-color-text-primary;
+
+ &:hover:not(:disabled) {
+ background: $semantic-color-surface-secondary;
+ }
+ }
+
+ .ui-accordion__content {
+ background: $semantic-color-surface-elevated;
+ color: $semantic-color-text-primary;
+ }
+ }
+
+ // Responsive design
+ @media (max-width: $semantic-breakpoint-md - 1) {
+ &--lg {
+ .ui-accordion__header {
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
+ font-size: $semantic-typography-font-size-md;
+ }
+
+ .ui-accordion__content {
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
+ font-size: $semantic-typography-font-size-md;
+ }
+ }
+ }
+
+ @media (max-width: $semantic-breakpoint-sm - 1) {
+ &--md,
+ &--lg {
+ .ui-accordion__header {
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ font-size: $semantic-typography-font-size-sm;
+ }
+
+ .ui-accordion__content {
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ font-size: $semantic-typography-font-size-sm;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/accordion/accordion.component.ts b/projects/ui-essentials/src/lib/components/data-display/accordion/accordion.component.ts
new file mode 100644
index 0000000..1b778df
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/accordion/accordion.component.ts
@@ -0,0 +1,355 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, ContentChildren, QueryList, AfterContentInit, OnDestroy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { Subject, takeUntil } from 'rxjs';
+
+export type AccordionSize = 'sm' | 'md' | 'lg';
+export type AccordionVariant = 'elevated' | 'outlined' | 'filled';
+
+export interface AccordionItem {
+ id: string;
+ title: string;
+ content?: string;
+ disabled?: boolean;
+ expanded?: boolean;
+}
+
+export interface AccordionExpandEvent {
+ item: AccordionItem;
+ index: number;
+ expanded: boolean;
+}
+
+@Component({
+ selector: 'ui-accordion-item',
+ standalone: true,
+ imports: [CommonModule],
+ template: `
+
+
+
+
+
+
+ @if (expanded || !collapseContent) {
+
+
+ @if (!hasContent && content) {
+
{{ content }}
+ }
+
+ }
+
+
+ `
+})
+export class AccordionItemComponent {
+ @Input() id!: string;
+ @Input() title!: string;
+ @Input() content?: string;
+ @Input() disabled = false;
+ @Input() expanded = false;
+ @Input() collapseContent = true; // Whether to remove content from DOM when collapsed
+ @Input() hasHeaderContent = false;
+ @Input() hasContent = false;
+
+ @Output() expandedChange = new EventEmitter();
+ @Output() itemToggle = new EventEmitter();
+
+ get headerId(): string {
+ return `accordion-header-${this.id}`;
+ }
+
+ get contentId(): string {
+ return `accordion-content-${this.id}`;
+ }
+
+ handleToggle(): void {
+ if (!this.disabled) {
+ this.expanded = !this.expanded;
+ this.expandedChange.emit(this.expanded);
+ this.itemToggle.emit({
+ item: {
+ id: this.id,
+ title: this.title,
+ content: this.content,
+ disabled: this.disabled,
+ expanded: this.expanded
+ },
+ index: 0, // Will be set by parent accordion
+ expanded: this.expanded
+ });
+ }
+ }
+
+ handleKeydown(event: KeyboardEvent): void {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ this.handleToggle();
+ }
+ }
+}
+
+@Component({
+ selector: 'ui-accordion',
+ standalone: true,
+ imports: [CommonModule, AccordionItemComponent],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ template: `
+
+
+ @if (items && items.length > 0) {
+ @for (item of items; track item.id; let i = $index) {
+
+
+ }
+ } @else {
+
+ }
+
+ `,
+ styleUrl: './accordion.component.scss'
+})
+export class AccordionComponent implements AfterContentInit, OnDestroy {
+ @Input() size: AccordionSize = 'md';
+ @Input() variant: AccordionVariant = 'elevated';
+ @Input() disabled = false;
+ @Input() multiple = false; // Allow multiple items to be expanded simultaneously
+ @Input() collapseContent = true; // Whether to remove content from DOM when collapsed
+ @Input() items: AccordionItem[] = []; // Programmatic items
+ @Input() expandedItems: string[] = []; // IDs of initially expanded items
+
+ @Output() itemToggle = new EventEmitter();
+ @Output() expandedItemsChange = new EventEmitter();
+
+ @ContentChildren(AccordionItemComponent) accordionItems!: QueryList;
+
+ private destroy$ = new Subject();
+
+ ngAfterContentInit(): void {
+ // Set up keyboard navigation between accordion items
+ this.setupKeyboardNavigation();
+
+ // Initialize expanded states
+ this.initializeExpandedStates();
+ }
+
+ ngOnDestroy(): void {
+ this.destroy$.next();
+ this.destroy$.complete();
+ }
+
+ private setupKeyboardNavigation(): void {
+ this.accordionItems.changes
+ .pipe(takeUntil(this.destroy$))
+ .subscribe(() => {
+ this.accordionItems.forEach((item, index) => {
+ // Update index for event emission
+ const originalToggle = item.handleToggle.bind(item);
+ item.handleToggle = () => {
+ originalToggle();
+ // Emit with correct index
+ const event = {
+ item: {
+ id: item.id,
+ title: item.title,
+ content: item.content,
+ disabled: item.disabled,
+ expanded: item.expanded
+ },
+ index,
+ expanded: item.expanded
+ };
+ this.handleItemToggle(event, index);
+ };
+ });
+ });
+ }
+
+ private initializeExpandedStates(): void {
+ if (this.items.length > 0) {
+ // Initialize programmatic items
+ this.items.forEach(item => {
+ if (this.expandedItems.includes(item.id)) {
+ item.expanded = true;
+ }
+ });
+ }
+ }
+
+ handleItemToggle(event: AccordionExpandEvent, index: number): void {
+ event.index = index;
+
+ if (!this.multiple && event.expanded) {
+ // Close other items if not in multiple mode
+ this.closeOtherItems(event.item.id);
+ }
+
+ // Update expanded items list
+ this.updateExpandedItems(event.item.id, event.expanded);
+
+ // Emit events
+ this.itemToggle.emit(event);
+ this.expandedItemsChange.emit([...this.expandedItems]);
+ }
+
+ private closeOtherItems(exceptId: string): void {
+ if (this.items.length > 0) {
+ // Close other programmatic items
+ this.items.forEach(item => {
+ if (item.id !== exceptId && item.expanded) {
+ item.expanded = false;
+ }
+ });
+ }
+
+ // Close other content-projected items
+ this.accordionItems.forEach(item => {
+ if (item.id !== exceptId && item.expanded) {
+ item.expanded = false;
+ item.expandedChange.emit(false);
+ }
+ });
+ }
+
+ private updateExpandedItems(itemId: string, expanded: boolean): void {
+ if (expanded) {
+ if (!this.expandedItems.includes(itemId)) {
+ this.expandedItems.push(itemId);
+ }
+ } else {
+ const index = this.expandedItems.indexOf(itemId);
+ if (index > -1) {
+ this.expandedItems.splice(index, 1);
+ }
+ }
+ }
+
+ // Public methods for programmatic control
+ expandItem(itemId: string): void {
+ const item = this.items.find(i => i.id === itemId);
+ const accordionItem = this.accordionItems.find(i => i.id === itemId);
+
+ if (item && !item.disabled) {
+ if (!this.multiple) {
+ this.closeAllItems();
+ }
+ item.expanded = true;
+ this.updateExpandedItems(itemId, true);
+ }
+
+ if (accordionItem && !accordionItem.disabled) {
+ if (!this.multiple) {
+ this.closeAllItems();
+ }
+ accordionItem.expanded = true;
+ accordionItem.expandedChange.emit(true);
+ this.updateExpandedItems(itemId, true);
+ }
+ }
+
+ collapseItem(itemId: string): void {
+ const item = this.items.find(i => i.id === itemId);
+ const accordionItem = this.accordionItems.find(i => i.id === itemId);
+
+ if (item) {
+ item.expanded = false;
+ this.updateExpandedItems(itemId, false);
+ }
+
+ if (accordionItem) {
+ accordionItem.expanded = false;
+ accordionItem.expandedChange.emit(false);
+ this.updateExpandedItems(itemId, false);
+ }
+ }
+
+ expandAll(): void {
+ if (this.multiple) {
+ this.items.forEach(item => {
+ if (!item.disabled) {
+ item.expanded = true;
+ this.updateExpandedItems(item.id, true);
+ }
+ });
+
+ this.accordionItems.forEach(item => {
+ if (!item.disabled) {
+ item.expanded = true;
+ item.expandedChange.emit(true);
+ this.updateExpandedItems(item.id, true);
+ }
+ });
+ }
+ }
+
+ collapseAll(): void {
+ this.items.forEach(item => {
+ item.expanded = false;
+ this.updateExpandedItems(item.id, false);
+ });
+
+ this.accordionItems.forEach(item => {
+ item.expanded = false;
+ item.expandedChange.emit(false);
+ this.updateExpandedItems(item.id, false);
+ });
+ }
+
+ private closeAllItems(): void {
+ this.items.forEach(item => {
+ item.expanded = false;
+ });
+
+ this.accordionItems.forEach(item => {
+ item.expanded = false;
+ item.expandedChange.emit(false);
+ });
+
+ this.expandedItems = [];
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/accordion/index.ts b/projects/ui-essentials/src/lib/components/data-display/accordion/index.ts
new file mode 100644
index 0000000..9a739bc
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/accordion/index.ts
@@ -0,0 +1,2 @@
+export { AccordionComponent, AccordionItemComponent } from './accordion.component';
+export type { AccordionSize, AccordionVariant, AccordionItem, AccordionExpandEvent } from './accordion.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/card/card.component.scss b/projects/ui-essentials/src/lib/components/data-display/card/card.component.scss
index 8f6da03..a74911a 100644
--- a/projects/ui-essentials/src/lib/components/data-display/card/card.component.scss
+++ b/projects/ui-essentials/src/lib/components/data-display/card/card.component.scss
@@ -111,6 +111,7 @@
// Variant styles - Material Design 3 inspired colors
&--elevated {
background-color: $semantic-color-surface-primary;
+ color: $semantic-color-text-primary;
border: none;
&:hover:not(.ui-card--disabled) {
@@ -120,16 +121,18 @@
}
&--filled {
- background-color: $semantic-color-container-primary;
+ background-color: $semantic-color-surface-secondary;
+ color: $semantic-color-text-primary;
border: $semantic-border-width-1 solid $semantic-color-border-primary;
&:hover:not(.ui-card--disabled) {
- background-color: $semantic-color-surface-interactive;
+ background-color: $semantic-color-surface-elevated;
}
}
&--outlined {
background-color: transparent;
+ color: $semantic-color-text-primary;
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
&:hover:not(.ui-card--disabled) {
@@ -239,12 +242,8 @@
.card-content-layer {
position: relative;
z-index: 1;
- background: rgba(255, 255, 255, 0.9); // Semi-transparent overlay
-
- @media (prefers-color-scheme: dark) {
- background: rgba(0, 0, 0, 0.7);
- color: $semantic-color-text-inverse;
- }
+ background: $semantic-color-surface-primary;
+ color: $semantic-color-text-primary;
}
}
}
@@ -255,6 +254,7 @@
flex-direction: column;
height: 100%;
flex: 1;
+ color: inherit;
}
.card-header {
@@ -273,6 +273,7 @@
.card-body {
flex: 1;
+ color: inherit;
// Typography reset
> *:first-child {
@@ -282,6 +283,10 @@
> *:last-child {
margin-bottom: 0;
}
+
+ p {
+ color: $semantic-color-text-primary;
+ }
}
.card-footer {
@@ -357,46 +362,11 @@
}
}
-// Dark mode support
-@media (prefers-color-scheme: dark) {
- .ui-card {
- &--elevated {
- background-color: $semantic-color-surface-primary;
- color: $semantic-color-text-inverse;
- }
-
- &--filled {
- background-color: $semantic-color-surface-secondary;
- border-color: $semantic-color-border-primary;
- color: $semantic-color-text-inverse;
- }
-
- &--outlined {
- background-color: transparent;
- border-color: $semantic-color-border-primary;
- color: $semantic-color-text-inverse;
- }
-
- .card-header {
- border-color: $semantic-color-border-primary;
-
- h1, h2, h3, h4, h5, h6 {
- color: $semantic-color-text-inverse;
- }
-
- p {
- color: $semantic-color-text-secondary;
- }
- }
-
- .card-footer {
- border-color: $semantic-color-border-primary;
- }
- }
-}
+// Dark mode support - Remove problematic dark mode overrides
+// Semantic tokens should handle dark mode automatically
-// Responsive adaptations
-@media (max-width: $semantic-sizing-breakpoint-tablet) {
+// Responsive adaptations
+@media (max-width: ($semantic-breakpoint-md - 1)) {
.ui-card {
margin-right: 0;
diff --git a/projects/ui-essentials/src/lib/components/data-display/carousel/carousel.component.scss b/projects/ui-essentials/src/lib/components/data-display/carousel/carousel.component.scss
index f0f72a4..413534e 100644
--- a/projects/ui-essentials/src/lib/components/data-display/carousel/carousel.component.scss
+++ b/projects/ui-essentials/src/lib/components/data-display/carousel/carousel.component.scss
@@ -12,114 +12,195 @@
}
}
-.carousel {
+.ui-carousel {
position: relative;
width: 100%;
overflow: hidden;
border-radius: 12px;
background: $semantic-color-surface-container;
- .carousel-container {
+ &__container {
position: relative;
width: 100%;
height: 400px; // Default height
overflow: hidden;
}
- .carousel-track {
+ &__track {
display: flex;
width: 100%;
height: 100%;
transition: transform $semantic-duration-medium $semantic-easing-standard;
}
- .carousel-slide {
+ &__slide {
flex: 0 0 100%;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
+ opacity: 1;
+ transform: scale(1);
+ transition: opacity $semantic-duration-medium $semantic-easing-standard,
+ transform $semantic-duration-medium $semantic-easing-standard;
- &.multiple-items {
- flex: 0 0 calc(100% / var(--items-per-view, 1));
+ &--active {
+ opacity: 1;
+ transform: scale(1);
}
- img {
+ .ui-carousel__image-container,
+ .ui-carousel__card,
+ .ui-carousel__content-slide {
+ width: 100%;
+ height: 100%;
+ }
+ }
+
+ &__image-container {
+ position: relative;
+ width: 100%;
+ height: 100%;
+
+ .ui-carousel__image {
width: 100%;
height: 100%;
object-fit: cover;
}
- .slide-content {
+ .ui-carousel__image-overlay {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
padding: $semantic-spacing-component-lg;
- text-align: center;
- color: $semantic-color-text-primary;
+ color: white;
- h3 {
+ .ui-carousel__title {
@include font-properties($semantic-typography-heading-h3);
- margin-bottom: $semantic-spacing-component-md;
+ margin-bottom: $semantic-spacing-component-sm;
}
- p {
+ .ui-carousel__subtitle {
@include font-properties($semantic-typography-body-medium);
+ opacity: 0.9;
+ }
+ }
+ }
+
+ &__card {
+ background: $semantic-color-surface-container;
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: $semantic-shadow-elevation-2;
+
+ .ui-carousel__card-image {
+ width: 100%;
+ height: 60%;
+ object-fit: cover;
+ }
+
+ .ui-carousel__card-content {
+ padding: $semantic-spacing-component-lg;
+ height: 40%;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+
+ .ui-carousel__title {
+ @include font-properties($semantic-typography-heading-h4);
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+
+ .ui-carousel__subtitle {
+ @include font-properties($semantic-typography-body-medium);
+ color: $semantic-color-text-secondary;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+
+ .ui-carousel__content {
+ @include font-properties($semantic-typography-body-small);
color: $semantic-color-text-secondary;
}
}
}
- .carousel-controls {
+ &__content-slide {
+ padding: $semantic-spacing-component-lg;
+ text-align: center;
+ color: $semantic-color-text-primary;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+
+ .ui-carousel__title {
+ @include font-properties($semantic-typography-heading-h3);
+ margin-bottom: $semantic-spacing-component-md;
+ }
+
+ .ui-carousel__subtitle {
+ @include font-properties($semantic-typography-body-medium);
+ color: $semantic-color-text-secondary;
+ margin-bottom: $semantic-spacing-component-md;
+ }
+
+ .ui-carousel__content {
+ @include font-properties($semantic-typography-body-medium);
+ color: $semantic-color-text-secondary;
+ }
+ }
+
+ &__nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 2;
+ background: rgba(255, 255, 255, 0.9);
+ border: none;
+ border-radius: 50%;
+ width: 48px;
+ height: 48px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ box-shadow: $semantic-shadow-elevation-3;
+ transition: all $semantic-motion-duration-fast $semantic-easing-standard;
- button {
- background: rgba(255, 255, 255, 0.9);
- border: none;
- border-radius: 50%;
- width: 48px;
- height: 48px;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- box-shadow: $semantic-shadow-elevation-3;
- transition: all $semantic-motion-duration-fast $semantic-easing-standard;
-
- &:hover {
- background: white;
- transform: scale(1.1);
- }
-
- &:focus {
- outline: 2px solid $semantic-color-primary;
- outline-offset: 2px;
- }
-
- &:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- transform: none;
- }
-
- svg {
- width: 20px;
- height: 20px;
- fill: $semantic-color-text-primary;
- }
+ &:hover {
+ background: white;
+ transform: translateY(-50%) scale(1.1);
}
- &.prev {
+ &:focus {
+ outline: 2px solid $semantic-color-primary;
+ outline-offset: 2px;
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ transform: translateY(-50%);
+ }
+
+ &--prev {
left: $semantic-spacing-component-md;
}
- &.next {
+ &--next {
right: $semantic-spacing-component-md;
}
+
+ .ui-carousel__nav-icon {
+ width: 20px;
+ height: 20px;
+ color: $semantic-color-text-primary;
+ }
}
- .carousel-indicators {
+ &__indicators {
position: absolute;
bottom: $semantic-spacing-component-md;
left: 50%;
@@ -128,54 +209,167 @@
gap: $semantic-spacing-component-xs;
z-index: 2;
- button {
+ &--dots .ui-carousel__indicator {
width: 12px;
height: 12px;
border-radius: 50%;
- border: none;
- background: rgba(255, 255, 255, 0.5);
- cursor: pointer;
- transition: all $semantic-duration-medium $semantic-easing-standard;
+ }
- &.active {
- background: $semantic-color-primary;
- transform: scale(1.2);
- }
+ &--bars .ui-carousel__indicator {
+ width: 24px;
+ height: 4px;
+ border-radius: 2px;
+ }
- &:hover {
- background: rgba(255, 255, 255, 0.8);
- }
+ &--thumbnails .ui-carousel__indicator {
+ width: 48px;
+ height: 32px;
+ border-radius: 4px;
+ overflow: hidden;
+ }
+ }
- &:focus {
- outline: 2px solid $semantic-color-primary;
- outline-offset: 2px;
+ &__indicator {
+ border: none;
+ background: rgba(255, 255, 255, 0.5);
+ cursor: pointer;
+ transition: all $semantic-duration-medium $semantic-easing-standard;
+
+ &--active {
+ background: $semantic-color-primary;
+ transform: scale(1.2);
+ }
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.8);
+ }
+
+ &:focus {
+ outline: 2px solid $semantic-color-primary;
+ outline-offset: 2px;
+ }
+
+ .ui-carousel__indicator-dot,
+ .ui-carousel__indicator-bar {
+ display: block;
+ width: 100%;
+ height: 100%;
+ border-radius: inherit;
+ }
+
+ .ui-carousel__indicator-thumbnail {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+ }
+
+ // Size variants
+ &--sm {
+ .ui-carousel__container {
+ height: 250px;
+ }
+ }
+
+ &--md {
+ .ui-carousel__container {
+ height: 400px;
+ }
+ }
+
+ &--lg {
+ .ui-carousel__container {
+ height: 600px;
+ }
+ }
+
+ // Transition effects
+ &--fade {
+ .ui-carousel__track {
+ // For fade transition, we need to position slides absolutely
+ position: relative;
+ }
+
+ .ui-carousel__slide {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ opacity: 0;
+ transition: opacity $semantic-duration-medium $semantic-easing-standard;
+ flex: none; // Override the flex property
+
+ &--active {
+ opacity: 1;
+ z-index: 1;
}
}
}
+ &--scale {
+ .ui-carousel__track {
+ position: relative;
+ }
+
+ .ui-carousel__slide {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ transform: scale(0.8);
+ opacity: 0.3;
+ transition: transform $semantic-duration-medium $semantic-easing-standard,
+ opacity $semantic-duration-medium $semantic-easing-standard;
+ flex: none; // Override the flex property
+
+ &--active {
+ transform: scale(1);
+ opacity: 1;
+ z-index: 1;
+ }
+ }
+ }
+
+ &--slide {
+ // Default slide behavior - already handled by track transform
+ .ui-carousel__track {
+ transition: transform $semantic-duration-medium $semantic-easing-standard;
+ }
+ }
+
+ // Disabled state
+ &--disabled {
+ opacity: 0.6;
+ pointer-events: none;
+ }
+
// Responsive design
@media (max-width: 768px) {
- .carousel-container {
+ &__container {
height: 300px;
}
- .carousel-controls button {
+ &--sm .ui-carousel__container {
+ height: 200px;
+ }
+
+ &--lg .ui-carousel__container {
+ height: 400px;
+ }
+
+ &__nav {
width: 40px;
height: 40px;
- svg {
+ .ui-carousel__nav-icon {
width: 16px;
height: 16px;
}
}
- .carousel-slide .slide-content {
+ &__card .ui-carousel__card-content,
+ &__content-slide {
padding: $semantic-spacing-component-md;
}
}
-}
-
-// Auto-play animation
-.carousel.auto-play .carousel-track {
- transition: transform $semantic-duration-medium $semantic-easing-standard;
}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/carousel/carousel.component.ts b/projects/ui-essentials/src/lib/components/data-display/carousel/carousel.component.ts
index de61493..e88541a 100644
--- a/projects/ui-essentials/src/lib/components/data-display/carousel/carousel.component.ts
+++ b/projects/ui-essentials/src/lib/components/data-display/carousel/carousel.component.ts
@@ -62,7 +62,7 @@ export interface CarouselItem {
+ [style.transform]="transition === 'slide' ? 'translateX(-' + (currentIndex() * 100) + '%)' : 'translateX(0)'">
@for (item of items; track item.id; let i = $index) {
+
+ @for (item of items; track item.id) {
+
+
+
+
+
+
+ @if (item.title) {
+
+ {{ item.title }}
+
+ }
+
+ @if (item.description) {
+
+ {{ item.description }}
+
+ }
+
+ @if (item.timestamp) {
+
+ {{ item.timestamp }}
+
+ }
+
+
+ }
+
+ `,
+ styleUrl: './timeline.component.scss'
+})
+export class TimelineComponent {
+ @Input() items: TimelineItem[] = [];
+ @Input() size: TimelineSize = 'md';
+ @Input() variant: TimelineVariant = 'primary';
+ @Input() orientation: TimelineOrientation = 'vertical';
+ @Input() ariaLabel?: string;
+
+ getItemAriaLabel(item: TimelineItem): string {
+ const status = this.getStatusLabel(item.status);
+ const title = item.title || 'Timeline item';
+ const timestamp = item.timestamp ? `, ${item.timestamp}` : '';
+ return `${title}, ${status}${timestamp}`;
+ }
+
+ private getStatusLabel(status: TimelineItemStatus): string {
+ switch (status) {
+ case 'completed':
+ return 'completed';
+ case 'active':
+ return 'in progress';
+ case 'pending':
+ return 'pending';
+ case 'error':
+ return 'error';
+ default:
+ return status;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/tree-view/index.ts b/projects/ui-essentials/src/lib/components/data-display/tree-view/index.ts
new file mode 100644
index 0000000..ae3ea06
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/tree-view/index.ts
@@ -0,0 +1 @@
+export * from './tree-view.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/tree-view/tree-view.component.scss b/projects/ui-essentials/src/lib/components/data-display/tree-view/tree-view.component.scss
new file mode 100644
index 0000000..12697ef
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/tree-view/tree-view.component.scss
@@ -0,0 +1,225 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+.ui-tree-view {
+ // Core Structure
+ display: block;
+ position: relative;
+
+ // Layout & Spacing
+ padding: $semantic-spacing-component-sm;
+
+ // Visual Design
+ background: $semantic-color-surface-primary;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ border-radius: $semantic-border-card-radius;
+
+ // 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;
+
+ // Size Variants
+ &--sm {
+ padding: $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);
+ }
+
+ &--md {
+ padding: $semantic-spacing-component-sm;
+ // Typography already set above for medium
+ }
+
+ &--lg {
+ padding: $semantic-spacing-component-md;
+ 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 Variants
+ &--primary {
+ border-color: $semantic-color-primary;
+
+ .ui-tree-view__node--selected {
+ background: $semantic-color-primary;
+ color: $semantic-color-on-primary;
+ }
+ }
+
+ &--secondary {
+ border-color: $semantic-color-secondary;
+
+ .ui-tree-view__node--selected {
+ background: $semantic-color-secondary;
+ color: $semantic-color-on-secondary;
+ }
+ }
+
+ // Tree Node
+ &__node {
+ display: flex;
+ align-items: center;
+ padding: $semantic-spacing-content-line-tight;
+ margin-bottom: 2px;
+ cursor: pointer;
+ border-radius: $semantic-border-radius-sm;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
+
+ // Node states
+ &:hover {
+ background: $semantic-color-surface-secondary;
+ }
+
+ &--selected {
+ background: $semantic-color-surface-elevated;
+ color: $semantic-color-text-primary;
+ font-weight: $semantic-typography-font-weight-medium;
+ }
+
+ &--disabled {
+ opacity: $semantic-opacity-disabled;
+ cursor: not-allowed;
+ pointer-events: none;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+ }
+
+ // Tree Node Content
+ &__node-content {
+ display: flex;
+ align-items: center;
+ flex: 1;
+ min-width: 0; // Allow text truncation
+ }
+
+ // Expand/Collapse Button
+ &__expand-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: $semantic-sizing-touch-minimum;
+ height: $semantic-sizing-touch-minimum;
+ margin-right: $semantic-spacing-component-xs;
+ border: none;
+ background: transparent;
+ border-radius: $semantic-border-radius-sm;
+ cursor: pointer;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
+
+ &:hover {
+ background: $semantic-color-surface-secondary;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+
+ &--expanded {
+ transform: rotate(90deg);
+ }
+
+ &--leaf {
+ cursor: default;
+ color: $semantic-color-text-secondary;
+ font-size: $semantic-typography-font-size-lg;
+
+ &:hover {
+ background: transparent;
+ }
+ }
+ }
+
+ // Node Icon
+ &__node-icon {
+ width: $semantic-sizing-icon-inline;
+ height: $semantic-sizing-icon-inline;
+ margin-right: $semantic-spacing-component-xs;
+ color: $semantic-color-text-secondary;
+
+ .ui-tree-view__node--selected & {
+ color: inherit;
+ }
+ }
+
+ // Node Label
+ &__node-label {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ // Children Container
+ &__children {
+ margin-left: $semantic-spacing-layout-section-sm;
+ border-left: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ padding-left: $semantic-spacing-component-sm;
+
+ &--hidden {
+ display: none;
+ }
+ }
+
+ // Loading State
+ &__loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: $semantic-spacing-component-md;
+ color: $semantic-color-text-secondary;
+ }
+
+ // Empty State
+ &__empty {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: $semantic-spacing-component-lg;
+ color: $semantic-color-text-secondary;
+ font-style: italic;
+ }
+
+ // Root level adjustments
+ & > .ui-tree-view__node {
+ &:first-child {
+ margin-top: 0;
+ }
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ // Responsive Design
+ @media (max-width: ($semantic-breakpoint-md - 1)) {
+ padding: $semantic-spacing-component-xs;
+
+ .ui-tree-view__children {
+ margin-left: $semantic-spacing-component-md;
+ padding-left: $semantic-spacing-component-xs;
+ }
+ }
+
+ @media (max-width: ($semantic-breakpoint-sm - 1)) {
+ 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);
+
+ .ui-tree-view__node {
+ padding: $semantic-spacing-content-line-tight $semantic-spacing-component-xs;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/tree-view/tree-view.component.ts b/projects/ui-essentials/src/lib/components/data-display/tree-view/tree-view.component.ts
new file mode 100644
index 0000000..765a90f
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/tree-view/tree-view.component.ts
@@ -0,0 +1,297 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+export interface TreeNode {
+ id: string;
+ label: string;
+ icon?: string;
+ children?: TreeNode[];
+ expanded?: boolean;
+ selected?: boolean;
+ disabled?: boolean;
+ data?: any;
+}
+
+type TreeViewSize = 'sm' | 'md' | 'lg';
+type TreeViewVariant = 'primary' | 'secondary';
+
+@Component({
+ selector: 'ui-tree-node',
+ standalone: true,
+ imports: [CommonModule, TreeNodeComponent],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ template: `
+
+
+ @if (hasChildren) {
+
+ ▶
+
+ } @else {
+
+ •
+
+ }
+
+
+ @if (showIcons && node.icon) {
+
+ {{ node.icon }}
+
+ }
+
+
+ {{ node.label }}
+
+
+
+
+ @if (hasChildren && node.expanded) {
+
+ @for (childNode of node.children; track childNode.id) {
+
+
+ }
+
+ }
+ `
+})
+export class TreeNodeComponent {
+ @Input() node!: TreeNode;
+ @Input() level = 0;
+ @Input() showIcons = true;
+ @Input() multiSelect = false;
+
+ @Output() nodeToggled = new EventEmitter<{ node: TreeNode; expanded: boolean }>();
+ @Output() nodeSelected = new EventEmitter<{ node: TreeNode; selected: boolean }>();
+
+ get hasChildren(): boolean {
+ return !!(this.node.children && this.node.children.length > 0);
+ }
+
+ handleNodeClick(): void {
+ if (!this.node.disabled) {
+ this.nodeSelected.emit({
+ node: this.node,
+ selected: !this.node.selected
+ });
+ }
+ }
+
+ handleToggleClick(event: Event): void {
+ event.stopPropagation();
+ if (!this.node.disabled && this.hasChildren) {
+ this.nodeToggled.emit({
+ node: this.node,
+ expanded: !this.node.expanded
+ });
+ }
+ }
+
+ handleKeydown(event: KeyboardEvent): void {
+ if (this.node.disabled) return;
+
+ switch (event.key) {
+ case 'Enter':
+ case ' ':
+ event.preventDefault();
+ this.handleNodeClick();
+ break;
+ case 'ArrowRight':
+ if (this.hasChildren && !this.node.expanded) {
+ event.preventDefault();
+ this.handleToggleClick(event);
+ }
+ break;
+ case 'ArrowLeft':
+ if (this.hasChildren && this.node.expanded) {
+ event.preventDefault();
+ this.handleToggleClick(event);
+ }
+ break;
+ }
+ }
+}
+
+@Component({
+ selector: 'ui-tree-view',
+ standalone: true,
+ imports: [CommonModule, TreeNodeComponent],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ template: `
+
+
+ @if (loading) {
+
+ Loading...
+
+ } @else if (!nodes || nodes.length === 0) {
+
+ {{ emptyMessage }}
+
+ } @else {
+ @for (node of nodes; track node.id) {
+
+ @if (renderTreeNode) {
+
+
+ } @else {
+
+
+ }
+
+ }
+ }
+
+ `,
+ styleUrl: './tree-view.component.scss'
+})
+export class TreeViewComponent {
+ @Input() nodes: TreeNode[] = [];
+ @Input() size: TreeViewSize = 'md';
+ @Input() variant: TreeViewVariant = 'primary';
+ @Input() loading = false;
+ @Input() showIcons = true;
+ @Input() multiSelect = false;
+ @Input() expandAll = false;
+ @Input() emptyMessage = 'No items to display';
+ @Input() ariaLabel = 'Tree view';
+ @Input() renderTreeNode?: any; // Custom template reference
+
+ @Output() nodeToggled = new EventEmitter<{ node: TreeNode; expanded: boolean }>();
+ @Output() nodeSelected = new EventEmitter<{ node: TreeNode; selected: boolean }>();
+ @Output() nodesChanged = new EventEmitter
();
+
+ handleToggle = (event: { node: TreeNode; expanded: boolean }): void => {
+ event.node.expanded = event.expanded;
+ this.nodeToggled.emit(event);
+ this.nodesChanged.emit(this.nodes);
+ };
+
+ handleSelect = (event: { node: TreeNode; selected: boolean }): void => {
+ if (!this.multiSelect) {
+ // Single select - clear all other selections
+ this.clearAllSelections(this.nodes);
+ }
+
+ event.node.selected = event.selected;
+ this.nodeSelected.emit(event);
+ this.nodesChanged.emit(this.nodes);
+ };
+
+ private clearAllSelections(nodes: TreeNode[]): void {
+ for (const node of nodes) {
+ node.selected = false;
+ if (node.children) {
+ this.clearAllSelections(node.children);
+ }
+ }
+ }
+
+ // Public API methods
+ expandAllNodes(): void {
+ this.setAllExpanded(this.nodes, true);
+ this.nodesChanged.emit(this.nodes);
+ }
+
+ collapseAllNodes(): void {
+ this.setAllExpanded(this.nodes, false);
+ this.nodesChanged.emit(this.nodes);
+ }
+
+ getSelectedNodes(): TreeNode[] {
+ return this.findSelectedNodes(this.nodes);
+ }
+
+ selectNode(nodeId: string): void {
+ const node = this.findNodeById(this.nodes, nodeId);
+ if (node && !node.disabled) {
+ this.handleSelect({ node, selected: true });
+ }
+ }
+
+ private setAllExpanded(nodes: TreeNode[], expanded: boolean): void {
+ for (const node of nodes) {
+ if (node.children && node.children.length > 0) {
+ node.expanded = expanded;
+ this.setAllExpanded(node.children, expanded);
+ }
+ }
+ }
+
+ private findSelectedNodes(nodes: TreeNode[]): TreeNode[] {
+ const selected: TreeNode[] = [];
+ for (const node of nodes) {
+ if (node.selected) {
+ selected.push(node);
+ }
+ if (node.children) {
+ selected.push(...this.findSelectedNodes(node.children));
+ }
+ }
+ return selected;
+ }
+
+ private findNodeById(nodes: TreeNode[], id: string): TreeNode | null {
+ for (const node of nodes) {
+ if (node.id === id) {
+ return node;
+ }
+ if (node.children) {
+ const found = this.findNodeById(node.children, id);
+ if (found) {
+ return found;
+ }
+ }
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/feedback/alert/alert.component.scss b/projects/ui-essentials/src/lib/components/feedback/alert/alert.component.scss
new file mode 100644
index 0000000..d6dd823
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/feedback/alert/alert.component.scss
@@ -0,0 +1,221 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+.ui-alert {
+ // Core Structure
+ display: flex;
+ position: relative;
+ align-items: flex-start;
+
+ // Layout & Spacing
+ padding: $semantic-spacing-component-md;
+ margin-bottom: $semantic-spacing-component-sm;
+ gap: $semantic-spacing-component-sm;
+
+ // Visual Design
+ background: $semantic-color-surface-elevated;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ border-radius: $semantic-border-card-radius;
+ box-shadow: $semantic-shadow-elevation-1;
+
+ // 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;
+
+ // Transitions
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
+
+ // Size Variants
+ &--sm {
+ padding: $semantic-spacing-component-sm;
+ 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);
+ }
+
+ &--md {
+ // Default styles already applied above
+ }
+
+ &--lg {
+ padding: $semantic-spacing-component-lg;
+ gap: $semantic-spacing-component-md;
+ 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 Variants
+ &--primary {
+ background: $semantic-color-surface-elevated;
+ border-color: $semantic-color-primary;
+ border-left-width: 4px;
+
+ .ui-alert__icon {
+ color: $semantic-color-primary;
+ }
+ }
+
+ &--success {
+ background: $semantic-color-surface-elevated;
+ border-color: $semantic-color-success;
+ border-left-width: 4px;
+
+ .ui-alert__icon {
+ color: $semantic-color-success;
+ }
+
+ .ui-alert__title {
+ color: $semantic-color-success;
+ }
+ }
+
+ &--warning {
+ background: $semantic-color-surface-elevated;
+ border-color: $semantic-color-warning;
+ border-left-width: 4px;
+
+ .ui-alert__icon {
+ color: $semantic-color-warning;
+ }
+
+ .ui-alert__title {
+ color: $semantic-color-warning;
+ }
+ }
+
+ &--danger {
+ background: $semantic-color-surface-elevated;
+ border-color: $semantic-color-danger;
+ border-left-width: 4px;
+
+ .ui-alert__icon {
+ color: $semantic-color-danger;
+ }
+
+ .ui-alert__title {
+ color: $semantic-color-danger;
+ }
+ }
+
+ &--info {
+ background: $semantic-color-surface-elevated;
+ border-color: $semantic-color-info;
+ border-left-width: 4px;
+
+ .ui-alert__icon {
+ color: $semantic-color-info;
+ }
+
+ .ui-alert__title {
+ color: $semantic-color-info;
+ }
+ }
+
+ // BEM Elements
+ &__icon {
+ flex-shrink: 0;
+ color: $semantic-color-text-secondary;
+ font-size: $semantic-sizing-icon-inline;
+ margin-top: 2px; // Slight optical alignment
+ }
+
+ &__content {
+ flex: 1;
+ min-width: 0; // Prevents flex overflow
+ }
+
+ &__title {
+ margin: 0 0 $semantic-spacing-content-line-tight 0;
+ 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;
+
+ &--bold {
+ font-weight: $semantic-typography-font-weight-semibold;
+ }
+ }
+
+ &__message {
+ margin: 0;
+ color: $semantic-color-text-secondary;
+ }
+
+ &__actions {
+ margin-top: $semantic-spacing-component-sm;
+ display: flex;
+ gap: $semantic-spacing-component-xs;
+ flex-wrap: wrap;
+ }
+
+ &__dismiss {
+ position: absolute;
+ top: $semantic-spacing-component-xs;
+ right: $semantic-spacing-component-xs;
+ background: transparent;
+ border: none;
+ color: $semantic-color-text-tertiary;
+ cursor: pointer;
+ padding: $semantic-spacing-component-xs;
+ border-radius: $semantic-border-radius-sm;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-width: $semantic-sizing-touch-minimum;
+ min-height: $semantic-sizing-touch-minimum;
+
+ &:hover {
+ background: $semantic-color-surface-secondary;
+ color: $semantic-color-text-secondary;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+
+ &:active {
+ background: $semantic-color-surface-primary;
+ }
+ }
+
+ // State Variants
+ &--dismissible {
+ padding-right: calc($semantic-spacing-component-lg + $semantic-sizing-touch-minimum);
+ }
+
+ // Responsive Design
+ @media (max-width: $semantic-breakpoint-sm - 1) {
+ padding: $semantic-spacing-component-sm;
+ gap: $semantic-spacing-component-xs;
+
+ &--lg {
+ padding: $semantic-spacing-component-md;
+ }
+
+ &__title {
+ 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);
+ }
+
+ &__message {
+ 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);
+ }
+
+ &--dismissible {
+ padding-right: calc($semantic-spacing-component-md + $semantic-sizing-touch-minimum);
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/feedback/alert/alert.component.ts b/projects/ui-essentials/src/lib/components/feedback/alert/alert.component.ts
new file mode 100644
index 0000000..981fbea
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/feedback/alert/alert.component.ts
@@ -0,0 +1,136 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { IconDefinition } from '@fortawesome/free-solid-svg-icons';
+import {
+ faCheckCircle,
+ faExclamationTriangle,
+ faExclamationCircle,
+ faInfoCircle,
+ faTimes
+} from '@fortawesome/free-solid-svg-icons';
+
+type AlertSize = 'sm' | 'md' | 'lg';
+type AlertVariant = 'primary' | 'success' | 'warning' | 'danger' | 'info';
+
+@Component({
+ selector: 'ui-alert',
+ standalone: true,
+ imports: [CommonModule, FontAwesomeModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ template: `
+
+
+ @if (showIcon && alertIcon) {
+
+
+ }
+
+
+ @if (title) {
+
+ {{ title }}
+
+ }
+
+
+
+
+
+ @if (actions && actions.length > 0) {
+
+
+
+ }
+
+
+ @if (dismissible) {
+
+
+
+ }
+
+ `,
+ styleUrl: './alert.component.scss'
+})
+export class AlertComponent {
+ @Input() size: AlertSize = 'md';
+ @Input() variant: AlertVariant = 'primary';
+ @Input() title?: string;
+ @Input() boldTitle = false;
+ @Input() showIcon = true;
+ @Input() dismissible = false;
+ @Input() dismissLabel = 'Dismiss alert';
+ @Input() role = 'alert';
+ @Input() ariaLive: 'polite' | 'assertive' | 'off' = 'polite';
+ @Input() actions: any[] = [];
+
+ @Output() dismissed = new EventEmitter();
+
+ // Icons
+ readonly faCheckCircle = faCheckCircle;
+ readonly faExclamationTriangle = faExclamationTriangle;
+ readonly faExclamationCircle = faExclamationCircle;
+ readonly faInfoCircle = faInfoCircle;
+ readonly faTimes = faTimes;
+
+ // Generate unique ID for accessibility
+ readonly alertId = Math.random().toString(36).substr(2, 9);
+
+ get alertIcon(): IconDefinition | null {
+ if (!this.showIcon) return null;
+
+ switch (this.variant) {
+ case 'success':
+ return this.faCheckCircle;
+ case 'warning':
+ return this.faExclamationTriangle;
+ case 'danger':
+ return this.faExclamationCircle;
+ case 'info':
+ return this.faInfoCircle;
+ case 'primary':
+ default:
+ return this.faInfoCircle;
+ }
+ }
+
+ handleDismiss(): void {
+ this.dismissed.emit();
+ }
+
+ handleDismissKeydown(event: KeyboardEvent): void {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ this.handleDismiss();
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/feedback/alert/index.ts b/projects/ui-essentials/src/lib/components/feedback/alert/index.ts
new file mode 100644
index 0000000..5a36e1b
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/feedback/alert/index.ts
@@ -0,0 +1 @@
+export * from './alert.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/media/video-player/video-player.component.scss b/projects/ui-essentials/src/lib/components/media/video-player/video-player.component.scss
index a154c14..a232b00 100644
--- a/projects/ui-essentials/src/lib/components/media/video-player/video-player.component.scss
+++ b/projects/ui-essentials/src/lib/components/media/video-player/video-player.component.scss
@@ -1,109 +1,5 @@
@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
-.video-player {
- position: relative;
- width: 100%;
- max-width: 800px;
- background: $semantic-color-surface;
- border-radius: 12px;
- overflow: hidden;
- box-shadow: $semantic-shadow-card-rest;
-
- &.fullscreen {
- max-width: none;
- width: 100vw;
- height: 100vh;
- border-radius: 0;
- }
-
- .video-container {
- position: relative;
- width: 100%;
- height: 0;
- padding-bottom: 56.25%; // 16:9 aspect ratio
- background: #000;
-
- video {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- object-fit: contain;
- }
- }
-
- .controls {
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
- padding: $semantic-spacing-component-lg $semantic-spacing-component-md $semantic-spacing-component-md;
- opacity: 0;
- transition: opacity $semantic-motion-duration-normal $semantic-motion-easing-ease;
-
- &.visible {
- opacity: 1;
- }
-
- .progress-bar {
- width: 100%;
- height: 4px;
- background: rgba(255, 255, 255, 0.3);
- border-radius: 2px;
- margin-bottom: $semantic-spacing-component-sm;
- cursor: pointer;
-
- .progress {
- height: 100%;
- background: $semantic-color-primary;
- border-radius: 2px;
- transition: width 0.1s linear;
- }
- }
-
- .control-buttons {
- display: flex;
- align-items: center;
- gap: $semantic-spacing-component-sm;
-
- button {
- background: none;
- border: none;
- color: white;
- cursor: pointer;
- padding: $semantic-spacing-component-xs;
- border-radius: 4px;
- transition: background-color $semantic-motion-duration-fast $semantic-motion-easing-ease;
-
- &:hover {
- background: rgba(255, 255, 255, 0.2);
- }
-
- &:focus {
- outline: 2px solid $semantic-color-primary;
- outline-offset: 2px;
- }
- }
-
- .time-display {
- color: white;
- 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);
- letter-spacing: map-get($semantic-typography-body-small, letter-spacing);
- margin-left: auto;
- }
- }
- }
-
- &:hover .controls {
- opacity: 1;
- }
-}
-
@mixin font-properties($font-map) {
@if type-of($font-map) == 'map' {
font-family: map-get($font-map, 'font-family');
@@ -114,4 +10,392 @@
} @else {
font: $font-map;
}
+}
+
+.ui-video-player {
+ position: relative;
+ width: 100%;
+ max-width: 800px;
+ background: $semantic-color-surface;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: $semantic-shadow-elevation-2;
+
+ &--fullscreen {
+ max-width: none !important;
+ width: 100vw !important;
+ height: 100vh !important;
+ border-radius: 0;
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 9999;
+ }
+
+ &--disabled {
+ opacity: 0.6;
+ pointer-events: none;
+ }
+
+ &--loading {
+ .ui-video-player__video {
+ cursor: wait;
+ }
+ }
+
+ // Size variants
+ &--sm {
+ max-width: 400px;
+ .ui-video-player__video-container {
+ padding-bottom: 56.25%; // 16:9 aspect ratio
+ }
+ }
+
+ &--md {
+ max-width: 800px;
+ .ui-video-player__video-container {
+ padding-bottom: 56.25%; // 16:9 aspect ratio
+ }
+ }
+
+ &--lg {
+ max-width: 1200px;
+ .ui-video-player__video-container {
+ padding-bottom: 56.25%; // 16:9 aspect ratio
+ }
+ }
+
+ // Variant styles
+ &--minimal {
+ border-radius: 0;
+ box-shadow: none;
+ background: transparent;
+ }
+
+ &--theater {
+ max-width: none;
+ width: 100%;
+ background: #000;
+
+ .ui-video-player__video-container {
+ padding-bottom: 42.85%; // 21:9 aspect ratio for theater mode
+ }
+ }
+
+ &__video-container {
+ position: relative;
+ width: 100%;
+ height: 0;
+ padding-bottom: 56.25%; // 16:9 aspect ratio
+ background: #000;
+ }
+
+ &__video {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ cursor: pointer;
+ }
+
+ &__loading-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.8);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: $semantic-spacing-component-md;
+ z-index: 2;
+ }
+
+ &__loading-spinner {
+ width: 40px;
+ height: 40px;
+ border: 3px solid rgba(255, 255, 255, 0.3);
+ border-top: 3px solid $semantic-color-primary;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ }
+
+ &__loading-text {
+ color: white;
+ @include font-properties($semantic-typography-body-medium);
+ }
+
+ &__controls {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
+ padding: $semantic-spacing-component-lg $semantic-spacing-component-md $semantic-spacing-component-md;
+ opacity: 0;
+ transition: opacity $semantic-duration-medium $semantic-easing-standard;
+ z-index: 3;
+ display: flex;
+ align-items: center;
+ gap: $semantic-spacing-component-sm;
+
+ &--visible {
+ opacity: 1;
+ }
+ }
+
+ &__control {
+ background: none;
+ border: none;
+ color: white;
+ cursor: pointer;
+ padding: $semantic-spacing-component-xs;
+ border-radius: 4px;
+ transition: background-color $semantic-duration-fast $semantic-easing-standard;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &:hover:not(:disabled) {
+ background: rgba(255, 255, 255, 0.2);
+ }
+
+ &:focus {
+ outline: 2px solid $semantic-color-primary;
+ outline-offset: 2px;
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+ }
+
+ &__control-icon {
+ width: 20px;
+ height: 20px;
+ }
+
+ &__progress-container {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ position: relative;
+ margin: 0 $semantic-spacing-component-sm;
+ }
+
+ &__progress-bar {
+ width: 100%;
+ height: 4px;
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ outline: none;
+ -webkit-appearance: none;
+ appearance: none;
+
+ &::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: $semantic-color-primary;
+ cursor: pointer;
+ border: 2px solid white;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ }
+
+ &::-moz-range-thumb {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: $semantic-color-primary;
+ cursor: pointer;
+ border: 2px solid white;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ }
+ }
+
+ &__progress-track {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 100%;
+ height: 4px;
+ background: rgba(255, 255, 255, 0.3);
+ border-radius: 2px;
+ pointer-events: none;
+ }
+
+ &__progress-filled {
+ height: 100%;
+ background: $semantic-color-primary;
+ border-radius: 2px;
+ transition: width 0.1s linear;
+ }
+
+ &__time {
+ color: white;
+ @include font-properties($semantic-typography-body-small);
+ white-space: nowrap;
+ margin: 0 $semantic-spacing-component-xs;
+ }
+
+ &__volume-bar {
+ width: 80px;
+ height: 4px;
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ outline: none;
+ -webkit-appearance: none;
+ appearance: none;
+ margin-left: $semantic-spacing-component-xs;
+
+ &::-webkit-slider-track {
+ width: 100%;
+ height: 4px;
+ background: rgba(255, 255, 255, 0.3);
+ border-radius: 2px;
+ }
+
+ &::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: $semantic-color-primary;
+ cursor: pointer;
+ border: 2px solid white;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ }
+
+ &::-moz-range-track {
+ width: 100%;
+ height: 4px;
+ background: rgba(255, 255, 255, 0.3);
+ border-radius: 2px;
+ }
+
+ &::-moz-range-thumb {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: $semantic-color-primary;
+ cursor: pointer;
+ border: 2px solid white;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ }
+ }
+
+ &__fallback {
+ color: white;
+ @include font-properties($semantic-typography-body-medium);
+ text-align: center;
+ padding: $semantic-spacing-component-lg;
+
+ a {
+ color: $semantic-color-primary;
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+ }
+
+ &__error {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.9);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: $semantic-spacing-component-md;
+ z-index: 4;
+ padding: $semantic-spacing-component-lg;
+ }
+
+ &__error-icon {
+ width: 48px;
+ height: 48px;
+ color: $semantic-color-error;
+
+ svg {
+ width: 100%;
+ height: 100%;
+ }
+ }
+
+ &__error-message {
+ color: white;
+ @include font-properties($semantic-typography-body-large);
+ text-align: center;
+ margin: 0;
+ }
+
+ &__error-link {
+ color: $semantic-color-primary;
+ @include font-properties($semantic-typography-body-medium);
+ text-decoration: none;
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
+ border: 1px solid $semantic-color-primary;
+ border-radius: 4px;
+ transition: all $semantic-duration-fast $semantic-easing-standard;
+
+ &:hover {
+ background: $semantic-color-primary;
+ color: white;
+ }
+ }
+
+ // Show controls on hover or when paused
+ &:hover &__controls,
+ &__controls--visible {
+ opacity: 1;
+ }
+
+ // Responsive design
+ @media (max-width: 768px) {
+ max-width: none;
+
+ &--sm,
+ &--md,
+ &--lg {
+ max-width: none;
+ }
+
+ &__controls {
+ padding: $semantic-spacing-component-md $semantic-spacing-component-sm;
+ gap: $semantic-spacing-component-xs;
+ }
+
+ &__control-icon {
+ width: 18px;
+ height: 18px;
+ }
+
+ &__volume-bar {
+ width: 60px;
+ }
+
+ &__time {
+ @include font-properties($semantic-typography-body-small);
+ }
+ }
+}
+
+@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/overlays/index.ts b/projects/ui-essentials/src/lib/components/overlays/index.ts
index e11e25c..aadefd4 100644
--- a/projects/ui-essentials/src/lib/components/overlays/index.ts
+++ b/projects/ui-essentials/src/lib/components/overlays/index.ts
@@ -1,4 +1,5 @@
export * from './modal';
export * from './drawer';
export * from './backdrop';
-export * from './overlay-container';
\ No newline at end of file
+export * from './overlay-container';
+export * from './popover';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/overlays/popover/index.ts b/projects/ui-essentials/src/lib/components/overlays/popover/index.ts
new file mode 100644
index 0000000..5c1711b
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/overlays/popover/index.ts
@@ -0,0 +1 @@
+export * from './popover.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/overlays/popover/popover.component.scss b/projects/ui-essentials/src/lib/components/overlays/popover/popover.component.scss
new file mode 100644
index 0000000..424f127
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/overlays/popover/popover.component.scss
@@ -0,0 +1,384 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+.ui-popover {
+ position: absolute;
+ display: block;
+ opacity: 0;
+ transform: scale(0.95);
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
+ z-index: $semantic-layer-dropdown;
+ will-change: opacity, transform;
+
+ // Core Structure
+ background: $semantic-color-surface-primary;
+ border: $semantic-border-card-width solid $semantic-color-border-primary;
+ border-radius: $semantic-border-card-radius;
+ box-shadow: $semantic-shadow-popover;
+
+ // Typography
+ font-size: $semantic-typography-font-size-sm;
+ color: $semantic-color-text-primary;
+ line-height: $semantic-typography-line-height-normal;
+
+ // Size Variants
+ &--sm {
+ min-width: $semantic-sizing-card-width-sm;
+ max-width: $semantic-sizing-modal-width-sm;
+ padding: $semantic-spacing-component-xs;
+ }
+
+ &--md {
+ min-width: $semantic-sizing-card-width-md;
+ max-width: $semantic-sizing-modal-width-md;
+ padding: $semantic-spacing-component-sm;
+ }
+
+ &--lg {
+ min-width: $semantic-sizing-card-width-lg;
+ max-width: $semantic-sizing-modal-width-lg;
+ padding: $semantic-spacing-component-md;
+ }
+
+ // Position Variants
+ &--top {
+ transform-origin: bottom center;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+
+ &--bottom {
+ transform-origin: top center;
+ margin-top: $semantic-spacing-component-sm;
+ }
+
+ &--left {
+ transform-origin: center right;
+ margin-right: $semantic-spacing-component-sm;
+ }
+
+ &--right {
+ transform-origin: center left;
+ margin-left: $semantic-spacing-component-sm;
+ }
+
+ &--top-start {
+ transform-origin: bottom left;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+
+ &--top-end {
+ transform-origin: bottom right;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+
+ &--bottom-start {
+ transform-origin: top left;
+ margin-top: $semantic-spacing-component-sm;
+ }
+
+ &--bottom-end {
+ transform-origin: top right;
+ margin-top: $semantic-spacing-component-sm;
+ }
+
+ // Variant Styles
+ &--default {
+ background: $semantic-color-surface-primary;
+ border-color: $semantic-color-border-primary;
+ }
+
+ &--elevated {
+ background: $semantic-color-surface-primary;
+ border: none;
+ box-shadow: $semantic-shadow-elevation-3;
+ }
+
+ &--floating {
+ background: $semantic-color-surface-secondary;
+ border-color: $semantic-color-border-subtle;
+ backdrop-filter: blur(8px);
+ }
+
+ &--menu {
+ padding: $semantic-spacing-content-line-tight;
+ background: $semantic-color-surface-primary;
+ }
+
+ &--tooltip {
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ background: $semantic-color-surface-dim;
+ color: $semantic-color-text-inverse;
+ border: none;
+ font-size: $semantic-typography-font-size-xs;
+ box-shadow: $semantic-shadow-elevation-2;
+ }
+
+ // State Variants
+ &--visible {
+ opacity: 1;
+ transform: scale(1);
+ pointer-events: auto;
+ }
+
+ &--entering {
+ opacity: 0;
+ transform: scale(0.95);
+ }
+
+ &--leaving {
+ opacity: 0;
+ transform: scale(0.95);
+ transition-duration: $semantic-motion-duration-fast;
+ }
+
+ &--disabled {
+ opacity: 0.5;
+ pointer-events: none;
+ }
+
+ // Arrow/Triangle
+ &__arrow {
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-style: solid;
+ pointer-events: none;
+
+ // Arrow positioning and styling based on popover position
+ .ui-popover--top & {
+ bottom: calc(-1 * $semantic-spacing-component-sm);
+ left: 50%;
+ transform: translateX(-50%);
+ border-width: $semantic-spacing-component-sm $semantic-spacing-component-sm 0 $semantic-spacing-component-sm;
+ border-color: $semantic-color-surface-primary transparent transparent transparent;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: calc(-1 * $semantic-spacing-component-sm - 1px);
+ left: calc(-1 * $semantic-spacing-component-sm);
+ border-width: $semantic-spacing-component-sm $semantic-spacing-component-sm 0 $semantic-spacing-component-sm;
+ border-color: $semantic-color-border-primary transparent transparent transparent;
+ border-style: solid;
+ }
+ }
+
+ .ui-popover--bottom & {
+ top: calc(-1 * $semantic-spacing-component-sm);
+ left: 50%;
+ transform: translateX(-50%);
+ border-width: 0 $semantic-spacing-component-sm $semantic-spacing-component-sm $semantic-spacing-component-sm;
+ border-color: transparent transparent $semantic-color-surface-primary transparent;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: calc(-1px);
+ left: calc(-1 * $semantic-spacing-component-sm);
+ border-width: 0 $semantic-spacing-component-sm $semantic-spacing-component-sm $semantic-spacing-component-sm;
+ border-color: transparent transparent $semantic-color-border-primary transparent;
+ border-style: solid;
+ }
+ }
+
+ .ui-popover--left & {
+ right: calc(-1 * $semantic-spacing-component-sm);
+ top: 50%;
+ transform: translateY(-50%);
+ border-width: $semantic-spacing-component-sm 0 $semantic-spacing-component-sm $semantic-spacing-component-sm;
+ border-color: transparent transparent transparent $semantic-color-surface-primary;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: calc(-1 * $semantic-spacing-component-sm);
+ left: calc(-1 * $semantic-spacing-component-sm - 1px);
+ border-width: $semantic-spacing-component-sm 0 $semantic-spacing-component-sm $semantic-spacing-component-sm;
+ border-color: transparent transparent transparent $semantic-color-border-primary;
+ border-style: solid;
+ }
+ }
+
+ .ui-popover--right & {
+ left: calc(-1 * $semantic-spacing-component-sm);
+ top: 50%;
+ transform: translateY(-50%);
+ border-width: $semantic-spacing-component-sm $semantic-spacing-component-sm $semantic-spacing-component-sm 0;
+ border-color: transparent $semantic-color-surface-primary transparent transparent;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: calc(-1 * $semantic-spacing-component-sm);
+ left: calc(-1px);
+ border-width: $semantic-spacing-component-sm $semantic-spacing-component-sm $semantic-spacing-component-sm 0;
+ border-color: transparent $semantic-color-border-primary transparent transparent;
+ border-style: solid;
+ }
+ }
+
+ // Hide arrow for elevated and floating variants
+ .ui-popover--elevated &,
+ .ui-popover--floating & {
+ display: none;
+ }
+ }
+
+ // Content Area
+ &__content {
+ position: relative;
+ width: 100%;
+ max-height: $semantic-sizing-modal-height-max;
+ overflow: auto;
+
+ // Scrollbar styling
+ scrollbar-width: thin;
+ scrollbar-color: $semantic-color-border-subtle $semantic-color-surface-secondary;
+
+ &::-webkit-scrollbar {
+ width: 6px;
+ }
+
+ &::-webkit-scrollbar-track {
+ background: $semantic-color-surface-secondary;
+ border-radius: $semantic-border-input-radius;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: $semantic-color-border-subtle;
+ border-radius: $semantic-border-input-radius;
+
+ &:hover {
+ background: $semantic-color-border-primary;
+ }
+ }
+ }
+
+ // Header (optional)
+ &__header {
+ padding: $semantic-spacing-component-sm;
+ border-bottom: $semantic-border-card-width solid $semantic-color-border-subtle;
+ margin-bottom: $semantic-spacing-content-line-tight;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ &__title {
+ font-size: $semantic-typography-font-size-md;
+ font-weight: $semantic-typography-font-weight-semibold;
+ color: $semantic-color-text-primary;
+ margin: 0;
+ }
+
+ // Footer (optional)
+ &__footer {
+ padding: $semantic-spacing-component-sm;
+ border-top: $semantic-border-card-width solid $semantic-color-border-subtle;
+ margin-top: $semantic-spacing-content-line-tight;
+ display: flex;
+ gap: $semantic-spacing-content-line-tight;
+ justify-content: flex-end;
+
+ &:first-child {
+ margin-top: 0;
+ }
+
+ &--start {
+ justify-content: flex-start;
+ }
+
+ &--center {
+ justify-content: center;
+ }
+
+ &--between {
+ justify-content: space-between;
+ }
+ }
+
+ // Interactive states for focusable content
+ &:not(.ui-popover--disabled) {
+ &:focus-within {
+ box-shadow: $semantic-shadow-popover, 0 0 0 2px $semantic-color-focus;
+ }
+ }
+
+ // Responsive adjustments
+ @media (max-width: $semantic-breakpoint-sm - 1) {
+ // On mobile, popovers should be more prominent
+ &:not(.ui-popover--tooltip) {
+ min-width: auto;
+ max-width: calc(100vw - #{$semantic-spacing-layout-section-xs});
+
+ &.ui-popover--sm {
+ padding: $semantic-spacing-component-sm;
+ }
+ }
+ }
+
+ // Dark mode support
+ :host-context(.dark-theme) & {
+ &--default {
+ background: $semantic-color-surface-primary;
+ border-color: $semantic-color-border-primary;
+ }
+
+ &--elevated {
+ background: $semantic-color-surface-primary;
+ box-shadow: $semantic-shadow-elevation-3;
+ }
+
+ &--floating {
+ background: $semantic-color-surface-secondary;
+ border-color: $semantic-color-border-subtle;
+ }
+ }
+
+ // Animation variants for different entrance effects
+ &--slide-in {
+ &.ui-popover--top {
+ transform: translateY(-10px) scale(0.95);
+
+ &.ui-popover--visible {
+ transform: translateY(0) scale(1);
+ }
+ }
+
+ &.ui-popover--bottom {
+ transform: translateY(10px) scale(0.95);
+
+ &.ui-popover--visible {
+ transform: translateY(0) scale(1);
+ }
+ }
+
+ &.ui-popover--left {
+ transform: translateX(-10px) scale(0.95);
+
+ &.ui-popover--visible {
+ transform: translateX(0) scale(1);
+ }
+ }
+
+ &.ui-popover--right {
+ transform: translateX(10px) scale(0.95);
+
+ &.ui-popover--visible {
+ transform: translateX(0) scale(1);
+ }
+ }
+ }
+}
+
+// Popover backdrop (if needed for modal-like behavior)
+.ui-popover-backdrop {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: transparent;
+ z-index: calc($semantic-layer-dropdown - 1);
+ pointer-events: auto;
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/overlays/popover/popover.component.ts b/projects/ui-essentials/src/lib/components/overlays/popover/popover.component.ts
new file mode 100644
index 0000000..d4431cb
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/overlays/popover/popover.component.ts
@@ -0,0 +1,645 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, OnInit, OnDestroy, AfterViewInit, inject, ElementRef, Renderer2, ViewChild, HostListener } from '@angular/core';
+import { CommonModule, DOCUMENT } from '@angular/common';
+import { signal, computed } from '@angular/core';
+
+type PopoverPosition = 'top' | 'bottom' | 'left' | 'right' | 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end';
+type PopoverSize = 'sm' | 'md' | 'lg';
+type PopoverVariant = 'default' | 'elevated' | 'floating' | 'menu' | 'tooltip';
+type PopoverTrigger = 'click' | 'hover' | 'focus' | 'manual';
+
+export interface PopoverConfig {
+ position?: PopoverPosition;
+ size?: PopoverSize;
+ variant?: PopoverVariant;
+ trigger?: PopoverTrigger;
+ visible?: boolean;
+ closable?: boolean;
+ backdropClosable?: boolean;
+ escapeClosable?: boolean;
+ showArrow?: boolean;
+ showBackdrop?: boolean;
+ autoPosition?: boolean;
+ offset?: number;
+ delay?: number;
+ hoverDelay?: number;
+ focusable?: boolean;
+ preventBodyScroll?: boolean;
+}
+
+export interface PopoverPositionData {
+ top: number;
+ left: number;
+ position: PopoverPosition;
+}
+
+@Component({
+ selector: 'ui-popover',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ template: `
+ @if (isVisible()) {
+
+ @if (showBackdrop) {
+
+ }
+
+
+
+
+ @if (showArrow && variant !== 'elevated' && variant !== 'floating') {
+
+ }
+
+
+ @if (title || headerSlot) {
+
+ }
+
+
+
+
+
+
+
+ @if (footerSlot) {
+
+ }
+
+ }
+ `,
+ styleUrl: './popover.component.scss'
+})
+export class PopoverComponent implements OnInit, OnDestroy, AfterViewInit {
+ // Dependencies
+ private elementRef = inject(ElementRef);
+ private renderer = inject(Renderer2);
+ private document = inject(DOCUMENT);
+
+ // ViewChild references
+ @ViewChild('popoverElement', { static: false }) popoverElement?: ElementRef;
+
+ // Inputs
+ @Input() position: PopoverPosition = 'bottom';
+ @Input() size: PopoverSize = 'md';
+ @Input() variant: PopoverVariant = 'default';
+ @Input() trigger: PopoverTrigger = 'click';
+ @Input() title = '';
+ @Input() disabled = false;
+ @Input() closable = true;
+ @Input() backdropClosable = false;
+ @Input() escapeClosable = true;
+ @Input() showArrow = true;
+ @Input() showBackdrop = false;
+ @Input() autoPosition = true;
+ @Input() offset = 8;
+ @Input() delay = 0;
+ @Input() hoverDelay = 300;
+ @Input() focusable = true;
+ @Input() preventBodyScroll = false;
+ @Input() animationType: 'scale' | 'slide' = 'scale';
+ @Input() footerAlignment: 'start' | 'center' | 'end' | 'between' = 'end';
+
+ // Reference element (trigger)
+ @Input() triggerElement?: HTMLElement;
+
+ // State Inputs
+ @Input() set visible(value: boolean) {
+ if (value !== this._visible()) {
+ this._visible.set(value);
+ if (value) {
+ this.show();
+ } else {
+ this.hide();
+ }
+ }
+ }
+
+ get visible(): boolean {
+ return this._visible();
+ }
+
+ // Outputs
+ @Output() visibleChange = new EventEmitter();
+ @Output() shown = new EventEmitter();
+ @Output() hidden = new EventEmitter();
+ @Output() positionChange = new EventEmitter();
+ @Output() backdropClicked = new EventEmitter();
+ @Output() escapePressed = new EventEmitter();
+
+ // Internal state signals
+ private _visible = signal(false);
+ private _entering = signal(false);
+ private _leaving = signal(false);
+ private _computedPosition = signal(this.position);
+ private _positionData = signal({ top: 0, left: 0, position: this.position });
+
+ // Computed signals
+ readonly isVisible = computed(() => this._visible() || this._entering() || this._leaving());
+ readonly isEntering = computed(() => this._entering());
+ readonly isLeaving = computed(() => this._leaving());
+ readonly computedPosition = computed(() => this._computedPosition());
+ readonly positionData = computed(() => this._positionData());
+
+ // Internal properties
+ private showTimer?: number;
+ private hideTimer?: number;
+ private resizeObserver?: ResizeObserver;
+ private mutationObserver?: MutationObserver;
+ private originalBodyOverflow = '';
+ private animationDuration = 200; // ms
+ private positionUpdateCounter = 0;
+
+ // Template helpers
+ readonly headerSlot = false; // Could be enhanced to detect slotted content
+ readonly footerSlot = false; // Could be enhanced to detect slotted content
+
+ // Unique IDs for accessibility
+ readonly titleId = `popover-title-${Math.random().toString(36).substr(2, 9)}`;
+ readonly contentId = `popover-content-${Math.random().toString(36).substr(2, 9)}`;
+
+ readonly transformOrigin = computed(() => {
+ const position = this.computedPosition();
+ switch (position) {
+ case 'top': return 'bottom center';
+ case 'top-start': return 'bottom left';
+ case 'top-end': return 'bottom right';
+ case 'bottom': return 'top center';
+ case 'bottom-start': return 'top left';
+ case 'bottom-end': return 'top right';
+ case 'left': return 'center right';
+ case 'right': return 'center left';
+ default: return 'center';
+ }
+ });
+
+ ngOnInit(): void {
+ if (this.visible) {
+ this.show();
+ }
+ this.setupTriggerListeners();
+ }
+
+ ngAfterViewInit(): void {
+ if (this.visible) {
+ setTimeout(() => this.updatePosition(), 0);
+ }
+ }
+
+ ngOnDestroy(): void {
+ this.clearTimers();
+ this.cleanup();
+ this.restoreBodyScroll();
+ }
+
+ /**
+ * Shows the popover with animation and positioning
+ */
+ show(): void {
+ if (this._visible() || this.disabled) return;
+
+ this.clearTimers();
+
+ this.showTimer = window.setTimeout(() => {
+ this.preventBodyScrollIfEnabled();
+ this._visible.set(true);
+ this._entering.set(true);
+ this.visibleChange.emit(true);
+
+ this.updatePosition();
+ this.setupPositionObservers();
+
+ // Animation timing
+ setTimeout(() => {
+ this._entering.set(false);
+ this.shown.emit();
+ }, this.animationDuration);
+ }, this.delay);
+ }
+
+ /**
+ * Hides the popover with animation
+ */
+ hide(): void {
+ if (!this._visible()) return;
+
+ this.clearTimers();
+ this.cleanup();
+
+ this._leaving.set(true);
+
+ // Animation timing
+ setTimeout(() => {
+ this._visible.set(false);
+ this._leaving.set(false);
+ this.restoreBodyScroll();
+ this.visibleChange.emit(false);
+ this.hidden.emit();
+ }, this.animationDuration);
+ }
+
+ /**
+ * Toggles popover visibility
+ */
+ toggle(): void {
+ if (this._visible()) {
+ this.hide();
+ } else {
+ this.show();
+ }
+ }
+
+ /**
+ * Updates the popover position relative to the trigger element
+ */
+ updatePosition(): void {
+ if (!this.triggerElement || !this.popoverElement) return;
+
+ const triggerRect = this.triggerElement.getBoundingClientRect();
+ const popoverEl = this.popoverElement.nativeElement;
+ const popoverRect = popoverEl.getBoundingClientRect();
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
+ const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
+
+ let position = this.position;
+ let top = 0;
+ let left = 0;
+
+ // Calculate initial position
+ const positions = this.calculatePositions(triggerRect, popoverRect, scrollLeft, scrollTop);
+ const initialPos = positions[position];
+ top = initialPos.top;
+ left = initialPos.left;
+
+ // Auto-position if enabled and popover goes outside viewport
+ if (this.autoPosition) {
+ const bestPosition = this.findBestPosition(positions, viewportWidth, viewportHeight, popoverRect);
+ position = bestPosition.position;
+ top = bestPosition.top;
+ left = bestPosition.left;
+ }
+
+ // Ensure popover stays within viewport bounds
+ const finalPosition = this.constrainToViewport(
+ { top, left, position },
+ popoverRect,
+ viewportWidth,
+ viewportHeight
+ );
+
+ // Update state
+ this._computedPosition.set(finalPosition.position);
+ this._positionData.set(finalPosition);
+
+ // Emit position change if it changed
+ if (finalPosition.position !== this.position && this.positionUpdateCounter > 0) {
+ this.positionChange.emit(finalPosition.position);
+ }
+
+ this.positionUpdateCounter++;
+ }
+
+ /**
+ * Calculates all possible positions for the popover
+ */
+ private calculatePositions(
+ triggerRect: DOMRect,
+ popoverRect: DOMRect,
+ scrollLeft: number,
+ scrollTop: number
+ ): Record {
+ const triggerCenterX = triggerRect.left + triggerRect.width / 2;
+ const triggerCenterY = triggerRect.top + triggerRect.height / 2;
+
+ return {
+ 'top': {
+ top: triggerRect.top - popoverRect.height - this.offset + scrollTop,
+ left: triggerCenterX - popoverRect.width / 2 + scrollLeft,
+ position: 'top'
+ },
+ 'top-start': {
+ top: triggerRect.top - popoverRect.height - this.offset + scrollTop,
+ left: triggerRect.left + scrollLeft,
+ position: 'top-start'
+ },
+ 'top-end': {
+ top: triggerRect.top - popoverRect.height - this.offset + scrollTop,
+ left: triggerRect.right - popoverRect.width + scrollLeft,
+ position: 'top-end'
+ },
+ 'bottom': {
+ top: triggerRect.bottom + this.offset + scrollTop,
+ left: triggerCenterX - popoverRect.width / 2 + scrollLeft,
+ position: 'bottom'
+ },
+ 'bottom-start': {
+ top: triggerRect.bottom + this.offset + scrollTop,
+ left: triggerRect.left + scrollLeft,
+ position: 'bottom-start'
+ },
+ 'bottom-end': {
+ top: triggerRect.bottom + this.offset + scrollTop,
+ left: triggerRect.right - popoverRect.width + scrollLeft,
+ position: 'bottom-end'
+ },
+ 'left': {
+ top: triggerCenterY - popoverRect.height / 2 + scrollTop,
+ left: triggerRect.left - popoverRect.width - this.offset + scrollLeft,
+ position: 'left'
+ },
+ 'right': {
+ top: triggerCenterY - popoverRect.height / 2 + scrollTop,
+ left: triggerRect.right + this.offset + scrollLeft,
+ position: 'right'
+ }
+ };
+ }
+
+ /**
+ * Finds the best position that fits within the viewport
+ */
+ private findBestPosition(
+ positions: Record,
+ viewportWidth: number,
+ viewportHeight: number,
+ popoverRect: DOMRect
+ ): PopoverPositionData {
+ const preferenceOrder: PopoverPosition[] = [
+ this.position,
+ ...Object.keys(positions).filter(p => p !== this.position) as PopoverPosition[]
+ ];
+
+ for (const pos of preferenceOrder) {
+ const posData = positions[pos];
+ const fitsHorizontally = posData.left >= 0 && posData.left + popoverRect.width <= viewportWidth;
+ const fitsVertically = posData.top >= 0 && posData.top + popoverRect.height <= viewportHeight;
+
+ if (fitsHorizontally && fitsVertically) {
+ return posData;
+ }
+ }
+
+ // If no position fits perfectly, return the preferred position
+ return positions[this.position];
+ }
+
+ /**
+ * Constrains the popover position to stay within viewport
+ */
+ private constrainToViewport(
+ position: PopoverPositionData,
+ popoverRect: DOMRect,
+ viewportWidth: number,
+ viewportHeight: number
+ ): PopoverPositionData {
+ const margin = 8; // Keep some margin from viewport edges
+
+ let { top, left } = position;
+
+ // Constrain horizontally
+ if (left < margin) {
+ left = margin;
+ } else if (left + popoverRect.width > viewportWidth - margin) {
+ left = viewportWidth - popoverRect.width - margin;
+ }
+
+ // Constrain vertically
+ if (top < margin) {
+ top = margin;
+ } else if (top + popoverRect.height > viewportHeight - margin) {
+ top = viewportHeight - popoverRect.height - margin;
+ }
+
+ return { ...position, top, left };
+ }
+
+ /**
+ * Sets up trigger event listeners based on trigger type
+ */
+ private setupTriggerListeners(): void {
+ if (!this.triggerElement) return;
+
+ switch (this.trigger) {
+ case 'click':
+ this.renderer.listen(this.triggerElement, 'click', (e) => {
+ e.preventDefault();
+ this.toggle();
+ });
+ break;
+
+ case 'hover':
+ this.renderer.listen(this.triggerElement, 'mouseenter', () => {
+ this.clearTimers();
+ this.showTimer = window.setTimeout(() => this.show(), this.hoverDelay);
+ });
+ this.renderer.listen(this.triggerElement, 'mouseleave', () => {
+ this.clearTimers();
+ this.hideTimer = window.setTimeout(() => this.hide(), this.hoverDelay);
+ });
+ // Keep popover open when hovering over it
+ this.renderer.listen(this.elementRef.nativeElement, 'mouseenter', () => {
+ this.clearTimers();
+ });
+ this.renderer.listen(this.elementRef.nativeElement, 'mouseleave', () => {
+ this.hideTimer = window.setTimeout(() => this.hide(), this.hoverDelay);
+ });
+ break;
+
+ case 'focus':
+ this.renderer.listen(this.triggerElement, 'focus', () => this.show());
+ this.renderer.listen(this.triggerElement, 'blur', () => this.hide());
+ break;
+
+ case 'manual':
+ // No automatic triggers, controlled programmatically
+ break;
+ }
+ }
+
+ /**
+ * Sets up observers for position updates
+ */
+ private setupPositionObservers(): void {
+ // Resize observer to update position on window resize
+ this.resizeObserver = new ResizeObserver(() => {
+ if (this._visible()) {
+ this.updatePosition();
+ }
+ });
+ this.resizeObserver.observe(document.body);
+
+ // Listen for window scroll
+ window.addEventListener('scroll', this.updatePositionThrottled, { passive: true });
+ window.addEventListener('resize', this.updatePositionThrottled, { passive: true });
+ }
+
+ private updatePositionThrottled = this.throttle(() => {
+ if (this._visible()) {
+ this.updatePosition();
+ }
+ }, 16); // ~60fps
+
+ /**
+ * Handles backdrop click
+ */
+ handleBackdropClick(event: MouseEvent): void {
+ if (this.backdropClosable) {
+ this.hide();
+ this.backdropClicked.emit(event);
+ }
+ }
+
+ /**
+ * Handles keyboard events
+ */
+ handleKeydown(event: KeyboardEvent): void {
+ if (event.key === 'Escape' && this.escapeClosable) {
+ event.preventDefault();
+ this.hide();
+ this.escapePressed.emit(event);
+ }
+ }
+
+ /**
+ * Global keyboard listener for ESC key
+ */
+ @HostListener('document:keydown', ['$event'])
+ onDocumentKeydown(event: KeyboardEvent): void {
+ if (this._visible() && event.key === 'Escape' && this.escapeClosable) {
+ event.preventDefault();
+ this.hide();
+ this.escapePressed.emit(event);
+ }
+ }
+
+ /**
+ * Global click listener to close popover on outside clicks
+ */
+ @HostListener('document:click', ['$event'])
+ onDocumentClick(event: MouseEvent): void {
+ if (!this._visible() || !this.closable) return;
+
+ const target = event.target as HTMLElement;
+ const isInsidePopover = this.elementRef.nativeElement.contains(target);
+ const isInsideTrigger = this.triggerElement?.contains(target);
+
+ if (!isInsidePopover && !isInsideTrigger && this.trigger === 'click') {
+ this.hide();
+ }
+ }
+
+ /**
+ * Prevents body scroll when popover is modal-like
+ */
+ private preventBodyScrollIfEnabled(): void {
+ if (!this.preventBodyScroll) return;
+
+ this.originalBodyOverflow = document.body.style.overflow;
+ document.body.style.overflow = 'hidden';
+ }
+
+ /**
+ * Restores body scroll
+ */
+ private restoreBodyScroll(): void {
+ if (!this.preventBodyScroll) return;
+
+ document.body.style.overflow = this.originalBodyOverflow;
+ }
+
+ /**
+ * Clears any active timers
+ */
+ private clearTimers(): void {
+ if (this.showTimer) {
+ clearTimeout(this.showTimer);
+ this.showTimer = undefined;
+ }
+ if (this.hideTimer) {
+ clearTimeout(this.hideTimer);
+ this.hideTimer = undefined;
+ }
+ }
+
+ /**
+ * Cleanup observers and listeners
+ */
+ private cleanup(): void {
+ this.resizeObserver?.disconnect();
+ window.removeEventListener('scroll', this.updatePositionThrottled);
+ window.removeEventListener('resize', this.updatePositionThrottled);
+ }
+
+ /**
+ * Throttle function for performance
+ */
+ private throttle void>(func: T, limit: number): T {
+ let inThrottle: boolean;
+ return ((...args: any[]) => {
+ if (!inThrottle) {
+ func.apply(this, args);
+ inThrottle = true;
+ setTimeout(() => inThrottle = false, limit);
+ }
+ }) as T;
+ }
+
+ /**
+ * Public API method to apply configuration
+ */
+ configure(config: PopoverConfig): void {
+ Object.assign(this, config);
+ }
+
+ /**
+ * Public API method to set trigger element
+ */
+ setTriggerElement(element: HTMLElement): void {
+ this.triggerElement = element;
+ this.setupTriggerListeners();
+ }
+}
\ No newline at end of file