diff --git a/.gitignore b/.gitignore
index cc7b141..5fb00d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,3 +40,14 @@ testem.log
# System files
.DS_Store
Thumbs.db
+dist/
+.angular/
+
+# Font files and directories
+*.woff
+*.woff2
+*.eot
+*.ttf
+*.otf
+assets/fonts/
+projects/demo-ui-essentials/public/fonts/
diff --git a/.gitignore copy b/.gitignore copy
new file mode 100644
index 0000000..ca2586e
--- /dev/null
+++ b/.gitignore copy
@@ -0,0 +1,50 @@
+# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
+
+# Compiled output
+/dist
+/tmp
+/out-tsc
+/bazel-out
+
+# Node
+/node_modules
+npm-debug.log
+yarn-error.log
+
+# IDEs and editors
+.idea/
+.project
+.classpath
+.c9/
+*.launch
+.settings/
+*.sublime-workspace
+
+# Visual Studio Code
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+.history/*
+
+# Miscellaneous
+/.angular/cache
+.sass-cache/
+/connect.lock
+/coverage
+/libpeerconnection.log
+testem.log
+/typings
+
+# System files
+.DS_Store
+Thumbs.db
+
+# Font files - exclude large binary font assets
+/assets/fonts/
+*.ttf
+*.otf
+*.woff
+*.woff2
+*.eot
diff --git a/CLAUDE.md b/CLAUDE.md
index 03f961b..08ceb7d 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -4,11 +4,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview
-SSuite is an Angular workspace containing three libraries designed as a modular component system:
+SSuite is an Angular workspace containing three libraries and a demo application:
- **shared-ui**: UI design system with comprehensive SCSS styling, design tokens, and reusable components
-- **shared-utils**: Common utilities and shared services
-- **ui-essentials**: Essential UI components and functionality
+- **shared-utils**: Common utilities and shared services
+- **ui-essentials**: Essential UI components with comprehensive component library (buttons, forms, data-display, navigation, media, feedback, overlays, layout)
+- **demo-ui-essentials**: Demo application showcasing all components with live examples
## Development Commands
@@ -39,17 +40,29 @@ ng test ui-essentials
### Development Server
```bash
-# Start development server (if main application exists)
-ng serve
+# Start demo application server
+ng serve demo-ui-essentials
+
+# Alternative start command
+npm start
```
## Architecture
+### Workspace Structure
+```
+projects/
+├── shared-ui/ - Design system and SCSS architecture
+├── shared-utils/ - Common utilities and services
+├── ui-essentials/ - Component library with 8 major categories
+└── demo-ui-essentials/ - Demo application with component examples
+```
+
### Library Structure
Each library follows Angular's standard structure:
- `src/lib/` - Library source code
- `src/public-api.ts` - Public exports
-- `ng-package.json` - Library packaging configuration
+- `ng-package.json` - Library packaging configuration
- `package.json` - Library metadata
### TypeScript Path Mapping
@@ -62,6 +75,17 @@ Libraries are mapped in `tsconfig.json` paths:
}
```
+### Component Categories (ui-essentials)
+The component library is organized into these categories:
+- **buttons**: Button variants and actions
+- **forms**: Input, checkbox, radio, search, switch, autocomplete, date/time pickers, file upload, form fields
+- **data-display**: Tables, lists, cards, chips, badges, avatars
+- **navigation**: App bars, menus, pagination
+- **media**: Image containers, carousels, video players
+- **feedback**: Progress indicators, status badges, theme switchers, empty states, loading spinners, skeleton loaders
+- **overlays**: Modals, drawers, backdrops, overlay containers
+- **layout**: Containers, spacers, grid systems
+
### Design System (shared-ui)
Contains a comprehensive SCSS architecture:
- **Base tokens**: Colors, typography, spacing, motion, shadows
@@ -95,10 +119,22 @@ Entry points:
- Tokens: `projects/shared-ui/src/styles/tokens.scss`
- Semantic: `projects/shared-ui/src/styles/semantic/index.scss`
-## Development Notes
+## Development Workflow
-- All libraries use Angular 19.2+ with strict TypeScript configuration
+### Component Development
+1. **New components**: Add to appropriate category in `ui-essentials/src/lib/components/`
+2. **Export new components**: Update category `index.ts` files
+3. **Demo components**: Create matching demo in `demo-ui-essentials/src/app/demos/`
+4. **Routing**: Add demo routes to `demos.routes.ts`
+
+### Build Dependencies
- Libraries must be built before being consumed by other projects
-- Use `ng generate` commands within library contexts for scaffolding
+- Demo application depends on built library artifacts in `dist/`
+- Use watch mode (`ng build --watch`) for development iterations
+
+### Library Development
+- All libraries use Angular 19.2+ with strict TypeScript configuration
+- Use `ng generate` commands within library contexts for scaffolding
- Libraries are publishable to npm after building to `dist/` directory
-- Testing uses Karma with Jasmine framework
\ No newline at end of file
+- Testing uses Karma with Jasmine framework
+- FontAwesome icons integrated via `@fortawesome/angular-fontawesome`
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/app.component.ts b/projects/demo-ui-essentials/src/app/app.component.ts
index 2dea9ad..3f9ef7a 100644
--- a/projects/demo-ui-essentials/src/app/app.component.ts
+++ b/projects/demo-ui-essentials/src/app/app.component.ts
@@ -1,15 +1,19 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { ButtonComponent } from '../../../ui-essentials/src/public-api';
+import { FabComponent } from "../../../ui-essentials/src/lib/components/buttons/fab.component";
+import { DashboardComponent } from "./features/dashboard/dashboard.component";
@Component({
selector: 'app-root',
- imports: [RouterOutlet, ButtonComponent],
+ imports: [RouterOutlet, ButtonComponent, FabComponent, DashboardComponent],
template: `
-
+
+
`
})
export class AppComponent {
title = 'demo-ui-essentials';
}
+
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/appbar-demo/appbar-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/appbar-demo/appbar-demo.component.ts
new file mode 100644
index 0000000..a7c6281
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/appbar-demo/appbar-demo.component.ts
@@ -0,0 +1,588 @@
+import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { AppbarComponent } from '../../../../../ui-essentials/src/lib/components/navigation/appbar';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import {
+ faBars,
+ faSearch,
+ faUser,
+ faHeart,
+ faBell,
+ faCog,
+ faShoppingCart,
+ faHome,
+ faArrowLeft,
+ faPlus,
+ faShare,
+ faEllipsisV,
+ faSun,
+ faMoon
+} from '@fortawesome/free-solid-svg-icons';
+
+@Component({
+ selector: 'ui-appbar-demo',
+ standalone: true,
+ imports: [
+ CommonModule,
+ AppbarComponent,
+ FontAwesomeModule
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
Appbar Component Showcase
+
+
+
+ Appbar Variants
+
+
+
Compact
+
+
+
+ Compact Appbar
+
+
+
+
+
+
+
Standard
+
+
+
+ Standard Appbar
+
+ JS
+
+
+
+
+
+
+
Large
+
+
+
+ Large Appbar with More Space
+
+
+
+
+
+
+
+
+
+
+
Prominent
+
+
+
+ Prominent Design System
+
+
+
+
+
+
+
+
+ Elevation & Position
+
+
+
Elevated
+
+
+
+ Elevated Appbar
+
+
+
+
+
+
+
Not Elevated
+
+
+
+ Flat Appbar
+
+
+
+
+
+
+
+
+ Slot Configurations
+
+
+
Left Icon + Title + Right Avatar
+
+
+
+ Home
+
+
+
+
+
+
+
Left Logo + Title + Right Menu
+
+
+
+ Dashboard
+
+
+
+
+
+
+
+
+
+
+
Left Avatar + Title + Right Icon
+
+
+
+ UI
+
+ Profile Settings
+
+
+
+
+
+
+
+
+
Multiple Right Elements
+
+
+
+ Multi-Action Bar
+
+
+
+
+
+
+
+
+
+
+
+
+ Real-world Examples
+
+
+
+
+
Social Media App
+
+
+
+ SocialHub
+
+ Feed
+
+
+
+
+
+
+
Settings Page
+
+
+
+ Account Settings
+
+
+
+
+
+
+
+
+
+
+
+ Code Examples
+
+
Basic Appbar:
+
<ui-appbar
+ variant="standard"
+ [slots]="{title: true, leftIcon: true, rightIcon: true}"
+ [elevated]="false">
+ <fa-icon [icon]="faBars" slot="left-icon"></fa-icon>
+ <span slot="title">My App</span>
+ <fa-icon [icon]="faSearch" slot="right-icon"></fa-icon>
+</ui-appbar>
+
+
Appbar with Logo and Avatar:
+
<ui-appbar
+ variant="standard"
+ [slots]="{title: true, leftLogo: true, rightAvatar: true}"
+ [elevated]="true">
+ <div slot="left-logo" style="font-weight: 600;">
+ MyBrand
+ </div>
+ <span slot="title">Dashboard</span>
+ <img slot="right-avatar" src="avatar.jpg"
+ style="width: 32px; height: 32px; border-radius: 50%;">
+</ui-appbar>
+
+
Appbar with Multiple Actions:
+
<ui-appbar
+ variant="standard"
+ [slots]="{title: true, leftIcon: true, rightMenu: true}"
+ [elevated]="false">
+ <fa-icon [icon]="faArrowLeft" slot="left-icon"></fa-icon>
+ <span slot="title">Settings</span>
+ <div slot="right-menu" style="display: flex; gap: 1rem;">
+ <fa-icon [icon]="faSearch"></fa-icon>
+ <fa-icon [icon]="faEllipsisV"></fa-icon>
+ </div>
+</ui-appbar>
+
+
+
+ `,
+ styles: [`
+ h2 {
+ color: hsl(279, 14%, 11%);
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ border-bottom: 2px solid hsl(258, 100%, 47%);
+ padding-bottom: 0.5rem;
+ }
+
+ h3 {
+ color: hsl(279, 14%, 25%);
+ font-size: 1.5rem;
+ margin-bottom: 1rem;
+ }
+
+ h4 {
+ color: hsl(287, 12%, 35%);
+ font-size: 1.125rem;
+ margin-bottom: 0.75rem;
+ }
+
+ section {
+ border: 1px solid hsl(289, 14%, 90%);
+ border-radius: 8px;
+ padding: 1.5rem;
+ background: hsl(286, 20%, 99%);
+ }
+
+ pre {
+ font-size: 0.875rem;
+ line-height: 1.5;
+ margin: 0.5rem 0;
+ }
+
+ code {
+ font-family: 'JetBrains Mono', monospace;
+ color: #d63384;
+ }
+
+ fa-icon {
+ transition: color 0.2s ease;
+ }
+
+ fa-icon:hover {
+ color: #007bff;
+ }
+
+ label {
+ font-size: 0.875rem;
+ user-select: none;
+ }
+
+ input[type="checkbox"] {
+ margin: 0;
+ }
+
+ select {
+ font-size: 0.875rem;
+ }
+ `]
+})
+export class AppbarDemoComponent {
+ // State signals
+ isDarkMode = signal(false);
+ lastAction = signal('');
+
+ // Demo configuration
+ demoConfig = signal({
+ variant: 'standard' as any,
+ elevated: false,
+ title: 'Interactive Demo',
+ slots: {
+ leftIcon: true,
+ leftLogo: false,
+ leftAvatar: false,
+ title: true,
+ rightIcon: true,
+ rightLogo: false,
+ rightAvatar: false,
+ rightMenu: false
+ }
+ });
+
+ // Font Awesome icons
+ faBars = faBars;
+ faSearch = faSearch;
+ faUser = faUser;
+ faHeart = faHeart;
+ faBell = faBell;
+ faCog = faCog;
+ faShoppingCart = faShoppingCart;
+ faHome = faHome;
+ faArrowLeft = faArrowLeft;
+ faPlus = faPlus;
+ faShare = faShare;
+ faEllipsisV = faEllipsisV;
+ faSun = faSun;
+ faMoon = faMoon;
+
+ handleAction(action: string): void {
+ this.lastAction.set(`${action} clicked at ${new Date().toLocaleTimeString()}`);
+ console.log(`Appbar action: ${action}`);
+ }
+
+ toggleTheme(): void {
+ this.isDarkMode.update(current => !current);
+ this.handleAction(`Theme switched to ${this.isDarkMode() ? 'dark' : 'light'} mode`);
+ }
+
+ handleImageError(event: Event): void {
+ const target = event.target as HTMLImageElement;
+ target.style.display = 'none';
+ const fallback = document.createElement('div');
+ fallback.style.width = '32px';
+ fallback.style.height = '32px';
+ fallback.style.borderRadius = '50%';
+ fallback.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
+ fallback.style.display = 'flex';
+ fallback.style.alignItems = 'center';
+ fallback.style.justifyContent = 'center';
+ fallback.style.color = 'white';
+ fallback.style.fontWeight = '500';
+ fallback.style.fontSize = '12px';
+ fallback.textContent = 'U';
+ target.parentNode?.insertBefore(fallback, target);
+ }
+
+ updateDemoConfig(key: string, event: Event): void {
+ const target = event.target as HTMLInputElement;
+ const value = target.checked;
+
+ this.demoConfig.update(config => ({
+ ...config,
+ [key]: value
+ }));
+ }
+
+ updateDemoSlot(slotName: string, event: Event): void {
+ const target = event.target as HTMLInputElement;
+ const checked = target.checked;
+
+ this.demoConfig.update(config => ({
+ ...config,
+ slots: {
+ ...config.slots,
+ [slotName]: checked
+ }
+ }));
+ }
+
+ updateDemoVariant(event: Event): void {
+ const target = event.target as HTMLSelectElement;
+ const variant = target.value as any;
+
+ this.demoConfig.update(config => ({
+ ...config,
+ variant,
+ title: variant.charAt(0).toUpperCase() + variant.slice(1) + ' Demo'
+ }));
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/autocomplete-demo/autocomplete-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/autocomplete-demo/autocomplete-demo.component.scss
new file mode 100644
index 0000000..a8d6250
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/autocomplete-demo/autocomplete-demo.component.scss
@@ -0,0 +1,184 @@
+@use '../../../../../shared-ui/src/styles/semantic/index' as *;
+
+.demo-container {
+ padding: 32px;
+ max-width: 1200px;
+ margin: 0 auto;
+
+ h2 {
+ color: $semantic-color-text-primary;
+ font-size: 32px;
+ margin-bottom: 48px;
+ border-bottom: $semantic-border-width-1 solid $semantic-color-border-primary;
+ padding-bottom: 16px;
+ }
+
+ h3 {
+ color: $semantic-color-text-primary;
+ font-size: 24px;
+ margin-bottom: 32px;
+ }
+
+ h4 {
+ color: $semantic-color-text-primary;
+ font-size: 20px;
+ margin-bottom: 16px;
+ }
+}
+
+.demo-section {
+ margin-bottom: 64px;
+ padding: 32px;
+ background: $semantic-color-surface-primary;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-lg;
+ box-shadow: $semantic-shadow-elevation-1;
+}
+
+.demo-row {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 32px;
+ align-items: start;
+}
+
+.demo-item {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+
+ > label {
+ font-weight: 500;
+ color: $semantic-color-text-secondary;
+ font-size: 14px;
+ }
+}
+
+.demo-form {
+ .form-values {
+ margin-top: 32px;
+ padding: 16px;
+ background: $semantic-color-surface-secondary;
+ border-radius: $semantic-border-radius-md;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+
+ h4 {
+ margin-bottom: 12px;
+ }
+
+ pre {
+ background: $semantic-color-background;
+ padding: 12px;
+ border-radius: $semantic-border-radius-sm;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ font-size: 12px;
+ color: $semantic-color-text-primary;
+ overflow-x: auto;
+ margin-bottom: 12px;
+ }
+
+ p {
+ margin: 0;
+ color: $semantic-color-text-secondary;
+ font-size: 14px;
+ }
+ }
+}
+
+.event-log {
+ margin-top: 32px;
+
+ .log-container {
+ max-height: 300px;
+ overflow-y: auto;
+ background: $semantic-color-background;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-md;
+ margin-bottom: 16px;
+ }
+
+ .log-entry {
+ display: grid;
+ grid-template-columns: 120px 1fr auto;
+ gap: 12px;
+ padding: 8px 12px;
+ border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ font-size: 12px;
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .log-event {
+ font-weight: 500;
+ color: $semantic-color-brand-primary;
+ }
+
+ .log-data {
+ color: $semantic-color-text-primary;
+ overflow-wrap: break-word;
+ }
+
+ .log-time {
+ color: $semantic-color-text-tertiary;
+ font-size: 11px;
+ }
+ }
+
+ .clear-log-btn {
+ padding: 8px 12px;
+ background: $semantic-color-surface-secondary;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-sm;
+ color: $semantic-color-text-secondary;
+ font-size: 12px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background: $semantic-color-surface-elevated;
+ border-color: $semantic-color-border-focus;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-brand-primary;
+ outline-offset: 2px;
+ }
+ }
+}
+
+// Responsive design
+@media (max-width: 1024px - 1) {
+ .demo-row {
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 24px;
+ }
+}
+
+@media (max-width: 768px - 1) {
+ .demo-container {
+ padding: 24px;
+ }
+
+ .demo-section {
+ padding: 24px;
+ }
+
+ .demo-row {
+ grid-template-columns: 1fr;
+ gap: 24px;
+ }
+
+ .event-log {
+ .log-entry {
+ grid-template-columns: 1fr;
+ gap: 8px;
+
+ .log-time {
+ text-align: right;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/autocomplete-demo/autocomplete-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/autocomplete-demo/autocomplete-demo.component.ts
new file mode 100644
index 0000000..8ded9d3
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/autocomplete-demo/autocomplete-demo.component.ts
@@ -0,0 +1,395 @@
+import { Component, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ReactiveFormsModule, FormControl, FormGroup } from '@angular/forms';
+import { AutocompleteComponent, AutocompleteOption } from '../../../../../ui-essentials/src/lib/components/forms';
+
+@Component({
+ selector: 'ui-autocomplete-demo',
+ standalone: true,
+ imports: [CommonModule, ReactiveFormsModule, AutocompleteComponent],
+ template: `
+
+
Autocomplete Demo
+
+
+
+ Sizes
+
+
+ Small
+
+
+
+ Medium (default)
+
+
+
+ Large
+
+
+
+
+
+
+
+ Variants
+
+
+ Outlined (default)
+
+
+
+ Filled
+
+
+
+
+
+
+
+ With Labels & Helper Text
+
+
+
+
+
+ States
+
+
+ Disabled
+
+
+
+ Read-only
+
+
+
+ Loading
+
+
+
+
+
+
+
+
+
+
+ Advanced Features
+
+
+ With Secondary Text
+
+
+
+ Custom Filter (starts with)
+
+
+
+ Limited Options (max 3)
+
+
+
+
+
+
+
+ Behavior Options
+
+
+ Open on Focus
+
+
+
+ Not Clearable
+
+
+
+ Min Query Length (3)
+
+
+
+
+
+
+
+ Reactive Forms Integration
+
+
+
+
+
+ Event Monitoring
+
+
+
+
Event Log:
+
+ @for (event of eventLog(); track event.id) {
+
+ {{ event.type }}
+ {{ event.data }}
+ {{ event.time }}
+
+ }
+
+
+ Clear Log
+
+
+
+
+ `,
+ styleUrl: './autocomplete-demo.component.scss'
+})
+export class AutocompleteDemoComponent {
+ // Form for reactive forms demo
+ demoForm = new FormGroup({
+ favoriteCountry: new FormControl(''),
+ preferredLanguage: new FormControl('')
+ });
+
+ // Event logging
+ private _eventLog = signal>([]);
+ eventLog = this._eventLog.asReadonly();
+ private eventCounter = 0;
+
+ // Sample data
+ countries: AutocompleteOption[] = [
+ { value: 'us', label: 'United States' },
+ { value: 'ca', label: 'Canada' },
+ { value: 'gb', label: 'United Kingdom' },
+ { value: 'de', label: 'Germany' },
+ { value: 'fr', label: 'France' },
+ { value: 'it', label: 'Italy' },
+ { value: 'es', label: 'Spain' },
+ { value: 'jp', label: 'Japan' },
+ { value: 'kr', label: 'South Korea' },
+ { value: 'cn', label: 'China' },
+ { value: 'in', label: 'India' },
+ { value: 'au', label: 'Australia' },
+ { value: 'br', label: 'Brazil' },
+ { value: 'mx', label: 'Mexico' },
+ { value: 'ru', label: 'Russia' },
+ { value: 'za', label: 'South Africa' },
+ { value: 'ng', label: 'Nigeria' },
+ { value: 'eg', label: 'Egypt' }
+ ];
+
+ programmingLanguages: AutocompleteOption[] = [
+ { value: 'typescript', label: 'TypeScript' },
+ { value: 'javascript', label: 'JavaScript' },
+ { value: 'python', label: 'Python' },
+ { value: 'java', label: 'Java' },
+ { value: 'csharp', label: 'C#' },
+ { value: 'cpp', label: 'C++' },
+ { value: 'go', label: 'Go' },
+ { value: 'rust', label: 'Rust' },
+ { value: 'swift', label: 'Swift' },
+ { value: 'kotlin', label: 'Kotlin' },
+ { value: 'php', label: 'PHP' },
+ { value: 'ruby', label: 'Ruby' },
+ { value: 'scala', label: 'Scala' },
+ { value: 'dart', label: 'Dart' }
+ ];
+
+ users: AutocompleteOption[] = [
+ { value: '1', label: 'John Doe', secondaryText: 'john.doe@example.com' },
+ { value: '2', label: 'Jane Smith', secondaryText: 'jane.smith@example.com' },
+ { value: '3', label: 'Bob Johnson', secondaryText: 'bob.johnson@example.com' },
+ { value: '4', label: 'Alice Brown', secondaryText: 'alice.brown@example.com' },
+ { value: '5', label: 'Charlie Wilson', secondaryText: 'charlie.wilson@example.com' },
+ { value: '6', label: 'Diana Davis', secondaryText: 'diana.davis@example.com' },
+ { value: '7', label: 'Eve Miller', secondaryText: 'eve.miller@example.com' },
+ { value: '8', label: 'Frank Garcia', secondaryText: 'frank.garcia@example.com' }
+ ];
+
+ fruits: AutocompleteOption[] = [
+ { value: 'apple', label: 'Apple' },
+ { value: 'banana', label: 'Banana' },
+ { value: 'orange', label: 'Orange' },
+ { value: 'grape', label: 'Grape' },
+ { value: 'strawberry', label: 'Strawberry' },
+ { value: 'blueberry', label: 'Blueberry' },
+ { value: 'pineapple', label: 'Pineapple' },
+ { value: 'mango', label: 'Mango' },
+ { value: 'kiwi', label: 'Kiwi' },
+ { value: 'peach', label: 'Peach' },
+ { value: 'cherry', label: 'Cherry' },
+ { value: 'watermelon', label: 'Watermelon' }
+ ];
+
+ // Custom filter function
+ startsWithFilter = (option: AutocompleteOption, query: string): boolean => {
+ return option.label.toLowerCase().startsWith(query.toLowerCase());
+ };
+
+ onValueChange(source: string, value: any): void {
+ console.log(`Value changed (${source}):`, value);
+ }
+
+ onOptionSelected(option: AutocompleteOption): void {
+ console.log('Option selected:', option);
+ }
+
+ logEvent(type: string, data: any): void {
+ const entry = {
+ id: ++this.eventCounter,
+ type,
+ data: typeof data === 'object' ? JSON.stringify(data) : String(data),
+ time: new Date().toLocaleTimeString()
+ };
+
+ const currentLog = this._eventLog();
+ this._eventLog.set([entry, ...currentLog.slice(0, 19)]); // Keep last 20 events
+ }
+
+ clearEventLog(): void {
+ this._eventLog.set([]);
+ this.eventCounter = 0;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/avatar-demo/avatar-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/avatar-demo/avatar-demo.component.ts
new file mode 100644
index 0000000..8ea8501
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/avatar-demo/avatar-demo.component.ts
@@ -0,0 +1,502 @@
+import { Component, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { AvatarComponent } from '../../../../../ui-essentials/src/lib/components/data-display/avatar';
+
+interface Activity {
+ user: string;
+ action: string;
+ time: string;
+ status?: 'online' | 'offline' | 'away' | 'busy';
+}
+
+@Component({
+ selector: 'ui-avatar-demo',
+ standalone: true,
+ imports: [
+ CommonModule,
+ AvatarComponent
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
Avatar Component Showcase
+
+
+
+
+
+
+ With Images
+
+
+
+
+
+
+
+
Fallback (Broken URL)
+
+
+
+
+
+
+
+
+
+ Notification Badges
+
+
+
+
+
+
+
+
+
+
+
+ Activity List Example
+
+
Recent Activity
+ @for (activity of activities; track activity.user) {
+
+
+
+
+
{{ activity.user }}
+
{{ activity.action }}
+
+
{{ activity.time }}
+
+ }
+
+
+
+
+
+ User List Example
+
+
+ @for (user of userProfiles; track user.id) {
+
+
0 ? user.notifications.toString() : ''">
+
+
+
{{ user.name }}
+
{{ user.role }}
+
{{ user.email }}
+
+
+
Last seen
+
{{ user.lastSeen }}
+
+
+ }
+
+
+
+
+
+
+
+
+
+ Avatar Group Example
+
+ @for (member of teamMembers; track member.name; let i = $index) {
+
0 ? '-8px' : '0'">
+
+ }
+
+ +{{ teamMembers.length }} members
+
+
+
+
+
+
+ Usage Examples
+
+
Basic Avatar:
+
<ui-avatar
+ size="md"
+ name="Current User"
+ status="online">
+</ui-avatar>
+
+
Avatar with Image:
+
<ui-avatar
+ size="lg"
+ name="John Doe"
+ imageUrl="https://example.com/avatar.jpg"
+ altText="John's profile picture"
+ status="away"
+ badge="5">
+</ui-avatar>
+
+
Custom Styled Avatar:
+
<ui-avatar
+ size="xl"
+ name="Admin User"
+ color="primary"
+ shape="rounded"
+ badge="⭐"
+ [loading]="false">
+</ui-avatar>
+
+
+
+
+
+ Interactive Example
+
+
+ Toggle Status
+
+
+ Toggle Badge
+
+
+ Cycle Size
+
+
+
+
+
+
+
+
Size: {{ interactiveSize }}
+
Status: {{ interactiveStatus || 'none' }}
+
Badge: {{ interactiveBadge || 'none' }}
+
+
+
+
+ `,
+ styles: [`
+ h2 {
+ color: hsl(279, 14%, 11%);
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ border-bottom: 2px solid hsl(258, 100%, 47%);
+ padding-bottom: 0.5rem;
+ }
+
+ h3 {
+ color: hsl(279, 14%, 25%);
+ font-size: 1.5rem;
+ margin-bottom: 1rem;
+ }
+
+ h4 {
+ color: hsl(287, 12%, 35%);
+ font-size: 1.125rem;
+ margin-bottom: 0.75rem;
+ }
+
+ section {
+ border: 1px solid hsl(289, 14%, 90%);
+ border-radius: 8px;
+ padding: 1.5rem;
+ background: hsl(286, 20%, 99%);
+ }
+
+ pre {
+ font-size: 0.875rem;
+ line-height: 1.5;
+ margin: 0.5rem 0;
+ }
+
+ code {
+ font-family: 'JetBrains Mono', monospace;
+ color: #d63384;
+ }
+ `]
+})
+export class AvatarDemoComponent {
+ activities: Activity[] = [
+ {
+ user: 'Alice Johnson',
+ action: 'Updated the project documentation and added new API endpoints',
+ time: '2 min ago',
+ status: 'online'
+ },
+ {
+ user: 'Bob Smith',
+ action: 'Completed code review for the authentication module',
+ time: '15 min ago',
+ status: 'away'
+ },
+ {
+ user: 'Carol Williams',
+ action: 'Created new user interface mockups for the dashboard',
+ time: '1 hour ago',
+ status: 'online'
+ },
+ {
+ user: 'David Brown',
+ action: 'Fixed critical bug in the payment processing system',
+ time: '2 hours ago',
+ status: 'busy'
+ },
+ {
+ user: 'Eve Davis',
+ action: 'Deployed version 2.1.0 to production environment',
+ time: '3 hours ago',
+ status: 'offline'
+ }
+ ];
+
+ userProfiles = [
+ {
+ id: 1,
+ name: 'Alice Johnson',
+ role: 'Senior Developer',
+ email: 'alice@company.com',
+ status: 'online' as const,
+ notifications: 3,
+ lastSeen: '2 min ago',
+ avatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=150&h=150&fit=crop&crop=face'
+ },
+ {
+ id: 2,
+ name: 'Bob Smith',
+ role: 'Product Manager',
+ email: 'bob@company.com',
+ status: 'away' as const,
+ notifications: 0,
+ lastSeen: '15 min ago',
+ avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face'
+ },
+ {
+ id: 3,
+ name: 'Carol Williams',
+ role: 'UX Designer',
+ email: 'carol@company.com',
+ status: 'online' as const,
+ notifications: 12,
+ lastSeen: 'Just now',
+ avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face'
+ }
+ ];
+
+ teamMembers = [
+ { name: 'Alex Chen', status: 'online' as const, avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face' },
+ { name: 'Maria Garcia', status: 'away' as const, avatar: 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=150&h=150&fit=crop&crop=face' },
+ { name: 'James Wilson', status: 'busy' as const, avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=150&h=150&fit=crop&crop=face' },
+ { name: 'Sarah Kim', status: 'online' as const, avatar: 'https://images.unsplash.com/photo-1534751516642-a1af1ef26a56?w=150&h=150&fit=crop&crop=face' },
+ ];
+
+ // Interactive demo properties
+ interactiveSize: any = 'lg';
+ interactiveStatus: any = 'online';
+ interactiveBadge = '3';
+
+ private statusCycle: any[] = ['online', 'away', 'busy', 'offline', null];
+ private statusIndex = 0;
+
+ private sizeCycle: any[] = ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'];
+ private sizeIndex = 3; // Start with 'lg'
+
+ cycleStatus(): void {
+ this.statusIndex = (this.statusIndex + 1) % this.statusCycle.length;
+ this.interactiveStatus = this.statusCycle[this.statusIndex];
+ }
+
+ toggleBadge(): void {
+ if (this.interactiveBadge) {
+ this.interactiveBadge = '';
+ } else {
+ this.interactiveBadge = Math.floor(Math.random() * 99 + 1).toString();
+ }
+ }
+
+ cycleSize(): void {
+ this.sizeIndex = (this.sizeIndex + 1) % this.sizeCycle.length;
+ this.interactiveSize = this.sizeCycle[this.sizeIndex];
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/backdrop-demo/backdrop-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/backdrop-demo/backdrop-demo.component.scss
new file mode 100644
index 0000000..f9bb4e7
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/backdrop-demo/backdrop-demo.component.scss
@@ -0,0 +1,204 @@
+@use '../../../../../shared-ui/src/styles/semantic/index' as tokens;
+
+.demo-container {
+ padding: tokens.$semantic-spacing-layout-md;
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.demo-section {
+ margin-bottom: tokens.$semantic-spacing-layout-lg;
+ padding: tokens.$semantic-spacing-component-md;
+ background: tokens.$semantic-color-surface-primary;
+ border: tokens.$semantic-border-width-1 solid tokens.$semantic-color-border-primary;
+ border-radius: tokens.$semantic-border-radius-md;
+ box-shadow: tokens.$semantic-shadow-elevation-1;
+
+ h3 {
+ margin: 0 0 tokens.$semantic-spacing-content-paragraph 0;
+ font-size: tokens.$semantic-typography-heading-h3-size;
+ color: tokens.$semantic-color-text-primary;
+ }
+}
+
+.demo-row {
+ display: flex;
+ gap: tokens.$semantic-spacing-component-sm;
+ flex-wrap: wrap;
+ margin-bottom: tokens.$semantic-spacing-content-paragraph;
+}
+
+.demo-controls {
+ display: flex;
+ align-items: center;
+ gap: tokens.$semantic-spacing-component-md;
+ margin-bottom: tokens.$semantic-spacing-content-paragraph;
+ flex-wrap: wrap;
+
+ label {
+ font-size: tokens.$semantic-typography-font-size-md;
+ color: tokens.$semantic-color-text-primary;
+ font-weight: tokens.$semantic-typography-font-weight-medium;
+ min-width: 100px;
+ }
+
+ input[type="range"] {
+ flex: 1;
+ min-width: 200px;
+ max-width: 300px;
+ margin: 0 tokens.$semantic-spacing-component-sm;
+ }
+}
+
+.demo-button {
+ padding: tokens.$semantic-spacing-component-sm tokens.$semantic-spacing-component-md;
+ background: tokens.$semantic-color-primary;
+ color: tokens.$semantic-color-on-primary;
+ border: none;
+ border-radius: tokens.$semantic-border-radius-sm;
+ font-size: tokens.$semantic-typography-font-size-md;
+ cursor: pointer;
+ transition: all tokens.$semantic-motion-duration-fast tokens.$semantic-motion-easing-ease;
+
+ &:hover {
+ background: tokens.$semantic-color-primary-hover;
+ box-shadow: tokens.$semantic-shadow-elevation-2;
+ }
+
+ &:active {
+ transform: translateY(1px);
+ box-shadow: tokens.$semantic-shadow-elevation-1;
+ }
+
+ &--secondary {
+ background: tokens.$semantic-color-surface-secondary;
+ color: tokens.$semantic-color-text-primary;
+ border: tokens.$semantic-border-width-1 solid tokens.$semantic-color-border-primary;
+
+ &:hover {
+ background: tokens.$semantic-color-surface-secondary;
+ }
+ }
+}
+
+// Backdrop content styles
+.backdrop-content {
+ background: tokens.$semantic-color-surface-primary;
+ padding: tokens.$semantic-spacing-component-lg;
+ border-radius: tokens.$semantic-border-radius-md;
+ box-shadow: tokens.$semantic-shadow-elevation-4;
+ text-align: center;
+ max-width: 400px;
+ margin: 0 auto;
+
+ h4 {
+ margin: 0 0 tokens.$semantic-spacing-content-paragraph 0;
+ font-size: tokens.$semantic-typography-heading-h4-size;
+ color: tokens.$semantic-color-text-primary;
+ }
+
+ p {
+ margin: 0 0 tokens.$semantic-spacing-content-paragraph 0;
+ font-size: tokens.$semantic-typography-font-size-md;
+ color: tokens.$semantic-color-text-secondary;
+ }
+
+ &--glass {
+ background: rgba(255, 255, 255, 0.9);
+ backdrop-filter: blur(10px);
+ -webkit-backdrop-filter: blur(10px);
+ border: tokens.$semantic-border-width-1 solid rgba(255, 255, 255, 0.2);
+ }
+
+ &--large {
+ max-width: 600px;
+ padding: tokens.$semantic-spacing-component-xl;
+ }
+
+ &--small {
+ max-width: 300px;
+ padding: tokens.$semantic-spacing-component-md;
+ }
+}
+
+// Event log styles
+.demo-log {
+ background: tokens.$semantic-color-surface-secondary;
+ border: tokens.$semantic-border-width-1 solid tokens.$semantic-color-border-subtle;
+ border-radius: tokens.$semantic-border-radius-sm;
+ padding: tokens.$semantic-spacing-component-md;
+ max-height: 200px;
+ overflow-y: auto;
+ margin-bottom: tokens.$semantic-spacing-content-paragraph;
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+}
+
+.log-entry {
+ font-size: tokens.$semantic-typography-font-size-sm;
+ color: tokens.$semantic-color-text-primary;
+ padding: tokens.$semantic-spacing-component-xs 0;
+ border-bottom: tokens.$semantic-border-width-1 solid tokens.$semantic-color-border-subtle;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &--empty {
+ color: tokens.$semantic-color-text-tertiary;
+ font-style: italic;
+ text-align: center;
+ border-bottom: none;
+ }
+}
+
+// Dark mode support
+:host-context(.dark-theme) {
+ .backdrop-content {
+ &--glass {
+ background: rgba(0, 0, 0, 0.8);
+ border: tokens.$semantic-border-width-1 solid rgba(255, 255, 255, 0.1);
+ }
+ }
+}
+
+// Responsive design
+@media (max-width: #{tokens.$semantic-breakpoint-md - 1px}) {
+ .demo-container {
+ padding: tokens.$semantic-spacing-layout-sm;
+ }
+
+ .demo-section {
+ padding: tokens.$semantic-spacing-component-sm;
+ }
+
+ .backdrop-content {
+ margin: tokens.$semantic-spacing-layout-sm;
+ padding: tokens.$semantic-spacing-component-md;
+
+ &--large,
+ &--small {
+ max-width: none;
+ padding: tokens.$semantic-spacing-component-md;
+ }
+ }
+
+ .demo-controls {
+ flex-direction: column;
+ align-items: stretch;
+
+ input[type="range"] {
+ min-width: auto;
+ }
+ }
+}
+
+@media (max-width: #{tokens.$semantic-breakpoint-sm - 1px}) {
+ .demo-row {
+ flex-direction: column;
+ }
+
+ .demo-button {
+ width: 100%;
+ text-align: center;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/backdrop-demo/backdrop-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/backdrop-demo/backdrop-demo.component.ts
new file mode 100644
index 0000000..5404e45
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/backdrop-demo/backdrop-demo.component.ts
@@ -0,0 +1,310 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { BackdropComponent } from '../../../../../ui-essentials/src/lib/components/overlays';
+
+@Component({
+ selector: 'ui-backdrop-demo',
+ standalone: true,
+ imports: [CommonModule, BackdropComponent],
+ template: `
+
+
Backdrop Demo
+
+
+
+ Basic Backdrop
+
+
+ Toggle Basic Backdrop
+
+
+
+
+
+
+
+
+ Variants
+
+ @for (variant of variants; track variant) {
+
+ {{ variant }} Backdrop
+
+ }
+
+
+ @if (currentVariant) {
+
+
+
{{ currentVariant | titlecase }} Backdrop
+
Click backdrop to close
+
+
+ }
+
+
+
+
+ Blur Effect
+
+
+ Toggle Blur Backdrop
+
+
+
+
+
+
Blur Backdrop
+
Notice the blur effect behind this content
+
Close
+
+
+
+
+
+
+ Opacity Control
+
+ Opacity: {{ currentOpacity }}
+
+
+ Show Backdrop
+
+
+
+
+
+
Opacity: {{ currentOpacity }}
+
Adjust the opacity using the slider above
+
+
+
+
+
+
+ Z-Index & Stacking
+
+
+ Show Stacked Backdrops
+
+
+
+
+
+
+
First Backdrop (z-index: 1000)
+
This is behind the second backdrop
+
+
+
+
+
+
+
Second Backdrop (z-index: 1010)
+
This is in front of the first backdrop
+
Close Both
+
+
+
+
+
+
+ Non-clickable Backdrop
+
+
+ Show Non-clickable
+
+
+
+
+
+
Non-clickable Backdrop
+
This backdrop cannot be clicked to close
+
Close
+
+
+
+
+
+
+ Event Log
+
+ @for (event of eventLog; track $index) {
+
{{ event }}
+ }
+ @empty {
+
No events yet. Interact with backdrops above.
+ }
+
+ Clear Log
+
+
+ `,
+ styleUrl: './backdrop-demo.component.scss'
+})
+export class BackdropDemoComponent {
+ // Basic backdrop
+ showBasicBackdrop = false;
+
+ // Variant backdrop
+ variants = ['default', 'blur', 'dark', 'light'] as const;
+ currentVariant: typeof this.variants[number] | null = null;
+ showVariant = false;
+
+ // Blur backdrop
+ showBlurBackdrop = false;
+
+ // Opacity backdrop
+ showOpacityBackdrop = false;
+ currentOpacity = 0.5;
+
+ // Stacked backdrops
+ showFirstBackdrop = false;
+ showSecondBackdrop = false;
+
+ // Non-clickable backdrop
+ showNonClickableBackdrop = false;
+
+ // Event log
+ eventLog: string[] = [];
+
+ toggleBasicBackdrop(): void {
+ this.showBasicBackdrop = !this.showBasicBackdrop;
+ this.logEvent(`Basic backdrop ${this.showBasicBackdrop ? 'opened' : 'closed'}`);
+ }
+
+ showVariantBackdrop(variant: typeof this.variants[number]): void {
+ this.currentVariant = variant;
+ this.showVariant = true;
+ this.logEvent(`${variant} backdrop opened`);
+ }
+
+ handleVariantVisibilityChange(visible: boolean): void {
+ this.showVariant = visible;
+ if (!visible && this.currentVariant) {
+ this.logEvent(`${this.currentVariant} backdrop closed`);
+ this.currentVariant = null;
+ }
+ }
+
+ toggleBlurBackdrop(): void {
+ this.showBlurBackdrop = !this.showBlurBackdrop;
+ this.logEvent(`Blur backdrop ${this.showBlurBackdrop ? 'opened' : 'closed'}`);
+ }
+
+ updateOpacity(event: Event): void {
+ const target = event.target as HTMLInputElement;
+ this.currentOpacity = parseFloat(target.value);
+ }
+
+ toggleOpacityBackdrop(): void {
+ this.showOpacityBackdrop = !this.showOpacityBackdrop;
+ this.logEvent(`Opacity backdrop ${this.showOpacityBackdrop ? 'opened' : 'closed'} with opacity ${this.currentOpacity}`);
+ }
+
+ showStackedBackdrops(): void {
+ this.showFirstBackdrop = true;
+ this.showSecondBackdrop = true;
+ this.logEvent('Stacked backdrops opened');
+ }
+
+ hideStackedBackdrops(): void {
+ this.showFirstBackdrop = false;
+ this.showSecondBackdrop = false;
+ this.logEvent('Stacked backdrops closed');
+ }
+
+ handleStackedBackdropChange(visible: boolean): void {
+ if (!visible) {
+ this.hideStackedBackdrops();
+ }
+ }
+
+ toggleNonClickableBackdrop(): void {
+ this.showNonClickableBackdrop = !this.showNonClickableBackdrop;
+ this.logEvent(`Non-clickable backdrop ${this.showNonClickableBackdrop ? 'opened' : 'closed'}`);
+ }
+
+ handleBackdropClick(message: string): void {
+ this.logEvent(message);
+ }
+
+ private logEvent(message: string): void {
+ const timestamp = new Date().toLocaleTimeString();
+ this.eventLog.unshift(`[${timestamp}] ${message}`);
+
+ // Keep only last 10 events
+ if (this.eventLog.length > 10) {
+ this.eventLog = this.eventLog.slice(0, 10);
+ }
+ }
+
+ clearEventLog(): void {
+ this.eventLog = [];
+ this.logEvent('Event log cleared');
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/badge-demo/badge-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/badge-demo/badge-demo.component.ts
new file mode 100644
index 0000000..3a52e40
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/badge-demo/badge-demo.component.ts
@@ -0,0 +1,155 @@
+import { Component, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { BadgeComponent } from '../../../../../ui-essentials/src/lib/components/data-display/badge';
+
+@Component({
+ selector: 'ui-badge-demo',
+ standalone: true,
+ imports: [
+ CommonModule,
+ BadgeComponent
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
Badge Component Showcase
+
+
+
+
+
+
+ Color Variants
+
+ Default
+ Primary
+ Secondary
+ Success
+ Warning
+ Danger
+ Info
+
+
+
+
+
+
+
+
+ Dot Badges
+
+
+
+
+
+
+
+
+
+
+
+ Usage Examples
+
+
Basic Badge:
+
<ui-badge variant="success" size="md">
+ Active
+</ui-badge>
+
+
Dot Badge:
+
<ui-badge
+ variant="warning"
+ [isDot]="true"
+ ariaLabel="Warning status">
+</ui-badge>
+
+
Custom Shape:
+
<ui-badge
+ variant="danger"
+ shape="square"
+ size="lg"
+ title="Critical status">
+ Critical
+</ui-badge>
+
+
+
+ `,
+ styles: [`
+ h2 {
+ color: hsl(279, 14%, 11%);
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ border-bottom: 2px solid hsl(258, 100%, 47%);
+ padding-bottom: 0.5rem;
+ }
+
+ h3 {
+ color: hsl(279, 14%, 25%);
+ font-size: 1.5rem;
+ margin-bottom: 1rem;
+ }
+
+ h4 {
+ color: hsl(287, 12%, 35%);
+ font-size: 1.125rem;
+ margin-bottom: 0.75rem;
+ }
+
+ section {
+ border: 1px solid hsl(289, 14%, 90%);
+ border-radius: 8px;
+ padding: 1.5rem;
+ background: hsl(286, 20%, 99%);
+ }
+
+ pre {
+ font-size: 0.875rem;
+ line-height: 1.5;
+ margin: 0.5rem 0;
+ }
+
+ code {
+ font-family: 'JetBrains Mono', monospace;
+ color: #d63384;
+ }
+ `]
+})
+export class BadgeDemoComponent {
+ // Simple demo with no interactive functionality for now
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/button-demo/button-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/button-demo/button-demo.component.ts
new file mode 100644
index 0000000..f9c7eb6
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/button-demo/button-demo.component.ts
@@ -0,0 +1,399 @@
+import { Component, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons';
+import { TextButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons';
+import { GhostButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons';
+import { FabComponent } from '../../../../../ui-essentials/src/lib/components/buttons';
+import { SimpleButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons';
+import {
+ faDownload,
+ faPlus,
+ faShare,
+ faHeart,
+ faBookmark,
+ faEdit,
+ faTrash,
+ faSave,
+ faArrowRight,
+ faArrowLeft,
+ faSearch,
+ faUpload
+} from '@fortawesome/free-solid-svg-icons';
+
+@Component({
+ selector: 'ui-button-demo',
+ standalone: true,
+ imports: [
+ CommonModule,
+ ButtonComponent,
+ TextButtonComponent,
+ GhostButtonComponent,
+ FabComponent,
+ SimpleButtonComponent
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
Button Component Showcase
+
+
+
+ Filled Buttons
+
+ Small
+ Medium
+ Large
+
+
+ Disabled
+ Loading
+ Full Width
+
+
+
+
+
+ Tonal Buttons
+
+ Small
+ Medium
+ Large
+
+
+ Disabled
+ Loading
+
+
+
+
+
+ Outlined Buttons
+
+ Small
+ Medium
+ Large
+
+
+ Disabled
+ Loading
+
+
+
+
+
+ Text Buttons
+
+ Small
+ Medium
+ Large
+
+
+ Disabled
+ Loading
+
+
+
+
+
+ Ghost Buttons
+
+ Small
+ Medium
+ Large
+
+
+ Disabled
+ Loading
+ Full Width
+
+
+
+
+
+ Floating Action Buttons (FAB)
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Buttons with Icons
+
+
+
+
Icons on Left
+
+ Download
+ Add New
+ Upload
+
+
+ Edit
+ Share
+ Save
+
+
+ Bookmark
+ Favorite
+ Search
+
+
+
+
+
+
Icons on Right
+
+ Next
+ Continue
+ Proceed
+
+
+ Back
+ Delete
+
+
+
+
+
+
Different Sizes with Icons
+
+ Small
+ Medium
+ Large
+
+
+
+
+
+
Icon with Minimal Text
+
+ DL
+ Edit
+ Del
+
+
+
+
+
+
+ Usage Examples
+
+
Basic Button:
+
<ui-button
+ variant="filled"
+ size="medium"
+ (clicked)="handleClick($event)">
+ Click Me
+</ui-button>
+
+
Text Button:
+
<ui-text-button
+ size="large"
+ [disabled]="false"
+ (clicked)="handleTextClick($event)">
+ Text Action
+</ui-text-button>
+
+
Button with Icon:
+
<ui-button
+ variant="filled"
+ [icon]="faDownload"
+ iconPosition="left"
+ (clicked)="downloadFile()">
+ Download
+</ui-button>
+
+
FAB with Position:
+
<ui-fab
+ variant="primary"
+ size="medium"
+ position="bottom-right"
+ [pulse]="true"
+ (clicked)="openDialog()">
+ +
+</ui-fab>
+
+
+
+
+
+ UI Essentials Library Components
+
+ Primary
+ Secondary
+ Disabled
+
+
+
+
+
+ Interactive Actions
+
+ Show Alert
+ {{ isLoading ? 'Stop Loading' : 'Start Loading' }}
+ Reset Demo
+
+
+ @if (lastClickedButton) {
+
+ Last clicked: {{ lastClickedButton }}
+
+ }
+
+
+
+
+ 💡
+
+
+ `,
+ styles: [`
+ h2 {
+ color: hsl(279, 14%, 11%);
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ border-bottom: 2px solid hsl(258, 100%, 47%);
+ padding-bottom: 0.5rem;
+ }
+
+ h3 {
+ color: hsl(279, 14%, 25%);
+ font-size: 1.5rem;
+ margin-bottom: 1rem;
+ }
+
+ h4 {
+ color: hsl(287, 12%, 35%);
+ font-size: 1.125rem;
+ margin-bottom: 0.75rem;
+ }
+
+ section {
+ border: 1px solid hsl(289, 14%, 90%);
+ border-radius: 8px;
+ padding: 1.5rem;
+ background: hsl(286, 20%, 99%);
+ }
+
+ pre {
+ font-size: 0.875rem;
+ line-height: 1.5;
+ margin: 0.5rem 0;
+ }
+
+ code {
+ font-family: 'JetBrains Mono', monospace;
+ color: #d63384;
+ }
+ `]
+})
+export class ButtonDemoComponent {
+ lastClickedButton: string = '';
+ isLoading: boolean = false;
+ shouldPulse: boolean = true;
+
+ // Font Awesome icons
+ faDownload = faDownload;
+ faPlus = faPlus;
+ faShare = faShare;
+ faHeart = faHeart;
+ faBookmark = faBookmark;
+ faEdit = faEdit;
+ faTrash = faTrash;
+ faSave = faSave;
+ faArrowRight = faArrowRight;
+ faArrowLeft = faArrowLeft;
+ faSearch = faSearch;
+ faUpload = faUpload;
+
+ handleClick(buttonType: string): void {
+ this.lastClickedButton = buttonType;
+ console.log(`Clicked: ${buttonType}`);
+ }
+
+ showAlert(message: string): void {
+ alert(message);
+ this.handleClick('Alert button');
+ }
+
+ toggleLoading(): void {
+ this.isLoading = !this.isLoading;
+ this.handleClick(`Loading toggle - ${this.isLoading ? 'started' : 'stopped'}`);
+ }
+
+ resetDemo(): void {
+ this.lastClickedButton = '';
+ this.isLoading = false;
+ this.shouldPulse = true;
+ console.log('Demo reset');
+ }
+
+ handleFabClick(): void {
+ this.shouldPulse = !this.shouldPulse;
+ this.handleClick(`FAB - Pulse ${this.shouldPulse ? 'enabled' : 'disabled'}`);
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/card-demo/card-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/card-demo/card-demo.component.ts
new file mode 100644
index 0000000..6d0d7e8
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/card-demo/card-demo.component.ts
@@ -0,0 +1,539 @@
+import { Component, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { CardComponent, GlassVariant } from '../../../../../ui-essentials/src/lib/components/data-display/card';
+import { ButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons';
+
+@Component({
+ selector: 'ui-card-demo',
+ standalone: true,
+ imports: [
+ CommonModule,
+ CardComponent,
+ ButtonComponent
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
Card Component Showcase
+
+
+
+ Card Variants
+
+
+
+
Elevated Card
+
Default elevated card with shadow
+
+ This is the most common card variant with a subtle elevation that lifts it above the surface.
+
+ Action
+
+
+
+
+
+
Filled Card
+
Card with background color
+
+ Filled cards use background color to separate content from the surrounding surface.
+
+ Action
+
+
+
+
+
+
Outlined Card
+
Card with border emphasis
+
+ Outlined cards use borders to separate content while maintaining a clean appearance.
+
+ Action
+
+
+
+
+
+
+
+ Size Variants
+
+
+
+
Small Card
+
+ Compact card for minimal content.
+
+ Small
+
+
+
+
+
+
Medium Card
+
+ Standard card size for most use cases with balanced spacing.
+
+ Medium
+
+
+
+
+
+
Large Card
+
+ Spacious card for detailed content with generous padding and larger typography.
+
+ Large
+
+
+
+
+
+
+
+ Elevation Variants
+
+
+
+
+
+
+
+
Small
+
Subtle shadow
+
+
+
+
+
+
Medium
+
Moderate shadow
+
+
+
+
+
+
Large
+
Prominent shadow
+
+
+
+
+
+
Extra Large
+
Deep shadow
+
+
+
+
+
+
+
+ Corner Radius Variants
+
+
+
+
No Radius
+
Sharp corners
+
+
+
+
+
+
Small
+
Subtle rounding
+
+
+
+
+
+
Medium
+
Standard rounding
+
+
+
+
+
+
Large
+
Generous rounding
+
+
+
+
+
+
+
+
+
+
+
+ Interactive States
+
+
+
+
Clickable Card
+
Click me!
+
+ This card responds to clicks and has hover effects. Try clicking or focusing with keyboard.
+
+
+
+
+
Disabled Card
+
Cannot interact
+
+ This card is disabled and cannot be interacted with.
+
+ Disabled
+
+
+
+
+
+
+
+ Glass Effect Variants
+
+ Glass morphism effects using semantic design tokens from the shared-ui library.
+ Each variant offers different levels of transparency and blur effects.
+
+
+
+
+
+
Translucent Glass
+
Ultra-light transparency
+
+
+ Barely visible background with subtle blur effect for minimal visual interference.
+ Perfect for overlays that need to preserve background visibility.
+
+
+ Translucent
+
+
+
+
+
+
Light Glass
+
Gentle transparency
+
+
+ Gentle glass effect that maintains excellent readability while adding modern depth.
+ Ideal for cards that need subtle separation from background.
+
+
+ Light
+
+
+
+
+
+
Medium Glass
+
Balanced effect
+
+
+ Perfect balance between visibility and glass morphism effect.
+ The most versatile variant suitable for most interface components.
+
+
+ Medium
+
+
+
+
+
+
Heavy Glass
+
Strong presence
+
+
+ Prominent glass effect with substantial background presence.
+ Great for important UI elements that need to stand out prominently.
+
+
+ Heavy
+
+
+
+
+
+
Frosted Glass
+
Maximum opacity with enhanced blur
+
+
+ Nearly opaque with enhanced blur for a true frosted glass appearance.
+ Perfect for modals, overlays, and primary interface panels.
+
+
+ Frosted
+
+
+
+
+ Interactive Glass Cards
+
+ Glass effects with full interactivity, smooth animations, and hover states.
+
+
+
+
+
Clickable Light Glass
+
Interactive with smooth animations
+
+
+ Click me! Light glass variant with full interactivity and animation support.
+ Features hover effects and touch feedback.
+
+
+
+
+
+
Clickable Heavy Glass
+
Strong glass with responsive feedback
+
+
+ Heavy glass variant combining strong visual presence with responsive interactions.
+ Perfect for call-to-action cards and important interactive elements.
+
+
+
+
+
+
Balanced Interactive Glass
+
Medium glass with keyboard navigation
+
+
+ Medium glass effect with complete accessibility support including keyboard navigation,
+ focus states, and screen reader compatibility.
+
+
+
+
+ Glass Implementation Details
+
+
+ Enhanced Glass System: All glass effects now use semantic design tokens from the shared-ui library,
+ ensuring consistent theming, better performance, and improved browser compatibility.
+
+
+ Semantic Tokens: Uses @include glass-{{ '{' }}variant{{ '}' }}(true) mixins
+ Animation Support: Built-in smooth transitions and hover effects
+ Accessibility: Proper focus states and keyboard navigation
+ Browser Support: Automatic fallbacks for older browsers
+ Performance: Optimized with will-change and isolation
+
+
+
+
+
+
+ Background Layer Cards
+
+
+
+
+
Gradient Background
+
Beautiful gradient overlay
+
+ This card has a gradient background layer that creates visual depth and interest.
+
+ Action
+
+
+
+
+
+
+
Pattern Background
+
Custom pattern design
+
+ Click me! Cards can have complex patterns and remain fully interactive with proper layering.
+
+
+
+
+
+
+ Usage Examples
+
+
Basic Card:
+
<ui-card variant="elevated" size="md" elevation="sm" radius="md">
+ <div slot="header">
+ <h4>Card Title</h4>
+ <p>Card subtitle</p>
+ </div>
+ <p>Card content goes here</p>
+ <div slot="footer">
+ <ui-button variant="filled">Action</ui-button>
+ </div>
+</ui-card>
+
+
Interactive Card:
+
<ui-card
+ variant="elevated"
+ [clickable]="true"
+ (cardClick)="handleCardClick($event)">
+ <p>Click me!</p>
+</ui-card>
+
+
Glass Effect Card:
+
<ui-card
+ variant="elevated"
+ [glass]="true"
+ glassVariant="medium"
+ elevation="md"
+ radius="lg">
+ <p>Glass morphism effect with semantic tokens</p>
+</ui-card>
+
+
Glass Variant Examples:
+
<!-- All glass variants using semantic design tokens -->
+<ui-card [glass]="true" glassVariant="translucent">Ultra-light transparency</ui-card>
+<ui-card [glass]="true" glassVariant="light">Gentle glass effect</ui-card>
+<ui-card [glass]="true" glassVariant="medium">Balanced glass morphism</ui-card>
+<ui-card [glass]="true" glassVariant="heavy">Strong visual presence</ui-card>
+<ui-card [glass]="true" glassVariant="frosted">Maximum opacity with blur</ui-card>
+
+
Interactive Glass Card:
+
<ui-card
+ variant="elevated"
+ [glass]="true"
+ glassVariant="light"
+ [clickable]="true"
+ (cardClick)="handleCardClick($event)"
+ elevation="lg"
+ radius="lg">
+ <p>Clickable glass card with animations</p>
+</ui-card>
+
+
Background Layer Card:
+
<ui-card
+ variant="elevated"
+ [hasBackgroundLayer]="true"
+ [hasHeader]="true">
+ <div slot="background">
+ <!-- Background content -->
+ </div>
+ <div slot="header">
+ <h4>Card with Background</h4>
+ </div>
+ <p>Content over background</p>
+</ui-card>
+
+
+
+
+
+ Interactive Actions
+ @if (lastAction) {
+
+ Last action: {{ lastAction }}
+
+ }
+
+ Reset Demo
+ Show Alert
+
+
+
+ `,
+ styles: [`
+ h2 {
+ color: hsl(279, 14%, 11%);
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ border-bottom: 2px solid hsl(258, 100%, 47%);
+ padding-bottom: 0.5rem;
+ }
+
+ h3 {
+ color: hsl(279, 14%, 25%);
+ font-size: 1.5rem;
+ margin-bottom: 1rem;
+ }
+
+ h4 {
+ color: hsl(287, 12%, 35%);
+ font-size: 1.125rem;
+ margin-bottom: 0.75rem;
+ }
+
+ section {
+ border: 1px solid hsl(289, 14%, 90%);
+ border-radius: 8px;
+ padding: 1.5rem;
+ background: hsl(286, 20%, 99%);
+ }
+
+ pre {
+ font-size: 0.875rem;
+ line-height: 1.5;
+ margin: 0.5rem 0;
+ }
+
+ code {
+ font-family: 'JetBrains Mono', monospace;
+ color: #d63384;
+ }
+ `]
+})
+export class CardDemoComponent {
+ lastAction: string = '';
+
+ handleClick(buttonType: string): void {
+ this.lastAction = `Button clicked: ${buttonType}`;
+ console.log(`Button clicked: ${buttonType}`);
+ }
+
+ handleCardClick(cardType: string): void {
+ this.lastAction = `Card clicked: ${cardType}`;
+ console.log(`Card clicked: ${cardType}`);
+ }
+
+ showAlert(message: string): void {
+ alert(message);
+ this.lastAction = 'Alert shown';
+ }
+
+ resetDemo(): void {
+ this.lastAction = '';
+ console.log('Demo reset');
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/carousel-demo/carousel-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/carousel-demo/carousel-demo.component.scss
new file mode 100644
index 0000000..8743cd9
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/carousel-demo/carousel-demo.component.scss
@@ -0,0 +1,101 @@
+@import "../../../../../shared-ui/src/styles/semantic";
+
+.carousel-demo {
+ padding: $semantic-spacing-layout-lg;
+
+ .demo-section {
+ margin-bottom: $semantic-spacing-section-md;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ .section-title {
+ font-size: $semantic-typography-font-size-xl;
+ font-weight: $semantic-typography-font-weight-semibold;
+ line-height: $semantic-typography-line-height-normal;
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-content-paragraph;
+ }
+
+ .section-description {
+ font-size: $semantic-typography-font-size-md;
+ font-weight: $semantic-typography-font-weight-normal;
+ line-height: $semantic-typography-line-height-relaxed;
+ color: $semantic-color-text-secondary;
+ margin-bottom: $semantic-spacing-content-paragraph;
+ }
+
+ .carousel-container {
+ margin-bottom: $semantic-spacing-layout-lg;
+ }
+
+ .features-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: $semantic-spacing-content-paragraph;
+ margin-top: $semantic-spacing-layout-lg;
+ }
+
+ .feature-card {
+ background: $semantic-color-surface-container;
+ border-radius: 12px;
+ padding: $semantic-spacing-content-paragraph;
+ border: 1px solid $semantic-color-border-primary;
+
+ .feature-title {
+ font-size: $semantic-typography-font-size-md;
+ font-weight: $semantic-typography-font-weight-semibold;
+ line-height: $semantic-typography-line-height-normal;
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+
+ .feature-description {
+ font-size: $semantic-typography-font-size-sm;
+ font-weight: $semantic-typography-font-weight-normal;
+ line-height: $semantic-typography-line-height-normal;
+ color: $semantic-color-text-secondary;
+ margin-bottom: $semantic-spacing-component-md;
+ }
+
+ .feature-example {
+ background: $semantic-color-surface-variant;
+ border-radius: 8px;
+ padding: $semantic-spacing-component-md;
+ border: 1px solid $semantic-color-border-secondary;
+
+ .example-label {
+ font-size: $semantic-typography-font-size-xs;
+ font-weight: $semantic-typography-font-weight-normal;
+ line-height: $semantic-typography-line-height-normal;
+ color: $semantic-color-text-tertiary;
+ margin-bottom: $semantic-spacing-component-xs;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ }
+
+ .example-content {
+ font-size: $semantic-typography-font-size-sm;
+ font-weight: $semantic-typography-font-weight-normal;
+ line-height: $semantic-typography-line-height-normal;
+ color: $semantic-color-text-secondary;
+ }
+ }
+ }
+
+ .code-snippet {
+ background: $semantic-color-surface-container;
+ border: 1px solid $semantic-color-border-primary;
+ border-radius: 8px;
+ padding: $semantic-spacing-content-paragraph;
+ margin-top: $semantic-spacing-content-paragraph;
+ font-family: 'Roboto Mono', monospace;
+ font-size: 0.875rem;
+ line-height: 1.4;
+ color: $semantic-color-text-primary;
+ overflow-x: auto;
+ }
+}
+
diff --git a/projects/demo-ui-essentials/src/app/demos/carousel-demo/carousel-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/carousel-demo/carousel-demo.component.ts
new file mode 100644
index 0000000..ca0c76d
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/carousel-demo/carousel-demo.component.ts
@@ -0,0 +1,345 @@
+import { Component, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { CarouselComponent, CarouselItem } from '../../../../../ui-essentials/src/lib/components/data-display/carousel';
+
+@Component({
+ selector: 'ui-carousel-demo',
+ standalone: true,
+ imports: [CommonModule, CarouselComponent],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
Carousel Component Demo
+
+
+
+ Size Variants
+
+
+
Small
+
+
+
+
+
+
Medium
+
+
+
+
+
+
Large
+
+
+
+
+
+
+
+
+ Variant Types
+
+
+
Image Carousel
+
+
+
+
+
+
Card Carousel
+
+
+
+
+
+
Content Carousel
+
+
+
+
+
+
+
+ Transition Effects
+
+
+
Slide
+
+
+
+
+
+
Fade
+
+
+
+
+
+
Scale
+
+
+
+
+
+
+
+
+ Indicator Styles
+
+
+
Dots
+
+
+
+
+
+
Bars
+
+
+
+
+
+
Thumbnails
+
+
+
+
+
+
+
+
+ Special Features
+
+
+
Autoplay
+
+
+
+
+
+
No Loop
+
+
+
+
+
+
Disabled
+
+
+
+
+
+
+
+
+ Interactive Example
+
+
+ Current slide: {{ currentSlide + 1 }} of {{ interactiveItems.length }}
+ @if (lastClickedItem) {
+ Last clicked: {{ lastClickedItem.title }}
+ }
+
+
+ `,
+ styleUrl: './carousel-demo.component.scss'
+})
+export class CarouselDemoComponent {
+ currentSlide = 0;
+ lastClickedItem: CarouselItem | null = null;
+
+ imageItems: CarouselItem[] = [
+ {
+ id: 1,
+ imageUrl: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&h=400&fit=crop',
+ title: 'Mountain Landscape',
+ subtitle: 'Serene mountain views'
+ },
+ {
+ id: 2,
+ imageUrl: 'https://images.unsplash.com/photo-1501594907352-04cda38ebc29?w=800&h=400&fit=crop',
+ title: 'Ocean Waves',
+ subtitle: 'Peaceful ocean scenery'
+ },
+ {
+ id: 3,
+ imageUrl: 'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800&h=400&fit=crop',
+ title: 'Forest Path',
+ subtitle: 'Mystical forest trail'
+ },
+ {
+ id: 4,
+ imageUrl: 'https://images.unsplash.com/photo-1501436513145-30f24e19fcc4?w=800&h=400&fit=crop',
+ title: 'Desert Sunset',
+ subtitle: 'Golden desert landscape'
+ }
+ ];
+
+ cardItems: CarouselItem[] = [
+ {
+ id: 1,
+ imageUrl: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=400&h=200&fit=crop',
+ title: 'Analytics Dashboard',
+ subtitle: 'Data Visualization',
+ content: 'Powerful analytics tools to understand your business metrics and make data-driven decisions.'
+ },
+ {
+ id: 2,
+ imageUrl: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=400&h=200&fit=crop',
+ title: 'Team Collaboration',
+ subtitle: 'Productivity Tools',
+ content: 'Enhanced collaboration features to keep your team connected and productive from anywhere.'
+ },
+ {
+ id: 3,
+ imageUrl: 'https://images.unsplash.com/photo-1563207153-f403bf289096?w=400&h=200&fit=crop',
+ title: 'Mobile Experience',
+ subtitle: 'Cross-Platform',
+ content: 'Seamless experience across all devices with our responsive design and mobile optimization.'
+ }
+ ];
+
+ contentItems: CarouselItem[] = [
+ {
+ id: 1,
+ title: 'Welcome to Our Platform',
+ subtitle: 'Getting Started Guide',
+ content: 'Discover amazing features and capabilities that will transform the way you work and collaborate with your team.'
+ },
+ {
+ id: 2,
+ title: 'Advanced Features',
+ subtitle: 'Power User Tips',
+ content: 'Unlock the full potential of our platform with advanced features designed for professional workflows.'
+ },
+ {
+ id: 3,
+ title: 'Success Stories',
+ subtitle: 'Customer Testimonials',
+ content: 'Join thousands of satisfied customers who have achieved remarkable results using our solutions.'
+ }
+ ];
+
+ thumbnailItems: CarouselItem[] = [
+ {
+ id: 1,
+ imageUrl: 'https://images.unsplash.com/photo-1517180102446-f3ece451e9d8?w=800&h=400&fit=crop',
+ thumbnail: 'https://images.unsplash.com/photo-1517180102446-f3ece451e9d8?w=100&h=60&fit=crop',
+ title: 'Urban Architecture',
+ subtitle: 'Modern city design'
+ },
+ {
+ id: 2,
+ imageUrl: 'https://images.unsplash.com/photo-1449824913935-59a10b8d2000?w=800&h=400&fit=crop',
+ thumbnail: 'https://images.unsplash.com/photo-1449824913935-59a10b8d2000?w=100&h=60&fit=crop',
+ title: 'Natural Beauty',
+ subtitle: 'Landscape photography'
+ },
+ {
+ id: 3,
+ imageUrl: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&h=400&fit=crop',
+ thumbnail: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=100&h=60&fit=crop',
+ title: 'Mountain Vista',
+ subtitle: 'Alpine scenery'
+ }
+ ];
+
+ interactiveItems: CarouselItem[] = [
+ {
+ id: 1,
+ imageUrl: 'https://images.unsplash.com/photo-1557804506-669a67965ba0?w=400&h=200&fit=crop',
+ title: 'Project Alpha',
+ subtitle: 'Innovation Hub',
+ content: 'Revolutionary project management tools designed to streamline your workflow and boost productivity.'
+ },
+ {
+ id: 2,
+ imageUrl: 'https://images.unsplash.com/photo-1552664730-d307ca884978?w=400&h=200&fit=crop',
+ title: 'Team Dynamics',
+ subtitle: 'Collaboration Suite',
+ content: 'Advanced collaboration features that bring teams together, regardless of location or time zone.'
+ },
+ {
+ id: 3,
+ imageUrl: 'https://images.unsplash.com/photo-1504384308090-c894fdcc538d?w=400&h=200&fit=crop',
+ title: 'Data Insights',
+ subtitle: 'Analytics Platform',
+ content: 'Comprehensive analytics and reporting tools to help you make informed business decisions.'
+ }
+ ];
+
+ handleSlideChange(index: number): void {
+ this.currentSlide = index;
+ console.log('Slide changed to:', index);
+ }
+
+ handleItemClick(event: {item: CarouselItem, index: number}): void {
+ this.lastClickedItem = event.item;
+ console.log('Item clicked:', event);
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/carousel-demo/index.ts b/projects/demo-ui-essentials/src/app/demos/carousel-demo/index.ts
new file mode 100644
index 0000000..40d2ada
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/carousel-demo/index.ts
@@ -0,0 +1 @@
+export * from './carousel-demo.component';
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/checkbox-demo/checkbox-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/checkbox-demo/checkbox-demo.component.ts
new file mode 100644
index 0000000..57a30ef
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/checkbox-demo/checkbox-demo.component.ts
@@ -0,0 +1,399 @@
+import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { CheckboxComponent } from '../../../../../ui-essentials/src/lib/components/forms/checkbox';
+
+@Component({
+ selector: 'ui-checkbox-demo',
+ standalone: true,
+ imports: [
+ CommonModule,
+ FormsModule,
+ CheckboxComponent
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
Checkbox Component Showcase
+
+
+
+ Basic Checkboxes
+
+
+
+
+
+
+
+
+
+ Size Variants
+
+
+
+
+
+
+
+
+
+ Color Variants
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ With Descriptions
+
+
+
+
+
+
+
+
+
+ Interactive Controls
+
+
+ Check All
+
+
+ Uncheck All
+
+
+ {{ globalDisabled() ? 'Enable All' : 'Disable All' }}
+
+
+
+ @if (lastSelection()) {
+
+ Last selection: {{ lastSelection() }}
+
+ }
+
+
+
+
+ Code Examples
+
+
Basic Usage:
+
<ui-checkbox
+ label="Accept terms"
+ [(ngModel)]="isAccepted"
+ (checkboxChange)="onAccept($event)">
+</ui-checkbox>
+
+
With Description:
+
<ui-checkbox
+ label="Marketing emails"
+ description="Receive promotional content"
+ variant="success"
+ size="lg"
+ [(ngModel)]="emailOptIn">
+</ui-checkbox>
+
+
Required Field:
+
<ui-checkbox
+ label="I agree to terms"
+ [required]="true"
+ state="error"
+ [(ngModel)]="agreedToTerms">
+</ui-checkbox>
+
+
+
+
+
+ Current Values
+
+
{{ getCurrentValues() }}
+
+
+
+ `,
+ styles: [`
+ h2 {
+ color: hsl(279, 14%, 11%);
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ border-bottom: 2px solid hsl(258, 100%, 47%);
+ padding-bottom: 0.5rem;
+ }
+
+ h3 {
+ color: hsl(279, 14%, 25%);
+ font-size: 1.5rem;
+ margin-bottom: 1rem;
+ }
+
+ h4 {
+ color: hsl(279, 14%, 35%);
+ font-size: 1.2rem;
+ margin-bottom: 0.5rem;
+ }
+
+ pre {
+ margin: 0;
+ font-family: 'Courier New', monospace;
+ font-size: 0.9rem;
+ line-height: 1.4;
+ }
+
+ code {
+ color: hsl(279, 14%, 15%);
+ }
+
+ section {
+ border: 1px solid #e9ecef;
+ padding: 1.5rem;
+ border-radius: 8px;
+ background: #ffffff;
+ }
+
+ button:hover {
+ opacity: 0.9;
+ transform: translateY(-1px);
+ }
+ `]
+})
+export class CheckboxDemoComponent {
+ // Basic checkboxes
+ basicChecked1 = signal(false);
+ basicChecked2 = signal(true);
+ basicChecked3 = signal(false);
+
+ // Size variants
+ sizeSmall = signal(false);
+ sizeMedium = signal(true);
+ sizeLarge = signal(false);
+
+ // Color variants
+ variantPrimary = signal(true);
+ variantSecondary = signal(false);
+ variantSuccess = signal(true);
+ variantWarning = signal(false);
+ variantDanger = signal(false);
+
+ // States
+ stateDisabledUnchecked = signal(false);
+ stateDisabledChecked = signal(true);
+ stateRequired = signal(false);
+ stateError = signal(false);
+
+ // With descriptions
+ descChecked1 = signal(false);
+ descChecked2 = signal(true);
+ descChecked3 = signal(true);
+
+ // Control states
+ globalDisabled = signal(false);
+ lastSelection = signal('');
+
+ onCheckboxChange(checkboxId: string, checked: boolean): void {
+ this.lastSelection.set(`${checkboxId}: ${checked ? 'checked' : 'unchecked'}`);
+ console.log(`Checkbox ${checkboxId} changed:`, checked);
+ }
+
+ checkAll(): void {
+ // Update all non-disabled checkboxes to checked
+ this.basicChecked1.set(true);
+ this.basicChecked2.set(true);
+ this.basicChecked3.set(true);
+ this.sizeSmall.set(true);
+ this.sizeMedium.set(true);
+ this.sizeLarge.set(true);
+ this.variantPrimary.set(true);
+ this.variantSecondary.set(true);
+ this.variantSuccess.set(true);
+ this.variantWarning.set(true);
+ this.variantDanger.set(true);
+ this.stateRequired.set(true);
+ this.stateError.set(true);
+ this.descChecked1.set(true);
+ this.descChecked2.set(true);
+ this.descChecked3.set(true);
+
+ this.lastSelection.set('All checkboxes checked');
+ }
+
+ uncheckAll(): void {
+ // Update all non-disabled checkboxes to unchecked
+ this.basicChecked1.set(false);
+ this.basicChecked2.set(false);
+ this.basicChecked3.set(false);
+ this.sizeSmall.set(false);
+ this.sizeMedium.set(false);
+ this.sizeLarge.set(false);
+ this.variantPrimary.set(false);
+ this.variantSecondary.set(false);
+ this.variantSuccess.set(false);
+ this.variantWarning.set(false);
+ this.variantDanger.set(false);
+ this.stateRequired.set(false);
+ this.stateError.set(false);
+ this.descChecked1.set(false);
+ this.descChecked2.set(false);
+ this.descChecked3.set(false);
+
+ this.lastSelection.set('All checkboxes unchecked');
+ }
+
+ toggleDisabled(): void {
+ this.globalDisabled.update(disabled => !disabled);
+ this.lastSelection.set(`Global disabled: ${this.globalDisabled() ? 'enabled' : 'disabled'}`);
+ }
+
+ getCurrentValues(): string {
+ const values = {
+ basic: {
+ checked1: this.basicChecked1(),
+ checked2: this.basicChecked2(),
+ checked3: this.basicChecked3()
+ },
+ sizes: {
+ small: this.sizeSmall(),
+ medium: this.sizeMedium(),
+ large: this.sizeLarge()
+ },
+ variants: {
+ primary: this.variantPrimary(),
+ secondary: this.variantSecondary(),
+ success: this.variantSuccess(),
+ warning: this.variantWarning(),
+ danger: this.variantDanger()
+ },
+ states: {
+ disabledUnchecked: this.stateDisabledUnchecked(),
+ disabledChecked: this.stateDisabledChecked(),
+ required: this.stateRequired(),
+ error: this.stateError()
+ },
+ descriptions: {
+ desc1: this.descChecked1(),
+ desc2: this.descChecked2(),
+ desc3: this.descChecked3()
+ },
+ controls: {
+ globalDisabled: this.globalDisabled(),
+ lastSelection: this.lastSelection()
+ }
+ };
+
+ return JSON.stringify(values, null, 2);
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/chip-demo/chip-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/chip-demo/chip-demo.component.ts
new file mode 100644
index 0000000..30f7220
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/chip-demo/chip-demo.component.ts
@@ -0,0 +1,443 @@
+import { Component, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ChipComponent } from '../../../../../ui-essentials/src/lib/components/data-display/chip';
+import { ButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons';
+import {
+ faHeart,
+ faUser,
+ faStar,
+ faTag,
+ faHome,
+ faEnvelope,
+ faPhone,
+ faMapMarker,
+ faCalendar,
+ faCog,
+ faTimes,
+ faPlus,
+ faCheck
+} from '@fortawesome/free-solid-svg-icons';
+
+interface ChipData {
+ id: string;
+ label: string;
+ selected: boolean;
+ disabled: boolean;
+}
+
+@Component({
+ selector: 'ui-chip-demo',
+ standalone: true,
+ imports: [
+ CommonModule,
+ ChipComponent,
+ ButtonComponent
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
Chip Component Showcase
+
+
+
+ Chip Variants
+
+
+
+
+
+
+ Selected State
+
+
+
+
+
+
+
+
+
+ Size Variants
+
+
+
+
+
+
+ With Icons
+
+
+
+
+
+
+
+
+
+ Corner Radius Variants
+
+
+
+
+
+
+
+
+
+
+
+ Chips with Icons
+
+ Leading Icons
+
+
+
+
+
+
+
+ Trailing Icons
+
+
+
+
+
+
+ With Avatars
+
+
+
+
+
+
+
+
+
+
+
+
+ Removable Chips
+
+ @for (tag of removableTags; track tag.id) {
+
+
+ }
+
+
+ Removable with Icons
+
+ @for (tagWithIcon of removableTagsWithIcons; track tagWithIcon.id) {
+
+
+ }
+
+
+
+ Add Tags Back
+
+
+
+
+
+ Interactive Selection
+ Click chips to toggle selection:
+
+ @for (item of selectableItems; track item.id) {
+
+
+ }
+
+
+ Selected: {{ getSelectedItems() }}
+ Clear Selection
+
+
+
+
+ Chip States
+
+ Interactive States
+
+
+
+
+
+
+
+ Non-interactive (Labels)
+
+
+
+
+
+
+
+
+
+ Chip Sets
+
+ Horizontal Scrolling Set
+
+
+
+
+
+
+
+
+
+
+
+ Wrapped Set
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Usage Examples
+
+
Basic Chip:
+
<ui-chip
+ variant="filled"
+ label="Basic Chip"
+ [clickable]="true"
+ (chipClick)="handleClick($event)">
+</ui-chip>
+
+
Chip with Icon:
+
<ui-chip
+ variant="outlined"
+ label="Home"
+ [leadingIcon]="faHome"
+ [clickable]="true"
+ (chipClick)="goHome()">
+</ui-chip>
+
+
Removable Chip:
+
<ui-chip
+ variant="filled"
+ label="Tag Name"
+ [removable]="true"
+ [closeIcon]="faTimes"
+ (chipRemove)="removeTag($event)">
+</ui-chip>
+
+
Chip with Avatar:
+
<ui-chip
+ variant="outlined"
+ label="John Doe"
+ avatar="path/to/avatar.jpg"
+ avatarAlt="John Doe"
+ [clickable]="true"
+ (chipClick)="viewProfile('john')">
+</ui-chip>
+
+
+
+
+
+ Interactive Actions
+ @if (lastAction) {
+
+ Last action: {{ lastAction }}
+
+ }
+
+ Reset Demo
+ Show Alert
+
+
+
+ `,
+ styles: [`
+ h2 {
+ color: hsl(279, 14%, 11%);
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ border-bottom: 2px solid hsl(258, 100%, 47%);
+ padding-bottom: 0.5rem;
+ }
+
+ h3 {
+ color: hsl(279, 14%, 25%);
+ font-size: 1.5rem;
+ margin-bottom: 1rem;
+ }
+
+ h4 {
+ color: hsl(287, 12%, 35%);
+ font-size: 1.125rem;
+ margin-bottom: 0.75rem;
+ }
+
+ section {
+ border: 1px solid hsl(289, 14%, 90%);
+ border-radius: 8px;
+ padding: 1.5rem;
+ background: hsl(286, 20%, 99%);
+ }
+
+ pre {
+ font-size: 0.875rem;
+ line-height: 1.5;
+ margin: 0.5rem 0;
+ }
+
+ code {
+ font-family: 'JetBrains Mono', monospace;
+ color: #d63384;
+ }
+ `]
+})
+export class ChipDemoComponent {
+ lastAction: string = '';
+
+ // FontAwesome icons
+ faHeart = faHeart;
+ faUser = faUser;
+ faStar = faStar;
+ faTag = faTag;
+ faHome = faHome;
+ faEnvelope = faEnvelope;
+ faPhone = faPhone;
+ faMapMarker = faMapMarker;
+ faCalendar = faCalendar;
+ faCog = faCog;
+ faTimes = faTimes;
+ faPlus = faPlus;
+ faCheck = faCheck;
+
+ removableTags: ChipData[] = [
+ { id: 'react', label: 'React', selected: false, disabled: false },
+ { id: 'angular', label: 'Angular', selected: false, disabled: false },
+ { id: 'vue', label: 'Vue.js', selected: false, disabled: false },
+ { id: 'svelte', label: 'Svelte', selected: false, disabled: false }
+ ];
+
+ removableTagsWithIcons: any[] = [
+ { id: 'home-tag', label: 'Home', icon: faHome },
+ { id: 'email-tag', label: 'Email', icon: faEnvelope },
+ { id: 'star-tag', label: 'Starred', icon: faStar }
+ ];
+
+ selectableItems: ChipData[] = [
+ { id: 'option1', label: 'Option 1', selected: false, disabled: false },
+ { id: 'option2', label: 'Option 2', selected: true, disabled: false },
+ { id: 'option3', label: 'Option 3', selected: false, disabled: false },
+ { id: 'option4', label: 'Option 4', selected: false, disabled: false },
+ { id: 'option5', label: 'Disabled', selected: false, disabled: true }
+ ];
+
+ private originalRemovableTags = [...this.removableTags];
+ private originalRemovableTagsWithIcons = [...this.removableTagsWithIcons];
+
+ handleChipClick(chipId: string): void {
+ this.lastAction = `Chip clicked: ${chipId}`;
+ console.log(`Chip clicked: ${chipId}`);
+ }
+
+ removeTag(tagId: string): void {
+ this.removableTags = this.removableTags.filter(tag => tag.id !== tagId);
+ this.lastAction = `Tag removed: ${tagId}`;
+ console.log(`Tag removed: ${tagId}`);
+ }
+
+ removeTagWithIcon(tagId: string): void {
+ this.removableTagsWithIcons = this.removableTagsWithIcons.filter(tag => tag.id !== tagId);
+ this.lastAction = `Tag with icon removed: ${tagId}`;
+ console.log(`Tag with icon removed: ${tagId}`);
+ }
+
+ addTags(): void {
+ this.removableTags = [...this.originalRemovableTags];
+ this.removableTagsWithIcons = [...this.originalRemovableTagsWithIcons];
+ this.lastAction = 'Tags restored';
+ console.log('Tags restored');
+ }
+
+ toggleSelection(itemId: string): void {
+ const item = this.selectableItems.find(i => i.id === itemId);
+ if (item && !item.disabled) {
+ item.selected = !item.selected;
+ this.lastAction = `Selection toggled: ${itemId} (${item.selected ? 'selected' : 'deselected'})`;
+ console.log(`Selection toggled: ${itemId}`, item.selected);
+ }
+ }
+
+ getSelectedItems(): string {
+ return this.selectableItems
+ .filter(item => item.selected)
+ .map(item => item.label)
+ .join(', ') || 'None';
+ }
+
+ clearSelection(): void {
+ this.selectableItems.forEach(item => {
+ if (!item.disabled) {
+ item.selected = false;
+ }
+ });
+ this.lastAction = 'Selection cleared';
+ console.log('Selection cleared');
+ }
+
+ showAlert(message: string): void {
+ alert(message);
+ this.lastAction = 'Alert shown';
+ }
+
+ resetDemo(): void {
+ this.lastAction = '';
+ this.clearSelection();
+ this.addTags();
+ console.log('Demo reset');
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/container-demo/container-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/container-demo/container-demo.component.scss
new file mode 100644
index 0000000..540d6fb
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/container-demo/container-demo.component.scss
@@ -0,0 +1,244 @@
+@use '../../../../../shared-ui/src/styles/semantic/index' as *;
+
+.demo-wrapper {
+ padding: $semantic-spacing-layout-md;
+ width: 100%;
+ max-width: 100%;
+ overflow-x: hidden; // Prevent horizontal overflow
+ box-sizing: border-box;
+}
+
+.demo-section {
+ margin-bottom: $semantic-spacing-layout-xl;
+ width: 100%;
+ overflow-x: hidden; // Prevent horizontal overflow
+
+ h2 {
+ margin-bottom: $semantic-spacing-layout-md;
+ color: $semantic-color-text-primary;
+ font-size: 1.5rem;
+ font-weight: 600;
+ text-align: center;
+ }
+
+ h3 {
+ margin-bottom: $semantic-spacing-component-lg;
+ color: $semantic-color-text-primary;
+ font-size: 1.25rem;
+ font-weight: 600;
+ }
+
+ h4 {
+ margin-bottom: $semantic-spacing-component-sm;
+ color: $semantic-color-text-secondary;
+ font-size: 1.125rem;
+ font-weight: 500;
+ }
+}
+
+.demo-containers-showcase {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-component-lg;
+ margin-bottom: $semantic-spacing-layout-lg;
+ width: 100%;
+ max-width: 100%;
+ overflow-x: hidden; // Prevent horizontal overflow
+ box-sizing: border-box;
+
+ // Ensure all child containers are properly constrained
+ .ui-container {
+ max-width: min(100%, var(--container-max-width, 100%));
+ margin-left: auto;
+ margin-right: auto;
+ }
+}
+
+.demo-container-wrapper {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center; // Center the containers
+ overflow-x: hidden; // Prevent horizontal overflow
+}
+
+.demo-label {
+ font-size: 0.875rem;
+ color: $semantic-color-text-secondary;
+ margin-bottom: $semantic-spacing-component-xs;
+ text-align: center;
+ font-weight: 500;
+}
+
+.demo-container-outline {
+ border: 2px dashed $semantic-color-border-subtle;
+ background: rgba($semantic-color-container-primary, 0.05);
+ max-width: 100%; // Ensure containers don't overflow their parent
+ box-sizing: border-box; // Include padding and border in width calculation
+ margin: 0 auto; // Center the container
+ position: relative; // For proper positioning context
+
+ // Override container component's auto margins that might be causing issues
+ &.ui-container {
+ margin-left: auto !important;
+ margin-right: auto !important;
+ left: auto !important;
+ right: auto !important;
+ }
+
+ &.demo-container-tall {
+ min-height: 150px;
+ }
+}
+
+.demo-content {
+ color: $semantic-color-text-primary;
+ text-align: center;
+ padding: $semantic-spacing-component-sm;
+ width: 100%;
+ max-width: 100%;
+ box-sizing: border-box;
+ overflow-wrap: break-word; // Handle long text properly
+ word-wrap: break-word;
+
+ &.hero-content {
+ h2 {
+ margin-bottom: $semantic-spacing-component-sm;
+ color: $semantic-color-text-primary;
+ }
+
+ p {
+ color: $semantic-color-text-secondary;
+ font-size: 1.125rem;
+ }
+ }
+}
+
+.demo-variant-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: $semantic-spacing-grid-gap-lg;
+}
+
+.demo-variant-item {
+ display: flex;
+ flex-direction: column;
+}
+
+.demo-flex-item {
+ background: $semantic-color-container-secondary;
+ color: $semantic-color-on-container-secondary;
+ padding: $semantic-spacing-component-sm;
+ border-radius: $semantic-border-radius-sm;
+ text-align: center;
+ font-weight: 500;
+ width: 100%; // Ensure flex items take full width
+ max-width: 100%;
+ box-sizing: border-box;
+ flex-shrink: 0; // Prevent shrinking
+}
+
+.demo-grid-item {
+ background: $semantic-color-container-tertiary;
+ color: $semantic-color-on-container-tertiary;
+ padding: $semantic-spacing-component-sm;
+ border-radius: $semantic-border-radius-sm;
+ text-align: center;
+ font-weight: 500;
+ min-height: 60px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.demo-paragraph {
+ margin-bottom: $semantic-spacing-component-sm;
+ color: $semantic-color-text-primary;
+ line-height: 1.5;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+}
+
+.demo-wide-content {
+ white-space: nowrap;
+ color: $semantic-color-text-primary;
+ padding: $semantic-spacing-component-sm;
+}
+
+// Feature cards styling
+:host ::ng-deep {
+ .demo-section ui-container[variant="card"] {
+ h3 {
+ margin-bottom: $semantic-spacing-component-sm;
+ color: $semantic-color-text-primary;
+ font-size: 1.125rem;
+ font-weight: 500;
+ }
+
+ p {
+ color: $semantic-color-text-secondary;
+ line-height: 1.5;
+ margin: 0;
+ }
+ }
+}
+
+// Additional container demo specific styles
+.demo-containers-showcase {
+ // Force all containers to behave properly
+ .ui-container {
+ // Ensure containers don't push beyond boundaries
+ position: relative;
+ left: 0 !important;
+ right: 0 !important;
+ transform: none !important;
+ }
+
+ // Size-specific constraints to prevent overflow
+ .ui-container--xs { --container-max-width: 475px; }
+ .ui-container--sm { --container-max-width: 640px; }
+ .ui-container--md { --container-max-width: 768px; }
+ .ui-container--lg { --container-max-width: 1024px; }
+ .ui-container--xl { --container-max-width: 1280px; }
+ .ui-container--2xl { --container-max-width: 1536px; }
+ .ui-container--full { --container-max-width: 100%; }
+
+ // Fix flex container alignment issues
+ .ui-container--flex {
+ align-items: stretch; // Default to stretch for consistent layout
+
+ &:not(.ui-container--flex-center):not(.ui-container--flex-start):not(.ui-container--flex-end) {
+ align-items: stretch; // Force default alignment for flex containers
+ }
+ }
+}
+
+@media (max-width: 1024px) {
+ // Force smaller containers on tablet
+ .demo-containers-showcase .ui-container {
+ max-width: min(100%, var(--container-max-width, 100%)) !important;
+ }
+}
+
+@media (max-width: 768px) {
+ .demo-variant-grid {
+ grid-template-columns: 1fr;
+ gap: $semantic-spacing-component-lg;
+ }
+
+ .demo-containers-showcase {
+ gap: $semantic-spacing-component-md;
+
+ // Force all containers to be full width on mobile
+ .ui-container {
+ max-width: 100% !important;
+ width: 100% !important;
+ }
+ }
+
+ .demo-wrapper {
+ padding: $semantic-spacing-component-md;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/container-demo/container-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/container-demo/container-demo.component.ts
new file mode 100644
index 0000000..58f347a
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/container-demo/container-demo.component.ts
@@ -0,0 +1,257 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ContainerComponent, GridSystemComponent } from '../../../../../ui-essentials/src/lib/components/layout';
+
+@Component({
+ selector: 'ui-container-demo',
+ standalone: true,
+ imports: [CommonModule, ContainerComponent, GridSystemComponent],
+ template: `
+
+
Container Demo
+
+
+
+ Size Variants
+
+ @for (size of sizes; track size) {
+
+
{{ size }} (max-width: {{ getMaxWidth(size) }})
+
+ {{ size }} container content
+
+
+ }
+
+
+
+
+
+ Container Variants
+
+
+
+
Default Container
+
+ Basic container with standard padding and centering
+
+
+
+
+
Card Container
+
+ Card-style container with background, border, and shadow
+
+
+
+
+
Section Container
+
+ Section container with larger vertical padding for page sections
+
+
+
+
+
Hero Container
+
+
+
Hero Section
+
Large padding and centered content for hero sections
+
+
+
+
+
+
+
+
+ Flex Container Variants
+
+
+
+
Flex Column (Default)
+
+ Item 1
+ Item 2
+ Item 3
+
+
+
+
+
Flex Row
+
+ Item 1
+ Item 2
+ Item 3
+
+
+
+
+
Flex Center
+
+ Centered
+ Content
+
+
+
+
+
Flex Space Between
+
+ Start
+ End
+
+
+
+
+
+
+
+ Grid Container Variants
+
+
+
+
Grid Auto
+
+ @for (item of getGridItems(6); track $index) {
+ {{ item }}
+ }
+
+
+
+
+
Grid 3 Columns
+
+ @for (item of getGridItems(6); track $index) {
+ {{ item }}
+ }
+
+
+
+
+
+
+
+ Padding Variants
+
+
+ @for (padding of paddingSizes; track padding) {
+
+
Padding: {{ padding }}
+
+ {{ padding }} padding content
+
+
+ }
+
+
+
+
+
+ Background Variants
+
+
+ @for (bg of backgrounds; track bg) {
+
+
Background: {{ bg }}
+
+ {{ bg }} background
+
+
+ }
+
+
+
+
+
+ Scrollable Containers
+
+
+
+
Vertical Scroll
+
+ @for (item of getLongContent(); track $index) {
+ {{ item }}
+ }
+
+
+
+
+
Horizontal Scroll
+
+
+ This is a very long line of content that will cause horizontal scrolling when it exceeds the container width. Keep reading to see the scroll behavior in action.
+
+
+
+
+
+
+
+
+ Real World Example
+
+
+ Product Features
+ Discover what makes our product special
+
+
+ @for (feature of features; track feature.title) {
+
+ {{ feature.title }}
+ {{ feature.description }}
+
+ }
+
+
+
+
+
+ `,
+ styleUrl: './container-demo.component.scss'
+})
+export class ContainerDemoComponent {
+ sizes: ('xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full')[] = ['xs', 'sm', 'md', 'lg', 'xl', '2xl', 'full'];
+ paddingSizes: ('xs' | 'sm' | 'md' | 'lg' | 'xl' | 'none')[] = ['xs', 'sm', 'md', 'lg', 'xl', 'none'];
+ backgrounds: ('transparent' | 'surface' | 'surface-secondary' | 'surface-elevated')[] = [
+ 'transparent', 'surface', 'surface-secondary', 'surface-elevated'
+ ];
+
+ features = [
+ { title: 'Fast', description: 'Lightning-fast performance optimized for speed' },
+ { title: 'Secure', description: 'Enterprise-grade security built from the ground up' },
+ { title: 'Scalable', description: 'Grows with your business needs seamlessly' },
+ { title: 'Reliable', description: '99.9% uptime guaranteed with redundant systems' },
+ { title: 'Easy to Use', description: 'Intuitive interface designed for productivity' },
+ { title: 'Supported', description: '24/7 customer support when you need it' }
+ ];
+
+ getMaxWidth(size: string): string {
+ const sizes: Record = {
+ 'xs': '475px',
+ 'sm': '640px',
+ 'md': '768px',
+ 'lg': '1024px',
+ 'xl': '1280px',
+ '2xl': '1536px',
+ 'full': 'none'
+ };
+ return sizes[size] || 'auto';
+ }
+
+ getGridItems(count: number): string[] {
+ return Array.from({ length: count }, (_, i) => `Item ${i + 1}`);
+ }
+
+ getLongContent(): string[] {
+ return [
+ 'This is paragraph 1 with some content to demonstrate vertical scrolling.',
+ 'This is paragraph 2 with more content to show how the container handles overflow.',
+ 'This is paragraph 3 continuing the demonstration of scrollable content.',
+ 'This is paragraph 4 adding even more content to ensure scrolling is needed.',
+ 'This is paragraph 5 with the final bit of content to complete the demo.',
+ 'This is paragraph 6 making sure we have enough content for scroll.',
+ 'This is paragraph 7 - almost done with our scrollable content demo.',
+ 'This is paragraph 8 - the last paragraph in our scrollable demo.'
+ ];
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/date-picker-demo/date-picker-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/date-picker-demo/date-picker-demo.component.scss
new file mode 100644
index 0000000..66e2046
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/date-picker-demo/date-picker-demo.component.scss
@@ -0,0 +1,152 @@
+.demo-container {
+ padding: 2rem;
+ max-width: 1200px;
+ margin: 0 auto;
+
+ h2 {
+ margin-bottom: 1.5rem;
+ font-size: 1.875rem;
+ }
+}
+
+.demo-section {
+ margin-bottom: 3rem;
+
+ h3 {
+ margin-bottom: 1rem;
+ font-size: 1.25rem;
+ border-bottom: 1px solid #e5e7eb;
+ padding-bottom: 0.5rem;
+ }
+}
+
+.demo-row {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 1.5rem;
+}
+
+.demo-item {
+ h4 {
+ margin-bottom: 0.75rem;
+ font-size: 1rem;
+ font-weight: 500;
+ color: #6b7280;
+ }
+}
+
+.demo-interactive {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1.5rem;
+ padding: 1.5rem;
+ border: 1px solid #e5e7eb;
+ border-radius: 0.5rem;
+ background: #f9fafb;
+}
+
+.demo-output {
+ h4 {
+ margin-bottom: 0.5rem;
+ font-size: 1rem;
+ }
+
+ p {
+ color: #6b7280;
+ margin-bottom: 0.25rem;
+ font-size: 0.875rem;
+ }
+}
+
+.demo-form {
+ padding: 1.5rem;
+ border: 1px solid #e5e7eb;
+ border-radius: 0.5rem;
+ background: #ffffff;
+}
+
+.form-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1.5rem;
+ margin-bottom: 1.5rem;
+}
+
+.form-actions {
+ display: flex;
+ gap: 0.75rem;
+ margin-bottom: 1.5rem;
+}
+
+.submit-btn, .reset-btn {
+ padding: 0.5rem 1rem;
+ border-radius: 0.375rem;
+ font-size: 0.875rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.submit-btn {
+ background: #3b82f6;
+ color: white;
+ border: 1px solid #3b82f6;
+
+ &:hover:not(:disabled) {
+ background: #2563eb;
+ border-color: #2563eb;
+ }
+
+ &:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+}
+
+.reset-btn {
+ background: transparent;
+ color: #6b7280;
+ border: 1px solid #d1d5db;
+
+ &:hover {
+ background: #f3f4f6;
+ color: #374151;
+ }
+}
+
+.form-status {
+ background: #f3f4f6;
+ padding: 0.75rem;
+ border-radius: 0.375rem;
+
+ p {
+ margin-bottom: 0.5rem;
+ font-size: 0.875rem;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ pre {
+ background: #e5e7eb;
+ padding: 0.5rem;
+ border-radius: 0.25rem;
+ font-size: 0.75rem;
+ overflow-x: auto;
+ }
+}
+
+@media (max-width: 768px) {
+ .demo-row {
+ grid-template-columns: 1fr;
+ }
+
+ .demo-interactive {
+ grid-template-columns: 1fr;
+ }
+
+ .form-row {
+ grid-template-columns: 1fr;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/date-picker-demo/date-picker-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/date-picker-demo/date-picker-demo.component.ts
new file mode 100644
index 0000000..c86e8a4
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/date-picker-demo/date-picker-demo.component.ts
@@ -0,0 +1,283 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { DatePickerComponent } from '../../../../../ui-essentials/src/lib/components/forms/date-picker/date-picker.component';
+
+@Component({
+ selector: 'ui-date-picker-demo',
+ standalone: true,
+ imports: [CommonModule, FormsModule, DatePickerComponent],
+ template: `
+
+
Date Picker Demo
+
+
+
+ Sizes
+
+ @for (size of sizes; track size) {
+
+
{{ size | uppercase }}
+
+
+
+ }
+
+
+
+
+
+ Variants
+
+ @for (variant of variants; track variant) {
+
+
{{ variant | titlecase }}
+
+
+
+ }
+
+
+
+
+
+ States
+
+
+
Default
+
+
+
+
+
+
Error
+
+
+
+
+
+
Success
+
+
+
+
+
+
Warning
+
+
+
+
+
+
+
+
+ Features
+
+
+
Required Field
+
+
+
+
+
+
Disabled
+
+
+
+
+
+
Not Clearable
+
+
+
+
+
+
Date Range Limits
+
+
+
+
+
+
+
+
+ Interactive Example
+
+
+
+
+
+
Selected Date:
+
{{ interactiveValue ? formatDate(interactiveValue) : 'No date selected' }}
+
Change count: {{ changeCount }}
+
+
+
+
+
+
+
+ `,
+ styleUrl: './date-picker-demo.component.scss'
+})
+export class DatePickerDemoComponent {
+ sizes = ['sm', 'md', 'lg'] as const;
+ variants = ['outlined', 'filled', 'underlined'] as const;
+
+ sampleValues: Record = {
+ sm: null,
+ md: null,
+ lg: null
+ };
+
+ variantValues: Record = {
+ outlined: null,
+ filled: null,
+ underlined: null
+ };
+
+ stateValues: Record = {
+ default: null,
+ error: null,
+ success: new Date(),
+ warning: null
+ };
+
+ featureValues: Record = {
+ required: null,
+ disabled: new Date(),
+ notClearable: null,
+ limited: null
+ };
+
+ interactiveValue: Date | null = null;
+ changeCount = 0;
+
+ formValues = {
+ startDate: null as Date | null,
+ endDate: null as Date | null
+ };
+
+ // Date limits for demo
+ minDate = new Date(2024, 0, 1); // January 1, 2024
+ maxDate = new Date(2024, 11, 31); // December 31, 2024
+
+ onDateChange(date: Date | null): void {
+ this.changeCount++;
+ console.log('Date changed:', date);
+ }
+
+ onSubmit(): void {
+ console.log('Form submitted:', this.formValues);
+ alert('Form submitted! Check console for values.');
+ }
+
+ resetForm(form: any): void {
+ form.resetForm();
+ this.formValues = {
+ startDate: null,
+ endDate: null
+ };
+ }
+
+ formatDate(date: Date): string {
+ return date.toLocaleDateString('en-US', {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric'
+ });
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/demos.routes.ts b/projects/demo-ui-essentials/src/app/demos/demos.routes.ts
new file mode 100644
index 0000000..81281ad
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/demos.routes.ts
@@ -0,0 +1,224 @@
+import {
+ Component,
+ ViewEncapsulation,
+ Input
+} from '@angular/core';
+import { AvatarDemoComponent } from './avatar-demo/avatar-demo.component';
+import { ButtonDemoComponent } from './button-demo/button-demo.component';
+import { TableDemoComponent } from './table-demo/table-demo.component';
+import { BadgeDemoComponent } from './badge-demo/badge-demo.component';
+import { MenuDemoComponent } from './menu-demo/menu-demo.component';
+import { InputDemoComponent } from './input-demo/input-demo.component';
+import { LayoutDemoComponent } from './layout-demo/layout-demo.component';
+import { RadioDemoComponent } from './radio-demo/radio-demo.component';
+import { CheckboxDemoComponent } from './checkbox-demo/checkbox-demo.component';
+import { SearchDemoComponent } from './search-demo/search-demo.component';
+import { SwitchDemoComponent } from './switch-demo/switch-demo.component';
+import { ProgressDemoComponent } from './progress-demo/progress-demo.component';
+import { CardDemoComponent } from './card-demo/card-demo.component';
+import { ChipDemoComponent } from './chip-demo/chip-demo.component';
+import { AppbarDemoComponent } from './appbar-demo/appbar-demo.component';
+import { FontAwesomeDemoComponent } from './fontawesome-demo/fontawesome-demo.component';
+import { ImageContainerDemoComponent } from './image-container-demo/image-container-demo.component';
+import { CarouselDemoComponent } from './carousel-demo/carousel-demo.component';
+import { VideoPlayerDemoComponent } from "./video-player-demo/video-player-demo.component";
+import { ListDemoComponent } from "./list-demo/list-demo.component";
+import { ModalDemoComponent } from "./modal-demo/modal-demo.component";
+import { DrawerDemoComponent } from "./drawer-demo/drawer-demo.component";
+import { DatePickerDemoComponent } from './date-picker-demo/date-picker-demo.component';
+import { TimePickerDemoComponent } from './time-picker-demo/time-picker-demo.component';
+import { GridSystemDemoComponent } from './grid-system-demo/grid-system-demo.component';
+import { SpacerDemoComponent } from './spacer-demo/spacer-demo.component';
+import { ContainerDemoComponent } from './container-demo/container-demo.component';
+import { PaginationDemoComponent } from './pagination-demo/pagination-demo.component';
+import { SkeletonLoaderDemoComponent } from './skeleton-loader-demo/skeleton-loader-demo.component';
+import { EmptyStateDemoComponent } from './empty-state-demo/empty-state-demo.component';
+import { FileUploadDemoComponent } from './file-upload-demo/file-upload-demo.component';
+import { FormFieldDemoComponent } from './form-field-demo/form-field-demo.component';
+import { AutocompleteDemoComponent } from './autocomplete-demo/autocomplete-demo.component';
+import { BackdropDemoComponent } from './backdrop-demo/backdrop-demo.component';
+import { OverlayContainerDemoComponent } from './overlay-container-demo/overlay-container-demo.component';
+import { LoadingSpinnerDemoComponent } from './loading-spinner-demo/loading-spinner-demo.component';
+
+
+@Component({
+ selector: 'ui-demo-routes',
+ encapsulation: ViewEncapsulation.None,
+ standalone: true,
+ template: `
+
+ @switch (this.route) {
+ @case ("home") {
+ }
+
+ @case ("appbar") {
+
+ }
+
+ @case ("avatar") {
+
+ }
+
+ @case ("buttons") {
+
+ }
+
+ @case ("cards") {
+
+ }
+
+ @case ("chips") {
+
+ }
+
+ @case ("badge") {
+
+ }
+
+ @case ("fontawesome") {
+
+ }
+
+ @case ("input") {
+
+ }
+ @case ("layout") {
+
+ }
+ @case ("radio") {
+
+ }
+
+ @case ("checkbox") {
+
+ }
+
+ @case ("search") {
+
+ }
+
+ @case ("switch") {
+
+ }
+
+ @case ("progress") {
+
+ }
+
+
+ @case ("lists") {
+
+ }
+ @case ("menu") {
+
+ }
+
+ @case ("table") {
+
+ }
+
+ @case ("image-container") {
+
+ }
+
+ @case ("carousel") {
+
+ }
+
+ @case ("video-player") {
+
+ }
+
+ @case ("modal") {
+
+ }
+
+ @case ("drawer") {
+
+ }
+
+ @case ("date-picker") {
+
+ }
+
+ @case ("time-picker") {
+
+ }
+
+ @case ("grid-system") {
+
+ }
+
+ @case ("spacer") {
+
+ }
+
+ @case ("container") {
+
+ }
+
+ @case ("pagination") {
+
+ }
+
+ @case ("skeleton-loader") {
+
+ }
+
+ @case ("empty-state") {
+
+ }
+
+ @case ("file-upload") {
+
+ }
+
+ @case ("form-field") {
+
+ }
+
+ @case ("autocomplete") {
+
+ }
+
+ @case ("backdrop") {
+
+ }
+
+ @case ("overlay-container") {
+
+ }
+
+ @case ("loading-spinner") {
+
+ }
+
+
+ }
+ `,
+ imports: [AvatarDemoComponent, ButtonDemoComponent, CardDemoComponent,
+ ChipDemoComponent, TableDemoComponent, BadgeDemoComponent,
+ MenuDemoComponent, InputDemoComponent, InputDemoComponent,
+ LayoutDemoComponent, RadioDemoComponent, CheckboxDemoComponent,
+ SearchDemoComponent, SwitchDemoComponent, ProgressDemoComponent,
+ AppbarDemoComponent, FontAwesomeDemoComponent, ImageContainerDemoComponent,
+ CarouselDemoComponent, VideoPlayerDemoComponent, ListDemoComponent,
+ ModalDemoComponent, DrawerDemoComponent, DatePickerDemoComponent, TimePickerDemoComponent,
+ GridSystemDemoComponent, SpacerDemoComponent, ContainerDemoComponent, PaginationDemoComponent,
+ SkeletonLoaderDemoComponent, EmptyStateDemoComponent, FileUploadDemoComponent, FormFieldDemoComponent,
+ AutocompleteDemoComponent, BackdropDemoComponent, OverlayContainerDemoComponent, LoadingSpinnerDemoComponent]
+})
+
+
+export class DemoRoutes {
+
+ @Input() route: string;
+
+ constructor() {
+ this.route = "home"
+ }
+
+ ngOnInit() {
+ }
+
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/drawer-demo/drawer-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/drawer-demo/drawer-demo.component.scss
new file mode 100644
index 0000000..1f0856d
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/drawer-demo/drawer-demo.component.scss
@@ -0,0 +1,158 @@
+@use '../../../../../shared-ui/src/styles/semantic' as *;
+
+.demo-container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: $semantic-spacing-layout-lg;
+
+ h2 {
+ font-size: $semantic-typography-heading-h2-size;
+ font-weight: $semantic-typography-font-weight-semibold;
+ color: $semantic-color-on-surface;
+ margin-bottom: $semantic-spacing-layout-lg;
+ border-bottom: 2px solid $semantic-color-outline;
+ padding-bottom: $semantic-spacing-component-md;
+ }
+}
+
+.demo-section {
+ margin-bottom: $semantic-spacing-layout-xl;
+
+ h3 {
+ font-size: $semantic-typography-heading-h3-size;
+ font-weight: $semantic-typography-font-weight-medium;
+ color: $semantic-color-on-surface;
+ margin-bottom: $semantic-spacing-layout-md;
+ }
+}
+
+.demo-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: $semantic-spacing-component-md;
+ margin-bottom: $semantic-spacing-layout-sm;
+}
+
+.config-panel {
+ background: $semantic-color-surface-secondary;
+ border: 1px solid $semantic-color-outline;
+ border-radius: $semantic-border-radius-lg;
+ padding: $semantic-spacing-component-lg;
+
+ .config-row {
+ display: flex;
+ align-items: center;
+ gap: $semantic-spacing-component-md;
+ margin-bottom: $semantic-spacing-component-md;
+ flex-wrap: wrap;
+
+ &:last-child {
+ margin-bottom: 0;
+ margin-top: $semantic-spacing-component-lg;
+ }
+
+ label {
+ font-size: $semantic-typography-font-size-sm;
+ font-weight: $semantic-typography-font-weight-medium;
+ color: $semantic-color-on-surface;
+ display: flex;
+ align-items: center;
+ gap: $semantic-spacing-component-xs;
+ white-space: nowrap;
+ }
+
+ select {
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ border: 1px solid $semantic-color-outline;
+ border-radius: $semantic-border-radius-sm;
+ background: $semantic-color-surface-primary;
+ color: $semantic-color-on-surface;
+ font-size: $semantic-typography-font-size-sm;
+ min-width: 120px;
+ }
+
+ input[type="checkbox"] {
+ margin: 0;
+ accent-color: $semantic-color-primary;
+ }
+ }
+}
+
+.event-log {
+ background: $semantic-color-surface-secondary;
+ border: 1px solid $semantic-color-outline;
+ border-radius: $semantic-border-radius-md;
+ padding: $semantic-spacing-component-md;
+ max-height: 200px;
+ overflow-y: auto;
+ margin-bottom: $semantic-spacing-component-md;
+ font-family: 'Courier New', monospace;
+ font-size: $semantic-typography-font-size-sm;
+
+ .event-item {
+ padding: $semantic-spacing-component-xs 0;
+ color: $semantic-color-on-surface-variant;
+ border-bottom: 1px solid $semantic-color-outline-variant;
+
+ &:last-child {
+ border-bottom: none;
+ }
+ }
+}
+
+.nav-item {
+ display: flex;
+ align-items: center;
+ gap: $semantic-spacing-component-sm;
+ padding: $semantic-spacing-component-md $semantic-spacing-component-lg;
+ border-radius: $semantic-border-radius-sm;
+ cursor: pointer;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
+ color: $semantic-color-on-surface;
+ margin-bottom: $semantic-spacing-component-xs;
+
+ &:hover {
+ background: $semantic-color-surface-variant;
+ color: $semantic-color-primary;
+ }
+
+ &:active {
+ background: $semantic-color-surface-container;
+ }
+
+ fa-icon {
+ width: 20px;
+ color: inherit;
+ }
+
+ span {
+ font-size: $semantic-typography-font-size-md;
+ font-weight: $semantic-typography-font-weight-medium;
+ }
+}
+
+// Responsive design
+@media (max-width: 768px) {
+ .demo-container {
+ padding: $semantic-spacing-component-lg;
+ }
+
+ .demo-row {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .config-panel .config-row {
+ flex-direction: column;
+ align-items: stretch;
+ gap: $semantic-spacing-component-sm;
+
+ label {
+ justify-content: space-between;
+ }
+
+ select {
+ min-width: auto;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/drawer-demo/drawer-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/drawer-demo/drawer-demo.component.ts
new file mode 100644
index 0000000..f7da689
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/drawer-demo/drawer-demo.component.ts
@@ -0,0 +1,484 @@
+import { Component, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { faBars, faUser, faCog, faHome, faChartLine, faEnvelope, faQuestionCircle } from '@fortawesome/free-solid-svg-icons';
+import { ButtonComponent, DrawerComponent } from "../../../../../ui-essentials/src/public-api";
+
+@Component({
+ selector: 'ui-drawer-demo',
+ standalone: true,
+ imports: [CommonModule, FormsModule, FontAwesomeModule, ButtonComponent, DrawerComponent],
+ template: `
+
+
Drawer Demo
+
+
+
+ Basic Usage
+
+
+
+ Open Basic Drawer
+
+
+
+
+
+
+ Positions
+
+ Left Drawer
+ Right Drawer
+ Top Drawer
+ Bottom Drawer
+
+
+
+
+
+ Sizes
+
+ Small Drawer
+ Medium Drawer
+ Large Drawer
+ XL Drawer
+
+
+
+
+
+ Configuration Options
+
+ No Header
+ With Footer
+ No Padding
+ Persistent
+ Not Closable
+
+
+
+
+
+ Interactive Features
+
+ Loading Drawer
+ Scrollable Content
+ Navigation Drawer
+
+
+
+
+
+
+
+
+ Event Log
+
+ @for (event of eventLog; track $index) {
+
{{ event }}
+ }
+ @if (eventLog.length === 0) {
+
No events yet...
+ }
+
+ Clear Log
+
+
+
+
+
+ This is a basic drawer with default settings.
+ You can close it by clicking the X button, pressing Escape, or clicking outside the drawer.
+
+
+
+
+ This drawer slides in from the left side.
+ Default position for navigation menus and sidebars.
+
+
+
+ This drawer slides in from the right side.
+ Great for secondary actions, filters, or settings panels.
+
+
+
+ This drawer slides down from the top.
+ Perfect for notifications, announcements, or quick access panels.
+
+
+
+ This drawer slides up from the bottom.
+ Commonly used for mobile interfaces and action sheets.
+
+
+
+
+ This is a small drawer (280px width).
+ Perfect for simple navigation or minimal content.
+
+
+
+ This is a medium drawer (360px width) - the default size.
+ Good balance between content space and screen real estate.
+
+
+
+ This is a large drawer (480px width).
+ Ideal for detailed content, forms, or complex interfaces.
+
+
+
+ This is an extra large drawer (640px width).
+ Use for very detailed interfaces or when you need more space.
+
+
+
+
+
+
+
Drawer without header
+
This drawer doesn't have a header section.
+
Close
+
+
+
+
+ This drawer has a footer with buttons.
+ The footer stays at the bottom even if the content is scrollable.
+
+
+
+ Help
+
+
+ Cancel
+ Save
+
+
+
+
+
+
+
This content spans the full width with no padding constraints.
+
+
+
Content below can still have its own padding.
+
+
+
+
+ This is a persistent drawer that doesn't have a backdrop.
+ It stays open until explicitly closed and doesn't block interaction with the rest of the page.
+ Perfect for navigation menus that should remain visible.
+ Close Drawer
+
+
+
+ This drawer cannot be closed by clicking the X, pressing Escape, or clicking the backdrop.
+ You must use the Close button below.
+
+
+ Close
+
+
+
+
+
+
+
+
+
+
+
Long Content
+ @for (item of longContent; track $index) {
+
{{ item }}
+ }
+
+
+
+
+
+
+
+ Home
+
+
+
+ Dashboard
+
+
+
+ Profile
+
+
+
+ Messages
+
+
+
+ Settings
+
+
+
+
+
+
+ Help
+
+
+
+
+
+
+ This drawer is configured based on your selections:
+
+ Size: {{ customConfig.size }}
+ Position: {{ customConfig.position }}
+ Closable: {{ customConfig.closable ? 'Yes' : 'No' }}
+ Backdrop Closable: {{ customConfig.backdropClosable ? 'Yes' : 'No' }}
+ Show Header: {{ customConfig.showHeader ? 'Yes' : 'No' }}
+ Show Footer: {{ customConfig.showFooter ? 'Yes' : 'No' }}
+ Persistent: {{ customConfig.persistent ? 'Yes' : 'No' }}
+
+
+ @if (customConfig.showFooter) {
+
+ Close
+
+ }
+
+ `,
+ styleUrl: './drawer-demo.component.scss'
+})
+export class DrawerDemoComponent {
+ // Icons
+ readonly faBars = faBars;
+ readonly faUser = faUser;
+ readonly faCog = faCog;
+ readonly faHome = faHome;
+ readonly faChartLine = faChartLine;
+ readonly faEnvelope = faEnvelope;
+ readonly faQuestionCircle = faQuestionCircle;
+
+ // Drawer states
+ basicDrawer = signal(false);
+
+ leftDrawer = signal(false);
+ rightDrawer = signal(false);
+ topDrawer = signal(false);
+ bottomDrawer = signal(false);
+
+ smallDrawer = signal(false);
+ mediumDrawer = signal(false);
+ largeDrawer = signal(false);
+ xlDrawer = signal(false);
+
+ noHeaderDrawer = signal(false);
+ withFooterDrawer = signal(false);
+ noPaddingDrawer = signal(false);
+ persistentDrawer = signal(false);
+ notClosableDrawer = signal(false);
+
+ loadingDrawer = signal(false);
+ scrollableDrawer = signal(false);
+ navigationDrawer = signal(false);
+ customDrawer = signal(false);
+
+ // Demo data
+ eventLog: string[] = [];
+
+ customConfig = {
+ size: 'md' as any,
+ position: 'left' as any,
+ closable: true,
+ backdropClosable: true,
+ showHeader: true,
+ showFooter: false,
+ persistent: false
+ };
+
+ longContent = Array.from({ length: 50 }, (_, i) =>
+ `This is paragraph ${i + 1}. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
+ quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.`
+ );
+
+ // Simulate loading
+ constructor() {
+ setTimeout(() => {
+ if (this.loadingDrawer()) {
+ this.loadingDrawer.set(false);
+ this.logEvent('Loading completed');
+ }
+ }, 3000);
+ }
+
+ logEvent(event: string): void {
+ const timestamp = new Date().toLocaleTimeString();
+ this.eventLog.unshift(`[${timestamp}] ${event}`);
+ if (this.eventLog.length > 10) {
+ this.eventLog = this.eventLog.slice(0, 10);
+ }
+ }
+
+ clearEventLog(): void {
+ this.eventLog = [];
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/empty-state-demo/empty-state-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/empty-state-demo/empty-state-demo.component.scss
new file mode 100644
index 0000000..882f780
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/empty-state-demo/empty-state-demo.component.scss
@@ -0,0 +1,93 @@
+@use '../../../../../shared-ui/src/styles/semantic/index' as *;
+
+.demo-container {
+ padding: $semantic-spacing-layout-md;
+ max-width: 1200px;
+ margin: 0 auto;
+
+ h2 {
+ margin-bottom: $semantic-spacing-layout-lg;
+ color: $semantic-color-text-primary;
+ font-size: $base-typography-font-size-2xl;
+ font-weight: $base-typography-font-weight-bold;
+ }
+}
+
+.demo-section {
+ margin-bottom: $semantic-spacing-layout-xl;
+
+ h3 {
+ margin-bottom: $semantic-spacing-layout-md;
+ color: $semantic-color-text-primary;
+ font-size: $base-typography-font-size-lg;
+ font-weight: $base-typography-font-weight-semibold;
+ border-bottom: 1px solid $semantic-color-border-subtle;
+ padding-bottom: $semantic-spacing-content-line-normal;
+ }
+}
+
+.demo-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: $semantic-spacing-layout-md;
+ align-items: start;
+
+ ui-empty-state {
+ width: 100%;
+ height: auto;
+ }
+}
+
+.demo-label {
+ margin: $semantic-spacing-content-line-normal 0 0 0;
+ text-align: center;
+ font-size: $base-typography-font-size-sm;
+ color: $semantic-color-text-secondary;
+ font-weight: $base-typography-font-weight-medium;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.demo-output {
+ margin-top: $semantic-spacing-content-heading;
+ padding: $semantic-spacing-component-md;
+ background: $semantic-color-surface-secondary;
+ border: 1px solid $semantic-color-border-subtle;
+ border-radius: $semantic-border-radius-md;
+
+ p {
+ margin: 0 0 $semantic-spacing-content-line-tight 0;
+ font-size: $base-typography-font-size-sm;
+ color: $semantic-color-text-secondary;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ strong {
+ color: $semantic-color-text-primary;
+ }
+ }
+}
+
+// Responsive Design
+@media (max-width: 1024px) {
+ .demo-grid {
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: $semantic-spacing-layout-sm;
+ }
+}
+
+@media (max-width: 768px) {
+ .demo-container {
+ padding: $semantic-spacing-layout-sm;
+ }
+
+ .demo-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .demo-section {
+ margin-bottom: $semantic-spacing-layout-lg;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/empty-state-demo/empty-state-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/empty-state-demo/empty-state-demo.component.ts
new file mode 100644
index 0000000..db0839b
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/empty-state-demo/empty-state-demo.component.ts
@@ -0,0 +1,165 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { EmptyStateComponent } from '../../../../../ui-essentials/src/lib/components/feedback';
+
+@Component({
+ selector: 'ui-empty-state-demo',
+ standalone: true,
+ imports: [CommonModule, EmptyStateComponent],
+ template: `
+
+
Empty State Demo
+
+
+
+ Sizes
+
+ @for (size of sizes; track size) {
+
+
+
{{ size }} size
+ }
+
+
+
+
+
+ Variants
+
+
+
+
Default
+
+
+
+
Search
+
+
+
+
Error
+
+
+
+
Loading
+
+
+
+
Success
+
+
+
+
+
+ States
+
+
+
+
Normal
+
+
+
+
Disabled
+
+
+
+
No Action
+
+
+
+
+
+ Custom Content
+
+
+
+
+ This is custom content projected into the empty state component.
+ You can add any HTML or Angular components here.
+
+
+
+
With Content Projection
+
+
+
+
+
+ Interactive Example
+
+
+
+
Action clicked: {{ lastAction || 'None' }}
+
Click count: {{ clickCount }}
+
+
+
+ `,
+ styleUrl: './empty-state-demo.component.scss'
+})
+export class EmptyStateDemoComponent {
+ sizes = ['sm', 'md', 'lg'] as const;
+ variants = ['default', 'search', 'error', 'loading', 'success'] as const;
+ clickCount = 0;
+ lastAction: string | null = null;
+
+ handleDemoClick(action: string): void {
+ this.clickCount++;
+ this.lastAction = action;
+ console.log('Empty state action clicked:', action);
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/file-upload-demo/file-upload-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/file-upload-demo/file-upload-demo.component.scss
new file mode 100644
index 0000000..6378002
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/file-upload-demo/file-upload-demo.component.scss
@@ -0,0 +1,160 @@
+@use '../../../../../shared-ui/src/styles/semantic/index' as *;
+
+.demo-container {
+ padding: $semantic-spacing-layout-md;
+ max-width: 1200px;
+ margin: 0 auto;
+
+ h2 {
+ font-size: $semantic-typography-heading-h2-size;
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-content-heading;
+ font-weight: $semantic-typography-font-weight-bold;
+ }
+
+ h3 {
+ font-size: $semantic-typography-heading-h3-size;
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-content-paragraph;
+ font-weight: $semantic-typography-font-weight-semibold;
+ }
+}
+
+.demo-section {
+ margin-bottom: $semantic-spacing-layout-lg;
+ padding: $semantic-spacing-component-lg;
+ background: $semantic-color-surface-primary;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ border-radius: $semantic-border-radius-lg;
+ box-shadow: $semantic-shadow-elevation-1;
+}
+
+.demo-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: $semantic-spacing-layout-md;
+ margin-bottom: $semantic-spacing-content-heading;
+}
+
+.demo-item {
+ padding: $semantic-spacing-component-md;
+ background: $semantic-color-surface-secondary;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-md;
+
+ h4 {
+ font-size: $semantic-typography-heading-h4-size;
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-content-list-item;
+ font-weight: $semantic-typography-font-weight-medium;
+ }
+
+ p {
+ font-size: $semantic-typography-font-size-sm;
+ color: $semantic-color-text-secondary;
+ margin-bottom: $semantic-spacing-content-paragraph;
+ }
+}
+
+.demo-actions {
+ display: flex;
+ gap: $semantic-spacing-content-paragraph;
+ align-items: center;
+ margin-bottom: $semantic-spacing-content-heading;
+ padding: $semantic-spacing-component-md;
+ background: $semantic-color-surface-variant;
+ border-radius: $semantic-border-radius-md;
+}
+
+.demo-button {
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ background: $semantic-color-primary;
+ color: $semantic-color-on-primary;
+ border: none;
+ border-radius: $semantic-border-radius-sm;
+ font-size: $semantic-typography-font-size-sm;
+ font-weight: $semantic-typography-font-weight-medium;
+ cursor: pointer;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease-in-out;
+
+ &:hover {
+ background: $semantic-color-primary-hover;
+ box-shadow: $semantic-shadow-elevation-2;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+
+ &--secondary {
+ background: $semantic-color-surface-primary;
+ color: $semantic-color-text-primary;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+
+ &:hover {
+ background: $semantic-color-surface-secondary;
+ }
+ }
+}
+
+.demo-info {
+ font-size: $semantic-typography-font-size-sm;
+ color: $semantic-color-text-tertiary;
+ margin-top: $semantic-spacing-content-list-item;
+}
+
+.file-list-demo {
+ margin-top: $semantic-spacing-content-paragraph;
+ padding: $semantic-spacing-component-md;
+ background: $semantic-color-surface-secondary;
+ border-radius: $semantic-border-radius-md;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+
+ h5 {
+ font-size: $semantic-typography-font-size-lg;
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-content-list-item;
+ font-weight: $semantic-typography-font-weight-semibold;
+ }
+
+ .file-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: $semantic-spacing-component-xs;
+ background: $semantic-color-surface-primary;
+ border-radius: $semantic-border-radius-sm;
+ margin-bottom: $semantic-spacing-content-line-normal;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ .file-info {
+ font-size: $semantic-typography-font-size-sm;
+ color: $semantic-color-text-primary;
+ }
+
+ .file-size {
+ font-size: $semantic-typography-font-size-xs;
+ color: $semantic-color-text-tertiary;
+ }
+ }
+}
+
+.code-demo {
+ background: $semantic-color-surface-variant;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ border-radius: $semantic-border-radius-md;
+ padding: $semantic-spacing-component-md;
+ margin-top: $semantic-spacing-content-paragraph;
+
+ pre {
+ font-family: 'JetBrains Mono', 'Consolas', monospace;
+ font-size: $semantic-typography-font-size-xs;
+ color: $semantic-color-text-secondary;
+ overflow-x: auto;
+ margin: 0;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/file-upload-demo/file-upload-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/file-upload-demo/file-upload-demo.component.ts
new file mode 100644
index 0000000..939cf2f
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/file-upload-demo/file-upload-demo.component.ts
@@ -0,0 +1,399 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
+import { FileUploadComponent, UploadedFile } from '../../../../../../projects/ui-essentials/src/lib/components/forms/file-upload';
+
+@Component({
+ selector: 'ui-file-upload-demo',
+ standalone: true,
+ imports: [CommonModule, ReactiveFormsModule, FileUploadComponent],
+ template: `
+
+
File Upload Demo
+
+
+
+ Sizes
+
+ @for (size of sizes; track size) {
+
+
{{ size.toUpperCase() }} Size
+
File upload with {{ size }} sizing.
+
+
+
+ }
+
+
+
+
+
+ Variants
+
+ @for (variant of variants; track variant) {
+
+
{{ variant | titlecase }} Variant
+
File upload with {{ variant }} styling.
+
+
+
+ }
+
+
+
+
+
+ File Type Restrictions
+
+
+
Images Only
+
Accepts only image files (JPEG, PNG, GIF, WebP).
+
+
+
+
+
+
Documents Only
+
Accepts PDF, Word, and Excel documents.
+
+
+
+
+
+
+
+
+ Size & File Limits
+
+
+
Size Limit (5MB)
+
Maximum file size of 5MB per file.
+
+
+
+
+
+
File Count Limit
+
Maximum of 3 files can be selected.
+
+
+
+
+
+
Single File
+
Only one file can be selected.
+
+
+
+
+
+
+
+
+ States
+
+
+
Disabled State
+
File upload in disabled state.
+
+
+
+
+
+
Error State
+
File upload showing error state.
+
+
+
+
+
+
Success State
+
File upload showing success state.
+
+
+
+
+
+
+
+
+ Reactive Form Integration
+
+
Form with Validation
+
File upload integrated with Angular reactive forms and validation.
+
+
+
+ @if (submittedFiles.length > 0) {
+
+
Submitted Files:
+ @for (file of submittedFiles; track file.id) {
+
+
+
{{ file.name }}
+
{{ formatFileSize(file.size) }}
+
+
+ }
+
+ }
+
+
+
+
+
+ Event Handling
+
+
Event Monitoring
+
Monitor file upload events in real-time.
+
+
+
+
+
+
Recent Events:
+ @if (recentEvents.length === 0) {
+
No events yet. Add or remove files to see events.
+ } @else {
+ @for (event of recentEvents.slice(-5).reverse(); track $index) {
+
+
+
{{ event.type }}: {{ event.fileName }}
+
{{ event.timestamp | date:'medium' }}
+
+
+ }
+ }
+
+
+
+
+
+
+
+ `,
+ styleUrl: './file-upload-demo.component.scss'
+})
+export class FileUploadDemoComponent {
+ sizes = ['sm', 'md', 'lg'] as const;
+ variants = ['outlined', 'filled', 'underlined'] as const;
+
+ uploadForm = new FormGroup({
+ files: new FormControl([], [Validators.required])
+ });
+
+ submittedFiles: UploadedFile[] = [];
+ recentEvents: Array<{type: string, fileName: string, timestamp: Date}> = [];
+
+ readonly codeExample = `import { FileUploadComponent, UploadedFile } from 'ui-essentials';
+
+// Basic usage
+
+
+
+// With reactive forms
+
+
+
+// Event handling
+onFilesSelected(files: UploadedFile[]): void {
+ console.log('Selected files:', files);
+ files.forEach(file => {
+ console.log(file.name, file.size, file.type);
+ });
+}`;
+
+ onFilesSelected(context: string, files: UploadedFile[]): void {
+ console.log(`Files selected in ${context}:`, files);
+ }
+
+ onFormFilesSelected(files: UploadedFile[]): void {
+ this.uploadForm.patchValue({ files });
+ this.uploadForm.get('files')?.markAsTouched();
+ }
+
+ onEventFilesSelected(files: UploadedFile[]): void {
+ this.addEvent('filesSelected', `${files.length} files total`);
+ }
+
+ onFileAdded(file: UploadedFile): void {
+ this.addEvent('fileAdded', file.name);
+ }
+
+ onFileRemoved(file: UploadedFile): void {
+ this.addEvent('fileRemoved', file.name);
+ }
+
+ private addEvent(type: string, fileName: string): void {
+ this.recentEvents.push({
+ type,
+ fileName,
+ timestamp: new Date()
+ });
+ }
+
+ onSubmit(): void {
+ if (this.uploadForm.valid) {
+ this.submittedFiles = this.uploadForm.get('files')?.value || [];
+ console.log('Form submitted with files:', this.submittedFiles);
+ }
+ }
+
+ resetForm(): void {
+ this.uploadForm.reset();
+ this.uploadForm.get('files')?.setValue([]);
+ this.submittedFiles = [];
+ }
+
+ getFormFieldState(fieldName: string): 'default' | 'error' | 'success' {
+ const control = this.uploadForm.get(fieldName);
+ if (control?.invalid && control?.touched) {
+ return 'error';
+ }
+ if (control?.valid && control?.value?.length > 0) {
+ return 'success';
+ }
+ return 'default';
+ }
+
+ getFormFieldError(fieldName: string): string {
+ const control = this.uploadForm.get(fieldName);
+ if (control?.invalid && control?.touched) {
+ if (control.errors?.['required']) {
+ return 'Please select at least one file';
+ }
+ }
+ return '';
+ }
+
+ formatFileSize(bytes: number): string {
+ if (bytes === 0) return '0 Bytes';
+ const k = 1024;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/fontawesome-demo/fontawesome-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/fontawesome-demo/fontawesome-demo.component.scss
new file mode 100644
index 0000000..e1fa710
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/fontawesome-demo/fontawesome-demo.component.scss
@@ -0,0 +1,522 @@
+@import "../../../../../shared-ui/src/styles/semantic";
+.demo-container {
+ padding: 2rem;
+ max-width: 1200px;
+ margin: 0 auto;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+}
+
+h2 {
+ color: #2c3e50;
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ border-bottom: 2px solid #6366f1;
+ padding-bottom: 0.5rem;
+}
+
+h3 {
+ color: #374151;
+ font-size: 1.5rem;
+ margin-bottom: 1rem;
+}
+
+h4 {
+ color: #6b7280;
+ font-size: 1.125rem;
+ margin-bottom: 0.75rem;
+}
+
+// Sections
+.demo-section {
+ border: 1px solid #e5e7eb;
+ border-radius: 8px;
+ padding: 1.5rem;
+ margin-bottom: 2rem;
+ background: #ffffff;
+}
+
+.intro-section {
+ border: 1px solid #e5e7eb;
+ border-radius: 8px;
+ padding: 1.5rem;
+ margin-bottom: 2rem;
+ background: #f8fafc;
+
+ p {
+ margin-bottom: 1rem;
+ color: #6b7280;
+ }
+}
+
+.info-box {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 1rem;
+ background: #dbeafe;
+ border-radius: 6px;
+ border-left: 4px solid #3b82f6;
+
+ .info-icon {
+ color: #3b82f6;
+ }
+
+ span {
+ color: #1f2937;
+ }
+}
+
+// Icon Grid
+.icon-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ gap: 1rem;
+ margin: 1rem 0;
+}
+
+.icon-example {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 1rem;
+ border: 1px solid #e5e7eb;
+ border-radius: 6px;
+ background: #ffffff;
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background: #f3f4f6;
+ border-color: #6366f1;
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ }
+
+ .icon-name {
+ font-weight: 600;
+ color: #1f2937;
+ margin: 0.5rem 0 0.25rem;
+ font-family: 'JetBrains Mono', 'Courier New', monospace;
+ }
+
+ .icon-usage {
+ font-size: 0.875rem;
+ color: #6b7280;
+ text-align: center;
+ margin-bottom: 0.5rem;
+ }
+
+ code {
+ font-size: 0.75rem;
+ background: #f3f4f6;
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ color: #6b7280;
+ font-family: 'JetBrains Mono', 'Courier New', monospace;
+ text-align: center;
+ }
+}
+
+// Size Demo
+.size-demo {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.size-row {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 0.5rem;
+ background: #f8fafc;
+ border-radius: 6px;
+
+ .size-label {
+ font-weight: 600;
+ min-width: 30px;
+ color: #1f2937;
+ }
+
+ fa-icon {
+ color: #6366f1;
+ min-width: 80px;
+ text-align: center;
+ }
+
+ code {
+ background: #ffffff;
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ color: #6b7280;
+ font-family: 'JetBrains Mono', 'Courier New', monospace;
+ font-size: 0.875rem;
+ }
+}
+
+// Styling Demo
+.styling-demo {
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+}
+
+.style-example {
+ padding: 1rem;
+ background: #f8fafc;
+ border-radius: 6px;
+
+ h4 {
+ margin-bottom: 1rem;
+ }
+}
+
+.color-row,
+.animation-row {
+ display: flex;
+ gap: 1rem;
+ align-items: center;
+ margin-bottom: 1rem;
+
+ fa-icon {
+ flex-shrink: 0;
+ }
+}
+
+// Use Cases
+.use-cases {
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+}
+
+.use-case {
+ padding: 1rem;
+ background: #f8fafc;
+ border-radius: 6px;
+}
+
+.nav-example {
+ display: flex;
+ gap: 2rem;
+ align-items: center;
+ flex-wrap: wrap;
+
+ fa-icon {
+ margin-right: 0.5rem;
+ color: #6366f1;
+ }
+}
+
+.action-buttons {
+ display: flex;
+ gap: 1rem;
+ flex-wrap: wrap;
+}
+
+.status-row {
+ display: flex;
+ gap: 2rem;
+ flex-wrap: wrap;
+}
+
+.status-item {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 1rem;
+ border-radius: 6px;
+
+ &.success {
+ background: #dcfce7;
+ color: #166534;
+
+ fa-icon {
+ color: #22c55e;
+ }
+ }
+
+ &.warning {
+ background: #fef3c7;
+ color: #92400e;
+
+ fa-icon {
+ color: #f59e0b;
+ }
+ }
+
+ &.error {
+ background: #fee2e2;
+ color: #991b1b;
+
+ fa-icon {
+ color: #ef4444;
+ }
+ }
+
+ &.info {
+ background: #dbeafe;
+ color: #1e40af;
+
+ fa-icon {
+ color: #3b82f6;
+ }
+ }
+}
+
+// Code Examples
+.code-examples {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.code-example {
+ padding: 1rem;
+ background: #f8fafc;
+ border-radius: 6px;
+
+ h4 {
+ margin-bottom: 0.5rem;
+ color: #6366f1;
+ }
+
+ pre {
+ margin: 0;
+ background: #ffffff;
+ padding: 1rem;
+ border-radius: 4px;
+ overflow-x: auto;
+ border: 1px solid #e5e7eb;
+
+ code {
+ font-family: 'JetBrains Mono', 'Courier New', monospace;
+ color: #1f2937;
+ font-size: 0.875rem;
+ line-height: 1.5;
+ }
+ }
+}
+
+// Selected Icon Details
+.selected-icon {
+ background: #ede9fe;
+ border-color: #6366f1;
+}
+
+.icon-details {
+ display: flex;
+ gap: 2rem;
+ margin-bottom: 1.5rem;
+ align-items: flex-start;
+
+ @media (max-width: 768px) {
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ gap: 1rem;
+ }
+}
+
+.icon-display {
+ flex-shrink: 0;
+ padding: 2rem;
+ background: #ffffff;
+ border-radius: 8px;
+ border: 2px solid #6366f1;
+
+ fa-icon {
+ color: #6366f1;
+ }
+}
+
+.icon-info {
+ flex: 1;
+
+ h4 {
+ color: #5b21b6;
+ font-size: 1.5rem;
+ margin-bottom: 1rem;
+ font-family: 'JetBrains Mono', 'Courier New', monospace;
+ }
+
+ p {
+ margin-bottom: 0.75rem;
+ color: #1f2937;
+ }
+}
+
+.copy-code {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 1rem;
+ background: #ffffff;
+ border-radius: 6px;
+ border: 1px solid #e5e7eb;
+
+ code {
+ flex: 1;
+ background: none;
+ color: #1f2937;
+ font-family: 'JetBrains Mono', 'Courier New', monospace;
+ font-size: 0.875rem;
+ }
+}
+
+// Interactive Demo
+.action-feedback {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+ padding: 1rem;
+ background: #dbeafe;
+ border-radius: 6px;
+ border-left: 4px solid #3b82f6;
+
+ fa-icon {
+ color: #3b82f6;
+ }
+
+ span {
+ color: #1f2937;
+ }
+}
+
+.interactive-buttons {
+ display: flex;
+ gap: 1rem;
+ flex-wrap: wrap;
+}
+
+// Best Practices
+.best-practices {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.practice-item {
+ display: flex;
+ gap: 1rem;
+ padding: 1rem;
+ background: #ffffff;
+ border-radius: 6px;
+ border: 1px solid #e5e7eb;
+
+ .practice-icon {
+ flex-shrink: 0;
+ margin-top: 0.25rem;
+
+ &.success {
+ color: #22c55e;
+ }
+
+ &.warning {
+ color: #f59e0b;
+ }
+ }
+
+ .practice-content {
+ flex: 1;
+
+ h4 {
+ color: #1f2937;
+ margin-bottom: 0.5rem;
+ }
+
+ p {
+ color: #6b7280;
+ margin-bottom: 0.75rem;
+ line-height: 1.5;
+ }
+
+ code {
+ background: #f3f4f6;
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ color: #374151;
+ font-family: 'JetBrains Mono', 'Courier New', monospace;
+ font-size: 0.875rem;
+ }
+ }
+}
+
+// Quick Reference
+.reference-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 1.5rem;
+}
+
+.reference-category {
+ padding: 1rem;
+ background: #ffffff;
+ border-radius: 6px;
+ border: 1px solid #e5e7eb;
+
+ h4 {
+ color: #6366f1;
+ margin-bottom: 1rem;
+ font-weight: 600;
+ }
+}
+
+.reference-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ font-size: 0.875rem;
+ line-height: 1.4;
+
+ code {
+ background: #f3f4f6;
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ color: #6366f1;
+ font-family: 'JetBrains Mono', 'Courier New', monospace;
+ font-weight: 600;
+ margin-right: 0.5rem;
+ }
+}
+
+// Responsive Design
+@media (max-width: 768px) {
+ .demo-container {
+ padding: 1rem;
+ }
+
+ .icon-grid {
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+ gap: 0.75rem;
+ }
+
+ .action-buttons,
+ .interactive-buttons {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .nav-example {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 1rem;
+ }
+
+ .status-row {
+ flex-direction: column;
+ gap: 1rem;
+ }
+
+ .size-row {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 0.5rem;
+ }
+
+ .color-row,
+ .animation-row {
+ justify-content: center;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/fontawesome-demo/fontawesome-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/fontawesome-demo/fontawesome-demo.component.ts
new file mode 100644
index 0000000..0ab0a5c
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/fontawesome-demo/fontawesome-demo.component.ts
@@ -0,0 +1,629 @@
+import { Component, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FaIconComponent } from '@fortawesome/angular-fontawesome';
+import { ButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons';
+import {
+ // Solid icons from shared-ui
+ faUser,
+ faHome,
+ faCog,
+ faSearch,
+ faPlus,
+ faEdit,
+ faTrash,
+ faSave,
+ faCancel,
+ faCheck,
+ faTimes,
+ faArrowLeft,
+ faArrowRight,
+ faChevronLeft,
+ faChevronRight,
+ faChevronUp,
+ faChevronDown,
+ faBars,
+ faEllipsisV,
+ faEye,
+ faEyeSlash,
+ faDownload,
+ faUpload,
+ faRefresh,
+ faSpinner,
+ faExclamationTriangle,
+ faInfoCircle,
+ faCheckCircle,
+ faTimesCircle,
+ faEnvelope
+} from '@fortawesome/free-solid-svg-icons';
+import {
+ // Regular icons from shared-ui
+ faHeart,
+ faBookmark,
+ faComment,
+ faThumbsUp
+} from '@fortawesome/free-regular-svg-icons';
+import {
+ // Brand icons from shared-ui
+ faGithub,
+ faTwitter,
+ faLinkedin,
+ faGoogle
+} from '@fortawesome/free-brands-svg-icons';
+
+@Component({
+ selector: 'ui-fontawesome-demo',
+ standalone: true,
+ imports: [
+ CommonModule,
+ FaIconComponent,
+ ButtonComponent
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
Font Awesome Icons Showcase
+
+
+
+ Font Awesome Integration
+ This demo showcases the Font Awesome icons available in the shared-ui library. Icons are pre-configured and ready to use across the application.
+
+
+ Available Categories: Solid, Regular, and Brand icons from Font Awesome
+
+
+
+
+
+ Basic Usage
+
+
+
+ faUser
+ fa-icon [icon]="faUser"
+
+
+
+ faHome
+ fa-icon [icon]="faHome"
+
+
+
+ faCog
+ fa-icon [icon]="faCog"
+
+
+
+
+
+
+ Icon Sizes
+
+
+ xs:
+
+ size="xs"
+
+
+ sm:
+
+ size="sm"
+
+
+ lg:
+
+ size="lg"
+
+
+ 2x:
+
+ size="2x"
+
+
+ 3x:
+
+ size="3x"
+
+
+
+
+
+
+ Solid Icons (free-solid-svg-icons)
+ Most commonly used icons for UI elements, actions, and navigation.
+
+
+
+ faUser
+ User profiles, accounts
+
+
+
+ faHome
+ Home navigation
+
+
+
+ faCog
+ Settings
+
+
+
+ faSearch
+ Search functionality
+
+
+
+ faPlus
+ Add new items
+
+
+
+ faEdit
+ Edit content
+
+
+
+ faTrash
+ Delete items
+
+
+
+ faSave
+ Save changes
+
+
+
+ faCheck
+ Confirm, success
+
+
+
+ faTimes
+ Close, dismiss
+
+
+
+ faArrowLeft
+ Back navigation
+
+
+
+ faArrowRight
+ Forward navigation
+
+
+
+ faDownload
+ Download files
+
+
+
+ faUpload
+ Upload files
+
+
+
+ faRefresh
+ Refresh content
+
+
+
+ faSpinner
+ Loading states
+
+
+
+
+
+
+ Regular Icons (free-regular-svg-icons)
+ Outlined versions of icons, great for secondary actions and states.
+
+
+
+ faHeart
+ Favorites, likes
+
+
+
+ faBookmark
+ Saved items
+
+
+
+ faComment
+ Comments
+
+
+
+ faThumbsUp
+ Approval
+
+
+
+
+
+
+ Brand Icons (free-brands-svg-icons)
+ Social media and company logos for external integrations.
+
+
+
+ faGithub
+ GitHub integration
+
+
+
+ faTwitter
+ Twitter sharing
+
+
+
+ faLinkedin
+ LinkedIn integration
+
+
+
+ faGoogle
+ Google services
+
+
+
+
+
+
+ Icon Styling & Transformations
+
+
+
Colors
+
+
+
+
+
+
+
style="color: #dc3545;"
+
+
+
+
Different Sizes
+
+
+
+
+
+
size="lg", size="2x", size="3x"
+
+
+
+
+
+
+ Common Use Cases
+
+
+
Navigation
+
+
Home
+
Profile
+
Settings
+
Search
+
+
+
+
+
Action Buttons
+
+ Add Item
+ Edit
+ Delete
+ Save
+
+
+
+
+
Status Indicators
+
+
+
+ Success
+
+
+
+ Warning
+
+
+
+ Error
+
+
+
+ Info
+
+
+
+
+
+
+
+
+ Implementation Examples
+
+
+
1. Import the Icon
+
import { faUser } from '@fortawesome/free-solid-svg-icons';
+
+
+
+
2. Add to Component
+
export class MyComponent {
+ faUser = faUser;
+ faHome = faHome;
+ faCog = faCog;
+}
+
+
+
+
3. Basic Icon Usage
+
<fa-icon [icon]="faUser"></fa-icon>
+<fa-icon [icon]="faHome" size="lg"></fa-icon>
+<fa-icon [icon]="faCog" size="2x"></fa-icon>
+
+
+
+
4. Styled Icons
+
<fa-icon
+ [icon]="faHeart"
+ size="2x"
+ style="color: #dc3545;">
+</fa-icon>
+
+
+
+
5. Icons in Buttons
+
<ui-button
+ variant="filled"
+ [icon]="faPlus"
+ iconPosition="left">
+ Add Item
+</ui-button>
+
+
+
+
6. Navigation with Icons
+
<nav class="nav-menu">
+ <a href="/home">
+ <fa-icon [icon]="faHome"></fa-icon> Home
+ </a>
+ <a href="/profile">
+ <fa-icon [icon]="faUser"></fa-icon> Profile
+ </a>
+</nav>
+
+
+
+
7. Status Messages
+
<div class="status-success">
+ <fa-icon [icon]="faCheckCircle"></fa-icon>
+ Operation completed successfully!
+</div>
+
+<div class="status-error">
+ <fa-icon [icon]="faTimesCircle"></fa-icon>
+ Something went wrong.
+</div>
+
+
+
+
8. Loading States
+
<div class="loading-container">
+ <fa-icon [icon]="faSpinner"></fa-icon>
+ Loading...
+</div>
+
+<ui-button
+ [disabled]="isLoading"
+ [icon]="isLoading ? faSpinner : faSave">
+ {{ isLoading ? 'Saving...' : 'Save' }}
+</ui-button>
+
+
+
+
+
+
+ Best Practices
+
+
+
+
+
Import Only What You Need
+
Only import the specific icons you're using to keep bundle size small.
+
import { faUser, faHome } from '@fortawesome/free-solid-svg-icons';
+
+
+
+
+
+
+
Use Semantic Naming
+
Choose icons that clearly represent their function or meaning.
+
faTrash for delete, faEdit for edit, faPlus for add
+
+
+
+
+
+
+
Consistent Sizing
+
Use consistent icon sizes throughout your application.
+
size="sm" for inline text, size="lg" for buttons
+
+
+
+
+
+
+
Accessibility
+
Add aria-labels for screen readers when icons convey important information.
+
<fa-icon [icon]="faUser" aria-label="User profile"></fa-icon>
+
+
+
+
+
+
+
+ Quick Reference
+
+
+
Actions
+
+ faPlus - Add/Create
+ faEdit - Edit/Modify
+ faTrash - Delete/Remove
+ faSave - Save/Confirm
+ faCancel - Cancel/Dismiss
+ faRefresh - Reload/Update
+
+
+
+
+
Navigation
+
+ faHome - Home page
+ faArrowLeft - Go back
+ faArrowRight - Go forward
+ faChevronUp - Expand up
+ faChevronDown - Expand down
+ faBars - Menu toggle
+
+
+
+
+
Status
+
+ faCheckCircle - Success
+ faTimesCircle - Error
+ faExclamationTriangle - Warning
+ faInfoCircle - Information
+ faSpinner - Loading
+
+
+
+
+
User Interface
+
+ faUser - User/Profile
+ faCog - Settings
+ faSearch - Search
+ faEye - View/Show
+ faEyeSlash - Hide
+ faDownload - Download
+
+
+
+
+
+
+
+ Interactive Demo
+
+
+ Refresh Demo
+
+
+ Download Examples
+
+
+ View Source
+
+
+
+ @if (lastAction) {
+
+
+ {{ lastAction }}
+
+ }
+
+
+ `,
+ styleUrl: './fontawesome-demo.component.scss'
+})
+export class FontAwesomeDemoComponent {
+ lastAction: string = '';
+ isLoading: boolean = false;
+
+ // Export icons for template use
+ faInfoCircle = faInfoCircle;
+ faUser = faUser;
+ faHome = faHome;
+ faCog = faCog;
+ faSearch = faSearch;
+ faPlus = faPlus;
+ faEdit = faEdit;
+ faTrash = faTrash;
+ faSave = faSave;
+ faCancel = faCancel;
+ faCheck = faCheck;
+ faTimes = faTimes;
+ faArrowLeft = faArrowLeft;
+ faArrowRight = faArrowRight;
+ faChevronLeft = faChevronLeft;
+ faChevronRight = faChevronRight;
+ faChevronUp = faChevronUp;
+ faChevronDown = faChevronDown;
+ faBars = faBars;
+ faEllipsisV = faEllipsisV;
+ faEye = faEye;
+ faEyeSlash = faEyeSlash;
+ faDownload = faDownload;
+ faUpload = faUpload;
+ faRefresh = faRefresh;
+ faSpinner = faSpinner;
+ faExclamationTriangle = faExclamationTriangle;
+ faCheckCircle = faCheckCircle;
+ faTimesCircle = faTimesCircle;
+ faEnvelope = faEnvelope;
+
+ // Regular icons
+ faHeart = faHeart;
+ faBookmark = faBookmark;
+ faComment = faComment;
+ faThumbsUp = faThumbsUp;
+
+ // Brand icons
+ faGithub = faGithub;
+ faTwitter = faTwitter;
+ faLinkedin = faLinkedin;
+ faGoogle = faGoogle;
+
+ refreshDemo(): void {
+ this.lastAction = 'Demo refreshed - all selections cleared';
+ console.log('Demo refreshed');
+ }
+
+ downloadDemo(): void {
+ this.lastAction = 'Demo download initiated (simulated)';
+ console.log('Download demo');
+ }
+
+ viewSource(): void {
+ this.lastAction = 'Opening source code (simulated)';
+ console.log('View source');
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/form-field-demo/form-field-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/form-field-demo/form-field-demo.component.scss
new file mode 100644
index 0000000..f947309
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/form-field-demo/form-field-demo.component.scss
@@ -0,0 +1,228 @@
+@use '../../../../../shared-ui/src/styles/semantic/index' as *;
+
+.demo-container {
+ padding: $semantic-spacing-layout-md;
+ max-width: 1200px;
+ margin: 0 auto;
+
+ h2 {
+ font-size: $semantic-typography-heading-h2-size;
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-content-heading;
+ font-weight: $semantic-typography-font-weight-bold;
+ }
+
+ h3 {
+ font-size: $semantic-typography-heading-h3-size;
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-content-paragraph;
+ font-weight: $semantic-typography-font-weight-semibold;
+ }
+}
+
+.demo-section {
+ margin-bottom: $semantic-spacing-layout-lg;
+ padding: $semantic-spacing-component-lg;
+ background: $semantic-color-surface-primary;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ border-radius: $semantic-border-radius-lg;
+ box-shadow: $semantic-shadow-elevation-1;
+}
+
+.demo-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: $semantic-spacing-layout-md;
+ margin-bottom: $semantic-spacing-content-heading;
+}
+
+.demo-item {
+ padding: $semantic-spacing-component-md;
+ background: $semantic-color-surface-secondary;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-md;
+ overflow: hidden;
+ box-sizing: border-box;
+
+ h4 {
+ font-size: $semantic-typography-heading-h4-size;
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-content-list-item;
+ font-weight: $semantic-typography-font-weight-medium;
+ }
+
+ p {
+ font-size: $semantic-typography-font-size-sm;
+ color: $semantic-color-text-secondary;
+ margin-bottom: $semantic-spacing-content-paragraph;
+ }
+}
+
+.demo-actions {
+ display: flex;
+ gap: $semantic-spacing-content-paragraph;
+ align-items: center;
+ margin-bottom: $semantic-spacing-content-heading;
+ padding: $semantic-spacing-component-md;
+ background: $semantic-color-surface-variant;
+ border-radius: $semantic-border-radius-md;
+}
+
+.demo-button {
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ background: $semantic-color-primary;
+ color: $semantic-color-on-primary;
+ border: none;
+ border-radius: $semantic-border-radius-sm;
+ font-size: $semantic-typography-font-size-sm;
+ font-weight: $semantic-typography-font-weight-medium;
+ cursor: pointer;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease-in-out;
+
+ &:hover {
+ background: $semantic-color-primary-hover;
+ box-shadow: $semantic-shadow-elevation-2;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+
+ &--secondary {
+ background: $semantic-color-surface-primary;
+ color: $semantic-color-text-primary;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+
+ &:hover {
+ background: $semantic-color-surface-secondary;
+ }
+ }
+}
+
+.demo-info {
+ font-size: $semantic-typography-font-size-sm;
+ color: $semantic-color-text-tertiary;
+ margin-top: $semantic-spacing-content-list-item;
+}
+
+.form-demo {
+ display: grid;
+ gap: $semantic-spacing-content-heading;
+}
+
+.demo-input {
+ width: 100%;
+ box-sizing: border-box;
+ padding: $semantic-spacing-component-sm;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-md;
+ font-size: $semantic-typography-font-size-md;
+ color: $semantic-color-text-primary;
+ background: $semantic-color-surface-primary;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease-in-out;
+
+ &:focus {
+ outline: none;
+ border-color: $semantic-color-primary;
+ box-shadow: 0 0 0 2px $semantic-color-primary-container;
+ }
+
+ &:disabled {
+ background: $semantic-color-surface-disabled;
+ color: $semantic-color-text-disabled;
+ cursor: not-allowed;
+ }
+
+ &--error {
+ border-color: $semantic-color-error;
+ background: $semantic-color-error-container;
+ }
+
+ &--success {
+ border-color: $semantic-color-success;
+ background: $semantic-color-tertiary-container;
+ }
+}
+
+.demo-textarea {
+ @extend .demo-input;
+ min-height: 100px;
+ resize: vertical;
+}
+
+.demo-select {
+ @extend .demo-input;
+ cursor: pointer;
+}
+
+.demo-checkbox,
+.demo-radio {
+ display: flex;
+ align-items: center;
+ gap: $semantic-spacing-content-line-normal;
+ cursor: pointer;
+
+ input {
+ margin: 0;
+ }
+
+ label {
+ font-size: $semantic-typography-font-size-sm;
+ color: $semantic-color-text-primary;
+ cursor: pointer;
+ }
+}
+
+.validation-demo {
+ display: grid;
+ gap: $semantic-spacing-content-paragraph;
+ padding: $semantic-spacing-component-md;
+ background: $semantic-color-surface-variant;
+ border-radius: $semantic-border-radius-md;
+}
+
+.form-status {
+ padding: $semantic-spacing-component-sm;
+ background: $semantic-color-surface-primary;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ border-radius: $semantic-border-radius-sm;
+ font-family: 'JetBrains Mono', 'Consolas', monospace;
+ font-size: $semantic-typography-font-size-xs;
+}
+
+.code-demo {
+ background: $semantic-color-surface-variant;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ border-radius: $semantic-border-radius-md;
+ padding: $semantic-spacing-component-md;
+ margin-top: $semantic-spacing-content-paragraph;
+
+ pre {
+ font-family: 'JetBrains Mono', 'Consolas', monospace;
+ font-size: $semantic-typography-font-size-xs;
+ color: $semantic-color-text-secondary;
+ overflow-x: auto;
+ margin: 0;
+ }
+}
+
+.layout-demo {
+ display: grid;
+ gap: $semantic-spacing-content-heading;
+}
+
+// Ensure form fields are properly contained
+ui-form-field {
+ display: block;
+ width: 100%;
+ box-sizing: border-box;
+}
+
+// Responsive adjustments
+@media (max-width: $semantic-breakpoint-md - 1) {
+ .demo-grid {
+ grid-template-columns: 1fr;
+ gap: $semantic-spacing-layout-sm;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/form-field-demo/form-field-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/form-field-demo/form-field-demo.component.ts
new file mode 100644
index 0000000..b8fb391
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/form-field-demo/form-field-demo.component.ts
@@ -0,0 +1,669 @@
+import { Component, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormControl, FormGroup, ReactiveFormsModule, Validators, AbstractControl, ValidationErrors } from '@angular/forms';
+import { FormFieldComponent } from '../../../../../ui-essentials/src/lib/components/forms/form-field';
+
+@Component({
+ selector: 'ui-form-field-demo',
+ standalone: true,
+ imports: [CommonModule, ReactiveFormsModule, FormFieldComponent],
+ template: `
+
+
Form Field Demo
+
+
+
+ Sizes
+
+ @for (size of sizes; track size) {
+
+
{{ size.toUpperCase() }} Size
+
Form field with {{ size }} sizing.
+
+
+
+
+ }
+
+
+
+
+
+ Layout Options
+
+
+
Vertical Layout (Default)
+
Standard vertical layout with label above the control.
+
+
+
+
+
+
+
Horizontal Layout
+
Horizontal layout with label beside the control.
+
+
+
+
+
+
+
Compact Layout
+
Compact spacing for tighter layouts.
+
+
+
+
+
+
+
+
+
+ States
+
+
+
Default State
+
Normal state with no validation.
+
+
+
+
+
+
+
Error State
+
Error state with error message.
+
+
+
+
+
+
+
Success State
+
Success state with success message.
+
+
+
+
+
+
+
Warning State
+
Warning state with warning message.
+
+
+
+
+
+
+
Info State
+
Info state with informational message.
+
+
+
+
+
+
+
Disabled State
+
Disabled field with disabled styling.
+
+
+
+
+
+
+
+
+
+ Required and Optional Fields
+
+
+
Required Field
+
Field marked as required with asterisk.
+
+
+
+
+
+
+
Optional Field
+
Field marked as optional with (optional) text.
+
+
+
+
+
+
+
+
+
+ Input Types
+
+
+
Text Input
+
+
+
+
+
+
+
Email Input
+
+
+
+
+
+
+
Number Input
+
+
+
+
+
+
+
Textarea
+
+
+
+
+
+
+
Select
+
+
+ Choose an option
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+
+
+
+
+
+
+ Character Count
+
+
+
With Character Counter
+
Shows character count with maximum limit.
+
+
+
+
+
+
+
+
+
+ Hint Text
+
+
+
With Hint
+
Additional helpful information with icon.
+
+
+
+
+
+
+
+
+
+ Reactive Form Integration
+
+
Form with Validation
+
Form fields with Angular reactive form validation.
+
+
+
+
+
+ Validate All
+
+
+ Reset Form
+
+
+ Fill Sample Data
+
+
+
+
+
+
+
+
+
+
+ `,
+ styleUrl: './form-field-demo.component.scss'
+})
+export class FormFieldDemoComponent implements OnInit {
+ sizes = ['sm', 'md', 'lg'] as const;
+ characterCountText = '';
+
+ validationForm = new FormGroup({
+ name: new FormControl('', [Validators.required, Validators.minLength(2)]),
+ email: new FormControl('', [Validators.required, Validators.email]),
+ password: new FormControl('', [Validators.required, Validators.minLength(8)]),
+ age: new FormControl('', [Validators.min(18), Validators.max(100)]),
+ website: new FormControl('', [this.urlValidator]),
+ bio: new FormControl('', [Validators.maxLength(200)])
+ });
+
+ validationMessages = {
+ required: 'This field is required',
+ email: 'Please enter a valid email address',
+ minlength: 'This field is too short',
+ maxlength: 'This field is too long',
+ min: 'Value is too low',
+ max: 'Value is too high',
+ url: 'Please enter a valid URL',
+ custom: 'Custom validation error'
+ };
+
+ readonly codeExample = `import { FormFieldComponent } from 'ui-essentials';
+
+// Basic usage
+
+
+
+
+// With validation messages
+
+
+
+
+// Horizontal layout
+
+
+
+ Option 1
+
+
+
+// Custom states
+
+
+ `;
+
+ ngOnInit(): void {
+ // Watch form changes for demo purposes
+ this.validationForm.valueChanges.subscribe(() => {
+ // Update demo display if needed
+ });
+ }
+
+ updateCharacterCount(event: Event): void {
+ const input = event.target as HTMLInputElement;
+ this.characterCountText = input.value;
+ }
+
+ isFieldInvalid(fieldName: string): boolean {
+ const field = this.validationForm.get(fieldName);
+ return field ? field.invalid && (field.dirty || field.touched) : false;
+ }
+
+ isFieldValid(fieldName: string): boolean {
+ const field = this.validationForm.get(fieldName);
+ return field ? field.valid && field.value && (field.dirty || field.touched) : false;
+ }
+
+ getFieldValue(fieldName: string): any {
+ return this.validationForm.get(fieldName)?.value;
+ }
+
+ markAllTouched(): void {
+ this.validationForm.markAllAsTouched();
+ }
+
+ resetValidationForm(): void {
+ this.validationForm.reset();
+ Object.keys(this.validationForm.controls).forEach(key => {
+ this.validationForm.get(key)?.setErrors(null);
+ });
+ }
+
+ fillSampleData(): void {
+ this.validationForm.patchValue({
+ name: 'John Doe',
+ email: 'john.doe@example.com',
+ password: 'SecurePass123!',
+ age: "30",
+ website: 'https://johndoe.com',
+ bio: 'Software developer with 10 years of experience in web development.'
+ });
+ this.validationForm.markAllAsTouched();
+ }
+
+ // Custom URL validator
+ private urlValidator(control: AbstractControl): ValidationErrors | null {
+ if (!control.value) return null;
+
+ try {
+ new URL(control.value);
+ return null;
+ } catch {
+ return { url: true };
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/grid-system-demo/grid-system-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/grid-system-demo/grid-system-demo.component.scss
new file mode 100644
index 0000000..5bc455f
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/grid-system-demo/grid-system-demo.component.scss
@@ -0,0 +1,81 @@
+@use '../../../../../shared-ui/src/styles/semantic' as tokens;
+
+.demo-container {
+ padding: tokens.$semantic-spacing-layout-md;
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.demo-section {
+ margin-bottom: tokens.$semantic-spacing-layout-lg;
+
+ h3 {
+ margin-bottom: tokens.$semantic-spacing-component-md;
+ color: tokens.$semantic-color-text-primary;
+ font-size: 1.25rem;
+ font-weight: 600;
+ }
+
+ h4 {
+ margin-bottom: tokens.$semantic-spacing-component-sm;
+ color: tokens.$semantic-color-text-secondary;
+ font-size: 1.125rem;
+ font-weight: 500;
+ }
+
+ p {
+ color: tokens.$semantic-color-text-secondary;
+ margin-bottom: tokens.$semantic-spacing-component-md;
+ }
+}
+
+.demo-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: tokens.$semantic-spacing-layout-md;
+}
+
+.demo-grid-container {
+ flex: 1;
+ min-width: 300px;
+ margin-bottom: tokens.$semantic-spacing-layout-sm;
+}
+
+.demo-grid {
+ border: 2px dashed tokens.$semantic-color-border-subtle;
+ border-radius: tokens.$semantic-border-radius-md;
+ padding: tokens.$semantic-spacing-component-md;
+
+ &--fixed-height {
+ min-height: 200px;
+ }
+}
+
+.demo-grid-item {
+ background: tokens.$semantic-color-container-primary;
+ color: tokens.$semantic-color-on-container-primary;
+ padding: tokens.$semantic-spacing-component-sm;
+ border-radius: tokens.$semantic-border-radius-sm;
+ text-align: center;
+ font-weight: 500;
+ min-height: 60px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &--small {
+ width: 80px;
+ height: 40px;
+ min-height: auto;
+ }
+}
+
+@media (max-width: 768px) {
+ .demo-row {
+ flex-direction: column;
+ }
+
+ .demo-grid-container {
+ min-width: auto;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/grid-system-demo/grid-system-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/grid-system-demo/grid-system-demo.component.ts
new file mode 100644
index 0000000..dca835d
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/grid-system-demo/grid-system-demo.component.ts
@@ -0,0 +1,132 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { GridSystemComponent } from '../../../../../ui-essentials/src/lib/components/layout';
+
+@Component({
+ selector: 'ui-grid-system-demo',
+ standalone: true,
+ imports: [CommonModule, GridSystemComponent],
+ template: `
+
+
Grid System Demo
+
+
+
+ Column Variants
+
+ @for (cols of columnVariants; track cols) {
+
+
{{ cols }} Columns
+
+ @for (item of getItems(getItemCount(cols)); track $index) {
+ {{ $index + 1 }}
+ }
+
+
+ }
+
+
+
+
+
+ Gap Sizes
+
+ @for (gap of gapSizes; track gap) {
+
+
Gap: {{ gap }}
+
+ @for (item of getItems(6); track $index) {
+ {{ $index + 1 }}
+ }
+
+
+ }
+
+
+
+
+
+ Auto Layout
+
+
Auto-fit (items stretch to fill)
+
+ @for (item of getItems(4); track $index) {
+ Auto-fit {{ $index + 1 }}
+ }
+
+
+
+
Auto-fill (creates empty columns)
+
+ @for (item of getItems(4); track $index) {
+ Auto-fill {{ $index + 1 }}
+ }
+
+
+
+
+
+
+ Alignment Options
+
+
+
Justify Items: Center
+
+ @for (item of getItems(6); track $index) {
+ {{ $index + 1 }}
+ }
+
+
+
+
Align Items: Center
+
+ @for (item of getItems(6); track $index) {
+ {{ $index + 1 }}
+ }
+
+
+
+
+
+
+
+ Custom Grid Template
+
+
Custom Columns: 200px 1fr 100px
+
+ Fixed 200px
+ Flexible 1fr
+ Fixed 100px
+
+
+
+
+
+
+ Responsive Grid
+ Resize the window to see responsive behavior
+
+ @for (item of getItems(24); track $index) {
+ {{ $index + 1 }}
+ }
+
+
+
+ `,
+ styleUrl: './grid-system-demo.component.scss'
+})
+export class GridSystemDemoComponent {
+ columnVariants: (1 | 2 | 3 | 4 | 6 | 12)[] = [1, 2, 3, 4, 6, 12];
+ gapSizes: ('xs' | 'sm' | 'md' | 'lg' | 'xl')[] = ['xs', 'sm', 'md', 'lg', 'xl'];
+
+ getItems(count: number): number[] {
+ return Array.from({ length: count }, (_, i) => i + 1);
+ }
+
+ getItemCount(cols: number | string): number {
+ if (typeof cols === 'number') {
+ return Math.min(cols * 2, 12);
+ }
+ return 6;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/image-container-demo/image-container-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/image-container-demo/image-container-demo.component.scss
new file mode 100644
index 0000000..dd58e19
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/image-container-demo/image-container-demo.component.scss
@@ -0,0 +1,205 @@
+@import "../../../../../shared-ui/src/styles/semantic";
+
+// Tokens available globally via main application styles
+
+.demo-container {
+ padding: $semantic-spacing-layout-md;
+
+ h2 {
+ margin-bottom: $semantic-spacing-component-sm;
+ color: $semantic-color-text-primary;
+ font-size: $base-typography-font-size-xl;
+ }
+
+ p {
+ margin-bottom: $semantic-spacing-layout-lg;
+ color: $semantic-color-text-secondary;
+ font-size: $base-typography-font-size-md;
+ }
+}
+
+.demo-section {
+ margin-bottom: $semantic-spacing-layout-xl;
+
+ h3 {
+ margin-bottom: $semantic-spacing-component-md;
+ color: $semantic-color-text-primary;
+ font-size: $base-typography-font-size-lg;
+ border-bottom: $semantic-border-divider-width solid $semantic-color-border-subtle;
+ padding-bottom: $semantic-spacing-component-xs;
+ }
+}
+
+.demo-row {
+ display: flex;
+ gap: $semantic-spacing-component-lg;
+ flex-wrap: wrap;
+ align-items: flex-start;
+}
+
+.demo-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: $semantic-spacing-component-lg;
+}
+
+.demo-gallery {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: $semantic-spacing-component-md;
+ margin-bottom: $semantic-spacing-layout-lg;
+}
+
+.demo-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: $semantic-spacing-component-sm;
+
+ .ui-image-container {
+ cursor: pointer;
+
+ &:hover {
+ transform: translateY(-2px);
+ box-shadow: $semantic-shadow-elevation-3;
+ }
+ }
+}
+
+.demo-label {
+ font-size: $base-typography-font-size-sm;
+ color: $semantic-color-text-secondary;
+ font-weight: $base-typography-font-weight-medium;
+ text-align: center;
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ background: $semantic-color-surface-secondary;
+ border-radius: $semantic-border-radius-sm;
+ border: $semantic-border-divider-width solid $semantic-color-border-subtle;
+}
+
+// Custom styling for hero image demo
+.hero-image {
+ position: relative;
+
+ [slot='caption'] {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-component-xs;
+
+ h4 {
+ margin: 0;
+ font-size: $base-typography-font-size-md;
+ font-weight: $base-typography-font-weight-semibold;
+ }
+
+ p {
+ margin: 0;
+ font-size: $base-typography-font-size-sm;
+ opacity: 0.9;
+ }
+ }
+}
+
+// Retry button styling
+.retry-btn {
+ margin-top: $semantic-spacing-component-xs;
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ background: $semantic-color-interactive-primary;
+ color: $semantic-color-text-inverse;
+ border: none;
+ border-radius: $semantic-border-radius-sm;
+ font-size: $base-typography-font-size-xs;
+ cursor: pointer;
+ transition: background-color $semantic-duration-fast $semantic-easing-standard;
+
+ &:hover {
+ background-color: $semantic-color-surface-interactive;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus-ring;
+ outline-offset: 2px;
+ }
+}
+
+// Event log styling
+.event-log {
+ background: $semantic-color-surface-secondary;
+ border: $semantic-border-divider-width solid $semantic-color-border-subtle;
+ border-radius: $semantic-border-radius-md;
+ padding: $semantic-spacing-component-md;
+ font-family: monospace;
+ font-size: $base-typography-font-size-xs;
+ max-height: 200px;
+ overflow-y: auto;
+}
+
+.event-item {
+ display: flex;
+ gap: $semantic-spacing-component-sm;
+ margin-bottom: $semantic-spacing-component-xs;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+}
+
+.event-time {
+ color: $semantic-color-text-tertiary;
+ min-width: 60px;
+}
+
+.event-type {
+ color: $semantic-color-interactive-primary;
+ font-weight: $base-typography-font-weight-medium;
+ min-width: 80px;
+}
+
+.event-message {
+ color: $semantic-color-text-secondary;
+ flex: 1;
+}
+
+// Responsive design
+@media (max-width: $semantic-sizing-breakpoint-tablet - 1) {
+ .demo-row {
+ gap: $semantic-spacing-component-md;
+ }
+
+ .demo-grid {
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+ gap: $semantic-spacing-component-md;
+ }
+
+ .demo-gallery {
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ }
+}
+
+@media (max-width: $semantic-sizing-breakpoint-mobile - 1) {
+ .demo-container {
+ padding: $semantic-spacing-layout-sm;
+ }
+
+ .demo-section {
+ margin-bottom: $semantic-spacing-layout-lg;
+ }
+
+ .demo-row {
+ flex-direction: column;
+ gap: $semantic-spacing-component-md;
+ align-items: center;
+ }
+
+ .demo-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .demo-gallery {
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+ }
+
+ .event-log {
+ font-size: $base-typography-font-size-xs;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/image-container-demo/image-container-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/image-container-demo/image-container-demo.component.ts
new file mode 100644
index 0000000..ec70eae
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/image-container-demo/image-container-demo.component.ts
@@ -0,0 +1,311 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ImageContainerComponent, ImageContainerSize, ImageContainerAspectRatio, ImageContainerObjectFit, ImageContainerShape } from '../../../../../ui-essentials/src/lib/components/data-display/image-container';
+import { BadgeComponent } from '../../../../../ui-essentials/src/lib/components/data-display/badge';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { faRefresh, faHeart, faPlay } from '@fortawesome/free-solid-svg-icons';
+
+@Component({
+ selector: 'ui-image-container-demo',
+ standalone: true,
+ imports: [CommonModule, ImageContainerComponent, BadgeComponent, FontAwesomeModule],
+ template: `
+
+
Image Container Demo
+
A flexible image container with lazy loading, aspect ratios, and various display options.
+
+
+
+ Sizes
+
+ @for (size of sizes; track size) {
+
+
+
+ {{ size }}
+
+ }
+
+
+
+
+
+ Aspect Ratios
+
+ @for (ratio of aspectRatios; track ratio) {
+
+
+
+ {{ ratio }}
+
+ }
+
+
+
+
+
+ Object Fit
+
+ @for (fit of objectFits; track fit) {
+
+
+
+ {{ fit }}
+
+ }
+
+
+
+
+
+ Shapes
+
+ @for (shape of shapes; track shape) {
+
+
+
+ {{ shape }}
+
+ }
+
+
+
+
+
+ Loading States
+
+
+
+
+ Loading
+
+
+
+
+
+
+ Custom Error
+ Retry
+
+
+
Error State
+
+
+
+
+
+ Eager Load
+
+
+
+
+
+
+ Interactive Features
+
+
+
+
+
+ Play Video
+
+
+
With Overlay
+
+
+
+
+
+ With Caption
+
+
+
+
+
+
+ Advanced Examples
+
+
+
+
+
+
+
+
Profile Avatar
+
+
+
+
+
+
Custom Caption Content
+
With additional styled elements
+
Featured
+
+
+
Hero Banner
+
+
+
+
+
+
+ Responsive Gallery
+
+ @for (image of galleryImages; track image.id; let i = $index) {
+
+
+ {{ image.title }}
+
+
+ }
+
+
+
+
+ @if (eventLog.length > 0) {
+
+ Event Log
+
+ @for (event of eventLog.slice(-5); track event.id) {
+
+ {{ event.time | date:'HH:mm:ss' }}
+ {{ event.type }}
+ {{ event.message }}
+
+ }
+
+
+ }
+
+ `,
+ styleUrl: './image-container-demo.component.scss'
+})
+export class ImageContainerDemoComponent {
+ // Icons
+ faRefresh = faRefresh;
+ faHeart = faHeart;
+ faPlay = faPlay;
+
+ // Demo data
+ sizes: ImageContainerSize[] = ['sm', 'md', 'lg', 'xl'];
+ aspectRatios: ImageContainerAspectRatio[] = ['1/1', '4/3', '16/9', '3/2', '2/1', '3/4', '9/16'];
+ objectFits: ImageContainerObjectFit[] = ['contain', 'cover', 'fill', 'scale-down'];
+ shapes: ImageContainerShape[] = ['square', 'rounded', 'circle'];
+
+ // Sample images (using placeholder service)
+ sampleImages = {
+ landscape: 'https://picsum.photos/800/600?random=1',
+ portrait: 'https://picsum.photos/600/800?random=2',
+ square: 'https://picsum.photos/600/600?random=3'
+ };
+
+ galleryImages = [
+ { id: 1, url: 'https://picsum.photos/400/400?random=10', title: 'Nature', alt: 'Beautiful nature scene' },
+ { id: 2, url: 'https://picsum.photos/400/300?random=11', title: 'Architecture', alt: 'Modern building' },
+ { id: 3, url: 'https://picsum.photos/400/500?random=12', title: 'Portrait', alt: 'Person portrait' },
+ { id: 4, url: 'https://picsum.photos/400/400?random=13', title: 'Urban', alt: 'City landscape' },
+ { id: 5, url: 'https://picsum.photos/400/300?random=14', title: 'Technology', alt: 'Tech equipment' },
+ { id: 6, url: 'https://picsum.photos/400/400?random=15', title: 'Abstract', alt: 'Abstract art' }
+ ];
+
+ // Event tracking
+ eventLog: Array<{id: number, type: string, message: string, time: Date}> = [];
+ private eventId = 1;
+
+ handleImageClick(context: string): void {
+ this.logEvent('click', `Image clicked in ${context} context`);
+ }
+
+ handleGalleryClick(image: any, index: number): void {
+ this.logEvent('gallery-click', `Gallery image "${image.title}" clicked (index ${index})`);
+ }
+
+ handleImageLoad(imageId: number): void {
+ this.logEvent('load', `Image ${imageId} loaded successfully`);
+ }
+
+ handleImageError(imageId: number): void {
+ this.logEvent('error', `Image ${imageId} failed to load`);
+ }
+
+ retryImage(event: Event): void {
+ event.stopPropagation();
+ this.logEvent('retry', 'Retry button clicked for failed image');
+ // In a real implementation, this would retry loading the image
+ }
+
+ private logEvent(type: string, message: string): void {
+ this.eventLog.push({
+ id: this.eventId++,
+ type,
+ message,
+ time: new Date()
+ });
+
+ // Keep only last 20 events
+ if (this.eventLog.length > 20) {
+ this.eventLog.splice(0, this.eventLog.length - 20);
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/image-container-demo/index.ts b/projects/demo-ui-essentials/src/app/demos/image-container-demo/index.ts
new file mode 100644
index 0000000..6ee0223
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/image-container-demo/index.ts
@@ -0,0 +1 @@
+export * from './image-container-demo.component';
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/index.ts b/projects/demo-ui-essentials/src/app/demos/index.ts
new file mode 100644
index 0000000..5b40d5f
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/index.ts
@@ -0,0 +1,17 @@
+export * from './appbar-demo/appbar-demo.component';
+export * from './avatar-demo/avatar-demo.component';
+export * from './badge-demo/badge-demo.component';
+export * from './button-demo/button-demo.component';
+export * from './card-demo/card-demo.component';
+export * from './checkbox-demo/checkbox-demo.component';
+export * from './chip-demo/chip-demo.component';
+export * from './fontawesome-demo/fontawesome-demo.component';
+export * from './input-demo/input-demo.component';
+export * from './layout-demo/layout-demo.component';
+export * from './menu-demo/menu-demo.component';
+export * from './progress-demo/progress-demo.component';
+export * from './radio-demo/radio-demo.component';
+export * from './search-demo/search-demo.component';
+export * from './switch-demo/switch-demo.component';
+export * from './table-demo/table-demo.component';
+export * from './demos.routes';
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/input-demo/input-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/input-demo/input-demo.component.ts
new file mode 100644
index 0000000..c114e3b
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/input-demo/input-demo.component.ts
@@ -0,0 +1,680 @@
+import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { TextInputComponent } from '../../../../../ui-essentials/src/lib/components/forms/input';
+import { TextareaComponent } from '../../../../../ui-essentials/src/lib/components/forms/input';
+import { InputWrapperComponent } from '../../../../../ui-essentials/src/lib/components/forms/input';
+import { faSearch, faEnvelope, faEdit } from '@fortawesome/free-solid-svg-icons';
+
+@Component({
+ selector: 'ui-input-demo',
+ standalone: true,
+ imports: [
+ CommonModule,
+ FormsModule,
+ TextInputComponent,
+ TextareaComponent,
+ InputWrapperComponent
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
Input Component Showcase
+
+
+
+ Text Input Variants
+
+
+
+
Outlined (Default)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Input Types
+
+
+
+
+
+
+
+
+
+
+
+
+ Input States
+
+
+
+
+
+
+
+
+
+
+ Inputs with Icons and Features
+
+
+
+
+
+
+
+
+
+
+ Special States
+
+
+
+
+
+
+
+
+
+
+ Character Limit
+
+
+
+
+
+
+
+
+ Textarea Variants
+
+
+
+
Basic Textareas
+
+
+
+
+
+
+
+
+
+
Textarea Variants
+
+
+
+
+
+
+
+
+
Textarea with Features
+
+
+
+
+
+
+
+
+
Textarea States
+
+
+
+
+
+
+
+
+
+
+
+
+ Usage Examples
+
+
Basic Text Input:
+
<ui-text-input
+ label="Email"
+ placeholder="Enter your email"
+ type="email"
+ variant="outlined"
+ size="md"
+ [(ngModel)]="email"
+ (inputChange)="handleEmailChange($event)">
+</ui-text-input>
+
+
Text Input with Icons:
+
<ui-text-input
+ label="Search"
+ placeholder="Search products..."
+ type="search"
+ variant="filled"
+ [prefixIcon]="faSearch"
+ [clearable]="true"
+ [(ngModel)]="searchTerm"
+ (inputChange)="handleSearch($event)">
+</ui-text-input>
+
+
Textarea:
+
<ui-textarea
+ label="Message"
+ placeholder="Enter your message..."
+ variant="outlined"
+ [rows]="4"
+ [maxLength]="500"
+ [showCharacterCount]="true"
+ [(ngModel)]="message"
+ (textareaChange)="handleMessageChange($event)">
+</ui-textarea>
+
+
Input Wrapper:
+
<ui-input-wrapper
+ [mode]="inputMode"
+ label="Dynamic Input"
+ placeholder="Switches between input and textarea"
+ variant="outlined"
+ [prefixIcon]="faEdit"
+ [(ngModel)]="content"
+ (inputChange)="handleContentChange($event)">
+</ui-input-wrapper>
+
+
+
+
+
+ Interactive Actions
+
+
+ {{ isLoading() ? 'Stop Loading' : 'Start Loading' }}
+
+
+ Fill Sample Data
+
+
+ Clear All
+
+
+
+ @if (lastChangedInput()) {
+
+ Last changed: {{ lastChangedInput() }} = "{{ lastChangedValue() }}"
+
+ }
+
+
+
+
+ Current Values
+
+
{{ getValuesAsJson() }}
+
+
+
+ `,
+ styles: [`
+ h2 {
+ color: hsl(279, 14%, 11%);
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ border-bottom: 2px solid hsl(258, 100%, 47%);
+ padding-bottom: 0.5rem;
+ }
+
+ h3 {
+ color: hsl(279, 14%, 25%);
+ font-size: 1.5rem;
+ margin-bottom: 1rem;
+ }
+
+ h4 {
+ color: hsl(287, 12%, 35%);
+ font-size: 1.125rem;
+ margin-bottom: 0.75rem;
+ }
+
+ section {
+ border: 1px solid hsl(289, 14%, 90%);
+ border-radius: 8px;
+ padding: 1.5rem;
+ background: hsl(286, 20%, 99%);
+ }
+
+ pre {
+ font-size: 0.875rem;
+ line-height: 1.5;
+ margin: 0.5rem 0;
+ }
+
+ code {
+ font-family: 'JetBrains Mono', monospace;
+ color: #d63384;
+ }
+
+ button {
+ transition: all 0.2s ease-in-out;
+ }
+
+ button:hover {
+ opacity: 0.9;
+ transform: translateY(-1px);
+ }
+
+ label {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-weight: 500;
+ cursor: pointer;
+ }
+ `]
+})
+export class InputDemoComponent {
+ textValues: Record = {};
+ lastChangedInput = signal('');
+ lastChangedValue = signal('');
+ isLoading = signal(false);
+ useTextarea = signal(false);
+
+ // FontAwesome icons
+ readonly faSearch = faSearch;
+ readonly faEnvelope = faEnvelope;
+ readonly faEdit = faEdit;
+
+ handleInputChange(inputName: string, value: string): void {
+ this.textValues[inputName] = value;
+ this.lastChangedInput.set(inputName);
+ this.lastChangedValue.set(value);
+ console.log(`Input changed: ${inputName} = "${value}"`);
+ }
+
+ handleClear(inputName: string): void {
+ this.textValues[inputName] = '';
+ this.lastChangedInput.set(inputName);
+ this.lastChangedValue.set('(cleared)');
+ console.log(`Input cleared: ${inputName}`);
+ }
+
+ toggleLoading(): void {
+ this.isLoading.set(!this.isLoading());
+ console.log(`Loading state: ${this.isLoading() ? 'started' : 'stopped'}`);
+ }
+
+ toggleInputMode(): void {
+ this.useTextarea.set(!this.useTextarea());
+ console.log(`Input mode: ${this.useTextarea() ? 'textarea' : 'input'}`);
+ }
+
+ fillSampleData(): void {
+ this.textValues = {
+ 'outlined-md': 'Sample outlined text',
+ 'email': 'user@example.com',
+ 'search': 'sample search query',
+ 'prefix-icon': 'search with icon',
+ 'textarea-md': 'This is a sample message\nin a textarea component\nwith multiple lines.',
+ 'wrapper': 'Dynamic wrapper content'
+ };
+ this.lastChangedInput.set('multiple');
+ this.lastChangedValue.set('(sample data filled)');
+ console.log('Sample data filled');
+ }
+
+ clearAllInputs(): void {
+ this.textValues = {};
+ this.lastChangedInput.set('all');
+ this.lastChangedValue.set('(cleared)');
+ console.log('All inputs cleared');
+ }
+
+ getValuesAsJson(): string {
+ return JSON.stringify(this.textValues, null, 2);
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/layout-demo/layout-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/layout-demo/layout-demo.component.ts
new file mode 100644
index 0000000..ba5ca44
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/layout-demo/layout-demo.component.ts
@@ -0,0 +1,1199 @@
+import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { DashboardShellLayoutComponent, WidgetGridLayoutComponent, WidgetContainerComponent, BentoGridLayoutComponent, KpiCardLayoutComponent, ListDetailLayoutComponent, FeedLayoutComponent, SupportingPaneLayoutComponent, GridContainerComponent, TabContainerComponent, ScrollContainerComponent, LoadingStateContainerComponent, TabItem, ErrorState, GridResponsiveConfig, LoadingState, VirtualScrollConfig } from "../../../../../ui-essentials/src/lib/layouts";
+
+// Note: Local layout components are available but not used in this demo
+// They can be imported if needed for specific layout demonstrations
+
+@Component({
+ selector: 'ui-layout-demo',
+ standalone: true,
+ imports: [
+ CommonModule,
+ DashboardShellLayoutComponent,
+ WidgetGridLayoutComponent,
+ WidgetContainerComponent,
+ BentoGridLayoutComponent,
+ KpiCardLayoutComponent,
+ ListDetailLayoutComponent,
+ FeedLayoutComponent,
+ SupportingPaneLayoutComponent,
+ GridContainerComponent,
+ TabContainerComponent,
+ ScrollContainerComponent,
+ LoadingStateContainerComponent
+],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
Layout Component Showcase
+
+
+
+ Dashboard Shell Layout
+
+
Standard Dashboard Shell
+
+
+
+
Dashboard Header
+
+ {{ sidebarCollapsed() ? 'Expand' : 'Collapse' }} Sidebar
+
+
+ {{ showFooter() ? 'Hide' : 'Show' }} Footer
+
+
+
+
+ Navigation
+
+
+
+
+
Main Content Area
+
This is the main content area of the dashboard. Content can scroll independently.
+
Widget 1
+
Widget 2
+
Widget 3
+
+
+
+ © 2024 Dashboard Footer
+
+
+
+
+
+
+
+
+ Widget Grid Layout
+
+
+
+
Small Grid (sm)
+
+
+
+
Widget 1
+
Small grid widget
+
+
+
+
+
Widget 2
+
Analytics data
+
+
+
+
+
Widget 3
+
Metrics overview
+
+
+
+
+
+
+
+
Medium Grid (md)
+
+
+
+
Revenue
+
$24,300
+
↗️ 12% increase
+
+
+
+
+
Users
+
8,423
+
👥 Active users
+
+
+
+
+
Orders
+
1,234
+
📦 This month
+
+
+
+
+
Conversion
+
3.2%
+
📈 Conversion rate
+
+
+
+
+
+
+
+
Large Grid (lg)
+
+
+
+
Performance Dashboard
+
+
+
System Status: All services operational
+
+
+
+
+
+
+
+
+
+ Bento Grid Layout
+
+
+
+
Balanced Bento
+
+
+
+
Featured
+
Main highlight content
+
+
+
Analytics
+
Data insights
+
+
+
Metrics
+
Performance data
+
+
+
Reports
+
Generated reports
+
+
+
+
+
+
+
+
+
+
+
+ KPI Card Layout
+
+
+
+
+
Total Revenue
+
$48,392
+
+
+ 💰
+
+
+
+ ↗️
+ 12.5% increase
+ vs last month
+
+
+
+
+
+
+ ↗️
+ 8.2% increase
+ vs last month
+
+
+
+
+
+
+
Conversion Rate
+
3.7%
+
+
+ 📈
+
+
+
+ ↘️
+ 2.1% decrease
+ vs last month
+
+
+
+
+
+
+
+ List Detail Layout
+
+
+
+
Items List
+
+
+
Project Alpha
+
Web Application
+
+
+
Project Beta
+
Mobile App
+
+
+
Project Gamma
+
API Service
+
+
+
+
+
+ @if (selectedItem() === 1) {
+
Project Alpha Details
+
A comprehensive web application built with Angular and TypeScript.
+
+
Status
+ Active
+
+
+
Team Members
+
John Doe, Jane Smith, Mike Johnson
+
+ }
+ @if (selectedItem() === 2) {
+
Project Beta Details
+
Native mobile application for iOS and Android platforms.
+
+
Status
+ In Progress
+
+
+
Team Members
+
Sarah Wilson, Tom Brown
+
+ }
+ @if (selectedItem() === 3) {
+
Project Gamma Details
+
RESTful API service built with Node.js and Express.
+
+
Status
+ Planning
+
+
+
Team Members
+
Alex Chen, Maria Garcia
+
+ }
+ @if (selectedItem() === 0) {
+
+
📋
+
Select an item from the list
+
Choose a project from the left panel to view its details here.
+
+ }
+
+
+
+
+
+
+
+ Feed Layout
+
+
+
+
+
+ 👤
+
+
+
John Doe
+
2 hours ago
+
+
+
+ Just launched our new feature! 🚀 Really excited to see how the community responds to this update.
+
+
+ 👍 Like
+ 💬 Comment
+ 📤 Share
+
+
+
+
+
+
+ 👩
+
+
+
Jane Smith
+
5 hours ago
+
+
+
+ Working on some exciting new layouts for our dashboard. The bento grid approach is really promising for organizing complex data visualizations.
+
+
+
📎 Attachment
+
dashboard-mockup.png
+
+
+ 👍 Like
+ 💬 Comment
+ 📤 Share
+
+
+
+
+
+
+ 👨
+
+
+
Mike Johnson
+
1 day ago
+
+
+
+ Great team meeting today! We've outlined the roadmap for Q2 and everyone is aligned on the priorities. Looking forward to shipping these features.
+
+
+ 👍 Like (3)
+ 💬 Comment (1)
+ 📤 Share
+
+
+
+
+
+
+
+
+ Supporting Pane Layout
+
+
+
+
Main Content Area
+
+ This is the primary content area where the main application content is displayed.
+ It takes up most of the available space and can scroll independently from the supporting pane.
+
+
+
+
Article Content
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+ Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+
+
+
+
+
Section 2
+
+ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+
+
+
+
+
Supporting Information
+
+
+
Quick Stats
+
142
+
Total Items
+
+
+
+
Recent Activity
+
• Item updated 2 min ago
+
• New comment added
+
• Status changed
+
+
+
+
+
+
+
+
+
+
+
+
+ Grid Container (New)
+
+
+
+
Responsive Grid
+
+
+
Span 6 Columns
+
Responsive: mobile 12, tablet 6, desktop 6
+
+
+
Span 3
+
Quarter width
+
+
+
Span 3
+
Quarter width
+
+
+
Span 4
+
One third width
+
+
+
Span 8
+
Two thirds width
+
+
+
+
+
+
+
Auto-Fit Grid
+
+ @for (item of gridItems(); track item.id) {
+
+
{{ item.title }}
+
{{ item.description }}
+
+ }
+
+
+
+
+
+ {{ showGridDebug() ? 'Hide' : 'Show' }} Grid Debug
+
+
+ Add Grid Item
+
+
+ Remove Grid Item
+
+
+
+
+
+
+ Tab Container (New)
+
+
+
Horizontal Tabs
+
+
+
+
+
Dashboard Content
+
This is the dashboard tab content with some sample data and metrics.
+
+
+
+
+
Analytics Content
+
Analytics data and charts would be displayed here.
+
+
📊
+
Charts and graphs
+
+
+
+
+
Settings Content
+
Application settings and preferences.
+
+
+
+
+
+
+
+
+
Vertical Pills Tabs
+
+
+
+
+
Profile Information
+
+
+ 👤
+
+
+
John Doe
+
john.doe@example.com
+
+
+
+
+
+
+
+
Security Settings
+
+
+ Two-factor authentication
+ Enabled
+
+
+ Login alerts
+
+
+
+
+
+
+
+
+
+
+
+ Scroll Container (New)
+
+
+
+
Virtual Scrolling (Large Dataset)
+
+
+
+
+
+
+ {{ item.avatar }}
+
+
+
{{ item.name }}
+
{{ item.email }}
+
+
+
+
+
+
+
+
+
+
+
Custom Scrollbars
+
+
+
+
Large Content Area
+
This content area is larger than the container, so it will show custom scrollbars.
+
You can scroll both horizontally and vertically to see all the content.
+
+ @for (item of Array(8).fill(0); track $index) {
+
+
Item {{ $index + 1 }}
+
Sample data
+
+ }
+
+
+
+
+
+
+
+
+
+ Loading State Container (New)
+
+
+
+ Show Loading
+
+
+ Show Success
+
+
+ Show Error
+
+
+ Reset to Idle
+
+
+ Spinner
+ Skeleton
+ Pulse
+ Shimmer
+
+
+
+
+
+
+
+
+
Sample Content
+
+ This content can be in various loading states. Use the buttons above to simulate different states.
+
+
+
+
Data Item 1
+
Sample data content
+
+
+
Data Item 2
+
More sample content
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Usage Examples
+
+
Dashboard Shell Layout:
+
<ui-dashboard-shell-layout
+ [sidebarCollapsed]="collapsed"
+ [showFooter]="true">
+ <div slot="header">Header Content</div>
+ <nav slot="sidebar">Navigation</nav>
+ <div slot="main">Main Content</div>
+ <div slot="footer">Footer Content</div>
+</ui-dashboard-shell-layout>
+
+
Widget Grid Layout:
+
<ui-widget-grid-layout
+ gridSize="md"
+ [autoFit]="true">
+ <ui-widget-container>
+ <div>Widget Content</div>
+ </ui-widget-container>
+</ui-widget-grid-layout>
+
+
Bento Grid Layout:
+
<ui-bento-grid-layout
+ variant="balanced"
+ [adaptive]="true">
+ <div>Grid Item 1</div>
+ <div>Grid Item 2</div>
+ <div>Grid Item 3</div>
+</ui-bento-grid-layout>
+
+
List Detail Layout:
+
<ui-list-detail-layout>
+ <div slot="list">
+ List of items
+ </div>
+ <div slot="detail">
+ Selected item details
+ </div>
+</ui-list-detail-layout>
+
+
+
+
+
+ Interactive Controls
+
+
+ {{ sidebarCollapsed() ? 'Expand' : 'Collapse' }} Sidebar
+
+
+ {{ showFooter() ? 'Hide' : 'Show' }} Footer
+
+
+ Reset List Selection
+
+
+
+ @if (lastAction()) {
+
+ Last action: {{ lastAction() }}
+
+ }
+
+
+ `,
+ styles: [`
+ h2 {
+ color: hsl(279, 14%, 11%);
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ border-bottom: 2px solid hsl(258, 100%, 47%);
+ padding-bottom: 0.5rem;
+ }
+
+ h3 {
+ color: hsl(279, 14%, 25%);
+ font-size: 1.5rem;
+ margin-bottom: 1rem;
+ }
+
+ h4 {
+ color: hsl(287, 12%, 35%);
+ font-size: 1.125rem;
+ margin-bottom: 0.75rem;
+ }
+
+ section {
+ border: 1px solid hsl(289, 14%, 90%);
+ border-radius: 8px;
+ padding: 1.5rem;
+ background: hsl(286, 20%, 99%);
+ }
+
+ pre {
+ font-size: 0.875rem;
+ line-height: 1.5;
+ margin: 0.5rem 0;
+ }
+
+ code {
+ font-family: 'JetBrains Mono', monospace;
+ color: #d63384;
+ }
+
+ button {
+ transition: all 0.2s ease-in-out;
+ }
+
+ button:hover {
+ opacity: 0.9;
+ transform: translateY(-1px);
+ }
+ `]
+})
+export class LayoutDemoComponent {
+ // Original properties
+ sidebarCollapsed = signal(false);
+ showFooter = signal(false);
+ selectedItem = signal(0);
+ lastAction = signal('');
+
+ // Grid Container properties
+ showGridDebug = signal(false);
+ responsiveConfig = signal({
+ mobile: 1,
+ tablet: 2,
+ desktop: 3,
+ wide: 4
+ });
+
+ gridItems = signal([
+ { id: 1, title: 'Dynamic Item 1', description: 'Auto-fit grid item', color: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
+ { id: 2, title: 'Dynamic Item 2', description: 'Responsive layout', color: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' },
+ { id: 3, title: 'Dynamic Item 3', description: 'Grid container', color: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' },
+ { id: 4, title: 'Dynamic Item 4', description: 'Flexible grid', color: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)' }
+ ]);
+
+ // Tab Container properties
+ tabItems = signal([
+ { id: 'dashboard', label: 'Dashboard', icon: '📊', badge: '3' },
+ { id: 'analytics', label: 'Analytics', icon: '📈', closeable: true },
+ { id: 'settings', label: 'Settings', icon: '⚙️', closeable: true }
+ ]);
+
+ activeTabId = signal('dashboard');
+
+ verticalTabItems = signal([
+ { id: 'profile', label: 'Profile', icon: '👤' },
+ { id: 'account', label: 'Account', icon: '🔧' },
+ { id: 'security', label: 'Security', icon: '🔒' }
+ ]);
+
+ activeVerticalTabId = signal('profile');
+
+ // Scroll Container properties
+ virtualConfig = signal({
+ itemHeight: 60,
+ bufferSize: 10
+ });
+
+ largeDataset = signal(this.generateLargeDataset(1000));
+
+ // Loading State Container properties
+ loadingState = signal('idle');
+ loadingConfig = signal({
+ variant: 'spinner' as const,
+ size: 'md' as const,
+ message: 'Loading data...',
+ showProgress: false,
+ progress: 0
+ });
+
+ errorState = signal(null);
+
+ // Utility property
+ Array = Array;
+
+ toggleSidebar(): void {
+ this.sidebarCollapsed.set(!this.sidebarCollapsed());
+ this.lastAction.set(`Sidebar ${this.sidebarCollapsed() ? 'collapsed' : 'expanded'}`);
+ console.log(`Sidebar ${this.sidebarCollapsed() ? 'collapsed' : 'expanded'}`);
+ }
+
+ toggleFooter(): void {
+ this.showFooter.set(!this.showFooter());
+ this.lastAction.set(`Footer ${this.showFooter() ? 'shown' : 'hidden'}`);
+ console.log(`Footer ${this.showFooter() ? 'shown' : 'hidden'}`);
+ }
+
+ selectItem(itemId: number): void {
+ this.selectedItem.set(itemId);
+ this.lastAction.set(`Selected item ${itemId}`);
+ console.log(`Selected item ${itemId}`);
+ }
+
+ resetSelection(): void {
+ this.selectedItem.set(0);
+ this.lastAction.set('Selection reset');
+ console.log('Selection reset');
+ }
+
+ // New container methods
+
+ // Grid Container methods
+ toggleGridDebug(): void {
+ this.showGridDebug.set(!this.showGridDebug());
+ this.lastAction.set(`Grid debug ${this.showGridDebug() ? 'enabled' : 'disabled'}`);
+ }
+
+ addGridItem(): void {
+ const colors = [
+ 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
+ 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
+ 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
+ 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
+ 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)',
+ 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)'
+ ];
+
+ const currentItems = this.gridItems();
+ const newId = Math.max(...currentItems.map(i => i.id)) + 1;
+ const newItem = {
+ id: newId,
+ title: `Dynamic Item ${newId}`,
+ description: `Added item #${newId}`,
+ color: colors[newId % colors.length]
+ };
+
+ this.gridItems.set([...currentItems, newItem]);
+ this.lastAction.set(`Added grid item ${newId}`);
+ }
+
+ removeGridItem(): void {
+ const currentItems = this.gridItems();
+ if (currentItems.length > 1) {
+ this.gridItems.set(currentItems.slice(0, -1));
+ this.lastAction.set('Removed last grid item');
+ }
+ }
+
+ // Tab Container methods
+ onTabChange(event: any): void {
+ this.activeTabId.set(event.activeTab.id);
+ this.lastAction.set(`Switched to tab: ${event.activeTab.label}`);
+ console.log('Tab changed:', event);
+ }
+
+ onTabClose(event: any): void {
+ const tabs = this.tabItems();
+ const updatedTabs = tabs.filter(tab => tab.id !== event.tab.id);
+ this.tabItems.set(updatedTabs);
+
+ // Switch to first available tab if current active tab was closed
+ if (event.tab.id === this.activeTabId() && updatedTabs.length > 0) {
+ this.activeTabId.set(updatedTabs[0].id);
+ }
+
+ this.lastAction.set(`Closed tab: ${event.tab.label}`);
+ console.log('Tab closed:', event);
+ }
+
+ onTabAdd(): void {
+ const currentTabs = this.tabItems();
+ const newTabId = `tab-${Date.now()}`;
+ const newTab: TabItem = {
+ id: newTabId,
+ label: `New Tab ${currentTabs.length + 1}`,
+ icon: '📄',
+ closeable: true
+ };
+
+ this.tabItems.set([...currentTabs, newTab]);
+ this.activeTabId.set(newTabId);
+ this.lastAction.set(`Added new tab: ${newTab.label}`);
+ console.log('Tab added:', newTab);
+ }
+
+ onVerticalTabChange(event: any): void {
+ this.activeVerticalTabId.set(event.activeTab.id);
+ this.lastAction.set(`Switched to vertical tab: ${event.activeTab.label}`);
+ console.log('Vertical tab changed:', event);
+ }
+
+ // Scroll Container methods
+ onScroll(event: any): void {
+ // Only log occasionally to avoid spam
+ if (Math.random() < 0.1) {
+ console.log('Scroll event:', event);
+ }
+ }
+
+ generateLargeDataset(size: number): any[] {
+ const names = ['John Doe', 'Jane Smith', 'Mike Johnson', 'Sarah Wilson', 'Tom Brown', 'Lisa Garcia', 'David Miller', 'Emily Davis'];
+ const colors = ['#007bff', '#28a745', '#ffc107', '#dc3545', '#6f42c1', '#20c997', '#fd7e14', '#17a2b8'];
+ const avatars = ['👨', '👩', '🧑', '👴', '👵', '👦', '👧', '🧒'];
+
+ return Array.from({ length: size }, (_, i) => ({
+ id: i + 1,
+ name: `${names[i % names.length]} ${Math.floor(i / names.length) + 1}`,
+ email: `user${i + 1}@example.com`,
+ color: colors[i % colors.length],
+ avatar: avatars[i % avatars.length]
+ }));
+ }
+
+ // Loading State Container methods
+ setLoadingState(state: LoadingState): void {
+ this.loadingState.set(state);
+
+ if (state === 'error') {
+ this.errorState.set({
+ message: 'Failed to load data from the server.',
+ code: 'NETWORK_ERROR',
+ details: { timestamp: new Date().toISOString(), endpoint: '/api/data' },
+ retryable: true
+ });
+ } else {
+ this.errorState.set(null);
+ }
+
+ this.lastAction.set(`Loading state changed to: ${state}`);
+ console.log('Loading state changed:', state);
+ }
+
+ changeLoadingVariant(event: Event): void {
+ const target = event.target as HTMLSelectElement;
+ const variant = target.value as any;
+
+ this.loadingConfig.update(config => ({
+ ...config,
+ variant,
+ message: `Loading with ${variant} variant...`
+ }));
+
+ this.lastAction.set(`Loading variant changed to: ${variant}`);
+ console.log('Loading variant changed:', variant);
+ }
+
+ onLoadingRetry(): void {
+ this.lastAction.set('Retry button clicked');
+ this.setLoadingState('loading');
+
+ // Simulate retry process
+ setTimeout(() => {
+ this.setLoadingState('success');
+ }, 2000);
+
+ console.log('Loading retry triggered');
+ }
+
+ onLoadingCancel(): void {
+ this.lastAction.set('Loading cancelled');
+ this.setLoadingState('idle');
+ console.log('Loading cancelled');
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/list-demo/list-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/list-demo/list-demo.component.ts
new file mode 100644
index 0000000..b45a5af
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/list-demo/list-demo.component.ts
@@ -0,0 +1,622 @@
+import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import {
+ ListItemComponent,
+ ListContainerComponent,
+ ListItemData
+} from '../../../../../ui-essentials/src/lib/components/data-display/list';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import {
+ faInbox,
+ faStar,
+ faPaperPlane,
+ faEdit,
+ faArchive,
+ faTrash,
+ faPhone,
+ faComment,
+ faPlay,
+ faDownload,
+ faCheck,
+ faChevronRight,
+ faPlus,
+ faMinus
+} from '@fortawesome/free-solid-svg-icons';
+
+@Component({
+ selector: 'ui-list-demo',
+ standalone: true,
+ imports: [
+ CommonModule,
+ ListItemComponent,
+ ListContainerComponent,
+ FontAwesomeModule
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
List Component Showcase
+
+
+
+ Basic Lists - Size Variants
+
+
+
+
Small (sm)
+
+ @for (item of navigationItems; track item.primary) {
+
+
+
+ }
+
+
+
+
+
Medium (md)
+
+ @for (item of navigationItems; track item.primary) {
+
+
+
+ }
+
+
+
+
+
Large (lg)
+
+ @for (item of navigationItems; track item.primary) {
+
+
+
+ }
+
+
+
+
+
+
+
+ Multi-line Lists
+
+
+
+
Two Lines
+
+ @for (item of twoLineItems; track item.primary) {
+
+
+
+
+
+ }
+
+
+
+
+
Three Lines
+
+ @for (item of threeLineItems; track item.primary) {
+
+
+
+
+
+
+
+ }
+
+
+
+
+
+
+
+ Lists with Avatars
+
+
+
+
Contacts - Avatar + One Line
+
+ @for (item of contactItems; track item.primary) {
+
+
+
+
+
+
+
+
+
+
+ }
+
+
+
+
+
Messages - Avatar + Two Lines
+
+ @for (item of messageItems; track item.primary) {
+
+
+ 2h
+
+
+ }
+
+
+
+
+
+
+
+ Lists with Media
+
+
+
+
Music - Media + Two Lines
+
+ @for (item of musicItems; track item.primary) {
+
+
+ 3:42
+
+
+
+
+
+ }
+
+
+
+
+
Videos - Media + Three Lines
+
+ @for (item of videoItems; track item.primary) {
+
+
+
+ }
+
+
+
+
+
+
+
+ Interactive Demo
+
+ Click items to interact with them:
+
+
+
+
+ @for (item of interactiveItems(); track item.primary) {
+
+
+ @if (item.selected) {
+
+ Enabled
+
+ } @else if (item.disabled) {
+
+ Disabled
+
+ } @else {
+
+ Click to enable
+
+ }
+
+
+ }
+
+
+
+
+
+
+ Usage Examples
+
+
Basic List with Icons:
+
<ui-list-container elevation="sm" spacing="xs" [rounded]="true">
+ <ui-list-item
+ [data]="{primary: 'Inbox'}"
+ size="md"
+ lines="one"
+ variant="text">
+ <fa-icon [icon]="faInbox" slot="trailing"></fa-icon>
+ </ui-list-item>
+</ui-list-container>
+
+
List with Avatar and Actions:
+
<ui-list-container elevation="md" spacing="sm" [rounded]="true">
+ <ui-list-item
+ [data]="{
+ primary: 'John Doe',
+ secondary: 'john@example.com',
+ avatarSrc: 'avatar.jpg'
+ }"
+ size="lg"
+ lines="two"
+ variant="avatar">
+ <button slot="trailing" (click)="call()">
+ <fa-icon [icon]="faPhone"></fa-icon>
+ </button>
+ </ui-list-item>
+</ui-list-container>
+
+
+
+ @if (lastAction()) {
+
+ Action: {{ lastAction() }}
+
+ }
+
+ `,
+ styles: [`
+ h2 {
+ color: hsl(279, 14%, 11%);
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ border-bottom: 2px solid hsl(258, 100%, 47%);
+ padding-bottom: 0.5rem;
+ }
+
+ h3 {
+ color: hsl(279, 14%, 25%);
+ font-size: 1.5rem;
+ margin-bottom: 1rem;
+ margin-top: 2rem;
+ }
+
+ h4 {
+ color: hsl(287, 12%, 35%);
+ font-size: 1.125rem;
+ margin-bottom: 0.75rem;
+ margin-top: 1rem;
+ }
+
+ section {
+ border: 1px solid hsl(289, 14%, 90%);
+ border-radius: 12px;
+ padding: 1.5rem;
+ background: hsl(286, 20%, 99%);
+ margin-bottom: 2rem;
+ }
+
+ pre {
+ font-size: 0.875rem;
+ line-height: 1.5;
+ margin: 0.5rem 0;
+ white-space: pre-wrap;
+ }
+
+ code {
+ font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Courier New', monospace;
+ color: #d63384;
+ }
+
+ button {
+ transition: all 0.15s ease;
+ }
+
+ button:hover {
+ transform: translateY(-1px);
+ }
+
+ button:active {
+ transform: translateY(0);
+ }
+
+ fa-icon {
+ transition: color 0.2s ease;
+ }
+ `]
+})
+export class ListDemoComponent {
+ // State signals
+ lastAction = signal('');
+
+ // Demo data
+ navigationItems: ListItemData[] = [
+ { primary: 'Inbox' },
+ { primary: 'Starred' },
+ { primary: 'Sent' },
+ { primary: 'Drafts' },
+ { primary: 'Archive' },
+ { primary: 'Trash' }
+ ];
+
+ twoLineItems: ListItemData[] = [
+ {
+ primary: 'Review Design System Updates',
+ secondary: 'Check new component specifications and design tokens'
+ },
+ {
+ primary: 'Team Standup Meeting',
+ secondary: 'Daily sync with development and design teams'
+ },
+ {
+ primary: 'Code Review - Authentication',
+ secondary: 'Review pull request #247 for OAuth integration'
+ }
+ ];
+
+ threeLineItems: ListItemData[] = [
+ {
+ primary: 'Angular 19 Migration',
+ secondary: 'Upgrade project to latest Angular version with control flow',
+ tertiary: 'Estimated completion: Next Sprint • Priority: High'
+ },
+ {
+ primary: 'Design System Documentation',
+ secondary: 'Create comprehensive documentation for all components',
+ tertiary: 'Status: In Progress • Assigned: Design Team'
+ }
+ ];
+
+ contactItems: ListItemData[] = [
+ {
+ primary: 'Sarah Wilson',
+ avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=sarah',
+ avatarAlt: 'Sarah Wilson avatar'
+ },
+ {
+ primary: 'Alex Chen',
+ avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=alex',
+ avatarAlt: 'Alex Chen avatar'
+ },
+ {
+ primary: 'Mike Rodriguez',
+ avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=mike',
+ avatarAlt: 'Mike Rodriguez avatar'
+ }
+ ];
+
+ messageItems: ListItemData[] = [
+ {
+ primary: 'John Doe',
+ secondary: 'Hey! How\'s the new component library coming along?',
+ avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=john',
+ avatarAlt: 'John Doe avatar'
+ },
+ {
+ primary: 'Jane Smith',
+ secondary: 'The design tokens look great. Ready for review when you are.',
+ avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=jane',
+ avatarAlt: 'Jane Smith avatar'
+ }
+ ];
+
+ musicItems: ListItemData[] = [
+ {
+ primary: 'Bohemian Rhapsody',
+ secondary: 'Queen • A Night at the Opera',
+ mediaSrc: 'https://picsum.photos/80/80?random=1',
+ mediaAlt: 'Album cover'
+ },
+ {
+ primary: 'Hotel California',
+ secondary: 'Eagles • Hotel California',
+ mediaSrc: 'https://picsum.photos/80/80?random=2',
+ mediaAlt: 'Album cover'
+ }
+ ];
+
+ videoItems: ListItemData[] = [
+ {
+ primary: 'Angular Fundamentals',
+ secondary: 'Complete guide to modern Angular development',
+ tertiary: 'Duration: 2h 30m • Updated: Today • 1.2M views',
+ mediaSrc: 'https://picsum.photos/80/80?random=5',
+ mediaAlt: 'Video thumbnail'
+ }
+ ];
+
+ interactiveItems = signal([
+ {
+ primary: 'Email Notifications',
+ secondary: 'Receive emails about important updates',
+ avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=email',
+ selected: false,
+ disabled: false
+ },
+ {
+ primary: 'SMS Alerts',
+ secondary: 'Get text messages for urgent notifications',
+ avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=sms',
+ selected: true,
+ disabled: false
+ },
+ {
+ primary: 'Marketing Communications',
+ secondary: 'Promotional offers and product updates',
+ avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=marketing',
+ selected: false,
+ disabled: true
+ }
+ ]);
+
+ // Font Awesome icons
+ faInbox = faInbox;
+ faStar = faStar;
+ faPaperPlane = faPaperPlane;
+ faEdit = faEdit;
+ faArchive = faArchive;
+ faTrash = faTrash;
+ faPhone = faPhone;
+ faComment = faComment;
+ faPlay = faPlay;
+ faDownload = faDownload;
+ faCheck = faCheck;
+ faChevronRight = faChevronRight;
+ faPlus = faPlus;
+ faMinus = faMinus;
+
+ // Helper methods
+ getIconForItem(itemName: string): any {
+ const iconMap: { [key: string]: any } = {
+ 'Inbox': faInbox,
+ 'Starred': faStar,
+ 'Sent': faPaperPlane,
+ 'Drafts': faEdit,
+ 'Archive': faArchive,
+ 'Trash': faTrash
+ };
+ return iconMap[itemName] || faInbox;
+ }
+
+ // Event handlers
+ handleAction(action: string, item?: string): void {
+ const message = item
+ ? `${action} action on "${item}" at ${new Date().toLocaleTimeString()}`
+ : `${action} action at ${new Date().toLocaleTimeString()}`;
+
+ this.lastAction.set(message);
+ console.log(`List action: ${action}`, item);
+
+ // Clear the message after 3 seconds
+ setTimeout(() => this.lastAction.set(''), 3000);
+ }
+
+ toggleInteractiveItem(itemName: string): void {
+ this.interactiveItems.update(items =>
+ items.map(item => {
+ if (item.primary === itemName && !item.disabled) {
+ return { ...item, selected: !item.selected };
+ }
+ return item;
+ })
+ );
+
+ this.handleAction('toggle', itemName);
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/loading-spinner-demo/loading-spinner-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/loading-spinner-demo/loading-spinner-demo.component.scss
new file mode 100644
index 0000000..c039aa3
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/loading-spinner-demo/loading-spinner-demo.component.scss
@@ -0,0 +1,242 @@
+.demo-container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 2rem;
+}
+
+.demo-section {
+ margin-bottom: 3rem;
+
+ h2 {
+ font-size: 1.875rem;
+ color: #1f2937;
+ margin-bottom: 1.5rem;
+ }
+
+ h3 {
+ font-size: 1.5rem;
+ color: #1f2937;
+ margin-bottom: 1rem;
+ border-bottom: 1px solid #d1d5db;
+ padding-bottom: 0.75rem;
+ }
+
+ h4 {
+ font-size: 1.25rem;
+ color: #1f2937;
+ margin-bottom: 0.5rem;
+ }
+
+ h5 {
+ font-size: 1.125rem;
+ color: #1f2937;
+ margin-bottom: 0.25rem;
+ }
+}
+
+.demo-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem;
+ align-items: flex-start;
+}
+
+.demo-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.75rem;
+ border: 1px solid #e5e7eb;
+ border-radius: 0.375rem;
+ background: #ffffff;
+ min-width: 120px;
+
+ p {
+ font-size: 0.875rem;
+ color: #6b7280;
+ margin: 0;
+ text-align: center;
+ text-transform: capitalize;
+ }
+}
+
+.demo-interactive {
+ padding: 1rem;
+ border: 1px solid #d1d5db;
+ border-radius: 0.5rem;
+ background: #f9fafb;
+
+ .demo-controls {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+ margin-bottom: 1rem;
+ padding-bottom: 0.75rem;
+ border-bottom: 1px solid #e5e7eb;
+
+ label {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+ font-size: 0.875rem;
+ color: #1f2937;
+ font-weight: 500;
+
+ select {
+ padding: 0.25rem 0.5rem;
+ border: 1px solid #d1d5db;
+ border-radius: 0.25rem;
+ background: #ffffff;
+ color: #1f2937;
+ font-size: 0.875rem;
+
+ &:focus {
+ outline: 2px solid #3b82f6;
+ outline-offset: 2px;
+ border-color: #3b82f6;
+ }
+ }
+
+ input[type="checkbox"] {
+ margin-right: 0.25rem;
+ }
+ }
+ }
+
+ .demo-result {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 120px;
+ background: #ffffff;
+ border: 1px dashed #d1d5db;
+ border-radius: 0.375rem;
+ }
+
+ p {
+ text-align: center;
+ margin-top: 0.5rem;
+ font-size: 0.875rem;
+ color: #6b7280;
+ }
+}
+
+.demo-usage {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 1rem;
+}
+
+.usage-example {
+ padding: 0.75rem;
+ border: 1px solid #d1d5db;
+ border-radius: 0.375rem;
+ background: #ffffff;
+
+ .demo-button {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 0.75rem;
+ background: #6366f1;
+ color: #ffffff;
+ border: none;
+ border-radius: 0.375rem;
+ font-size: 1rem;
+ cursor: pointer;
+ transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);
+
+ &:hover:not(:disabled) {
+ background: #5855eb;
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
+ }
+
+ &:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+
+ &:focus-visible {
+ outline: 2px solid #3b82f6;
+ outline-offset: 2px;
+ }
+ }
+
+ .page-loading-example,
+ .processing-example {
+ min-height: 150px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.75rem;
+ border: 1px solid #e5e7eb;
+ border-radius: 0.25rem;
+ background: #f9fafb;
+
+ .page-content,
+ .process-result {
+ text-align: center;
+
+ p {
+ margin: 0.5rem 0;
+ color: #6b7280;
+ }
+
+ button {
+ padding: 0.25rem 0.5rem;
+ background: #64748b;
+ color: #ffffff;
+ border: none;
+ border-radius: 0.25rem;
+ font-size: 0.875rem;
+ cursor: pointer;
+ transition: background 150ms cubic-bezier(0.4, 0, 0.2, 1);
+
+ &:hover {
+ background: #475569;
+ }
+
+ &:focus-visible {
+ outline: 2px solid #3b82f6;
+ outline-offset: 2px;
+ }
+ }
+ }
+ }
+}
+
+// Responsive breakpoints
+@media (max-width: 768px) {
+ .demo-container {
+ padding: 1rem;
+ }
+
+ .demo-row {
+ gap: 0.75rem;
+ }
+
+ .demo-item {
+ min-width: 100px;
+ padding: 0.5rem;
+ }
+
+ .demo-interactive .demo-controls {
+ flex-direction: column;
+ align-items: flex-start;
+
+ label {
+ width: 100%;
+
+ select {
+ width: 100%;
+ }
+ }
+ }
+
+ .demo-usage {
+ grid-template-columns: 1fr;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/loading-spinner-demo/loading-spinner-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/loading-spinner-demo/loading-spinner-demo.component.ts
new file mode 100644
index 0000000..8738817
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/loading-spinner-demo/loading-spinner-demo.component.ts
@@ -0,0 +1,294 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { LoadingSpinnerComponent } from 'ui-essentials';
+
+@Component({
+ selector: 'ui-loading-spinner-demo',
+ standalone: true,
+ imports: [CommonModule, FormsModule, LoadingSpinnerComponent],
+ template: `
+
+
Loading Spinner Demo
+
+
+
+ Sizes
+
+ @for (size of sizes; track size) {
+
+ }
+
+
+
+
+
+ Types
+
+ @for (type of types; track type) {
+
+ }
+
+
+
+
+
+ Colors
+
+ @for (variant of variants; track variant) {
+
+ }
+
+
+
+
+
+ Speeds
+
+ @for (speed of speeds; track speed) {
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+ Interactive
+
+
+
+ Size:
+
+ @for (size of sizes; track size) {
+ {{ size }}
+ }
+
+
+
+
+ Type:
+
+ @for (type of types; track type) {
+ {{ type }}
+ }
+
+
+
+
+ Variant:
+
+ @for (variant of variants; track variant) {
+ {{ variant }}
+ }
+
+
+
+
+ Speed:
+
+ @for (speed of speeds; track speed) {
+ {{ speed }}
+ }
+
+
+
+
+
+ Show Label
+
+
+
+
+ Disabled
+
+
+
+
+
+
+
+
+ @if (clickCount > 0) {
+
Spinner clicked: {{ clickCount }} times
+ }
+
+
+
+
+
+ Common Usage Examples
+
+
+
Loading Button
+
+ @if (isLoading) {
+
+ Loading...
+ } @else {
+ Click to Load
+ }
+
+
+
+
+
Page Loading
+
+ @if (pageLoading) {
+
+
+ } @else {
+
+
Page Content
+
This is the loaded content.
+
Reload Page
+
+ }
+
+
+
+
+
Data Processing
+
+ @if (processing) {
+
+
+ } @else {
+
+
✅ Data processed successfully!
+
Process Data
+
+ }
+
+
+
+
+
+ `,
+ styleUrl: './loading-spinner-demo.component.scss'
+})
+export class LoadingSpinnerDemoComponent {
+ sizes = ['sm', 'md', 'lg'] as const;
+ types = ['spin', 'dots', 'pulse', 'bars'] as const;
+ variants = ['primary', 'secondary', 'accent', 'success', 'warning', 'danger', 'info'] as const;
+ speeds = ['slow', 'normal', 'fast'] as const;
+
+ // Interactive demo state
+ selectedSize = 'md' as const;
+ selectedType = 'spin' as const;
+ selectedVariant = 'primary' as const;
+ selectedSpeed = 'normal' as const;
+ showInteractiveLabel = false;
+ isDisabled = false;
+ interactiveLabel = 'Loading';
+ clickCount = 0;
+
+ // Usage examples state
+ isLoading = false;
+ pageLoading = false;
+ processing = false;
+
+ handleSpinnerClick(event: MouseEvent): void {
+ if (!this.isDisabled) {
+ this.clickCount++;
+ console.log('Spinner clicked', event);
+ }
+ }
+
+ simulateLoading(): void {
+ this.isLoading = true;
+ setTimeout(() => {
+ this.isLoading = false;
+ }, 2000);
+ }
+
+ simulatePageLoad(): void {
+ this.pageLoading = true;
+ setTimeout(() => {
+ this.pageLoading = false;
+ }, 3000);
+ }
+
+ simulateProcessing(): void {
+ this.processing = true;
+ setTimeout(() => {
+ this.processing = false;
+ }, 2500);
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/menu-demo/menu-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/menu-demo/menu-demo.component.scss
new file mode 100644
index 0000000..239a6c3
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/menu-demo/menu-demo.component.scss
@@ -0,0 +1,225 @@
+@use "../../../../../shared-ui/src/styles/tokens" as *;
+
+// Tokens available globally via main application styles
+
+// ==========================================================================
+// MENU DEMO COMPONENT
+// ==========================================================================
+// Demonstration component showcasing all menu variants and capabilities
+// ==========================================================================
+
+.menu-demo {
+ padding: $semantic-spacing-component-xl;
+ max-width: 1400px;
+ margin: 0 auto;
+
+ h2 {
+ font-size: 2rem;
+ font-weight: 600;
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-layout-lg;
+ text-align: center;
+ border-bottom: 2px solid $semantic-color-interactive-primary;
+ padding-bottom: $semantic-spacing-component-sm;
+ }
+}
+
+.demo-section {
+ margin-bottom: $semantic-spacing-layout-xl;
+ border: 1px solid $semantic-color-border-secondary;
+ border-radius: $semantic-border-radius-lg;
+ padding: $semantic-spacing-component-xl;
+ background: $semantic-color-surface-elevated;
+
+ h3 {
+ font-size: 1.5rem;
+ font-weight: 500;
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-component-xl;
+ padding-bottom: $semantic-spacing-component-sm;
+ border-bottom: 2px solid $semantic-color-border-secondary;
+ }
+}
+
+.demo-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
+ gap: $semantic-spacing-component-xl;
+ margin-bottom: $semantic-spacing-layout-md;
+}
+
+.demo-item {
+ h4 {
+ font-size: 1rem;
+ font-weight: 500;
+ color: $semantic-color-text-secondary;
+ margin-bottom: $semantic-spacing-component-md;
+ text-align: center;
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ background-color: $semantic-color-surface-secondary;
+ border-radius: $semantic-border-radius-sm;
+ }
+}
+
+.demo-single {
+ max-width: 800px;
+ margin: 0 auto;
+
+ h4 {
+ font-size: 1rem;
+ font-weight: 500;
+ color: $semantic-color-text-secondary;
+ margin-bottom: $semantic-spacing-component-md;
+ text-align: center;
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ background-color: $semantic-color-surface-secondary;
+ border-radius: $semantic-border-radius-sm;
+ }
+}
+
+// ==========================================================================
+// USAGE EXAMPLES SECTION
+// ==========================================================================
+
+.usage-examples {
+ background: $semantic-color-surface-secondary;
+ padding: $semantic-spacing-component-xl;
+ border-radius: $semantic-border-radius-md;
+ border-left: 4px solid $semantic-color-interactive-primary;
+
+ h4 {
+ font-size: 1rem;
+ font-weight: 600;
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-component-sm;
+ margin-top: $semantic-spacing-component-lg;
+ text-align: left;
+ padding: 0;
+ background: none;
+
+ &:first-child {
+ margin-top: 0;
+ }
+ }
+
+ pre {
+ background: $semantic-color-surface-primary;
+ padding: $semantic-spacing-component-md;
+ border-radius: $semantic-border-radius-sm;
+ overflow-x: auto;
+ margin: $semantic-spacing-component-sm 0 $semantic-spacing-component-lg 0;
+ border: 1px solid $semantic-color-border-secondary;
+ }
+
+ code {
+ font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
+ font-size: 0.875rem;
+ line-height: 1.5;
+ color: $semantic-color-text-primary;
+ }
+}
+
+// ==========================================================================
+// INTERACTIVE DEMO SECTION
+// ==========================================================================
+
+.interactive-demo {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: $semantic-spacing-component-lg;
+}
+
+.demo-feedback {
+ padding: $semantic-spacing-component-md;
+ background: $semantic-color-container-primary;
+ color: $semantic-color-on-container-primary;
+ border-radius: $semantic-border-radius-md;
+ border: 1px solid $semantic-color-border-secondary;
+ text-align: center;
+
+ strong {
+ font-weight: 600;
+ }
+}
+
+// ==========================================================================
+// RESPONSIVE DESIGN
+// ==========================================================================
+
+@media (max-width: 768px) {
+ .menu-demo {
+ padding: $semantic-spacing-component-lg;
+
+ h2 {
+ font-size: 1.5rem;
+ }
+ }
+
+ .demo-grid {
+ grid-template-columns: 1fr;
+ gap: $semantic-spacing-component-lg;
+ }
+
+ .demo-section {
+ padding: $semantic-spacing-component-lg;
+
+ h3 {
+ font-size: 1.25rem;
+ }
+ }
+
+ .usage-examples {
+ padding: $semantic-spacing-component-lg;
+
+ pre {
+ font-size: 0.75rem;
+ }
+ }
+}
+
+@media (max-width: 480px) {
+ .menu-demo {
+ padding: $semantic-spacing-component-md;
+ }
+
+ .demo-section {
+ padding: $semantic-spacing-component-md;
+ }
+
+ .demo-grid {
+ gap: $semantic-spacing-component-md;
+ }
+}
+
+// ==========================================================================
+// ACCESSIBILITY ENHANCEMENTS
+// ==========================================================================
+
+// Focus indicators for better keyboard navigation
+.menu-demo {
+ .demo-section:focus-within {
+ outline: 2px solid $semantic-color-border-focus;
+ outline-offset: 4px;
+ border-radius: $semantic-border-radius-md;
+ }
+}
+
+// High contrast mode support
+@media (prefers-contrast: high) {
+ .demo-section {
+ border: 2px solid $semantic-color-border-primary;
+ }
+
+ .demo-feedback {
+ border: 2px solid $semantic-color-border-primary;
+ }
+}
+
+// Reduced motion support
+@media (prefers-reduced-motion: reduce) {
+ .menu-demo * {
+ transition: none;
+ animation: none;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/menu-demo/menu-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/menu-demo/menu-demo.component.ts
new file mode 100644
index 0000000..ed5a04e
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/menu-demo/menu-demo.component.ts
@@ -0,0 +1,697 @@
+import { Component, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import {
+ faUser, faHome, faCog, faEnvelope, faDownload, faShare, faFileExport,
+ faChartBar, faChartLine, faFileAlt, faShieldAlt, faDatabase, faTrash,
+ faStar, faBolt, faInfoCircle, faExclamationTriangle, faFolder, faCirclePlus,
+ faFolderOpen, faEdit, faSave, faTachometerAlt, faProjectDiagram, faPlayCircle,
+ faCheckCircle, faArchive, faCalendarWeek, faCalendarAlt, faCalendar, faSlidersH,
+ faPalette, faBell, faQuestionCircle, faSignOutAlt, faBox, faConciergeBell,
+ faCircle, faRefresh, faToggleOn
+} from '@fortawesome/free-solid-svg-icons';
+import { faAngular, faGithub } from '@fortawesome/free-brands-svg-icons';
+import { MenuItemComponent, MenuContainerComponent, MenuSubmenuComponent, MenuItemData } from '../../../../../ui-essentials/src/lib/components/navigation/menu';
+
+@Component({
+ selector: 'ui-menu-demo',
+ standalone: true,
+ imports: [
+ CommonModule,
+ MenuItemComponent,
+ MenuContainerComponent,
+ MenuSubmenuComponent
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+ `,
+ styleUrls: ['./menu-demo.component.scss']
+})
+export class MenuDemoComponent {
+ // FontAwesome icons
+ faUser = faUser;
+ faHome = faHome;
+ faCog = faCog;
+ faEnvelope = faEnvelope;
+ faDownload = faDownload;
+ faShare = faShare;
+ faFileExport = faFileExport;
+ faChartBar = faChartBar;
+ faChartLine = faChartLine;
+ faFileAlt = faFileAlt;
+ faShieldAlt = faShieldAlt;
+ faDatabase = faDatabase;
+ faTrash = faTrash;
+ faStar = faStar;
+ faBolt = faBolt;
+ faInfoCircle = faInfoCircle;
+ faExclamationTriangle = faExclamationTriangle;
+ faFolder = faFolder;
+ faCirclePlus = faCirclePlus;
+ faFolderOpen = faFolderOpen;
+ faEdit = faEdit;
+ faSave = faSave;
+ faTachometerAlt = faTachometerAlt;
+ faProjectDiagram = faProjectDiagram;
+ faPlayCircle = faPlayCircle;
+ faCheckCircle = faCheckCircle;
+ faArchive = faArchive;
+ faCalendarWeek = faCalendarWeek;
+ faCalendarAlt = faCalendarAlt;
+ faCalendar = faCalendar;
+ faSlidersH = faSlidersH;
+ faPalette = faPalette;
+ faBell = faBell;
+ faQuestionCircle = faQuestionCircle;
+ faSignOutAlt = faSignOutAlt;
+ faBox = faBox;
+ faConciergeBell = faConciergeBell;
+ faCircle = faCircle;
+ faRefresh = faRefresh;
+ faToggleOn = faToggleOn;
+ faAngular = faAngular;
+ faGithub = faGithub;
+
+ lastClickedItem: string = '';
+ submenuStates: Record = {
+ projects: false,
+ reports: false,
+ preferences: false
+ };
+
+ scrollableItems: MenuItemData[] = Array.from({ length: 15 }, (_, i) => ({
+ id: `item-${i + 1}`,
+ label: `Scrollable Item ${i + 1}`,
+ icon: this.faCircle
+ }));
+
+ handleMenuClick(action: string, item: MenuItemData): void {
+ this.lastClickedItem = `${action} (${item.label})`;
+ console.log(`Menu clicked: ${action}`, item);
+ }
+
+ toggleSubmenu(key: string, event: { item: MenuItemData; isOpen: boolean }): void {
+ this.submenuStates[key] = event.isOpen;
+ console.log(`Submenu ${key} ${event.isOpen ? 'opened' : 'closed'}`);
+ }
+
+ showAlert(message: string): void {
+ alert(message);
+ this.lastClickedItem = 'Alert shown';
+ }
+
+ toggleDemoState(): void {
+ // Toggle all submenus
+ Object.keys(this.submenuStates).forEach(key => {
+ this.submenuStates[key] = !this.submenuStates[key];
+ });
+ this.lastClickedItem = 'Demo state toggled';
+ }
+
+ resetDemo(): void {
+ this.lastClickedItem = '';
+ this.submenuStates = {
+ projects: false,
+ reports: false,
+ preferences: false
+ };
+ console.log('Demo reset');
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/modal-demo/modal-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/modal-demo/modal-demo.component.scss
new file mode 100644
index 0000000..4a5b005
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/modal-demo/modal-demo.component.scss
@@ -0,0 +1,130 @@
+@use '../../../../../shared-ui/src/styles/semantic/index' as *;
+
+.demo-container {
+ padding: $semantic-spacing-layout-md;
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.demo-section {
+ margin-bottom: $semantic-spacing-layout-xl;
+
+ h2 {
+ color: $semantic-color-on-surface;
+ font-size: $semantic-typography-heading-h2-size;
+ margin-bottom: $semantic-spacing-content-heading;
+ border-bottom: 1px solid $semantic-color-outline;
+ padding-bottom: $semantic-spacing-content-line-normal;
+ }
+
+ h3 {
+ color: $semantic-color-on-surface;
+ font-size: $semantic-typography-heading-h4-size;
+ margin-bottom: $semantic-spacing-component-lg;
+ }
+}
+
+.demo-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: $semantic-spacing-component-sm;
+ align-items: flex-start;
+
+ @media (max-width: 768px) {
+ flex-direction: column;
+ align-items: stretch;
+ }
+}
+
+.config-panel {
+ background: $semantic-color-surface-variant;
+ border: 1px solid $semantic-color-outline;
+ border-radius: $semantic-border-radius-md;
+ padding: $semantic-spacing-component-lg;
+ margin-top: $semantic-spacing-component-lg;
+}
+
+.config-row {
+ display: flex;
+ align-items: center;
+ gap: $semantic-spacing-component-md;
+ margin-bottom: $semantic-spacing-component-md;
+ flex-wrap: wrap;
+
+ &:last-child {
+ margin-bottom: 0;
+ margin-top: $semantic-spacing-component-lg;
+ justify-content: flex-start;
+ }
+
+ label {
+ font-size: $semantic-typography-font-size-sm;
+ color: $semantic-color-on-surface;
+ display: flex;
+ align-items: center;
+ gap: $semantic-spacing-component-xs;
+ white-space: nowrap;
+
+ &:first-child {
+ min-width: 80px;
+ font-weight: $semantic-typography-font-weight-medium;
+ }
+ }
+
+ select {
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ border: 1px solid $semantic-color-outline;
+ border-radius: $semantic-border-radius-sm;
+ background: $semantic-color-surface-primary;
+ color: $semantic-color-on-surface;
+ font-size: $semantic-typography-font-size-sm;
+ min-width: 120px;
+
+ &:focus {
+ outline: 2px solid $semantic-color-primary;
+ outline-offset: 2px;
+ border-color: $semantic-color-primary;
+ }
+ }
+
+ input[type="checkbox"] {
+ accent-color: $semantic-color-primary;
+ transform: scale(1.1);
+ }
+
+ @media (max-width: 768px) {
+ flex-direction: column;
+ align-items: flex-start;
+
+ label:first-child {
+ min-width: auto;
+ }
+ }
+}
+
+.event-log {
+ background: $semantic-color-surface-primary;
+ border: 1px solid $semantic-color-outline;
+ border-radius: $semantic-border-radius-md;
+ padding: $semantic-spacing-component-md;
+ max-height: 300px;
+ overflow-y: auto;
+ margin-bottom: $semantic-spacing-component-md;
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ font-size: $semantic-typography-font-size-sm;
+}
+
+.event-item {
+ padding: $semantic-spacing-component-xs 0;
+ border-bottom: 1px solid $semantic-color-outline-variant;
+ color: $semantic-color-on-surface-variant;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &:first-child {
+ color: $semantic-color-on-surface;
+ font-weight: $semantic-typography-font-weight-medium;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/modal-demo/modal-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/modal-demo/modal-demo.component.ts
new file mode 100644
index 0000000..2a86cee
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/modal-demo/modal-demo.component.ts
@@ -0,0 +1,510 @@
+import { Component, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { faExclamationTriangle, faCheckCircle, faInfoCircle } from '@fortawesome/free-solid-svg-icons';
+import { ButtonComponent, ModalComponent } from "../../../../../ui-essentials/src/public-api";
+
+@Component({
+ selector: 'ui-modal-demo',
+ standalone: true,
+ imports: [CommonModule, FormsModule, FontAwesomeModule, ButtonComponent, ModalComponent],
+ template: `
+
+
Modal Demo
+
+
+
+ Basic Usage
+
+
+ Open Basic Modal
+
+
+
+
+
+
+ Sizes
+
+ Small Modal
+ Medium Modal
+ Large Modal
+ XL Modal
+ Fullscreen Modal
+
+
+
+
+
+ Variants
+
+ Default
+ Warning
+ Danger
+ Success
+
+
+
+
+
+ Configuration Options
+
+ No Header
+ With Footer
+ No Padding
+ Not Closable
+
+
+
+
+
+ Interactive Features
+
+ Loading Modal
+ Scrollable Content
+ Form Modal
+
+
+
+
+
+
+
+
+ Event Log
+
+ @for (event of eventLog; track $index) {
+
{{ event }}
+ }
+ @if (eventLog.length === 0) {
+
No events yet...
+ }
+
+ Clear Log
+
+
+
+
+
+ This is a basic modal with default settings.
+ You can close it by clicking the X button, pressing Escape, or clicking outside the modal.
+
+
+
+
+ This is a small modal (400px width).
+
+
+
+ This is a medium modal (600px width) - the default size.
+
+
+
+ This is a large modal (800px width).
+ Perfect for forms, detailed content, or complex interfaces.
+
+
+
+ This is an extra large modal (1000px width).
+ Use this for very complex interfaces or wide content.
+
+
+
+ This modal takes up the entire screen.
+ Perfect for complex applications or immersive experiences.
+
+
+
+
+ This is the default modal variant.
+
+
+
+
+
+
Are you sure you want to continue? This action cannot be undone.
+
+
+
+ Cancel
+ Continue
+
+
+
+
+
+
+
This will permanently delete the item. This action cannot be undone.
+
+
+
+ Cancel
+ Delete
+
+
+
+
+
+
+
Your changes have been saved successfully!
+
+
+
+ Close
+
+
+
+
+
+
+
+
Modal without header
+
This modal doesn't have a header section.
+
Close
+
+
+
+
+ This modal has a footer with buttons aligned between start and end.
+
+
+
+ Help
+
+
+ Cancel
+ Save
+
+
+
+
+
+
+
This content spans the full width with no padding constraints.
+
+
+
+
+ This modal cannot be closed by clicking the X, pressing Escape, or clicking the backdrop.
+ You must use the Close button below.
+
+
+ Close
+
+
+
+
+
+
+
+
+
+
+
Long Content
+ @for (item of longContent; track $index) {
+
{{ item }}
+ }
+
+
+
+
+
+
+ Name:
+
+
+
+
+ Email:
+
+
+
+
+ Message:
+
+
+
+
+
+ Cancel
+ Submit
+
+
+
+
+
+ This modal is configured based on your selections:
+
+ Size: {{ customConfig.size }}
+ Variant: {{ customConfig.variant }}
+ Closable: {{ customConfig.closable ? 'Yes' : 'No' }}
+ Backdrop Closable: {{ customConfig.backdropClosable ? 'Yes' : 'No' }}
+ Show Header: {{ customConfig.showHeader ? 'Yes' : 'No' }}
+ Show Footer: {{ customConfig.showFooter ? 'Yes' : 'No' }}
+
+
+ @if (customConfig.showFooter) {
+
+ Close
+
+ }
+
+ `,
+ styleUrl: './modal-demo.component.scss'
+})
+export class ModalDemoComponent {
+ // Icons
+ readonly faExclamationTriangle = faExclamationTriangle;
+ readonly faCheckCircle = faCheckCircle;
+ readonly faInfoCircle = faInfoCircle;
+
+ // Modal states
+ basicModal = signal(false);
+ smallModal = signal(false);
+ mediumModal = signal(false);
+ largeModal = signal(false);
+ xlModal = signal(false);
+ fullscreenModal = signal(false);
+
+ defaultModal = signal(false);
+ warningModal = signal(false);
+ dangerModal = signal(false);
+ successModal = signal(false);
+
+ noHeaderModal = signal(false);
+ noFooterModal = signal(false);
+ noPaddingModal = signal(false);
+ notClosableModal = signal(false);
+
+ loadingModal = signal(false);
+ scrollableModal = signal(false);
+ formModal = signal(false);
+ customModal = signal(false);
+
+ // Demo data
+ eventLog: string[] = [];
+
+ customConfig = {
+ size: 'md' as any,
+ variant: 'default' as any,
+ closable: true,
+ backdropClosable: true,
+ showHeader: true,
+ showFooter: false
+ };
+
+ formData = {
+ name: '',
+ email: '',
+ message: ''
+ };
+
+ longContent = Array.from({ length: 50 }, (_, i) =>
+ `This is paragraph ${i + 1}. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
+ quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.`
+ );
+
+ // Simulate loading
+ constructor() {
+ setTimeout(() => {
+ if (this.loadingModal()) {
+ this.loadingModal.set(false);
+ this.logEvent('Loading completed');
+ }
+ }, 3000);
+ }
+
+ logEvent(event: string): void {
+ const timestamp = new Date().toLocaleTimeString();
+ this.eventLog.unshift(`[${timestamp}] ${event}`);
+ if (this.eventLog.length > 10) {
+ this.eventLog = this.eventLog.slice(0, 10);
+ }
+ }
+
+ clearEventLog(): void {
+ this.eventLog = [];
+ }
+
+ handleFormSubmit(): void {
+ this.logEvent(`Form submitted: ${JSON.stringify(this.formData)}`);
+ this.formModal.set(false);
+
+ // Reset form
+ this.formData = {
+ name: '',
+ email: '',
+ message: ''
+ };
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/overlay-container-demo/overlay-container-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/overlay-container-demo/overlay-container-demo.component.scss
new file mode 100644
index 0000000..bb1b69e
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/overlay-container-demo/overlay-container-demo.component.scss
@@ -0,0 +1,250 @@
+@use '../../../../../shared-ui/src/styles/semantic/index' as tokens;
+
+.demo-container {
+ padding: tokens.$semantic-spacing-layout-md;
+ max-width: 1200px;
+ margin: 0 auto;
+
+ h2 {
+ font-size: tokens.$semantic-typography-heading-h2-size;
+ color: tokens.$semantic-color-text-primary;
+ margin-bottom: tokens.$semantic-spacing-content-heading;
+ border-bottom: tokens.$semantic-border-width-1 solid tokens.$semantic-color-border-subtle;
+ padding-bottom: tokens.$semantic-spacing-content-paragraph;
+ }
+
+ h3 {
+ font-size: tokens.$semantic-typography-heading-h3-size;
+ color: tokens.$semantic-color-text-primary;
+ margin-bottom: tokens.$semantic-spacing-content-paragraph;
+ }
+}
+
+.demo-section {
+ margin-bottom: tokens.$semantic-spacing-layout-lg;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+}
+
+.demo-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: tokens.$semantic-spacing-component-sm;
+ margin-bottom: tokens.$semantic-spacing-content-paragraph;
+}
+
+.demo-button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-height: tokens.$semantic-sizing-button-height-md;
+ padding: tokens.$semantic-spacing-component-sm tokens.$semantic-spacing-component-md;
+
+ // Visual design
+ background: tokens.$semantic-color-primary;
+ color: tokens.$semantic-color-on-primary;
+ border: none;
+ border-radius: tokens.$semantic-border-radius-md;
+ box-shadow: tokens.$semantic-shadow-button-rest;
+
+ // Typography
+ font-size: tokens.$semantic-typography-font-size-md;
+ font-weight: tokens.$semantic-typography-font-weight-medium;
+ text-decoration: none;
+ white-space: nowrap;
+
+ // Transitions
+ transition: background-color tokens.$semantic-motion-duration-fast tokens.$semantic-motion-easing-ease,
+ box-shadow tokens.$semantic-motion-duration-fast tokens.$semantic-motion-easing-ease,
+ transform tokens.$semantic-motion-duration-fast tokens.$semantic-motion-easing-ease;
+
+ // Cursor
+ cursor: pointer;
+ user-select: none;
+
+ // Interactive states
+ &:hover {
+ background: tokens.$semantic-color-primary-hover;
+ box-shadow: tokens.$semantic-shadow-button-hover;
+ transform: translateY(-1px);
+ }
+
+ &:focus-visible {
+ outline: 2px solid tokens.$semantic-color-focus;
+ outline-offset: 2px;
+ }
+
+ &:active {
+ background: tokens.$semantic-color-primary-pressed;
+ box-shadow: tokens.$semantic-shadow-button-active;
+ transform: translateY(0);
+ }
+
+ &:disabled {
+ background: tokens.$semantic-color-surface-disabled;
+ color: tokens.$semantic-color-text-disabled;
+ box-shadow: none;
+ cursor: not-allowed;
+ transform: none;
+ }
+}
+
+.demo-stats {
+ display: flex;
+ flex-direction: column;
+ gap: tokens.$semantic-spacing-component-xs;
+
+ p {
+ font-size: tokens.$semantic-typography-font-size-md;
+ color: tokens.$semantic-color-text-secondary;
+ margin: 0;
+
+ &:before {
+ content: "📊 ";
+ margin-right: tokens.$semantic-spacing-component-xs;
+ }
+ }
+}
+
+.overlay-content {
+ padding: tokens.$semantic-spacing-component-lg;
+ text-align: center;
+
+ h3 {
+ font-size: tokens.$semantic-typography-heading-h3-size;
+ color: tokens.$semantic-color-text-primary;
+ margin: 0 0 tokens.$semantic-spacing-content-paragraph 0;
+ }
+
+ p {
+ font-size: tokens.$semantic-typography-font-size-md;
+ color: tokens.$semantic-color-text-secondary;
+ margin: 0 0 tokens.$semantic-spacing-content-heading 0;
+ line-height: 1.5;
+ }
+
+ button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-height: tokens.$semantic-sizing-button-height-md;
+ padding: tokens.$semantic-spacing-component-sm tokens.$semantic-spacing-component-lg;
+
+ // Visual design
+ background: tokens.$semantic-color-secondary;
+ color: tokens.$semantic-color-on-secondary;
+ border: tokens.$semantic-border-width-1 solid tokens.$semantic-color-border-primary;
+ border-radius: tokens.$semantic-border-radius-md;
+ box-shadow: tokens.$semantic-shadow-button-rest;
+
+ // Typography
+ font-size: tokens.$semantic-typography-font-size-md;
+ font-weight: tokens.$semantic-typography-font-weight-medium;
+
+ // Transitions
+ transition: background-color tokens.$semantic-motion-duration-fast tokens.$semantic-motion-easing-ease,
+ border-color tokens.$semantic-motion-duration-fast tokens.$semantic-motion-easing-ease,
+ box-shadow tokens.$semantic-motion-duration-fast tokens.$semantic-motion-easing-ease,
+ transform tokens.$semantic-motion-duration-fast tokens.$semantic-motion-easing-ease;
+
+ cursor: pointer;
+
+ &:hover {
+ background: tokens.$semantic-color-secondary-hover;
+ border-color: tokens.$semantic-color-border-primary;
+ box-shadow: tokens.$semantic-shadow-button-hover;
+ transform: translateY(-1px);
+ }
+
+ &:focus-visible {
+ outline: 2px solid tokens.$semantic-color-focus;
+ outline-offset: 2px;
+ }
+
+ &:active {
+ background: tokens.$semantic-color-secondary-pressed;
+ box-shadow: tokens.$semantic-shadow-button-active;
+ transform: translateY(0);
+ }
+ }
+}
+
+.focus-trap-demo {
+ display: flex;
+ flex-direction: column;
+ gap: tokens.$semantic-spacing-component-sm;
+ align-items: center;
+
+ button,
+ input {
+ min-height: tokens.$semantic-sizing-input-height-md;
+ padding: tokens.$semantic-spacing-component-sm tokens.$semantic-spacing-component-md;
+ border: tokens.$semantic-border-width-1 solid tokens.$semantic-color-border-primary;
+ border-radius: tokens.$semantic-border-radius-md;
+ font-size: tokens.$semantic-typography-font-size-md;
+
+ &:focus-visible {
+ outline: 2px solid tokens.$semantic-color-focus;
+ outline-offset: 2px;
+ }
+ }
+
+ input {
+ background: tokens.$semantic-color-surface-secondary;
+ color: tokens.$semantic-color-text-primary;
+
+ &::placeholder {
+ color: tokens.$semantic-color-text-tertiary;
+ }
+ }
+
+ button {
+ background: tokens.$semantic-color-tertiary;
+ color: tokens.$semantic-color-on-tertiary;
+ cursor: pointer;
+
+ &:hover {
+ background: tokens.$semantic-color-secondary-hover;
+ }
+
+ &:last-child {
+ background: tokens.$semantic-color-danger;
+ color: tokens.$semantic-color-on-danger;
+
+ &:hover {
+ background: tokens.$semantic-color-primary-hover;
+ }
+ }
+ }
+}
+
+// Responsive Design
+@media (max-width: #{tokens.$semantic-breakpoint-md - 1px}) {
+ .demo-container {
+ padding: tokens.$semantic-spacing-layout-sm;
+ }
+
+ .demo-row {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .demo-button {
+ width: 100%;
+ }
+}
+
+@media (max-width: #{tokens.$semantic-breakpoint-sm - 1px}) {
+ .overlay-content {
+ padding: tokens.$semantic-spacing-component-md;
+ }
+
+ .focus-trap-demo {
+ button,
+ input {
+ width: 100%;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/overlay-container-demo/overlay-container-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/overlay-container-demo/overlay-container-demo.component.ts
new file mode 100644
index 0000000..ee50560
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/overlay-container-demo/overlay-container-demo.component.ts
@@ -0,0 +1,271 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { OverlayContainerComponent } from '../../../../../ui-essentials/src/lib/components/overlays/overlay-container/overlay-container.component';
+
+@Component({
+ selector: 'ui-overlay-container-demo',
+ standalone: true,
+ imports: [CommonModule, OverlayContainerComponent],
+ template: `
+
+
Overlay Container Demo
+
+
+
+ Positions
+
+ @for (position of positions; track position) {
+
+ {{ position }} position
+
+ }
+
+
+
+
+
+ Sizes
+
+ @for (size of sizes; track size) {
+
+ {{ size }} size
+
+ }
+
+
+
+
+
+ Variants
+
+ @for (variant of variants; track variant) {
+
+ {{ variant }}
+
+ }
+
+
+
+
+
+ Features
+
+
+ No Backdrop
+
+
+ Backdrop Blur
+
+
+ Focus Trap
+
+
+ No ESC Close
+
+
+
+
+
+
+ Offset Positioning
+
+
+ Offset Overlay
+
+
+
+
+
+
+ Stats
+
+
Overlays shown: {{ overlayCount }}
+
Backdrop clicks: {{ backdropClickCount }}
+
ESC key presses: {{ escapeCount }}
+
+
+
+
+
+
+
+
{{ currentOverlay.title }}
+
{{ currentOverlay.description }}
+ @if (currentOverlay.focusTrap) {
+
+ First Button
+
+ Second Button
+ Close
+
+ } @else {
+
Close Overlay
+ }
+
+
+ `,
+ styleUrl: './overlay-container-demo.component.scss'
+})
+export class OverlayContainerDemoComponent {
+ positions = ['center', 'top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right'] as const;
+ sizes = ['auto', 'sm', 'md', 'lg', 'xl', 'fullscreen'] as const;
+ variants = ['default', 'elevated', 'floating', 'modal', 'popover'] as const;
+
+ // Stats
+ overlayCount = 0;
+ backdropClickCount = 0;
+ escapeCount = 0;
+
+ // Current overlay state
+ currentOverlay = {
+ visible: false,
+ position: 'center' as typeof this.positions[number],
+ size: 'auto' as typeof this.sizes[number],
+ variant: 'default' as typeof this.variants[number],
+ showBackdrop: true,
+ backdropBlur: false,
+ focusTrap: false,
+ escapeClosable: true,
+ offsetX: 0,
+ offsetY: 0,
+ title: 'Overlay Title',
+ description: 'This is an overlay container demonstration.'
+ };
+
+ showOverlay(position: typeof this.positions[number], size: typeof this.sizes[number], variant: typeof this.variants[number]): void {
+ this.currentOverlay = {
+ ...this.currentOverlay,
+ visible: true,
+ position,
+ size,
+ variant,
+ showBackdrop: true,
+ backdropBlur: false,
+ focusTrap: false,
+ escapeClosable: true,
+ offsetX: 0,
+ offsetY: 0,
+ title: `${variant} Overlay`,
+ description: `Position: ${position}, Size: ${size}, Variant: ${variant}`
+ };
+ this.overlayCount++;
+ }
+
+ showFeatureOverlay(feature: string): void {
+ let config = {
+ visible: true,
+ position: 'center' as const,
+ size: 'md' as const,
+ variant: 'default' as const,
+ showBackdrop: true,
+ backdropBlur: false,
+ focusTrap: false,
+ escapeClosable: true,
+ offsetX: 0,
+ offsetY: 0,
+ title: 'Feature Demo',
+ description: 'Demonstrating overlay features.'
+ };
+
+ switch (feature) {
+ case 'no-backdrop':
+ config.showBackdrop = false;
+ config.title = 'No Backdrop';
+ config.description = 'This overlay has no backdrop.';
+ break;
+ case 'backdrop-blur':
+ config.backdropBlur = true;
+ config.title = 'Backdrop Blur';
+ config.description = 'This overlay has a blurred backdrop effect.';
+ break;
+ case 'focus-trap':
+ config.focusTrap = true;
+ config.title = 'Focus Trap';
+ config.description = 'Try pressing Tab - focus is trapped within this overlay.';
+ break;
+ case 'no-escape':
+ config.escapeClosable = false;
+ config.title = 'No ESC Close';
+ config.description = 'ESC key will not close this overlay.';
+ break;
+ }
+
+ this.currentOverlay = { ...this.currentOverlay, ...config };
+ this.overlayCount++;
+ }
+
+ showOffsetOverlay(): void {
+ this.currentOverlay = {
+ ...this.currentOverlay,
+ visible: true,
+ position: 'top-right',
+ size: 'sm',
+ variant: 'floating',
+ showBackdrop: false,
+ backdropBlur: false,
+ focusTrap: false,
+ escapeClosable: true,
+ offsetX: -20,
+ offsetY: 20,
+ title: 'Offset Overlay',
+ description: 'This overlay is positioned with X and Y offsets.'
+ };
+ this.overlayCount++;
+ }
+
+ closeOverlay(): void {
+ this.currentOverlay.visible = false;
+ }
+
+ handleVisibilityChange(visible: boolean): void {
+ this.currentOverlay.visible = visible;
+ }
+
+ handleBackdropClick(): void {
+ this.backdropClickCount++;
+ console.log('Backdrop clicked');
+ }
+
+ handleEscapePress(): void {
+ this.escapeCount++;
+ console.log('ESC key pressed');
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/pagination-demo/pagination-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/pagination-demo/pagination-demo.component.ts
new file mode 100644
index 0000000..4a3d545
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/pagination-demo/pagination-demo.component.ts
@@ -0,0 +1,523 @@
+import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { PaginationComponent } from '../../../../../ui-essentials/src/lib/components/navigation/pagination';
+
+@Component({
+ selector: 'ui-pagination-demo',
+ standalone: true,
+ imports: [CommonModule, PaginationComponent],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
Pagination Component Showcase
+
+
+
+ Sizes
+
+
Small
+
+
+
+
+
Medium (Default)
+
+
+
+
+
Large
+
+
+
+
+
+
+
+ Alignment
+
+
Left Aligned
+
+
+
+
+
Center Aligned (Default)
+
+
+
+
+
Right Aligned
+
+
+
+
+
+
+
+ With Page Information
+
+
Basic Page Info
+
+
+
+
+
With Items Count
+
+
+
+
+
Without Info
+
+
+
+
+
+
+
+ Compact Style
+
+
Compact Pagination
+
+
+
+
+
Compact with Labels
+
+
+
+
+
+
+
+ Different Page Counts
+
+
Few Pages (5 total)
+
+
+
+
+
Many Pages (50 total)
+
+
+
+
+
Limited Visible Pages (maxVisible=5)
+
+
+
+
+
+
+
+ Labels and States
+
+
With Navigation Labels
+
+
+
+
+
Custom ARIA Labels
+
+
+
+
+
Disabled State
+
+
+
+
+
+
+
+ Interactive Pagination
+
+
Controlled Pagination
+
+
+ First Page
+
+
+ Last Page
+
+
+ Jump to Middle
+
+
+ Reset
+
+
+
+
+
+
+ @if (lastPageChange) {
+
+ Last page change: {{ lastPageChange }}
+
+ }
+
+
+
+
+ Usage Examples
+
+
Basic Pagination:
+
<ui-pagination
+ [currentPage]="currentPage"
+ [totalPages]="totalPages"
+ (pageChange)="onPageChange($event)">
+</ui-pagination>
+
+
With Item Information:
+
<ui-pagination
+ [currentPage]="currentPage"
+ [totalPages]="totalPages"
+ [totalItems]="totalItems"
+ [itemsPerPage]="itemsPerPage"
+ [showInfo]="true"
+ (pageChange)="onPageChange($event)">
+</ui-pagination>
+
+
Compact with Labels:
+
<ui-pagination
+ [currentPage]="currentPage"
+ [totalPages]="totalPages"
+ [compact]="true"
+ [showLabels]="true"
+ size="sm"
+ alignment="left"
+ previousLabel="Back"
+ nextLabel="Forward"
+ (pageChange)="onPageChange($event)">
+</ui-pagination>
+
+
Customized Pagination:
+
<ui-pagination
+ [currentPage]="currentPage"
+ [totalPages]="totalPages"
+ [maxVisible]="5"
+ size="lg"
+ alignment="center"
+ ariaLabel="Search results pagination"
+ [showInfo]="true"
+ (pageChange)="handlePageChange($event)">
+</ui-pagination>
+
+
+
+
+
+ Edge Cases
+
+
Single Page
+
+
+
+
+
Two Pages
+
+
+
+
+
Large Dataset (1000+ pages)
+
+
+
+
+
+ `,
+ styles: [`
+ h2 {
+ color: hsl(279, 14%, 11%);
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ border-bottom: 2px solid hsl(258, 100%, 47%);
+ padding-bottom: 0.5rem;
+ }
+
+ h3 {
+ color: hsl(279, 14%, 25%);
+ font-size: 1.5rem;
+ margin-bottom: 1rem;
+ }
+
+ h4 {
+ color: hsl(287, 12%, 35%);
+ font-size: 1.125rem;
+ margin-bottom: 0.75rem;
+ }
+
+ section {
+ border: 1px solid hsl(289, 14%, 90%);
+ border-radius: 8px;
+ padding: 1.5rem;
+ background: hsl(286, 20%, 99%);
+ }
+
+ pre {
+ font-size: 0.875rem;
+ line-height: 1.5;
+ margin: 0.5rem 0;
+ }
+
+ code {
+ font-family: 'JetBrains Mono', monospace;
+ color: #d63384;
+ }
+
+ button:hover {
+ background: #f0f0f0 !important;
+ }
+
+ button:active {
+ background: #e0e0e0 !important;
+ }
+ `]
+})
+export class PaginationDemoComponent {
+ // Size variants
+ currentPageSmall = signal(2);
+ currentPageMedium = signal(3);
+ currentPageLarge = signal(4);
+
+ // Alignment variants
+ currentPageLeft = signal(1);
+ currentPageCenter = signal(5);
+ currentPageRight = signal(8);
+
+ // Info variants
+ currentPageInfo = signal(3);
+ currentPageItems = signal(2);
+ currentPageNoInfo = signal(6);
+
+ // Compact variants
+ currentPageCompact = signal(4);
+ currentPageCompactLabels = signal(2);
+
+ // Different counts
+ currentPageFew = signal(3);
+ currentPageMany = signal(25);
+ currentPageLimited = signal(15);
+
+ // Labels and states
+ currentPageLabels = signal(1);
+ currentPageAria = signal(1);
+ currentPageDisabled = signal(5);
+
+ // Interactive
+ currentPageInteractive = signal(1);
+ totalPagesInteractive = signal(20);
+ totalItemsInteractive = signal(387);
+
+ // Edge cases
+ currentPageTwo = signal(1);
+ currentPageLargeDataset = signal(234);
+
+ // Constants for edge cases
+ totalPagesOne = signal(1);
+ currentPageOne = signal(1);
+ totalPagesTwo = signal(2);
+ totalPagesLargeDataset = signal(1247);
+
+ // Demo data
+ totalPagesDemo = signal(12);
+ totalPagesFew = signal(5);
+ totalPagesMany = signal(50);
+ totalItemsDemo = 234;
+ itemsPerPageDemo = 20;
+ totalPagesItems = signal(Math.ceil(this.totalItemsDemo / this.itemsPerPageDemo));
+
+ // Interactive state
+ lastPageChange: string = '';
+
+ onPageChange(event: any, type: string): void {
+ this.lastPageChange = `${type}: Page ${event.previousPage} → ${event.page}`;
+ console.log(`Page changed in ${type}:`, event);
+
+ // Update the appropriate signal based on type
+ switch (type) {
+ case 'small':
+ this.currentPageSmall.set(event.page);
+ break;
+ case 'medium':
+ this.currentPageMedium.set(event.page);
+ break;
+ case 'large':
+ this.currentPageLarge.set(event.page);
+ break;
+ case 'left':
+ this.currentPageLeft.set(event.page);
+ break;
+ case 'center':
+ this.currentPageCenter.set(event.page);
+ break;
+ case 'right':
+ this.currentPageRight.set(event.page);
+ break;
+ case 'info':
+ this.currentPageInfo.set(event.page);
+ break;
+ case 'items':
+ this.currentPageItems.set(event.page);
+ break;
+ case 'no-info':
+ this.currentPageNoInfo.set(event.page);
+ break;
+ case 'compact':
+ this.currentPageCompact.set(event.page);
+ break;
+ case 'compact-labels':
+ this.currentPageCompactLabels.set(event.page);
+ break;
+ case 'few':
+ this.currentPageFew.set(event.page);
+ break;
+ case 'many':
+ this.currentPageMany.set(event.page);
+ break;
+ case 'limited':
+ this.currentPageLimited.set(event.page);
+ break;
+ case 'labels':
+ this.currentPageLabels.set(event.page);
+ break;
+ case 'aria':
+ this.currentPageAria.set(event.page);
+ break;
+ case 'two':
+ this.currentPageTwo.set(event.page);
+ break;
+ case 'large-dataset':
+ this.currentPageLargeDataset.set(event.page);
+ break;
+ }
+ }
+
+ onInteractivePageChange(event: any): void {
+ this.currentPageInteractive.set(event.page);
+ this.lastPageChange = `Interactive: Page ${event.previousPage} → ${event.page}`;
+ }
+
+ // Interactive controls
+ goToFirstPage(): void {
+ this.currentPageInteractive.set(1);
+ this.lastPageChange = 'Programmatically navigated to first page';
+ }
+
+ goToLastPage(): void {
+ this.currentPageInteractive.set(this.totalPagesInteractive());
+ this.lastPageChange = 'Programmatically navigated to last page';
+ }
+
+ jumpToMiddle(): void {
+ const middle = Math.ceil(this.totalPagesInteractive() / 2);
+ this.currentPageInteractive.set(middle);
+ this.lastPageChange = `Programmatically jumped to middle page (${middle})`;
+ }
+
+ resetInteractive(): void {
+ this.currentPageInteractive.set(1);
+ this.lastPageChange = 'Interactive pagination reset';
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/progress-demo/progress-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/progress-demo/progress-demo.component.ts
new file mode 100644
index 0000000..b42d327
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/progress-demo/progress-demo.component.ts
@@ -0,0 +1,584 @@
+import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { ProgressBarComponent } from '../../../../../ui-essentials/src/lib/components/data-display/progress';
+
+@Component({
+ selector: 'ui-progress-demo',
+ standalone: true,
+ imports: [
+ CommonModule,
+ FormsModule,
+ ProgressBarComponent
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
Progress Bar Component Showcase
+
+
+
+ Basic Progress Bars
+
+
+
+
+
+
+
+
+
+
+
+
+ Color Variants
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Progress Bar Types
+
+
+
+
+
+
+
+
+
+
+
+
+ Striped and Animated Progress Bars
+
+
+
+
+
+
+
+
+
+
+
+
+ Progress Bar States
+
+
+
+
+
+
+
+
+
+
+ Size and Variant Combinations
+
+
+
+
+
Small Size Variants
+
+
+
+
+
+
+
+
+
+
Large Size Variants
+
+
+
+
+
+
+
+
+
+
+
+
+ Progress Bars without Labels
+
+
+
+
+
+
+
+
+
+
+
+
+ Interactive Demo
+
+
+
+
+
+ Progress Value:
+
+ {{ demoProgress }}%
+
+
+
+ Buffer Value:
+
+ {{ demoBuffer }}%
+
+
+
+ Size:
+
+ Small
+ Medium
+ Large
+
+
+
+
+ Variant:
+
+ Primary
+ Secondary
+ Success
+ Warning
+ Danger
+
+
+
+
+ Type:
+
+ Determinate
+ Indeterminate
+ Buffer
+
+
+
+
+
+
+
+ Start Demo
+
+
+ Reset
+
+
+
+
+
+
+
Live Preview:
+
+
+
+
+
+
+
+
+
+
+ Usage Examples
+
+
Basic Progress Bar:
+
<ui-progress-bar
+ label="Download Progress"
+ size="md"
+ variant="primary"
+ [progress]="75"
+ helperText="3 of 4 files downloaded">
+</ui-progress-bar>
+
+
Indeterminate Progress:
+
<ui-progress-bar
+ label="Loading..."
+ progressType="indeterminate"
+ variant="primary"
+ [animated]="true"
+ helperText="Please wait while we process your request">
+</ui-progress-bar>
+
+
Buffer Progress:
+
<ui-progress-bar
+ label="Video Loading"
+ progressType="buffer"
+ [progress]="currentProgress"
+ [buffer]="bufferProgress"
+ variant="secondary"
+ helperText="Buffering video content">
+</ui-progress-bar>
+
+
Striped Animated Progress:
+
<ui-progress-bar
+ label="Upload Progress"
+ [progress]="uploadPercent"
+ variant="success"
+ [striped]="true"
+ [animated]="true"
+ size="lg"
+ helperText="Uploading files...">
+</ui-progress-bar>
+
+
+
+
+
+ Programmatic Control Example
+
+
+
+
+
+
+ Simulate Progress
+
+
+ Pause
+
+
+ Reset
+
+
+ Complete
+
+
+
+
+ Current Progress: {{ programmableValue() }}% |
+ Status: {{ progressStatus() }}
+
+
+
+
+
+ `,
+ styles: [`
+ h2 {
+ color: hsl(279, 14%, 11%);
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ border-bottom: 2px solid hsl(258, 100%, 47%);
+ padding-bottom: 0.5rem;
+ }
+
+ h3 {
+ color: hsl(279, 14%, 25%);
+ font-size: 1.5rem;
+ margin-bottom: 1rem;
+ }
+
+ h4 {
+ color: hsl(287, 12%, 35%);
+ font-size: 1.125rem;
+ margin-bottom: 0.75rem;
+ }
+
+ section {
+ border: 1px solid hsl(289, 14%, 90%);
+ border-radius: 8px;
+ padding: 1.5rem;
+ background: hsl(286, 20%, 99%);
+ }
+
+ pre {
+ font-size: 0.875rem;
+ line-height: 1.5;
+ margin: 0.5rem 0;
+ }
+
+ code {
+ font-family: 'JetBrains Mono', monospace;
+ color: #d63384;
+ }
+
+ select, input[type="checkbox"], input[type="range"] {
+ cursor: pointer;
+ }
+
+ label {
+ cursor: pointer;
+ }
+
+ button:hover {
+ opacity: 0.9;
+ }
+
+ button:active {
+ transform: scale(0.98);
+ }
+ `]
+})
+export class ProgressDemoComponent {
+
+ // Animated progress values
+ determinateProgress = signal(35);
+ bufferProgress = signal(25);
+ bufferValue = signal(65);
+
+ // Interactive demo properties
+ demoProgress = 60;
+ demoBuffer = 80;
+ demoSize: 'sm' | 'md' | 'lg' = 'md';
+ demoVariant: 'primary' | 'secondary' | 'success' | 'warning' | 'danger' = 'primary';
+ demoType: 'determinate' | 'indeterminate' | 'buffer' = 'determinate';
+ demoStriped = false;
+ demoAnimated = false;
+ demoShowPercentage = true;
+ demoDisabled = false;
+
+ // Programmable progress
+ programmableValue = signal(0);
+ progressStatus = signal('Ready');
+ private progressInterval: any;
+ private isPaused = false;
+
+ ngOnInit(): void {
+ // Start some animated progress bars
+ this.animateProgress();
+ }
+
+ ngOnDestroy(): void {
+ if (this.progressInterval) {
+ clearInterval(this.progressInterval);
+ }
+ }
+
+ private animateProgress(): void {
+ // Animate the determinate progress
+ setInterval(() => {
+ const current = this.determinateProgress();
+ const next = (current + 1) % 101;
+ this.determinateProgress.set(next);
+ }, 100);
+
+ // Animate the buffer progress
+ setInterval(() => {
+ const currentBuffer = this.bufferValue();
+ const currentProgress = this.bufferProgress();
+
+ const nextBuffer = (currentBuffer + 0.5) % 101;
+ const nextProgress = Math.min(nextBuffer - 20, 100);
+
+ this.bufferValue.set(nextBuffer);
+ this.bufferProgress.set(Math.max(0, nextProgress));
+ }, 150);
+ }
+
+ startDemo(): void {
+ this.demoProgress = 0;
+ let progress = 0;
+
+ const interval = setInterval(() => {
+ progress += Math.random() * 3;
+ this.demoProgress = Math.min(100, progress);
+
+ if (progress >= 100) {
+ clearInterval(interval);
+ }
+ }, 100);
+ }
+
+ resetDemo(): void {
+ this.demoProgress = 0;
+ this.demoBuffer = 0;
+ this.demoSize = 'md';
+ this.demoVariant = 'primary';
+ this.demoType = 'determinate';
+ this.demoStriped = false;
+ this.demoAnimated = false;
+ this.demoShowPercentage = true;
+ this.demoDisabled = false;
+ }
+
+ simulateProgress(): void {
+ if (this.progressInterval) {
+ clearInterval(this.progressInterval);
+ }
+
+ this.isPaused = false;
+ this.progressStatus.set('Processing...');
+
+ this.progressInterval = setInterval(() => {
+ if (!this.isPaused) {
+ const current = this.programmableValue();
+ if (current < 100) {
+ this.programmableValue.set(current + Math.random() * 2);
+ } else {
+ this.progressStatus.set('Complete!');
+ clearInterval(this.progressInterval);
+ }
+ }
+ }, 50);
+ }
+
+ pauseProgress(): void {
+ this.isPaused = !this.isPaused;
+ this.progressStatus.set(this.isPaused ? 'Paused' : 'Processing...');
+ }
+
+ resetProgrammable(): void {
+ if (this.progressInterval) {
+ clearInterval(this.progressInterval);
+ }
+ this.programmableValue.set(0);
+ this.progressStatus.set('Ready');
+ this.isPaused = false;
+ }
+
+ completeProgress(): void {
+ if (this.progressInterval) {
+ clearInterval(this.progressInterval);
+ }
+ this.programmableValue.set(100);
+ this.progressStatus.set('Complete!');
+ this.isPaused = false;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/radio-demo/radio-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/radio-demo/radio-demo.component.ts
new file mode 100644
index 0000000..54ca59f
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/radio-demo/radio-demo.component.ts
@@ -0,0 +1,665 @@
+import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import {
+ RadioButtonComponent,
+ RadioGroupComponent,
+ RadioButtonData
+} from '../../../../../ui-essentials/src/lib/components/forms/radio';
+
+@Component({
+ selector: 'ui-radio-demo',
+ standalone: true,
+ imports: [
+ CommonModule,
+ FormsModule,
+ RadioButtonComponent,
+ RadioGroupComponent
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
Radio Button Component Showcase
+
+
+
+ Basic Radio Buttons
+
+
+
+
Individual Radio Buttons
+
+
+
+
+
+
+
+
+
+
Radio Group
+
+
+
+
+
+
+ Size Variants
+
+
+
+
Small (sm)
+
+
+
+
+
+
Medium (md) - Default
+
+
+
+
+
+
Large (lg)
+
+
+
+
+
+
+ Color Variants
+
+
+
+
Primary (Default)
+
+
+
+
+
+
Secondary
+
+
+
+
+
+
Success
+
+
+
+
+
+
Warning
+
+
+
+
+
+
Danger
+
+
+
+
+
+
+ Orientation
+
+
+
+
Vertical (Default)
+
+
+
+
+
+
Horizontal
+
+
+
+
+
+
+ Radio Buttons with Descriptions
+
+
+
+
+
+ Radio Button States
+
+
+
+
Disabled Group
+
+
+
+
+
+
Individual Disabled Options
+
+
+
+
+
+
Error State
+
+
+
+
+
+
With Helper Text
+
+
+
+
+
+
+ Dense Layout
+
+
+
+
Normal Spacing
+
+
+
+
+
+
Dense Spacing
+
+
+
+
+
+
+
+ Usage Examples
+
+
Basic Radio Group:
+
<ui-radio-group
+ label="Choose an option"
+ [options]="options"
+ name="radio-group"
+ [(ngModel)]="selectedValue"
+ (selectionChange)="onSelectionChange($event)">
+</ui-radio-group>
+
+
Individual Radio Button:
+
<ui-radio-button
+ value="option1"
+ label="Option 1"
+ name="radio-name"
+ size="md"
+ variant="primary"
+ [(ngModel)]="selectedValue"
+ (selectionChange)="onSelectionChange($event)">
+</ui-radio-button>
+
+
With Descriptions:
+
<ui-radio-group
+ label="Subscription Plans"
+ [options]="[
+ {{ '{' }}
+ value: 'basic',
+ label: 'Basic Plan',
+ description: 'Perfect for individuals'
+ {{ '}' }},
+ {{ '{' }}
+ value: 'pro',
+ label: 'Pro Plan',
+ description: 'For small teams'
+ {{ '}' }}
+ ]"
+ name="subscription"
+ [(ngModel)]="plan">
+</ui-radio-group>
+
+
Horizontal Layout:
+
<ui-radio-group
+ label="Payment Method"
+ [options]="paymentOptions"
+ orientation="horizontal"
+ name="payment"
+ [(ngModel)]="paymentMethod">
+</ui-radio-group>
+
+
+
+
+
+ Interactive Controls
+
+
+ Reset All Selections
+
+
+ Fill Sample Selections
+
+
+ {{ globalDisabled() ? 'Enable All' : 'Disable All' }}
+
+
+
+ @if (lastSelection()) {
+
+ Last selection: {{ lastSelection() }}
+
+ }
+
+
+
+
+ Current Values
+
+
{{ getCurrentValues() }}
+
+
+
+ `,
+ styles: [`
+ h2 {
+ color: hsl(279, 14%, 11%);
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ border-bottom: 2px solid hsl(258, 100%, 47%);
+ padding-bottom: 0.5rem;
+ }
+
+ h3 {
+ color: hsl(279, 14%, 25%);
+ font-size: 1.5rem;
+ margin-bottom: 1rem;
+ }
+
+ h4 {
+ color: hsl(287, 12%, 35%);
+ font-size: 1.125rem;
+ margin-bottom: 0.75rem;
+ }
+
+ section {
+ border: 1px solid hsl(289, 14%, 90%);
+ border-radius: 8px;
+ padding: 1.5rem;
+ background: hsl(286, 20%, 99%);
+ }
+
+ pre {
+ font-size: 0.875rem;
+ line-height: 1.5;
+ margin: 0.5rem 0;
+ }
+
+ code {
+ font-family: 'JetBrains Mono', monospace;
+ color: #d63384;
+ }
+
+ button {
+ transition: all 0.2s ease-in-out;
+ }
+
+ button:hover {
+ opacity: 0.9;
+ transform: translateY(-1px);
+ }
+ `]
+})
+export class RadioDemoComponent {
+ // Basic selections
+ basicSelection = '';
+ groupSelection = '';
+
+ // Size selections
+ sizeSelections: Record = {
+ 'sm': '',
+ 'md': '',
+ 'lg': ''
+ };
+
+ // Variant selections
+ variantSelections: Record = {
+ 'primary': '',
+ 'secondary': '',
+ 'success': '',
+ 'warning': '',
+ 'danger': ''
+ };
+
+ // Orientation selections
+ orientationSelections: Record = {
+ 'vertical': '',
+ 'horizontal': ''
+ };
+
+ // Other selections
+ planSelection = '';
+ disabledSelection = '';
+ errorSelection = '';
+ notificationSelection = '';
+
+ // Dense selections
+ denseSelections: Record = {
+ 'normal': '',
+ 'dense': ''
+ };
+
+ // Signals for reactive state
+ lastSelection = signal('');
+ globalDisabled = signal(false);
+
+ // Option data
+ basicOptions: RadioButtonData[] = [
+ { value: 'basic1', label: 'Basic Option 1' },
+ { value: 'basic2', label: 'Basic Option 2' },
+ { value: 'basic3', label: 'Basic Option 3' }
+ ];
+
+ sizeOptions: RadioButtonData[] = [
+ { value: 'size1', label: 'First Choice' },
+ { value: 'size2', label: 'Second Choice' },
+ { value: 'size3', label: 'Third Choice' }
+ ];
+
+ colorOptions: RadioButtonData[] = [
+ { value: 'color1', label: 'Red' },
+ { value: 'color2', label: 'Blue' },
+ { value: 'color3', label: 'Green' }
+ ];
+
+ orientationOptions: RadioButtonData[] = [
+ { value: 'orient1', label: 'Option A' },
+ { value: 'orient2', label: 'Option B' },
+ { value: 'orient3', label: 'Option C' },
+ { value: 'orient4', label: 'Option D' }
+ ];
+
+ planOptions: RadioButtonData[] = [
+ {
+ value: 'basic',
+ label: 'Basic Plan',
+ description: 'Perfect for individuals and small projects. Includes 5GB storage and basic support.'
+ },
+ {
+ value: 'pro',
+ label: 'Professional Plan',
+ description: 'Great for small teams. Includes 50GB storage, priority support, and advanced features.'
+ },
+ {
+ value: 'enterprise',
+ label: 'Enterprise Plan',
+ description: 'For large organizations. Unlimited storage, 24/7 support, and custom integrations.'
+ }
+ ];
+
+ stateOptions: RadioButtonData[] = [
+ { value: 'state1', label: 'Option 1' },
+ { value: 'state2', label: 'Option 2' },
+ { value: 'state3', label: 'Option 3' }
+ ];
+
+ disabledOptions: RadioButtonData[] = [
+ { value: 'available1', label: 'Available Option 1' },
+ { value: 'disabled1', label: 'Disabled Option', disabled: true },
+ { value: 'available2', label: 'Available Option 2' },
+ { value: 'disabled2', label: 'Another Disabled Option', disabled: true }
+ ];
+
+ errorOptions: RadioButtonData[] = [
+ { value: 'error1', label: 'Option 1' },
+ { value: 'error2', label: 'Option 2' },
+ { value: 'error3', label: 'Option 3' }
+ ];
+
+ notificationOptions: RadioButtonData[] = [
+ { value: 'email', label: 'Email notifications' },
+ { value: 'sms', label: 'SMS notifications' },
+ { value: 'push', label: 'Push notifications' },
+ { value: 'none', label: 'No notifications' }
+ ];
+
+ denseOptions: RadioButtonData[] = [
+ { value: 'dense1', label: 'Compact Option 1' },
+ { value: 'dense2', label: 'Compact Option 2' },
+ { value: 'dense3', label: 'Compact Option 3' },
+ { value: 'dense4', label: 'Compact Option 4' }
+ ];
+
+ handleSelectionChange(group: string, value: string): void {
+ this.lastSelection.set(`${group}: ${value}`);
+ console.log(`Selection changed in ${group}: ${value}`);
+ }
+
+ handleSizeSelection(size: string, value: string): void {
+ this.sizeSelections[size] = value;
+ this.handleSelectionChange(`size-${size}`, value);
+ }
+
+ handleVariantSelection(variant: string, value: string): void {
+ this.variantSelections[variant] = value;
+ this.handleSelectionChange(`variant-${variant}`, value);
+ }
+
+ handleOrientationSelection(orientation: string, value: string): void {
+ this.orientationSelections[orientation] = value;
+ this.handleSelectionChange(`orientation-${orientation}`, value);
+ }
+
+ handleDenseSelection(type: string, value: string): void {
+ this.denseSelections[type] = value;
+ this.handleSelectionChange(`dense-${type}`, value);
+ }
+
+ resetAllSelections(): void {
+ this.basicSelection = '';
+ this.groupSelection = '';
+ this.planSelection = '';
+ this.disabledSelection = '';
+ this.errorSelection = '';
+ this.notificationSelection = '';
+
+ Object.keys(this.sizeSelections).forEach(key => {
+ this.sizeSelections[key] = '';
+ });
+
+ Object.keys(this.variantSelections).forEach(key => {
+ this.variantSelections[key] = '';
+ });
+
+ Object.keys(this.orientationSelections).forEach(key => {
+ this.orientationSelections[key] = '';
+ });
+
+ Object.keys(this.denseSelections).forEach(key => {
+ this.denseSelections[key] = '';
+ });
+
+ this.lastSelection.set('All selections reset');
+ console.log('All selections reset');
+ }
+
+ fillSampleSelections(): void {
+ this.basicSelection = 'option2';
+ this.groupSelection = 'basic2';
+ this.planSelection = 'pro';
+ this.notificationSelection = 'email';
+ this.sizeSelections['md'] = 'size2';
+ this.variantSelections['primary'] = 'color1';
+ this.orientationSelections['horizontal'] = 'orient3';
+ this.denseSelections['dense'] = 'dense2';
+
+ this.lastSelection.set('Sample selections filled');
+ console.log('Sample selections filled');
+ }
+
+ toggleDisabled(): void {
+ this.globalDisabled.set(!this.globalDisabled());
+ this.lastSelection.set(`All controls ${this.globalDisabled() ? 'disabled' : 'enabled'}`);
+ console.log(`Global disabled state: ${this.globalDisabled()}`);
+ }
+
+ getCurrentValues(): string {
+ const values = {
+ basic: this.basicSelection,
+ group: this.groupSelection,
+ plan: this.planSelection,
+ disabled: this.disabledSelection,
+ error: this.errorSelection,
+ notification: this.notificationSelection,
+ sizes: this.sizeSelections,
+ variants: this.variantSelections,
+ orientations: this.orientationSelections,
+ dense: this.denseSelections
+ };
+
+ return JSON.stringify(values, null, 2);
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/search-demo/search-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/search-demo/search-demo.component.ts
new file mode 100644
index 0000000..8b30265
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/search-demo/search-demo.component.ts
@@ -0,0 +1,550 @@
+import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { SearchBarComponent, SearchSuggestion } from '../../../../../ui-essentials/src/lib/components/forms/search';
+import {
+ faSearch,
+ faMicrophone,
+ faCamera,
+ faFilter,
+ faUser,
+ faFile,
+ faFolder,
+ faTag,
+ faHeart,
+ faStar
+} from '@fortawesome/free-solid-svg-icons';
+
+@Component({
+ selector: 'ui-search-demo',
+ standalone: true,
+ imports: [
+ CommonModule,
+ FormsModule,
+ SearchBarComponent
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
Search Bar Component Showcase
+
+
+
+ Basic Search Bars
+
+
+
+
+
+
+
+
+
+ Size Variants
+
+
+
+
+
+
+
+
+
+ Style Variants
+
+
+
+
+
+
+
+
+
+
+
+
+ With Suggestions
+
+
+
+
+
+
+
+
+
+
+
+ Advanced Features
+
+
+
+
+
+
+
+
+
+ Interactive Controls
+
+
+ Clear All Searches
+
+
+ Fill Sample Searches
+
+
+ {{ globalDisabled() ? 'Enable All' : 'Disable All' }}
+
+
+
+ @if (lastAction()) {
+
+ Last action: {{ lastAction() }}
+
+ }
+
+
+
+
+ Code Examples
+
+
Basic Usage:
+
<ui-search-bar
+ placeholder="Search..."
+ [(ngModel)]="searchValue"
+ (searchChange)="onSearchChange($event)"
+ (searchSubmit)="onSearchSubmit($event)">
+</ui-search-bar>
+
+
With Suggestions:
+
<ui-search-bar
+ placeholder="Search with suggestions..."
+ [suggestions]="suggestions"
+ [(ngModel)]="searchValue"
+ (suggestionSelect)="onSuggestionSelect($event)">
+</ui-search-bar>
+
+
With Icons:
+
<ui-search-bar
+ placeholder="Search with icons..."
+ [leadingIcon]="faSearch"
+ [trailingIcons]="iconButtons"
+ variant="filled"
+ size="lg"
+ (trailingIconClick)="onIconClick($event)">
+</ui-search-bar>
+
+
+
+
+
+ Current Values
+
+
{{ getCurrentValues() }}
+
+
+
+ `,
+ styles: [`
+ h2 {
+ color: hsl(279, 14%, 11%);
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ border-bottom: 2px solid hsl(258, 100%, 47%);
+ padding-bottom: 0.5rem;
+ }
+
+ h3 {
+ color: hsl(279, 14%, 25%);
+ font-size: 1.5rem;
+ margin-bottom: 1rem;
+ }
+
+ h4 {
+ color: hsl(279, 14%, 35%);
+ font-size: 1.2rem;
+ margin-bottom: 0.5rem;
+ }
+
+ pre {
+ margin: 0;
+ font-family: 'Courier New', monospace;
+ font-size: 0.9rem;
+ line-height: 1.4;
+ }
+
+ code {
+ color: hsl(279, 14%, 15%);
+ }
+
+ section {
+ border: 1px solid #e9ecef;
+ padding: 1.5rem;
+ border-radius: 8px;
+ background: #ffffff;
+ }
+
+ button:hover {
+ opacity: 0.9;
+ transform: translateY(-1px);
+ }
+ `]
+})
+export class SearchDemoComponent {
+ // FontAwesome icons
+ protected readonly faSearch = faSearch;
+ protected readonly faMicrophone = faMicrophone;
+ protected readonly faCamera = faCamera;
+ protected readonly faFilter = faFilter;
+ protected readonly faUser = faUser;
+ protected readonly faFile = faFile;
+ protected readonly faFolder = faFolder;
+ protected readonly faTag = faTag;
+ protected readonly faHeart = faHeart;
+ protected readonly faStar = faStar;
+
+ // Basic search values
+ basicSearch1 = signal('');
+ basicSearch2 = signal('');
+ basicSearch3 = signal('');
+
+ // Size variants
+ sizeSmall = signal('');
+ sizeMedium = signal('');
+ sizeLarge = signal('');
+
+ // Variant styles
+ variantOutlined = signal('');
+ variantFilled = signal('');
+ variantElevated = signal('');
+
+ // Icon searches
+ iconSearch1 = signal('');
+ iconSearch2 = signal('');
+ iconSearch3 = signal('');
+
+ // Suggestion searches
+ suggestionSearch1 = signal('');
+ suggestionSearch2 = signal('');
+
+ // State searches
+ stateDisabled = signal('Disabled value');
+ stateReadonly = signal('Readonly value');
+ stateError = signal('');
+ stateHelper = signal('');
+
+ // Advanced searches
+ advancedSearch1 = signal('');
+ advancedSearch2 = signal('');
+ advancedSearch3 = signal('');
+
+ // Control states
+ globalDisabled = signal(false);
+ lastAction = signal('');
+
+ // Icon configurations
+ basicTrailingIcons = [
+ {
+ id: 'mic',
+ icon: faMicrophone,
+ label: 'Voice search',
+ disabled: false
+ }
+ ];
+
+ multipleTrailingIcons = [
+ {
+ id: 'mic',
+ icon: faMicrophone,
+ label: 'Voice search',
+ disabled: false
+ },
+ {
+ id: 'camera',
+ icon: faCamera,
+ label: 'Image search',
+ disabled: false
+ },
+ {
+ id: 'filter',
+ icon: faFilter,
+ label: 'Filter results',
+ disabled: false
+ }
+ ];
+
+ // Search suggestions
+ suggestions: SearchSuggestion[] = [
+ { id: '1', text: 'Angular components', icon: faFile },
+ { id: '2', text: 'TypeScript interfaces', icon: faFile },
+ { id: '3', text: 'SCSS mixins', icon: faFile },
+ { id: '4', text: 'Design tokens', icon: faTag },
+ { id: '5', text: 'Material Design', icon: faHeart },
+ { id: '6', text: 'Component library', icon: faFolder },
+ { id: '7', text: 'UI patterns', icon: faStar },
+ { id: '8', text: 'Accessibility guidelines', icon: faFile }
+ ];
+
+ categorizedSuggestions: SearchSuggestion[] = [
+ { id: '1', text: 'User profile', category: 'Users', icon: faUser },
+ { id: '2', text: 'Project documentation', category: 'Files', icon: faFile },
+ { id: '3', text: 'Design system', category: 'Files', icon: faFolder },
+ { id: '4', text: 'Component library', category: 'Code', icon: faTag },
+ { id: '5', text: 'UI components', category: 'Code', icon: faTag },
+ { id: '6', text: 'Admin settings', category: 'Settings', icon: faFilter }
+ ];
+
+ onSearchChange(searchId: string, value: string): void {
+ this.lastAction.set(`Search changed: ${searchId} = "${value}"`);
+ console.log(`Search ${searchId} changed:`, value);
+ }
+
+ onSearchSubmit(searchId: string, value: string): void {
+ this.lastAction.set(`Search submitted: ${searchId} = "${value}"`);
+ console.log(`Search ${searchId} submitted:`, value);
+ }
+
+ onSuggestionSelect(suggestion: SearchSuggestion): void {
+ this.lastAction.set(`Suggestion selected: "${suggestion.text}" (${suggestion.id})`);
+ console.log('Suggestion selected:', suggestion);
+ }
+
+ onTrailingIconClick(iconData: {id: string, icon: any}): void {
+ this.lastAction.set(`Icon clicked: ${iconData.id}`);
+ console.log('Trailing icon clicked:', iconData);
+ }
+
+ clearAllSearches(): void {
+ this.basicSearch1.set('');
+ this.basicSearch2.set('');
+ this.basicSearch3.set('');
+ this.sizeSmall.set('');
+ this.sizeMedium.set('');
+ this.sizeLarge.set('');
+ this.variantOutlined.set('');
+ this.variantFilled.set('');
+ this.variantElevated.set('');
+ this.iconSearch1.set('');
+ this.iconSearch2.set('');
+ this.iconSearch3.set('');
+ this.suggestionSearch1.set('');
+ this.suggestionSearch2.set('');
+ this.stateError.set('');
+ this.stateHelper.set('');
+ this.advancedSearch1.set('');
+ this.advancedSearch2.set('');
+ this.advancedSearch3.set('');
+
+ this.lastAction.set('All searches cleared');
+ }
+
+ fillSampleSearches(): void {
+ this.basicSearch1.set('Sample search');
+ this.basicSearch2.set('Labeled search');
+ this.basicSearch3.set('Required search');
+ this.sizeSmall.set('Small');
+ this.sizeMedium.set('Medium');
+ this.sizeLarge.set('Large');
+ this.variantOutlined.set('Outlined');
+ this.variantFilled.set('Filled');
+ this.variantElevated.set('Elevated');
+ this.iconSearch1.set('User search');
+ this.iconSearch2.set('Voice search');
+ this.iconSearch3.set('Multi-icon search');
+ this.suggestionSearch1.set('Angular');
+ this.suggestionSearch2.set('Design');
+ this.stateError.set('Error text');
+ this.stateHelper.set('Helper text');
+ this.advancedSearch1.set('Advanced');
+ this.advancedSearch2.set('Focused');
+ this.advancedSearch3.set('Limited');
+
+ this.lastAction.set('All searches filled with sample data');
+ }
+
+ toggleStates(): void {
+ this.globalDisabled.update(disabled => !disabled);
+ this.lastAction.set(`Global state: ${this.globalDisabled() ? 'disabled' : 'enabled'}`);
+ }
+
+ getCurrentValues(): string {
+ const values = {
+ basic: {
+ search1: this.basicSearch1(),
+ search2: this.basicSearch2(),
+ search3: this.basicSearch3()
+ },
+ sizes: {
+ small: this.sizeSmall(),
+ medium: this.sizeMedium(),
+ large: this.sizeLarge()
+ },
+ variants: {
+ outlined: this.variantOutlined(),
+ filled: this.variantFilled(),
+ elevated: this.variantElevated()
+ },
+ icons: {
+ icon1: this.iconSearch1(),
+ icon2: this.iconSearch2(),
+ icon3: this.iconSearch3()
+ },
+ suggestions: {
+ suggestion1: this.suggestionSearch1(),
+ suggestion2: this.suggestionSearch2()
+ },
+ states: {
+ disabled: this.stateDisabled(),
+ readonly: this.stateReadonly(),
+ error: this.stateError(),
+ helper: this.stateHelper()
+ },
+ advanced: {
+ advanced1: this.advancedSearch1(),
+ advanced2: this.advancedSearch2(),
+ advanced3: this.advancedSearch3()
+ },
+ controls: {
+ globalDisabled: this.globalDisabled(),
+ lastAction: this.lastAction()
+ }
+ };
+
+ return JSON.stringify(values, null, 2);
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/skeleton-loader-demo/skeleton-loader-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/skeleton-loader-demo/skeleton-loader-demo.component.scss
new file mode 100644
index 0000000..2eb4483
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/skeleton-loader-demo/skeleton-loader-demo.component.scss
@@ -0,0 +1,317 @@
+.demo-container {
+ padding: 2rem;
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.demo-section {
+ margin-bottom: 3rem;
+
+ h3 {
+ font-size: 1.5rem;
+ color: #1f2937;
+ margin-bottom: 1rem;
+ border-bottom: 1px solid #d1d5db;
+ padding-bottom: 0.5rem;
+ }
+}
+
+.demo-subsection {
+ margin-bottom: 1.5rem;
+
+ h4 {
+ font-size: 1.125rem;
+ color: #4b5563;
+ margin-bottom: 0.75rem;
+ }
+}
+
+.demo-row {
+ display: flex;
+ gap: 1rem;
+ flex-wrap: wrap;
+ margin-bottom: 1rem;
+}
+
+.demo-column {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+ margin-bottom: 1rem;
+}
+
+.demo-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 1.5rem;
+ margin-bottom: 1rem;
+}
+
+.demo-item {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ padding: 1rem;
+ background: #f9fafb;
+ border-radius: 0.5rem;
+ border: 1px solid #e5e7eb;
+
+ label {
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: #6b7280;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ }
+}
+
+.demo-interactive {
+ background: #f9fafb;
+ border: 1px solid #e5e7eb;
+ border-radius: 0.75rem;
+ padding: 1.5rem;
+
+ .demo-controls {
+ display: flex;
+ gap: 1rem;
+ align-items: center;
+ margin-bottom: 1rem;
+ padding-bottom: 1rem;
+ border-bottom: 1px solid #e5e7eb;
+
+ .toggle-button {
+ background: #3b82f6;
+ color: white;
+ border: none;
+ border-radius: 0.5rem;
+ padding: 0.5rem 1rem;
+ font-size: 0.875rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background: #2563eb;
+ transform: translateY(-2px);
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+ }
+
+ label {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-size: 0.875rem;
+ color: #6b7280;
+
+ select {
+ background: #ffffff;
+ border: 1px solid #d1d5db;
+ border-radius: 0.25rem;
+ padding: 0.25rem;
+ font-size: 0.875rem;
+ color: #1f2937;
+
+ &:focus {
+ outline: 2px solid #3b82f6;
+ outline-offset: 2px;
+ }
+ }
+ }
+ }
+
+ .demo-content {
+ min-height: 400px;
+
+ .real-content {
+ h3 {
+ font-size: 1.5rem;
+ color: #1f2937;
+ margin-bottom: 0.5rem;
+ }
+
+ .meta {
+ font-size: 0.875rem;
+ color: #9ca3af;
+ margin-bottom: 1rem;
+ }
+
+ img {
+ width: 100%;
+ max-width: 400px;
+ height: auto;
+ border-radius: 0.5rem;
+ margin-bottom: 1rem;
+ }
+
+ p {
+ font-size: 1rem;
+ line-height: 1.5;
+ color: #1f2937;
+ margin-bottom: 0.75rem;
+ }
+ }
+ }
+}
+
+.best-practices {
+ background: #f9fafb;
+ border: 1px solid #e5e7eb;
+ border-radius: 0.5rem;
+ padding: 1.5rem;
+
+ ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+
+ li {
+ font-size: 1rem;
+ color: #1f2937;
+ margin-bottom: 0.75rem;
+ padding-left: 1rem;
+ position: relative;
+
+ &::before {
+ content: '✓';
+ position: absolute;
+ left: 0;
+ color: #10b981;
+ font-weight: bold;
+ }
+
+ strong {
+ color: #1f2937;
+ font-weight: 600;
+ }
+ }
+ }
+}
+
+.code-examples {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 1rem;
+
+ .code-example {
+ background: #f9fafb;
+ border: 1px solid #e5e7eb;
+ border-radius: 0.5rem;
+ padding: 1rem;
+
+ h4 {
+ font-size: 0.875rem;
+ color: #6b7280;
+ margin-bottom: 0.5rem;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ }
+
+ pre {
+ background: #ffffff;
+ border-radius: 0.25rem;
+ padding: 0.5rem;
+ margin: 0;
+ overflow-x: auto;
+
+ code {
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ font-size: 0.875rem;
+ color: #1f2937;
+ white-space: pre;
+ line-height: 1.4;
+ }
+ }
+ }
+}
+
+// Responsive adjustments
+@media (max-width: 768px) {
+ .demo-container {
+ padding: 1rem;
+ }
+
+ .demo-row {
+ flex-direction: column;
+ }
+
+ .demo-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .demo-interactive .demo-controls {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 0.5rem;
+ }
+
+ .code-examples {
+ grid-template-columns: 1fr;
+ }
+}
+
+// Dark mode adjustments (if needed in the future)
+@media (prefers-color-scheme: dark) {
+ .demo-item {
+ background: #374151;
+ border-color: #4b5563;
+
+ label {
+ color: #9ca3af;
+ }
+ }
+
+ .demo-interactive {
+ background: #374151;
+ border-color: #4b5563;
+
+ .demo-controls label select {
+ background: #4b5563;
+ border-color: #6b7280;
+ color: #f3f4f6;
+ }
+ }
+
+ .best-practices {
+ background: #374151;
+ border-color: #4b5563;
+
+ ul li {
+ color: #f3f4f6;
+
+ strong {
+ color: #f3f4f6;
+ }
+ }
+ }
+
+ .code-examples .code-example {
+ background: #374151;
+ border-color: #4b5563;
+
+ h4 {
+ color: #9ca3af;
+ }
+
+ pre {
+ background: #4b5563;
+
+ code {
+ color: #f3f4f6;
+ }
+ }
+ }
+
+ .demo-section h3 {
+ color: #f3f4f6;
+ border-color: #6b7280;
+ }
+
+ .demo-subsection h4 {
+ color: #d1d5db;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/skeleton-loader-demo/skeleton-loader-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/skeleton-loader-demo/skeleton-loader-demo.component.ts
new file mode 100644
index 0000000..60a3f5e
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/skeleton-loader-demo/skeleton-loader-demo.component.ts
@@ -0,0 +1,303 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { SKELETON_COMPONENTS } from '../../../../../ui-essentials/src/lib/components/feedback/skeleton-loader';
+
+@Component({
+ selector: 'ui-skeleton-loader-demo',
+ standalone: true,
+ imports: [CommonModule, FormsModule, ...SKELETON_COMPONENTS],
+ template: `
+
+
Skeleton Loader Demo
+
Skeleton loaders provide visual placeholders while content is loading, improving perceived performance and user experience.
+
+
+
+ Sizes
+
+ @for (size of sizes; track size) {
+
+ {{ size }}
+
+
+ }
+
+
+
+
+
+ Shapes
+
+ @for (shape of shapes; track shape) {
+
+ {{ shape }}
+
+
+ }
+
+
+
+
+
+ Animation Types
+
+ @for (animation of animations; track animation) {
+
+ {{ animation }}
+
+
+
+ }
+
+
+
+
+
+ Width Variants
+
+ @for (width of widths; track width) {
+
+ {{ width }}
+
+
+
+ }
+
+
+
+
+
+ Pre-built Components
+
+
+
+
Text Block
+
+
+ 3 lines (default)
+
+
+
+ 5 lines
+
+
+
+ Custom last line width
+
+
+
+
+
+
+
+
Profile
+
+
+ Default
+
+
+
+ Small Avatar
+
+
+
+ No Extra Info
+
+
+
+
+
+
+
+
Card
+
+
+ Full Card
+
+
+
+ No Image
+
+
+
+ No Actions
+
+
+
+
+
+
+
+
Article
+
+ Full Article
+
+
+
+ Article without Image
+
+
+
+
+
+
+
Table Rows
+
+ Table Skeleton (3 rows)
+ @for (row of [1,2,3]; track row) {
+
+ }
+
+
+
+
+
+
+ Skeleton Groups
+
+
+
Vertical Group
+
+
+
+
+
+
+
+
+
+
+
Horizontal Group
+
+
+
+
+
+
+
+
+
+
+
Grid Group
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Interactive Loading Simulation
+
+
+
+ {{ isLoading ? 'Show Content' : 'Show Loading' }}
+
+
+ Animation:
+
+ @for (animation of animations; track animation) {
+ {{ animation }}
+ }
+
+
+
+
+
+ @if (isLoading) {
+
+
+
+ } @else {
+
+
+ Sample Article Title
+ Published on January 15, 2024 by John Doe
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+ Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+
+ }
+
+
+
+
+
+
+ Best Practices
+
+
+ Match Content Structure: Skeleton shapes should closely match the final content layout
+ Use Appropriate Animations: Shimmer for fast loading, pulse for slower operations
+ Respect Accessibility: Include proper ARIA labels and roles
+ Consider Performance: Reduce animation complexity on low-performance devices
+ Maintain Consistency: Use consistent skeleton patterns across your application
+
+
+
+
+
+
+ Code Examples
+
+
+
Basic Skeleton
+
<ui-skeleton-loader shape="text" size="md"></ui-skeleton-loader>
+
+
+
+
Profile Skeleton
+
<ui-skeleton-profile avatarSize="lg"></ui-skeleton-profile>
+
+
+
+
Custom Group
+
<ui-skeleton-group variant="horizontal">
+ <ui-skeleton-loader shape="avatar" size="sm"></ui-skeleton-loader>
+ <ui-skeleton-loader shape="text" width="w-75"></ui-skeleton-loader>
+</ui-skeleton-group>
+
+
+
+
+ `,
+ styleUrl: './skeleton-loader-demo.component.scss'
+})
+export class SkeletonLoaderDemoComponent {
+ sizes = ['xs', 'sm', 'md', 'lg', 'xl'] as const;
+ shapes = ['text', 'heading', 'avatar', 'card', 'button', 'image', 'circle', 'rounded', 'square'] as const;
+ animations = ['shimmer', 'pulse', 'wave'] as const;
+ widths = ['w-25', 'w-50', 'w-75', 'w-full'] as const;
+
+ isLoading = true;
+ selectedAnimation = 'shimmer' as any;
+
+ toggleLoading(): void {
+ this.isLoading = !this.isLoading;
+ }
+
+ onAnimationChange(): void {
+ // Force re-render by toggling loading state
+ if (this.isLoading) {
+ this.isLoading = false;
+ setTimeout(() => {
+ this.isLoading = true;
+ }, 50);
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/spacer-demo/spacer-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/spacer-demo/spacer-demo.component.scss
new file mode 100644
index 0000000..691a376
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/spacer-demo/spacer-demo.component.scss
@@ -0,0 +1,176 @@
+@use '../../../../../shared-ui/src/styles/semantic' as tokens;
+
+.demo-container {
+ padding: tokens.$semantic-spacing-layout-md;
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.demo-section {
+ margin-bottom: tokens.$semantic-spacing-layout-lg;
+
+ h3 {
+ margin-bottom: tokens.$semantic-spacing-component-md;
+ color: tokens.$semantic-color-text-primary;
+ font-size: 1.25rem;
+ font-weight: 600;
+ }
+
+ h4 {
+ margin-bottom: tokens.$semantic-spacing-component-sm;
+ color: tokens.$semantic-color-text-secondary;
+ font-size: 1.125rem;
+ font-weight: 500;
+ }
+
+ p {
+ color: tokens.$semantic-color-text-secondary;
+ margin-bottom: tokens.$semantic-spacing-component-md;
+ }
+}
+
+.demo-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: tokens.$semantic-spacing-layout-md;
+}
+
+.demo-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: tokens.$semantic-spacing-layout-md;
+}
+
+.demo-spacer-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ min-width: 200px;
+ margin-bottom: tokens.$semantic-spacing-component-lg;
+}
+
+.demo-direction-container {
+ padding: tokens.$semantic-spacing-component-md;
+ border: 1px solid tokens.$semantic-color-border-subtle;
+ border-radius: tokens.$semantic-border-radius-md;
+}
+
+.demo-horizontal-container {
+ display: flex;
+ align-items: center;
+ border: 2px dashed tokens.$semantic-color-border-subtle;
+ border-radius: tokens.$semantic-border-radius-sm;
+ padding: tokens.$semantic-spacing-component-sm;
+ min-height: 60px;
+}
+
+.demo-vertical-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ border: 2px dashed tokens.$semantic-color-border-subtle;
+ border-radius: tokens.$semantic-border-radius-sm;
+ padding: tokens.$semantic-spacing-component-sm;
+ min-height: 160px;
+}
+
+.demo-flex-container {
+ margin-bottom: tokens.$semantic-spacing-component-lg;
+}
+
+.demo-flex-row {
+ display: flex;
+ align-items: center;
+ border: 2px dashed tokens.$semantic-color-border-subtle;
+ border-radius: tokens.$semantic-border-radius-sm;
+ padding: tokens.$semantic-spacing-component-sm;
+ min-height: 60px;
+ width: 100%;
+}
+
+.demo-flex-column {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ border: 2px dashed tokens.$semantic-color-border-subtle;
+ border-radius: tokens.$semantic-border-radius-sm;
+ padding: tokens.$semantic-spacing-component-sm;
+ height: 200px;
+ width: 100%;
+}
+
+.demo-box {
+ background: tokens.$semantic-color-container-primary;
+ color: tokens.$semantic-color-on-container-primary;
+ padding: tokens.$semantic-spacing-component-sm;
+ border-radius: tokens.$semantic-border-radius-sm;
+ font-weight: 500;
+ min-width: 60px;
+ text-align: center;
+}
+
+.demo-label {
+ font-size: 0.875rem;
+ color: tokens.$semantic-color-text-secondary;
+ margin-bottom: tokens.$semantic-spacing-component-xs;
+ text-align: center;
+ font-weight: 500;
+}
+
+.demo-card {
+ background: tokens.$semantic-color-surface-primary;
+ border: 1px solid tokens.$semantic-color-border-subtle;
+ border-radius: tokens.$semantic-border-radius-md;
+ padding: tokens.$semantic-spacing-component-lg;
+ max-width: 400px;
+
+ .demo-card-header {
+ font-weight: 600;
+ color: tokens.$semantic-color-text-primary;
+ font-size: 1.125rem;
+ }
+
+ .demo-card-content {
+ color: tokens.$semantic-color-text-secondary;
+ line-height: 1.5;
+ }
+
+ .demo-card-actions {
+ display: flex;
+ align-items: center;
+ }
+}
+
+.demo-button {
+ background: tokens.$semantic-color-surface-secondary;
+ color: tokens.$semantic-color-text-primary;
+ border: 1px solid tokens.$semantic-color-border-subtle;
+ border-radius: tokens.$semantic-border-radius-sm;
+ padding: tokens.$semantic-spacing-component-sm tokens.$semantic-spacing-component-md;
+ font-size: 0.875rem;
+ cursor: pointer;
+
+ &--primary {
+ background: tokens.$semantic-color-container-primary;
+ color: tokens.$semantic-color-on-container-primary;
+ border-color: tokens.$semantic-color-container-primary;
+ }
+
+ &:hover {
+ opacity: 0.9;
+ }
+}
+
+@media (max-width: 768px) {
+ .demo-row {
+ flex-direction: column;
+ }
+
+ .demo-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .demo-spacer-container {
+ min-width: auto;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/spacer-demo/spacer-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/spacer-demo/spacer-demo.component.ts
new file mode 100644
index 0000000..8afa5b4
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/spacer-demo/spacer-demo.component.ts
@@ -0,0 +1,163 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { SpacerComponent } from '../../../../../ui-essentials/src/lib/components/layout';
+
+@Component({
+ selector: 'ui-spacer-demo',
+ standalone: true,
+ imports: [CommonModule, SpacerComponent],
+ template: `
+
+
Spacer Demo
+
+
+
+ Size Variants
+ Spacers with debug mode enabled to show spacing
+
+ @for (size of sizes; track size) {
+
+ }
+
+
+
+
+
+ Directional Spacing
+
+
+
+
+
+
+ Component Spacing Variants
+ Semantic spacing tokens for different component contexts
+
+ @for (variant of componentVariants; track variant) {
+
+
{{ variant }}
+
A
+
+
B
+
+ }
+
+
+
+
+
+ Layout Spacing Variants
+ Semantic spacing tokens for layout elements
+
+ @for (variant of layoutVariants; track variant) {
+
+
{{ variant }}
+
Section A
+
+
Section B
+
+ }
+
+
+
+
+
+ Flexible Spacer
+ Spacer that grows to fill available space
+
+
+
Horizontal Flexible
+
+
+
+
+
+
+
+
+ Custom Spacing
+
+
+
Custom Width: 100px
+
+
+
+
+
Custom Height: 80px
+
+
+
+
+
+
+
+ Real World Usage
+
+
+
Card Layout with Spacers
+
+
+
Main content goes here with proper spacing
+
+
+ Cancel
+
+ Save
+
+
+
+
+ `,
+ styleUrl: './spacer-demo.component.scss'
+})
+export class SpacerDemoComponent {
+ sizes: ('xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl')[] = ['xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl'];
+ componentVariants: ('component-xs' | 'component-sm' | 'component-md' | 'component-lg' | 'component-xl')[] = [
+ 'component-xs', 'component-sm', 'component-md', 'component-lg', 'component-xl'
+ ];
+ layoutVariants: ('layout-xs' | 'layout-sm' | 'layout-md' | 'layout-lg' | 'layout-xl')[] = [
+ 'layout-xs', 'layout-sm', 'layout-md', 'layout-lg', 'layout-xl'
+ ];
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/switch-demo/switch-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/switch-demo/switch-demo.component.ts
new file mode 100644
index 0000000..0ffdcdf
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/switch-demo/switch-demo.component.ts
@@ -0,0 +1,389 @@
+import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormControl, FormGroup, ReactiveFormsModule, Validators, FormsModule } from '@angular/forms';
+import { SwitchComponent } from '../../../../../ui-essentials/src/lib/components/forms/switch';
+
+@Component({
+ selector: 'ui-switch-demo',
+ standalone: true,
+ imports: [
+ CommonModule,
+ ReactiveFormsModule,
+ FormsModule,
+ SwitchComponent
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
Switch Component Showcase
+
+
+
+ Basic Switches
+
+
+
+
+
+
+
+
+
+ Variants
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Size Comparison
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Switches without Labels
+
+
+
+ Small
+
+
+
+ Medium
+
+
+
+ Large
+
+
+
+
+
+
+ With Helper Text
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reactive Forms Integration
+
+
+
+
+
+
+
+
+
+
+
+
Form Values:
+
{{ getFormValues() }}
+
+
+
+
+ Reset Form
+
+
+ Toggle All
+
+
+
+
+
+
+
+ Usage Examples
+
+
Basic Switch:
+
<ui-switch
+ label="Enable feature"
+ size="md"
+ variant="primary"
+ (switchChange)="onToggle($event)">
+</ui-switch>
+
+
Switch with Helper Text:
+
<ui-switch
+ label="Email Notifications"
+ helperText="Receive updates via email"
+ variant="success"
+ [ngModel]="emailEnabled"
+ (switchChange)="toggleEmail($event)">
+</ui-switch>
+
+
Reactive Form Switch:
+
<ui-switch
+ formControlName="agreement"
+ label="I agree to terms"
+ variant="primary"
+ [required]="true">
+</ui-switch>
+
+
Switch without Label:
+
<ui-switch
+ size="lg"
+ variant="secondary"
+ ariaLabel="Toggle feature"
+ (switchChange)="handleToggle($event)">
+</ui-switch>
+
+
+
+
+
+ Interactive Demo
+
+
+ Size:
+
+ Small
+ Medium
+ Large
+
+
+
+ Variant:
+
+ Primary
+ Secondary
+ Success
+ Warning
+ Danger
+
+
+
+
+
+
+
Live Preview:
+
+
+
+
+ Current state: {{ demoCheckedValue ? 'ON' : 'OFF' }}
+
+
+
+
+
+ @if (lastAction()) {
+
+
+ Last Action: {{ lastAction() }}
+
+
+ }
+
+ `,
+ styles: [`
+ h2 {
+ color: hsl(279, 14%, 11%);
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ border-bottom: 2px solid hsl(258, 100%, 47%);
+ padding-bottom: 0.5rem;
+ }
+
+ h3 {
+ color: hsl(279, 14%, 25%);
+ font-size: 1.5rem;
+ margin-bottom: 1rem;
+ }
+
+ h4 {
+ color: hsl(287, 12%, 35%);
+ font-size: 1.125rem;
+ margin-bottom: 0.75rem;
+ }
+
+ section {
+ border: 1px solid hsl(289, 14%, 90%);
+ border-radius: 8px;
+ padding: 1.5rem;
+ background: hsl(286, 20%, 99%);
+ }
+
+ pre {
+ font-size: 0.875rem;
+ line-height: 1.5;
+ margin: 0.5rem 0;
+ }
+
+ code {
+ font-family: 'JetBrains Mono', monospace;
+ color: #d63384;
+ }
+
+ select, input[type="checkbox"] {
+ cursor: pointer;
+ }
+
+ label {
+ cursor: pointer;
+ }
+ `]
+})
+export class SwitchDemoComponent {
+ lastAction = signal('');
+
+ // Template values for ngModel binding
+ preCheckedValue = true;
+ autoSaveValue = true;
+
+ // Interactive demo properties
+ demoSizeValue: 'sm' | 'md' | 'lg' = 'md';
+ demoVariantValue: 'primary' | 'secondary' | 'success' | 'warning' | 'danger' = 'primary';
+ demoDisabledValue = false;
+ demoRequiredValue = false;
+ demoWithHelperValue = false;
+ demoCheckedValue = false;
+
+ // Reactive form
+ settingsForm = new FormGroup({
+ notifications: new FormControl(false),
+ newsletter: new FormControl(true),
+ analytics: new FormControl(false, [Validators.requiredTrue]),
+ marketing: new FormControl(false)
+ });
+
+ handleToggle(switchName: string, checked: boolean): void {
+ const action = `${switchName}: ${checked ? 'ON' : 'OFF'}`;
+ this.lastAction.set(action);
+ console.log(`Switch toggled - ${action}`);
+ }
+
+ handleDemoToggle(checked: boolean): void {
+ this.demoCheckedValue = checked;
+ this.handleToggle('Interactive Demo', checked);
+ }
+
+ getFormValues(): string {
+ const values = this.settingsForm.value;
+ return JSON.stringify(values, null, 2);
+ }
+
+ resetForm(): void {
+ this.settingsForm.reset({
+ notifications: false,
+ newsletter: false,
+ analytics: false,
+ marketing: false
+ });
+ this.lastAction.set('Form reset to default values');
+ }
+
+ toggleAllSwitches(): void {
+ const allEnabled = Object.values(this.settingsForm.value).every(val => val === true);
+ const newState = !allEnabled;
+
+ this.settingsForm.patchValue({
+ notifications: newState,
+ newsletter: newState,
+ analytics: newState,
+ marketing: newState
+ });
+
+ this.lastAction.set(`All switches turned ${newState ? 'ON' : 'OFF'}`);
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/table-demo/table-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/table-demo/table-demo.component.ts
new file mode 100644
index 0000000..5aac226
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/table-demo/table-demo.component.ts
@@ -0,0 +1,637 @@
+import { Component, ChangeDetectionStrategy, TemplateRef, ViewChild } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import {
+ TableComponent,
+ TableActionsComponent,
+ type TableColumn,
+ type TableAction,
+ type TableSortEvent
+} from '../../../../../ui-essentials/src/lib/components/data-display/table';
+import { StatusBadgeComponent } from '../../../../../ui-essentials/src/lib/components/feedback';
+interface User {
+ id: number;
+ name: string;
+ email: string;
+ status: 'active' | 'inactive' | 'pending';
+ role: string;
+ lastLogin: string;
+ joinDate: string;
+}
+
+@Component({
+ selector: 'ui-table-demo',
+ standalone: true,
+ imports: [
+ CommonModule,
+ TableComponent,
+ StatusBadgeComponent,
+ TableActionsComponent
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
Table Component Showcase
+
+
+
+
+
+
+ Table Variants
+
+
+
Striped Table
+
+
+
+
+
+
Bordered Table
+
+
+
+
+
+
Minimal Table
+
+
+
+
+
+
+
+ Size Variants
+
+
+
Compact Size
+
+
+
+
+
+
Comfortable Size
+
+
+
+
+
+
+
+ Advanced Table with Status Badges and Actions
+
+
+
+
+
+
+ {{ value | titlecase }}
+
+
+
+
+
+
+
+
+
+
+
+ Sticky Header Table
+
+
+
+
+
+
+
+
+ Loading State
+
+
+
+ {{ isLoading ? 'Stop Loading' : 'Start Loading' }}
+
+
+
+
+
+
+
+
+ Selectable Rows
+
+
+ @if (selectedUsers.length > 0) {
+
+
Selected users: {{ selectedUsers.length }}
+
+ @for (user of selectedUsers; track user.id) {
+
+ {{ user.name }}
+
+ }
+
+
+ }
+
+
+
+
+ Usage Examples
+
+
Basic Table:
+
<ui-table
+ [data]="tableData"
+ [columns]="columns"
+ variant="striped"
+ size="default"
+ [hoverable]="true"
+ (sort)="handleSort($event)"
+ (rowClick)="handleRowClick($event)">
+</ui-table>
+
+
With Custom Cell Templates:
+
<ui-table
+ [data]="userData"
+ [columns]="columns"
+ [cellTemplates]="cellTemplates">
+</ui-table>
+
+<ng-template #statusTemplate let-value>
+ <ui-status-badge [variant]="getVariant(value)">
+ {{ "{{ value }}" }}
+ </ui-status-badge>
+</ng-template>
+
+
Column Configuration:
+
columns: TableColumn[] = [
+ {{ '{' }} key: 'name', label: 'Name', sortable: true {{ '}' }},
+ {{ '{' }} key: 'email', label: 'Email', sortable: true {{ '}' }},
+ {{ '{' }} key: 'status', label: 'Status', align: 'center' {{ '}' }},
+ {{ '{' }} key: 'actions', label: 'Actions', align: 'right' {{ '}' }}
+];
+
+
+
+
+
+ Interactive Demo Controls
+
+
+ Add Random User
+
+
+ Clear All Users
+
+
+ Reset Demo
+
+
+
+ @if (lastAction) {
+
+ Last Action: {{ lastAction }}
+
+ }
+
+
+ `,
+ styles: [`
+ h2 {
+ color: hsl(279, 14%, 11%);
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ border-bottom: 2px solid hsl(258, 100%, 47%);
+ padding-bottom: 0.5rem;
+ }
+
+ h3 {
+ color: hsl(279, 14%, 25%);
+ font-size: 1.5rem;
+ margin-bottom: 1rem;
+ }
+
+ h4 {
+ color: hsl(287, 12%, 35%);
+ font-size: 1.125rem;
+ margin-bottom: 0.75rem;
+ }
+
+ section {
+ border: 1px solid hsl(289, 14%, 90%);
+ border-radius: 8px;
+ padding: 1.5rem;
+ background: hsl(286, 20%, 99%);
+ }
+
+ pre {
+ font-size: 0.875rem;
+ line-height: 1.5;
+ margin: 0.5rem 0;
+ }
+
+ code {
+ font-family: 'JetBrains Mono', monospace;
+ color: #d63384;
+ }
+ `]
+})
+export class TableDemoComponent {
+ @ViewChild('statusTemplate') statusTemplate!: TemplateRef;
+ @ViewChild('actionsTemplate') actionsTemplate!: TemplateRef;
+
+ isLoading = false;
+ selectedUsers: User[] = [];
+ lastAction = '';
+ currentSortColumn = '';
+ currentSortDirection: 'asc' | 'desc' | null = null;
+
+ cellTemplates: Record> = {};
+
+ userData: User[] = [
+ {
+ id: 1,
+ name: 'Alice Johnson',
+ email: 'alice@example.com',
+ status: 'active',
+ role: 'Admin',
+ lastLogin: '2024-01-15',
+ joinDate: '2023-01-15'
+ },
+ {
+ id: 2,
+ name: 'Bob Smith',
+ email: 'bob@example.com',
+ status: 'inactive',
+ role: 'User',
+ lastLogin: '2024-01-10',
+ joinDate: '2023-02-20'
+ },
+ {
+ id: 3,
+ name: 'Carol Williams',
+ email: 'carol@example.com',
+ status: 'pending',
+ role: 'Editor',
+ lastLogin: '2024-01-14',
+ joinDate: '2023-03-10'
+ },
+ {
+ id: 4,
+ name: 'David Brown',
+ email: 'david@example.com',
+ status: 'active',
+ role: 'User',
+ lastLogin: '2024-01-16',
+ joinDate: '2023-04-05'
+ },
+ {
+ id: 5,
+ name: 'Eve Davis',
+ email: 'eve@example.com',
+ status: 'active',
+ role: 'Admin',
+ lastLogin: '2024-01-16',
+ joinDate: '2023-05-12'
+ },
+ {
+ id: 6,
+ name: 'Frank Miller',
+ email: 'frank@example.com',
+ status: 'inactive',
+ role: 'User',
+ lastLogin: '2024-01-08',
+ joinDate: '2023-06-18'
+ }
+ ];
+
+ basicColumns: TableColumn[] = [
+ { key: 'name', label: 'Name', sortable: true },
+ { key: 'email', label: 'Email', sortable: true },
+ { key: 'role', label: 'Role', sortable: true },
+ { key: 'lastLogin', label: 'Last Login', sortable: true, align: 'right' }
+ ];
+
+ advancedColumns: TableColumn[] = [
+ { key: 'name', label: 'Name', sortable: true },
+ { key: 'email', label: 'Email', sortable: true },
+ { key: 'status', label: 'Status', align: 'center' },
+ { key: 'role', label: 'Role', sortable: true },
+ { key: 'actions', label: 'Actions', align: 'right', width: '120px' }
+ ];
+
+ largeDataset: User[] = [];
+
+ ngAfterViewInit(): void {
+ // Set up cell templates after view init
+ setTimeout(() => {
+ this.cellTemplates = {
+ 'status': this.statusTemplate,
+ 'actions': this.actionsTemplate
+ };
+ });
+
+ // Generate large dataset for sticky header demo
+ this.largeDataset = this.generateLargeDataset(50);
+ }
+
+ handleSort(event: TableSortEvent): void {
+ this.lastAction = `Sorted by ${event.column} (${event.direction || 'none'})`;
+ console.log('Sort event:', event);
+ }
+
+ handleAdvancedSort(event: TableSortEvent): void {
+ this.currentSortColumn = event.column;
+ this.currentSortDirection = event.direction;
+ this.handleSort(event);
+
+ // Actually sort the data
+ if (event.direction) {
+ this.userData.sort((a, b) => {
+ const aVal = this.getCellValue(a, event.column);
+ const bVal = this.getCellValue(b, event.column);
+
+ if (event.direction === 'asc') {
+ return aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
+ } else {
+ return aVal > bVal ? -1 : aVal < bVal ? 1 : 0;
+ }
+ });
+ }
+ }
+
+ handleRowClick(event: {row: User, index: number}): void {
+ this.lastAction = `Clicked row: ${event.row.name} (index: ${event.index})`;
+ console.log('Row click:', event);
+ }
+
+ handleAdvancedRowClick(event: {row: User, index: number}): void {
+ this.lastAction = `Advanced row click: ${event.row.name}`;
+ console.log('Advanced row click:', event);
+ }
+
+ handleSelectionChange(selectedUsers: User[]): void {
+ this.selectedUsers = selectedUsers;
+ this.lastAction = `Selection changed: ${selectedUsers.length} users selected`;
+ }
+
+ handleAction(event: any): void {
+ const { action, data, index } = event;
+ this.lastAction = `Action "${action.id}" on user ${data.name} (index: ${index})`;
+
+ switch (action.id) {
+ case 'edit':
+ console.log('Edit user:', data);
+ break;
+ case 'delete':
+ this.deleteUser(data.id);
+ break;
+ case 'view':
+ console.log('View user:', data);
+ break;
+ }
+ }
+
+ getStatusVariant(status: string): any {
+ const variants: any = {
+ 'active': 'success',
+ 'inactive': 'danger',
+ 'pending': 'warning'
+ };
+ return variants[status] || 'neutral';
+ }
+
+ getActionsForUser(user: User): TableAction[] {
+ const baseActions: TableAction[] = [
+ { id: 'view', label: 'View', variant: 'view', icon: '👁️' },
+ { id: 'edit', label: 'Edit', variant: 'edit', icon: '✏️' }
+ ];
+
+ if (user.status !== 'active') {
+ baseActions.push({
+ id: 'delete',
+ label: 'Delete',
+ variant: 'delete',
+ icon: '🗑️',
+ confirmMessage: `Are you sure you want to delete ${user.name}?`
+ });
+ }
+
+ return baseActions;
+ }
+
+ toggleLoading(): void {
+ this.isLoading = !this.isLoading;
+ this.lastAction = `Loading ${this.isLoading ? 'started' : 'stopped'}`;
+ }
+
+ addRandomUser(): void {
+ const names = ['John Doe', 'Jane Smith', 'Mike Johnson', 'Sarah Wilson', 'Tom Anderson'];
+ const roles = ['Admin', 'User', 'Editor', 'Moderator'];
+ const statuses: ('active' | 'inactive' | 'pending')[] = ['active', 'inactive', 'pending'];
+
+ const newUser: User = {
+ id: Math.max(...this.userData.map(u => u.id)) + 1,
+ name: names[Math.floor(Math.random() * names.length)],
+ email: `user${Date.now()}@example.com`,
+ status: statuses[Math.floor(Math.random() * statuses.length)],
+ role: roles[Math.floor(Math.random() * roles.length)],
+ lastLogin: new Date().toISOString().split('T')[0],
+ joinDate: new Date().toISOString().split('T')[0]
+ };
+
+ this.userData = [...this.userData, newUser];
+ this.lastAction = `Added new user: ${newUser.name}`;
+ }
+
+ clearAllUsers(): void {
+ this.userData = [];
+ this.selectedUsers = [];
+ this.lastAction = 'Cleared all users';
+ }
+
+ resetDemo(): void {
+ this.ngOnInit();
+ this.selectedUsers = [];
+ this.isLoading = false;
+ this.lastAction = 'Demo reset to initial state';
+ }
+
+ private deleteUser(id: number): void {
+ this.userData = this.userData.filter(user => user.id !== id);
+ this.selectedUsers = this.selectedUsers.filter(user => user.id !== id);
+ this.lastAction = `Deleted user with ID: ${id}`;
+ }
+
+ private getCellValue(row: any, key: string): any {
+ return key.split('.').reduce((obj, prop) => obj?.[prop], row) ?? '';
+ }
+
+ private generateLargeDataset(count: number): User[] {
+ const dataset: User[] = [];
+ const names = ['Alex', 'Morgan', 'Casey', 'Jordan', 'Taylor', 'Riley', 'Avery', 'Quinn'];
+ const domains = ['example.com', 'test.com', 'demo.com', 'sample.com'];
+ const roles = ['Admin', 'User', 'Editor', 'Moderator', 'Viewer'];
+ const statuses: ('active' | 'inactive' | 'pending')[] = ['active', 'inactive', 'pending'];
+
+ for (let i = 0; i < count; i++) {
+ dataset.push({
+ id: i + 100,
+ name: `${names[i % names.length]} ${i + 1}`,
+ email: `user${i + 100}@${domains[i % domains.length]}`,
+ status: statuses[i % statuses.length],
+ role: roles[i % roles.length],
+ lastLogin: new Date(2024, 0, (i % 30) + 1).toISOString().split('T')[0],
+ joinDate: new Date(2023, (i % 12), (i % 28) + 1).toISOString().split('T')[0]
+ });
+ }
+
+ return dataset;
+ }
+
+ ngOnInit(): void {
+ // Reset to initial data
+ this.userData = [
+ {
+ id: 1,
+ name: 'Alice Johnson',
+ email: 'alice@example.com',
+ status: 'active',
+ role: 'Admin',
+ lastLogin: '2024-01-15',
+ joinDate: '2023-01-15'
+ },
+ {
+ id: 2,
+ name: 'Bob Smith',
+ email: 'bob@example.com',
+ status: 'inactive',
+ role: 'User',
+ lastLogin: '2024-01-10',
+ joinDate: '2023-02-20'
+ },
+ {
+ id: 3,
+ name: 'Carol Williams',
+ email: 'carol@example.com',
+ status: 'pending',
+ role: 'Editor',
+ lastLogin: '2024-01-14',
+ joinDate: '2023-03-10'
+ },
+ {
+ id: 4,
+ name: 'David Brown',
+ email: 'david@example.com',
+ status: 'active',
+ role: 'User',
+ lastLogin: '2024-01-16',
+ joinDate: '2023-04-05'
+ },
+ {
+ id: 5,
+ name: 'Eve Davis',
+ email: 'eve@example.com',
+ status: 'active',
+ role: 'Admin',
+ lastLogin: '2024-01-16',
+ joinDate: '2023-05-12'
+ },
+ {
+ id: 6,
+ name: 'Frank Miller',
+ email: 'frank@example.com',
+ status: 'inactive',
+ role: 'User',
+ lastLogin: '2024-01-08',
+ joinDate: '2023-06-18'
+ }
+ ];
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/time-picker-demo/time-picker-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/time-picker-demo/time-picker-demo.component.scss
new file mode 100644
index 0000000..66e2046
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/time-picker-demo/time-picker-demo.component.scss
@@ -0,0 +1,152 @@
+.demo-container {
+ padding: 2rem;
+ max-width: 1200px;
+ margin: 0 auto;
+
+ h2 {
+ margin-bottom: 1.5rem;
+ font-size: 1.875rem;
+ }
+}
+
+.demo-section {
+ margin-bottom: 3rem;
+
+ h3 {
+ margin-bottom: 1rem;
+ font-size: 1.25rem;
+ border-bottom: 1px solid #e5e7eb;
+ padding-bottom: 0.5rem;
+ }
+}
+
+.demo-row {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 1.5rem;
+}
+
+.demo-item {
+ h4 {
+ margin-bottom: 0.75rem;
+ font-size: 1rem;
+ font-weight: 500;
+ color: #6b7280;
+ }
+}
+
+.demo-interactive {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1.5rem;
+ padding: 1.5rem;
+ border: 1px solid #e5e7eb;
+ border-radius: 0.5rem;
+ background: #f9fafb;
+}
+
+.demo-output {
+ h4 {
+ margin-bottom: 0.5rem;
+ font-size: 1rem;
+ }
+
+ p {
+ color: #6b7280;
+ margin-bottom: 0.25rem;
+ font-size: 0.875rem;
+ }
+}
+
+.demo-form {
+ padding: 1.5rem;
+ border: 1px solid #e5e7eb;
+ border-radius: 0.5rem;
+ background: #ffffff;
+}
+
+.form-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1.5rem;
+ margin-bottom: 1.5rem;
+}
+
+.form-actions {
+ display: flex;
+ gap: 0.75rem;
+ margin-bottom: 1.5rem;
+}
+
+.submit-btn, .reset-btn {
+ padding: 0.5rem 1rem;
+ border-radius: 0.375rem;
+ font-size: 0.875rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.submit-btn {
+ background: #3b82f6;
+ color: white;
+ border: 1px solid #3b82f6;
+
+ &:hover:not(:disabled) {
+ background: #2563eb;
+ border-color: #2563eb;
+ }
+
+ &:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+}
+
+.reset-btn {
+ background: transparent;
+ color: #6b7280;
+ border: 1px solid #d1d5db;
+
+ &:hover {
+ background: #f3f4f6;
+ color: #374151;
+ }
+}
+
+.form-status {
+ background: #f3f4f6;
+ padding: 0.75rem;
+ border-radius: 0.375rem;
+
+ p {
+ margin-bottom: 0.5rem;
+ font-size: 0.875rem;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ pre {
+ background: #e5e7eb;
+ padding: 0.5rem;
+ border-radius: 0.25rem;
+ font-size: 0.75rem;
+ overflow-x: auto;
+ }
+}
+
+@media (max-width: 768px) {
+ .demo-row {
+ grid-template-columns: 1fr;
+ }
+
+ .demo-interactive {
+ grid-template-columns: 1fr;
+ }
+
+ .form-row {
+ grid-template-columns: 1fr;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/time-picker-demo/time-picker-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/time-picker-demo/time-picker-demo.component.ts
new file mode 100644
index 0000000..220a81f
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/time-picker-demo/time-picker-demo.component.ts
@@ -0,0 +1,394 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { TimePickerComponent, TimeValue } from '../../../../../ui-essentials/src/lib/components/forms/time-picker/time-picker.component';
+
+@Component({
+ selector: 'ui-time-picker-demo',
+ standalone: true,
+ imports: [CommonModule, FormsModule, TimePickerComponent],
+ template: `
+
+
Time Picker Demo
+
+
+
+ Sizes
+
+ @for (size of sizes; track size) {
+
+
{{ size | uppercase }}
+
+
+
+ }
+
+
+
+
+
+ Variants
+
+ @for (variant of variants; track variant) {
+
+
{{ variant | titlecase }}
+
+
+
+ }
+
+
+
+
+
+ Time Formats
+
+
+
12-Hour Format
+
+
+
+
+
+
24-Hour Format
+
+
+
+
+
+
With Seconds
+
+
+
+
+
+
Custom Steps
+
+
+
+
+
+
+
+
+ States
+
+
+
Default
+
+
+
+
+
+
Error
+
+
+
+
+
+
Success
+
+
+
+
+
+
Warning
+
+
+
+
+
+
+
+
+ Features
+
+
+
Required Field
+
+
+
+
+
+
Disabled
+
+
+
+
+
+
Not Clearable
+
+
+
+
+
+
With Presets
+
+
+
+
+
+
+
+
+ Interactive Example
+
+
+
+
+
+
Selected Time:
+
{{ interactiveValue ? formatTime(interactiveValue) : 'No time selected' }}
+
24-hour format: {{ interactiveValue ? format24Hour(interactiveValue) : 'N/A' }}
+
Change count: {{ changeCount }}
+
+
+
+
+
+
+ Form Integration
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Submit Form
+
+
+ Reset
+
+
+
+
+
+
+
+ `,
+ styleUrl: './time-picker-demo.component.scss'
+})
+export class TimePickerDemoComponent {
+ sizes = ['sm', 'md', 'lg'] as const;
+ variants = ['outlined', 'filled', 'underlined'] as const;
+
+ sampleValues: Record = {
+ sm: null,
+ md: null,
+ lg: null
+ };
+
+ variantValues: Record = {
+ outlined: null,
+ filled: null,
+ underlined: null
+ };
+
+ formatValues: Record = {
+ twelve: null,
+ twentyFour: null,
+ withSeconds: null,
+ customSteps: null
+ };
+
+ stateValues: Record = {
+ default: null,
+ error: null,
+ success: { hours: 9, minutes: 30, seconds: 0 },
+ warning: null
+ };
+
+ featureValues: Record = {
+ required: null,
+ disabled: { hours: 12, minutes: 0, seconds: 0 },
+ notClearable: null,
+ presets: null
+ };
+
+ interactiveValue: TimeValue | null = null;
+ changeCount = 0;
+
+ formValues = {
+ startTime: null as TimeValue | null,
+ endTime: null as TimeValue | null,
+ breakTime: null as TimeValue | null,
+ duration: null as TimeValue | null
+ };
+
+ // Preset times for demo
+ presetTimes = [
+ { label: '9:00 AM', value: { hours: 9, minutes: 0, seconds: 0 } },
+ { label: '12:00 PM', value: { hours: 12, minutes: 0, seconds: 0 } },
+ { label: '1:00 PM', value: { hours: 13, minutes: 0, seconds: 0 } },
+ { label: '5:00 PM', value: { hours: 17, minutes: 0, seconds: 0 } }
+ ];
+
+ breakPresets = [
+ { label: '15 min', value: { hours: 0, minutes: 15, seconds: 0 } },
+ { label: '30 min', value: { hours: 0, minutes: 30, seconds: 0 } },
+ { label: '1 hour', value: { hours: 1, minutes: 0, seconds: 0 } }
+ ];
+
+ onTimeChange(time: TimeValue | null): void {
+ this.changeCount++;
+ console.log('Time changed:', time);
+ }
+
+ onSubmit(): void {
+ console.log('Form submitted:', this.formValues);
+ alert('Form submitted! Check console for values.');
+ }
+
+ resetForm(form: any): void {
+ form.resetForm();
+ this.formValues = {
+ startTime: null,
+ endTime: null,
+ breakTime: null,
+ duration: null
+ };
+ }
+
+ formatTime(time: TimeValue): string {
+ const hours = time.hours;
+ const displayHour = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
+ const period = hours >= 12 ? 'PM' : 'AM';
+ const minutes = time.minutes.toString().padStart(2, '0');
+
+ if (time.seconds !== undefined && time.seconds > 0) {
+ const seconds = time.seconds.toString().padStart(2, '0');
+ return `${displayHour}:${minutes}:${seconds} ${period}`;
+ }
+
+ return `${displayHour}:${minutes} ${period}`;
+ }
+
+ format24Hour(time: TimeValue): string {
+ const hours = time.hours.toString().padStart(2, '0');
+ const minutes = time.minutes.toString().padStart(2, '0');
+
+ if (time.seconds !== undefined && time.seconds > 0) {
+ const seconds = time.seconds.toString().padStart(2, '0');
+ return `${hours}:${minutes}:${seconds}`;
+ }
+
+ return `${hours}:${minutes}`;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/video-player-demo/video-player-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/video-player-demo/video-player-demo.component.scss
new file mode 100644
index 0000000..d40eb44
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/video-player-demo/video-player-demo.component.scss
@@ -0,0 +1,204 @@
+@use "../../../../../shared-ui/src/styles/semantic" as *;
+
+
+// ==========================================================================
+// VIDEO PLAYER DEMO COMPONENT
+// ==========================================================================
+// Demonstration component showcasing all video player variants and capabilities
+// ==========================================================================
+
+.video-player-demo {
+ padding: $semantic-spacing-layout-lg;
+ max-width: 1200px;
+ margin: 0 auto;
+
+ h2 {
+ color: $semantic-color-text-primary;
+ font-family: map-get($semantic-typography-heading-h1, font-family);
+ font-size: map-get($semantic-typography-heading-h1, font-size);
+ font-weight: map-get($semantic-typography-heading-h1, font-weight);
+ line-height: map-get($semantic-typography-heading-h1, line-height);
+ margin-bottom: $semantic-spacing-layout-section-sm;
+ border-bottom: 2px solid $semantic-color-primary;
+ padding-bottom: $semantic-spacing-component-xs;
+ }
+
+ h3 {
+ color: $semantic-color-text-secondary;
+ 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);
+ margin-bottom: $semantic-spacing-content-heading;
+ }
+
+ h4 {
+ color: $semantic-color-text-tertiary;
+ 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);
+ margin-bottom: $semantic-spacing-content-paragraph;
+ }
+
+ .demo-section {
+ border: 1px solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-lg;
+ padding: $semantic-spacing-component-xl;
+ background: $semantic-color-surface-secondary;
+ margin-bottom: $semantic-spacing-layout-md;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ .variant-demo {
+ margin-bottom: $semantic-spacing-component-lg;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ .player-container {
+ border: 1px solid $semantic-color-border-secondary;
+ border-radius: $semantic-border-radius-md;
+ overflow: hidden;
+ margin-bottom: $semantic-spacing-component-md;
+ background: $semantic-color-surface-primary;
+ }
+
+ .demo-description {
+ margin-bottom: $semantic-spacing-content-heading;
+ color: $semantic-color-text-tertiary;
+ 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);
+ }
+
+ .controls-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: $semantic-spacing-component-md;
+ margin-bottom: $semantic-spacing-component-lg;
+ padding: $semantic-spacing-component-md;
+ background: $semantic-color-surface-container;
+ border-radius: $semantic-border-radius-md;
+ }
+
+ .control-label {
+ display: flex;
+ align-items: center;
+ gap: $semantic-spacing-component-xs;
+ cursor: pointer;
+ 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);
+ user-select: none;
+ color: $semantic-color-text-primary;
+
+ input[type="checkbox"] {
+ margin: 0;
+ accent-color: $semantic-color-primary;
+ }
+ }
+
+ .select-controls {
+ display: flex;
+ gap: $semantic-spacing-component-lg;
+ margin-bottom: $semantic-spacing-component-lg;
+ flex-wrap: wrap;
+ }
+
+ .select-label {
+ display: flex;
+ align-items: center;
+ gap: $semantic-spacing-component-xs;
+ font-family: map-get($semantic-typography-body-small, font-family);
+ font-size: map-get($semantic-typography-body-small, font-size);
+ font-weight: map-get($semantic-typography-body-small, font-weight);
+ line-height: map-get($semantic-typography-body-small, line-height);
+ color: $semantic-color-text-primary;
+ }
+
+ .demo-select {
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ border: 1px solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-sm;
+ background: $semantic-color-surface-primary;
+ color: $semantic-color-text-primary;
+ font-family: map-get($semantic-typography-body-small, font-family);
+ font-size: map-get($semantic-typography-body-small, font-size);
+
+ &:focus {
+ outline: 2px solid $semantic-color-primary;
+ outline-offset: 2px;
+ }
+ }
+
+ .event-display {
+ margin-top: $semantic-spacing-component-md;
+ padding: $semantic-spacing-component-md;
+ background: $semantic-color-primary-container;
+ border: 1px solid $semantic-color-primary;
+ border-radius: $semantic-border-radius-sm;
+ color: $semantic-color-text-primary;
+ font-family: map-get($semantic-typography-body-small, font-family);
+ font-size: map-get($semantic-typography-body-small, font-size);
+ font-weight: map-get($semantic-typography-body-small, font-weight);
+ line-height: map-get($semantic-typography-body-small, line-height);
+ }
+
+ .code-examples {
+ background: $semantic-color-surface-container;
+ padding: $semantic-spacing-component-xl;
+ border-radius: $semantic-border-radius-md;
+ border-left: 4px solid $semantic-color-primary;
+ }
+
+ .code-example {
+ margin-bottom: $semantic-spacing-component-lg;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ h4 {
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+
+ pre {
+ background: $semantic-color-surface-primary;
+ padding: $semantic-spacing-component-md;
+ border-radius: $semantic-border-radius-sm;
+ overflow-x: auto;
+ border: 1px solid $semantic-color-border-secondary;
+
+ code {
+ font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
+ font-size: map-get($semantic-typography-code-block, font-size);
+ line-height: map-get($semantic-typography-code-block, line-height);
+ color: $semantic-color-text-primary;
+ }
+ }
+ }
+}
+
+// Responsive adjustments
+@media (max-width: 768px) {
+ .video-player-demo {
+ padding: $semantic-spacing-layout-md;
+
+ .controls-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .select-controls {
+ flex-direction: column;
+ gap: $semantic-spacing-component-md;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/video-player-demo/video-player-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/video-player-demo/video-player-demo.component.ts
new file mode 100644
index 0000000..7f4c9ee
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/video-player-demo/video-player-demo.component.ts
@@ -0,0 +1,543 @@
+import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { VideoPlayerComponent, VideoSource, VideoTrack, VideoPlayerSize, VideoPlayerVariant } from '../../../../../ui-essentials/src/lib/components/media/video-player/video-player.component';
+
+@Component({
+ selector: 'ui-video-player-demo',
+ standalone: true,
+ imports: [
+ CommonModule,
+ VideoPlayerComponent
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
Video Player Component Showcase
+
+
+
+ Video Player Variants
+
+
+
Default Player
+
+
+
+
+
+
+
+
Minimal Player
+
+
+
+
+
+
+
+
Theater Player
+
+
+
+
+
+
+
+
+
+ Size Variants
+
+
+
+
+
Medium (md) - Default
+
+
+
+
+
+
+
+
+
+
+
+ Feature Demonstrations
+
+
+
Player with Subtitles
+
+
+
+
+
+
+
+
Autoplay Player (Muted)
+
+
+
+
+
+
+
+
Player without Volume Control
+
+
+
+
+
+
+
+
Disabled Player
+
+
+
+
+
+
+
+
+
+
+
+
+ Real-world Examples
+
+
+
Course Video Player
+
+ Typical setup for educational content with subtitles and chapter markers
+
+
+
+
+
+
+
+
+
Product Demo Player
+
+ Autoplay setup for product demonstrations and marketing content
+
+
+
+
+
+
+
+
+
Media Gallery Player
+
+ Compact player for media galleries and previews
+
+
+
+
+
+
+
+
+
+
+ Error Handling
+
+
+
Invalid Video Source
+
+ Demonstrates error handling when video fails to load
+
+
+
+
+
+
+
+
+
+
+ Code Examples
+
+
+
Basic Video Player:
+
<ui-video-player
+ [sources]="videoSources"
+ poster="poster-image.jpg"
+ [showControls]="true"
+ ariaLabel="Demo video player">
+</ui-video-player>
+
+
+
+
Video Player with Subtitles:
+
<ui-video-player
+ [sources]="videoSources"
+ [tracks]="subtitleTracks"
+ poster="poster-image.jpg"
+ size="lg"
+ [showControls]="true"
+ [allowFullscreen]="true"
+ (play)="onVideoPlay()"
+ (pause)="onVideoPause()">
+</ui-video-player>
+
+
+
+
Autoplay Video (Marketing/Demo):
+
<ui-video-player
+ [sources]="videoSources"
+ variant="minimal"
+ size="md"
+ [autoplay]="true"
+ [muted]="true"
+ [loop]="true"
+ [showVolumeControl]="false"
+ [allowFullscreen]="false">
+</ui-video-player>
+
+
+
+
+ `,
+ styleUrl: './video-player-demo.component.scss'
+})
+export class VideoPlayerDemoComponent {
+ // State signals
+ lastEvent = signal('');
+
+ // Demo video sources - using sample videos from the web
+ basicVideoSources = signal([
+ {
+ src: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
+ type: 'video/mp4',
+ quality: '1080p',
+ label: 'HD'
+ },
+ {
+ src: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.webm',
+ type: 'video/webm',
+ quality: '1080p',
+ label: 'HD WebM'
+ }
+ ]);
+
+ errorVideoSources = signal([
+ {
+ src: 'https://invalid-url.example.com/nonexistent.mp4',
+ type: 'video/mp4',
+ quality: '1080p',
+ label: 'Invalid Source'
+ }
+ ]);
+
+ videoTracks = signal([
+ {
+ src: 'data:text/vtt;base64,V0VCVlRUCgowMDowMDowMC4wMDAgLS0+IDAwOjAwOjA1LjAwMAo8Zm9udCBjb2xvcj0iI2ZmZmZmZiI+U2FtcGxlIHN1YnRpdGxlIHRleHQ8L2ZvbnQ+CgowMDowMDowNS4wMDAgLS0+IDAwOjAwOjEwLjAwMAo8Zm9udCBjb2xvcj0iI2ZmZmZmZiI+VGhpcyBpcyBhIGRlbW9uc3RyYXRpb24gb2Ygc3VidGl0bGVzPC9mb250Pg==',
+ kind: 'subtitles',
+ srclang: 'en',
+ label: 'English',
+ default: true
+ }
+ ]);
+
+ // Demo configuration
+ demoConfig = signal({
+ size: 'md' as VideoPlayerSize,
+ variant: 'default' as VideoPlayerVariant,
+ autoplay: false,
+ loop: false,
+ showControls: true,
+ showVolumeControl: true,
+ allowFullscreen: true,
+ disabled: false,
+ poster: 'https://images.unsplash.com/photo-1574717024653-61fd2cf4d44d?w=800&h=450&fit=crop'
+ });
+
+ handleVideoEvent(event: string): void {
+ this.lastEvent.set(`${event} at ${new Date().toLocaleTimeString()}`);
+ console.log(`Video event: ${event}`);
+ }
+
+ handleTimeUpdate(time: number): void {
+ // Only log significant time updates to avoid console spam
+ if (Math.floor(time) % 10 === 0) {
+ console.log(`Time update: ${this.formatTime(time)}`);
+ }
+ }
+
+ handleVolumeChange(volume: number): void {
+ this.lastEvent.set(`Volume changed to ${Math.round(volume * 100)}% at ${new Date().toLocaleTimeString()}`);
+ console.log(`Volume changed: ${volume}`);
+ }
+
+ handleFullscreenChange(isFullscreen: boolean): void {
+ this.lastEvent.set(`${isFullscreen ? 'Entered' : 'Exited'} fullscreen at ${new Date().toLocaleTimeString()}`);
+ console.log(`Fullscreen: ${isFullscreen}`);
+ }
+
+ handleVideoError(event: Event): void {
+ this.lastEvent.set(`Video error occurred at ${new Date().toLocaleTimeString()}`);
+ console.error('Video error:', event);
+ }
+
+ updateDemoConfig(key: string, event: Event): void {
+ const target = event.target as HTMLInputElement;
+ const value = target.checked;
+
+ this.demoConfig.update(config => ({
+ ...config,
+ [key]: value
+ }));
+ }
+
+ updateDemoSize(event: Event): void {
+ const target = event.target as HTMLSelectElement;
+ const size = target.value as VideoPlayerSize;
+
+ this.demoConfig.update(config => ({
+ ...config,
+ size
+ }));
+ }
+
+ updateDemoVariant(event: Event): void {
+ const target = event.target as HTMLSelectElement;
+ const variant = target.value as VideoPlayerVariant;
+
+ this.demoConfig.update(config => ({
+ ...config,
+ variant
+ }));
+ }
+
+ private formatTime(seconds: number): string {
+ if (isNaN(seconds)) return '0:00';
+
+ const minutes = Math.floor(seconds / 60);
+ const secs = Math.floor(seconds % 60);
+
+ return `${minutes}:${secs.toString().padStart(2, '0')}`;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.component.ts b/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.component.ts
new file mode 100644
index 0000000..73c4136
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.component.ts
@@ -0,0 +1,210 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { DashboardSidebarComponent, SidebarMenuItem } from "./dashboard.sidebar.component";
+import { LayoutContainerComponent } from "../../shared/layout-containers/layout-container.component";
+import {
+ faWindowMaximize, faUserCircle, faCertificate, faHandPointer, faIdCard, faTags,
+ faCheckSquare, faKeyboard, faThLarge, faList, faDotCircle, faSearch, faToggleOn,
+ faTable, faImage, faImages, faPlay, faBars, faEdit, faEye, faCompass,
+ faVideo, faComment, faMousePointer, faLayerGroup, faSquare, faCalendarDays, faClock,
+ faGripVertical, faArrowsAlt, faBoxOpen, faChevronLeft, faSpinner, faExclamationTriangle,
+ faCloudUploadAlt, faFileText, faListAlt, faCircle, faExpandArrowsAlt
+} from '@fortawesome/free-solid-svg-icons';
+import { DemoRoutes } from '../../demos';
+import { ScrollContainerComponent } from '../../../../../ui-essentials/src/lib/layouts';
+// import { DemoRoutes } from "../../../../../ui-essentials/src/public-api";
+
+@Component({
+ selector: 'skyui-dashboard',
+ standalone: true,
+ imports: [
+ CommonModule,
+ DashboardSidebarComponent,
+ LayoutContainerComponent,
+ ScrollContainerComponent,
+ DemoRoutes
+],
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+})
+export class DashboardComponent {
+ // Sidebar state
+ sidebarCollapsed = false;
+ route: string = "image-container"
+ faBars = faBars;
+
+ // FontAwesome icons
+ faWindowMaximize = faWindowMaximize;
+ faUserCircle = faUserCircle;
+ faCertificate = faCertificate;
+ faHandPointer = faHandPointer;
+ faIdCard = faIdCard;
+ faTags = faTags;
+ faCheckSquare = faCheckSquare;
+ faKeyboard = faKeyboard;
+ faThLarge = faThLarge;
+ faList = faList;
+ faDotCircle = faDotCircle;
+ faSearch = faSearch;
+ faToggleOn = faToggleOn;
+ faTable = faTable;
+ faImage = faImage;
+ faImages = faImages;
+ faPlay = faPlay;
+ faEdit = faEdit;
+ faEye = faEye;
+ faCompass = faCompass;
+ faVideo = faVideo;
+ faComment = faComment;
+ faMousePointer = faMousePointer;
+ faLayerGroup = faLayerGroup;
+ faSquare = faSquare;
+ faCalendarDays = faCalendarDays;
+ faClock = faClock;
+ faGripVertical = faGripVertical;
+ faArrowsAlt = faArrowsAlt;
+ faBoxOpen = faBoxOpen;
+ faChevronLeft = faChevronLeft;
+ faSpinner = faSpinner;
+ faExclamationTriangle = faExclamationTriangle;
+ faCloudUploadAlt = faCloudUploadAlt;
+ faFileText = faFileText;
+ faListAlt = faListAlt;
+ faCircle = faCircle;
+ faExpandArrowsAlt = faExpandArrowsAlt;
+
+ menuItems: any = []
+
+ // Toggle sidebar method
+ toggleSidebar() {
+ this.sidebarCollapsed = !this.sidebarCollapsed;
+ }
+
+ addMenuItem(id: string, label: string, icon?: any, children?: SidebarMenuItem[]): void {
+ const menuItem: SidebarMenuItem = {
+ id,
+ label,
+ icon,
+ active: false,
+ children,
+ isParent: !!children,
+ expanded: false
+ };
+ this.menuItems.push(menuItem);
+ }
+
+ createChildItem(id: string, label: string, icon?: any): SidebarMenuItem {
+ return {
+ id,
+ label,
+ icon,
+ active: false,
+ isParent: false
+ };
+ }
+
+
+ ngOnInit() {
+ // Forms category
+ const formsChildren = [
+ this.createChildItem("input", "Input Fields", this.faKeyboard),
+ this.createChildItem("checkbox", "Checkboxes", this.faCheckSquare),
+ this.createChildItem("radio", "Radio Buttons", this.faDotCircle),
+ this.createChildItem("search", "Search Bars", this.faSearch),
+ this.createChildItem("switch", "Switches", this.faToggleOn),
+ this.createChildItem("autocomplete", "Autocomplete", this.faListAlt),
+ this.createChildItem("date-picker", "Date Picker", this.faCalendarDays),
+ this.createChildItem("time-picker", "Time Picker", this.faClock),
+ this.createChildItem("file-upload", "File Upload", this.faCloudUploadAlt),
+ this.createChildItem("form-field", "Form Field", this.faFileText)
+ ];
+ this.addMenuItem("forms", "Forms", this.faEdit, formsChildren);
+
+ // Data Display category
+ const dataDisplayChildren = [
+ this.createChildItem("table", "Tables", this.faTable),
+ this.createChildItem("lists", "Lists", this.faList),
+ this.createChildItem("progress", "Progress Bars", this.faCheckSquare),
+ this.createChildItem("badge", "Badges", this.faCertificate),
+ this.createChildItem("avatar", "Avatars", this.faUserCircle),
+ this.createChildItem("cards", "Cards", this.faIdCard)
+ ];
+ this.addMenuItem("data-display", "Data Display", this.faEye, dataDisplayChildren);
+
+ // Navigation category
+ const navigationChildren = [
+ this.createChildItem("appbar", "App Bars", this.faWindowMaximize),
+ this.createChildItem("menu", "Menus", this.faBars),
+ this.createChildItem("pagination", "Pagination", this.faChevronLeft)
+ ];
+ this.addMenuItem("navigation", "Navigation", this.faCompass, navigationChildren);
+
+ // Media category
+ const mediaChildren = [
+ this.createChildItem("image-container", "Image Container", this.faImage),
+ this.createChildItem("carousel", "Carousel", this.faImages),
+ this.createChildItem("video-player", "Video Player", this.faPlay)
+ ];
+ this.addMenuItem("media", "Media", this.faVideo, mediaChildren);
+
+ // Actions category
+ const actionsChildren = [
+ this.createChildItem("buttons", "Buttons", this.faMousePointer)
+ ];
+ this.addMenuItem("actions", "Actions", this.faMousePointer, actionsChildren);
+
+ // Feedback category
+ const feedbackChildren = [
+ this.createChildItem("chips", "Chips", this.faTags),
+ this.createChildItem("loading-spinner", "Loading Spinner", this.faSpinner),
+ this.createChildItem("skeleton-loader", "Skeleton Loader", this.faSpinner),
+ this.createChildItem("empty-state", "Empty State", this.faExclamationTriangle)
+ ];
+ this.addMenuItem("feedback", "Feedback", this.faComment, feedbackChildren);
+
+ // Layout category
+ const layoutChildren = [
+ this.createChildItem("layout", "Layout Demos", this.faThLarge),
+ this.createChildItem("grid-system", "Grid System", this.faGripVertical),
+ this.createChildItem("spacer", "Spacer", this.faArrowsAlt),
+ this.createChildItem("container", "Container", this.faBoxOpen)
+ ];
+ this.addMenuItem("layouts", "Layouts", this.faLayerGroup, layoutChildren);
+
+ // Overlays category
+ const overlaysChildren = [
+ this.createChildItem("modal", "Modal/Dialog", this.faSquare),
+ this.createChildItem("drawer", "Drawer/Sidebar", this.faBars),
+ this.createChildItem("backdrop", "Backdrop", this.faCircle),
+ this.createChildItem("overlay-container", "Overlay Container", this.faExpandArrowsAlt)
+ ];
+ this.addMenuItem("overlays", "Overlays", this.faLayerGroup, overlaysChildren);
+
+ // Utilities (standalone)
+ this.addMenuItem("fontawesome", "FontAwesome Icons", this.faCheckSquare);
+ }
+
+ menuClick(id: string) {
+ console.log(JSON.stringify(id))
+
+ this.route = id.toString()
+ }
+
+}
diff --git a/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.sidebar.component.scss b/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.sidebar.component.scss
new file mode 100644
index 0000000..c8fc7c2
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.sidebar.component.scss
@@ -0,0 +1,63 @@
+@use '../../../../../shared-ui/src/styles/semantic' as semantic;
+
+.sidebar-container {
+ padding: 1rem 0;
+
+ .menu-item-wrapper {
+ margin-bottom: 0.25rem;
+
+ .parent-menu-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.75rem 1rem;
+ cursor: pointer;
+ border-radius: 8px;
+ margin: 0 0.5rem;
+ transition: background-color 0.2s ease;
+
+ &:hover {
+ background-color: semantic.$semantic-color-surface-interactive;
+ }
+
+ .parent-content {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+
+ .parent-icon {
+ font-size: 1rem;
+ color: semantic.$semantic-color-text-secondary;
+ }
+
+ .parent-label {
+ font-weight: 500;
+ color: semantic.$semantic-color-text-primary;
+ font-size: 0.875rem;
+ }
+ }
+
+ .chevron-icon {
+ font-size: 0.75rem;
+ color: semantic.$semantic-color-text-tertiary;
+ transition: transform 0.2s ease;
+ }
+ }
+
+ .children-container {
+ margin-left: 1rem;
+ padding-left: 1rem;
+ border-left: 2px solid semantic.$semantic-color-border-secondary;
+ margin-top: 0.25rem;
+
+ ::ng-deep .child-menu-item {
+ margin-bottom: 0.125rem;
+
+ ui-menu-item {
+ --menu-item-padding: 0.5rem 1rem;
+ --menu-item-font-size: 0.8125rem;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.sidebar.component.ts b/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.sidebar.component.ts
new file mode 100644
index 0000000..bf53ab0
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.sidebar.component.ts
@@ -0,0 +1,132 @@
+import { Component, EventEmitter, Output, Input, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { MenuItemComponent } from '../../../../../ui-essentials/src/lib/components/navigation/menu';
+import { faChevronRight, faChevronDown } from '@fortawesome/free-solid-svg-icons';
+
+export interface SidebarMenuItem {
+ id: string;
+ label: string;
+ icon?: any; // FontAwesome icon definition
+ active: boolean;
+ children?: SidebarMenuItem[];
+ expanded?: boolean;
+ isParent?: boolean;
+}
+
+@Component({
+ selector: 'skyui-dashboard-sidebar',
+ standalone: true,
+ imports: [
+ CommonModule,
+ FontAwesomeModule,
+ MenuItemComponent
+],
+ template: `
+
+ `,
+ styleUrls: ['./dashboard.sidebar.component.scss']
+
+})
+export class DashboardSidebarComponent implements OnInit {
+
+ @Input() data: SidebarMenuItem[] = [];
+ @Input() set active(value: string) { this.setActive(value) }
+ @Output() selected = new EventEmitter();
+
+ faChevronRight = faChevronRight;
+ faChevronDown = faChevronDown;
+
+ handleMenuClick(id: string): void {
+ this.setActive(id);
+ this.selected.emit(id);
+ }
+
+ setActive(id: string): void {
+ this.setActiveRecursive(this.data, id);
+ }
+
+ private setActiveRecursive(items: SidebarMenuItem[], id: string): void {
+ items.forEach(item => {
+ item.active = item.id === id;
+ if (item.children) {
+ this.setActiveRecursive(item.children, id);
+ }
+ });
+ }
+
+ getActiveMenuItem(): SidebarMenuItem | undefined {
+ return this.getActiveMenuItemRecursive(this.data);
+ }
+
+ private getActiveMenuItemRecursive(items: SidebarMenuItem[]): SidebarMenuItem | undefined {
+ for (const item of items) {
+ if (item.active) return item;
+ if (item.children) {
+ const found = this.getActiveMenuItemRecursive(item.children);
+ if (found) return found;
+ }
+ }
+ return undefined;
+ }
+
+ clearSelection(): void {
+ this.clearSelectionRecursive(this.data);
+ }
+
+ private clearSelectionRecursive(items: SidebarMenuItem[]): void {
+ items.forEach(item => {
+ item.active = false;
+ if (item.children) {
+ this.clearSelectionRecursive(item.children);
+ }
+ });
+ }
+
+ toggleExpanded(item: SidebarMenuItem): void {
+ if (item.isParent) {
+ item.expanded = !item.expanded;
+ }
+ }
+
+ ngOnInit(): void {
+ }
+
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/shared/layout-containers/layout-container.component.scss b/projects/demo-ui-essentials/src/app/shared/layout-containers/layout-container.component.scss
new file mode 100644
index 0000000..d65e5a6
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/shared/layout-containers/layout-container.component.scss
@@ -0,0 +1,175 @@
+// Layout Container Component - Main application shell layout
+.ui-layout-container {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+
+ box-sizing: border-box;
+ overflow: hidden;
+
+ display: flex;
+ flex-wrap: nowrap;
+ flex-direction: column;
+ align-items: stretch;
+ flex-flow: column;
+
+ // Top navigation bar
+ &__top-bar {
+ order: 1;
+ flex: 0 0 64px; // Standard app bar height
+ box-sizing: border-box;
+ overflow: hidden;
+ transition: all 300ms cubic-bezier(0.2, 0.0, 0, 1.0);
+ position: relative;
+ }
+
+ // Main content wrapper
+ &__centre-wrapper {
+ width: 100%;
+ box-sizing: border-box;
+ position: relative;
+
+ order: 2;
+ flex: 1;
+ overflow: hidden;
+ min-height: 0;
+
+ display: flex;
+ flex-wrap: nowrap;
+ flex-direction: row;
+ align-items: stretch;
+
+ // Left sidebar navigation
+ .left-navigation {
+ order: 1;
+ flex: 0 0 64px; // Collapsed sidebar width
+ overflow: hidden;
+ position: relative;
+ box-sizing: border-box;
+ height: 100%;
+ transition: all 300ms cubic-bezier(0.2, 0.0, 0, 1.0);
+
+ // Navigation states
+ &--hide {
+ flex: 0 0 0;
+ }
+
+ &--rail {
+ flex: 0 0 80px; // Rail navigation width
+ }
+
+ &--full {
+ flex: 0 0 320px; // Expanded sidebar width
+ }
+ }
+
+ // Main content area
+ .main-body {
+ order: 2;
+ flex: 1;
+ overflow: hidden;
+ min-height: 0;
+ box-sizing: border-box;
+ position: relative;
+
+ display: flex;
+ flex-wrap: nowrap;
+ flex-direction: column;
+ align-items: stretch;
+
+ // Inner header section
+ &__header {
+ order: 1;
+ flex: 0 0 0;
+ box-sizing: border-box;
+ overflow: hidden;
+ transition: all 300ms cubic-bezier(0.2, 0.0, 0, 1.0);
+ width: 100%;
+ position: relative;
+ }
+
+ // Inner content body
+ &__body {
+ order: 2;
+ flex: 1;
+ overflow: hidden;
+ min-height: 0;
+ box-sizing: border-box;
+ }
+
+ // Inner footer section
+ &__footer {
+ order: 3;
+ flex: 0 0 0;
+ box-sizing: border-box;
+ overflow: hidden;
+ transition: all 300ms cubic-bezier(0.2, 0.0, 0, 1.0);
+ width: 100%;
+ position: relative;
+ }
+ }
+
+ // Right sidebar navigation
+ .right-navigation {
+ order: 3;
+ flex: 0 0 320px; // Right sidebar width
+ overflow: hidden;
+ position: relative;
+ box-sizing: border-box;
+ height: 100%;
+ transition: all 300ms cubic-bezier(0.2, 0.0, 0, 1.0);
+
+ // Right navigation states
+ &--hide {
+ flex: 0 0 0;
+ }
+
+ &--show {
+ flex: 0 0 320px; // Shown right sidebar width
+ }
+ }
+ }
+
+ // Bottom bar section
+ &__bottom-bar {
+ order: 3;
+ flex: 0 0 0;
+ overflow: hidden;
+ box-sizing: border-box;
+ transition: all 300ms cubic-bezier(0.2, 0.0, 0, 1.0);
+ }
+
+ // Responsive design for smaller screens
+ @media (max-width: 768px) {
+ &__centre-wrapper {
+ .left-navigation {
+ &--full {
+ flex: 0 0 280px; // Smaller full width on mobile
+ }
+ }
+
+ .right-navigation {
+ &--show {
+ flex: 0 0 280px; // Smaller right sidebar on mobile
+ }
+ }
+ }
+ }
+
+ // Mobile breakpoint - stack navigation vertically
+ @media (max-width: 640px) {
+ &__centre-wrapper {
+ flex-direction: column;
+
+ .left-navigation,
+ .right-navigation {
+ &--full,
+ &--show {
+ flex: 0 0 64px; // Navigation becomes horizontal bar on mobile
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/shared/layout-containers/layout-container.component.ts b/projects/demo-ui-essentials/src/app/shared/layout-containers/layout-container.component.ts
new file mode 100644
index 0000000..f7c7b46
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/shared/layout-containers/layout-container.component.ts
@@ -0,0 +1,138 @@
+import { Component, ChangeDetectionStrategy, input, computed } from '@angular/core';
+
+export type NavigationState = 'hide' | 'rail' | 'full';
+export type SidebarState = 'hide' | 'show';
+
+@Component({
+ selector: 'ui-layout-container',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ styleUrl: './layout-container.component.scss',
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+})
+export class LayoutContainerComponent {
+ // Input signals for layout configuration
+ leftNavigationState = input('rail');
+ rightNavigationState = input('hide');
+
+ // Show/hide inputs
+ showTopBar = input(false);
+ showInnerHeader = input(false);
+ showInnerFooter = input(false);
+ showBottomBar = input(false);
+
+ // Size inputs (in pixels) - with sensible defaults
+ topBarHeight = input(64);
+ innerHeaderHeight = input(56);
+ innerFooterHeight = input(48);
+ bottomBarHeight = input(56);
+
+ // Custom width overrides
+ customLeftWidth = input(null);
+ customRightWidth = input(null);
+
+ // Computed CSS classes for navigation states
+ leftNavigationClasses = computed(() => {
+ const state = this.leftNavigationState();
+ return `left-navigation--${state}`;
+ });
+
+ rightNavigationClasses = computed(() => {
+ const state = this.rightNavigationState();
+ return `right-navigation--${state}`;
+ });
+
+ // Computed widths based on state and custom overrides
+ leftNavigationWidth = computed(() => {
+ const customWidth = this.customLeftWidth();
+ if (customWidth !== null) return customWidth;
+
+ const state = this.leftNavigationState();
+ switch (state) {
+ case 'hide': return 0;
+ case 'rail': return 81;
+ case 'full': return 320;
+ default: return 64;
+ }
+ });
+
+ rightNavigationWidth = computed(() => {
+ const customWidth = this.customRightWidth();
+ if (customWidth !== null) return customWidth;
+
+ const state = this.rightNavigationState();
+ switch (state) {
+ case 'hide': return 0;
+ case 'show': return 320;
+ default: return 0;
+ }
+ });
+
+ // Computed effective heights (0 when hidden, configured height when shown)
+ effectiveTopBarHeight = computed(() => {
+ return this.showTopBar() ? this.topBarHeight() : 0;
+ });
+
+ effectiveInnerHeaderHeight = computed(() => {
+ return this.showInnerHeader() ? this.innerHeaderHeight() : 0;
+ });
+
+ effectiveInnerFooterHeight = computed(() => {
+ return this.showInnerFooter() ? this.innerFooterHeight() : 0;
+ });
+
+ effectiveBottomBarHeight = computed(() => {
+ return this.showBottomBar() ? this.bottomBarHeight() : 0;
+ });
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/scss/_variables.scss b/projects/demo-ui-essentials/src/scss/_variables.scss
new file mode 100644
index 0000000..e731d61
--- /dev/null
+++ b/projects/demo-ui-essentials/src/scss/_variables.scss
@@ -0,0 +1,81 @@
+// ==========================================================================
+// SKYUI PROJECT VARIABLES
+// ==========================================================================
+// Project-specific variable overrides that build upon the shared-ui token system
+// Import order: base tokens → semantic tokens → project overrides
+// NOTE: Semantic tokens are imported in styles.scss and available here
+// ==========================================================================
+
+// ==========================================================================
+// FONT FAMILY OVERRIDES
+// ==========================================================================
+// Override base font families to use Comfortaa
+$base-typography-font-family-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
+$base-typography-font-family-display: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
+
+@import '../../../shared-ui/src/styles/semantic';
+
+// These overrides will cascade through all semantic typography tokens
+// that use $base-typography-font-family-sans and $base-typography-font-family-display
+
+// ==========================================================================
+// PROJECT-SPECIFIC OVERRIDES
+// ==========================================================================
+// Custom values that override semantic tokens for SkyUI project needs
+// Use semantic tokens as base values to maintain design system consistency
+
+// BRAND CUSTOMIZATION
+// Override brand colors while maintaining semantic structure
+$skyui-brand-primary: $semantic-color-brand-primary !default;
+$skyui-brand-secondary: $semantic-color-brand-secondary !default;
+$skyui-brand-accent: $semantic-color-brand-tertiary !default;
+
+// SURFACE CUSTOMIZATION
+// Project-specific surface treatments
+$skyui-surface-elevated: $semantic-color-surface-elevated !default;
+$skyui-surface-sunken: $semantic-color-surface-sunken !default;
+$skyui-surface-interactive: $semantic-color-surface-interactive !default;
+
+// TYPOGRAPHY SCALE ADJUSTMENTS
+// Semantic typography tokens can be overridden for project-specific needs
+$skyui-heading-display: $semantic-typography-heading-display !default;
+$skyui-heading-h1: $semantic-typography-heading-h1 !default;
+$skyui-heading-h2: $semantic-typography-heading-h2 !default;
+$skyui-body-large: $semantic-typography-body-large !default;
+$skyui-body-medium: $semantic-typography-body-medium !default;
+
+// SPACING CUSTOMIZATION
+// Build on semantic spacing tokens
+$skyui-space-component: $semantic-spacing-component-md !default;
+$skyui-space-layout: $semantic-spacing-layout-lg !default;
+$skyui-space-section: $semantic-spacing-section-xl !default;
+
+// INTERACTIVE ELEMENTS
+// Button and form element customizations
+$skyui-interactive-primary: $semantic-color-interactive-primary !default;
+$skyui-interactive-secondary: $semantic-color-interactive-secondary !default;
+$skyui-border-radius-component: $semantic-border-radius-md !default;
+$skyui-border-radius-container: $semantic-border-radius-lg !default;
+
+// ELEVATION & SHADOWS
+// Project-specific shadow treatments
+$skyui-shadow-elevated: $semantic-shadow-elevated !default;
+$skyui-shadow-floating: $semantic-shadow-floating !default;
+
+// ==========================================================================
+// COMPONENT-SPECIFIC OVERRIDES
+// ==========================================================================
+// Variables for specific component customizations
+
+// Dashboard specific
+$skyui-dashboard-grid-gap: $semantic-spacing-layout-md !default;
+$skyui-dashboard-card-padding: $semantic-spacing-component-lg !default;
+
+// Navigation specific
+$skyui-nav-height: 64px !default;
+$skyui-sidebar-width: 280px !default;
+$skyui-sidebar-collapsed-width: 72px !default;
+
+// Form specific
+$skyui-input-border-radius: $semantic-border-radius-sm !default;
+$skyui-input-padding: $semantic-spacing-component-sm !default;
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/scss/overrides/_colors.scss b/projects/demo-ui-essentials/src/scss/overrides/_colors.scss
new file mode 100644
index 0000000..f12266d
--- /dev/null
+++ b/projects/demo-ui-essentials/src/scss/overrides/_colors.scss
@@ -0,0 +1,380 @@
+// ==========================================================================
+// SKYUI COLOR OVERRIDES
+// ==========================================================================
+// Project-specific color customizations built on semantic color tokens
+// Extends the shared design system with SkyUI-specific color patterns
+// ==========================================================================
+
+// ==========================================================================
+// THEME COLOR UTILITIES
+// ==========================================================================
+// Utility classes for applying semantic colors consistently
+
+.skyui-color {
+ // Brand color utilities
+ &--brand-primary {
+ color: $semantic-color-brand-primary;
+ }
+
+ &--brand-secondary {
+ color: $semantic-color-brand-secondary;
+ }
+
+ &--brand-accent {
+ color: $semantic-color-brand-accent;
+ }
+
+ // Text color utilities
+ &--text-primary {
+ color: $semantic-color-text-primary;
+ }
+
+ &--text-secondary {
+ color: $semantic-color-text-secondary;
+ }
+
+ &--text-tertiary {
+ color: $semantic-color-text-tertiary;
+ }
+
+ &--text-inverse {
+ color: $semantic-color-text-inverse;
+ }
+
+ // Feedback colors
+ &--success {
+ color: $semantic-color-success;
+ }
+
+ &--warning {
+ color: $semantic-color-warning;
+ }
+
+ &--danger {
+ color: $semantic-color-danger;
+ }
+
+ &--info {
+ color: $semantic-color-info;
+ }
+}
+
+// ==========================================================================
+// BACKGROUND COLOR UTILITIES
+// ==========================================================================
+// Background color utilities using semantic tokens
+
+.skyui-bg {
+ // Surface backgrounds
+ &--surface-primary {
+ background-color: $semantic-color-surface-primary;
+ }
+
+ &--surface-secondary {
+ background-color: $semantic-color-surface-secondary;
+ }
+
+ &--surface-elevated {
+ background-color: $semantic-color-surface-elevated;
+ }
+
+ &--surface-sunken {
+ background-color: $semantic-color-surface-sunken;
+ }
+
+ &--surface-interactive {
+ background-color: $semantic-color-surface-interactive;
+
+ &:hover {
+ background-color: var(--color-surface-interactive-hover, $semantic-color-surface-interactive);
+ filter: brightness(0.95);
+ }
+ }
+
+ // Brand backgrounds
+ &--brand-primary {
+ background-color: $semantic-color-brand-primary;
+ color: $semantic-color-on-brand-primary;
+ }
+
+ &--brand-secondary {
+ background-color: $semantic-color-brand-secondary;
+ color: $semantic-color-on-brand-secondary;
+ }
+
+ // Container backgrounds
+ &--container-primary {
+ background-color: $semantic-color-container-primary;
+ color: $semantic-color-on-container-primary;
+ }
+
+ &--container-secondary {
+ background-color: $semantic-color-container-secondary;
+ color: $semantic-color-on-container-secondary;
+ }
+
+ // Feedback backgrounds
+ &--success {
+ background-color: var(--color-success-light, $semantic-color-success);
+ filter: brightness(1.4) saturate(0.6);
+ color: var(--color-success-dark, $semantic-color-success);
+ border: 1px solid var(--color-success-border, $semantic-color-success);
+ }
+
+ &--warning {
+ background-color: var(--color-warning-light, $semantic-color-warning);
+ filter: brightness(1.4) saturate(0.6);
+ color: var(--color-warning-dark, $semantic-color-warning);
+ border: 1px solid var(--color-warning-border, $semantic-color-warning);
+ }
+
+ &--danger {
+ background-color: var(--color-danger-light, $semantic-color-danger);
+ filter: brightness(1.4) saturate(0.6);
+ color: var(--color-danger-dark, $semantic-color-danger);
+ border: 1px solid var(--color-danger-border, $semantic-color-danger);
+ }
+
+ &--info {
+ background-color: var(--color-info-light, $semantic-color-info);
+ filter: brightness(1.4) saturate(0.6);
+ color: var(--color-info-dark, $semantic-color-info);
+ border: 1px solid var(--color-info-border, $semantic-color-info);
+ }
+}
+
+// ==========================================================================
+// BORDER COLOR UTILITIES
+// ==========================================================================
+// Border color utilities using semantic border tokens
+
+.skyui-border {
+ // Standard borders
+ &--primary {
+ border-color: $semantic-color-border-primary;
+ }
+
+ &--secondary {
+ border-color: $semantic-color-border-secondary;
+ }
+
+ &--focus {
+ border-color: $semantic-color-border-focus;
+ outline: none;
+ }
+
+ &--error {
+ border-color: $semantic-color-border-error;
+ }
+
+ &--disabled {
+ border-color: $semantic-color-border-disabled;
+ }
+
+ // Border styles
+ &--solid {
+ border-style: solid;
+ border-width: 1px;
+ }
+
+ &--dashed {
+ border-style: dashed;
+ border-width: 1px;
+ }
+
+ &--thick {
+ border-width: 2px;
+ }
+}
+
+// ==========================================================================
+// INTERACTIVE STATE COLORS
+// ==========================================================================
+// Color variations for interactive elements
+
+.skyui-interactive {
+ // Primary interactive states
+ &--primary {
+ color: $semantic-color-interactive-primary;
+
+ &:hover {
+ color: var(--color-primary-hover, $semantic-color-interactive-primary);
+ }
+
+ &:active {
+ color: var(--color-primary-active, $semantic-color-interactive-primary);
+ }
+
+ &:focus {
+ color: $semantic-color-interactive-primary;
+ outline: 2px solid $semantic-color-focus-ring;
+ outline-offset: 2px;
+ }
+ }
+
+ // Secondary interactive states
+ &--secondary {
+ color: $semantic-color-interactive-secondary;
+
+ &:hover {
+ color: var(--color-secondary-hover, $semantic-color-interactive-secondary);
+ }
+
+ &:active {
+ color: var(--color-secondary-active, $semantic-color-interactive-secondary);
+ }
+ }
+
+ // Tertiary interactive states
+ &--tertiary {
+ color: $semantic-color-interactive-tertiary;
+
+ &:hover {
+ color: var(--color-tertiary-hover, $semantic-color-interactive-tertiary);
+ }
+
+ &:active {
+ color: var(--color-tertiary-active, $semantic-color-interactive-tertiary);
+ }
+ }
+
+ // Neutral interactive states
+ &--neutral {
+ color: $semantic-color-text-secondary;
+
+ &:hover {
+ color: $semantic-color-text-primary;
+ }
+ }
+}
+
+// ==========================================================================
+// COMPONENT-SPECIFIC COLORS
+// ==========================================================================
+// Color patterns for specific SkyUI components
+
+// Dashboard specific colors
+.skyui-dashboard {
+ &__card {
+ background-color: $semantic-color-surface-elevated;
+ border: 1px solid $semantic-color-border-secondary;
+ box-shadow: $semantic-shadow-elevated;
+
+ &:hover {
+ box-shadow: $semantic-shadow-floating;
+ }
+ }
+
+ &__metric {
+ &--positive {
+ color: $semantic-color-success;
+ }
+
+ &--negative {
+ color: $semantic-color-danger;
+ }
+
+ &--neutral {
+ color: $semantic-color-text-secondary;
+ }
+ }
+}
+
+// Navigation specific colors
+.skyui-navigation {
+ &__item {
+ color: $semantic-color-text-secondary;
+
+ &:hover {
+ color: $semantic-color-text-primary;
+ background-color: $semantic-color-surface-interactive;
+ }
+
+ &.active {
+ color: $semantic-color-brand-primary;
+ background-color: var(--color-brand-primary-light, $semantic-color-brand-primary);
+ font-weight: $base-typography-font-weight-medium;
+ }
+ }
+
+ &__divider {
+ background-color: $semantic-color-border-secondary;
+ }
+}
+
+// Form specific colors
+.skyui-form {
+ &__input {
+ background-color: $semantic-color-surface-primary;
+ border-color: $semantic-color-border-secondary;
+ color: $semantic-color-text-primary;
+
+ &:focus {
+ border-color: $semantic-color-border-focus;
+ box-shadow: 0 0 0 3px $semantic-color-focus-ring;
+ }
+
+ &.error {
+ border-color: $semantic-color-border-error;
+ }
+
+ &:disabled {
+ background-color: $semantic-color-surface-disabled;
+ border-color: $semantic-color-border-disabled;
+ color: $semantic-color-text-disabled;
+ }
+ }
+
+ &__label {
+ color: $semantic-color-text-primary;
+
+ &.required {
+ &::after {
+ color: $semantic-color-danger;
+ }
+ }
+ }
+}
+
+// ==========================================================================
+// STATUS AND NOTIFICATION COLORS
+// ==========================================================================
+// Color patterns for status indicators and notifications
+
+.skyui-status {
+ &--online {
+ color: $semantic-color-success;
+
+ &::before {
+ content: '●';
+ margin-right: $semantic-spacing-component-2xs;
+ }
+ }
+
+ &--offline {
+ color: $semantic-color-danger;
+
+ &::before {
+ content: '●';
+ margin-right: $semantic-spacing-component-2xs;
+ }
+ }
+
+ &--pending {
+ color: $semantic-color-warning;
+
+ &::before {
+ content: '●';
+ margin-right: $semantic-spacing-component-2xs;
+ }
+ }
+
+ &--inactive {
+ color: $semantic-color-text-tertiary;
+
+ &::before {
+ content: '●';
+ margin-right: $semantic-spacing-component-2xs;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/scss/overrides/_index.scss b/projects/demo-ui-essentials/src/scss/overrides/_index.scss
new file mode 100644
index 0000000..18e3d4c
--- /dev/null
+++ b/projects/demo-ui-essentials/src/scss/overrides/_index.scss
@@ -0,0 +1,12 @@
+// ==========================================================================
+// SKYUI OVERRIDES INDEX
+// ==========================================================================
+// Centralized import for all SkyUI project overrides
+// Import order maintains proper cascade and dependency resolution
+// ==========================================================================
+
+// Typography overrides - semantic typography token extensions
+@import 'typography';
+
+// Color overrides - semantic color token extensions
+@import 'colors';
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/scss/overrides/_typography.scss b/projects/demo-ui-essentials/src/scss/overrides/_typography.scss
new file mode 100644
index 0000000..6408afb
--- /dev/null
+++ b/projects/demo-ui-essentials/src/scss/overrides/_typography.scss
@@ -0,0 +1,256 @@
+// ==========================================================================
+// SKYUI TYPOGRAPHY OVERRIDES
+// ==========================================================================
+// Project-specific typography customizations built on semantic typography tokens
+// Extends the shared design system with SkyUI-specific typography patterns
+// ==========================================================================
+
+// Typography utility mixins that use semantic tokens
+@mixin apply-typography($typography-map) {
+ font-family: map-get($typography-map, font-family);
+ font-size: map-get($typography-map, font-size);
+ font-weight: map-get($typography-map, font-weight);
+ line-height: map-get($typography-map, line-height);
+ letter-spacing: map-get($typography-map, letter-spacing);
+}
+
+// ==========================================================================
+// HEADING STYLES
+// ==========================================================================
+// Custom heading treatments that build on semantic typography
+
+.skyui-heading {
+ // Display heading for hero sections
+ &--display {
+ @include apply-typography($semantic-typography-heading-display);
+ color: $semantic-color-text-primary;
+
+ // Project-specific enhancements
+ text-align: center;
+ margin-bottom: $semantic-spacing-component-xl;
+
+ @media (max-width: $base-breakpoint-md) {
+ font-size: $base-typography-font-size-2xl;
+ }
+ }
+
+ // Primary headings
+ &--h1 {
+ @include apply-typography($semantic-typography-heading-h1);
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-component-lg;
+
+ // SkyUI specific styling
+ position: relative;
+
+ &::after {
+ content: '';
+ position: absolute;
+ bottom: -#{$semantic-spacing-component-xs};
+ left: 0;
+ width: 60px;
+ height: 3px;
+ background-color: $semantic-color-brand-primary;
+ }
+ }
+
+ // Secondary headings
+ &--h2 {
+ @include apply-typography($semantic-typography-heading-h2);
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-component-md;
+ margin-top: $semantic-spacing-component-xl;
+ }
+
+ // Tertiary headings
+ &--h3 {
+ @include apply-typography($semantic-typography-heading-h3);
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-component-sm;
+ margin-top: $semantic-spacing-component-lg;
+ }
+
+ // Quaternary headings
+ &--h4 {
+ @include apply-typography($semantic-typography-heading-h4);
+ color: $semantic-color-text-secondary;
+ margin-bottom: $semantic-spacing-component-xs;
+ margin-top: $semantic-spacing-component-md;
+ }
+}
+
+// ==========================================================================
+// BODY TEXT STYLES
+// ==========================================================================
+// Content text styles using semantic typography tokens
+
+.skyui-text {
+ // Large body text for important content
+ &--large {
+ @include apply-typography($semantic-typography-body-large);
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-component-md;
+ }
+
+ // Standard body text
+ &--medium {
+ @include apply-typography($semantic-typography-body-medium);
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+
+ // Small body text for captions, etc.
+ &--small {
+ @include apply-typography($semantic-typography-body-small);
+ color: $semantic-color-text-secondary;
+ margin-bottom: $semantic-spacing-component-xs;
+ }
+
+ // Caption text
+ &--caption {
+ @include apply-typography($semantic-typography-caption);
+ color: $semantic-color-text-tertiary;
+ }
+
+ // Muted/secondary text
+ &--muted {
+ color: $semantic-color-text-secondary;
+ }
+
+ // Emphasized text
+ &--emphasis {
+ color: $semantic-color-brand-primary;
+ font-weight: $base-typography-font-weight-medium;
+ }
+}
+
+// ==========================================================================
+// INTERACTIVE TEXT STYLES
+// ==========================================================================
+// Text styles for buttons, links, and interactive elements
+
+.skyui-interactive-text {
+ // Button text styles
+ &--button-large {
+ @include apply-typography($semantic-typography-button-large);
+ text-transform: uppercase;
+ }
+
+ &--button-medium {
+ @include apply-typography($semantic-typography-button-medium);
+ text-transform: uppercase;
+ }
+
+ &--button-small {
+ @include apply-typography($semantic-typography-button-small);
+ text-transform: uppercase;
+ }
+
+ // Link styles
+ &--link {
+ @include apply-typography($semantic-typography-body-medium);
+ color: $semantic-color-interactive-primary;
+ text-decoration: none;
+
+ &:hover {
+ color: var(--color-primary-hover, $semantic-color-interactive-primary);
+ filter: brightness(0.9);
+ text-decoration: underline;
+ }
+
+ &:focus {
+ outline: 2px solid $semantic-color-border-focus;
+ outline-offset: 2px;
+ }
+ }
+
+ // Navigation text
+ &--nav-primary {
+ @include apply-typography($semantic-typography-nav-primary);
+ color: $semantic-color-text-primary;
+
+ &.active {
+ color: $semantic-color-brand-primary;
+ font-weight: $base-typography-font-weight-semibold;
+ }
+ }
+
+ &--nav-secondary {
+ @include apply-typography($semantic-typography-nav-secondary);
+ color: $semantic-color-text-secondary;
+ }
+}
+
+// ==========================================================================
+// FORM TEXT STYLES
+// ==========================================================================
+// Text styles for form elements using semantic tokens
+
+.skyui-form-text {
+ // Labels
+ &--label {
+ @include apply-typography($semantic-typography-label);
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-component-xs;
+ display: block;
+
+ &.required::after {
+ content: ' *';
+ color: $semantic-color-danger;
+ }
+ }
+
+ // Input text
+ &--input {
+ @include apply-typography($semantic-typography-input);
+ color: $semantic-color-text-primary;
+
+ &::placeholder {
+ @include apply-typography($semantic-typography-placeholder);
+ color: $semantic-color-text-tertiary;
+ }
+ }
+
+ // Help text
+ &--help {
+ @include apply-typography($semantic-typography-caption);
+ color: $semantic-color-text-secondary;
+ margin-top: $semantic-spacing-component-xs;
+ }
+
+ // Error text
+ &--error {
+ @include apply-typography($semantic-typography-caption);
+ color: $semantic-color-danger;
+ margin-top: $semantic-spacing-component-xs;
+ }
+}
+
+// ==========================================================================
+// CODE TEXT STYLES
+// ==========================================================================
+// Code and monospace text styles
+
+.skyui-code {
+ // Inline code
+ &--inline {
+ @include apply-typography($semantic-typography-code-inline);
+ background-color: $semantic-color-surface-secondary;
+ color: $semantic-color-text-primary;
+ padding: $semantic-spacing-component-2xs $semantic-spacing-component-xs;
+ border-radius: $semantic-border-radius-sm;
+ border: 1px solid $semantic-color-border-secondary;
+ }
+
+ // Code block
+ &--block {
+ @include apply-typography($semantic-typography-code-block);
+ background-color: $semantic-color-surface-sunken;
+ color: $semantic-color-text-primary;
+ padding: $semantic-spacing-component-md;
+ border-radius: $semantic-border-radius-md;
+ border: 1px solid $semantic-color-border-secondary;
+ overflow-x: auto;
+ white-space: pre;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/styles.scss b/projects/demo-ui-essentials/src/styles.scss
index 90d4ee0..5f2f657 100644
--- a/projects/demo-ui-essentials/src/styles.scss
+++ b/projects/demo-ui-essentials/src/styles.scss
@@ -1 +1,35 @@
/* You can add global styles to this file, and also import other style files */
+
+// Set font paths before importing fontfaces
+$inter-font-path: "/fonts/inter/" !default;
+$comfortaa-font-path: "/fonts/comfortaa/" !default;
+
+// Import specific fonts we need for the demo
+@import '../../shared-ui/src/styles/fontfaces/inter';
+@import '../../shared-ui/src/styles/fontfaces/comfortaa';
+
+// Import project variables (which now has semantic tokens available)
+@import 'scss/_variables';
+
+html {
+ height: 100%;
+ font-size: 16px; // Base font size for rem calculations
+}
+
+body {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+ background-color: $semantic-color-surface-primary;
+ color: $semantic-color-text-primary;
+
+ // Apply base typography using semantic tokens
+ 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);
+
+ // Improve text rendering
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
diff --git a/projects/shared-ui/src/styles/base/_spacing.scss b/projects/shared-ui/src/styles/base/_spacing.scss
index d3a4ff9..905cd0f 100644
--- a/projects/shared-ui/src/styles/base/_spacing.scss
+++ b/projects/shared-ui/src/styles/base/_spacing.scss
@@ -16,6 +16,7 @@ $base-spacing-2-5: 0.625rem;
$base-spacing-3: 0.75rem;
$base-spacing-3-5: 0.875rem;
$base-spacing-4: 1rem;
+$base-spacing-4-5: 1.125rem;
$base-spacing-5: 1.25rem;
$base-spacing-6: 1.5rem;
$base-spacing-7: 1.75rem;
diff --git a/projects/shared-ui/src/styles/semantic/index.scss b/projects/shared-ui/src/styles/semantic/index.scss
index 97ab0ae..980ca89 100644
--- a/projects/shared-ui/src/styles/semantic/index.scss
+++ b/projects/shared-ui/src/styles/semantic/index.scss
@@ -9,19 +9,19 @@
@forward '../base';
// Forward semantic token categories
-@forward 'colors/index';
-@forward 'typography/index';
-@forward 'spacing/index';
-@forward 'motion/index';
+@forward 'colors';
+@forward 'typography';
+@forward 'spacing';
+@forward 'motion';
//@forward 'elevation/index';
//@forward 'layout/index';
//@forward 'breakpoints/index';
// Forward additional semantic categories
-@forward 'borders/index';
-@forward 'shadows/index';
-@forward 'sizing/index';
-@forward 'glass/index';
-@forward 'z-index/index';
-@forward 'opacity/index';
+@forward 'borders';
+@forward 'shadows';
+@forward 'sizing';
+@forward 'glass';
+@forward 'z-index';
+@forward 'opacity';
diff --git a/projects/shared-ui/src/styles/semantic/spacing/_index.scss b/projects/shared-ui/src/styles/semantic/spacing/_index.scss
index 65538b8..5f6d65d 100644
--- a/projects/shared-ui/src/styles/semantic/spacing/_index.scss
+++ b/projects/shared-ui/src/styles/semantic/spacing/_index.scss
@@ -7,4 +7,4 @@
// Note: base tokens imported at semantic/index level
// Semantic spacing mappings
-@import 'semantic-spacing';
\ No newline at end of file
+@forward 'semantic-spacing';
\ No newline at end of file
diff --git a/projects/shared-ui/src/styles/semantic/spacing/_semantic-spacing.scss b/projects/shared-ui/src/styles/semantic/spacing/_semantic-spacing.scss
index ab53395..749a711 100644
--- a/projects/shared-ui/src/styles/semantic/spacing/_semantic-spacing.scss
+++ b/projects/shared-ui/src/styles/semantic/spacing/_semantic-spacing.scss
@@ -114,11 +114,12 @@ $semantic-spacing-2-5: $base-spacing-2-5 !default;
$semantic-spacing-3: $base-spacing-3 !default;
$semantic-spacing-3-5: $base-spacing-3-5 !default;
$semantic-spacing-4: $base-spacing-4 !default;
+$semantic-spacing-4-5: $base-spacing-4-5 !default;
$semantic-spacing-5: $base-spacing-5 !default;
$semantic-spacing-6: $base-spacing-6 !default;
$semantic-spacing-7: $base-spacing-7 !default;
$semantic-spacing-8: $base-spacing-8 !default;
-$semantic-spacing-9: $base-spacing-9 !default;
+$semantic-spacing-9: $base-spacing-9 !default;
$semantic-spacing-10: $base-spacing-10 !default;
$semantic-spacing-11: $base-spacing-11 !default;
$semantic-spacing-12: $base-spacing-12 !default;
diff --git a/projects/shared-ui/src/styles/tokens.scss b/projects/shared-ui/src/styles/tokens.scss
index 5d0b279..9dd3766 100644
--- a/projects/shared-ui/src/styles/tokens.scss
+++ b/projects/shared-ui/src/styles/tokens.scss
@@ -11,4 +11,4 @@
// ==========================================================================
// Forward complete semantic layer (includes base tokens)
-@forward 'semantic/index";
\ No newline at end of file
+@forward 'semantic/index';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/buttons/fab.component.scss b/projects/ui-essentials/src/lib/components/buttons/fab.component.scss
new file mode 100644
index 0000000..59fd5de
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/buttons/fab.component.scss
@@ -0,0 +1,303 @@
+@use "../../../../../shared-ui/src/styles/semantic/index" as *;
+.ui-fab {
+ // Reset and base styles
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border: none;
+ border-radius: 50%;
+ cursor: pointer;
+ outline: none;
+ position: relative;
+ overflow: hidden;
+
+ // Interaction states
+ user-select: none;
+ -webkit-tap-highlight-color: transparent;
+ box-sizing: border-box;
+
+ // Transitions
+ transition: all $semantic-motion-duration-normal $semantic-motion-easing-ease-out;
+
+ // Size variants
+ &--small {
+ width: $semantic-spacing-10; // 2.5rem
+ height: $semantic-spacing-10; // 2.5rem
+ box-shadow: $semantic-shadow-card-hover;
+
+ .fab-icon {
+ font-size: $semantic-typography-font-size-lg; // 1.125rem
+ }
+
+ .fab-touch-overlay {
+ width: $semantic-spacing-4; // 1rem
+ height: $semantic-spacing-4; // 1rem
+ top: $semantic-spacing-3; // 0.75rem
+ left: $semantic-spacing-3; // 0.75rem
+ border-radius: $semantic-border-radius-sm;
+ }
+
+ .fab-spinner {
+ width: $semantic-spacing-4; // 1rem
+ height: $semantic-spacing-4; // 1rem
+ border-width: $semantic-border-width-2; // 2px (closest to 1.5px)
+ }
+ }
+
+ &--medium {
+ width: $semantic-spacing-14; // 3.5rem
+ height: $semantic-spacing-14; // 3.5rem
+ box-shadow: $semantic-shadow-card-active;
+
+ .fab-icon {
+ font-size: $semantic-typography-font-size-2xl; // 1.5rem
+ }
+
+ .fab-touch-overlay {
+ width: $semantic-spacing-5; // 1.25rem
+ height: $semantic-spacing-5; // 1.25rem
+ top: $semantic-spacing-4-5; // ~1.125rem (closest available)
+ left: $semantic-spacing-4-5; // ~1.125rem (closest available)
+ border-radius: $semantic-border-radius-lg;
+ }
+
+ .fab-spinner {
+ width: $semantic-spacing-5; // 1.25rem
+ height: $semantic-spacing-5; // 1.25rem
+ border-width: $semantic-border-width-2;
+ }
+ }
+
+ &--large {
+ width: $semantic-spacing-24; // 6rem
+ height: $semantic-spacing-24; // 6rem
+ box-shadow: $semantic-shadow-card-featured;
+
+ .fab-icon {
+ font-size: $semantic-typography-font-size-5xl; // 3rem (closest to 2.5rem)
+ }
+
+ .fab-touch-overlay {
+ width: $semantic-spacing-10; // 2.5rem
+ height: $semantic-spacing-10; // 2.5rem
+ top: $semantic-spacing-7; // 1.75rem
+ left: $semantic-spacing-7; // 1.75rem
+ border-radius: $semantic-border-radius-xl;
+ }
+
+ .fab-spinner {
+ width: $semantic-spacing-8; // 2rem
+ height: $semantic-spacing-8; // 2rem
+ border-width: $semantic-border-width-3;
+ }
+ }
+
+ // Color variants
+ &--primary {
+ background-color: $semantic-color-interactive-primary;
+ color: $semantic-color-on-brand-primary;
+
+ &:hover:not(:disabled) {
+ background-color: $semantic-color-brand-primary;
+ transform: $semantic-motion-hover-transform-scale-md;
+ box-shadow: $semantic-shadow-elevation-4;
+ }
+ }
+
+ &--secondary {
+ background-color: $semantic-color-interactive-secondary;
+ color: $semantic-color-on-brand-secondary;
+
+ &:hover:not(:disabled) {
+ background-color: $semantic-color-brand-secondary;
+ transform: $semantic-motion-hover-transform-scale-md;
+ box-shadow: $semantic-shadow-elevation-4;
+ }
+ }
+
+ &--tertiary {
+ background-color: $semantic-color-interactive-tertiary;
+ color: $semantic-color-on-brand-tertiary;
+
+ &:hover:not(:disabled) {
+ background-color: $semantic-color-brand-tertiary;
+ transform: $semantic-motion-hover-transform-scale-md;
+ box-shadow: $semantic-shadow-elevation-4;
+ }
+ }
+
+ // Position variants
+ &--bottom-right {
+ position: fixed;
+ bottom: $semantic-spacing-8; // 2rem
+ right: $semantic-spacing-6; // 1.5rem
+ z-index: $semantic-z-index-toast; // 1080
+ }
+
+ &--bottom-left {
+ position: fixed;
+ bottom: $semantic-spacing-8; // 2rem
+ left: $semantic-spacing-6; // 1.5rem
+ z-index: $semantic-z-index-toast; // 1080
+ }
+
+ &--top-right {
+ position: fixed;
+ top: $semantic-spacing-8; // 2rem
+ right: $semantic-spacing-6; // 1.5rem
+ z-index: $semantic-z-index-toast; // 1080
+ }
+
+ &--top-left {
+ position: fixed;
+ top: $semantic-spacing-8; // 2rem
+ left: $semantic-spacing-6; // 1.5rem
+ z-index: $semantic-z-index-toast; // 1080
+ }
+
+ // States
+ &:active:not(:disabled) {
+ transform: scale(0.92);
+ }
+
+ &:focus-visible {
+ box-shadow:
+ $semantic-shadow-button-focus,
+ $semantic-shadow-card-active;
+ }
+
+ &--disabled {
+ opacity: $semantic-opacity-disabled;
+ cursor: not-allowed;
+ pointer-events: none;
+ box-shadow: $semantic-shadow-elevation-1;
+ }
+
+ &--loading {
+ cursor: wait;
+
+ .fab-icon {
+ opacity: $semantic-opacity-invisible;
+ }
+ }
+
+ // Pulse effect
+ &--pulse {
+ overflow: visible;
+
+ .fab-pulse {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: inherit;
+ border-radius: inherit;
+ animation: fab-pulse 2s infinite;
+ z-index: -1;
+ }
+ }
+
+ @keyframes fab-pulse {
+ 0% {
+ transform: scale(1);
+ opacity: 1;
+ }
+ 100% {
+ transform: scale(1.4);
+ opacity: 0;
+ }
+ }
+
+ // Expand animation
+ &--expanding {
+ animation: fab-expand 300ms cubic-bezier(0, 0, 0.2, 1);
+ }
+
+ @keyframes fab-expand {
+ 0% {
+ transform: scale(1);
+ opacity: 1;
+ }
+ 50% {
+ transform: scale(1.2);
+ opacity: 0.8;
+ }
+ 100% {
+ transform: scale(1);
+ opacity: 1;
+ }
+ }
+
+ // Touch overlay for ripple effect
+ .fab-touch-overlay {
+ position: absolute;
+ background-color: rgba(0, 0, 0, 0);
+ z-index: 1;
+ pointer-events: none;
+ }
+
+ // Icon container
+ .fab-icon {
+ position: relative;
+ z-index: 2;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ line-height: 1;
+ }
+
+ // Loader styles
+ .fab-loader {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 3;
+ }
+
+ .fab-spinner {
+ border: 2px solid rgba(255, 255, 255, 0.3);
+ border-top: 2px solid white;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ }
+
+ @keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+ }
+
+ // Ripple effect
+ &::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: radial-gradient(circle, rgba(255, 255, 255, 0.3) 0%, transparent 70%);
+ transform: scale(0);
+ opacity: 0;
+ pointer-events: none;
+ transition: transform 0.3s ease, opacity 0.3s ease;
+ border-radius: inherit;
+ }
+
+ &:active:not(:disabled)::after {
+ transform: scale(1);
+ opacity: 1;
+ transition: none;
+ }
+
+ // Extended FAB support (for future use)
+ &--extended {
+ border-radius: $semantic-border-radius-pill; // 1.75rem equivalent
+ padding: 0 $semantic-spacing-4; // 1rem
+ width: auto;
+ min-width: $semantic-spacing-20; // 5rem
+ height: $semantic-spacing-14; // 3.5rem
+
+ .fab-icon {
+ margin-right: $semantic-spacing-1-5; // 0.375rem
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/buttons/fab.component.ts b/projects/ui-essentials/src/lib/components/buttons/fab.component.ts
new file mode 100644
index 0000000..a126776
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/buttons/fab.component.ts
@@ -0,0 +1,79 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+export type FabSize = 'small' | 'medium' | 'large';
+export type FabVariant = 'primary' | 'secondary' | 'tertiary';
+export type FabPosition = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | 'static';
+
+@Component({
+ selector: 'ui-fab',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+ @if (loading) {
+
+ } @else {
+
+
+
+ }
+
+ @if (pulse) {
+
+ }
+
+
+
+ `,
+ styleUrl: './fab.component.scss'
+})
+export class FabComponent {
+ @Input() variant: FabVariant = 'primary';
+ @Input() size: FabSize = 'medium';
+ @Input() disabled: boolean = false;
+ @Input() loading: boolean = false;
+ @Input() type: 'button' | 'submit' | 'reset' = 'button';
+ @Input() position: FabPosition = 'static';
+ @Input() pulse: boolean = false;
+ @Input() expandOnClick: boolean = false;
+ @Input() class: string = '';
+
+ @Output() clicked = new EventEmitter();
+
+ get fabClasses(): string {
+ return [
+ 'ui-fab',
+ `ui-fab--${this.variant}`,
+ `ui-fab--${this.size}`,
+ `ui-fab--${this.position}`,
+ this.disabled ? 'ui-fab--disabled' : '',
+ this.loading ? 'ui-fab--loading' : '',
+ this.pulse ? 'ui-fab--pulse' : '',
+ this.expandOnClick ? 'ui-fab--expandable' : '',
+ this.class
+ ].filter(Boolean).join(' ');
+ }
+
+ handleClick(event: Event): void {
+ if (!this.disabled && !this.loading) {
+ this.clicked.emit(event);
+
+ if (this.expandOnClick) {
+ // Add animation class for expand effect
+ (event.target as HTMLElement).classList.add('ui-fab--expanding');
+ setTimeout(() => {
+ (event.target as HTMLElement).classList.remove('ui-fab--expanding');
+ }, 300);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/buttons/ghost-button.component.scss b/projects/ui-essentials/src/lib/components/buttons/ghost-button.component.scss
new file mode 100644
index 0000000..005a721
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/buttons/ghost-button.component.scss
@@ -0,0 +1,141 @@
+@use "../../../../../shared-ui/src/styles/semantic/index" as *;
+.ui-ghost-button {
+ // Reset and base styles
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border: $semantic-border-width-1 solid transparent;
+ background: transparent;
+ cursor: pointer;
+ text-decoration: none;
+ outline: none;
+ position: relative;
+ overflow: hidden;
+ border-radius: $semantic-border-radius-lg;
+
+ // Typography
+ font-family: $semantic-typography-font-family-sans;
+ font-weight: $semantic-typography-font-weight-medium;
+ text-align: center;
+ text-transform: none;
+ letter-spacing: $semantic-typography-letter-spacing-wide;
+ white-space: nowrap;
+ color: $semantic-color-text-secondary;
+
+ // Interaction states
+ user-select: none;
+ -webkit-tap-highlight-color: transparent;
+ box-sizing: border-box;
+
+ // Transitions
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease-out;
+
+ // Size variants
+ &--small {
+ height: $semantic-spacing-9; // 2.25rem
+ padding: 0 $semantic-spacing-2; // 0.5rem
+ font-size: $semantic-typography-font-size-sm;
+ line-height: $semantic-typography-line-height-normal;
+ min-width: $semantic-spacing-16; // 4rem
+ }
+
+ &--medium {
+ height: $semantic-spacing-11; // 2.75rem
+ padding: 0 $semantic-spacing-4; // 1rem
+ font-size: $semantic-typography-font-size-md;
+ line-height: $semantic-typography-line-height-normal;
+ min-width: $semantic-spacing-20; // 5rem
+ }
+
+ &--large {
+ height: $semantic-spacing-12; // 3rem (closest to 3.25rem)
+ padding: 0 $semantic-spacing-6; // 1.5rem
+ font-size: $semantic-typography-font-size-lg;
+ line-height: $semantic-typography-line-height-normal;
+ min-width: $semantic-spacing-24; // 6rem
+ }
+
+ // Hover states
+ &:hover:not(:disabled) {
+ background-color: $semantic-color-surface-container;
+ color: $semantic-color-text-primary;
+ border-color: $semantic-color-border-secondary;
+ transform: $semantic-motion-hover-transform-lift-sm;
+ box-shadow: $semantic-shadow-button-hover;
+ }
+
+ &:active:not(:disabled) {
+ background-color: $semantic-color-surface-high;
+ transform: scale(0.98);
+ box-shadow: $semantic-shadow-none;
+ }
+
+ &:focus-visible {
+ background-color: $semantic-color-surface-container;
+ border-color: $semantic-color-border-focus;
+ box-shadow: $semantic-shadow-button-focus;
+ }
+
+ // States
+ &--disabled {
+ opacity: $semantic-opacity-disabled;
+ cursor: not-allowed;
+ pointer-events: none;
+ }
+
+ &--loading {
+ cursor: wait;
+
+ .ghost-button-content {
+ opacity: $semantic-opacity-invisible;
+ }
+ }
+
+ &--full-width {
+ width: 100%;
+ }
+
+ // Loader styles
+ .ghost-button-loader {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
+
+ .ghost-button-spinner {
+ width: $semantic-spacing-4; // 1rem
+ height: $semantic-spacing-4; // 1rem
+ border: $semantic-border-width-2 solid rgba($semantic-color-brand-primary, 0.3);
+ border-top: $semantic-border-width-2 solid $semantic-color-brand-primary;
+ border-radius: $semantic-border-radius-full;
+ animation: spin 1s $semantic-motion-easing-linear infinite;
+ }
+
+ @keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+ }
+
+ // Ripple effect (basic implementation)
+ &::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: radial-gradient(circle, rgba($semantic-color-brand-primary, 0.08) 0%, transparent 70%);
+ transform: scale(0);
+ opacity: $semantic-opacity-invisible;
+ pointer-events: none;
+ transition: transform $semantic-motion-duration-normal $semantic-motion-easing-ease, opacity $semantic-motion-duration-normal $semantic-motion-easing-ease;
+ border-radius: inherit;
+ }
+
+ &:active:not(:disabled)::after {
+ transform: scale(1);
+ opacity: $semantic-opacity-opaque;
+ transition: none;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/buttons/ghost-button.component.ts b/projects/ui-essentials/src/lib/components/buttons/ghost-button.component.ts
new file mode 100644
index 0000000..9bffdca
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/buttons/ghost-button.component.ts
@@ -0,0 +1,55 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+export type GhostButtonSize = 'small' | 'medium' | 'large';
+
+@Component({
+ selector: 'ui-ghost-button',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+ @if (loading) {
+
+ } @else {
+
+ }
+
+ `,
+ styleUrl: './ghost-button.component.scss'
+})
+export class GhostButtonComponent {
+ @Input() size: GhostButtonSize = 'medium';
+ @Input() disabled: boolean = false;
+ @Input() loading: boolean = false;
+ @Input() type: 'button' | 'submit' | 'reset' = 'button';
+ @Input() fullWidth: boolean = false;
+ @Input() class: string = '';
+
+ @Output() clicked = new EventEmitter();
+
+ get buttonClasses(): string {
+ return [
+ 'ui-ghost-button',
+ `ui-ghost-button--${this.size}`,
+ this.disabled ? 'ui-ghost-button--disabled' : '',
+ this.loading ? 'ui-ghost-button--loading' : '',
+ this.fullWidth ? 'ui-ghost-button--full-width' : '',
+ this.class
+ ].filter(Boolean).join(' ');
+ }
+
+ handleClick(event: Event): void {
+ if (!this.disabled && !this.loading) {
+ this.clicked.emit(event);
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/buttons/index copy.ts b/projects/ui-essentials/src/lib/components/buttons/index copy.ts
new file mode 100644
index 0000000..c640c61
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/buttons/index copy.ts
@@ -0,0 +1,5 @@
+export * from './simple-button.component';
+export * from './button.component';
+export * from './text-button.component';
+export * from './ghost-button.component';
+export * from './fab.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/buttons/index.ts b/projects/ui-essentials/src/lib/components/buttons/index.ts
index 643ee81..6435039 100644
--- a/projects/ui-essentials/src/lib/components/buttons/index.ts
+++ b/projects/ui-essentials/src/lib/components/buttons/index.ts
@@ -1 +1,5 @@
export * from './button.component';
+export * from './text-button.component';
+export * from './ghost-button.component';
+export * from './fab.component';
+export * from './simple-button.component';
diff --git a/projects/ui-essentials/src/lib/components/buttons/simple-button.component.scss b/projects/ui-essentials/src/lib/components/buttons/simple-button.component.scss
new file mode 100644
index 0000000..b8f5bb7
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/buttons/simple-button.component.scss
@@ -0,0 +1,91 @@
+@use "../../../../../shared-ui/src/styles/semantic/index" as *;
+
+// Tokens available globally via main application styles
+
+.ui-simple-button {
+ // Reset and base styles
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border: none;
+ border-radius: $semantic-border-radius-md;
+ cursor: pointer;
+ text-decoration: none;
+ outline: none;
+ position: relative;
+ box-sizing: border-box;
+
+ // Typography
+ font-family: $semantic-typography-font-family-sans;
+ font-weight: $semantic-typography-font-weight-medium;
+ font-size: $semantic-typography-font-size-sm;
+ line-height: $semantic-typography-line-height-normal;
+ text-align: center;
+ white-space: nowrap;
+
+ // Default sizing
+ height: $semantic-spacing-10; // 2.5rem
+ padding: 0 $semantic-spacing-4; // 1rem
+ min-width: $semantic-spacing-20; // 5rem
+
+ // Interaction states
+ user-select: none;
+ -webkit-tap-highlight-color: transparent;
+
+ // Transitions
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease-out;
+
+ // Primary variant
+ &--primary {
+ background-color: $semantic-color-interactive-primary;
+ color: $semantic-color-on-brand-primary;
+
+ &:hover:not(:disabled) {
+ background-color: $semantic-color-brand-primary;
+ transform: translateY(-1px);
+ box-shadow: $semantic-shadow-button-hover;
+ }
+
+ &:active:not(:disabled) {
+ transform: scale(0.98);
+ box-shadow: $semantic-shadow-button-active;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-brand-primary;
+ outline-offset: 2px;
+ }
+ }
+
+ // Secondary variant
+ &--secondary {
+ background-color: $semantic-color-container-primary;
+ color: $semantic-color-on-container-primary;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+
+ &:hover:not(:disabled) {
+ background-color: $semantic-color-container-primary;
+ border-color: $semantic-color-interactive-primary;
+ transform: translateY(-1px);
+ filter: brightness(0.95);
+ }
+
+ &:active:not(:disabled) {
+ background-color: $semantic-color-container-primary;
+ transform: scale(0.98);
+ filter: brightness(0.9);
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-brand-primary;
+ outline-offset: 2px;
+ }
+ }
+
+ // Disabled state
+ &:disabled {
+ opacity: $semantic-opacity-disabled;
+ cursor: not-allowed;
+ pointer-events: none;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/buttons/simple-button.component.ts b/projects/ui-essentials/src/lib/components/buttons/simple-button.component.ts
new file mode 100644
index 0000000..b723f02
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/buttons/simple-button.component.ts
@@ -0,0 +1,41 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+@Component({
+ selector: 'ui-simple-button',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
+
+ `,
+ styleUrl: './simple-button.component.scss'
+})
+export class SimpleButtonComponent {
+ @Input() variant: 'primary' | 'secondary' = 'primary';
+ @Input() disabled: boolean = false;
+ @Input() type: 'button' | 'submit' | 'reset' = 'button';
+
+ @Output() clicked = new EventEmitter();
+
+ get buttonClasses(): string {
+ return [
+ 'ui-simple-button',
+ `ui-simple-button--${this.variant}`,
+ this.disabled ? 'ui-simple-button--disabled' : ''
+ ].filter(Boolean).join(' ');
+ }
+
+ handleClick(event: Event): void {
+ if (!this.disabled) {
+ this.clicked.emit(event);
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/buttons/text-button.component.scss b/projects/ui-essentials/src/lib/components/buttons/text-button.component.scss
new file mode 100644
index 0000000..7052d21
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/buttons/text-button.component.scss
@@ -0,0 +1,132 @@
+@use "../../../../../shared-ui/src/styles/semantic/index" as *;
+.ui-text-button {
+ // Reset and base styles
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ text-decoration: none;
+ outline: none;
+ position: relative;
+ overflow: hidden;
+
+ // Typography
+ font-family: $semantic-typography-font-family-sans;
+ font-weight: $semantic-typography-font-weight-medium;
+ text-align: center;
+ text-transform: none;
+ letter-spacing: $semantic-typography-letter-spacing-wide;
+ white-space: nowrap;
+ color: $semantic-color-interactive-primary;
+
+ // Interaction states
+ user-select: none;
+ -webkit-tap-highlight-color: transparent;
+ box-sizing: border-box;
+ border-radius: $semantic-border-radius-sm;
+
+ // Transitions
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease-out;
+
+ // Size variants
+ &--small {
+ height: $semantic-spacing-8; // 2rem
+ padding: 0 $semantic-spacing-1; // 0.25rem
+ font-size: $semantic-typography-font-size-sm;
+ line-height: $semantic-typography-line-height-normal;
+ min-width: $semantic-spacing-12; // 3rem
+ }
+
+ &--medium {
+ height: $semantic-spacing-10; // 2.5rem
+ padding: 0 $semantic-spacing-2; // 0.5rem
+ font-size: $semantic-typography-font-size-md;
+ line-height: $semantic-typography-line-height-normal;
+ min-width: $semantic-spacing-16; // 4rem
+ }
+
+ &--large {
+ height: $semantic-spacing-12; // 3rem
+ padding: 0 $semantic-spacing-3; // 0.75rem
+ font-size: $semantic-typography-font-size-lg;
+ line-height: $semantic-typography-line-height-normal;
+ min-width: $semantic-spacing-20; // 5rem
+ }
+
+ // Hover states
+ &:hover:not(:disabled) {
+ background-color: rgba($semantic-color-brand-primary, 0.08);
+ color: $semantic-color-brand-primary;
+ }
+
+ &:active:not(:disabled) {
+ background-color: rgba($semantic-color-brand-primary, 0.12);
+ transform: scale(0.98);
+ }
+
+ &:focus-visible {
+ background-color: rgba($semantic-color-brand-primary, 0.08);
+ box-shadow: $semantic-shadow-button-focus;
+ }
+
+ // States
+ &--disabled {
+ opacity: $semantic-opacity-disabled;
+ cursor: not-allowed;
+ pointer-events: none;
+ }
+
+ &--loading {
+ cursor: wait;
+
+ .text-button-content {
+ opacity: $semantic-opacity-invisible;
+ }
+ }
+
+ // Loader styles
+ .text-button-loader {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
+
+ .text-button-spinner {
+ width: $semantic-spacing-3-5; // 0.875rem
+ height: $semantic-spacing-3-5; // 0.875rem
+ border: $semantic-border-width-2 solid rgba($semantic-color-brand-primary, 0.3); // closest to 1.5px
+ border-top: $semantic-border-width-2 solid $semantic-color-brand-primary;
+ border-radius: $semantic-border-radius-full;
+ animation: spin 1s $semantic-motion-easing-linear infinite;
+ }
+
+ @keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+ }
+
+ // Ripple effect (basic implementation)
+ &::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: radial-gradient(circle, rgba($semantic-color-brand-primary, 0.12) 0%, transparent 70%);
+ transform: scale(0);
+ opacity: $semantic-opacity-invisible;
+ pointer-events: none;
+ transition: transform $semantic-motion-duration-normal $semantic-motion-easing-ease, opacity $semantic-motion-duration-normal $semantic-motion-easing-ease;
+ border-radius: inherit;
+ }
+
+ &:active:not(:disabled)::after {
+ transform: scale(1);
+ opacity: $semantic-opacity-opaque;
+ transition: none;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/buttons/text-button.component.ts b/projects/ui-essentials/src/lib/components/buttons/text-button.component.ts
new file mode 100644
index 0000000..03dc103
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/buttons/text-button.component.ts
@@ -0,0 +1,53 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+export type TextButtonSize = 'small' | 'medium' | 'large';
+
+@Component({
+ selector: 'ui-text-button',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+ @if (loading) {
+
+ } @else {
+
+ }
+
+ `,
+ styleUrl: './text-button.component.scss'
+})
+export class TextButtonComponent {
+ @Input() size: TextButtonSize = 'medium';
+ @Input() disabled: boolean = false;
+ @Input() loading: boolean = false;
+ @Input() type: 'button' | 'submit' | 'reset' = 'button';
+ @Input() class: string = '';
+
+ @Output() clicked = new EventEmitter();
+
+ get buttonClasses(): string {
+ return [
+ 'ui-text-button',
+ `ui-text-button--${this.size}`,
+ this.disabled ? 'ui-text-button--disabled' : '',
+ this.loading ? 'ui-text-button--loading' : '',
+ this.class
+ ].filter(Boolean).join(' ');
+ }
+
+ handleClick(event: Event): void {
+ if (!this.disabled && !this.loading) {
+ this.clicked.emit(event);
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/avatar/avatar.component.scss b/projects/ui-essentials/src/lib/components/data-display/avatar/avatar.component.scss
new file mode 100644
index 0000000..c0ebdd0
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/avatar/avatar.component.scss
@@ -0,0 +1,244 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+.skyui-avatar {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ background-color: $semantic-color-surface-secondary;
+ color: $semantic-color-text-secondary;
+ box-sizing: border-box;
+ flex-shrink: 0;
+ user-select: none;
+ cursor: pointer;
+
+ // Size variants based on semantic sizing tokens
+ &--size-xs {
+ width: 24px;
+ height: 24px;
+
+ .skyui-avatar__initials {
+ font-size: calc(24px * 0.4);
+ font-weight: 500;
+ }
+
+ .skyui-avatar__icon {
+ font-size: calc(24px * 0.6);
+ }
+ }
+
+ &--size-sm {
+ width: 32px;
+ height: 32px;
+
+ .skyui-avatar__initials {
+ font-size: calc(32px * 0.4);
+ font-weight: 500;
+ }
+
+ .skyui-avatar__icon {
+ font-size: calc(32px * 0.6);
+ }
+ }
+
+ &--size-md {
+ width: 40px;
+ height: 40px;
+
+ .skyui-avatar__initials {
+ font-size: calc(40px * 0.4);
+ font-weight: 500;
+ }
+
+ .skyui-avatar__icon {
+ font-size: calc(40px * 0.6);
+ }
+ }
+
+ &--size-lg {
+ width: 48px;
+ height: 48px;
+
+ .skyui-avatar__initials {
+ font-size: calc(48px * 0.4);
+ font-weight: 600;
+ }
+
+ .skyui-avatar__icon {
+ font-size: calc(48px * 0.6);
+ }
+ }
+
+ &--size-xl {
+ width: 64px;
+ height: 64px;
+
+ .skyui-avatar__initials {
+ font-size: calc(64px * 0.35);
+ font-weight: 600;
+ }
+
+ .skyui-avatar__icon {
+ font-size: calc(64px * 0.5);
+ }
+ }
+
+ &--size-xxl {
+ width: 80px;
+ height: 80px;
+
+ .skyui-avatar__initials {
+ font-size: calc(80px * 0.35);
+ font-weight: 600;
+ }
+
+ .skyui-avatar__icon {
+ font-size: calc(80px * 0.5);
+ }
+ }
+
+ // Loading state
+ &--loading {
+ background-color: $semantic-color-surface-disabled;
+
+ .skyui-avatar__loading {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .loading-spinner {
+ width: 50%;
+ height: 50%;
+ border: 2px solid $semantic-color-border-subtle;
+ border-top: 2px solid $semantic-color-brand-primary;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ }
+ }
+
+ // Avatar image
+ &__image {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 50%;
+ overflow: hidden;
+ }
+
+ // Fallback container
+ &__fallback {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: $semantic-color-container-primary;
+ color: $semantic-color-on-container-primary;
+ border-radius: 50%;
+ overflow: hidden;
+ }
+
+ // Initials text
+ &__initials {
+ line-height: 1;
+ text-align: center;
+ letter-spacing: -0.02em;
+ }
+
+ // Icon
+ &__icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ // Status indicator
+ &__status {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ width: 25%;
+ height: 25%;
+ min-width: 8px;
+ min-height: 8px;
+ border-radius: 50%;
+ border: 2px solid $semantic-color-surface-primary;
+ box-sizing: border-box;
+
+ &--online {
+ background-color: $semantic-color-success;
+ }
+
+ &--offline {
+ background-color: $semantic-color-text-tertiary;
+ }
+
+ &--away {
+ background-color: $semantic-color-warning;
+ }
+
+ &--busy {
+ background-color: $semantic-color-danger;
+ }
+ }
+
+ // Badge indicator positioning
+ &__badge {
+ position: absolute;
+ top: -4px;
+ right: -4px;
+ }
+}
+
+// Animation for loading spinner
+@keyframes spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+// Focus and hover states for interactive avatars
+.skyui-avatar {
+ &[role="button"]:focus-visible,
+ &[tabindex]:focus-visible {
+ outline: 2px solid $semantic-color-brand-primary;
+ outline-offset: 2px;
+ }
+
+ &[role="button"]:hover,
+ &[tabindex]:hover {
+ transform: scale(1.05);
+ transition: transform $semantic-duration-fast $semantic-easing-standard;
+ }
+}
+
+// High contrast mode support
+@media (prefers-contrast: high) {
+ .skyui-avatar {
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+
+ &__status {
+ border-width: $semantic-border-width-3;
+ }
+ }
+}
+
+// Reduced motion support
+@media (prefers-reduced-motion: reduce) {
+ .skyui-avatar {
+ &[role="button"]:hover,
+ &[tabindex]:hover {
+ transition: none;
+ }
+
+ .loading-spinner {
+ animation: none;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/avatar/avatar.component.ts b/projects/ui-essentials/src/lib/components/data-display/avatar/avatar.component.ts
new file mode 100644
index 0000000..bbca1f4
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/avatar/avatar.component.ts
@@ -0,0 +1,108 @@
+import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { faUser } from '@fortawesome/free-solid-svg-icons';
+import { BadgeComponent } from '../badge/badge.component';
+
+export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
+
+@Component({
+ selector: 'ui-avatar',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [CommonModule, FontAwesomeModule, BadgeComponent],
+ template: `
+
+
+ @if (loading) {
+
+ } @else {
+ @if (imageUrl && !imageError) {
+
+ } @else {
+
+ @if (computedInitials) {
+ {{ computedInitials }}
+ } @else {
+
+
+ }
+
+ }
+ }
+
+ @if (status) {
+
+
+ }
+
+ @if (badge !== undefined && badge !== null) {
+
+ {{ badge }}
+
+ }
+
+ `,
+ styleUrl: './avatar.component.scss'
+})
+export class AvatarComponent {
+ @Input() imageUrl?: string;
+ @Input() name?: string;
+ @Input() initials?: string;
+ @Input() size: AvatarSize = 'md';
+ @Input() altText?: string;
+ @Input() ariaLabel?: string;
+ @Input() loading: boolean = false;
+ @Input() status?: 'online' | 'offline' | 'away' | 'busy';
+ @Input() statusLabel?: string;
+ @Input() badge?: string | number;
+ @Input() badgeLabel?: string;
+
+ faUser = faUser;
+ imageError = false;
+
+ onImageError(): void {
+ this.imageError = true;
+ }
+
+ onImageLoad(): void {
+ this.imageError = false;
+ }
+
+ // Auto-generate initials from name if not provided
+ get computedInitials(): string {
+ if (this.initials) {
+ return this.initials.slice(0, 2).toUpperCase();
+ }
+
+ if (this.name) {
+ const nameParts = this.name.trim().split(/\s+/);
+ if (nameParts.length >= 2) {
+ return (nameParts[0][0] + nameParts[nameParts.length - 1][0]).toUpperCase();
+ } else if (nameParts[0]) {
+ return nameParts[0].slice(0, 2).toUpperCase();
+ }
+ }
+
+ return '';
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/avatar/index.ts b/projects/ui-essentials/src/lib/components/data-display/avatar/index.ts
new file mode 100644
index 0000000..1638b86
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/avatar/index.ts
@@ -0,0 +1 @@
+export * from './avatar.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/badge/badge.component.scss b/projects/ui-essentials/src/lib/components/data-display/badge/badge.component.scss
new file mode 100644
index 0000000..e42d9de
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/badge/badge.component.scss
@@ -0,0 +1,176 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+/**
+ * ==========================================================================
+ * BADGE COMPONENT STYLES
+ * ==========================================================================
+ * Material Design 3 inspired badge component with design token integration.
+ * Supports multiple variants, sizes, shapes, and dot mode for notifications.
+ * ==========================================================================
+ */
+
+
+// Tokens available globally via main application styles
+
+.ui-badge {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: $semantic-typography-font-weight-medium;
+ line-height: 1;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: middle;
+ user-select: none;
+ box-sizing: border-box;
+ transition: all $semantic-duration-short $semantic-easing-standard;
+ background-color: aqua;
+
+ // Default size (md)
+ min-width: $semantic-sizing-icon-navigation;
+ height: $semantic-sizing-icon-navigation;
+ padding: 0 $semantic-spacing-component-xs;
+ font-size: $semantic-typography-font-size-xs;
+ border-radius: $semantic-border-radius-full;
+
+ // Default variant styling
+ background-color: $semantic-color-surface-secondary;
+ color: $semantic-color-text-primary;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+
+ // Size variants
+ &[data-size="xs"] {
+ min-width: $semantic-typography-icon-small-size;
+ height: $semantic-typography-icon-small-size;
+ padding: 0 calc($semantic-spacing-component-xs / 2);
+ font-size: $semantic-typography-font-size-xs;
+ }
+
+ &[data-size="sm"] {
+ min-width: $semantic-sizing-icon-button;
+ height: $semantic-sizing-icon-button;
+ padding: 0 $semantic-spacing-component-xs;
+ font-size: $semantic-typography-font-size-xs;
+ }
+
+ &[data-size="md"] {
+ min-width: $semantic-sizing-icon-navigation;
+ height: $semantic-sizing-icon-navigation;
+ padding: 0 $semantic-spacing-component-xs;
+ font-size: $semantic-typography-font-size-xs;
+ }
+
+ &[data-size="lg"] {
+ min-width: $semantic-sizing-icon-header;
+ height: $semantic-sizing-icon-header;
+ padding: 0 $semantic-spacing-component-sm;
+ font-size: $semantic-typography-font-size-sm;
+ }
+
+ // Shape variants
+ &[data-shape="pill"] {
+ border-radius: $semantic-border-radius-full;
+ }
+
+ &[data-shape="rounded"] {
+ border-radius: $semantic-border-radius-md;
+ }
+
+ &[data-shape="square"] {
+ border-radius: $semantic-border-radius-sm;
+ }
+
+ // Dot mode - small notification dot
+ &[data-dot="true"] {
+ min-width: 0;
+ width: 8px;
+ height: 8px;
+ padding: 0;
+ border-radius: 50%;
+ border: 2px solid $semantic-color-surface-primary;
+
+ &[data-size="xs"] {
+ width: 6px;
+ height: 6px;
+ }
+
+ &[data-size="sm"] {
+ width: 8px;
+ height: 8px;
+ }
+
+ &[data-size="md"] {
+ width: 10px;
+ height: 10px;
+ }
+
+ &[data-size="lg"] {
+ width: 12px;
+ height: 12px;
+ }
+ }
+
+ // Color variants
+ &[data-variant="default"] {
+ background-color: $semantic-color-surface-secondary;
+ color: $semantic-color-text-primary;
+ border-color: $semantic-color-border-subtle;
+ }
+
+ &[data-variant="primary"] {
+ background-color: $semantic-color-brand-primary;
+ color: $semantic-color-text-inverse;
+ border-color: $semantic-color-brand-primary;
+ }
+
+ &[data-variant="secondary"] {
+ background-color: $semantic-color-brand-secondary;
+ color: $semantic-color-text-inverse;
+ border-color: $semantic-color-brand-secondary;
+ }
+
+ &[data-variant="success"] {
+ background-color: $semantic-color-success;
+ color: $semantic-color-text-inverse;
+ border-color: $semantic-color-success;
+ }
+
+ &[data-variant="warning"] {
+ background-color: $semantic-color-warning;
+ color: $semantic-color-text-inverse;
+ border-color: $semantic-color-warning;
+ }
+
+ &[data-variant="danger"] {
+ background-color: $semantic-color-danger;
+ color: $semantic-color-text-inverse;
+ border-color: $semantic-color-danger;
+ }
+
+ &[data-variant="info"] {
+ background-color: $semantic-color-info;
+ color: $semantic-color-text-inverse;
+ border-color: $semantic-color-info;
+ }
+
+ // Hover states for interactive badges
+ &:hover {
+ transform: translateY(-1px);
+ box-shadow: $semantic-shadow-elevation-1;
+ }
+
+ // Focus states for accessibility
+ &:focus {
+ outline: 2px solid $semantic-color-focus-ring;
+ outline-offset: 2px;
+ }
+
+ // Responsive adjustments
+ @media (max-width: calc($semantic-breakpoint-sm - 1px)) {
+ &[data-size="lg"] {
+ min-width: $semantic-sizing-icon-navigation;
+ height: $semantic-sizing-icon-navigation;
+ padding: 0 $semantic-spacing-component-xs;
+ font-size: $semantic-typography-font-size-xs;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/badge/badge.component.ts b/projects/ui-essentials/src/lib/components/data-display/badge/badge.component.ts
new file mode 100644
index 0000000..99952aa
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/badge/badge.component.ts
@@ -0,0 +1,37 @@
+import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+export type BadgeVariant = 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info';
+export type BadgeSize = 'xs' | 'sm' | 'md' | 'lg';
+export type BadgeShape = 'pill' | 'rounded' | 'square';
+
+@Component({
+ selector: 'ui-badge',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+ @if (!isDot) {
+
+ }
+
+ `,
+ styleUrls: ['./badge.component.scss']
+})
+export class BadgeComponent {
+ @Input() variant: BadgeVariant = 'default';
+ @Input() size: BadgeSize = 'md';
+ @Input() shape: BadgeShape = 'pill';
+ @Input() isDot: boolean = false;
+ @Input() ariaLabel?: string;
+ @Input() title?: string;
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/badge/index.ts b/projects/ui-essentials/src/lib/components/data-display/badge/index.ts
new file mode 100644
index 0000000..216ebbb
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/badge/index.ts
@@ -0,0 +1 @@
+export * from './badge.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
new file mode 100644
index 0000000..8f6da03
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/card/card.component.scss
@@ -0,0 +1,430 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+// Tokens available globally via main application styles
+
+.ui-card {
+ position: relative;
+ width: 100%;
+ box-sizing: border-box;
+ margin-right: $semantic-spacing-grid-gap-xs;
+ margin-bottom: $semantic-spacing-grid-gap-xs;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+
+ // Base styles
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+ transition: all $semantic-duration-fast $semantic-easing-standard;
+
+ // Size variants - following 8px grid increments
+ &--sm {
+ min-height: $semantic-sizing-card-height-sm;
+
+ .card-content-layer {
+ padding: $semantic-spacing-component-padding-sm;
+ }
+
+ .card-header {
+ padding-bottom: $semantic-spacing-component-sm;
+ }
+
+ .card-footer {
+ padding-top: $semantic-spacing-component-sm;
+ }
+ }
+
+ &--md {
+ min-height: $semantic-sizing-card-height-md;
+
+ .card-content-layer {
+ padding: $semantic-spacing-component-padding-lg;
+ }
+
+ .card-header {
+ padding-bottom: $semantic-spacing-component-md;
+ }
+
+ .card-footer {
+ padding-top: $semantic-spacing-component-md;
+ }
+ }
+
+ &--lg {
+ min-height: $semantic-sizing-card-height-lg;
+
+ .card-content-layer {
+ padding: $semantic-spacing-component-padding-xl;
+ }
+
+ .card-header {
+ padding-bottom: $semantic-spacing-component-lg;
+ }
+
+ .card-footer {
+ padding-top: $semantic-spacing-component-lg;
+ }
+ }
+
+ // Elevation variants - Material Design shadow levels
+ &--elevation-none {
+ box-shadow: none;
+ }
+
+ &--elevation-sm {
+ box-shadow: $semantic-shadow-elevation-1;
+ }
+
+ &--elevation-md {
+ box-shadow: $semantic-shadow-elevation-2;
+ }
+
+ &--elevation-lg {
+ box-shadow: $semantic-shadow-elevation-3;
+ }
+
+ &--elevation-xl {
+ box-shadow: $semantic-shadow-elevation-4;
+ }
+
+ // Radius variants - consistent scale
+ &--radius-none {
+ border-radius: 0;
+ }
+
+ &--radius-sm {
+ border-radius: $semantic-border-radius-sm;
+ }
+
+ &--radius-md {
+ border-radius: $semantic-border-radius-md;
+ }
+
+ &--radius-lg {
+ border-radius: $semantic-border-radius-lg;
+ }
+
+ &--radius-full {
+ border-radius: $semantic-border-radius-full;
+ aspect-ratio: 1;
+ }
+
+ // Variant styles - Material Design 3 inspired colors
+ &--elevated {
+ background-color: $semantic-color-surface-primary;
+ border: none;
+
+ &:hover:not(.ui-card--disabled) {
+ transform: translateY(-2px);
+ box-shadow: $semantic-shadow-card-hover;
+ }
+ }
+
+ &--filled {
+ background-color: $semantic-color-container-primary;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+
+ &:hover:not(.ui-card--disabled) {
+ background-color: $semantic-color-surface-interactive;
+ }
+ }
+
+ &--outlined {
+ background-color: transparent;
+ border: $semantic-border-width-1 solid $semantic-color-border-secondary;
+
+ &:hover:not(.ui-card--disabled) {
+ border-color: $semantic-color-border-focus;
+ background-color: $semantic-color-surface-elevated;
+ }
+ }
+
+ // Clickable state
+ &--clickable {
+ cursor: pointer;
+ user-select: none;
+
+ &:active:not(.ui-card--disabled) {
+ transform: scale(0.98); // Subtle press effect
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-border-focus;
+ outline-offset: 2px;
+ }
+ }
+
+ // Disabled state
+ &--disabled {
+ opacity: 0.38; // Material Design disabled opacity
+ cursor: not-allowed;
+ pointer-events: none;
+ }
+
+ // Glass effect using CSS variables and semantic approach
+ &--glass {
+ position: relative;
+ isolation: isolate;
+ backdrop-filter: blur(var(--glass-blur-md, 8px));
+ -webkit-backdrop-filter: blur(var(--glass-blur-md, 8px));
+ border: 1px solid var(--glass-border-color, rgba(255, 255, 255, 0.2));
+ transition: all $semantic-duration-short $semantic-easing-standard;
+
+ .card-content-layer {
+ position: relative;
+ z-index: 2;
+ }
+
+ // Glass variant styles using CSS variables - fixed syntax
+ &.ui-card--glass-translucent {
+ background: rgba(var(--glass-background-base, 255, 255, 255), var(--glass-opacity-translucent, 0.1));
+ backdrop-filter: blur(var(--glass-blur-sm, 4px));
+ -webkit-backdrop-filter: blur(var(--glass-blur-sm, 4px));
+ }
+
+ &.ui-card--glass-light {
+ background: rgba(var(--glass-background-base, 255, 255, 255), var(--glass-opacity-light, 0.3));
+ backdrop-filter: blur(var(--glass-blur-md, 8px));
+ -webkit-backdrop-filter: blur(var(--glass-blur-md, 8px));
+ }
+
+ &.ui-card--glass-medium {
+ background: rgba(var(--glass-background-base, 255, 255, 255), var(--glass-opacity-medium, 0.5));
+ backdrop-filter: blur(var(--glass-blur-md, 8px));
+ -webkit-backdrop-filter: blur(var(--glass-blur-md, 8px));
+ }
+
+ &.ui-card--glass-heavy {
+ background: rgba(var(--glass-background-base, 255, 255, 255), var(--glass-opacity-heavy, 0.7));
+ backdrop-filter: blur(var(--glass-blur-lg, 16px));
+ -webkit-backdrop-filter: blur(var(--glass-blur-lg, 16px));
+ }
+
+ &.ui-card--glass-frosted {
+ background: rgba(var(--glass-background-base, 255, 255, 255), var(--glass-opacity-frosted, 0.85));
+ backdrop-filter: blur(var(--glass-blur-xl, 24px));
+ -webkit-backdrop-filter: blur(var(--glass-blur-xl, 24px));
+ }
+
+ // Interactive glass states
+ &.ui-card--clickable:hover:not(.ui-card--disabled) {
+ transform: translateY(-2px);
+ box-shadow: $semantic-shadow-elevation-3;
+ }
+
+ &.ui-card--clickable:active:not(.ui-card--disabled) {
+ transform: scale(0.98);
+ }
+
+ // Disabled state for glass cards
+ &.ui-card--disabled {
+ backdrop-filter: blur(var(--glass-blur-xs, 2px));
+ -webkit-backdrop-filter: blur(var(--glass-blur-xs, 2px));
+ opacity: 0.6;
+ }
+ }
+
+ // With background layer
+ &--with-background {
+ .card-background-layer {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ box-sizing: border-box;
+ transform: translateZ(0);
+ z-index: 0;
+ }
+
+ .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;
+ }
+ }
+ }
+}
+
+// Card content structure
+.card-content-layer {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ flex: 1;
+}
+
+.card-header {
+ border-bottom: $semantic-border-width-1 solid $semantic-color-border-secondary;
+
+ h1, h2, h3, h4, h5, h6 {
+ margin: 0;
+ color: $semantic-color-text-primary;
+ }
+
+ p {
+ margin: 0;
+ color: $semantic-color-text-secondary;
+ }
+}
+
+.card-body {
+ flex: 1;
+
+ // Typography reset
+ > *:first-child {
+ margin-top: 0;
+ }
+
+ > *:last-child {
+ margin-bottom: 0;
+ }
+}
+
+.card-footer {
+ border-top: $semantic-border-width-1 solid $semantic-color-border-secondary;
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ gap: $semantic-spacing-component-sm;
+}
+
+// Glass overlay for enhanced glass effect
+.card-glass-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(145deg,
+ rgba(255, 255, 255, 0.1) 0%,
+ rgba(255, 255, 255, 0.05) 50%,
+ rgba(255, 255, 255, 0.1) 100%);
+ pointer-events: none;
+ z-index: 1;
+ border-radius: inherit;
+}
+
+// Ripple effect for clickable cards
+.card-ripple-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ overflow: hidden;
+ pointer-events: none;
+ z-index: 3;
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 0;
+ height: 0;
+ border-radius: 50%;
+ background: rgba(120, 53, 255, 0.2); // Brand color ripple
+ transform: translate(-50%, -50%) scale(0);
+ opacity: 0;
+ transition: transform 0.3s ease, opacity 0.3s ease;
+ }
+
+ .ui-card--clickable:active & {
+ &::after {
+ transform: translate(-50%, -50%) scale(2);
+ opacity: 1;
+ transition: none;
+ }
+ }
+}
+
+// Hover effects based on elevation
+.ui-card:hover:not(.ui-card--disabled) {
+ &.ui-card--elevation-sm {
+ box-shadow: $semantic-shadow-elevation-2;
+ }
+
+ &.ui-card--elevation-md {
+ box-shadow: $semantic-shadow-elevation-3;
+ }
+
+ &.ui-card--elevation-lg {
+ box-shadow: $semantic-shadow-elevation-4;
+ }
+}
+
+// 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;
+ }
+ }
+}
+
+// Responsive adaptations
+@media (max-width: $semantic-sizing-breakpoint-tablet) {
+ .ui-card {
+ margin-right: 0;
+
+ &--lg {
+ .card-content-layer {
+ padding: $semantic-spacing-component-padding-lg;
+ }
+ }
+
+ .card-footer {
+ flex-direction: column;
+ gap: $semantic-spacing-component-md;
+ }
+ }
+}
+
+// Accessibility
+@media (prefers-reduced-motion: reduce) {
+ .ui-card {
+ transition: none;
+
+ &:hover,
+ &:active {
+ transform: none;
+ }
+ }
+
+ .card-ripple-overlay::after {
+ transition: none;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/card/card.component.ts b/projects/ui-essentials/src/lib/components/data-display/card/card.component.ts
new file mode 100644
index 0000000..f5f50e6
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/card/card.component.ts
@@ -0,0 +1,99 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+export type CardVariant = 'elevated' | 'filled' | 'outlined';
+export type CardSize = 'sm' | 'md' | 'lg';
+export type CardElevation = 'none' | 'sm' | 'md' | 'lg' | 'xl';
+export type CardRadius = 'none' | 'sm' | 'md' | 'lg' | 'full';
+export type GlassVariant = 'translucent' | 'light' | 'medium' | 'heavy' | 'frosted';
+
+@Component({
+ selector: 'ui-card',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+ @if (hasBackgroundLayer) {
+
+
+
+ }
+
+
+ @if (hasHeader) {
+
+ }
+
+
+
+
+
+ @if (hasFooter) {
+
+ }
+
+
+ @if (glass) {
+
+ }
+
+ @if (clickable && !disabled) {
+
+ }
+
+ `,
+ styleUrl: './card.component.scss'
+})
+export class CardComponent {
+ @Input() variant: CardVariant = 'elevated';
+ @Input() size: CardSize = 'md';
+ @Input() elevation: CardElevation = 'sm';
+ @Input() radius: CardRadius = 'md';
+ @Input() disabled: boolean = false;
+ @Input() clickable: boolean = false;
+ @Input() glass: boolean = false;
+ @Input() glassVariant: GlassVariant = 'medium';
+ @Input() backgroundImage?: string;
+ @Input() hasBackgroundLayer: boolean = false;
+ @Input() hasHeader: boolean = false;
+ @Input() hasFooter: boolean = false;
+ @Input() class: string = '';
+
+ @Output() cardClick = new EventEmitter();
+
+ get cardClasses(): string {
+ return [
+ 'ui-card',
+ `ui-card--${this.variant}`,
+ `ui-card--${this.size}`,
+ `ui-card--elevation-${this.elevation}`,
+ `ui-card--radius-${this.radius}`,
+ this.disabled ? 'ui-card--disabled' : '',
+ this.clickable ? 'ui-card--clickable' : '',
+ this.glass ? 'ui-card--glass' : '',
+ this.glass ? `ui-card--glass-${this.glassVariant}` : '',
+ this.hasBackgroundLayer ? 'ui-card--with-background' : '',
+ this.class
+ ].filter(Boolean).join(' ');
+ }
+
+ handleClick(event: Event): void {
+ if (this.clickable && !this.disabled) {
+ this.cardClick.emit(event);
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/card/index.ts b/projects/ui-essentials/src/lib/components/data-display/card/index.ts
new file mode 100644
index 0000000..0904481
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/card/index.ts
@@ -0,0 +1 @@
+export * from './card.component';
\ No newline at end of file
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
new file mode 100644
index 0000000..f0f72a4
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/carousel/carousel.component.scss
@@ -0,0 +1,181 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+@mixin font-properties($font-map) {
+ @if type-of($font-map) == 'map' {
+ font-family: map-get($font-map, 'font-family');
+ font-size: map-get($font-map, 'font-size');
+ line-height: map-get($font-map, 'line-height');
+ font-weight: map-get($font-map, 'font-weight');
+ letter-spacing: map-get($font-map, 'letter-spacing');
+ } @else {
+ font: $font-map;
+ }
+}
+
+.carousel {
+ position: relative;
+ width: 100%;
+ overflow: hidden;
+ border-radius: 12px;
+ background: $semantic-color-surface-container;
+
+ .carousel-container {
+ position: relative;
+ width: 100%;
+ height: 400px; // Default height
+ overflow: hidden;
+ }
+
+ .carousel-track {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ transition: transform $semantic-duration-medium $semantic-easing-standard;
+ }
+
+ .carousel-slide {
+ flex: 0 0 100%;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &.multiple-items {
+ flex: 0 0 calc(100% / var(--items-per-view, 1));
+ }
+
+ img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+
+ .slide-content {
+ padding: $semantic-spacing-component-lg;
+ text-align: center;
+ color: $semantic-color-text-primary;
+
+ h3 {
+ @include font-properties($semantic-typography-heading-h3);
+ margin-bottom: $semantic-spacing-component-md;
+ }
+
+ p {
+ @include font-properties($semantic-typography-body-medium);
+ color: $semantic-color-text-secondary;
+ }
+ }
+ }
+
+ .carousel-controls {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ z-index: 2;
+
+ 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;
+ }
+ }
+
+ &.prev {
+ left: $semantic-spacing-component-md;
+ }
+
+ &.next {
+ right: $semantic-spacing-component-md;
+ }
+ }
+
+ .carousel-indicators {
+ position: absolute;
+ bottom: $semantic-spacing-component-md;
+ left: 50%;
+ transform: translateX(-50%);
+ display: flex;
+ gap: $semantic-spacing-component-xs;
+ z-index: 2;
+
+ button {
+ 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);
+ }
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.8);
+ }
+
+ &:focus {
+ outline: 2px solid $semantic-color-primary;
+ outline-offset: 2px;
+ }
+ }
+ }
+
+ // Responsive design
+ @media (max-width: 768px) {
+ .carousel-container {
+ height: 300px;
+ }
+
+ .carousel-controls button {
+ width: 40px;
+ height: 40px;
+
+ svg {
+ width: 16px;
+ height: 16px;
+ }
+ }
+
+ .carousel-slide .slide-content {
+ 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
new file mode 100644
index 0000000..de61493
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/carousel/carousel.component.ts
@@ -0,0 +1,252 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, signal, computed } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+export type CarouselSize = 'sm' | 'md' | 'lg';
+export type CarouselVariant = 'card' | 'image' | 'content';
+export type CarouselIndicatorStyle = 'dots' | 'bars' | 'thumbnails' | 'none';
+export type CarouselTransition = 'slide' | 'fade' | 'scale';
+
+export interface CarouselItem {
+ id: string | number;
+ imageUrl?: string;
+ title?: string;
+ subtitle?: string;
+ content?: string;
+ thumbnail?: string;
+}
+
+@Component({
+ selector: 'ui-carousel',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ template: `
+
+
+
+ @if (showNavigation && items.length > 1) {
+
+
+
+
+
+
+
+
+
+
+
+ }
+
+
+
+
+ @for (item of items; track item.id; let i = $index) {
+
+
+
+ @switch (variant) {
+ @case ('image') {
+ @if (item.imageUrl) {
+
+
+ @if (item.title || item.subtitle) {
+
+ @if (item.title) {
+
{{ item.title }}
+ }
+ @if (item.subtitle) {
+
{{ item.subtitle }}
+ }
+
+ }
+
+ }
+ }
+
+ @case ('card') {
+
+ @if (item.imageUrl) {
+
+ }
+
+ @if (item.title) {
+
{{ item.title }}
+ }
+ @if (item.subtitle) {
+
{{ item.subtitle }}
+ }
+ @if (item.content) {
+
{{ item.content }}
+ }
+
+
+ }
+
+ @case ('content') {
+
+ @if (item.title) {
+
{{ item.title }}
+ }
+ @if (item.subtitle) {
+
{{ item.subtitle }}
+ }
+ @if (item.content) {
+
+ }
+
+ }
+ }
+
+ }
+
+
+
+
+ @if (showIndicators && indicatorStyle !== 'none' && items.length > 1) {
+
+ @for (item of items; track item.id; let i = $index) {
+
+ @switch (indicatorStyle) {
+ @case ('thumbnails') {
+ @if (item.thumbnail || item.imageUrl) {
+
+ }
+ }
+ @case ('dots') {
+
+ }
+ @case ('bars') {
+
+ }
+ }
+
+ }
+
+ }
+
+ `,
+ styleUrl: './carousel.component.scss'
+})
+export class CarouselComponent {
+ @Input() items: CarouselItem[] = [];
+ @Input() size: CarouselSize = 'md';
+ @Input() variant: CarouselVariant = 'image';
+ @Input() transition: CarouselTransition = 'slide';
+ @Input() indicatorStyle: CarouselIndicatorStyle = 'dots';
+ @Input() disabled = false;
+ @Input() autoplay = false;
+ @Input() autoplayInterval = 3000;
+ @Input() loop = true;
+ @Input() showNavigation = true;
+ @Input() showIndicators = true;
+ @Input() ariaLabel = 'Image carousel';
+ @Input() initialIndex = 0;
+
+ @Output() slideChange = new EventEmitter();
+ @Output() itemClick = new EventEmitter<{item: CarouselItem, index: number}>();
+
+ private _currentIndex = signal(this.initialIndex);
+ private _autoplayTimer?: number;
+
+ currentIndex = this._currentIndex.asReadonly();
+
+ ngOnInit(): void {
+ this._currentIndex.set(this.initialIndex);
+ if (this.autoplay && !this.disabled) {
+ this.startAutoplay();
+ }
+ }
+
+ ngOnDestroy(): void {
+ this.stopAutoplay();
+ }
+
+ goToSlide(index: number): void {
+ if (this.disabled || index < 0 || index >= this.items.length) {
+ return;
+ }
+
+ this._currentIndex.set(index);
+ this.slideChange.emit(index);
+ this.resetAutoplay();
+ }
+
+ goToNext(): void {
+ const nextIndex = this.loop && this.currentIndex() === this.items.length - 1
+ ? 0
+ : Math.min(this.currentIndex() + 1, this.items.length - 1);
+ this.goToSlide(nextIndex);
+ }
+
+ goToPrevious(): void {
+ const prevIndex = this.loop && this.currentIndex() === 0
+ ? this.items.length - 1
+ : Math.max(this.currentIndex() - 1, 0);
+ this.goToSlide(prevIndex);
+ }
+
+ private startAutoplay(): void {
+ this.stopAutoplay();
+ this._autoplayTimer = window.setInterval(() => {
+ this.goToNext();
+ }, this.autoplayInterval);
+ }
+
+ private stopAutoplay(): void {
+ if (this._autoplayTimer) {
+ clearInterval(this._autoplayTimer);
+ this._autoplayTimer = undefined;
+ }
+ }
+
+ private resetAutoplay(): void {
+ if (this.autoplay && !this.disabled) {
+ this.stopAutoplay();
+ this.startAutoplay();
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/carousel/index.ts b/projects/ui-essentials/src/lib/components/data-display/carousel/index.ts
new file mode 100644
index 0000000..7a3d383
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/carousel/index.ts
@@ -0,0 +1 @@
+export * from './carousel.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/chip/chip.component.scss b/projects/ui-essentials/src/lib/components/data-display/chip/chip.component.scss
new file mode 100644
index 0000000..6b8f7df
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/chip/chip.component.scss
@@ -0,0 +1,451 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+.ui-chip {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ position: relative;
+ vertical-align: top;
+ white-space: nowrap;
+ overflow: hidden;
+ margin-right: 0.75rem;
+ margin-bottom: 0.5rem;
+
+ // Prevent text selection
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+
+ // Base typography
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+ font-weight: 500;
+ text-decoration: none;
+ outline: none;
+
+ // Transitions
+ transition: all $semantic-duration-fast $semantic-easing-standard;
+
+ // Size variants
+ &--sm {
+ height: 24px;
+ font-size: 0.75rem;
+ line-height: 1;
+
+ .chip-label {
+ padding: 0 0.5rem;
+ }
+
+ .chip-leading-icon,
+ .chip-trailing-icon {
+ width: 18px;
+ height: 18px;
+ font-size: 0.75rem;
+ margin: 0 0.25rem;
+ }
+
+ .chip-avatar {
+ width: 18px;
+ height: 18px;
+ margin: 0 0.25rem 0 0.125rem;
+
+ img {
+ width: 18px;
+ height: 18px;
+ border-radius: 9px;
+ }
+ }
+
+ &.ui-chip--with-leading-icon .chip-label,
+ &.ui-chip--with-avatar .chip-label {
+ padding-left: 0.25rem;
+ }
+
+ &.ui-chip--with-trailing-icon .chip-label,
+ &.ui-chip--removable .chip-label {
+ padding-right: 0.25rem;
+ }
+ }
+
+ &--md {
+ height: 32px;
+ font-size: 0.875rem;
+ line-height: 1;
+
+ .chip-label {
+ padding: 0 0.75rem;
+ }
+
+ .chip-leading-icon,
+ .chip-trailing-icon {
+ width: 24px;
+ height: 24px;
+ font-size: 1rem;
+ margin: 0 0.25rem;
+ }
+
+ .chip-avatar {
+ width: 24px;
+ height: 24px;
+ margin: 0 0.25rem 0 0.125rem;
+
+ img {
+ width: 24px;
+ height: 24px;
+ border-radius: 12px;
+ }
+ }
+
+ &.ui-chip--with-leading-icon .chip-label,
+ &.ui-chip--with-avatar .chip-label {
+ padding-left: 0.5rem;
+ }
+
+ &.ui-chip--with-trailing-icon .chip-label,
+ &.ui-chip--removable .chip-label {
+ padding-right: 0.5rem;
+ }
+ }
+
+ &--lg {
+ height: 40px;
+ font-size: 1rem;
+ line-height: 1;
+
+ .chip-label {
+ padding: 0 1rem;
+ }
+
+ .chip-leading-icon,
+ .chip-trailing-icon {
+ width: 28px;
+ height: 28px;
+ font-size: 1.125rem;
+ margin: 0 0.375rem;
+ }
+
+ .chip-avatar {
+ width: 32px;
+ height: 32px;
+ margin: 0 0.375rem 0 0.25rem;
+
+ img {
+ width: 32px;
+ height: 32px;
+ border-radius: 16px;
+ }
+ }
+
+ &.ui-chip--with-leading-icon .chip-label,
+ &.ui-chip--with-avatar .chip-label {
+ padding-left: 0.5rem;
+ }
+
+ &.ui-chip--with-trailing-icon .chip-label,
+ &.ui-chip--removable .chip-label {
+ padding-right: 0.5rem;
+ }
+ }
+
+ // Radius variants
+ &--radius-none {
+ border-radius: 0;
+ }
+
+ &--radius-sm {
+ border-radius: 0.25rem;
+ }
+
+ &--radius-md {
+ border-radius: 0.375rem;
+ }
+
+ &--radius-lg {
+ border-radius: 0.5rem;
+ }
+
+ &--radius-capsule {
+ border-radius: 50px;
+ }
+
+ // Variant styles
+ &--filled {
+ background-color: $semantic-color-surface-elevated;
+ color: $semantic-color-text-primary;
+ border: 1px solid transparent;
+
+ &:hover:not(.ui-chip--disabled) {
+ background-color: $semantic-color-surface-selected;
+ transform: translateY(-1px);
+ }
+
+ &:active:not(.ui-chip--disabled) {
+ transform: scale(0.95);
+ }
+
+ &.ui-chip--selected {
+ background-color: $semantic-color-primary;
+ color: $semantic-color-text-inverse;
+
+ &:hover:not(.ui-chip--disabled) {
+ background-color: $semantic-color-primary-hover;
+ }
+ }
+ }
+
+ &--outlined {
+ background-color: transparent;
+ color: $semantic-color-primary;
+ border: 1px solid $semantic-color-border-primary;
+
+ &:hover:not(.ui-chip--disabled) {
+ background-color: $semantic-color-surface-elevated;
+ border-color: $semantic-color-primary;
+ transform: translateY(-1px);
+ }
+
+ &:active:not(.ui-chip--disabled) {
+ background-color: $semantic-color-surface-primary;
+ transform: scale(0.95);
+ }
+
+ &.ui-chip--selected {
+ background-color: $semantic-color-surface-primary;
+ border-color: $semantic-color-primary;
+
+ &:hover:not(.ui-chip--disabled) {
+ background-color: $semantic-color-surface-elevated;
+ }
+ }
+ }
+
+ &--elevated {
+ background-color: $semantic-color-surface-primary;
+ color: $semantic-color-text-primary;
+ border: none;
+ box-shadow: $semantic-shadow-elevation-1;
+
+ &:hover:not(.ui-chip--disabled) {
+ box-shadow: $semantic-shadow-elevation-2;
+ transform: translateY(-1px);
+ }
+
+ &:active:not(.ui-chip--disabled) {
+ box-shadow: $semantic-shadow-elevation-1;
+ transform: scale(0.95);
+ }
+
+ &.ui-chip--selected {
+ background-color: $semantic-color-primary;
+ color: $semantic-color-text-inverse;
+ box-shadow: $semantic-shadow-elevation-2;
+
+ &:hover:not(.ui-chip--disabled) {
+ background-color: $semantic-color-primary-hover;
+ box-shadow: $semantic-shadow-elevation-3;
+ }
+ }
+ }
+
+ // States
+ &--clickable {
+ cursor: pointer;
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-border-focus;
+ outline-offset: 2px;
+ }
+ }
+
+ &--disabled {
+ opacity: 0.38;
+ cursor: not-allowed;
+ pointer-events: none;
+ }
+
+ &--selected {
+ // Selected styles are handled in variant sections above
+ }
+
+ // Icon and avatar styles
+ .chip-leading-icon,
+ .chip-trailing-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ }
+
+ .chip-trailing-icon {
+ cursor: pointer;
+ border-radius: 50%;
+ transition: background-color $semantic-duration-fast ease;
+
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.1);
+ }
+
+ &:focus-visible {
+ outline: 1px solid $semantic-color-border-focus;
+ outline-offset: 1px;
+ }
+ }
+
+ .chip-avatar {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ overflow: hidden;
+
+ img {
+ object-fit: cover;
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ }
+ }
+
+ .chip-label {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex: 1;
+ min-width: 0;
+ text-align: center;
+ }
+}
+
+// Dark mode support
+@media (prefers-color-scheme: dark) {
+ .ui-chip {
+ &--filled {
+ background-color: hsl(279, 14%, 25%);
+ color: hsl(0, 0%, 90%);
+
+ &:hover:not(.ui-chip--disabled) {
+ background-color: hsl(279, 14%, 30%);
+ }
+
+ &.ui-chip--selected {
+ background-color: hsl(258, 100%, 47%);
+ color: white;
+
+ &:hover:not(.ui-chip--disabled) {
+ background-color: hsl(258, 100%, 52%);
+ }
+ }
+ }
+
+ &--outlined {
+ background-color: transparent;
+ color: hsl(258, 100%, 67%);
+ border-color: hsl(279, 14%, 45%);
+
+ &:hover:not(.ui-chip--disabled) {
+ background-color: hsl(279, 14%, 15%);
+ border-color: hsl(258, 100%, 67%);
+ }
+
+ &:active:not(.ui-chip--disabled) {
+ background-color: hsl(279, 14%, 20%);
+ }
+
+ &.ui-chip--selected {
+ background-color: hsl(279, 14%, 20%);
+ border-color: hsl(258, 100%, 67%);
+
+ &:hover:not(.ui-chip--disabled) {
+ background-color: hsl(279, 14%, 25%);
+ }
+ }
+ }
+
+ &--elevated {
+ background-color: hsl(279, 14%, 15%);
+ color: hsl(0, 0%, 90%);
+
+ &:hover:not(.ui-chip--disabled) {
+ background-color: hsl(279, 14%, 20%);
+ }
+
+ &.ui-chip--selected {
+ background-color: hsl(258, 100%, 47%);
+ color: white;
+
+ &:hover:not(.ui-chip--disabled) {
+ background-color: hsl(258, 100%, 52%);
+ }
+ }
+ }
+
+ .chip-trailing-icon:hover {
+ background-color: rgba(255, 255, 255, 0.1);
+ }
+
+ .chip-avatar img {
+ border-color: rgba(255, 255, 255, 0.2);
+ }
+ }
+}
+
+// Accessibility
+@media (prefers-reduced-motion: reduce) {
+ .ui-chip {
+ transition: none;
+
+ &:hover,
+ &:active {
+ transform: none;
+ }
+ }
+}
+
+// Chip group/set container utility
+.ui-chip-set {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ align-items: flex-start;
+
+ &--row {
+ flex-wrap: nowrap;
+ overflow-x: auto;
+ scroll-padding-left: 1rem;
+ scroll-snap-type: x mandatory;
+
+ .ui-chip {
+ scroll-snap-align: center;
+ flex-shrink: 0;
+ }
+
+ &::-webkit-scrollbar {
+ height: 0;
+ background: transparent;
+ }
+
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+ }
+
+ &--no-scroll {
+ overflow-x: visible;
+ }
+}
+
+// Responsive
+@media (max-width: 768px) {
+ .ui-chip {
+ margin-right: 0.5rem;
+ margin-bottom: 0.375rem;
+
+ &--lg {
+ height: 36px;
+ font-size: 0.875rem;
+
+ .chip-label {
+ padding: 0 0.75rem;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/chip/chip.component.ts b/projects/ui-essentials/src/lib/components/data-display/chip/chip.component.ts
new file mode 100644
index 0000000..c6930bd
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/chip/chip.component.ts
@@ -0,0 +1,105 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
+
+export type ChipVariant = 'filled' | 'outlined' | 'elevated';
+export type ChipSize = 'sm' | 'md' | 'lg';
+export type ChipRadius = 'none' | 'sm' | 'md' | 'lg' | 'capsule';
+
+@Component({
+ selector: 'ui-chip',
+ standalone: true,
+ imports: [CommonModule, FontAwesomeModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+ @if (leadingIcon) {
+
+
+
+ }
+
+ @if (avatar) {
+
+
+
+ }
+
+
+ {{ label }}
+
+
+ @if (removable && closeIcon) {
+
+
+
+ }
+
+ @if (trailingIcon && !removable) {
+
+
+
+ }
+
+ `,
+ styleUrl: './chip.component.scss'
+})
+export class ChipComponent {
+ @Input() label: string = '';
+ @Input() variant: ChipVariant = 'filled';
+ @Input() size: ChipSize = 'md';
+ @Input() radius: ChipRadius = 'md';
+ @Input() disabled: boolean = false;
+ @Input() selected: boolean = false;
+ @Input() clickable: boolean = false;
+ @Input() removable: boolean = false;
+ @Input() leadingIcon?: IconDefinition;
+ @Input() trailingIcon?: IconDefinition;
+ @Input() closeIcon?: IconDefinition;
+ @Input() avatar?: string;
+ @Input() avatarAlt?: string;
+ @Input() class: string = '';
+
+ @Output() chipClick = new EventEmitter();
+ @Output() chipRemove = new EventEmitter();
+
+ get chipClasses(): string {
+ return [
+ 'ui-chip',
+ `ui-chip--${this.variant}`,
+ `ui-chip--${this.size}`,
+ `ui-chip--radius-${this.radius}`,
+ this.disabled ? 'ui-chip--disabled' : '',
+ this.selected ? 'ui-chip--selected' : '',
+ this.clickable ? 'ui-chip--clickable' : '',
+ this.removable ? 'ui-chip--removable' : '',
+ this.leadingIcon ? 'ui-chip--with-leading-icon' : '',
+ this.trailingIcon ? 'ui-chip--with-trailing-icon' : '',
+ this.avatar ? 'ui-chip--with-avatar' : '',
+ this.class
+ ].filter(Boolean).join(' ');
+ }
+
+ handleClick(event: Event): void {
+ if (!this.disabled && this.clickable) {
+ this.chipClick.emit(event);
+ }
+ }
+
+ handleRemove(event: Event): void {
+ event.stopPropagation();
+ if (!this.disabled) {
+ this.chipRemove.emit(event);
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/chip/index.ts b/projects/ui-essentials/src/lib/components/data-display/chip/index.ts
new file mode 100644
index 0000000..bfb6f60
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/chip/index.ts
@@ -0,0 +1 @@
+export * from './chip.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/image-container/image-container.component.scss b/projects/ui-essentials/src/lib/components/data-display/image-container/image-container.component.scss
new file mode 100644
index 0000000..7ae7d13
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/image-container/image-container.component.scss
@@ -0,0 +1,314 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+// Tokens available globally via main application styles
+
+.ui-image-container {
+ // Core Structure
+ position: relative;
+ display: inline-block;
+ overflow: hidden;
+
+ // Layout & Aspect Ratio
+ aspect-ratio: var(--aspect-ratio, 1/1);
+
+ // Visual Design
+ background: $semantic-color-surface-secondary;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-md;
+
+ // Transitions
+ transition: all $semantic-duration-fast $semantic-easing-standard;
+
+ // Size Variants
+ &--size-sm {
+ min-width: $semantic-sizing-card-width-sm * 0.5; // 8rem
+ max-width: $semantic-sizing-card-width-sm; // 16rem
+ min-height: $semantic-sizing-card-height-sm * 0.5; // 4rem
+ }
+
+ &--size-md {
+ min-width: $semantic-sizing-card-width-md * 0.5; // 10rem
+ max-width: $semantic-sizing-card-width-md; // 20rem
+ min-height: $semantic-sizing-card-height-md * 0.5; // 6rem
+ }
+
+ &--size-lg {
+ min-width: $semantic-sizing-card-width-lg * 0.5; // 12rem
+ max-width: $semantic-sizing-card-width-lg; // 24rem
+ min-height: $semantic-sizing-card-height-lg * 0.5; // 8rem
+ }
+
+ &--size-xl {
+ min-width: $semantic-sizing-card-width-lg * 0.75; // 18rem
+ max-width: $semantic-sizing-content-narrow; // 42rem
+ min-height: $semantic-sizing-card-height-lg * 0.75; // 12rem
+ }
+
+ // Aspect Ratio Variants
+ &--aspect-1-1 { aspect-ratio: 1/1; }
+ &--aspect-4-3 { aspect-ratio: 4/3; }
+ &--aspect-16-9 { aspect-ratio: 16/9; }
+ &--aspect-3-2 { aspect-ratio: 3/2; }
+ &--aspect-2-1 { aspect-ratio: 2/1; }
+ &--aspect-3-4 { aspect-ratio: 3/4; }
+ &--aspect-9-16 { aspect-ratio: 9/16; }
+
+ // Shape Variants
+ &--shape-square {
+ border-radius: $semantic-border-radius-sm;
+ }
+
+ &--shape-rounded {
+ border-radius: $semantic-border-radius-md;
+ }
+
+ &--shape-circle {
+ border-radius: $semantic-border-radius-full;
+ aspect-ratio: 1/1;
+ }
+
+ // State Variants
+ &--loading {
+ cursor: wait;
+ }
+
+ &--error {
+ border-color: $semantic-color-border-error;
+ border-style: dashed;
+ }
+
+ &--lazy {
+ background: $semantic-color-surface-secondary;
+
+ &:not(.ui-image-container--loaded) {
+ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+ }
+ }
+
+ // Interactive States
+ &:not(.ui-image-container--error) {
+ &:hover {
+ box-shadow: $semantic-shadow-elevation-2;
+ transform: translateY(-1px);
+ border-color: $semantic-color-border-primary;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus-ring;
+ outline-offset: 2px;
+ }
+
+ &:active {
+ transform: translateY(0);
+ box-shadow: $semantic-shadow-elevation-1;
+ }
+ }
+
+ // Image Element
+ &__image {
+ width: 100%;
+ height: 100%;
+ display: block;
+
+ // Object Fit Variants
+ &--fit-contain { object-fit: contain; }
+ &--fit-cover { object-fit: cover; }
+ &--fit-fill { object-fit: fill; }
+ &--fit-scale-down { object-fit: scale-down; }
+ &--fit-none { object-fit: none; }
+
+ // Smooth appearance transition
+ opacity: 0;
+ transition: opacity $semantic-duration-medium $semantic-easing-standard;
+
+ .ui-image-container--loaded & {
+ opacity: 1;
+ }
+ }
+
+ // Loading State
+ &__loading {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: $semantic-color-surface-secondary;
+ z-index: 2;
+ }
+
+ &__spinner {
+ width: $semantic-sizing-icon-navigation;
+ height: $semantic-sizing-icon-navigation;
+ border: 2px solid $semantic-color-border-primary;
+ border-top: 2px solid $semantic-color-interactive-primary;
+ border-radius: $semantic-border-radius-full;
+ animation: spin 1s linear infinite;
+ }
+
+ // Error State
+ &__error {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: $semantic-spacing-component-xs;
+ background: $semantic-color-surface-secondary;
+ color: $semantic-color-danger;
+ z-index: 2;
+ padding: $semantic-spacing-component-sm;
+ text-align: center;
+ }
+
+ &__error-icon {
+ color: $semantic-color-text-secondary;
+
+ svg {
+ width: $semantic-sizing-icon-navigation;
+ height: $semantic-sizing-icon-navigation;
+ }
+ }
+
+ &__error-text {
+ font-size: $semantic-typography-font-size-xs;
+ color: $semantic-color-text-secondary;
+ font-weight: $semantic-typography-font-weight-medium;
+ }
+
+ // Overlay
+ &__overlay {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(0, 0, 0, 0.4);
+ opacity: 0;
+ transition: opacity $semantic-duration-fast $semantic-easing-standard;
+ z-index: 3;
+
+ .ui-image-container:hover & {
+ opacity: 1;
+ }
+ }
+
+ // Caption
+ &__caption {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
+ color: white;
+ padding: $semantic-spacing-component-md $semantic-spacing-component-sm $semantic-spacing-component-sm;
+ font-size: $semantic-typography-font-size-sm;
+ line-height: $semantic-typography-line-height-tight;
+ z-index: 4;
+ }
+
+ // Dark Mode Support
+ :host-context(.dark-theme) & {
+ background: $semantic-color-surface-primary;
+ border-color: $semantic-color-border-subtle;
+
+ &__loading {
+ background: $semantic-color-surface-primary;
+ }
+
+ &__spinner {
+ border-color: $semantic-color-border-subtle;
+ border-top-color: $semantic-color-interactive-primary;
+ }
+ }
+
+ // Responsive Design
+ @media (max-width: $semantic-sizing-breakpoint-tablet - 1) {
+ &--size-xl {
+ max-width: $semantic-sizing-card-width-lg;
+ }
+
+ &--size-lg {
+ max-width: $semantic-sizing-card-width-md;
+ }
+ }
+
+ @media (max-width: $semantic-sizing-breakpoint-mobile - 1) {
+ // Mobile adjustments
+ &__caption {
+ font-size: $semantic-typography-font-size-xs;
+ padding: $semantic-spacing-component-xs;
+ }
+
+ &__error-text {
+ font-size: $semantic-typography-font-size-xs;
+ }
+ }
+
+ // Accessibility - Reduced motion
+ @media (prefers-reduced-motion: reduce) {
+ transition: none;
+
+ &__image {
+ transition: none;
+ }
+
+ &__overlay {
+ transition: none;
+ }
+
+ &--lazy:not(.ui-image-container--loaded) {
+ animation: none;
+ }
+
+ &__spinner {
+ animation: none;
+ }
+ }
+}
+
+// Animation Keyframes
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes pulse {
+ 0%, 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+}
+
+// Utility classes for content projection
+.ui-image-container {
+ [slot='error'] {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: $semantic-spacing-component-xs;
+ padding: $semantic-spacing-component-sm;
+ text-align: center;
+ height: 100%;
+ color: $semantic-color-danger;
+ }
+
+ [slot='overlay'] {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: white;
+ font-weight: $semantic-typography-font-weight-medium;
+ text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
+ }
+
+ [slot='caption'] {
+ font-size: $semantic-typography-font-size-sm;
+ line-height: $semantic-typography-line-height-tight;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/image-container/image-container.component.ts b/projects/ui-essentials/src/lib/components/data-display/image-container/image-container.component.ts
new file mode 100644
index 0000000..14f5fc6
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/image-container/image-container.component.ts
@@ -0,0 +1,217 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, ElementRef, OnInit, OnDestroy, inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+export type ImageContainerSize = 'sm' | 'md' | 'lg' | 'xl';
+export type ImageContainerAspectRatio = '1/1' | '4/3' | '16/9' | '3/2' | '2/1' | '3/4' | '9/16';
+export type ImageContainerObjectFit = 'contain' | 'cover' | 'fill' | 'scale-down' | 'none';
+export type ImageContainerShape = 'square' | 'rounded' | 'circle';
+
+@Component({
+ selector: 'ui-image-container',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ template: `
+
+
+ @if (loading && !isLoaded) {
+
+ }
+
+ @if (hasError) {
+
+
+
+ Failed to load image
+
+
+ } @else {
+
+ }
+
+ @if (overlay) {
+
+
+
+ }
+
+ @if (caption) {
+
+
+ {{ caption }}
+
+
+ }
+
+ `,
+ styleUrl: './image-container.component.scss'
+})
+export class ImageContainerComponent implements OnInit, OnDestroy {
+ @Input() src!: string;
+ @Input() alt = '';
+ @Input() size: ImageContainerSize = 'md';
+ @Input() aspectRatio: ImageContainerAspectRatio = '1/1';
+ @Input() objectFit: ImageContainerObjectFit = 'cover';
+ @Input() shape: ImageContainerShape = 'rounded';
+ @Input() lazy = true;
+ @Input() loading = false;
+ @Input() placeholder?: string;
+ @Input() caption?: string;
+ @Input() overlay = false;
+ @Input() ariaLabel?: string;
+ @Input() errorAlt?: string;
+
+ // Advanced image attributes
+ @Input() srcset?: string;
+ @Input() sizes?: string;
+ @Input() crossorigin?: 'anonymous' | 'use-credentials';
+ @Input() referrerPolicy?: 'no-referrer' | 'no-referrer-when-downgrade' | 'origin' | 'origin-when-cross-origin' | 'same-origin' | 'strict-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url';
+
+ @Output() imageLoaded = new EventEmitter();
+ @Output() imageError = new EventEmitter();
+ @Output() imageClick = new EventEmitter();
+ @Output() loadingChange = new EventEmitter();
+
+ private elementRef = inject(ElementRef);
+ private intersectionObserver?: IntersectionObserver;
+
+ hasError = false;
+ isLoaded = false;
+ currentSrc = '';
+
+ ngOnInit(): void {
+ this.initializeImage();
+ if (this.lazy && this.supportsIntersectionObserver()) {
+ this.setupLazyLoading();
+ } else {
+ this.loadImage();
+ }
+ }
+
+ ngOnDestroy(): void {
+ if (this.intersectionObserver) {
+ this.intersectionObserver.disconnect();
+ }
+ }
+
+ private initializeImage(): void {
+ this.currentSrc = this.placeholder || this.src;
+ this.hasError = false;
+ this.isLoaded = false;
+ }
+
+ private setupLazyLoading(): void {
+ if (!this.supportsIntersectionObserver()) {
+ this.loadImage();
+ return;
+ }
+
+ this.intersectionObserver = new IntersectionObserver(
+ (entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting) {
+ this.loadImage();
+ this.intersectionObserver?.unobserve(entry.target);
+ }
+ });
+ },
+ {
+ rootMargin: '50px 0px',
+ threshold: 0.01
+ }
+ );
+
+ this.intersectionObserver.observe(this.elementRef.nativeElement);
+ }
+
+ private loadImage(): void {
+ if (this.src && !this.isLoaded && !this.hasError) {
+ this.currentSrc = this.src;
+ this.loading = true;
+ this.loadingChange.emit(true);
+ }
+ }
+
+ private supportsIntersectionObserver(): boolean {
+ return 'IntersectionObserver' in window;
+ }
+
+ handleLoadStart(): void {
+ this.loading = true;
+ this.loadingChange.emit(true);
+ }
+
+ handleLoad(event: Event): void {
+ this.loading = false;
+ this.isLoaded = true;
+ this.hasError = false;
+ this.loadingChange.emit(false);
+ this.imageLoaded.emit(event);
+ }
+
+ handleError(event: Event): void {
+ this.loading = false;
+ this.hasError = true;
+ this.isLoaded = false;
+ this.loadingChange.emit(false);
+ this.imageError.emit(event);
+ }
+
+ handleClick(event: MouseEvent): void {
+ if (!this.hasError) {
+ this.imageClick.emit(event);
+ }
+ }
+
+ // Method to retry loading failed images
+ retryLoad(): void {
+ this.hasError = false;
+ this.isLoaded = false;
+ this.loadImage();
+ }
+
+ getContainerClasses(): string {
+ const aspectClass = `ui-image-container--aspect-${this.aspectRatio.replace('/', '-')}`;
+ const sizeClass = `ui-image-container--size-${this.size}`;
+ const shapeClass = `ui-image-container--shape-${this.shape}`;
+
+ return `ui-image-container ${sizeClass} ${aspectClass} ${shapeClass}`;
+ }
+
+ getImageClasses(): string {
+ const fitClass = `ui-image-container__image--fit-${this.objectFit}`;
+ return `ui-image-container__image ${fitClass}`;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/image-container/index.ts b/projects/ui-essentials/src/lib/components/data-display/image-container/index.ts
new file mode 100644
index 0000000..b7b3a8c
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/image-container/index.ts
@@ -0,0 +1 @@
+export * from './image-container.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/index.ts b/projects/ui-essentials/src/lib/components/data-display/index.ts
new file mode 100644
index 0000000..524223f
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/index.ts
@@ -0,0 +1,12 @@
+export * from './avatar';
+export * from './badge';
+export * from './card';
+export * from './carousel';
+export * from './chip';
+export * from './image-container';
+export * from './list';
+export * from './progress';
+export * from './table';
+// Selectively export from feedback to avoid ProgressBarComponent conflict
+export { StatusBadgeComponent } from '../feedback';
+export type { StatusBadgeVariant, StatusBadgeSize, StatusBadgeShape } from '../feedback';
diff --git a/projects/ui-essentials/src/lib/components/data-display/list/README.md b/projects/ui-essentials/src/lib/components/data-display/list/README.md
new file mode 100644
index 0000000..1ef3912
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/list/README.md
@@ -0,0 +1,223 @@
+# List Components
+
+Comprehensive list component system built with Angular 19+ and semantic design tokens. Features multiple variants, sizes, and interactive states for maximum flexibility.
+
+## Components
+
+### `ListItemComponent`
+Individual list item with support for avatars, media, icons, and multiple text lines.
+
+### `ListContainerComponent`
+Container for list items with elevation, spacing, and scrolling capabilities.
+
+### `ListExamplesComponent`
+Demonstration component showcasing all variants and usage patterns.
+
+## Features
+
+- **Multiple Sizes**: `sm`, `md`, `lg`
+- **Line Variants**: One, two, or three lines of text
+- **Content Types**: Text-only, avatar, media, icon
+- **Interactive States**: Hover, focus, selected, disabled
+- **Container Options**: Elevation, spacing, scrolling, rounded corners
+- **Text Overflow**: Automatic ellipsis handling
+- **Accessibility**: Full ARIA support and keyboard navigation
+- **Responsive**: Mobile-first design with breakpoint adjustments
+
+## Usage
+
+### Basic Text List
+
+```typescript
+import { ListItemComponent, ListContainerComponent } from './shared/components';
+
+// In your component:
+items = [
+ { primary: 'Inbox' },
+ { primary: 'Starred' },
+ { primary: 'Sent' }
+];
+```
+
+```html
+
+ @for (item of items; track item.primary) {
+
+ }
+
+```
+
+### Avatar List with Two Lines
+
+```typescript
+avatarItems = [
+ {
+ primary: 'John Doe',
+ secondary: 'Software Engineer',
+ avatarSrc: 'https://example.com/avatar.jpg',
+ avatarAlt: 'John Doe'
+ }
+];
+```
+
+```html
+
+ @for (item of avatarItems; track item.primary) {
+
+
+
+
+
+ }
+
+```
+
+### Media List with Three Lines
+
+```typescript
+mediaItems = [
+ {
+ primary: 'Angular Course',
+ secondary: 'Complete guide to modern development',
+ tertiary: 'Duration: 2h 30m • Updated: Today',
+ mediaSrc: 'https://example.com/thumb.jpg',
+ mediaAlt: 'Course thumbnail'
+ }
+];
+```
+
+```html
+
+ @for (item of mediaItems; track item.primary) {
+
+
+
+ 4.8
+
+
+ }
+
+```
+
+### Scrollable List
+
+```html
+
+ @for (item of longList; track item.id) {
+
+ }
+
+```
+
+## API
+
+### ListItemComponent Props
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `data` | `ListItemData` | required | Item content and configuration |
+| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Item height and spacing |
+| `lines` | `'one' \| 'two' \| 'three'` | `'one'` | Number of text lines |
+| `variant` | `'text' \| 'avatar' \| 'media'` | `'text'` | Content type |
+| `interactive` | `boolean` | `true` | Enable hover/focus states |
+| `divider` | `boolean` | `false` | Show bottom border |
+
+### ListContainerComponent Props
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `elevation` | `'none' \| 'sm' \| 'md' \| 'lg'` | `'none'` | Shadow elevation |
+| `spacing` | `'none' \| 'xs' \| 'sm' \| 'md' \| 'lg'` | `'sm'` | Item spacing |
+| `scrollable` | `boolean` | `false` | Enable vertical scrolling |
+| `maxHeight` | `string?` | - | Maximum container height |
+| `fadeIndicator` | `boolean` | `true` | Show fade at scroll bottom |
+| `dense` | `boolean` | `false` | Reduce spacing between items |
+| `rounded` | `boolean` | `false` | Round container corners |
+| `ariaLabel` | `string?` | - | Accessibility label |
+
+### ListItemData Interface
+
+```typescript
+interface ListItemData {
+ primary: string; // Main text (required)
+ secondary?: string; // Secondary text
+ tertiary?: string; // Tertiary text (three-line only)
+ avatarSrc?: string; // Avatar image URL
+ avatarAlt?: string; // Avatar alt text
+ mediaSrc?: string; // Media image URL
+ mediaAlt?: string; // Media alt text
+ icon?: string; // Icon class name
+ disabled?: boolean; // Disabled state
+ selected?: boolean; // Selected state
+}
+```
+
+## Slots
+
+### Trailing Content
+Use the `slot="trailing"` attribute to add content to the right side of list items:
+
+```html
+
+ Action
+
+```
+
+## Design Tokens
+
+The components use semantic design tokens for consistent styling:
+
+- **Colors**: `$semantic-color-*`
+- **Spacing**: `$semantic-spacing-*`
+- **Shadows**: `$semantic-shadow-*`
+- **Borders**: `$semantic-border-radius-*`
+
+## Accessibility
+
+- Full ARIA support with proper roles and labels
+- Keyboard navigation support
+- Focus management and indicators
+- Screen reader friendly
+- High contrast mode support
+- Reduced motion support
+
+## Responsive Design
+
+- Mobile-first approach
+- Breakpoint-specific adjustments
+- Touch-friendly tap targets
+- Optimized spacing for different screen sizes
+
+## Examples
+
+Import and use the `ListExamplesComponent` to see all variants in action:
+
+```typescript
+import { ListExamplesComponent } from './shared/components';
+```
+
+```html
+
+```
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/list/index.ts b/projects/ui-essentials/src/lib/components/data-display/list/index.ts
new file mode 100644
index 0000000..aac01ec
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/list/index.ts
@@ -0,0 +1,23 @@
+// ==========================================================================
+// LIST COMPONENTS
+// ==========================================================================
+// Comprehensive list component system with multiple variants and sizes
+// Built with Angular 19+ and semantic design tokens
+// ==========================================================================
+
+export * from './list-item.component';
+export * from './list-container.component';
+export * from './list-examples.component';
+
+// Re-export types for convenience
+export type {
+ ListItemSize,
+ ListItemLines,
+ ListItemVariant,
+ ListItemData
+} from './list-item.component';
+
+export type {
+ ListContainerElevation,
+ ListContainerSpacing
+} from './list-container.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/list/list-container.component.scss b/projects/ui-essentials/src/lib/components/data-display/list/list-container.component.scss
new file mode 100644
index 0000000..f5c6b11
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/list/list-container.component.scss
@@ -0,0 +1,263 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+// Tokens available globally via main application styles
+
+// ==========================================================================
+// LIST CONTAINER COMPONENT
+// ==========================================================================
+// Container component for list items with elevation, spacing, and scrolling
+// Provides consistent layout and styling using semantic design tokens
+// ==========================================================================
+
+.list-container {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ box-sizing: border-box;
+ background-color: $semantic-color-surface-primary;
+ position: relative;
+ overflow: hidden;
+
+ // ==========================================================================
+ // ELEVATION VARIANTS
+ // ==========================================================================
+
+ &--elevation-none {
+ box-shadow: none;
+ }
+
+ &--elevation-sm {
+ box-shadow: $semantic-shadow-elevation-1;
+ }
+
+ &--elevation-md {
+ box-shadow: $semantic-shadow-elevation-3;
+ }
+
+ &--elevation-lg {
+ box-shadow: $semantic-shadow-elevation-4;
+ }
+
+ // ==========================================================================
+ // SPACING VARIANTS
+ // ==========================================================================
+
+ &--spacing-none {
+ padding: 0;
+
+ .list-item:not(:last-child) {
+ margin-bottom: 0;
+ }
+ }
+
+ &--spacing-xs {
+ padding: $semantic-spacing-component-xs 0;
+
+ .list-item:not(:last-child) {
+ margin-bottom: $semantic-spacing-component-xs;
+ }
+ }
+
+ &--spacing-sm {
+ padding: $semantic-spacing-component-sm 0;
+
+ .list-item:not(:last-child) {
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+ }
+
+ &--spacing-md {
+ padding: $semantic-spacing-component-md 0;
+
+ .list-item:not(:last-child) {
+ margin-bottom: $semantic-spacing-component-md;
+ }
+ }
+
+ &--spacing-lg {
+ padding: $semantic-spacing-component-lg 0;
+
+ .list-item:not(:last-child) {
+ margin-bottom: $semantic-spacing-component-lg;
+ }
+ }
+
+ // ==========================================================================
+ // LAYOUT MODIFIERS
+ // ==========================================================================
+
+ &--scrollable {
+ overflow-y: auto;
+ -webkit-overflow-scrolling: touch;
+
+ // Custom scrollbar styling
+ &::-webkit-scrollbar {
+ width: 6px;
+ }
+
+ &::-webkit-scrollbar-track {
+ background: $semantic-color-surface-secondary;
+ border-radius: 3px;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: $semantic-color-border-primary;
+ border-radius: 3px;
+
+ &:hover {
+ background: $semantic-color-border-secondary;
+ }
+ }
+
+ // Firefox scrollbar
+ scrollbar-width: thin;
+ scrollbar-color: $semantic-color-border-primary $semantic-color-surface-secondary;
+ }
+
+ &--max-height {
+ height: var(--list-max-height);
+ max-height: var(--list-max-height);
+ }
+
+ &--dense {
+ &.list-container--spacing-xs .list-item:not(:last-child) {
+ margin-bottom: $semantic-spacing-micro-tight;
+ }
+
+ &.list-container--spacing-sm .list-item:not(:last-child) {
+ margin-bottom: $semantic-spacing-component-xs;
+ }
+
+ &.list-container--spacing-md .list-item:not(:last-child) {
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+
+ &.list-container--spacing-lg .list-item:not(:last-child) {
+ margin-bottom: $semantic-spacing-component-md;
+ }
+ }
+
+ &--rounded {
+ border-radius: $semantic-border-radius-md;
+ overflow: hidden;
+
+ .list-item:first-child {
+ border-top-left-radius: $semantic-border-radius-md;
+ border-top-right-radius: $semantic-border-radius-md;
+ }
+
+ .list-item:last-child {
+ border-bottom-left-radius: $semantic-border-radius-md;
+ border-bottom-right-radius: $semantic-border-radius-md;
+ }
+ }
+}
+
+// ==========================================================================
+// FADE INDICATOR
+// ==========================================================================
+
+.list-container__fade-bottom {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 24px;
+ background: linear-gradient(
+ to bottom,
+ transparent 0%,
+ $semantic-color-surface-primary 100%
+ );
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity 0.2s ease-in-out;
+
+ .list-container--scrollable:not(.list-container--at-bottom) & {
+ opacity: 1;
+ }
+}
+
+// ==========================================================================
+// DYNAMIC HEIGHT SETUP
+// ==========================================================================
+
+// JavaScript will set this CSS custom property
+.list-container--max-height {
+ --list-max-height: 400px;
+}
+
+// ==========================================================================
+// RESPONSIVE ADJUSTMENTS
+// ==========================================================================
+
+@media (max-width: 768px) {
+ .list-container {
+ &--spacing-md {
+ padding: $semantic-spacing-component-sm 0;
+
+ .list-item:not(:last-child) {
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+ }
+
+ &--spacing-lg {
+ padding: $semantic-spacing-component-md 0;
+
+ .list-item:not(:last-child) {
+ margin-bottom: $semantic-spacing-component-md;
+ }
+ }
+ }
+}
+
+// ==========================================================================
+// ACCESSIBILITY ENHANCEMENTS
+// ==========================================================================
+
+.list-container {
+ &:focus-within {
+ outline: 2px solid $semantic-color-border-focus;
+ outline-offset: 2px;
+ }
+}
+
+// ==========================================================================
+// ANIMATION UTILITIES
+// ==========================================================================
+
+.list-container {
+ // Smooth height transitions when content changes
+ transition: height 0.2s ease-in-out;
+}
+
+// List item enter/leave animations
+@keyframes list-item-enter {
+ from {
+ opacity: 0;
+ transform: translateY(-8px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes list-item-leave {
+ from {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ to {
+ opacity: 0;
+ transform: translateY(-8px);
+ }
+}
+
+// Add these classes via JavaScript for dynamic content
+.list-item-enter {
+ animation: list-item-enter 0.2s ease-out;
+}
+
+.list-item-leave {
+ animation: list-item-leave 0.2s ease-out;
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/list/list-container.component.ts b/projects/ui-essentials/src/lib/components/data-display/list/list-container.component.ts
new file mode 100644
index 0000000..be11390
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/list/list-container.component.ts
@@ -0,0 +1,52 @@
+import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+export type ListContainerElevation = 'none' | 'sm' | 'md' | 'lg';
+export type ListContainerSpacing = 'none' | 'xs' | 'sm' | 'md' | 'lg';
+
+@Component({
+ selector: 'ui-list-container',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [CommonModule],
+ template: `
+
+
+
+
+ @if (scrollable && fadeIndicator) {
+
+ }
+
+ `,
+ styleUrls: ['./list-container.component.scss']
+})
+export class ListContainerComponent {
+ @Input() elevation: ListContainerElevation = 'none';
+ @Input() spacing: ListContainerSpacing = 'sm';
+ @Input() scrollable = false;
+ @Input() maxHeight?: string;
+ @Input() fadeIndicator = true;
+ @Input() dense = false;
+ @Input() rounded = false;
+ @Input() ariaLabel?: string;
+
+ getContainerClasses(): string {
+ const classes = [
+ `list-container--elevation-${this.elevation}`,
+ `list-container--spacing-${this.spacing}`
+ ];
+
+ if (this.scrollable) classes.push('list-container--scrollable');
+ if (this.dense) classes.push('list-container--dense');
+ if (this.rounded) classes.push('list-container--rounded');
+ if (this.maxHeight) classes.push('list-container--max-height');
+
+ return classes.join(' ');
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/list/list-examples.component.scss b/projects/ui-essentials/src/lib/components/data-display/list/list-examples.component.scss
new file mode 100644
index 0000000..a27262a
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/list/list-examples.component.scss
@@ -0,0 +1,234 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+// Tokens available globally via main application styles
+
+// ==========================================================================
+// LIST EXAMPLES COMPONENT
+// ==========================================================================
+// Demonstration component showcasing all list variants and capabilities
+// ==========================================================================
+
+.list-examples {
+ padding: $semantic-spacing-component-xl;
+ max-width: 1200px;
+ margin: 0 auto;
+
+ h2 {
+ font-size: 2rem;
+ font-weight: 600;
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-layout-lg;
+ text-align: center;
+ }
+}
+
+.example-section {
+ margin-bottom: $semantic-spacing-layout-xl;
+
+ h3 {
+ font-size: 1.5rem;
+ font-weight: 500;
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-component-xl;
+ padding-bottom: $semantic-spacing-component-sm;
+ border-bottom: 2px solid $semantic-color-border-secondary;
+ }
+}
+
+.example-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
+ gap: $semantic-spacing-component-xl;
+ margin-bottom: $semantic-spacing-layout-md;
+}
+
+.example-item {
+ h4 {
+ font-size: 1rem;
+ font-weight: 500;
+ color: $semantic-color-text-secondary;
+ margin-bottom: $semantic-spacing-component-md;
+ text-align: center;
+ }
+}
+
+.example-single {
+ max-width: 600px;
+ margin: 0 auto;
+}
+
+// ==========================================================================
+// TRAILING SLOT COMPONENTS
+// ==========================================================================
+
+.action-button {
+ background: none;
+ border: none;
+ color: $semantic-color-text-tertiary;
+ cursor: pointer;
+ padding: $semantic-spacing-component-xs;
+ border-radius: $semantic-border-radius-sm;
+ transition: all 0.15s ease-in-out;
+
+ &:hover {
+ background-color: $semantic-color-surface-interactive;
+ color: $semantic-color-text-secondary;
+ }
+
+ &:focus {
+ outline: 2px solid $semantic-color-border-focus;
+ outline-offset: 2px;
+ }
+
+ i {
+ font-size: 14px;
+ }
+}
+
+.timestamp {
+ font-size: 0.75rem;
+ color: $semantic-color-text-tertiary;
+ font-weight: 400;
+}
+
+.play-button {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ background-color: $semantic-color-interactive-primary;
+ color: $semantic-color-on-brand-primary;
+ border: none;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.15s ease-in-out;
+
+ &:hover {
+ transform: scale(1.05);
+ box-shadow: $semantic-shadow-elevation-1;
+ }
+
+ &:focus {
+ outline: 2px solid $semantic-color-border-focus;
+ outline-offset: 2px;
+ }
+
+ i {
+ font-size: 12px;
+ margin-left: 2px; // Optical alignment for play icon
+ }
+}
+
+.rating {
+ display: flex;
+ align-items: center;
+ gap: $semantic-spacing-micro-tight;
+ color: $semantic-color-warning;
+ font-size: 0.875rem;
+ font-weight: 500;
+
+ i {
+ font-size: 14px;
+ }
+}
+
+.select-button {
+ background-color: $semantic-color-interactive-primary;
+ color: $semantic-color-on-brand-primary;
+ border: none;
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ border-radius: $semantic-border-radius-sm;
+ font-size: 0.875rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.15s ease-in-out;
+
+ &:hover {
+ background-color: $semantic-color-interactive-secondary;
+ }
+
+ &:focus {
+ outline: 2px solid $semantic-color-border-focus;
+ outline-offset: 2px;
+ }
+}
+
+.text-success {
+ color: $semantic-color-success;
+}
+
+// ==========================================================================
+// RESPONSIVE DESIGN
+// ==========================================================================
+
+@media (max-width: 768px) {
+ .list-examples {
+ padding: $semantic-spacing-component-lg;
+
+ h2 {
+ font-size: 1.5rem;
+ }
+ }
+
+ .example-grid {
+ grid-template-columns: 1fr;
+ gap: $semantic-spacing-component-lg;
+ }
+
+ .example-section {
+ margin-bottom: $semantic-spacing-layout-md;
+
+ h3 {
+ font-size: 1.25rem;
+ }
+ }
+}
+
+@media (max-width: 480px) {
+ .list-examples {
+ padding: $semantic-spacing-component-md;
+ }
+
+ .example-grid {
+ gap: $semantic-spacing-component-md;
+ }
+}
+
+// ==========================================================================
+// ACCESSIBILITY ENHANCEMENTS
+// ==========================================================================
+
+// Focus indicators for better keyboard navigation
+.list-examples {
+ button:focus,
+ .action-button:focus,
+ .play-button:focus,
+ .select-button:focus {
+ outline: 2px solid $semantic-color-border-focus;
+ outline-offset: 2px;
+ border-radius: $semantic-border-radius-sm;
+ }
+}
+
+// High contrast mode support
+@media (prefers-contrast: high) {
+ .action-button,
+ .play-button,
+ .select-button {
+ border: 1px solid $semantic-color-border-primary;
+ }
+}
+
+// Reduced motion support
+@media (prefers-reduced-motion: reduce) {
+ .action-button,
+ .play-button,
+ .select-button {
+ transition: none;
+ }
+
+ .play-button:hover {
+ transform: none;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/list/list-examples.component.ts b/projects/ui-essentials/src/lib/components/data-display/list/list-examples.component.ts
new file mode 100644
index 0000000..92a5f94
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/list/list-examples.component.ts
@@ -0,0 +1,374 @@
+import { Component, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ListItemComponent, ListItemData } from './list-item.component';
+import { ListContainerComponent } from './list-container.component';
+
+@Component({
+ selector: 'ui-list-examples',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [CommonModule, ListItemComponent, ListContainerComponent],
+ template: `
+
+
List Component Examples
+
+
+
+ Text Lists - Different Sizes
+
+
+
+
Small (sm)
+
+ @for (item of basicTextItems; track item.primary) {
+
+ }
+
+
+
+
+
Medium (md)
+
+ @for (item of basicTextItems; track item.primary) {
+
+ }
+
+
+
+
+
Large (lg)
+
+ @for (item of basicTextItems; track item.primary) {
+
+ }
+
+
+
+
+
+
+
+ Multi-line Text Lists
+
+
+
+
Two Lines
+
+ @for (item of twoLineItems; track item.primary) {
+
+ }
+
+
+
+
+
Three Lines
+
+ @for (item of threeLineItems; track item.primary) {
+
+ }
+
+
+
+
+
+
+
+ Lists with Avatars
+
+
+
+
Avatar + One Line
+
+ @for (item of avatarItems; track item.primary) {
+
+
+
+
+
+ }
+
+
+
+
+
Avatar + Two Lines
+
+ @for (item of avatarTwoLineItems; track item.primary) {
+
+ 2h
+
+ }
+
+
+
+
+
+
+
+ Lists with Media
+
+
+
+
Media + Two Lines
+
+ @for (item of mediaItems; track item.primary) {
+
+
+
+
+
+ }
+
+
+
+
+
Media + Three Lines
+
+ @for (item of mediaThreeLineItems; track item.primary) {
+
+
+
+ 4.8
+
+
+ }
+
+
+
+
+
+
+
+ Scrollable List
+
+
+
+ @for (item of longList; track item.primary) {
+
+ @if (item.selected) {
+
+ }
+
+ }
+
+
+
+
+
+
+ Interactive States
+
+
+
+ @for (item of interactiveItems; track item.primary) {
+
+ @if (item.selected) {
+
+ } @else {
+ Select
+ }
+
+ }
+
+
+
+
+ `,
+ styleUrls: ['./list-examples.component.scss']
+})
+export class ListExamplesComponent {
+ basicTextItems: ListItemData[] = [
+ { primary: 'Inbox' },
+ { primary: 'Starred' },
+ { primary: 'Sent' },
+ { primary: 'Drafts' },
+ { primary: 'Archive' }
+ ];
+
+ twoLineItems: ListItemData[] = [
+ {
+ primary: 'Meeting with Design Team',
+ secondary: 'Discuss new component library architecture and implementation'
+ },
+ {
+ primary: 'Code Review - Authentication',
+ secondary: 'Review pull request #247 for OAuth integration updates'
+ },
+ {
+ primary: 'Performance Optimization',
+ secondary: 'Analyze bundle size and implement lazy loading strategies'
+ }
+ ];
+
+ threeLineItems: ListItemData[] = [
+ {
+ primary: 'Angular 19 Migration',
+ secondary: 'Upgrade project to latest Angular version with new control flow',
+ tertiary: 'Estimated completion: Next Sprint'
+ },
+ {
+ primary: 'Design System Documentation',
+ secondary: 'Create comprehensive documentation for all components',
+ tertiary: 'Priority: High - Needed for team onboarding'
+ }
+ ];
+
+ avatarItems: ListItemData[] = [
+ {
+ primary: 'John Doe',
+ avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=john',
+ avatarAlt: 'John Doe avatar'
+ },
+ {
+ primary: 'Jane Smith',
+ avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=jane',
+ avatarAlt: 'Jane Smith avatar'
+ },
+ {
+ primary: 'Mike Johnson',
+ avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=mike',
+ avatarAlt: 'Mike Johnson avatar'
+ }
+ ];
+
+ avatarTwoLineItems: ListItemData[] = [
+ {
+ primary: 'Sarah Wilson',
+ secondary: 'Hey! How\'s the new component library coming along?',
+ avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=sarah',
+ avatarAlt: 'Sarah Wilson avatar'
+ },
+ {
+ primary: 'Alex Chen',
+ secondary: 'The design tokens look great. Ready for review.',
+ avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=alex',
+ avatarAlt: 'Alex Chen avatar'
+ }
+ ];
+
+ mediaItems: ListItemData[] = [
+ {
+ primary: 'Angular Fundamentals',
+ secondary: 'Complete guide to modern Angular development',
+ mediaSrc: 'https://picsum.photos/80/80?random=1',
+ mediaAlt: 'Angular course thumbnail'
+ },
+ {
+ primary: 'TypeScript Deep Dive',
+ secondary: 'Advanced TypeScript patterns and best practices',
+ mediaSrc: 'https://picsum.photos/80/80?random=2',
+ mediaAlt: 'TypeScript course thumbnail'
+ }
+ ];
+
+ mediaThreeLineItems: ListItemData[] = [
+ {
+ primary: 'Design Systems at Scale',
+ secondary: 'Building maintainable design systems for large organizations',
+ tertiary: 'Duration: 2h 30m • Updated: Today',
+ mediaSrc: 'https://picsum.photos/80/80?random=3',
+ mediaAlt: 'Design systems course'
+ },
+ {
+ primary: 'Component Architecture',
+ secondary: 'Scalable component patterns in modern web applications',
+ tertiary: 'Duration: 1h 45m • Updated: Yesterday',
+ mediaSrc: 'https://picsum.photos/80/80?random=4',
+ mediaAlt: 'Component architecture course'
+ }
+ ];
+
+ longList: ListItemData[] = Array.from({ length: 20 }, (_, i) => ({
+ primary: `Contact ${i + 1}`,
+ secondary: `contact${i + 1}@example.com`,
+ avatarSrc: `https://api.dicebear.com/7.x/avataaars/svg?seed=contact${i}`,
+ avatarAlt: `Contact ${i + 1} avatar`,
+ selected: i === 3 || i === 7 || i === 12
+ }));
+
+ interactiveItems: ListItemData[] = [
+ {
+ primary: 'Enable Notifications',
+ secondary: 'Get notified about important updates',
+ avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=notify',
+ selected: false
+ },
+ {
+ primary: 'Dark Mode',
+ secondary: 'Switch to dark theme',
+ avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=dark',
+ selected: true
+ },
+ {
+ primary: 'Auto-save',
+ secondary: 'Automatically save your work',
+ avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=save',
+ selected: false,
+ disabled: true
+ }
+ ];
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/list/list-item.component.scss b/projects/ui-essentials/src/lib/components/data-display/list/list-item.component.scss
new file mode 100644
index 0000000..9ab3801
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/list/list-item.component.scss
@@ -0,0 +1,360 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+// Tokens available globally via main application styles
+
+// ==========================================================================
+// LIST ITEM COMPONENT
+// ==========================================================================
+// Comprehensive list item component with multiple variants and sizes
+// Uses semantic design tokens for consistent styling across the design system
+// ==========================================================================
+
+.list-item {
+ display: flex;
+ align-items: stretch;
+ width: 100%;
+ box-sizing: border-box;
+ position: relative;
+ user-select: none;
+ background-color: $semantic-color-surface-primary;
+ color: $semantic-color-text-primary;
+
+ // Interactive states
+ &--interactive {
+ cursor: pointer;
+ transition: background-color 0.15s ease-in-out;
+
+ &:hover:not(.list-item--disabled) {
+ background-color: $semantic-color-surface-interactive;
+ }
+
+ &:focus-within:not(.list-item--disabled) {
+ outline: 2px solid $semantic-color-border-focus;
+ outline-offset: -2px;
+ }
+ }
+
+ // States
+ &--selected {
+ background-color: $semantic-color-container-primary;
+ color: $semantic-color-on-container-primary;
+ }
+
+ &--disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ color: $semantic-color-text-disabled;
+ }
+
+ &--divider {
+ border-bottom: 1px solid $semantic-color-border-secondary;
+ }
+
+ // ==========================================================================
+ // SIZE VARIANTS
+ // ==========================================================================
+
+ &--sm {
+ min-height: 40px;
+
+ &.list-item--one-line { height: 40px; }
+ &.list-item--two-line { min-height: 56px; }
+ &.list-item--three-line { min-height: 72px; }
+ }
+
+ &--md {
+ min-height: 48px;
+
+ &.list-item--one-line { height: 48px; }
+ &.list-item--two-line { min-height: 64px; }
+ &.list-item--three-line { min-height: 80px; }
+ }
+
+ &--lg {
+ min-height: 56px;
+
+ &.list-item--one-line { height: 56px; }
+ &.list-item--two-line { min-height: 72px; }
+ &.list-item--three-line { min-height: 88px; }
+ }
+}
+
+// ==========================================================================
+// LAYOUT ELEMENTS
+// ==========================================================================
+
+.list-item__avatar,
+.list-item__media,
+.list-item__icon {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.list-item__avatar {
+ .list-item--sm & {
+ width: 32px;
+ padding: $semantic-spacing-component-sm;
+ }
+
+ .list-item--md & {
+ width: 40px;
+ padding: $semantic-spacing-component-md;
+ }
+
+ .list-item--lg & {
+ width: 48px;
+ padding: $semantic-spacing-component-lg;
+ }
+}
+
+.list-item__avatar-image {
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ object-fit: cover;
+
+ .list-item--sm & {
+ width: 24px;
+ height: 24px;
+ }
+
+ .list-item--md & {
+ width: 32px;
+ height: 32px;
+ }
+
+ .list-item--lg & {
+ width: 40px;
+ height: 40px;
+ }
+}
+
+.list-item__media {
+ .list-item--sm & {
+ width: 48px;
+ padding: $semantic-spacing-component-sm;
+ }
+
+ .list-item--md & {
+ width: 56px;
+ padding: $semantic-spacing-component-md;
+ }
+
+ .list-item--lg & {
+ width: 64px;
+ padding: $semantic-spacing-component-lg;
+ }
+}
+
+.list-item__media-image {
+ width: 100%;
+ height: auto;
+ border-radius: $semantic-border-radius-sm;
+ object-fit: cover;
+
+ .list-item--sm & {
+ width: 40px;
+ height: 40px;
+ }
+
+ .list-item--md & {
+ width: 48px;
+ height: 48px;
+ }
+
+ .list-item--lg & {
+ width: 56px;
+ height: 56px;
+ }
+}
+
+.list-item__icon {
+ .list-item--sm & {
+ width: 32px;
+ padding: $semantic-spacing-component-sm;
+ }
+
+ .list-item--md & {
+ width: 40px;
+ padding: $semantic-spacing-component-md;
+ }
+
+ .list-item--lg & {
+ width: 48px;
+ padding: $semantic-spacing-component-lg;
+ }
+
+ i {
+ color: $semantic-color-text-secondary;
+
+ .list-item--sm & { font-size: 16px; }
+ .list-item--md & { font-size: 20px; }
+ .list-item--lg & { font-size: 24px; }
+ }
+}
+
+.list-item__content {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
+ gap: $semantic-spacing-micro-tight;
+
+ .list-item--sm & {
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ }
+
+ .list-item--lg & {
+ padding: $semantic-spacing-component-md $semantic-spacing-component-lg;
+ }
+}
+
+.list-item__primary {
+ font-size: 1rem;
+ font-weight: 500;
+ line-height: 1.25;
+ color: inherit;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ .list-item--sm & {
+ font-size: 0.875rem;
+ }
+
+ .list-item--lg & {
+ font-size: 1.125rem;
+ }
+}
+
+.list-item__secondary {
+ font-size: 0.875rem;
+ font-weight: 400;
+ line-height: 1.25;
+ color: $semantic-color-text-secondary;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ .list-item--sm & {
+ font-size: 0.75rem;
+ }
+
+ .list-item--lg & {
+ font-size: 1rem;
+ }
+}
+
+.list-item__tertiary {
+ font-size: 0.75rem;
+ font-weight: 400;
+ line-height: 1.25;
+ color: $semantic-color-text-tertiary;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ .list-item--sm & {
+ font-size: 0.6875rem;
+ }
+
+ .list-item--lg & {
+ font-size: 0.875rem;
+ }
+}
+
+.list-item__trailing {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ padding-right: $semantic-spacing-component-md;
+
+ .list-item--sm & {
+ padding-right: $semantic-spacing-component-sm;
+ }
+
+ .list-item--lg & {
+ padding-right: $semantic-spacing-component-lg;
+ }
+
+ &:empty {
+ display: none;
+ }
+}
+
+// ==========================================================================
+// LINE VARIANTS - Content layout adjustments
+// ==========================================================================
+
+.list-item--one-line {
+ .list-item__content {
+ justify-content: center;
+ }
+}
+
+.list-item--two-line {
+ .list-item__content {
+ justify-content: center;
+ gap: $semantic-spacing-micro-tight;
+ }
+}
+
+.list-item--three-line {
+ .list-item__content {
+ justify-content: flex-start;
+ padding-top: $semantic-spacing-component-sm;
+ padding-bottom: $semantic-spacing-component-sm;
+ gap: $semantic-spacing-micro-tight;
+
+ .list-item--sm & {
+ padding-top: $semantic-spacing-component-xs;
+ padding-bottom: $semantic-spacing-component-xs;
+ }
+
+ .list-item--lg & {
+ padding-top: $semantic-spacing-component-md;
+ padding-bottom: $semantic-spacing-component-md;
+ }
+ }
+}
+
+// ==========================================================================
+// VARIANT SPECIFIC STYLES
+// ==========================================================================
+
+.list-item--avatar {
+ .list-item__content {
+ padding-left: $semantic-spacing-component-xs;
+ }
+}
+
+.list-item--media {
+ .list-item__content {
+ padding-left: $semantic-spacing-component-xs;
+ }
+}
+
+// ==========================================================================
+// RESPONSIVE ADJUSTMENTS
+// ==========================================================================
+
+@media (max-width: 768px) {
+ .list-item {
+ &--lg {
+ min-height: 48px;
+
+ &.list-item--one-line { height: 48px; }
+ &.list-item--two-line { min-height: 64px; }
+ &.list-item--three-line { min-height: 80px; }
+ }
+ }
+
+ .list-item__content {
+ padding-left: $semantic-spacing-component-sm;
+ padding-right: $semantic-spacing-component-sm;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/list/list-item.component.ts b/projects/ui-essentials/src/lib/components/data-display/list/list-item.component.ts
new file mode 100644
index 0000000..9754be8
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/list/list-item.component.ts
@@ -0,0 +1,109 @@
+import { Component, Input, ChangeDetectionStrategy, inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+export type ListItemSize = 'sm' | 'md' | 'lg';
+export type ListItemLines = 'one' | 'two' | 'three';
+export type ListItemVariant = 'text' | 'avatar' | 'media';
+
+export interface ListItemData {
+ primary: string;
+ secondary?: string;
+ tertiary?: string;
+ avatarSrc?: string;
+ avatarAlt?: string;
+ mediaSrc?: string;
+ mediaAlt?: string;
+ icon?: string;
+ disabled?: boolean;
+ selected?: boolean;
+}
+
+@Component({
+ selector: 'ui-list-item',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [CommonModule],
+ template: `
+
+
+ @if (variant === 'avatar' && data.avatarSrc) {
+
+
+
+ }
+
+ @if (variant === 'media' && data.mediaSrc) {
+
+ }
+
+ @if (data.icon) {
+
+
+
+ }
+
+
+
+ @if (lines === 'one') {
+
{{ data.primary }}
+ }
+
+ @if (lines === 'two') {
+
{{ data.primary }}
+
{{ data.secondary }}
+ }
+
+ @if (lines === 'three') {
+
{{ data.primary }}
+
{{ data.secondary }}
+
{{ data.tertiary }}
+ }
+
+
+
+
+
+
+
+ `,
+ styleUrls: ['./list-item.component.scss']
+})
+export class ListItemComponent {
+ @Input() data!: ListItemData;
+ @Input() size: ListItemSize = 'md';
+ @Input() lines: ListItemLines = 'one';
+ @Input() variant: ListItemVariant = 'text';
+ @Input() interactive = true;
+ @Input() divider = false;
+
+ getItemClasses(): string {
+ const classes = [
+ `list-item--${this.size}`,
+ `list-item--${this.lines}-line`,
+ `list-item--${this.variant}`
+ ];
+
+ if (this.interactive) classes.push('list-item--interactive');
+ if (this.divider) classes.push('list-item--divider');
+ if (this.data.disabled) classes.push('list-item--disabled');
+ if (this.data.selected) classes.push('list-item--selected');
+
+ return classes.join(' ');
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/progress/index.ts b/projects/ui-essentials/src/lib/components/data-display/progress/index.ts
new file mode 100644
index 0000000..e4f9cc4
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/progress/index.ts
@@ -0,0 +1 @@
+export * from './progress-bar.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/progress/progress-bar.component.scss b/projects/ui-essentials/src/lib/components/data-display/progress/progress-bar.component.scss
new file mode 100644
index 0000000..845c7e9
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/progress/progress-bar.component.scss
@@ -0,0 +1,421 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+// Tokens available globally via main application styles
+
+// ==========================================================================
+// PROGRESS BAR COMPONENT STYLES
+// ==========================================================================
+// Modern progress bar implementation using design tokens
+// Follows Material Design 3 principles with semantic token system
+// Includes determinate, indeterminate, and buffer progress types
+// ==========================================================================
+
+.progress-bar-wrapper {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+
+ // Size variants
+ &.progress-bar-wrapper--sm {
+ gap: $semantic-spacing-component-xs;
+ }
+
+ &.progress-bar-wrapper--md {
+ gap: $semantic-spacing-component-sm;
+ }
+
+ &.progress-bar-wrapper--lg {
+ gap: $semantic-spacing-component-md;
+ }
+
+ // State variants
+ &.progress-bar-wrapper--disabled {
+ opacity: 0.6;
+ pointer-events: none;
+ }
+}
+
+.progress-bar {
+ position: relative;
+ display: block;
+ width: 100%;
+ background-color: $semantic-color-surface-secondary;
+ border-radius: $semantic-border-radius-full;
+ overflow: hidden;
+ transition: all $semantic-duration-fast $semantic-easing-standard;
+
+ // Size variants
+ &.progress-bar--sm {
+ height: 4px;
+ }
+
+ &.progress-bar--md {
+ height: 6px;
+ }
+
+ &.progress-bar--lg {
+ height: 8px;
+ }
+
+ // Disabled state
+ &.progress-bar--disabled {
+ background-color: $semantic-color-surface-disabled;
+ }
+}
+
+.progress-bar__fill {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ border-radius: inherit;
+ transition: width $semantic-duration-medium $semantic-easing-standard;
+
+ // Variant colors
+ .progress-bar-wrapper--primary & {
+ background-color: $semantic-color-brand-primary;
+ }
+
+ .progress-bar-wrapper--secondary & {
+ background-color: $semantic-color-text-secondary;
+ }
+
+ .progress-bar-wrapper--success & {
+ background-color: $semantic-color-success;
+ }
+
+ .progress-bar-wrapper--warning & {
+ background-color: $semantic-color-warning;
+ }
+
+ .progress-bar-wrapper--danger & {
+ background-color: $semantic-color-danger;
+ }
+
+ // Disabled state
+ &.progress-bar__fill--disabled {
+ background-color: $semantic-color-surface-disabled !important;
+ }
+
+ // Striped effect
+ &.progress-bar__fill--striped {
+ background-image: linear-gradient(
+ 45deg,
+ rgba(255, 255, 255, 0.15) 25%,
+ transparent 25%,
+ transparent 50%,
+ rgba(255, 255, 255, 0.15) 50%,
+ rgba(255, 255, 255, 0.15) 75%,
+ transparent 75%,
+ transparent
+ );
+ background-size: 1rem 1rem;
+ }
+
+ // Animated stripes
+ &.progress-bar__fill--animated {
+ animation: progress-bar-stripes 1s linear infinite;
+ }
+}
+
+// Indeterminate progress bar
+.progress-bar__fill--indeterminate {
+ width: 100%;
+
+ &:before {
+ content: '';
+ position: absolute;
+ background-color: inherit;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ will-change: left, right;
+ animation: indeterminate 2.1s cubic-bezier(0.650, 0.815, 0.735, 0.395) infinite;
+ }
+
+ &:after {
+ content: '';
+ position: absolute;
+ background-color: inherit;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ will-change: left, right;
+ animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.840, 0.440, 1.000) infinite;
+ animation-delay: 1.15s;
+ }
+}
+
+// Buffer progress bar
+.progress-bar__buffer {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ background-color: rgba($semantic-color-text-secondary, 0.3);
+ border-radius: inherit;
+ transition: width $semantic-duration-medium $semantic-easing-standard;
+
+ &.progress-bar__buffer--disabled {
+ background-color: $semantic-color-surface-disabled;
+ }
+}
+
+.progress-bar__fill--buffer {
+ z-index: 1;
+}
+
+// Label and percentage
+.progress-bar__label {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ color: $semantic-color-text-primary;
+ font-weight: $semantic-typography-font-weight-medium;
+ line-height: $semantic-typography-line-height-tight;
+
+ // Size variants
+ &.progress-bar__label--sm {
+ font-size: $semantic-typography-font-size-sm;
+ margin-bottom: $semantic-spacing-component-xs;
+ }
+
+ &.progress-bar__label--md {
+ font-size: $semantic-typography-font-size-md;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+
+ &.progress-bar__label--lg {
+ font-size: $semantic-typography-font-size-lg;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+
+ // Disabled state
+ &.progress-bar__label--disabled {
+ color: $semantic-color-text-disabled;
+ }
+}
+
+.progress-bar__label-text {
+ flex: 1;
+ margin-right: $semantic-spacing-component-sm;
+}
+
+.progress-bar__percentage {
+ font-weight: $semantic-typography-font-weight-bold;
+ color: $semantic-color-text-secondary;
+ font-variant-numeric: tabular-nums;
+
+ .progress-bar__label--disabled & {
+ color: $semantic-color-text-disabled;
+ }
+}
+
+.progress-bar__helper-text {
+ color: $semantic-color-text-secondary;
+ line-height: $semantic-typography-line-height-tight;
+
+ // Size variants
+ &.progress-bar__helper-text--sm {
+ font-size: $semantic-typography-font-size-xs;
+ margin-top: $semantic-spacing-component-xs;
+ }
+
+ &.progress-bar__helper-text--md {
+ font-size: $semantic-typography-font-size-sm;
+ margin-top: $semantic-spacing-component-xs;
+ }
+
+ &.progress-bar__helper-text--lg {
+ font-size: $semantic-typography-font-size-md;
+ margin-top: $semantic-spacing-component-sm;
+ }
+
+ // Disabled state
+ &.progress-bar__helper-text--disabled {
+ color: $semantic-color-text-disabled;
+ }
+}
+
+// ==========================================================================
+// ANIMATIONS
+// ==========================================================================
+
+// Striped animation
+@keyframes progress-bar-stripes {
+ 0% {
+ background-position-x: 1rem;
+ }
+ 100% {
+ background-position-x: 0;
+ }
+}
+
+// Indeterminate animations
+@keyframes indeterminate {
+ 0% {
+ left: -35%;
+ right: 100%;
+ }
+ 60% {
+ left: 100%;
+ right: -90%;
+ }
+ 100% {
+ left: 100%;
+ right: -90%;
+ }
+}
+
+@keyframes indeterminate-short {
+ 0% {
+ left: -200%;
+ right: 100%;
+ }
+ 60% {
+ left: 107%;
+ right: -8%;
+ }
+ 100% {
+ left: 107%;
+ right: -8%;
+ }
+}
+
+// Pulse animation for focus/hover states
+@keyframes progress-bar-pulse {
+ 0% {
+ box-shadow: 0 0 0 0 rgba($semantic-color-brand-primary, 0.4);
+ }
+ 70% {
+ box-shadow: 0 0 0 4px rgba($semantic-color-brand-primary, 0);
+ }
+ 100% {
+ box-shadow: 0 0 0 0 rgba($semantic-color-brand-primary, 0);
+ }
+}
+
+// ==========================================================================
+// HOVER AND FOCUS STATES
+// ==========================================================================
+
+.progress-bar:hover:not(.progress-bar--disabled) {
+ transform: scaleY(1.1);
+ box-shadow: $semantic-shadow-elevation-1;
+}
+
+.progress-bar:focus-visible {
+ outline: $semantic-border-width-2 solid $semantic-color-brand-primary;
+ outline-offset: 2px;
+}
+
+// ==========================================================================
+// RESPONSIVE DESIGN
+// ==========================================================================
+
+@media (max-width: $semantic-sizing-breakpoint-tablet - 1) {
+ .progress-bar-wrapper {
+ // Slightly larger progress bars on mobile for better visibility
+ &.progress-bar-wrapper--sm {
+ .progress-bar--sm {
+ height: 5px;
+ }
+ }
+
+ &.progress-bar-wrapper--md {
+ .progress-bar--md {
+ height: 7px;
+ }
+ }
+
+ &.progress-bar-wrapper--lg {
+ .progress-bar--lg {
+ height: 9px;
+ }
+ }
+ }
+}
+
+// ==========================================================================
+// ACCESSIBILITY ENHANCEMENTS
+// ==========================================================================
+
+// Reduced motion preference
+@media (prefers-reduced-motion: reduce) {
+ .progress-bar__fill,
+ .progress-bar__buffer {
+ transition: none !important;
+ }
+
+ .progress-bar__fill--animated,
+ .progress-bar__fill--indeterminate:before,
+ .progress-bar__fill--indeterminate:after {
+ animation: none !important;
+ }
+}
+
+// High contrast mode
+@media (prefers-contrast: high) {
+ .progress-bar {
+ border: $semantic-border-width-1 solid $semantic-color-text-primary;
+ }
+
+ .progress-bar__fill {
+ border: $semantic-border-width-1 solid $semantic-color-text-primary;
+ border-radius: 0;
+ }
+}
+
+// ==========================================================================
+// THEME VARIANTS
+// ==========================================================================
+
+// Dark theme adjustments
+@media (prefers-color-scheme: dark) {
+ .progress-bar {
+ background-color: rgba($semantic-color-surface-primary, 0.1);
+ }
+
+ .progress-bar__buffer {
+ background-color: rgba($semantic-color-text-secondary, 0.2);
+ }
+}
+
+// ==========================================================================
+// SPECIAL STATES AND EFFECTS
+// ==========================================================================
+
+// Success completion effect
+.progress-bar-wrapper--success {
+ .progress-bar__fill[style*="100%"] {
+ animation: progress-complete 0.6s $semantic-easing-spring;
+ }
+}
+
+@keyframes progress-complete {
+ 0% {
+ transform: scaleX(1);
+ }
+ 50% {
+ transform: scaleX(1.02);
+ }
+ 100% {
+ transform: scaleX(1);
+ }
+}
+
+// Error state pulsing
+.progress-bar-wrapper--danger {
+ .progress-bar__fill {
+ animation: progress-error 2s ease-in-out infinite;
+ }
+}
+
+@keyframes progress-error {
+ 0%, 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.8;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/progress/progress-bar.component.ts b/projects/ui-essentials/src/lib/components/data-display/progress/progress-bar.component.ts
new file mode 100644
index 0000000..8305f1f
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/progress/progress-bar.component.ts
@@ -0,0 +1,213 @@
+import { Component, Input, ChangeDetectionStrategy, signal, computed, ViewChild, ElementRef } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+export type ProgressBarSize = 'sm' | 'md' | 'lg';
+export type ProgressBarVariant = 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
+export type ProgressBarType = 'determinate' | 'indeterminate' | 'buffer';
+export type ProgressBarState = 'default' | 'disabled';
+
+@Component({
+ selector: 'ui-progress-bar',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [CommonModule],
+ template: `
+
+
+ @if (showLabelComputed()) {
+
+ {{ label }}
+ @if (showPercentageComputed() && type() === 'determinate') {
+ {{ Math.round(value()) }}%
+ }
+
+ }
+
+
+
+ @if (type() === 'determinate') {
+
+
+ }
+
+ @if (type() === 'indeterminate') {
+
+
+ }
+
+ @if (type() === 'buffer') {
+
+
+
+
+ }
+
+
+
+ @if (helperTextComputed()) {
+
+ {{ helperTextComputed() }}
+
+ }
+
+
+ `,
+ styleUrls: ['./progress-bar.component.scss']
+})
+export class ProgressBarComponent {
+ @ViewChild('progressElement', { static: false }) progressElement!: ElementRef;
+
+ // Core inputs
+ @Input() set progress(value: number) { this._value.set(Math.max(0, Math.min(100, value))); }
+ @Input() set buffer(value: number) { this._bufferValue.set(Math.max(0, Math.min(100, value))); }
+ @Input() label: string = '';
+ @Input() size: ProgressBarSize = 'md';
+ @Input() variant: ProgressBarVariant = 'primary';
+ @Input() progressType: ProgressBarType = 'determinate';
+ @Input() state: ProgressBarState = 'default';
+
+ // Display options
+ @Input() showLabel = true;
+ @Input() showPercentage = true;
+ @Input() striped = false;
+ @Input() animated = false;
+
+ // Accessibility
+ @Input() ariaLabel: string = '';
+ @Input() helperText: string = '';
+ @Input() progressId: string = `progress-${Math.random().toString(36).substr(2, 9)}`;
+
+ // Exposed Math for template
+ protected readonly Math = Math;
+
+ // Internal state
+ private _value = signal(0);
+ private _bufferValue = signal(0);
+
+ // Computed properties
+ protected value = computed(() => this._value());
+ protected bufferValue = computed(() => this._bufferValue());
+ protected type = computed(() => this.progressType);
+ protected showLabelComputed = computed(() => this.label && this.showLabel);
+ protected showPercentageComputed = computed(() => this.showPercentage && this.progressType === 'determinate');
+ protected helperTextComputed = computed(() => this.helperText);
+
+ ngOnInit(): void {
+ // Initialize with default values if not set
+ if (this._value() === 0 && this.progressType === 'determinate') {
+ this._value.set(0);
+ }
+ }
+
+ getAriaValueText(): string {
+ if (this.progressType === 'determinate') {
+ return `${Math.round(this.value())}% complete`;
+ } else if (this.progressType === 'indeterminate') {
+ return 'Loading...';
+ } else {
+ return `${Math.round(this.value())}% of ${Math.round(this.bufferValue())}%`;
+ }
+ }
+
+ // CSS class getters
+ getWrapperClasses(): string {
+ const classes = [
+ `progress-bar-wrapper--${this.size}`,
+ `progress-bar-wrapper--${this.variant}`,
+ `progress-bar-wrapper--${this.progressType}`,
+ `progress-bar-wrapper--${this.state}`
+ ];
+
+ if (this.state === 'disabled') classes.push('progress-bar-wrapper--disabled');
+ if (this.striped) classes.push('progress-bar-wrapper--striped');
+ if (this.animated) classes.push('progress-bar-wrapper--animated');
+
+ return classes.join(' ');
+ }
+
+ getProgressBarClasses(): string {
+ const classes = [
+ `progress-bar--${this.size}`,
+ `progress-bar--${this.variant}`,
+ `progress-bar--${this.progressType}`,
+ `progress-bar--${this.state}`
+ ];
+
+ if (this.state === 'disabled') classes.push('progress-bar--disabled');
+
+ return classes.join(' ');
+ }
+
+ getFillClasses(): string {
+ const classes = [`progress-bar__fill--${this.size}`];
+
+ if (this.striped) classes.push('progress-bar__fill--striped');
+ if (this.animated) classes.push('progress-bar__fill--animated');
+ if (this.state === 'disabled') classes.push('progress-bar__fill--disabled');
+
+ return classes.join(' ');
+ }
+
+ getBufferClasses(): string {
+ const classes = [`progress-bar__buffer--${this.size}`];
+
+ if (this.state === 'disabled') classes.push('progress-bar__buffer--disabled');
+
+ return classes.join(' ');
+ }
+
+ getLabelClasses(): string {
+ const classes = [`progress-bar__label--${this.size}`];
+
+ if (this.state === 'disabled') classes.push('progress-bar__label--disabled');
+
+ return classes.join(' ');
+ }
+
+ getHelperTextClasses(): string {
+ const classes = [`progress-bar__helper-text--${this.size}`];
+
+ if (this.state === 'disabled') classes.push('progress-bar__helper-text--disabled');
+
+ return classes.join(' ');
+ }
+
+ // Public methods for programmatic control
+ setProgress(value: number): void {
+ this._value.set(Math.max(0, Math.min(100, value)));
+ }
+
+ setBuffer(value: number): void {
+ this._bufferValue.set(Math.max(0, Math.min(100, value)));
+ }
+
+ reset(): void {
+ this._value.set(0);
+ this._bufferValue.set(0);
+ }
+
+ complete(): void {
+ this._value.set(100);
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/table-actions.component.scss b/projects/ui-essentials/src/lib/components/data-display/table-actions.component.scss
new file mode 100644
index 0000000..3568e99
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/table-actions.component.scss
@@ -0,0 +1,400 @@
+@use "../../../../../shared-ui/src/styles/semantic/index" as *;
+.ui-table-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+ position: relative;
+
+ // Size variants
+ &--small {
+ gap: 0.125rem;
+ }
+
+ &--medium {
+ gap: 0.25rem;
+ }
+
+ &--large {
+ gap: 0.375rem;
+ }
+
+ // Layout modifiers
+ &--compact {
+ gap: 0.125rem;
+
+ .ui-table-action {
+ min-width: auto;
+ }
+ }
+
+ &--with-labels {
+ gap: 0.5rem;
+ }
+}
+
+.ui-table-action {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.25rem;
+ padding: 0.375rem;
+ border: 1px solid transparent;
+ border-radius: 0.25rem;
+ background: transparent;
+ color: hsl(287, 12%, 47%);
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+ font-size: 0.875rem;
+ font-weight: 500;
+ line-height: 1;
+ cursor: pointer;
+ transition: all 150ms cubic-bezier(0, 0, 0.2, 1);
+ min-width: 2rem;
+ min-height: 2rem;
+ position: relative;
+ user-select: none;
+
+ &:hover:not(:disabled) {
+ background: hsl(289, 14%, 96%);
+ color: hsl(279, 14%, 11%);
+ transform: scale(1.05);
+ }
+
+ &:active:not(:disabled) {
+ transform: scale(0.98);
+ }
+
+ &:focus-visible {
+ outline: 2px solid hsl(258, 100%, 47%);
+ outline-offset: 1px;
+ }
+
+ // Action variants
+ &--edit {
+ color: hsl(207, 90%, 45%);
+
+ &:hover:not(:disabled) {
+ background: hsl(207, 90%, 95%);
+ color: hsl(207, 90%, 35%);
+ border-color: hsl(207, 90%, 80%);
+ }
+ }
+
+ &--delete {
+ color: hsl(0, 84%, 45%);
+
+ &:hover:not(:disabled) {
+ background: hsl(0, 84%, 95%);
+ color: hsl(0, 84%, 35%);
+ border-color: hsl(0, 84%, 80%);
+ }
+ }
+
+ &--view {
+ color: hsl(142, 76%, 45%);
+
+ &:hover:not(:disabled) {
+ background: hsl(142, 76%, 95%);
+ color: hsl(142, 76%, 35%);
+ border-color: hsl(142, 76%, 80%);
+ }
+ }
+
+ &--download {
+ color: hsl(45, 93%, 45%);
+
+ &:hover:not(:disabled) {
+ background: hsl(45, 93%, 95%);
+ color: hsl(45, 93%, 35%);
+ border-color: hsl(45, 93%, 80%);
+ }
+ }
+
+ &--custom {
+ color: hsl(258, 100%, 47%);
+
+ &:hover:not(:disabled) {
+ background: hsl(263, 100%, 95%);
+ color: hsl(258, 100%, 35%);
+ border-color: hsl(263, 100%, 80%);
+ }
+ }
+
+ // Size variants
+ .ui-table-actions--small & {
+ padding: 0.25rem;
+ min-width: 1.5rem;
+ min-height: 1.5rem;
+ font-size: 0.75rem;
+
+ .ui-table-action__icon {
+ font-size: 0.75rem;
+ }
+ }
+
+ .ui-table-actions--large & {
+ padding: 0.5rem;
+ min-width: 2.5rem;
+ min-height: 2.5rem;
+ font-size: 1rem;
+
+ .ui-table-action__icon {
+ font-size: 1rem;
+ }
+ }
+
+ // States
+ &--disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+ pointer-events: none;
+ }
+
+ &--active {
+ background: hsl(289, 14%, 90%);
+ color: hsl(279, 14%, 11%);
+ border-color: hsl(289, 14%, 80%);
+ }
+
+ // More button specific styles
+ &--more {
+ color: hsl(287, 12%, 47%);
+
+ &:hover:not(:disabled) {
+ background: hsl(289, 14%, 90%);
+ color: hsl(279, 14%, 11%);
+ }
+
+ &.ui-table-action--active {
+ background: hsl(258, 100%, 95%);
+ color: hsl(258, 100%, 47%);
+ border-color: hsl(258, 100%, 80%);
+ }
+ }
+}
+
+.ui-table-action__icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0.875rem;
+ line-height: 1;
+ flex-shrink: 0;
+
+ // Icon-only adjustments
+ .ui-table-actions:not(.ui-table-actions--with-labels) & {
+ margin: 0;
+ }
+}
+
+.ui-table-action__label {
+ font-size: 0.8125rem;
+ white-space: nowrap;
+ flex-shrink: 0;
+
+ // Hide labels by default, show when --with-labels
+ .ui-table-actions:not(.ui-table-actions--with-labels) & {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+ }
+}
+
+// Dropdown styles
+.ui-table-actions__dropdown {
+ position: relative;
+ display: inline-flex;
+}
+
+.ui-table-actions__dropdown-menu {
+ position: absolute;
+ top: calc(100% + 0.25rem);
+ right: 0;
+ min-width: 10rem;
+ background: hsl(286, 20%, 99%);
+ border: 1px solid hsl(289, 14%, 80%);
+ border-radius: 0.375rem;
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
+ padding: 0.25rem;
+ z-index: 50;
+ animation: dropdown-appear 150ms cubic-bezier(0, 0, 0.2, 1);
+
+ // Ensure dropdown is on top
+ &::before {
+ content: '';
+ position: absolute;
+ top: -0.25rem;
+ right: 0.75rem;
+ width: 0;
+ height: 0;
+ border-left: 0.25rem solid transparent;
+ border-right: 0.25rem solid transparent;
+ border-bottom: 0.25rem solid hsl(289, 14%, 80%);
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: -0.1875rem;
+ right: 0.8125rem;
+ width: 0;
+ height: 0;
+ border-left: 0.1875rem solid transparent;
+ border-right: 0.1875rem solid transparent;
+ border-bottom: 0.1875rem solid hsl(286, 20%, 99%);
+ }
+}
+
+.ui-table-actions__dropdown-item {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ width: 100%;
+ padding: 0.5rem 0.75rem;
+ border: none;
+ border-radius: 0.25rem;
+ background: transparent;
+ color: hsl(279, 14%, 11%);
+ font-family: inherit;
+ font-size: 0.875rem;
+ font-weight: 500;
+ text-align: left;
+ cursor: pointer;
+ transition: background-color 150ms ease;
+
+ &:hover:not(:disabled) {
+ background: hsl(289, 14%, 96%);
+ }
+
+ &:focus-visible {
+ outline: 2px solid hsl(258, 100%, 47%);
+ outline-offset: -2px;
+ }
+
+ &:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+ }
+
+ .ui-table-action__icon {
+ font-size: 0.875rem;
+ }
+
+ .ui-table-action__label {
+ position: static;
+ width: auto;
+ height: auto;
+ padding: 0;
+ margin: 0;
+ overflow: visible;
+ clip: auto;
+ white-space: normal;
+ border: none;
+ }
+}
+
+// Animation for dropdown
+@keyframes dropdown-appear {
+ 0% {
+ opacity: 0;
+ transform: translateY(-0.25rem) scale(0.98);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+}
+
+// Tooltip support (when labels are hidden)
+.ui-table-actions:not(.ui-table-actions--with-labels) {
+ .ui-table-action {
+ position: relative;
+
+ &[title]:hover::after {
+ content: attr(title);
+ position: absolute;
+ bottom: calc(100% + 0.5rem);
+ left: 50%;
+ transform: translateX(-50%);
+ padding: 0.375rem 0.5rem;
+ background: hsl(279, 14%, 11%);
+ color: white;
+ font-size: 0.75rem;
+ white-space: nowrap;
+ border-radius: 0.25rem;
+ z-index: 100;
+ animation: tooltip-appear 150ms ease-out;
+ }
+
+ &[title]:hover::before {
+ content: '';
+ position: absolute;
+ bottom: calc(100% + 0.125rem);
+ left: 50%;
+ transform: translateX(-50%);
+ width: 0;
+ height: 0;
+ border-left: 0.25rem solid transparent;
+ border-right: 0.25rem solid transparent;
+ border-top: 0.25rem solid hsl(279, 14%, 11%);
+ z-index: 100;
+ }
+ }
+}
+
+@keyframes tooltip-appear {
+ 0% {
+ opacity: 0;
+ transform: translateX(-50%) translateY(0.125rem);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateX(-50%) translateY(0);
+ }
+}
+
+// Responsive design
+@media (max-width: 640px) {
+ .ui-table-actions {
+ &--with-labels .ui-table-action__label {
+ display: none;
+ }
+
+ &--with-labels {
+ gap: 0.25rem;
+ }
+ }
+}
+
+// High contrast mode
+@media (prefers-contrast: high) {
+ .ui-table-action {
+ border: 1px solid;
+
+ &:hover:not(:disabled) {
+ border-width: 2px;
+ }
+ }
+
+ .ui-table-actions__dropdown-menu {
+ border-width: 2px;
+ }
+}
+
+// Reduced motion
+@media (prefers-reduced-motion: reduce) {
+ .ui-table-action,
+ .ui-table-actions__dropdown-menu,
+ .ui-table-actions__dropdown-item {
+ transition: none;
+ }
+
+ .ui-table-actions__dropdown-menu {
+ animation: none;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/table-actions.component.ts b/projects/ui-essentials/src/lib/components/data-display/table-actions.component.ts
new file mode 100644
index 0000000..4a3f38a
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/table-actions.component.ts
@@ -0,0 +1,177 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+export type ActionVariant = 'edit' | 'delete' | 'view' | 'download' | 'custom';
+export type ActionSize = 'small' | 'medium' | 'large';
+
+export interface TableAction {
+ id: string;
+ label: string;
+ variant: ActionVariant;
+ icon?: string;
+ disabled?: boolean;
+ hidden?: boolean;
+ tooltip?: string;
+ confirmMessage?: string;
+}
+
+@Component({
+ selector: 'ui-table-actions',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+ @for (action of visibleActions; track action.id) {
+
+ @if (action.icon) {
+
+ }
+ @if (showLabels) {
+ {{ action.label }}
+ }
+
+ }
+
+ @if (hasMoreActions) {
+
+
+ ⋯
+ @if (showLabels) {
+ More
+ }
+
+
+ @if (dropdownOpen) {
+
+ }
+
+ }
+
+ `,
+ styleUrl: './table-actions.component.scss'
+})
+export class TableActionsComponent {
+ @Input() actions: TableAction[] = [];
+ @Input() size: ActionSize = 'medium';
+ @Input() maxVisibleActions: number = 3;
+ @Input() showLabels: boolean = false;
+ @Input() compact: boolean = false;
+ @Input() data: any = null; // Row data passed to action handlers
+ @Input() index: number = -1; // Row index
+ @Input() class: string = '';
+
+ @Output() actionClick = new EventEmitter<{action: TableAction, data: any, index: number, event: Event}>();
+
+ dropdownOpen = false;
+
+ get visibleActions(): TableAction[] {
+ const filtered = this.actions.filter(action => !action.hidden);
+ return filtered.slice(0, this.maxVisibleActions);
+ }
+
+ get hiddenActions(): TableAction[] {
+ const filtered = this.actions.filter(action => !action.hidden);
+ return filtered.slice(this.maxVisibleActions);
+ }
+
+ get hasMoreActions(): boolean {
+ return this.hiddenActions.length > 0;
+ }
+
+ get containerClasses(): string {
+ return [
+ 'ui-table-actions',
+ `ui-table-actions--${this.size}`,
+ this.compact ? 'ui-table-actions--compact' : '',
+ this.showLabels ? 'ui-table-actions--with-labels' : '',
+ this.class
+ ].filter(Boolean).join(' ');
+ }
+
+ getActionClasses(action: TableAction): string {
+ return [
+ 'ui-table-action',
+ `ui-table-action--${action.variant}`,
+ action.disabled ? 'ui-table-action--disabled' : '',
+ ].filter(Boolean).join(' ');
+ }
+
+ getMoreButtonClasses(): string {
+ return [
+ `ui-table-action--${this.size}`,
+ this.dropdownOpen ? 'ui-table-action--active' : ''
+ ].filter(Boolean).join(' ');
+ }
+
+ handleActionClick(action: TableAction, event: Event): void {
+ if (action.disabled) {
+ return;
+ }
+
+ // Close dropdown if it's open
+ this.dropdownOpen = false;
+
+ // Handle confirmation if required
+ if (action.confirmMessage) {
+ if (!confirm(action.confirmMessage)) {
+ return;
+ }
+ }
+
+ this.actionClick.emit({
+ action,
+ data: this.data,
+ index: this.index,
+ event
+ });
+ }
+
+ toggleDropdown(): void {
+ this.dropdownOpen = !this.dropdownOpen;
+
+ if (this.dropdownOpen) {
+ // Close dropdown when clicking outside
+ setTimeout(() => {
+ const handleClickOutside = (event: Event) => {
+ const target = event.target as Element;
+ if (!target.closest('.ui-table-actions__dropdown')) {
+ this.dropdownOpen = false;
+ document.removeEventListener('click', handleClickOutside);
+ }
+ };
+ document.addEventListener('click', handleClickOutside);
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/table/index.ts b/projects/ui-essentials/src/lib/components/data-display/table/index.ts
new file mode 100644
index 0000000..42326e9
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/table/index.ts
@@ -0,0 +1,2 @@
+export * from './table.component';
+export * from '../table-actions.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/table/table.component.scss b/projects/ui-essentials/src/lib/components/data-display/table/table.component.scss
new file mode 100644
index 0000000..743202c
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/table/table.component.scss
@@ -0,0 +1,441 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+// Modern table component using design tokens
+.ui-table-container {
+ position: relative;
+ width: 100%;
+ overflow: auto;
+ background: hsl(286, 20%, 99%); // Surface color
+ border-radius: 0.5rem;
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
+
+ // Sticky header container
+ &--sticky-header {
+ max-height: 400px;
+
+ .ui-table__header {
+ position: sticky;
+ top: 0;
+ z-index: 10;
+ background: hsl(285, 9%, 87%); // Surface dim
+ }
+ }
+
+ // Max height container
+ &--max-height {
+ max-height: var(--ui-table-max-height, 500px);
+ }
+
+ // Loading state
+ &--loading {
+ pointer-events: none;
+ }
+}
+
+.ui-table {
+ width: 100%;
+ border-collapse: separate;
+ border-spacing: 0;
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+ position: relative;
+
+ // Subtle noise texture for modern brutalist feel
+ &::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: url('data:image/svg+xml;utf8, ');
+ pointer-events: none;
+ mix-blend-mode: overlay;
+ border-radius: inherit;
+ z-index: 1;
+ }
+
+ // Size variants
+ &--compact {
+ .ui-table__header-cell,
+ .ui-table__body-cell {
+ padding: 0.5rem 0.75rem;
+ font-size: 0.875rem;
+ }
+ }
+
+ &--default {
+ .ui-table__header-cell,
+ .ui-table__body-cell {
+ padding: 0.75rem 1rem;
+ font-size: 0.875rem;
+ }
+ }
+
+ &--comfortable {
+ .ui-table__header-cell,
+ .ui-table__body-cell {
+ padding: 1rem 1.25rem;
+ font-size: 1rem;
+ }
+ }
+
+ // Style variants
+ &--bordered {
+ border: 1px solid hsl(289, 14%, 80%);
+
+ .ui-table__header-cell,
+ .ui-table__body-cell {
+ border-right: 1px solid hsl(289, 14%, 80%);
+
+ &:last-child {
+ border-right: none;
+ }
+ }
+
+ .ui-table__body-row {
+ border-bottom: 1px solid hsl(289, 14%, 80%);
+ }
+ }
+
+ &--minimal {
+ .ui-table__header {
+ border-bottom: 2px solid hsl(258, 100%, 47%);
+ }
+
+ .ui-table__body-row {
+ border-bottom: none;
+
+ &:not(:last-child) {
+ border-bottom: 1px solid hsl(289, 14%, 90%);
+ }
+ }
+ }
+
+ // Hover effects
+ &--hoverable {
+ .ui-table__body-row {
+ transition: background-color 150ms cubic-bezier(0, 0, 0.2, 1);
+ cursor: pointer;
+
+ &:hover {
+ background: hsl(289, 14%, 96%);
+ position: relative;
+ z-index: 2;
+ }
+ }
+ }
+
+ // Selection
+ &--selectable {
+ .ui-table__body-row {
+ cursor: pointer;
+
+ &--selected {
+ background: hsl(263, 100%, 95%);
+ border-left: 3px solid hsl(258, 100%, 47%);
+ }
+ }
+ }
+}
+
+// Header styles
+.ui-table__header {
+ background: hsl(285, 9%, 87%);
+ border-bottom: 2px solid hsl(287, 12%, 59%);
+ position: relative;
+ z-index: 5;
+}
+
+.ui-table__header-row {
+ // No additional styles needed
+}
+
+.ui-table__header-cell {
+ text-align: left;
+ font-weight: 700;
+ font-size: 0.75rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: hsl(279, 14%, 11%);
+ position: relative;
+ white-space: nowrap;
+ user-select: none;
+
+ // Alignment variants
+ &--left {
+ text-align: left;
+ }
+
+ &--center {
+ text-align: center;
+ }
+
+ &--right {
+ text-align: right;
+ }
+
+ // Sortable headers
+ &--sortable {
+ cursor: pointer;
+ transition: background-color 150ms ease;
+
+ &:hover {
+ background: hsl(285, 9%, 82%);
+ }
+
+ &--sorted {
+ color: hsl(258, 100%, 47%);
+ background: hsl(263, 100%, 95%);
+ }
+ }
+
+ // Sticky columns
+ &--sticky {
+ position: sticky;
+ left: 0;
+ background: inherit;
+ z-index: 6;
+ box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1);
+ }
+}
+
+.ui-table__header-content {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ min-height: 1.5rem;
+}
+
+.ui-table__header-text {
+ flex: 1;
+}
+
+.ui-table__sort-indicator {
+ font-size: 0.875rem;
+ color: hsl(258, 100%, 47%);
+ font-weight: bold;
+ opacity: 0.8;
+
+ &[data-direction="asc"] {
+ opacity: 1;
+ }
+
+ &[data-direction="desc"] {
+ opacity: 1;
+ }
+}
+
+// Body styles
+.ui-table__body {
+ position: relative;
+ z-index: 2;
+}
+
+.ui-table__body-row {
+ transition: all 150ms cubic-bezier(0, 0, 0.2, 1);
+ border-bottom: 1px solid hsl(289, 14%, 90%);
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ // Striped variant
+ &--striped {
+ background: hsl(289, 14%, 98%);
+ }
+
+ // Selection state
+ &--selected {
+ background: hsl(263, 100%, 95%);
+ box-shadow: inset 3px 0 0 hsl(258, 100%, 47%);
+ }
+}
+
+.ui-table__body-cell {
+ color: hsl(279, 14%, 11%);
+ font-weight: 400;
+ line-height: 1.5;
+ position: relative;
+
+ // Alignment variants
+ &--left {
+ text-align: left;
+ }
+
+ &--center {
+ text-align: center;
+ }
+
+ &--right {
+ text-align: right;
+ }
+
+ // Sticky columns
+ &--sticky {
+ position: sticky;
+ left: 0;
+ background: inherit;
+ z-index: 3;
+ box-shadow: 2px 0 4px rgba(0, 0, 0, 0.05);
+
+ .ui-table__body-row:hover & {
+ background: hsl(289, 14%, 96%);
+ }
+
+ .ui-table__body-row--selected & {
+ background: hsl(263, 100%, 95%);
+ }
+
+ .ui-table__body-row--striped & {
+ background: hsl(289, 14%, 98%);
+ }
+ }
+}
+
+.ui-table__cell-content {
+ display: block;
+ width: 100%;
+ word-break: break-word;
+}
+
+// Empty state
+.ui-table__empty-row {
+ background: transparent;
+
+ &:hover {
+ background: transparent !important;
+ }
+}
+
+.ui-table__empty-cell {
+ padding: 3rem 1rem;
+ text-align: center;
+ border-bottom: none;
+}
+
+.ui-table__empty-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1rem;
+}
+
+.ui-table__empty-text {
+ color: hsl(287, 12%, 47%);
+ font-size: 1rem;
+ font-weight: 500;
+}
+
+// Loading overlay
+.ui-table__loading-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(255, 255, 255, 0.9);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 1rem;
+ z-index: 20;
+ backdrop-filter: blur(2px);
+}
+
+.ui-table__loading-spinner {
+ width: 2rem;
+ height: 2rem;
+ border: 3px solid hsl(289, 14%, 80%);
+ border-top: 3px solid hsl(258, 100%, 47%);
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+.ui-table__loading-text {
+ color: hsl(287, 12%, 47%);
+ font-size: 0.875rem;
+ font-weight: 500;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+// Responsive design
+@media (max-width: 768px) {
+ .ui-table-container {
+ border-radius: 0;
+ margin: 0 -1rem;
+ }
+
+ .ui-table {
+ &--compact {
+ .ui-table__header-cell,
+ .ui-table__body-cell {
+ padding: 0.375rem 0.5rem;
+ font-size: 0.8125rem;
+ }
+ }
+
+ &--default {
+ .ui-table__header-cell,
+ .ui-table__body-cell {
+ padding: 0.5rem 0.625rem;
+ font-size: 0.8125rem;
+ }
+ }
+ }
+
+ .ui-table__header-cell,
+ .ui-table__body-cell {
+ &:first-child {
+ padding-left: 1rem;
+ }
+
+ &:last-child {
+ padding-right: 1rem;
+ }
+ }
+}
+
+// Focus management
+.ui-table__header-cell--sortable:focus-visible {
+ outline: 2px solid hsl(258, 100%, 47%);
+ outline-offset: -2px;
+}
+
+// Print styles
+@media print {
+ .ui-table-container {
+ box-shadow: none;
+ border: 1px solid #000;
+ }
+
+ .ui-table {
+ &::before {
+ display: none;
+ }
+ }
+
+ .ui-table__loading-overlay {
+ display: none;
+ }
+}
+
+// High contrast mode
+@media (prefers-contrast: high) {
+ .ui-table {
+ border: 2px solid;
+ }
+
+ .ui-table__header-cell,
+ .ui-table__body-cell {
+ border: 1px solid;
+ }
+
+ .ui-table__body-row:hover {
+ background: highlight;
+ color: highlighttext;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/table/table.component.ts b/projects/ui-essentials/src/lib/components/data-display/table/table.component.ts
new file mode 100644
index 0000000..9f0131d
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/table/table.component.ts
@@ -0,0 +1,246 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ContentChild, TemplateRef } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+export type TableVariant = 'default' | 'striped' | 'bordered' | 'minimal';
+export type TableSize = 'compact' | 'default' | 'comfortable';
+export type TableColumnAlign = 'left' | 'center' | 'right';
+
+export interface TableColumn {
+ key: string;
+ label: string;
+ sortable?: boolean;
+ align?: TableColumnAlign;
+ width?: string;
+ sticky?: boolean;
+}
+
+export interface TableSortEvent {
+ column: string;
+ direction: 'asc' | 'desc' | null;
+}
+
+@Component({
+ selector: 'ui-table',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
+
+ @if (showHeader && columns.length > 0) {
+
+ }
+
+
+
+ @if (data.length === 0 && showEmptyState) {
+
+
+
+ @if (emptyTemplate) {
+
+ } @else {
+
{{ emptyMessage }}
+ }
+
+
+
+ } @else {
+ @for (row of data; track trackByFn ? trackByFn($index, row) : $index; let rowIndex = $index) {
+
+ @for (column of columns; track column.key) {
+
+ @if (getCellTemplate(column.key)) {
+
+ } @else {
+
+ {{ getCellValue(row, column.key) }}
+
+ }
+
+ }
+
+ }
+ }
+
+
+
+
+ @if (loading) {
+
+
+
{{ loadingMessage }}
+
+ }
+
+ `,
+ styleUrl: './table.component.scss'
+})
+export class TableComponent {
+ @Input() data: any[] = [];
+ @Input() columns: TableColumn[] = [];
+ @Input() variant: TableVariant = 'default';
+ @Input() size: TableSize = 'default';
+ @Input() showHeader: boolean = true;
+ @Input() showEmptyState: boolean = true;
+ @Input() emptyMessage: string = 'No data available';
+ @Input() loading: boolean = false;
+ @Input() loadingMessage: string = 'Loading...';
+ @Input() hoverable: boolean = true;
+ @Input() selectable: boolean = false;
+ @Input() stickyHeader: boolean = false;
+ @Input() maxHeight: string = '';
+ @Input() sortColumn: string = '';
+ @Input() sortDirection: 'asc' | 'desc' | null = null;
+ @Input() trackByFn?: (index: number, item: any) => any;
+ @Input() cellTemplates: Record> = {};
+ @Input() class: string = '';
+
+ @Output() rowClick = new EventEmitter<{row: any, index: number}>();
+ @Output() sort = new EventEmitter();
+ @Output() selectionChange = new EventEmitter();
+
+ @ContentChild('emptyTemplate') emptyTemplate?: TemplateRef;
+
+ selectedRows: Set = new Set();
+
+ get tableContainerClasses(): string {
+ return [
+ 'ui-table-container',
+ this.stickyHeader ? 'ui-table-container--sticky-header' : '',
+ this.maxHeight ? 'ui-table-container--max-height' : '',
+ this.loading ? 'ui-table-container--loading' : '',
+ this.class
+ ].filter(Boolean).join(' ');
+ }
+
+ get tableClasses(): string {
+ return [
+ 'ui-table',
+ `ui-table--${this.variant}`,
+ `ui-table--${this.size}`,
+ this.hoverable ? 'ui-table--hoverable' : '',
+ this.selectable ? 'ui-table--selectable' : '',
+ ].filter(Boolean).join(' ');
+ }
+
+ getHeaderCellClasses(column: TableColumn): string {
+ return [
+ 'ui-table__header-cell',
+ `ui-table__header-cell--${column.align || 'left'}`,
+ column.sortable ? 'ui-table__header-cell--sortable' : '',
+ column.sticky ? 'ui-table__header-cell--sticky' : '',
+ this.sortColumn === column.key ? 'ui-table__header-cell--sorted' : ''
+ ].filter(Boolean).join(' ');
+ }
+
+ getBodyCellClasses(column: TableColumn, row: any): string {
+ return [
+ 'ui-table__body-cell',
+ `ui-table__body-cell--${column.align || 'left'}`,
+ column.sticky ? 'ui-table__body-cell--sticky' : ''
+ ].filter(Boolean).join(' ');
+ }
+
+ getRowClasses(row: any, index: number): string {
+ return [
+ 'ui-table__body-row',
+ this.selectedRows.has(row) ? 'ui-table__body-row--selected' : '',
+ index % 2 === 1 && this.variant === 'striped' ? 'ui-table__body-row--striped' : ''
+ ].filter(Boolean).join(' ');
+ }
+
+ handleSort(column: TableColumn): void {
+ if (!column.sortable) return;
+
+ let newDirection: 'asc' | 'desc' | null;
+
+ if (this.sortColumn === column.key) {
+ // Toggle through: asc -> desc -> null -> asc
+ if (this.sortDirection === 'asc') {
+ newDirection = 'desc';
+ } else if (this.sortDirection === 'desc') {
+ newDirection = null;
+ } else {
+ newDirection = 'asc';
+ }
+ } else {
+ newDirection = 'asc';
+ }
+
+ this.sort.emit({
+ column: column.key,
+ direction: newDirection
+ });
+ }
+
+ handleRowClick(row: any, index: number): void {
+ if (this.selectable) {
+ this.toggleRowSelection(row);
+ }
+
+ this.rowClick.emit({ row, index });
+ }
+
+ toggleRowSelection(row: any): void {
+ if (this.selectedRows.has(row)) {
+ this.selectedRows.delete(row);
+ } else {
+ this.selectedRows.add(row);
+ }
+
+ this.selectionChange.emit(Array.from(this.selectedRows));
+ }
+
+ getCellValue(row: any, key: string): any {
+ return key.split('.').reduce((obj, prop) => obj?.[prop], row) ?? '';
+ }
+
+ getCellTemplate(columnKey: string): TemplateRef | null {
+ return this.cellTemplates[columnKey] || null;
+ }
+
+ getSortAttribute(columnKey: string): string | null {
+ if (this.sortColumn === columnKey) {
+ return this.sortDirection === 'asc' ? 'ascending' : 'descending';
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/feedback/empty-state/empty-state.component.scss b/projects/ui-essentials/src/lib/components/feedback/empty-state/empty-state.component.scss
new file mode 100644
index 0000000..e49b2a7
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/feedback/empty-state/empty-state.component.scss
@@ -0,0 +1,309 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+.ui-empty-state {
+ // Core Structure
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ position: relative;
+
+ // Layout & Spacing
+ padding: $semantic-spacing-component-lg;
+ min-height: 200px;
+
+ // Visual Design
+ background: $semantic-color-surface-primary;
+ border-radius: $semantic-border-card-radius;
+
+ // Typography
+ color: $semantic-color-text-secondary;
+
+ // Sizing Variants
+ &--sm {
+ min-height: 120px;
+ padding: $semantic-spacing-component-sm;
+
+ .ui-empty-state__icon {
+ width: $semantic-sizing-icon-inline;
+ height: $semantic-sizing-icon-inline;
+ margin-bottom: $semantic-spacing-content-line-tight;
+ }
+
+ .ui-empty-state__title {
+ font-size: $base-typography-font-size-sm;
+ margin-bottom: $semantic-spacing-content-line-tight;
+ }
+
+ .ui-empty-state__description {
+ font-size: $base-typography-font-size-sm;
+ margin-bottom: $semantic-spacing-content-line-normal;
+ }
+ }
+
+ &--md {
+ min-height: 200px;
+ padding: $semantic-spacing-component-md;
+
+ .ui-empty-state__icon {
+ width: $semantic-sizing-icon-button;
+ height: $semantic-sizing-icon-button;
+ margin-bottom: $semantic-spacing-content-line-normal;
+ }
+
+ .ui-empty-state__title {
+ font-size: $base-typography-font-size-lg;
+ margin-bottom: $semantic-spacing-content-line-normal;
+ }
+
+ .ui-empty-state__description {
+ font-size: $base-typography-font-size-md;
+ margin-bottom: $semantic-spacing-content-paragraph;
+ }
+ }
+
+ &--lg {
+ min-height: 300px;
+ padding: $semantic-spacing-component-lg;
+
+ .ui-empty-state__icon {
+ width: $semantic-sizing-icon-navigation;
+ height: $semantic-sizing-icon-navigation;
+ margin-bottom: $semantic-spacing-content-paragraph;
+ }
+
+ .ui-empty-state__title {
+ font-size: $base-typography-font-size-xl;
+ margin-bottom: $semantic-spacing-content-line-normal;
+ }
+
+ .ui-empty-state__description {
+ font-size: $base-typography-font-size-lg;
+ margin-bottom: $semantic-spacing-content-heading;
+ }
+ }
+
+ // Variant Styles
+ &--default {
+ .ui-empty-state__icon {
+ color: $semantic-color-text-tertiary;
+ }
+ }
+
+ &--search {
+ .ui-empty-state__icon {
+ color: $semantic-color-primary;
+ }
+
+ .ui-empty-state__title {
+ color: $semantic-color-text-primary;
+ }
+ }
+
+ &--error {
+ .ui-empty-state__icon {
+ color: $semantic-color-error;
+ }
+
+ .ui-empty-state__title {
+ color: $semantic-color-error;
+ }
+ }
+
+ &--loading {
+ .ui-empty-state__icon {
+ color: $semantic-color-primary;
+ }
+ }
+
+ &--success {
+ .ui-empty-state__icon {
+ color: $semantic-color-success;
+ }
+
+ .ui-empty-state__title {
+ color: $semantic-color-success;
+ }
+ }
+}
+
+.ui-empty-state__icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ margin-bottom: $semantic-spacing-content-line-normal;
+
+ svg, img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ }
+}
+
+.ui-empty-state__spinner {
+ width: 100%;
+ height: 100%;
+ border: 3px solid $semantic-color-border-subtle;
+ border-top: 3px solid $semantic-color-primary;
+ border-radius: 50%;
+ animation: spin $semantic-duration-medium $semantic-easing-standard infinite;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+.ui-empty-state__title {
+ margin: 0 0 $semantic-spacing-content-line-normal 0;
+ color: $semantic-color-text-primary;
+ font-weight: $base-typography-font-weight-semibold;
+ line-height: $base-typography-line-height-tight;
+}
+
+.ui-empty-state__description {
+ margin: 0 0 $semantic-spacing-content-paragraph 0;
+ color: $semantic-color-text-secondary;
+ line-height: $base-typography-line-height-relaxed;
+ max-width: 400px;
+}
+
+.ui-empty-state__content {
+ margin-bottom: $semantic-spacing-content-paragraph;
+
+ &:empty {
+ display: none;
+ }
+}
+
+.ui-empty-state__actions {
+ display: flex;
+ gap: $semantic-spacing-content-line-normal;
+ justify-content: center;
+ flex-wrap: wrap;
+}
+
+.ui-empty-state__action {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
+
+ // Visual Design
+ background: $semantic-color-primary;
+ color: $semantic-color-on-primary;
+ border: $semantic-border-button-width solid $semantic-color-primary;
+ border-radius: $semantic-border-button-radius;
+ box-shadow: $semantic-shadow-button-rest;
+
+ // Typography
+ font-size: $base-typography-font-size-md;
+ font-weight: $base-typography-font-weight-medium;
+ text-decoration: none;
+
+ // Transitions
+ transition: all $semantic-duration-fast $semantic-easing-standard;
+
+ cursor: pointer;
+ user-select: none;
+
+ // Interactive States
+ &:not([disabled]) {
+ &:hover {
+ background: $semantic-color-primary;
+ border-color: $semantic-color-primary;
+ box-shadow: $semantic-shadow-button-hover;
+ transform: translateY(-1px);
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+
+ &:active {
+ background: $semantic-color-primary;
+ border-color: $semantic-color-primary;
+ box-shadow: $semantic-shadow-button-active;
+ transform: translateY(0);
+ }
+ }
+
+ &[disabled] {
+ opacity: 0.38;
+ cursor: not-allowed;
+ pointer-events: none;
+ }
+}
+
+// Dark Mode Support
+:host-context(.dark-theme) .ui-empty-state {
+ background: $semantic-color-surface-secondary;
+}
+
+// Responsive Design
+@media (max-width: 768px) {
+ .ui-empty-state {
+ padding: $semantic-spacing-component-md;
+ min-height: 160px;
+
+ &--lg {
+ min-height: 200px;
+ }
+
+ .ui-empty-state__description {
+ max-width: 300px;
+ }
+ }
+}
+
+@media (max-width: 480px) {
+ .ui-empty-state {
+ padding: $semantic-spacing-component-sm;
+ min-height: 120px;
+
+ .ui-empty-state__icon {
+ width: $semantic-sizing-icon-inline;
+ height: $semantic-sizing-icon-inline;
+ }
+
+ .ui-empty-state__title {
+ font-size: $base-typography-font-size-sm;
+ }
+
+ .ui-empty-state__description {
+ font-size: $base-typography-font-size-sm;
+ max-width: 280px;
+ }
+ }
+}
+
+// High contrast mode
+@media (prefers-contrast: high) {
+ .ui-empty-state__action {
+ border-width: 2px;
+ font-weight: $base-typography-font-weight-bold;
+ }
+}
+
+// Reduced motion
+@media (prefers-reduced-motion: reduce) {
+ .ui-empty-state__spinner {
+ animation: none;
+ border-top-color: $semantic-color-primary;
+ }
+
+ .ui-empty-state__action {
+ transition: none;
+
+ &:hover {
+ transform: none;
+ }
+
+ &:active {
+ transform: none;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/feedback/empty-state/empty-state.component.ts b/projects/ui-essentials/src/lib/components/feedback/empty-state/empty-state.component.ts
new file mode 100644
index 0000000..6b523d0
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/feedback/empty-state/empty-state.component.ts
@@ -0,0 +1,76 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+type EmptyStateSize = 'sm' | 'md' | 'lg';
+type EmptyStateVariant = 'default' | 'search' | 'error' | 'loading' | 'success';
+
+@Component({
+ selector: 'ui-empty-state',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ template: `
+
+
+ @if (icon || variant === 'loading') {
+
+ @if (variant === 'loading') {
+
+ } @else if (icon) {
+
+ }
+
+ }
+
+ @if (title) {
+
{{ title }}
+ }
+
+ @if (description) {
+
{{ description }}
+ }
+
+
+
+
+
+ @if (actionLabel) {
+
+
+ {{ actionLabel }}
+
+
+ }
+
+ `,
+ styleUrl: './empty-state.component.scss'
+})
+export class EmptyStateComponent {
+ @Input() size: EmptyStateSize = 'md';
+ @Input() variant: EmptyStateVariant = 'default';
+ @Input() icon = '';
+ @Input() title = '';
+ @Input() description = '';
+ @Input() actionLabel = '';
+ @Input() disabled = false;
+ @Input() role = 'region';
+ @Input() ariaLabel = 'Empty state';
+
+ @Output() action = new EventEmitter();
+
+ handleAction(event: MouseEvent): void {
+ if (!this.disabled) {
+ this.action.emit(event);
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/feedback/empty-state/index.ts b/projects/ui-essentials/src/lib/components/feedback/empty-state/index.ts
new file mode 100644
index 0000000..c587e3c
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/feedback/empty-state/index.ts
@@ -0,0 +1 @@
+export * from './empty-state.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/feedback/index.ts b/projects/ui-essentials/src/lib/components/feedback/index.ts
new file mode 100644
index 0000000..37917c5
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/feedback/index.ts
@@ -0,0 +1,5 @@
+export * from "./status-badge.component";
+export * from "./skeleton-loader";
+export * from "./empty-state";
+export * from "./loading-spinner";
+// export * from "./theme-switcher.component"; // Temporarily disabled due to CSS variable issues
diff --git a/projects/ui-essentials/src/lib/components/feedback/loading-spinner/index.ts b/projects/ui-essentials/src/lib/components/feedback/loading-spinner/index.ts
new file mode 100644
index 0000000..320a444
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/feedback/loading-spinner/index.ts
@@ -0,0 +1 @@
+export * from './loading-spinner.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/feedback/loading-spinner/loading-spinner.component.scss b/projects/ui-essentials/src/lib/components/feedback/loading-spinner/loading-spinner.component.scss
new file mode 100644
index 0000000..d1b54cb
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/feedback/loading-spinner/loading-spinner.component.scss
@@ -0,0 +1,248 @@
+@use "../../../../../../shared-ui/src/styles/semantic" as *;
+
+.ui-loading-spinner {
+ // Core Structure
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ flex-shrink: 0;
+
+ // Sizing Variants
+ &--sm {
+ width: $semantic-sizing-icon-inline;
+ height: $semantic-sizing-icon-inline;
+ }
+
+ &--md {
+ width: $semantic-sizing-icon-button;
+ height: $semantic-sizing-icon-button;
+ }
+
+ &--lg {
+ width: $semantic-sizing-icon-navigation;
+ height: $semantic-sizing-icon-navigation;
+ }
+
+ // Color Variants
+ &--primary {
+ color: $semantic-color-brand-primary;
+ }
+
+ &--secondary {
+ color: $semantic-color-brand-secondary;
+ }
+
+ &--accent {
+ color: $semantic-color-brand-accent;
+ }
+
+ &--success {
+ color: $semantic-color-success;
+ }
+
+ &--warning {
+ color: $semantic-color-warning;
+ }
+
+ &--danger {
+ color: $semantic-color-danger;
+ }
+
+ &--info {
+ color: $semantic-color-info;
+ }
+
+ // Style Variants
+ &--spin .ui-loading-spinner__spin {
+ border: 2px solid $semantic-color-border-secondary;
+ border-top: 2px solid currentColor;
+ border-radius: 50%;
+ width: 100%;
+ height: 100%;
+ animation: ui-spinner-spin 1s ease-in-out infinite;
+ }
+
+ &--dots {
+ .ui-loading-spinner__dots {
+ display: flex;
+ gap: 0.25rem;
+
+ .ui-loading-spinner__dot {
+ width: 25%;
+ height: 25%;
+ background-color: currentColor;
+ border-radius: 50%;
+ animation: ui-spinner-dots 0.6s ease-in-out infinite;
+
+ &:nth-child(1) { animation-delay: 0ms; }
+ &:nth-child(2) { animation-delay: 150ms; }
+ &:nth-child(3) { animation-delay: 300ms; }
+ }
+ }
+ }
+
+ &--pulse .ui-loading-spinner__pulse {
+ width: 100%;
+ height: 100%;
+ background-color: currentColor;
+ border-radius: 50%;
+ animation: ui-spinner-pulse 1s ease-in-out infinite;
+ }
+
+ &--bars {
+ .ui-loading-spinner__bars {
+ display: flex;
+ align-items: flex-end;
+ gap: 20%;
+ height: 100%;
+
+ .ui-loading-spinner__bar {
+ width: 15%;
+ background-color: currentColor;
+ border-radius: 0.125rem;
+ animation: ui-spinner-bars 0.6s ease-in-out infinite;
+
+ &:nth-child(1) { animation-delay: 0ms; }
+ &:nth-child(2) { animation-delay: 100ms; }
+ &:nth-child(3) { animation-delay: 200ms; }
+ }
+ }
+ }
+
+ // State Variants
+ &--disabled {
+ opacity: 0.38;
+ pointer-events: none;
+ }
+
+ // Speed Variants
+ &--slow {
+ .ui-loading-spinner__spin {
+ animation-duration: 2s;
+ }
+
+ .ui-loading-spinner__dots .ui-loading-spinner__dot {
+ animation-duration: 1.2s;
+ }
+
+ .ui-loading-spinner__pulse {
+ animation-duration: 2s;
+ }
+
+ .ui-loading-spinner__bars .ui-loading-spinner__bar {
+ animation-duration: 1.2s;
+ }
+ }
+
+ &--fast {
+ .ui-loading-spinner__spin {
+ animation-duration: 0.5s;
+ }
+
+ .ui-loading-spinner__dots .ui-loading-spinner__dot {
+ animation-duration: 0.3s;
+ }
+
+ .ui-loading-spinner__pulse {
+ animation-duration: 0.5s;
+ }
+
+ .ui-loading-spinner__bars .ui-loading-spinner__bar {
+ animation-duration: 0.3s;
+ }
+ }
+
+ // Label
+ &__label {
+ margin-left: 0.5rem;
+ color: $semantic-color-text-secondary;
+ font-size: 0.875rem;
+ font-weight: 500;
+ }
+
+ // Screen Reader Only Text
+ &__sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+ }
+
+ // Accessibility - Respect reduced motion preference
+ @media (prefers-reduced-motion: reduce) {
+ .ui-loading-spinner__spin,
+ .ui-loading-spinner__dots .ui-loading-spinner__dot,
+ .ui-loading-spinner__pulse,
+ .ui-loading-spinner__bars .ui-loading-spinner__bar {
+ animation: none;
+ }
+
+ .ui-loading-spinner__spin {
+ border-top-color: transparent;
+ border-right-color: transparent;
+ border-bottom-color: transparent;
+ }
+
+ .ui-loading-spinner__pulse {
+ opacity: 0.6;
+ }
+ }
+
+ // High contrast mode
+ @media (prefers-contrast: high) {
+ .ui-loading-spinner__spin {
+ border-width: 3px;
+ }
+ }
+}
+
+// Keyframes
+@keyframes ui-spinner-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes ui-spinner-dots {
+ 0%, 20%, 80%, 100% {
+ transform: scale(1);
+ opacity: 1;
+ }
+ 50% {
+ transform: scale(1.5);
+ opacity: 0.7;
+ }
+}
+
+@keyframes ui-spinner-pulse {
+ 0% {
+ transform: scale(1);
+ opacity: 1;
+ }
+ 50% {
+ transform: scale(1.1);
+ opacity: 0.5;
+ }
+ 100% {
+ transform: scale(1);
+ opacity: 1;
+ }
+}
+
+@keyframes ui-spinner-bars {
+ 0%, 40%, 100% {
+ height: 40%;
+ }
+ 20% {
+ height: 100%;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/feedback/loading-spinner/loading-spinner.component.ts b/projects/ui-essentials/src/lib/components/feedback/loading-spinner/loading-spinner.component.ts
new file mode 100644
index 0000000..5777d14
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/feedback/loading-spinner/loading-spinner.component.ts
@@ -0,0 +1,108 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+type SpinnerSize = 'sm' | 'md' | 'lg';
+type SpinnerVariant = 'primary' | 'secondary' | 'accent' | 'success' | 'warning' | 'danger' | 'info';
+type SpinnerType = 'spin' | 'dots' | 'pulse' | 'bars';
+type SpinnerSpeed = 'slow' | 'normal' | 'fast';
+
+@Component({
+ selector: 'ui-loading-spinner',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ template: `
+
+
+
+
+ {{ srText || defaultSrText }}
+
+
+
+ @switch (type) {
+ @case ('spin') {
+
+ }
+
+ @case ('dots') {
+
+ }
+
+ @case ('pulse') {
+
+ }
+
+ @case ('bars') {
+
+ }
+ }
+
+
+ @if (label && showLabel) {
+
{{ label }}
+ }
+
+ `,
+ styleUrl: './loading-spinner.component.scss'
+})
+export class LoadingSpinnerComponent {
+ @Input() size: SpinnerSize = 'md';
+ @Input() variant: SpinnerVariant = 'primary';
+ @Input() type: SpinnerType = 'spin';
+ @Input() speed: SpinnerSpeed = 'normal';
+ @Input() disabled = false;
+ @Input() label = '';
+ @Input() showLabel = false;
+ @Input() role = 'status';
+ @Input() tabIndex = 0;
+ @Input() ariaLabel = '';
+ @Input() srText = '';
+
+ @Output() clicked = new EventEmitter();
+
+ get defaultAriaLabel(): string {
+ if (this.ariaLabel) return this.ariaLabel;
+ return this.label ? `Loading: ${this.label}` : 'Loading';
+ }
+
+ get defaultSrText(): string {
+ if (this.srText) return this.srText;
+ return this.label ? `Loading ${this.label}...` : 'Loading...';
+ }
+
+ handleClick(event: MouseEvent): void {
+ if (!this.disabled) {
+ this.clicked.emit(event);
+ }
+ }
+
+ handleKeydown(event: KeyboardEvent): void {
+ // Allow keyboard interaction if the spinner is interactive
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ this.handleClick(event as any);
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/feedback/skeleton-loader/index.ts b/projects/ui-essentials/src/lib/components/feedback/skeleton-loader/index.ts
new file mode 100644
index 0000000..f30995c
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/feedback/skeleton-loader/index.ts
@@ -0,0 +1 @@
+export * from './skeleton-loader.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/feedback/skeleton-loader/skeleton-loader.component.scss b/projects/ui-essentials/src/lib/components/feedback/skeleton-loader/skeleton-loader.component.scss
new file mode 100644
index 0000000..bd8ad99
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/feedback/skeleton-loader/skeleton-loader.component.scss
@@ -0,0 +1,293 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+.ui-skeleton-loader {
+ display: block;
+ width: 100%;
+ overflow: hidden;
+ position: relative;
+ background: $semantic-color-surface-variant;
+
+ // Core structure
+ &::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(
+ 90deg,
+ transparent,
+ rgba(255, 255, 255, 0.2),
+ transparent
+ );
+ animation: shimmer 2s ease-in-out infinite;
+ transform: translateX(-100%);
+ }
+
+ // Size variants
+ &--xs {
+ height: 0.5rem;
+ border-radius: 0.125rem;
+ }
+
+ &--sm {
+ height: 0.75rem;
+ border-radius: 0.25rem;
+ }
+
+ &--md {
+ height: 1rem;
+ border-radius: 0.375rem;
+ }
+
+ &--lg {
+ height: 1.25rem;
+ border-radius: 0.5rem;
+ }
+
+ &--xl {
+ height: 1.5rem;
+ border-radius: 0.5rem;
+ }
+
+ // Shape variants
+ &--text {
+ height: 1.25rem;
+ border-radius: 0.25rem;
+ }
+
+ &--heading {
+ height: 1.75rem;
+ border-radius: 0.25rem;
+ }
+
+ &--avatar {
+ border-radius: 50%;
+ aspect-ratio: 1;
+
+ &.ui-skeleton-loader--xs { width: 1rem; }
+ &.ui-skeleton-loader--sm { width: 1.5rem; }
+ &.ui-skeleton-loader--md { width: 2rem; }
+ &.ui-skeleton-loader--lg { width: 2.5rem; }
+ &.ui-skeleton-loader--xl { width: 3rem; }
+ }
+
+ &--card {
+ height: 200px;
+ border-radius: 0.5rem;
+ }
+
+ &--button {
+ height: 2.5rem;
+ width: 120px;
+ border-radius: 0.375rem;
+ }
+
+ &--image {
+ aspect-ratio: 16/9;
+ border-radius: 0.375rem;
+ }
+
+ &--circle {
+ border-radius: 50%;
+ aspect-ratio: 1;
+ }
+
+ &--rounded {
+ border-radius: 0.375rem;
+ }
+
+ &--square {
+ border-radius: 0;
+ }
+
+ // Animation variants
+ &--pulse {
+ &::after {
+ display: none;
+ }
+
+ animation: pulse 1.5s ease-in-out infinite alternate;
+ }
+
+ &--wave {
+ &::after {
+ background: linear-gradient(
+ 90deg,
+ transparent 0%,
+ rgba(255, 255, 255, 0.4) 50%,
+ transparent 100%
+ );
+ animation: wave 2s ease-in-out infinite;
+ }
+ }
+
+ &--shimmer {
+ // Default shimmer animation (already applied)
+ }
+
+ // Width variants
+ &--w-25 { width: 25%; }
+ &--w-50 { width: 50%; }
+ &--w-75 { width: 75%; }
+ &--w-full { width: 100%; }
+
+ // State variants
+ &--loading {
+ opacity: 1;
+ }
+
+ &--loaded {
+ opacity: 0;
+ transition: opacity 0.3s ease;
+ }
+
+ // Dark mode support
+ :host-context(.dark-theme) & {
+ background: #374151;
+
+ &::after {
+ background: linear-gradient(
+ 90deg,
+ transparent,
+ rgba(255, 255, 255, 0.1),
+ transparent
+ );
+ }
+ }
+
+ // High contrast mode
+ @media (prefers-contrast: high) {
+ background: #6b7280;
+
+ &::after {
+ background: linear-gradient(
+ 90deg,
+ transparent,
+ rgba(255, 255, 255, 0.6),
+ transparent
+ );
+ }
+ }
+
+ // Reduced motion
+ @media (prefers-reduced-motion: reduce) {
+ &::after {
+ animation: none;
+ }
+
+ &.ui-skeleton-loader--pulse {
+ animation: none;
+ }
+ }
+}
+
+// Skeleton group for multiple skeleton items
+.ui-skeleton-group {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+
+ // Horizontal group
+ &--horizontal {
+ flex-direction: row;
+ align-items: center;
+ }
+
+ // Grid group
+ &--grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 1rem;
+ }
+
+ // Avatar with text group
+ &--avatar-text {
+ flex-direction: row;
+ align-items: flex-start;
+ gap: 0.75rem;
+
+ .ui-skeleton-loader--avatar {
+ flex-shrink: 0;
+ }
+
+ .ui-skeleton-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ }
+ }
+
+ // Card group
+ &--card {
+ .ui-skeleton-loader {
+ &:first-child {
+ height: 180px;
+ border-radius: 0.5rem 0.5rem 0 0;
+ }
+
+ &:not(:first-child) {
+ margin: 0.75rem;
+ height: 1rem;
+
+ &:last-child {
+ width: 60%;
+ }
+ }
+ }
+ }
+
+ // Table group
+ &--table {
+ .ui-skeleton-loader {
+ height: 1.25rem;
+
+ &:first-child {
+ width: 30%;
+ }
+
+ &:nth-child(2) {
+ width: 50%;
+ }
+
+ &:last-child {
+ width: 20%;
+ }
+ }
+ }
+}
+
+// Animations
+@keyframes shimmer {
+ 0% {
+ transform: translateX(-100%);
+ }
+ 100% {
+ transform: translateX(100%);
+ }
+}
+
+@keyframes pulse {
+ 0% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0.6;
+ }
+}
+
+@keyframes wave {
+ 0% {
+ transform: translateX(-100%) scale(0.8);
+ opacity: 0;
+ }
+ 50% {
+ opacity: 1;
+ }
+ 100% {
+ transform: translateX(100%) scale(1.2);
+ opacity: 0;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/feedback/skeleton-loader/skeleton-loader.component.ts b/projects/ui-essentials/src/lib/components/feedback/skeleton-loader/skeleton-loader.component.ts
new file mode 100644
index 0000000..7f0ea9e
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/feedback/skeleton-loader/skeleton-loader.component.ts
@@ -0,0 +1,347 @@
+import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+export type SkeletonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
+export type SkeletonShape = 'text' | 'heading' | 'avatar' | 'card' | 'button' | 'image' | 'circle' | 'rounded' | 'square';
+export type SkeletonAnimation = 'shimmer' | 'pulse' | 'wave';
+export type SkeletonWidth = 'w-25' | 'w-50' | 'w-75' | 'w-full' | 'auto';
+
+@Component({
+ selector: 'ui-skeleton-loader',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ template: `
+
+
+ `,
+ styleUrl: './skeleton-loader.component.scss'
+})
+export class SkeletonLoaderComponent {
+ @Input() size: SkeletonSize = 'md';
+ @Input() shape: SkeletonShape = 'text';
+ @Input() animation: SkeletonAnimation = 'shimmer';
+ @Input() width: SkeletonWidth = 'w-full';
+ @Input() loading = true;
+ @Input() customWidth?: string;
+ @Input() customHeight?: string;
+ @Input() rounded = false;
+ @Input() circle = false;
+ @Input() ariaLabel = 'Loading content';
+ @Input() role = 'status';
+ @Input() class = '';
+
+ get skeletonClasses(): string {
+ const classes = [
+ 'ui-skeleton-loader',
+ `ui-skeleton-loader--${this.size}`,
+ `ui-skeleton-loader--${this.shape}`,
+ `ui-skeleton-loader--${this.animation}`,
+ this.width !== 'auto' ? `ui-skeleton-loader--${this.width}` : '',
+ this.loading ? 'ui-skeleton-loader--loading' : 'ui-skeleton-loader--loaded',
+ this.rounded ? 'ui-skeleton-loader--rounded' : '',
+ this.circle ? 'ui-skeleton-loader--circle' : '',
+ this.class
+ ].filter(Boolean);
+
+ return classes.join(' ');
+ }
+}
+
+@Component({
+ selector: 'ui-skeleton-group',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ template: `
+
+
+
+ `,
+ styleUrl: './skeleton-loader.component.scss'
+})
+export class SkeletonGroupComponent {
+ @Input() variant: 'vertical' | 'horizontal' | 'grid' | 'avatar-text' | 'card' | 'table' = 'vertical';
+ @Input() ariaLabel = 'Loading multiple items';
+ @Input() role = 'status';
+ @Input() class = '';
+
+ get groupClasses(): string {
+ const classes = [
+ 'ui-skeleton-group',
+ this.variant !== 'vertical' ? `ui-skeleton-group--${this.variant}` : '',
+ this.class
+ ].filter(Boolean);
+
+ return classes.join(' ');
+ }
+}
+
+// Pre-built skeleton patterns for common use cases
+@Component({
+ selector: 'ui-skeleton-text-block',
+ standalone: true,
+ imports: [CommonModule, SkeletonLoaderComponent, SkeletonGroupComponent],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+ @for (line of lineArray; track $index) {
+
+
+ }
+
+ `
+})
+export class SkeletonTextBlockComponent {
+ @Input() lines = 3;
+ @Input() size: SkeletonSize = 'md';
+ @Input() animation: SkeletonAnimation = 'shimmer';
+ @Input() lastLineWidth: SkeletonWidth = 'w-75';
+
+ get lineArray(): number[] {
+ return Array.from({ length: this.lines }, (_, i) => i);
+ }
+
+ getLineWidth(index: number): SkeletonWidth {
+ return index === this.lines - 1 ? this.lastLineWidth : 'w-full';
+ }
+}
+
+@Component({
+ selector: 'ui-skeleton-article',
+ standalone: true,
+ imports: [CommonModule, SkeletonLoaderComponent, SkeletonGroupComponent],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
+
+
+
+
+
+
+
+
+ @if (showImage) {
+
+
+ }
+
+
+ @for (line of contentArray; track $index) {
+
+
+ }
+
+ `
+})
+export class SkeletonArticleComponent {
+ @Input() showImage = true;
+ @Input() contentLines = 5;
+ @Input() animation: SkeletonAnimation = 'shimmer';
+
+ get contentArray(): number[] {
+ return Array.from({ length: this.contentLines }, (_, i) => i);
+ }
+
+ getContentLineWidth(index: number): SkeletonWidth {
+ if (index === this.contentLines - 1) return 'w-50';
+ if (index === this.contentLines - 2) return 'w-75';
+ return 'w-full';
+ }
+}
+
+@Component({
+ selector: 'ui-skeleton-card',
+ standalone: true,
+ imports: [CommonModule, SkeletonLoaderComponent, SkeletonGroupComponent],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
+ @if (showImage) {
+
+
+ }
+
+
+
+
+
+
+ @for (line of contentArray; track $index) {
+
+
+ }
+
+
+ @if (showActions) {
+
+
+
+
+
+
+ }
+
+ `
+})
+export class SkeletonCardComponent {
+ @Input() showImage = true;
+ @Input() showActions = true;
+ @Input() contentLines = 3;
+ @Input() animation: SkeletonAnimation = 'shimmer';
+
+ get contentArray(): number[] {
+ return Array.from({ length: this.contentLines }, (_, i) => i);
+ }
+
+ getCardLineWidth(index: number): SkeletonWidth {
+ if (index === this.contentLines - 1) return 'w-50';
+ return 'w-full';
+ }
+}
+
+@Component({
+ selector: 'ui-skeleton-profile',
+ standalone: true,
+ imports: [CommonModule, SkeletonLoaderComponent, SkeletonGroupComponent],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @if (showExtraInfo) {
+
+
+ }
+
+
+ `
+})
+export class SkeletonProfileComponent {
+ @Input() avatarSize: SkeletonSize = 'lg';
+ @Input() showExtraInfo = true;
+ @Input() animation: SkeletonAnimation = 'shimmer';
+}
+
+@Component({
+ selector: 'ui-skeleton-table-row',
+ standalone: true,
+ imports: [CommonModule, SkeletonLoaderComponent, SkeletonGroupComponent],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+ @for (column of columnArray; track $index) {
+
+
+ }
+
+ `
+})
+export class SkeletonTableRowComponent {
+ @Input() columns = 3;
+ @Input() animation: SkeletonAnimation = 'shimmer';
+
+ get columnArray(): number[] {
+ return Array.from({ length: this.columns }, (_, i) => i);
+ }
+
+ getColumnWidth(index: number): SkeletonWidth {
+ const widths: SkeletonWidth[] = ['w-25', 'w-50', 'w-25', 'w-full'];
+ return widths[index % widths.length];
+ }
+}
+
+// Convenience export for all skeleton components
+export const SKELETON_COMPONENTS = [
+ SkeletonLoaderComponent,
+ SkeletonGroupComponent,
+ SkeletonTextBlockComponent,
+ SkeletonArticleComponent,
+ SkeletonCardComponent,
+ SkeletonProfileComponent,
+ SkeletonTableRowComponent
+] as const;
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/feedback/status-badge.component.scss b/projects/ui-essentials/src/lib/components/feedback/status-badge.component.scss
new file mode 100644
index 0000000..422f310
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/feedback/status-badge.component.scss
@@ -0,0 +1,253 @@
+@use "../../../../../shared-ui/src/styles/semantic/index" as *;
+
+.ui-status-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.25rem;
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+ font-weight: 500;
+ line-height: 1;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: baseline;
+ transition: all 150ms cubic-bezier(0, 0, 0.2, 1);
+ position: relative;
+
+ // Size variants
+ &--small {
+ padding: 0.125rem 0.375rem;
+ font-size: 0.625rem;
+ min-height: 1.125rem;
+
+ &.ui-status-badge--with-icon .ui-status-badge__icon {
+ font-size: 0.625rem;
+ }
+
+ &.ui-status-badge--with-dot .ui-status-badge__dot {
+ width: 0.375rem;
+ height: 0.375rem;
+ }
+ }
+
+ &--medium {
+ padding: 0.25rem 0.5rem;
+ font-size: 0.75rem;
+ min-height: 1.25rem;
+
+ &.ui-status-badge--with-icon .ui-status-badge__icon {
+ font-size: 0.75rem;
+ }
+
+ &.ui-status-badge--with-dot .ui-status-badge__dot {
+ width: 0.5rem;
+ height: 0.5rem;
+ }
+ }
+
+ &--large {
+ padding: 0.375rem 0.75rem;
+ font-size: 0.875rem;
+ min-height: 1.5rem;
+
+ &.ui-status-badge--with-icon .ui-status-badge__icon {
+ font-size: 0.875rem;
+ }
+
+ &.ui-status-badge--with-dot .ui-status-badge__dot {
+ width: 0.625rem;
+ height: 0.625rem;
+ }
+ }
+
+ // Shape variants
+ &--rounded {
+ border-radius: 0.25rem;
+ }
+
+ &--pill {
+ border-radius: 9999px;
+ }
+
+ &--square {
+ border-radius: 0;
+ }
+
+ // Style modifiers
+ &--uppercase {
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ }
+
+ &--bold {
+ font-weight: 700;
+ }
+
+ // Success variant
+ &--success {
+ background: hsl(142, 76%, 90%);
+ color: hsl(142, 76%, 25%);
+ border: 1px solid hsl(142, 76%, 80%);
+
+ &.ui-status-badge--outlined {
+ background: transparent;
+ color: hsl(142, 76%, 35%);
+ border-color: hsl(142, 76%, 50%);
+ }
+
+ .ui-status-badge__dot {
+ background: hsl(142, 76%, 45%);
+ }
+ }
+
+ // Warning variant
+ &--warning {
+ background: hsl(45, 93%, 88%);
+ color: hsl(45, 93%, 25%);
+ border: 1px solid hsl(45, 93%, 75%);
+
+ &.ui-status-badge--outlined {
+ background: transparent;
+ color: hsl(45, 93%, 35%);
+ border-color: hsl(45, 93%, 50%);
+ }
+
+ .ui-status-badge__dot {
+ background: hsl(45, 93%, 45%);
+ }
+ }
+
+ // Danger variant
+ &--danger {
+ background: hsl(0, 84%, 90%);
+ color: hsl(0, 84%, 25%);
+ border: 1px solid hsl(0, 84%, 80%);
+
+ &.ui-status-badge--outlined {
+ background: transparent;
+ color: hsl(0, 84%, 35%);
+ border-color: hsl(0, 84%, 50%);
+ }
+
+ .ui-status-badge__dot {
+ background: hsl(0, 84%, 45%);
+ }
+ }
+
+ // Info variant
+ &--info {
+ background: hsl(207, 90%, 88%);
+ color: hsl(207, 90%, 25%);
+ border: 1px solid hsl(207, 90%, 75%);
+
+ &.ui-status-badge--outlined {
+ background: transparent;
+ color: hsl(207, 90%, 35%);
+ border-color: hsl(207, 90%, 50%);
+ }
+
+ .ui-status-badge__dot {
+ background: hsl(207, 90%, 45%);
+ }
+ }
+
+ // Neutral variant
+ &--neutral {
+ background: hsl(289, 14%, 90%);
+ color: hsl(279, 14%, 25%);
+ border: 1px solid hsl(289, 14%, 80%);
+
+ &.ui-status-badge--outlined {
+ background: transparent;
+ color: hsl(279, 14%, 35%);
+ border-color: hsl(289, 14%, 60%);
+ }
+
+ .ui-status-badge__dot {
+ background: hsl(287, 12%, 47%);
+ }
+ }
+
+ // Primary variant
+ &--primary {
+ background: hsl(263, 100%, 88%);
+ color: hsl(260, 100%, 25%);
+ border: 1px solid hsl(263, 100%, 75%);
+
+ &.ui-status-badge--outlined {
+ background: transparent;
+ color: hsl(258, 100%, 47%);
+ border-color: hsl(258, 100%, 47%);
+ }
+
+ .ui-status-badge__dot {
+ background: hsl(258, 100%, 47%);
+ }
+ }
+
+ // Secondary variant
+ &--secondary {
+ background: hsl(262, 25%, 84%);
+ color: hsl(256, 29%, 25%);
+ border: 1px solid hsl(262, 25%, 75%);
+
+ &.ui-status-badge--outlined {
+ background: transparent;
+ color: hsl(258, 29%, 40%);
+ border-color: hsl(258, 29%, 40%);
+ }
+
+ .ui-status-badge__dot {
+ background: hsl(258, 29%, 40%);
+ }
+ }
+}
+
+.ui-status-badge__icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ line-height: 1;
+}
+
+.ui-status-badge__dot {
+ border-radius: 50%;
+ flex-shrink: 0;
+}
+
+.ui-status-badge__text {
+ flex: 1;
+ min-width: 0;
+}
+
+// Hover effects for interactive badges (when used in buttons, etc.)
+button .ui-status-badge,
+a .ui-status-badge,
+[role="button"] .ui-status-badge {
+ transition: all 150ms cubic-bezier(0, 0, 0.2, 1);
+
+ &:hover {
+ transform: scale(1.02);
+ }
+}
+
+// High contrast mode
+@media (prefers-contrast: high) {
+ .ui-status-badge {
+ border-width: 2px;
+ font-weight: 700;
+
+ &--outlined {
+ background: buttonface;
+ color: buttontext;
+ border-color: buttontext;
+ }
+ }
+}
+
+// Reduced motion
+@media (prefers-reduced-motion: reduce) {
+ .ui-status-badge {
+ transition: none;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/feedback/status-badge.component.ts b/projects/ui-essentials/src/lib/components/feedback/status-badge.component.ts
new file mode 100644
index 0000000..3b1ae4d
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/feedback/status-badge.component.ts
@@ -0,0 +1,54 @@
+import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+export type StatusBadgeVariant = 'success' | 'warning' | 'danger' | 'info' | 'neutral' | 'primary' | 'secondary';
+export type StatusBadgeSize = 'small' | 'medium' | 'large';
+export type StatusBadgeShape = 'rounded' | 'pill' | 'square';
+
+@Component({
+ selector: 'ui-status-badge',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+ @if (icon) {
+
+ }
+ @if (dot) {
+
+ }
+
+
+
+
+ `,
+ styleUrl: './status-badge.component.scss'
+})
+export class StatusBadgeComponent {
+ @Input() variant: StatusBadgeVariant = 'neutral';
+ @Input() size: StatusBadgeSize = 'medium';
+ @Input() shape: StatusBadgeShape = 'rounded';
+ @Input() icon: string = '';
+ @Input() dot: boolean = false;
+ @Input() outlined: boolean = false;
+ @Input() uppercase: boolean = true;
+ @Input() bold: boolean = false;
+ @Input() ariaLabel: string = '';
+ @Input() class: string = '';
+
+ get badgeClasses(): string {
+ return [
+ 'ui-status-badge',
+ `ui-status-badge--${this.variant}`,
+ `ui-status-badge--${this.size}`,
+ `ui-status-badge--${this.shape}`,
+ this.outlined ? 'ui-status-badge--outlined' : '',
+ this.uppercase ? 'ui-status-badge--uppercase' : '',
+ this.bold ? 'ui-status-badge--bold' : '',
+ this.icon ? 'ui-status-badge--with-icon' : '',
+ this.dot ? 'ui-status-badge--with-dot' : '',
+ this.class
+ ].filter(Boolean).join(' ');
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/feedback/theme-switcher.component.scss b/projects/ui-essentials/src/lib/components/feedback/theme-switcher.component.scss
new file mode 100644
index 0000000..f4c4861
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/feedback/theme-switcher.component.scss
@@ -0,0 +1,579 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+/**
+ * ==========================================================================
+ * THEME SWITCHER COMPONENT STYLES
+ * ==========================================================================
+ * Material Design 3 inspired styling for the theme switcher component.
+ * Uses CSS variables for runtime theming support.
+ * ==========================================================================
+ */
+
+
+// Tokens available globally via main application styles
+
+
+.theme-switcher {
+ background-color: $semantic-color-surface-primary;
+ border-radius: $semantic-border-radius-lg;
+ padding: $semantic-spacing-component-lg;
+ box-shadow: $semantic-shadow-elevated;
+ border: 1px solid $semantic-color-border-primary;
+ position: relative;
+ max-width: 400px;
+ width: 100%;
+
+ // ==========================================================================
+ // HEADER STYLES
+ // ==========================================================================
+
+ &__header {
+ margin-bottom: $semantic-spacing-component-lg;
+ }
+
+ &__title {
+ @extend .skyui-heading--h3 !optional;
+ color: $semantic-color-text-primary;
+ margin: 0 0 $semantic-spacing-component-2xs 0;
+ }
+
+ &__subtitle {
+ @extend .skyui-text--small !optional;
+ color: $semantic-color-text-secondary;
+ margin: 0;
+ }
+
+ // ==========================================================================
+ // SECTION STYLES
+ // ==========================================================================
+
+ &__section {
+ margin-bottom: $semantic-spacing-component-lg;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ &__section-title {
+ @extend .skyui-heading--h4 !optional;
+ color: $semantic-color-text-primary;
+ margin: 0 0 $semantic-spacing-component-sm 0;
+ }
+
+ &__section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+
+ // ==========================================================================
+ // DARK MODE TOGGLE STYLES
+ // ==========================================================================
+
+ &__option {
+ display: flex;
+ align-items: center;
+ }
+
+ &__label {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ @extend .skyui-text--medium !optional;
+ color: $semantic-color-text-primary;
+
+ &:hover {
+ color: $css-color-primary;
+ }
+ }
+
+ &__checkbox {
+ position: absolute;
+ opacity: 0;
+ cursor: pointer;
+
+ &:checked + &-custom {
+ background-color: $css-color-primary;
+ border-color: $css-color-primary;
+
+ &::after {
+ display: block;
+ }
+ }
+
+ &:focus + &-custom {
+ outline: 2px solid $css-color-primary;
+ outline-offset: 2px;
+ }
+
+ &:disabled + &-custom {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+ }
+
+ &__checkbox-custom {
+ width: 20px;
+ height: 20px;
+ border: 2px solid $css-color-outline;
+ border-radius: $semantic-border-radius-sm;
+ margin-right: $semantic-spacing-component-sm;
+ position: relative;
+ transition: all 0.2s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &::after {
+ content: '✓';
+ color: $css-color-on-primary;
+ font-size: 12px;
+ font-weight: bold;
+ display: none;
+ }
+ }
+
+ &__text {
+ user-select: none;
+ }
+
+ // ==========================================================================
+ // THEME GRID STYLES
+ // ==========================================================================
+
+ .theme-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
+ gap: $semantic-spacing-component-sm;
+ }
+
+ .theme-card {
+ background-color: $semantic-color-primary-container;
+ border: 2px solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-md;
+ padding: $semantic-spacing-component-sm;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ position: relative;
+ text-align: center;
+
+ &:hover:not(&--disabled) {
+ border-color: $css-color-primary;
+ box-shadow: $semantic-shadow-elevated;
+ transform: translateY(-1px);
+ }
+
+ &--active {
+ border-color: $css-color-primary;
+ background-color: $css-color-primary-container;
+ }
+
+ &--disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+
+ &__preview {
+ margin-bottom: $semantic-spacing-component-xs;
+ }
+
+ &__color-primary {
+ width: 100%;
+ height: 32px;
+ border-radius: $semantic-border-radius-sm;
+ margin-bottom: $semantic-spacing-component-2xs;
+ }
+
+ &__color-details {
+ display: flex;
+ gap: $semantic-spacing-component-2xs;
+ }
+
+ &__color-secondary,
+ &__color-tertiary {
+ flex: 1;
+ height: 16px;
+ border-radius: $semantic-border-radius-xs;
+ }
+
+ &__name {
+ @extend .skyui-text--small !optional;
+ color: $semantic-color-text-primary;
+ font-weight: $semantic-typography-font-weight-medium;
+ display: block;
+ }
+
+ &__active-indicator {
+ position: absolute;
+ top: 4px;
+ right: 4px;
+ color: $css-color-primary;
+ background-color: $semantic-color-surface-primary;
+ border-radius: 50%;
+ width: 20px;
+ height: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ }
+ }
+
+ // ==========================================================================
+ // TOGGLE BUTTON STYLES
+ // ==========================================================================
+
+ &__toggle-btn {
+ @extend .skyui-interactive-text--button-small !optional;
+ background-color: transparent;
+ color: $css-color-primary;
+ border: 1px solid $css-color-primary;
+ border-radius: $semantic-border-radius-sm;
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background-color: $css-color-primary-container;
+ border-color: $css-color-primary;
+ }
+
+ &--active {
+ background-color: $css-color-primary;
+ color: $css-color-on-primary;
+
+ &:hover {
+ background-color: var(--color-primary-hover, $css-color-primary);
+ filter: brightness(0.9);
+ }
+ }
+
+ &:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+ }
+
+ // ==========================================================================
+ // CUSTOM PANEL STYLES
+ // ==========================================================================
+
+ &__custom-panel {
+ max-height: 0;
+ overflow: hidden;
+ transition: max-height 0.3s ease;
+
+ &--visible {
+ max-height: 800px;
+ }
+ }
+
+ // ==========================================================================
+ // CUSTOM COLOR INPUT STYLES
+ // ==========================================================================
+
+ .custom-color-input {
+ margin-bottom: $semantic-spacing-component-md;
+
+ &__label {
+ @extend .skyui-form-text--label !optional;
+ display: block;
+ margin-bottom: $semantic-spacing-component-2xs;
+ color: $semantic-color-text-primary;
+ }
+
+ &__container {
+ display: flex;
+ gap: $semantic-spacing-component-xs;
+ align-items: center;
+ }
+
+ &__picker {
+ width: 48px;
+ height: 48px;
+ border: none;
+ border-radius: $semantic-border-radius-sm;
+ cursor: pointer;
+ background: none;
+
+ &:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+ }
+
+ &__text {
+ @extend .skyui-form-text--input !optional;
+ flex: 1;
+ padding: $semantic-spacing-component-sm;
+ border: 1px solid $css-color-outline;
+ border-radius: $semantic-border-radius-sm;
+ background-color: $semantic-color-surface-primary;
+ color: $semantic-color-text-primary;
+ font-family: $semantic-typography-font-family-mono;
+
+ &:focus {
+ border-color: $css-color-primary;
+ outline: none;
+ box-shadow: 0 0 0 2px rgba($css-color-primary, 0.2);
+ }
+
+ &:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ background-color: $semantic-color-surface-variant;
+ }
+ }
+ }
+
+ // ==========================================================================
+ // THEME NAME INPUT STYLES
+ // ==========================================================================
+
+ .custom-theme-name {
+ margin-bottom: $semantic-spacing-component-md;
+
+ &__label {
+ @extend .skyui-form-text--label !optional;
+ display: block;
+ margin-bottom: $semantic-spacing-component-2xs;
+ color: $semantic-color-text-primary;
+ }
+
+ &__input {
+ @extend .skyui-form-text--input !optional;
+ width: 100%;
+ padding: $semantic-spacing-component-sm;
+ border: 1px solid $css-color-outline;
+ border-radius: $semantic-border-radius-sm;
+ background-color: $semantic-color-surface-primary;
+ color: $semantic-color-text-primary;
+
+ &:focus {
+ border-color: $css-color-primary;
+ outline: none;
+ box-shadow: 0 0 0 2px rgba($css-color-primary, 0.2);
+ }
+
+ &:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ background-color: $semantic-color-surface-variant;
+ }
+ }
+ }
+
+ // ==========================================================================
+ // COLOR VALIDATION STYLES
+ // ==========================================================================
+
+ .color-validation {
+ margin-bottom: $semantic-spacing-component-md;
+ padding: $semantic-spacing-component-sm;
+ border-radius: $semantic-border-radius-sm;
+
+ &--error {
+ background-color: rgba($css-color-error, 0.1);
+ border: 1px solid $css-color-error;
+ }
+
+ &--success {
+ background-color: rgba($semantic-color-success, 0.1);
+ border: 1px solid $semantic-color-success;
+ }
+
+ &__title {
+ @extend .skyui-text--small !optional;
+ font-weight: $semantic-typography-font-weight-medium;
+ margin-bottom: $semantic-spacing-component-2xs;
+ }
+
+ &__list {
+ margin: 0;
+ padding-left: $semantic-spacing-component-md;
+
+ li {
+ @extend .skyui-text--small !optional;
+ margin-bottom: $semantic-spacing-component-2xs;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+
+ &--error &__title,
+ &--error &__list {
+ color: $css-color-error;
+ }
+
+ &--success &__title,
+ &--success &__list {
+ color: $semantic-color-success;
+ }
+ }
+
+ // ==========================================================================
+ // COLOR PREVIEW STYLES
+ // ==========================================================================
+
+ .color-preview {
+ margin-bottom: $semantic-spacing-component-md;
+
+ &__title {
+ @extend .skyui-text--small !optional;
+ font-weight: $semantic-typography-font-weight-medium;
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-component-xs;
+ }
+
+ &__palette {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
+ gap: $semantic-spacing-component-2xs;
+ }
+
+ &__swatch {
+ aspect-ratio: 1;
+ border-radius: $semantic-border-radius-sm;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ border: 1px solid $semantic-color-border-primary;
+ }
+
+ &__swatch-label {
+ font-size: 10px;
+ font-weight: $semantic-typography-font-weight-medium;
+ text-transform: capitalize;
+ text-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
+ }
+ }
+
+ // ==========================================================================
+ // ACTION BUTTON STYLES
+ // ==========================================================================
+
+ .custom-color-actions {
+ margin-bottom: $semantic-spacing-component-md;
+
+ &__apply {
+ @extend .skyui-interactive-text--button-medium !optional;
+ width: 100%;
+ background-color: $css-color-primary;
+ color: $css-color-on-primary;
+ border: none;
+ border-radius: $semantic-border-radius-md;
+ padding: $semantic-spacing-component-md $semantic-spacing-component-lg;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ font-weight: $semantic-typography-font-weight-medium;
+
+ &:hover:not(:disabled) {
+ background-color: var(--color-primary-hover, $css-color-primary);
+ filter: brightness(0.9);
+ box-shadow: $semantic-shadow-elevated;
+ transform: translateY(-1px);
+ }
+
+ &:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ transform: none;
+ box-shadow: none;
+ }
+ }
+ }
+
+ // ==========================================================================
+ // RESET BUTTON STYLES
+ // ==========================================================================
+
+ &__reset-btn {
+ @extend .skyui-interactive-text--button-medium !optional;
+ width: 100%;
+ background-color: transparent;
+ color: $css-color-error;
+ border: 1px solid $css-color-error;
+ border-radius: $semantic-border-radius-md;
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-lg;
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover:not(:disabled) {
+ background-color: rgba($css-color-error, 0.1);
+ box-shadow: $semantic-shadow-elevated;
+ }
+
+ &:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+ }
+
+ // ==========================================================================
+ // LOADING OVERLAY STYLES
+ // ==========================================================================
+
+ &__loading-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba($css-color-scrim, 0.5);
+ border-radius: $semantic-border-radius-lg;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ z-index: 10;
+ }
+
+ &__loading-spinner {
+ width: 32px;
+ height: 32px;
+ border: 3px solid $semantic-color-border-primary;
+ border-top: 3px solid $css-color-primary;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+
+ &__loading-text {
+ @extend .skyui-text--small !optional;
+ color: $semantic-color-text-primary;
+ font-weight: $semantic-typography-font-weight-medium;
+ }
+}
+
+// ==========================================================================
+// ANIMATIONS
+// ==========================================================================
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+// ==========================================================================
+// RESPONSIVE DESIGN
+// ==========================================================================
+
+@media (max-width: $semantic-breakpoint-sm) {
+ .theme-switcher {
+ padding: $semantic-spacing-component-md;
+
+ .theme-grid {
+ grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
+ }
+
+ .color-preview {
+ &__palette {
+ grid-template-columns: repeat(auto-fit, minmax(60px, 1fr));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/feedback/theme-switcher.component.ts b/projects/ui-essentials/src/lib/components/feedback/theme-switcher.component.ts
new file mode 100644
index 0000000..76eb8bf
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/feedback/theme-switcher.component.ts
@@ -0,0 +1,471 @@
+/**
+ * ==========================================================================
+ * THEME SWITCHER COMPONENT
+ * ==========================================================================
+ * Interactive component for runtime theme switching.
+ * Supports predefined themes, custom colors, and dark mode toggle.
+ * Provides color validation and preview functionality.
+ * ==========================================================================
+ */
+
+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { Subject } from 'rxjs';
+import { takeUntil, debounceTime, distinctUntilChanged } from 'rxjs/operators';
+
+import { ThemeService, ThemeConfig, ThemeState } from '../../../core/services/theme.service';
+
+@Component({
+ selector: 'app-theme-switcher',
+ standalone: true,
+ imports: [CommonModule, FormsModule],
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
+ Dark mode
+
+
+
+
+
+
+
Predefined Themes
+
+ @for (theme of availableThemes; track theme.name) {
+
+
+
{{ getThemeDisplayName(theme.name) }}
+ @if (isThemeActive(theme.name)) {
+
+ }
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Theme Name (Optional)
+
+
+
+
+ @if (colorValidation) {
+
+ @if (!colorValidation.isValid) {
+
+
⚠️ Issues found:
+
+ @for (warning of colorValidation.warnings; track warning) {
+ {{ warning }}
+ }
+
+
+ }
+ @if (colorValidation.recommendations) {
+
+
💡 Recommendations:
+
+ @for (rec of colorValidation.recommendations; track rec) {
+ {{ rec }}
+ }
+
+
+ }
+
+ }
+
+
+ @if (showColorPreview && colorValidation?.isValid) {
+
+
Color Preview
+
+ @for (color of colorPreview | keyvalue; track color.key) {
+
+ {{ color.key }}
+
+ }
+
+
+ }
+
+
+
+
+ @if (!isApplyingTheme) {
+ Apply Custom Theme
+ } @else {
+ Applying...
+ }
+
+
+
+
+
+
+
+
+ Reset to Default
+
+
+
+
+ @if (isApplyingTheme) {
+
+ }
+
+ `,
+ styleUrls: ['./theme-switcher.component.scss']
+})
+export class ThemeSwitcherComponent implements OnInit, OnDestroy {
+ private destroy$ = new Subject();
+
+ // Component state
+ currentTheme: ThemeState = {
+ currentTheme: 'default',
+ isDark: false,
+ primaryColor: '#6750A4',
+ customThemes: []
+ };
+
+ availableThemes: ThemeConfig[] = [];
+ isDarkMode = false;
+
+ // Custom color input
+ customColorInput = '#6750A4';
+ customThemeName = 'My Theme';
+ colorPreview: { [key: string]: string } = {};
+ colorValidation: { isValid: boolean; warnings: string[]; recommendations?: string[]; } | null = null;
+
+ // UI state
+ showCustomColorPanel = false;
+ showColorPreview = false;
+ isApplyingTheme = false;
+
+ constructor(private themeService: ThemeService) {}
+
+ ngOnInit(): void {
+ this.initializeComponent();
+ this.setupSubscriptions();
+ }
+
+ ngOnDestroy(): void {
+ this.destroy$.next();
+ this.destroy$.complete();
+ }
+
+ // ==========================================================================
+ // INITIALIZATION METHODS
+ // ==========================================================================
+
+ /**
+ * Initializes component state
+ */
+ private initializeComponent(): void {
+ this.availableThemes = this.themeService.getAvailableThemes();
+ this.currentTheme = this.themeService.getCurrentTheme();
+ this.isDarkMode = this.themeService.isDarkMode();
+ this.customColorInput = this.currentTheme.primaryColor;
+ }
+
+ /**
+ * Sets up reactive subscriptions
+ */
+ private setupSubscriptions(): void {
+ // Listen to theme state changes
+ this.themeService.themeState$
+ .pipe(takeUntil(this.destroy$))
+ .subscribe(themeState => {
+ this.currentTheme = themeState;
+ this.availableThemes = this.themeService.getAvailableThemes();
+ });
+
+ // Listen to dark mode changes
+ this.themeService.isDark$
+ .pipe(takeUntil(this.destroy$))
+ .subscribe(isDark => {
+ this.isDarkMode = isDark;
+ });
+ }
+
+ // ==========================================================================
+ // THEME SWITCHING METHODS
+ // ==========================================================================
+
+ /**
+ * Applies a predefined theme
+ * @param themeName - Name of the theme to apply
+ */
+ onThemeChange(themeName: string): void {
+ if (themeName !== this.currentTheme.currentTheme) {
+ this.isApplyingTheme = true;
+ setTimeout(() => {
+ this.themeService.applyTheme(themeName, this.isDarkMode);
+ this.isApplyingTheme = false;
+ }, 100);
+ }
+ }
+
+ /**
+ * Toggles dark mode
+ */
+ onDarkModeToggle(): void {
+ this.themeService.toggleDarkMode();
+ }
+
+ /**
+ * Resets to default theme
+ */
+ onResetTheme(): void {
+ this.isApplyingTheme = true;
+ setTimeout(() => {
+ this.themeService.resetToDefault();
+ this.customColorInput = this.themeService.getCurrentTheme().primaryColor;
+ this.isApplyingTheme = false;
+ }, 100);
+ }
+
+ // ==========================================================================
+ // CUSTOM COLOR METHODS
+ // ==========================================================================
+
+ /**
+ * Handles custom color input changes
+ * @param color - Color input value
+ */
+ onCustomColorChange(color: string): void {
+ this.customColorInput = color;
+ this.validateAndPreviewColor(color);
+ }
+
+ /**
+ * Validates and previews a custom color
+ * @param color - Color to validate and preview
+ */
+ private validateAndPreviewColor(color: string): void {
+ // Clear previous validation
+ this.colorValidation = null;
+ this.colorPreview = {};
+ this.showColorPreview = false;
+
+ if (!color || color.length < 4) {
+ return;
+ }
+
+ try {
+ // Validate color
+ this.colorValidation = this.themeService.validateColor(color);
+
+ // Generate preview if valid
+ if (this.colorValidation?.isValid) {
+ this.colorPreview = this.themeService.generateColorPreview(color);
+ this.showColorPreview = true;
+ }
+ } catch (error) {
+ this.colorValidation = {
+ isValid: false,
+ warnings: ['Invalid color format']
+ };
+ }
+ }
+
+ /**
+ * Applies custom color theme
+ */
+ onApplyCustomColor(): void {
+ if (!this.colorValidation?.isValid) {
+ return;
+ }
+
+ this.isApplyingTheme = true;
+ setTimeout(() => {
+ this.themeService.applyCustomTheme(
+ this.customColorInput,
+ this.customThemeName || 'Custom Theme',
+ this.isDarkMode
+ );
+ this.showCustomColorPanel = false;
+ this.isApplyingTheme = false;
+ }, 100);
+ }
+
+ /**
+ * Toggles custom color panel
+ */
+ onToggleCustomPanel(): void {
+ this.showCustomColorPanel = !this.showCustomColorPanel;
+ if (this.showCustomColorPanel) {
+ this.validateAndPreviewColor(this.customColorInput);
+ }
+ }
+
+ // ==========================================================================
+ // UTILITY METHODS
+ // ==========================================================================
+
+ /**
+ * Gets display name for theme
+ * @param themeName - Theme internal name
+ * @returns Human-readable theme name
+ */
+ getThemeDisplayName(themeName: string): string {
+ const displayNames: { [key: string]: string } = {
+ 'default': 'Material Purple',
+ 'ocean': 'Ocean Blue',
+ 'forest': 'Forest Green',
+ 'sunset': 'Sunset Orange'
+ };
+ return displayNames[themeName] || themeName;
+ }
+
+ /**
+ * Gets CSS class for theme preview
+ * @param themeName - Theme name
+ * @returns CSS class name
+ */
+ getThemePreviewClass(themeName: string): string {
+ return `theme-preview--${themeName}`;
+ }
+
+ /**
+ * Checks if theme is currently active
+ * @param themeName - Theme name to check
+ * @returns True if theme is active
+ */
+ isThemeActive(themeName: string): boolean {
+ return this.currentTheme.currentTheme === themeName;
+ }
+
+ /**
+ * Gets color contrast text color
+ * @param backgroundColor - Background color
+ * @returns Text color (black or white)
+ */
+ getContrastTextColor(backgroundColor: string): string {
+ // Simple contrast check - in production you might want to use ColorUtils.getContrast
+ const hex = backgroundColor.replace('#', '');
+ const r = parseInt(hex.substr(0, 2), 16);
+ const g = parseInt(hex.substr(2, 2), 16);
+ const b = parseInt(hex.substr(4, 2), 16);
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
+ return luminance > 0.5 ? '#000000' : '#FFFFFF';
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/autocomplete/autocomplete.component.scss b/projects/ui-essentials/src/lib/components/forms/autocomplete/autocomplete.component.scss
new file mode 100644
index 0000000..0ece8c7
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/autocomplete/autocomplete.component.scss
@@ -0,0 +1,353 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+.ui-autocomplete {
+ // Core Structure
+ position: relative;
+ display: inline-flex;
+ flex-direction: column;
+ width: 100%;
+
+ // Size Variants
+ &--sm {
+ .ui-autocomplete__input-wrapper {
+ min-height: 40px;
+ }
+
+ .ui-autocomplete__input {
+ font-size: 14px;
+ padding: 8px 12px;
+ }
+
+ .ui-autocomplete__label {
+ font-size: 12px;
+ }
+ }
+
+ &--md {
+ .ui-autocomplete__input-wrapper {
+ min-height: 48px;
+ }
+
+ .ui-autocomplete__input {
+ font-size: 16px;
+ padding: 12px 16px;
+ }
+
+ .ui-autocomplete__label {
+ font-size: 14px;
+ }
+ }
+
+ &--lg {
+ .ui-autocomplete__input-wrapper {
+ min-height: 56px;
+ }
+
+ .ui-autocomplete__input {
+ font-size: 18px;
+ padding: 16px 20px;
+ }
+
+ .ui-autocomplete__label {
+ font-size: 16px;
+ }
+ }
+
+ // Variant Styles
+ &--outlined {
+ .ui-autocomplete__input-wrapper {
+ background: $semantic-color-surface-primary;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-md;
+ }
+
+ &:hover:not(.ui-autocomplete--disabled) .ui-autocomplete__input-wrapper {
+ border-color: $semantic-color-border-focus;
+ }
+ }
+
+ &--filled {
+ .ui-autocomplete__input-wrapper {
+ background: $semantic-color-surface-secondary;
+ border: $semantic-border-width-1 solid transparent;
+ border-radius: $semantic-border-radius-md $semantic-border-radius-md 0 0;
+ border-bottom-color: $semantic-color-border-primary;
+ }
+
+ &:hover:not(.ui-autocomplete--disabled) .ui-autocomplete__input-wrapper {
+ border-bottom-color: $semantic-color-border-focus;
+ }
+ }
+
+ // State Variants
+ &--focused {
+ .ui-autocomplete__input-wrapper {
+ border-color: $semantic-color-brand-primary;
+ box-shadow: $semantic-shadow-button-focus;
+ }
+
+ .ui-autocomplete__label {
+ color: $semantic-color-brand-primary;
+ }
+ }
+
+ &--error {
+ .ui-autocomplete__input-wrapper {
+ border-color: $semantic-color-error;
+ }
+
+ .ui-autocomplete__label {
+ color: $semantic-color-error;
+ }
+ }
+
+ &--disabled {
+ opacity: 0.38;
+ cursor: not-allowed;
+
+ .ui-autocomplete__input {
+ cursor: not-allowed;
+ }
+ }
+
+ &--loading {
+ .ui-autocomplete__input {
+ cursor: wait;
+ }
+ }
+
+ // Label
+ &__label {
+ display: block;
+ margin-bottom: 8px;
+ color: $semantic-color-text-secondary;
+ font-weight: 500;
+ transition: color 0.2s ease;
+
+ &--required::after {
+ content: ' *';
+ color: $semantic-color-error;
+ }
+ }
+
+ // Input Wrapper
+ &__input-wrapper {
+ position: relative;
+ display: flex;
+ align-items: center;
+ transition: all 0.2s ease;
+ }
+
+ // Input Field
+ &__input {
+ flex: 1;
+ border: none;
+ outline: none;
+ background: transparent;
+ color: $semantic-color-text-primary;
+ font-family: inherit;
+ transition: all 0.2s ease;
+
+ &::placeholder {
+ color: $semantic-color-text-tertiary;
+ }
+
+ &:focus {
+ outline: none;
+ }
+
+ &:disabled {
+ color: $semantic-color-text-tertiary;
+ cursor: not-allowed;
+ }
+ }
+
+ // Clear Button
+ &__clear-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 8px;
+ margin-right: 8px;
+ border: none;
+ background: transparent;
+ color: $semantic-color-text-secondary;
+ border-radius: $semantic-border-radius-sm;
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background: $semantic-color-surface-elevated;
+ color: $semantic-color-text-primary;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-brand-primary;
+ outline-offset: 2px;
+ }
+
+ &:disabled {
+ opacity: 0.38;
+ cursor: not-allowed;
+ }
+ }
+
+ // Dropdown Container
+ &__dropdown {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ z-index: 1000;
+ margin-top: 8px;
+ background: $semantic-color-surface-primary;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-md;
+ box-shadow: $semantic-shadow-elevation-3;
+ max-height: 240px;
+ overflow-y: auto;
+
+ &--sm {
+ max-height: 200px;
+ }
+
+ &--lg {
+ max-height: 280px;
+ }
+ }
+
+ // Option Item
+ &__option {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ padding: 12px 16px;
+ border: none;
+ background: transparent;
+ color: $semantic-color-text-primary;
+ font-family: inherit;
+ text-align: left;
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &--sm {
+ padding: 8px 12px;
+ font-size: 14px;
+ }
+
+ &--lg {
+ padding: 16px 20px;
+ font-size: 18px;
+ }
+
+ &:hover {
+ background: $semantic-color-surface-secondary;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-brand-primary;
+ outline-offset: -2px;
+ }
+
+ &--highlighted {
+ background: $semantic-color-surface-secondary;
+ color: $semantic-color-text-primary;
+ }
+
+ &--selected {
+ background: rgba($semantic-color-brand-primary, 0.08);
+ color: $semantic-color-brand-primary;
+ font-weight: 500;
+ }
+
+ &--disabled {
+ opacity: 0.38;
+ cursor: not-allowed;
+
+ &:hover {
+ background: transparent;
+ }
+ }
+ }
+
+ // Option Text
+ &__option-text {
+ flex: 1;
+ }
+
+ // Option Secondary Text
+ &__option-secondary {
+ color: $semantic-color-text-secondary;
+ font-size: 12px;
+ margin-left: 12px;
+ }
+
+ // Loading State
+ &__loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 16px;
+ color: $semantic-color-text-secondary;
+ font-size: 14px;
+ }
+
+ // No Options State
+ &__no-options {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 16px;
+ color: $semantic-color-text-secondary;
+ font-size: 14px;
+ font-style: italic;
+ }
+
+ // Helper Text
+ &__helper-text {
+ margin-top: 8px;
+ color: $semantic-color-text-secondary;
+ font-size: 12px;
+ }
+
+ // Error Text
+ &__error-text {
+ margin-top: 8px;
+ color: $semantic-color-error;
+ font-size: 12px;
+ }
+
+ // Full Width
+ &--full-width {
+ width: 100%;
+ }
+
+ // Dark Mode Support
+ :host-context(.dark-theme) & {
+ .ui-autocomplete__input::placeholder {
+ color: $semantic-color-text-secondary;
+ }
+ }
+
+ // Responsive Design
+ @media (max-width: 768px - 1) {
+ &__dropdown {
+ max-height: 200px;
+ }
+ }
+
+ @media (max-width: 576px - 1) {
+ // Mobile adjustments
+ &__input {
+ font-size: 16px;
+ }
+
+ &__dropdown {
+ max-height: 180px;
+ }
+
+ &__option {
+ padding: 12px 16px;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/autocomplete/autocomplete.component.ts b/projects/ui-essentials/src/lib/components/forms/autocomplete/autocomplete.component.ts
new file mode 100644
index 0000000..eb12d3e
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/autocomplete/autocomplete.component.ts
@@ -0,0 +1,506 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, forwardRef, signal, ViewChild, ElementRef, HostListener } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule, ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { faTimes } from '@fortawesome/free-solid-svg-icons';
+
+export type AutocompleteSize = 'sm' | 'md' | 'lg';
+export type AutocompleteVariant = 'outlined' | 'filled';
+
+export interface AutocompleteOption {
+ value: any;
+ label: string;
+ secondaryText?: string;
+ disabled?: boolean;
+}
+
+@Component({
+ selector: 'ui-autocomplete',
+ standalone: true,
+ imports: [CommonModule, FormsModule, FontAwesomeModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => AutocompleteComponent),
+ multi: true
+ }
+ ],
+ template: `
+
+ @if (label) {
+
+ {{ label }}
+
+ }
+
+
+ = 0 ? autocompleteId + '-option-' + highlightedIndex() : null"
+ [attr.aria-describedby]="getAriaDescribedBy()"
+ [attr.aria-invalid]="hasError"
+ (input)="onInput($event)"
+ (focus)="onFocus()"
+ (blur)="onBlur()"
+ (keydown)="onKeyDown($event)"
+ autocomplete="off"
+ />
+
+ @if (displayValue() && clearable && !disabled && !readonly) {
+
+
+
+ }
+
+
+ @if (isDropdownOpen()) {
+
+ @if (loading) {
+
+ Loading...
+
+ } @else if (filteredOptions().length === 0) {
+
+ {{ noOptionsText }}
+
+ } @else {
+ @for (option of filteredOptions(); track option.value; let index = $index) {
+
+ {{ option.label }}
+ @if (option.secondaryText) {
+ {{ option.secondaryText }}
+ }
+
+ }
+ }
+
+ }
+
+ @if (helperText && !hasError) {
+
+ {{ helperText }}
+
+ }
+
+ @if (errorText && hasError) {
+
+ {{ errorText }}
+
+ }
+
+ `,
+ styleUrl: './autocomplete.component.scss'
+})
+export class AutocompleteComponent implements ControlValueAccessor {
+ @ViewChild('inputElement', { static: true }) inputElement!: ElementRef;
+
+ // Core inputs
+ @Input() label: string = '';
+ @Input() placeholder: string = 'Search...';
+ @Input() helperText: string = '';
+ @Input() errorText: string = '';
+ @Input() size: AutocompleteSize = 'md';
+ @Input() variant: AutocompleteVariant = 'outlined';
+
+ // Options and filtering
+ @Input() options: AutocompleteOption[] = [];
+ @Input() filterFn?: (option: AutocompleteOption, query: string) => boolean;
+ @Input() minQueryLength: number = 0;
+ @Input() maxOptions: number = 100;
+ @Input() noOptionsText: string = 'No options found';
+
+ // Behavior
+ @Input() disabled: boolean = false;
+ @Input() readonly: boolean = false;
+ @Input() required: boolean = false;
+ @Input() clearable: boolean = true;
+ @Input() loading: boolean = false;
+ @Input() fullWidth: boolean = false;
+ @Input() openOnFocus: boolean = false;
+ @Input() closeOnSelect: boolean = true;
+
+ // Accessibility
+ @Input() ariaLabel: string = '';
+ @Input() autocompleteId: string = `ui-autocomplete-${Math.random().toString(36).substr(2, 9)}`;
+
+ // Outputs
+ @Output() valueChange = new EventEmitter();
+ @Output() optionSelected = new EventEmitter();
+ @Output() queryChange = new EventEmitter();
+ @Output() focused = new EventEmitter();
+ @Output() blurred = new EventEmitter();
+ @Output() dropdownOpen = new EventEmitter();
+ @Output() dropdownClose = new EventEmitter();
+
+ // Font Awesome icons
+ protected readonly faTimes = faTimes;
+
+ // Internal state
+ private _selectedOption = signal(null);
+ private _query = signal('');
+ private _displayValue = signal('');
+ private _isFocused = signal(false);
+ private _isDropdownOpen = signal(false);
+ private _highlightedIndex = signal(-1);
+ private _filteredOptions = signal([]);
+
+ // Public signals
+ selectedOption = this._selectedOption.asReadonly();
+ query = this._query.asReadonly();
+ displayValue = this._displayValue.asReadonly();
+ isFocused = this._isFocused.asReadonly();
+ isDropdownOpen = this._isDropdownOpen.asReadonly();
+ highlightedIndex = this._highlightedIndex.asReadonly();
+ filteredOptions = this._filteredOptions.asReadonly();
+
+ // ControlValueAccessor implementation
+ private onChange = (value: any) => {};
+ private onTouched = () => {};
+
+ get hasError(): boolean {
+ return !!this.errorText;
+ }
+
+ ngOnInit(): void {
+ this.updateFilteredOptions();
+ }
+
+ ngOnChanges(): void {
+ this.updateFilteredOptions();
+ }
+
+ // ControlValueAccessor methods
+ writeValue(value: any): void {
+ if (value) {
+ const option = this.options.find(opt => opt.value === value);
+ if (option) {
+ this._selectedOption.set(option);
+ this._displayValue.set(option.label);
+ this._query.set(option.label);
+ } else {
+ // If value doesn't match any option, treat as free text
+ this._selectedOption.set(null);
+ this._displayValue.set(String(value));
+ this._query.set(String(value));
+ }
+ } else {
+ this.clearValue();
+ }
+ this.updateFilteredOptions();
+ }
+
+ registerOnChange(fn: (value: any) => void): void {
+ this.onChange = fn;
+ }
+
+ registerOnTouched(fn: () => void): void {
+ this.onTouched = fn;
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ }
+
+ // Event handlers
+ onInput(event: Event): void {
+ if (this.readonly) return;
+
+ const target = event.target as HTMLInputElement;
+ const value = target.value;
+
+ this._query.set(value);
+ this._displayValue.set(value);
+ this._selectedOption.set(null);
+ this._highlightedIndex.set(-1);
+
+ this.updateFilteredOptions();
+ this.openDropdown();
+
+ this.onChange(value);
+ this.valueChange.emit(value);
+ this.queryChange.emit(value);
+ }
+
+ onFocus(): void {
+ this._isFocused.set(true);
+ this.focused.emit();
+
+ if (this.openOnFocus) {
+ this.openDropdown();
+ }
+ }
+
+ onBlur(): void {
+ // Delay to allow for option selection
+ setTimeout(() => {
+ this._isFocused.set(false);
+ this.closeDropdown();
+ this.onTouched();
+ this.blurred.emit();
+ }, 150);
+ }
+
+ onKeyDown(event: KeyboardEvent): void {
+ if (this.readonly) return;
+
+ switch (event.key) {
+ case 'ArrowDown':
+ event.preventDefault();
+ this.highlightNext();
+ this.openDropdown();
+ break;
+
+ case 'ArrowUp':
+ event.preventDefault();
+ this.highlightPrevious();
+ this.openDropdown();
+ break;
+
+ case 'Enter':
+ event.preventDefault();
+ if (this.isDropdownOpen() && this.highlightedIndex() >= 0) {
+ const option = this.filteredOptions()[this.highlightedIndex()];
+ if (option && !option.disabled) {
+ this.selectOption(option);
+ }
+ }
+ break;
+
+ case 'Escape':
+ this.closeDropdown();
+ this.inputElement.nativeElement.blur();
+ break;
+
+ case 'Tab':
+ this.closeDropdown();
+ break;
+ }
+ }
+
+ // Option selection
+ selectOption(option: AutocompleteOption): void {
+ if (option.disabled) return;
+
+ this._selectedOption.set(option);
+ this._displayValue.set(option.label);
+ this._query.set(option.label);
+ this._highlightedIndex.set(-1);
+
+ if (this.closeOnSelect) {
+ this.closeDropdown();
+ }
+
+ this.onChange(option.value);
+ this.valueChange.emit(option.value);
+ this.optionSelected.emit(option);
+
+ // Return focus to input
+ this.inputElement.nativeElement.focus();
+ }
+
+ clearValue(): void {
+ this._selectedOption.set(null);
+ this._displayValue.set('');
+ this._query.set('');
+ this._highlightedIndex.set(-1);
+
+ this.updateFilteredOptions();
+ this.closeDropdown();
+
+ this.onChange(null);
+ this.valueChange.emit(null);
+ this.queryChange.emit('');
+
+ if (this.inputElement) {
+ this.inputElement.nativeElement.focus();
+ }
+ }
+
+ // Dropdown management
+ openDropdown(): void {
+ if (this.disabled || this.readonly) return;
+
+ if (this.query().length >= this.minQueryLength || this.filteredOptions().length > 0) {
+ this._isDropdownOpen.set(true);
+ this.dropdownOpen.emit();
+ }
+ }
+
+ closeDropdown(): void {
+ this._isDropdownOpen.set(false);
+ this._highlightedIndex.set(-1);
+ this.dropdownClose.emit();
+ }
+
+ // Navigation
+ highlightNext(): void {
+ const options = this.filteredOptions();
+ if (options.length === 0) return;
+
+ const currentIndex = this.highlightedIndex();
+ let nextIndex = currentIndex + 1;
+
+ // Skip disabled options
+ while (nextIndex < options.length && options[nextIndex].disabled) {
+ nextIndex++;
+ }
+
+ if (nextIndex >= options.length) {
+ // Wrap to first enabled option
+ nextIndex = 0;
+ while (nextIndex < options.length && options[nextIndex].disabled) {
+ nextIndex++;
+ }
+ }
+
+ if (nextIndex < options.length) {
+ this._highlightedIndex.set(nextIndex);
+ }
+ }
+
+ highlightPrevious(): void {
+ const options = this.filteredOptions();
+ if (options.length === 0) return;
+
+ const currentIndex = this.highlightedIndex();
+ let prevIndex = currentIndex - 1;
+
+ // Skip disabled options
+ while (prevIndex >= 0 && options[prevIndex].disabled) {
+ prevIndex--;
+ }
+
+ if (prevIndex < 0) {
+ // Wrap to last enabled option
+ prevIndex = options.length - 1;
+ while (prevIndex >= 0 && options[prevIndex].disabled) {
+ prevIndex--;
+ }
+ }
+
+ if (prevIndex >= 0) {
+ this._highlightedIndex.set(prevIndex);
+ }
+ }
+
+ setHighlightedIndex(index: number): void {
+ this._highlightedIndex.set(index);
+ }
+
+ // Utility methods
+ isSelected(option: AutocompleteOption): boolean {
+ const selected = this.selectedOption();
+ return selected ? selected.value === option.value : false;
+ }
+
+ getAriaDescribedBy(): string | null {
+ const ids: string[] = [];
+
+ if (this.helperText && !this.hasError) {
+ ids.push(this.autocompleteId + '-helper');
+ }
+
+ if (this.errorText && this.hasError) {
+ ids.push(this.autocompleteId + '-error');
+ }
+
+ return ids.length > 0 ? ids.join(' ') : null;
+ }
+
+ private updateFilteredOptions(): void {
+ const query = this.query();
+ const options = this.options;
+
+ if (query.length < this.minQueryLength) {
+ this._filteredOptions.set([]);
+ return;
+ }
+
+ let filtered: AutocompleteOption[];
+
+ if (this.filterFn) {
+ filtered = options.filter(option => this.filterFn!(option, query));
+ } else {
+ // Default filtering: case-insensitive substring match on label
+ const lowerQuery = query.toLowerCase();
+ filtered = options.filter(option =>
+ option.label.toLowerCase().includes(lowerQuery) ||
+ (option.secondaryText && option.secondaryText.toLowerCase().includes(lowerQuery))
+ );
+ }
+
+ // Limit results
+ if (filtered.length > this.maxOptions) {
+ filtered = filtered.slice(0, this.maxOptions);
+ }
+
+ this._filteredOptions.set(filtered);
+
+ // Reset highlighted index if out of bounds
+ if (this.highlightedIndex() >= filtered.length) {
+ this._highlightedIndex.set(-1);
+ }
+ }
+
+ // Close dropdown when clicking outside
+ @HostListener('document:click', ['$event'])
+ onDocumentClick(event: Event): void {
+ const target = event.target as Element;
+ if (!target.closest('.ui-autocomplete')) {
+ this.closeDropdown();
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/autocomplete/index.ts b/projects/ui-essentials/src/lib/components/forms/autocomplete/index.ts
new file mode 100644
index 0000000..02025f4
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/autocomplete/index.ts
@@ -0,0 +1 @@
+export * from './autocomplete.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/checkbox/checkbox.component.scss b/projects/ui-essentials/src/lib/components/forms/checkbox/checkbox.component.scss
new file mode 100644
index 0000000..7fa9c4d
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/checkbox/checkbox.component.scss
@@ -0,0 +1,332 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+// Tokens available globally via main application styles
+
+// ==========================================================================
+// CHECKBOX COMPONENT STYLES
+// ==========================================================================
+// Modern checkbox implementation using design tokens
+// Follows Material Design 3 principles with semantic token system
+// ==========================================================================
+
+.checkbox-wrapper {
+ display: flex;
+ align-items: flex-start;
+ position: relative;
+ cursor: pointer;
+ user-select: none;
+ transition: all $semantic-duration-fast $semantic-easing-standard;
+
+ &:hover:not(.checkbox-wrapper--disabled) {
+ .checkbox-control {
+ border-color: $semantic-color-border-focus;
+ box-shadow: $semantic-shadow-input-focus;
+ }
+ }
+
+ &:focus-within:not(.checkbox-wrapper--disabled) {
+ .checkbox-control {
+ border-color: $semantic-color-brand-primary;
+ box-shadow: $semantic-shadow-button-focus;
+ }
+ }
+
+ // Disabled state
+ &.checkbox-wrapper--disabled {
+ cursor: not-allowed;
+ opacity: 0.6;
+
+ .checkbox-control {
+ border-color: $semantic-color-border-disabled;
+ background-color: $semantic-color-surface-disabled;
+ }
+
+ .checkbox-content {
+ color: $semantic-color-text-disabled;
+ }
+ }
+
+ // Checked state
+ &.checkbox-wrapper--checked:not(.checkbox-wrapper--disabled) {
+ .checkbox-control {
+ background-color: $semantic-color-brand-primary;
+ border-color: $semantic-color-brand-primary;
+ }
+
+ .checkbox-icon {
+ color: $semantic-color-on-brand-primary;
+ }
+ }
+
+ // Size variants with proper spacing and alignment
+ &.checkbox-wrapper--sm {
+ gap: $semantic-spacing-component-sm; // 8px spacing
+ align-items: center; // Center align for single line content
+
+ .checkbox-control {
+ height: 20px;
+ width: 20px;
+ }
+
+ .checkbox-label {
+ font-size: $semantic-typography-font-size-sm;
+ line-height: $semantic-typography-line-height-normal;
+ }
+
+ .checkbox-description {
+ font-size: $semantic-typography-font-size-xs;
+ line-height: $semantic-typography-line-height-tight;
+ }
+
+ // If description exists, align to flex-start for multi-line content
+ &.checkbox-wrapper--with-description {
+ align-items: flex-start;
+ }
+ }
+
+ &.checkbox-wrapper--md {
+ gap: $semantic-spacing-component-md; // 12px spacing
+ align-items: center; // Center align for single line content
+
+ .checkbox-control {
+ height: 24px;
+ width: 24px;
+ }
+
+ .checkbox-label {
+ font-size: $semantic-typography-font-size-md;
+ line-height: $semantic-typography-line-height-relaxed;
+ }
+
+ .checkbox-description {
+ font-size: $semantic-typography-font-size-sm;
+ line-height: $semantic-typography-line-height-normal;
+ }
+
+ // If description exists, align to flex-start for multi-line content
+ &.checkbox-wrapper--with-description {
+ align-items: flex-start;
+ }
+ }
+
+ &.checkbox-wrapper--lg {
+ gap: $semantic-spacing-component-md; // 12px spacing (within range)
+ align-items: center; // Center align for single line content
+
+ .checkbox-control {
+ height: 28px;
+ width: 28px;
+ }
+
+ .checkbox-label {
+ font-size: $semantic-typography-font-size-lg;
+ line-height: $semantic-typography-line-height-relaxed;
+ }
+
+ .checkbox-description {
+ font-size: $semantic-typography-font-size-md;
+ line-height: $semantic-typography-line-height-relaxed;
+ }
+
+ // If description exists, align to flex-start for multi-line content
+ &.checkbox-wrapper--with-description {
+ align-items: flex-start;
+ }
+ }
+
+ // Variant colors
+ &.checkbox-wrapper--primary.checkbox-wrapper--checked:not(.checkbox-wrapper--disabled) {
+ .checkbox-control {
+ background-color: $semantic-color-brand-primary;
+ border-color: $semantic-color-brand-primary;
+ }
+ }
+
+ &.checkbox-wrapper--secondary.checkbox-wrapper--checked:not(.checkbox-wrapper--disabled) {
+ .checkbox-control {
+ background-color: $semantic-color-brand-secondary;
+ border-color: $semantic-color-brand-secondary;
+ }
+ }
+
+ &.checkbox-wrapper--success.checkbox-wrapper--checked:not(.checkbox-wrapper--disabled) {
+ .checkbox-control {
+ background-color: $semantic-color-success;
+ border-color: $semantic-color-success;
+ }
+
+ .checkbox-icon {
+ color: $semantic-color-on-success;
+ }
+ }
+
+ &.checkbox-wrapper--warning.checkbox-wrapper--checked:not(.checkbox-wrapper--disabled) {
+ .checkbox-control {
+ background-color: $semantic-color-warning;
+ border-color: $semantic-color-warning;
+ }
+
+ .checkbox-icon {
+ color: $semantic-color-on-warning;
+ }
+ }
+
+ &.checkbox-wrapper--danger.checkbox-wrapper--checked:not(.checkbox-wrapper--disabled) {
+ .checkbox-control {
+ background-color: $semantic-color-danger;
+ border-color: $semantic-color-danger;
+ }
+
+ .checkbox-icon {
+ color: $semantic-color-on-danger;
+ }
+ }
+
+ // Error state
+ &.checkbox-wrapper--error:not(.checkbox-wrapper--disabled) {
+ .checkbox-control {
+ border-color: $semantic-color-danger;
+ }
+
+ .checkbox-label {
+ color: $semantic-color-danger;
+ }
+ }
+}
+
+.checkbox-input {
+ position: absolute;
+ opacity: 0;
+ cursor: pointer;
+ height: 0;
+ width: 0;
+ margin: 0;
+ padding: 0;
+}
+
+.checkbox-control {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ background-color: $semantic-color-surface-primary;
+ border: $semantic-border-width-2 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-sm;
+ transition: all $semantic-duration-fast $semantic-easing-standard;
+
+ // Base sizes (overridden by wrapper variants)
+ height: 24px;
+ width: 24px;
+}
+
+.checkbox-checkmark {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0;
+ transform: scale(0.8);
+ transition: all $semantic-duration-fast $semantic-easing-spring;
+
+ &.checkbox-checkmark--checked {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+.checkbox-icon {
+ width: 60%;
+ height: 60%;
+ color: transparent;
+ transition: color $semantic-duration-fast $semantic-easing-standard;
+}
+
+.checkbox-content {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-component-xs;
+ // Removed margin-left as it's now handled by wrapper gap
+}
+
+.checkbox-label {
+ color: $semantic-color-text-primary;
+ font-weight: $semantic-typography-font-weight-medium;
+ transition: color $semantic-duration-fast $semantic-easing-standard;
+
+ &.checkbox-label--required {
+ .checkbox-required-indicator {
+ color: $semantic-color-danger;
+ margin-left: 2px;
+ }
+ }
+}
+
+.checkbox-description {
+ color: $semantic-color-text-secondary;
+ margin-top: $semantic-spacing-component-xs;
+ transition: color $semantic-duration-fast $semantic-easing-standard;
+}
+
+// Content disabled state
+.checkbox-content--disabled {
+ .checkbox-label,
+ .checkbox-description {
+ color: $semantic-color-text-disabled;
+ }
+}
+
+// ==========================================================================
+// RESPONSIVE DESIGN
+// ==========================================================================
+
+@media (max-width: $semantic-breakpoint-md - 1) {
+ .checkbox-wrapper {
+ // Slightly larger touch targets on mobile
+ &.checkbox-wrapper--sm .checkbox-control {
+ height: calc(#{20px} + 4px);
+ width: calc(#{20px} + 4px);
+ }
+
+ &.checkbox-wrapper--md .checkbox-control {
+ height: calc(#{24px} + 4px);
+ width: calc(#{24px} + 4px);
+ }
+
+ &.checkbox-wrapper--lg .checkbox-control {
+ height: calc(#{28px} + 4px);
+ width: calc(#{28px} + 4px);
+ }
+ }
+}
+
+// ==========================================================================
+// ACCESSIBILITY ENHANCEMENTS
+// ==========================================================================
+
+// Reduced motion preference
+@media (prefers-reduced-motion: reduce) {
+ .checkbox-wrapper,
+ .checkbox-control,
+ .checkbox-checkmark,
+ .checkbox-icon,
+ .checkbox-label,
+ .checkbox-description {
+ transition: none !important;
+ animation: none !important;
+ }
+}
+
+// High contrast mode
+@media (prefers-contrast: high) {
+ .checkbox-wrapper {
+ .checkbox-control {
+ border-width: $semantic-border-width-3;
+ }
+
+ &.checkbox-wrapper--checked:not(.checkbox-wrapper--disabled) {
+ .checkbox-control {
+ border-width: $semantic-border-width-3;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/checkbox/checkbox.component.ts b/projects/ui-essentials/src/lib/components/forms/checkbox/checkbox.component.ts
new file mode 100644
index 0000000..c79e271
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/checkbox/checkbox.component.ts
@@ -0,0 +1,156 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, forwardRef, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+
+export type CheckboxSize = 'sm' | 'md' | 'lg';
+export type CheckboxVariant = 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
+export type CheckboxState = 'default' | 'error' | 'disabled';
+
+@Component({
+ selector: 'ui-checkbox',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [CommonModule],
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => CheckboxComponent),
+ multi: true
+ }
+ ],
+ template: `
+
+
+
+
+ @if (label || description) {
+
+ @if (label) {
+
+ {{ label }}
+ @if (required) {
+ *
+ }
+
+ }
+ @if (description) {
+
{{ description }}
+ }
+
+ }
+
+ `,
+ styleUrls: ['./checkbox.component.scss']
+})
+export class CheckboxComponent implements ControlValueAccessor {
+ @Input() label: string = '';
+ @Input() description: string = '';
+ @Input() size: CheckboxSize = 'md';
+ @Input() variant: CheckboxVariant = 'primary';
+ @Input() state: CheckboxState = 'default';
+ @Input() disabled = false;
+ @Input() required = false;
+ @Input() checkboxId: string = `checkbox-${Math.random().toString(36).substr(2, 9)}`;
+
+ @Output() checkboxChange = new EventEmitter();
+ @Output() checkboxFocus = new EventEmitter();
+ @Output() checkboxBlur = new EventEmitter();
+
+ private checked = signal(false);
+
+ // ControlValueAccessor implementation
+ private onChange = (value: boolean) => {};
+ private onTouched = () => {};
+
+ isChecked(): boolean {
+ return this.checked();
+ }
+
+ writeValue(value: boolean): void {
+ this.checked.set(!!value);
+ }
+
+ registerOnChange(fn: (value: boolean) => void): void {
+ this.onChange = fn;
+ }
+
+ registerOnTouched(fn: () => void): void {
+ this.onTouched = fn;
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ }
+
+ getWrapperClasses(): string {
+ const classes = [
+ `checkbox-wrapper--${this.size}`,
+ `checkbox-wrapper--${this.variant}`,
+ `checkbox-wrapper--${this.state}`
+ ];
+
+ if (this.disabled) classes.push('checkbox-wrapper--disabled');
+ if (this.isChecked()) classes.push('checkbox-wrapper--checked');
+ if (this.description) classes.push('checkbox-wrapper--with-description');
+
+ return classes.join(' ');
+ }
+
+ getControlClasses(): string {
+ const classes = [
+ `checkbox-control--${this.size}`,
+ `checkbox-control--${this.variant}`,
+ `checkbox-control--${this.state}`
+ ];
+
+ if (this.disabled) classes.push('checkbox-control--disabled');
+ if (this.isChecked()) classes.push('checkbox-control--checked');
+
+ return classes.join(' ');
+ }
+
+ getContentClasses(): string {
+ const classes = [
+ `checkbox-content--${this.size}`
+ ];
+
+ if (this.disabled) classes.push('checkbox-content--disabled');
+
+ return classes.join(' ');
+ }
+
+ onCheckboxChange(event: Event): void {
+ const target = event.target as HTMLInputElement;
+ const value = target.checked;
+
+ this.checked.set(value);
+ this.onChange(value);
+ this.checkboxChange.emit(value);
+ }
+
+ onFocus(event: FocusEvent): void {
+ this.checkboxFocus.emit(event);
+ }
+
+ onBlur(event: FocusEvent): void {
+ this.onTouched();
+ this.checkboxBlur.emit(event);
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/checkbox/index.ts b/projects/ui-essentials/src/lib/components/forms/checkbox/index.ts
new file mode 100644
index 0000000..c0b086d
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/checkbox/index.ts
@@ -0,0 +1 @@
+export * from './checkbox.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/date-picker/date-picker.component.scss b/projects/ui-essentials/src/lib/components/forms/date-picker/date-picker.component.scss
new file mode 100644
index 0000000..65eb7af
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/date-picker/date-picker.component.scss
@@ -0,0 +1,504 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+.ui-date-picker {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ position: relative;
+
+ // ==========================================================================
+ // SIZE VARIANTS
+ // ==========================================================================
+
+ &--sm {
+ .ui-date-picker__container {
+ min-height: 36px;
+ }
+
+ .ui-date-picker__field {
+ font-size: 0.875rem;
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ }
+
+ .ui-date-picker__label {
+ font-size: 0.8125rem;
+ margin-bottom: $semantic-spacing-component-xs;
+ }
+ }
+
+ &--md {
+ .ui-date-picker__container {
+ min-height: 48px;
+ }
+
+ .ui-date-picker__field {
+ font-size: 1rem;
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
+ }
+
+ .ui-date-picker__label {
+ font-size: 0.875rem;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+ }
+
+ &--lg {
+ .ui-date-picker__container {
+ min-height: 56px;
+ }
+
+ .ui-date-picker__field {
+ font-size: 1.125rem;
+ padding: $semantic-spacing-component-md $semantic-spacing-component-lg;
+ }
+
+ .ui-date-picker__label {
+ font-size: 1rem;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+ }
+
+ // ==========================================================================
+ // VARIANT STYLES
+ // ==========================================================================
+
+ &--outlined {
+ .ui-date-picker__container {
+ border: $semantic-border-width-2 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-md;
+ background-color: $semantic-color-surface-primary;
+ }
+
+ .ui-date-picker__field {
+ border: none;
+ background: transparent;
+ }
+ }
+
+ &--filled {
+ .ui-date-picker__container {
+ background-color: $semantic-color-surface-secondary;
+ border: $semantic-border-width-2 solid transparent;
+ border-radius: $semantic-border-radius-md;
+ border-bottom: 3px solid $semantic-color-border-primary;
+ }
+
+ .ui-date-picker__field {
+ border: none;
+ background: transparent;
+ }
+ }
+
+ &--underlined {
+ .ui-date-picker__container {
+ background: transparent;
+ border: none;
+ border-bottom: $semantic-border-width-2 solid $semantic-color-border-primary;
+ border-radius: 0;
+ }
+
+ .ui-date-picker__field {
+ border: none;
+ background: transparent;
+ }
+ }
+
+ // ==========================================================================
+ // STATE VARIANTS
+ // ==========================================================================
+
+ &--error {
+ .ui-date-picker__container {
+ border-color: $semantic-color-error;
+ }
+
+ .ui-date-picker__label {
+ color: $semantic-color-error;
+ }
+ }
+
+ &--success {
+ .ui-date-picker__container {
+ border-color: $semantic-color-success;
+ }
+ }
+
+ &--warning {
+ .ui-date-picker__container {
+ border-color: $semantic-color-warning;
+ }
+ }
+
+ &--disabled {
+ opacity: 0.6;
+ pointer-events: none;
+
+ .ui-date-picker__container {
+ background: $semantic-color-surface-secondary;
+ cursor: not-allowed;
+ }
+ }
+
+ &--open {
+ .ui-date-picker__container {
+ border-color: $semantic-color-primary;
+ box-shadow: $semantic-shadow-input-focus;
+ }
+ }
+
+ // ==========================================================================
+ // COMPONENT ELEMENTS
+ // ==========================================================================
+
+ &__label {
+ font-weight: $semantic-typography-font-weight-medium;
+ color: $semantic-color-text-primary;
+ line-height: $semantic-typography-line-height-tight;
+
+ &--required {
+ .ui-date-picker__required-indicator {
+ color: $semantic-color-error;
+ margin-left: $semantic-spacing-component-xs;
+ }
+ }
+ }
+
+ &__container {
+ display: flex;
+ align-items: center;
+ position: relative;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease-in-out;
+ cursor: pointer;
+
+ &:hover:not(.ui-date-picker--disabled &) {
+ border-color: $semantic-color-border-secondary;
+ }
+
+ &--has-clear {
+ .ui-date-picker__field {
+ padding-right: $semantic-spacing-component-xl;
+ }
+ }
+ }
+
+ &__prefix-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-left: $semantic-spacing-component-sm;
+ color: $semantic-color-text-tertiary;
+ pointer-events: none;
+ }
+
+ &__field {
+ flex: 1;
+ border: none;
+ outline: none;
+ background: transparent;
+ color: $semantic-color-text-primary;
+ cursor: pointer;
+ font-family: inherit;
+
+ &::placeholder {
+ color: $semantic-color-text-tertiary;
+ }
+
+ &:focus {
+ outline: none;
+ }
+ }
+
+ &__clear-btn {
+ position: absolute;
+ right: $semantic-spacing-component-sm;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: $semantic-sizing-icon-button;
+ height: $semantic-sizing-icon-button;
+ border: none;
+ background: transparent;
+ color: $semantic-color-text-tertiary;
+ cursor: pointer;
+ border-radius: $semantic-border-radius-sm;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease-in-out;
+
+ &:hover {
+ background: $semantic-color-surface-secondary;
+ color: $semantic-color-text-secondary;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+ }
+
+ &__helper-text {
+ font-size: $semantic-typography-font-size-sm;
+ margin-top: $semantic-spacing-component-xs;
+ line-height: $semantic-typography-line-height-normal;
+
+ &--error {
+ color: $semantic-color-error;
+ }
+
+ &--success {
+ color: $semantic-color-success;
+ }
+
+ &--warning {
+ color: $semantic-color-warning;
+ }
+
+ &--default {
+ color: $semantic-color-text-secondary;
+ }
+ }
+
+ // ==========================================================================
+ // CALENDAR DROPDOWN
+ // ==========================================================================
+
+ &__dropdown {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ z-index: 1000;
+ margin-top: $semantic-spacing-component-xs;
+ background: $semantic-color-surface-primary;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-lg;
+ box-shadow: $semantic-shadow-dropdown;
+ padding: $semantic-spacing-component-md;
+ min-width: 280px;
+ }
+
+ &__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: $semantic-spacing-component-md;
+ }
+
+ &__nav-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: $semantic-sizing-icon-navigation;
+ height: $semantic-sizing-icon-navigation;
+ border: none;
+ background: transparent;
+ color: $semantic-color-text-secondary;
+ cursor: pointer;
+ border-radius: $semantic-border-radius-sm;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease-in-out;
+
+ &:hover {
+ background: $semantic-color-surface-secondary;
+ color: $semantic-color-text-primary;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+ }
+
+ &__month-year {
+ flex: 1;
+ text-align: center;
+ }
+
+ &__month-btn {
+ border: none;
+ background: transparent;
+ color: $semantic-color-text-primary;
+ font-size: 1rem;
+ font-weight: $semantic-typography-font-weight-medium;
+ cursor: pointer;
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ border-radius: $semantic-border-radius-sm;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease-in-out;
+
+ &:hover {
+ background: $semantic-color-surface-secondary;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+ }
+
+ &__calendar {
+ width: 100%;
+ }
+
+ &__day-headers {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ gap: $semantic-spacing-component-xs;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+
+ &__day-header {
+ text-align: center;
+ font-size: $semantic-typography-font-size-sm;
+ font-weight: $semantic-typography-font-weight-medium;
+ color: $semantic-color-text-secondary;
+ padding: $semantic-spacing-component-xs;
+ }
+
+ &__days {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ gap: $semantic-spacing-component-xs;
+ }
+
+ &__day {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ border: none;
+ background: transparent;
+ color: $semantic-color-text-primary;
+ cursor: pointer;
+ border-radius: $semantic-border-radius-sm;
+ font-size: 0.875rem;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease-in-out;
+
+ &:hover:not(&--disabled) {
+ background: $semantic-color-surface-secondary;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+
+ &--today {
+ background: $semantic-color-primary-container;
+ color: $semantic-color-primary;
+ font-weight: $semantic-typography-font-weight-medium;
+ }
+
+ &--selected {
+ background: $semantic-color-primary;
+ color: $semantic-color-on-primary;
+ font-weight: $semantic-typography-font-weight-medium;
+
+ &:hover {
+ background: $semantic-color-primary-focus;
+ }
+ }
+
+ &--other-month {
+ color: $semantic-color-text-tertiary;
+ }
+
+ &--disabled {
+ color: $semantic-color-text-disabled;
+ cursor: not-allowed;
+ opacity: 0.5;
+ }
+ }
+
+ // ==========================================================================
+ // MONTH/YEAR PICKER
+ // ==========================================================================
+
+ &__month-year-picker {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-component-md;
+ }
+
+ &__months {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: $semantic-spacing-component-xs;
+ }
+
+ &__month-option {
+ padding: $semantic-spacing-component-sm;
+ border: none;
+ background: transparent;
+ color: $semantic-color-text-primary;
+ cursor: pointer;
+ border-radius: $semantic-border-radius-sm;
+ font-size: 0.875rem;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease-in-out;
+
+ &:hover {
+ background: $semantic-color-surface-secondary;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+
+ &--selected {
+ background: $semantic-color-primary;
+ color: $semantic-color-on-primary;
+ font-weight: $semantic-typography-font-weight-medium;
+
+ &:hover {
+ background: $semantic-color-primary-focus;
+ }
+ }
+ }
+
+ &__year-input {
+ display: flex;
+ justify-content: center;
+ }
+
+ &__year-field {
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-md;
+ background: $semantic-color-surface-primary;
+ color: $semantic-color-text-primary;
+ font-size: 1rem;
+ text-align: center;
+ width: 80px;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease-in-out;
+
+ &:focus {
+ outline: none;
+ border-color: $semantic-color-primary;
+ box-shadow: $semantic-shadow-input-focus;
+ }
+
+ &::-webkit-outer-spin-button,
+ &::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
+
+ &[type=number] {
+ -moz-appearance: textfield;
+ }
+ }
+
+ // ==========================================================================
+ // RESPONSIVE DESIGN
+ // ==========================================================================
+
+ @media (max-width: 768px) {
+ &__dropdown {
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ right: auto;
+ transform: translate(-50%, -50%);
+ width: 90vw;
+ max-width: 320px;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/date-picker/date-picker.component.ts b/projects/ui-essentials/src/lib/components/forms/date-picker/date-picker.component.ts
new file mode 100644
index 0000000..39bf607
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/date-picker/date-picker.component.ts
@@ -0,0 +1,433 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, forwardRef, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
+import { faCalendarDays, faTimes, faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons';
+
+export type DatePickerSize = 'sm' | 'md' | 'lg';
+export type DatePickerVariant = 'outlined' | 'filled' | 'underlined';
+export type DatePickerState = 'default' | 'error' | 'success' | 'warning';
+
+@Component({
+ selector: 'ui-date-picker',
+ standalone: true,
+ imports: [CommonModule, FontAwesomeModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => DatePickerComponent),
+ multi: true
+ }
+ ],
+ template: `
+
+
+ @if (label) {
+
+ {{ label }}
+ @if (required) {
+ *
+ }
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+ @if (clearable && value && !disabled) {
+
+
+
+ }
+
+
+
+ @if (isOpen()) {
+
+
+
+
+
+ @if (!showMonthView()) {
+
+
+
+
+
+
+
+ @for (day of calendarDays; track day.date.getTime()) {
+
+ {{ day.date.getDate() }}
+
+ }
+
+
+ } @else {
+
+
+
+ @for (month of months; track month.value) {
+
+ {{ month.label }}
+
+ }
+
+
+
+
+
+
+ }
+
+ }
+
+
+ @if (helperText || errorMessage) {
+
+ {{ state === 'error' ? errorMessage : helperText }}
+
+ }
+
+ `,
+ styleUrl: './date-picker.component.scss'
+})
+export class DatePickerComponent implements ControlValueAccessor {
+ @Input() label: string = '';
+ @Input() placeholder: string = 'Select date';
+ @Input() size: DatePickerSize = 'md';
+ @Input() variant: DatePickerVariant = 'outlined';
+ @Input() state: DatePickerState = 'default';
+ @Input() disabled = false;
+ @Input() required = false;
+ @Input() clearable = true;
+ @Input() helperText: string = '';
+ @Input() errorMessage: string = '';
+ @Input() minDate: Date | null = null;
+ @Input() maxDate: Date | null = null;
+ @Input() dateFormat: string = 'MM/dd/yyyy';
+ @Input() inputId: string = `date-picker-${Math.random().toString(36).substr(2, 9)}`;
+
+ @Output() dateChange = new EventEmitter();
+ @Output() calendarOpen = new EventEmitter();
+ @Output() calendarClose = new EventEmitter();
+
+ value: Date | null = null;
+ isOpen = signal(false);
+ currentMonth = signal(new Date().getMonth());
+ currentYear = signal(new Date().getFullYear());
+ showMonthView = signal(false);
+
+ readonly faCalendarDays = faCalendarDays;
+ readonly faTimes = faTimes;
+ readonly faChevronLeft = faChevronLeft;
+ readonly faChevronRight = faChevronRight;
+
+ readonly dayHeaders = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
+ readonly months = [
+ { value: 0, label: 'January' },
+ { value: 1, label: 'February' },
+ { value: 2, label: 'March' },
+ { value: 3, label: 'April' },
+ { value: 4, label: 'May' },
+ { value: 5, label: 'June' },
+ { value: 6, label: 'July' },
+ { value: 7, label: 'August' },
+ { value: 8, label: 'September' },
+ { value: 9, label: 'October' },
+ { value: 10, label: 'November' },
+ { value: 11, label: 'December' }
+ ];
+
+ readonly minYear = 1900;
+ readonly maxYear = 2100;
+
+ private onChange = (value: Date | null) => {};
+ private onTouched = () => {};
+
+ writeValue(value: Date | null): void {
+ this.value = value;
+ if (value) {
+ this.currentMonth.set(value.getMonth());
+ this.currentYear.set(value.getFullYear());
+ }
+ }
+
+ registerOnChange(fn: (value: Date | null) => void): void {
+ this.onChange = fn;
+ }
+
+ registerOnTouched(fn: () => void): void {
+ this.onTouched = fn;
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ }
+
+ get formattedValue(): string {
+ if (!this.value) return '';
+ return this.formatDate(this.value);
+ }
+
+ get calendarDays() {
+ const year = this.currentYear();
+ const month = this.currentMonth();
+ const firstDay = new Date(year, month, 1);
+ const lastDay = new Date(year, month + 1, 0);
+ const startDate = new Date(firstDay);
+ startDate.setDate(startDate.getDate() - firstDay.getDay());
+
+ const days = [];
+ const currentDate = new Date(startDate);
+
+ for (let i = 0; i < 42; i++) {
+ const isOtherMonth = currentDate.getMonth() !== month;
+ const isToday = this.isSameDay(currentDate, new Date());
+ const isSelected = this.value ? this.isSameDay(currentDate, this.value) : false;
+ const isDisabled = this.isDateDisabled(currentDate);
+
+ days.push({
+ date: new Date(currentDate),
+ isOtherMonth,
+ isToday,
+ isSelected,
+ isDisabled
+ });
+
+ currentDate.setDate(currentDate.getDate() + 1);
+ }
+
+ return days;
+ }
+
+ getWrapperClasses(): string {
+ const classes = [
+ `ui-date-picker--${this.size}`,
+ `ui-date-picker--${this.variant}`,
+ `ui-date-picker--${this.state}`
+ ];
+
+ if (this.disabled) classes.push('ui-date-picker--disabled');
+ if (this.isOpen()) classes.push('ui-date-picker--open');
+
+ return classes.join(' ');
+ }
+
+ getContainerClasses(): string {
+ const classes: string[] = [];
+ if (this.clearable && this.value) classes.push('ui-date-picker__container--has-clear');
+ return classes.join(' ');
+ }
+
+ getHelperTextClasses(): string {
+ return `ui-date-picker__helper-text--${this.state}`;
+ }
+
+ getMonthYearLabel(): string {
+ const monthName = this.months[this.currentMonth()].label;
+ return `${monthName} ${this.currentYear()}`;
+ }
+
+ getDayAriaLabel(day: any): string {
+ const date = day.date;
+ const dayName = date.toLocaleDateString('en-US', { weekday: 'long' });
+ const monthName = date.toLocaleDateString('en-US', { month: 'long' });
+ return `${dayName}, ${monthName} ${date.getDate()}, ${date.getFullYear()}`;
+ }
+
+ toggleCalendar(): void {
+ if (this.disabled) return;
+
+ if (this.isOpen()) {
+ this.closeCalendar();
+ } else {
+ this.openCalendar();
+ }
+ }
+
+ openCalendar(): void {
+ this.isOpen.set(true);
+ this.calendarOpen.emit();
+ }
+
+ closeCalendar(): void {
+ this.isOpen.set(false);
+ this.showMonthView.set(false);
+ this.onTouched();
+ this.calendarClose.emit();
+ }
+
+ toggleMonthView(): void {
+ this.showMonthView.set(!this.showMonthView());
+ }
+
+ previousMonth(): void {
+ if (this.currentMonth() === 0) {
+ this.currentMonth.set(11);
+ this.currentYear.set(this.currentYear() - 1);
+ } else {
+ this.currentMonth.set(this.currentMonth() - 1);
+ }
+ }
+
+ nextMonth(): void {
+ if (this.currentMonth() === 11) {
+ this.currentMonth.set(0);
+ this.currentYear.set(this.currentYear() + 1);
+ } else {
+ this.currentMonth.set(this.currentMonth() + 1);
+ }
+ }
+
+ selectMonth(month: number): void {
+ this.currentMonth.set(month);
+ this.showMonthView.set(false);
+ }
+
+ selectDate(date: Date): void {
+ if (this.isDateDisabled(date)) return;
+
+ this.value = new Date(date);
+ this.onChange(this.value);
+ this.dateChange.emit(this.value);
+ this.closeCalendar();
+ }
+
+ clearValue(): void {
+ this.value = null;
+ this.onChange(null);
+ this.dateChange.emit(null);
+ }
+
+ onInputKeyDown(event: KeyboardEvent): void {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ this.toggleCalendar();
+ } else if (event.key === 'Escape' && this.isOpen()) {
+ event.preventDefault();
+ this.closeCalendar();
+ }
+ }
+
+ onYearInput(event: Event): void {
+ const target = event.target as HTMLInputElement;
+ const year = parseInt(target.value, 10);
+
+ if (!isNaN(year) && year >= this.minYear && year <= this.maxYear) {
+ this.currentYear.set(year);
+ }
+ }
+
+ private formatDate(date: Date): string {
+ // Simple MM/dd/yyyy format - can be enhanced based on dateFormat input
+ const month = (date.getMonth() + 1).toString().padStart(2, '0');
+ const day = date.getDate().toString().padStart(2, '0');
+ const year = date.getFullYear();
+ return `${month}/${day}/${year}`;
+ }
+
+ private isSameDay(date1: Date, date2: Date): boolean {
+ return date1.getDate() === date2.getDate() &&
+ date1.getMonth() === date2.getMonth() &&
+ date1.getFullYear() === date2.getFullYear();
+ }
+
+ private isDateDisabled(date: Date): boolean {
+ if (this.minDate && date < this.minDate) return true;
+ if (this.maxDate && date > this.maxDate) return true;
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/date-picker/index.ts b/projects/ui-essentials/src/lib/components/forms/date-picker/index.ts
new file mode 100644
index 0000000..d10ab33
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/date-picker/index.ts
@@ -0,0 +1 @@
+export * from './date-picker.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/file-upload/file-upload.component.scss b/projects/ui-essentials/src/lib/components/forms/file-upload/file-upload.component.scss
new file mode 100644
index 0000000..3bf3242
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/file-upload/file-upload.component.scss
@@ -0,0 +1,355 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+.ui-file-upload {
+ position: relative;
+ width: 100%;
+
+ // Size Variants
+ &--sm {
+ min-height: 36px;
+
+ .ui-file-upload__dropzone {
+ padding: $semantic-spacing-component-sm;
+ font-size: 0.875rem;
+ }
+ }
+
+ &--md {
+ min-height: 48px;
+
+ .ui-file-upload__dropzone {
+ padding: $semantic-spacing-component-md;
+ font-size: 1rem;
+ }
+ }
+
+ &--lg {
+ min-height: 56px;
+
+ .ui-file-upload__dropzone {
+ padding: $semantic-spacing-component-lg;
+ font-size: 1.125rem;
+ }
+ }
+
+ // Variant Styles
+ &--outlined {
+ .ui-file-upload__dropzone {
+ background: $semantic-color-surface-primary;
+ border: 1px solid $semantic-color-border-primary;
+ border-radius: 0.375rem;
+ }
+ }
+
+ &--filled {
+ .ui-file-upload__dropzone {
+ background: $semantic-color-surface-secondary;
+ border: 1px solid transparent;
+ border-radius: 0.375rem;
+ }
+ }
+
+ &--underlined {
+ .ui-file-upload__dropzone {
+ background: transparent;
+ border: none;
+ border-bottom: 1px solid $semantic-color-border-primary;
+ border-radius: 0;
+ }
+ }
+
+ // State Styles
+ &--disabled {
+ opacity: 0.38;
+ cursor: not-allowed;
+
+ .ui-file-upload__dropzone {
+ pointer-events: none;
+ background: $semantic-color-surface-primary;
+ color: $semantic-color-text-disabled;
+ }
+ }
+
+ &--error {
+ .ui-file-upload__dropzone {
+ border-color: $semantic-color-error;
+ background: $semantic-color-error-container;
+ }
+ }
+
+ &--success {
+ .ui-file-upload__dropzone {
+ border-color: $semantic-color-success;
+ background: $semantic-color-surface-secondary;
+ }
+ }
+
+ &--dragover {
+ .ui-file-upload__dropzone {
+ border-color: $semantic-color-primary;
+ background: $semantic-color-surface-secondary;
+ box-shadow: $semantic-shadow-elevation-2;
+ transform: scale(1.02);
+ }
+ }
+
+ // Label
+ &__label {
+ display: block;
+ margin-bottom: $semantic-spacing-stack-xs;
+ font-size: 0.875rem;
+ font-weight: 500;
+ color: $semantic-color-text-primary;
+
+ &--required::after {
+ content: ' *';
+ color: $semantic-color-error;
+ }
+ }
+
+ // Dropzone
+ &__dropzone {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ cursor: pointer;
+ transition: all 0.15s $semantic-motion-easing-ease-in-out;
+ color: $semantic-color-text-secondary;
+
+ &:hover:not(.ui-file-upload--disabled &) {
+ border-color: $semantic-color-primary;
+ background: $semantic-color-surface-secondary;
+ box-shadow: $semantic-shadow-elevation-1;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+ }
+
+ // Hidden Input
+ &__input {
+ position: absolute;
+ opacity: 0;
+ width: 0;
+ height: 0;
+ overflow: hidden;
+ }
+
+ // Upload Icon
+ &__icon {
+ font-size: 1.5rem;
+ margin-bottom: $semantic-spacing-stack-xs;
+ color: $semantic-color-text-tertiary;
+ }
+
+ // Upload Text
+ &__text {
+ margin-bottom: $semantic-spacing-stack-xs;
+
+ &-primary {
+ font-weight: 600;
+ color: $semantic-color-text-primary;
+ }
+
+ &-secondary {
+ font-size: 0.875rem;
+ color: $semantic-color-text-tertiary;
+ }
+ }
+
+ // Browse Button
+ &__browse-btn {
+ display: inline-flex;
+ align-items: center;
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ background: $semantic-color-primary;
+ color: $semantic-color-on-primary;
+ border: none;
+ border-radius: 0.25rem;
+ font-size: 0.875rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.15s $semantic-motion-easing-ease-in-out;
+ margin-top: $semantic-spacing-stack-xs;
+
+ &:hover {
+ background: $semantic-color-primary-hover;
+ box-shadow: $semantic-shadow-elevation-2;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+ }
+
+ // File List
+ &__file-list {
+ margin-top: $semantic-spacing-stack-md;
+ }
+
+ // File Item
+ &__file-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: $semantic-spacing-component-sm;
+ background: $semantic-color-surface-secondary;
+ border: 1px solid $semantic-color-border-subtle;
+ border-radius: 0.25rem;
+ margin-bottom: $semantic-spacing-stack-xs;
+ transition: all 0.15s $semantic-motion-easing-ease-in-out;
+
+ &:hover {
+ box-shadow: $semantic-shadow-elevation-1;
+ }
+
+ &--uploading {
+ opacity: 0.7;
+ position: relative;
+ overflow: hidden;
+ }
+
+ &--error {
+ background: $semantic-color-error-container;
+ border-color: $semantic-color-error;
+ }
+
+ &--success {
+ background: $semantic-color-surface-secondary;
+ border-color: $semantic-color-success;
+ }
+ }
+
+ // File Info
+ &__file-info {
+ display: flex;
+ align-items: center;
+ flex: 1;
+ min-width: 0;
+ }
+
+ &__file-icon {
+ font-size: 1.25rem;
+ margin-right: $semantic-spacing-stack-xs;
+ color: $semantic-color-text-tertiary;
+ }
+
+ &__file-details {
+ min-width: 0;
+ flex: 1;
+ }
+
+ &__file-name {
+ font-size: 0.875rem;
+ font-weight: 500;
+ color: $semantic-color-text-primary;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin-bottom: 2px;
+ }
+
+ &__file-size {
+ font-size: 0.8125rem;
+ color: $semantic-color-text-tertiary;
+ }
+
+ // File Actions
+ &__file-actions {
+ display: flex;
+ align-items: center;
+ gap: $semantic-spacing-stack-xs;
+ }
+
+ // Remove Button
+ &__remove-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ background: transparent;
+ border: none;
+ border-radius: 0.25rem;
+ color: $semantic-color-text-tertiary;
+ cursor: pointer;
+ transition: all 0.15s $semantic-motion-easing-ease-in-out;
+
+ &:hover {
+ background: $semantic-color-error-container;
+ color: $semantic-color-error;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+ }
+
+ // Progress Bar
+ &__progress {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ height: 2px;
+ background: $semantic-color-primary;
+ border-radius: 0.125rem;
+ transition: width 0.3s $semantic-motion-easing-ease-in-out;
+ }
+
+ // Helper Text
+ &__helper-text {
+ margin-top: $semantic-spacing-stack-xs;
+ font-size: 0.875rem;
+
+ &--default {
+ color: $semantic-color-text-tertiary;
+ }
+
+ &--error {
+ color: $semantic-color-error;
+ }
+
+ &--success {
+ color: $semantic-color-success;
+ }
+ }
+
+ // Loading State
+ &--loading {
+ .ui-file-upload__dropzone {
+ cursor: wait;
+ opacity: 0.7;
+ }
+ }
+
+ // Responsive Design
+ @media (max-width: $semantic-breakpoint-md - 1) {
+ &__dropzone {
+ padding: $semantic-spacing-component-sm;
+ }
+
+ &__file-name {
+ font-size: 0.75rem;
+ }
+ }
+
+ @media (max-width: $semantic-breakpoint-sm - 1) {
+ &__dropzone {
+ padding: $semantic-spacing-component-xs;
+ }
+
+ &__text-primary {
+ font-size: 0.875rem;
+ }
+
+ &__text-secondary {
+ font-size: 0.8125rem;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/file-upload/file-upload.component.ts b/projects/ui-essentials/src/lib/components/forms/file-upload/file-upload.component.ts
new file mode 100644
index 0000000..e8156ff
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/file-upload/file-upload.component.ts
@@ -0,0 +1,420 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, forwardRef } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
+import { faCloudUploadAlt, faFile, faImage, faFilePdf, faFileWord, faFileExcel, faFileCode, faTimes, faSpinner } from '@fortawesome/free-solid-svg-icons';
+
+export type FileUploadSize = 'sm' | 'md' | 'lg';
+export type FileUploadVariant = 'outlined' | 'filled' | 'underlined';
+export type FileUploadState = 'default' | 'error' | 'success' | 'warning';
+
+export interface UploadedFile {
+ file: File;
+ id: string;
+ name: string;
+ size: number;
+ type: string;
+ status: 'pending' | 'uploading' | 'completed' | 'error';
+ progress?: number;
+ error?: string;
+ url?: string;
+}
+
+@Component({
+ selector: 'ui-file-upload',
+ standalone: true,
+ imports: [CommonModule, FontAwesomeModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => FileUploadComponent),
+ multi: true
+ }
+ ],
+ template: `
+
+
+ @if (label) {
+
+ {{ label }}
+
+ }
+
+
+
+
+
+
+
+ {{ isDragOver ? 'Drop files here' : 'Drag & drop files here' }}
+
+
+ @if (acceptedTypes.length > 0) {
+
Accepted formats: {{ getAcceptedTypesText() }}
+ }
+ @if (maxFileSize) {
+
Max file size: {{ formatFileSize(maxFileSize) }}
+ }
+ @if (maxFiles && maxFiles > 1) {
+
Max {{ maxFiles }} files
+ }
+
+
+
+
+ Browse Files
+
+
+
+
+
+
+
+ @if (uploadedFiles.length > 0) {
+
+ @for (uploadedFile of uploadedFiles; track uploadedFile.id) {
+
+
+
+
+
+ {{ uploadedFile.name }}
+
+
+ {{ formatFileSize(uploadedFile.size) }}
+ @if (uploadedFile.status === 'error' && uploadedFile.error) {
+ - {{ uploadedFile.error }}
+ }
+
+
+
+
+
+ @if (uploadedFile.status === 'uploading') {
+
+ } @else {
+
+
+
+ }
+
+
+
+ @if (uploadedFile.status === 'uploading' && uploadedFile.progress !== undefined) {
+
+ }
+
+ }
+
+ }
+
+
+ @if (helperText || errorMessage) {
+
+ {{ state === 'error' ? errorMessage : helperText }}
+
+ }
+
+ `,
+ styleUrl: './file-upload.component.scss'
+})
+export class FileUploadComponent implements ControlValueAccessor {
+ @Input() label: string = '';
+ @Input() size: FileUploadSize = 'md';
+ @Input() variant: FileUploadVariant = 'outlined';
+ @Input() state: FileUploadState = 'default';
+ @Input() disabled = false;
+ @Input() required = false;
+ @Input() multiple = false;
+ @Input() accept: string = '';
+ @Input() acceptedTypes: string[] = [];
+ @Input() maxFileSize: number | null = null; // in bytes
+ @Input() maxFiles: number | null = null;
+ @Input() helperText: string = '';
+ @Input() errorMessage: string = '';
+ @Input() autoUpload = false;
+ @Input() uploadUrl: string = '';
+ @Input() inputId: string = `file-upload-${Math.random().toString(36).substr(2, 9)}`;
+
+ @Output() filesSelected = new EventEmitter();
+ @Output() fileAdded = new EventEmitter();
+ @Output() fileRemoved = new EventEmitter();
+ @Output() uploadProgress = new EventEmitter<{file: UploadedFile, progress: number}>();
+ @Output() uploadComplete = new EventEmitter();
+ @Output() uploadError = new EventEmitter<{file: UploadedFile, error: string}>();
+
+ uploadedFiles: UploadedFile[] = [];
+ isDragOver = false;
+
+ // FontAwesome icons
+ readonly faCloudUploadAlt = faCloudUploadAlt;
+ readonly faFile = faFile;
+ readonly faImage = faImage;
+ readonly faFilePdf = faFilePdf;
+ readonly faFileWord = faFileWord;
+ readonly faFileExcel = faFileExcel;
+ readonly faFileCode = faFileCode;
+ readonly faTimes = faTimes;
+ readonly faSpinner = faSpinner;
+
+ // ControlValueAccessor implementation
+ private onChange = (value: UploadedFile[]) => {};
+ private onTouched = () => {};
+
+ writeValue(value: UploadedFile[]): void {
+ this.uploadedFiles = value || [];
+ }
+
+ registerOnChange(fn: (value: UploadedFile[]) => void): void {
+ this.onChange = fn;
+ }
+
+ registerOnTouched(fn: () => void): void {
+ this.onTouched = fn;
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ }
+
+ getWrapperClasses(): string {
+ const classes = [
+ `ui-file-upload--${this.size}`,
+ `ui-file-upload--${this.variant}`,
+ `ui-file-upload--${this.state}`
+ ];
+
+ if (this.disabled) classes.push('ui-file-upload--disabled');
+ if (this.isDragOver) classes.push('ui-file-upload--dragover');
+
+ return classes.join(' ');
+ }
+
+ getHelperTextClasses(): string {
+ return `ui-file-upload__helper-text--${this.state}`;
+ }
+
+ openFileDialog(): void {
+ if (!this.disabled) {
+ const input = document.getElementById(this.inputId) as HTMLInputElement;
+ input?.click();
+ }
+ }
+
+ onFileSelected(event: Event): void {
+ const input = event.target as HTMLInputElement;
+ if (input.files) {
+ this.handleFiles(Array.from(input.files));
+ // Reset input value to allow selecting the same file again
+ input.value = '';
+ }
+ }
+
+ onDragOver(event: DragEvent): void {
+ if (!this.disabled) {
+ event.preventDefault();
+ event.stopPropagation();
+ this.isDragOver = true;
+ }
+ }
+
+ onDragLeave(event: DragEvent): void {
+ if (!this.disabled) {
+ event.preventDefault();
+ event.stopPropagation();
+ this.isDragOver = false;
+ }
+ }
+
+ onDrop(event: DragEvent): void {
+ if (!this.disabled) {
+ event.preventDefault();
+ event.stopPropagation();
+ this.isDragOver = false;
+
+ const files = event.dataTransfer?.files;
+ if (files) {
+ this.handleFiles(Array.from(files));
+ }
+ }
+ }
+
+ onKeyDown(event: KeyboardEvent): void {
+ if ((event.key === 'Enter' || event.key === ' ') && !this.disabled) {
+ event.preventDefault();
+ this.openFileDialog();
+ }
+ }
+
+ handleFiles(files: File[]): void {
+ let validFiles: File[] = files;
+
+ // Filter by accepted types
+ if (this.acceptedTypes.length > 0) {
+ validFiles = files.filter(file => this.isFileTypeAccepted(file));
+ }
+
+ // Filter by file size
+ if (this.maxFileSize) {
+ validFiles = validFiles.filter(file => file.size <= this.maxFileSize!);
+ }
+
+ // Limit number of files
+ if (this.maxFiles) {
+ const remainingSlots = this.maxFiles - this.uploadedFiles.length;
+ validFiles = validFiles.slice(0, remainingSlots);
+ }
+
+ // Create UploadedFile objects
+ const newUploadedFiles: UploadedFile[] = validFiles.map(file => ({
+ file,
+ id: this.generateFileId(),
+ name: file.name,
+ size: file.size,
+ type: file.type,
+ status: 'pending',
+ progress: 0
+ }));
+
+ // Add to uploaded files array
+ if (this.multiple) {
+ this.uploadedFiles = [...this.uploadedFiles, ...newUploadedFiles];
+ } else {
+ this.uploadedFiles = newUploadedFiles;
+ }
+
+ // Emit events
+ newUploadedFiles.forEach(uploadedFile => {
+ this.fileAdded.emit(uploadedFile);
+ });
+
+ this.filesSelected.emit(this.uploadedFiles);
+ this.onChange(this.uploadedFiles);
+ this.onTouched();
+
+ // Auto upload if enabled
+ if (this.autoUpload && this.uploadUrl) {
+ newUploadedFiles.forEach(uploadedFile => {
+ this.uploadFile(uploadedFile);
+ });
+ }
+ }
+
+ removeFile(fileId: string): void {
+ const fileIndex = this.uploadedFiles.findIndex(f => f.id === fileId);
+ if (fileIndex > -1) {
+ const removedFile = this.uploadedFiles[fileIndex];
+ this.uploadedFiles.splice(fileIndex, 1);
+ this.uploadedFiles = [...this.uploadedFiles]; // Trigger change detection
+
+ this.fileRemoved.emit(removedFile);
+ this.onChange(this.uploadedFiles);
+ }
+ }
+
+ private uploadFile(uploadedFile: UploadedFile): void {
+ uploadedFile.status = 'uploading';
+
+ const formData = new FormData();
+ formData.append('file', uploadedFile.file);
+
+ // Simulate upload progress (replace with actual upload logic)
+ let progress = 0;
+ const interval = setInterval(() => {
+ progress += 10;
+ uploadedFile.progress = progress;
+ this.uploadProgress.emit({ file: uploadedFile, progress });
+
+ if (progress >= 100) {
+ clearInterval(interval);
+ uploadedFile.status = 'completed';
+ this.uploadComplete.emit(uploadedFile);
+ }
+ }, 200);
+ }
+
+ private isFileTypeAccepted(file: File): boolean {
+ return this.acceptedTypes.some(type => {
+ if (type.startsWith('.')) {
+ return file.name.toLowerCase().endsWith(type.toLowerCase());
+ }
+ return file.type.includes(type);
+ });
+ }
+
+ private generateFileId(): string {
+ return `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+ }
+
+ getFileIcon(fileType: string): IconDefinition {
+ if (fileType.startsWith('image/')) return this.faImage;
+ if (fileType.includes('pdf')) return this.faFilePdf;
+ if (fileType.includes('word') || fileType.includes('document')) return this.faFileWord;
+ if (fileType.includes('excel') || fileType.includes('spreadsheet')) return this.faFileExcel;
+ if (fileType.includes('code') || fileType.includes('javascript') || fileType.includes('typescript')) return this.faFileCode;
+ return this.faFile;
+ }
+
+ formatFileSize(bytes: number): string {
+ if (bytes === 0) return '0 Bytes';
+ const k = 1024;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+ }
+
+ getAcceptedTypesText(): string {
+ return this.acceptedTypes.join(', ').toUpperCase();
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/file-upload/index.ts b/projects/ui-essentials/src/lib/components/forms/file-upload/index.ts
new file mode 100644
index 0000000..6581b56
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/file-upload/index.ts
@@ -0,0 +1 @@
+export * from './file-upload.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/form-field/form-field.component.scss b/projects/ui-essentials/src/lib/components/forms/form-field/form-field.component.scss
new file mode 100644
index 0000000..1113ff2
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/form-field/form-field.component.scss
@@ -0,0 +1,317 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+.ui-form-field {
+ position: relative;
+ width: 100%;
+ margin-bottom: $semantic-spacing-stack-md;
+
+ // Size Variants
+ &--sm {
+ .ui-form-field__label {
+ font-size: 0.8125rem;
+ margin-bottom: $semantic-spacing-stack-xs;
+ }
+
+ .ui-form-field__helper-text {
+ font-size: 0.75rem;
+ margin-top: $semantic-spacing-stack-xs;
+ }
+ }
+
+ &--md {
+ .ui-form-field__label {
+ font-size: 0.875rem;
+ margin-bottom: $semantic-spacing-stack-sm;
+ }
+
+ .ui-form-field__helper-text {
+ font-size: 0.8125rem;
+ margin-top: $semantic-spacing-stack-sm;
+ }
+ }
+
+ &--lg {
+ .ui-form-field__label {
+ font-size: 1rem;
+ margin-bottom: $semantic-spacing-stack-sm;
+ }
+
+ .ui-form-field__helper-text {
+ font-size: 0.875rem;
+ margin-top: $semantic-spacing-stack-sm;
+ }
+ }
+
+ // State Variants
+ &--error {
+ .ui-form-field__label {
+ color: $semantic-color-error;
+ }
+
+ .ui-form-field__helper-text {
+ color: $semantic-color-error;
+ }
+ }
+
+ &--success {
+ .ui-form-field__label {
+ color: $semantic-color-success;
+ }
+
+ .ui-form-field__helper-text {
+ color: $semantic-color-success;
+ }
+ }
+
+ &--warning {
+ .ui-form-field__label {
+ color: $semantic-color-warning;
+ }
+
+ .ui-form-field__helper-text {
+ color: $semantic-color-warning;
+ }
+ }
+
+ &--disabled {
+ .ui-form-field__label {
+ color: $semantic-color-text-disabled;
+ }
+
+ .ui-form-field__helper-text {
+ color: $semantic-color-text-disabled;
+ }
+ }
+
+ // Label
+ &__label {
+ display: block;
+ font-weight: 500;
+ color: $semantic-color-text-primary;
+ cursor: pointer;
+ transition: color 0.15s $semantic-motion-easing-ease-in-out;
+
+ &--required::after {
+ content: ' *';
+ color: $semantic-color-error;
+ font-weight: 400;
+ }
+
+ &--optional {
+ &::after {
+ content: ' (optional)';
+ color: $semantic-color-text-tertiary;
+ font-weight: 400;
+ font-size: 0.9em;
+ }
+ }
+ }
+
+ // Control Container
+ &__control {
+ position: relative;
+ width: 100%;
+ }
+
+ // Helper Text
+ &__helper-text {
+ color: $semantic-color-text-tertiary;
+ line-height: 1.4;
+ transition: color 0.15s $semantic-motion-easing-ease-in-out;
+
+ &--error {
+ color: $semantic-color-error;
+ }
+
+ &--success {
+ color: $semantic-color-success;
+ }
+
+ &--warning {
+ color: $semantic-color-warning;
+ }
+
+ &--info {
+ color: $semantic-color-info;
+ }
+ }
+
+ // Validation Message
+ &__validation {
+ display: flex;
+ align-items: flex-start;
+ gap: $semantic-spacing-stack-xs;
+ margin-top: $semantic-spacing-stack-xs;
+
+ &--error {
+ color: $semantic-color-error;
+ }
+
+ &--success {
+ color: $semantic-color-success;
+ }
+
+ &--warning {
+ color: $semantic-color-warning;
+ }
+
+ &--info {
+ color: $semantic-color-info;
+ }
+ }
+
+ &__validation-icon {
+ font-size: 1rem;
+ margin-top: 0.125rem;
+ flex-shrink: 0;
+ line-height: 1;
+ }
+
+ &__validation-text {
+ font-size: 0.8125rem;
+ line-height: 1.4;
+ flex: 1;
+ }
+
+ // Character Counter
+ &__char-counter {
+ font-size: 0.75rem;
+ color: $semantic-color-text-tertiary;
+ text-align: right;
+ margin-top: $semantic-spacing-stack-xs;
+
+ &--over-limit {
+ color: $semantic-color-error;
+ }
+ }
+
+ // Hint Text
+ &__hint {
+ font-size: 0.8125rem;
+ color: $semantic-color-text-secondary;
+ margin-top: $semantic-spacing-stack-xs;
+ display: flex;
+ align-items: flex-start;
+ gap: $semantic-spacing-stack-xs;
+ }
+
+ &__hint-icon {
+ font-size: 1rem;
+ color: $semantic-color-info;
+ margin-top: 0.125rem;
+ flex-shrink: 0;
+ line-height: 1;
+ }
+
+ // Layout Variants
+ &--horizontal {
+ display: flex;
+ align-items: flex-start;
+ gap: $semantic-spacing-stack-lg;
+
+ .ui-form-field__label {
+ flex: 0 0 auto;
+ min-width: 120px;
+ margin-bottom: 0;
+ padding-top: $semantic-spacing-stack-xs;
+ }
+
+ .ui-form-field__content {
+ flex: 1;
+ min-width: 0;
+ }
+ }
+
+ // Content Wrapper
+ &__content {
+ width: 100%;
+ }
+
+ // Focus Ring Support
+ &:focus-within {
+ .ui-form-field__label {
+ color: $semantic-color-primary;
+ }
+ }
+
+ // Group Support (for radio buttons, checkboxes, etc.)
+ &--group {
+ .ui-form-field__control {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-stack-sm;
+ }
+
+ &.ui-form-field--horizontal {
+ .ui-form-field__control {
+ flex-direction: row;
+ flex-wrap: wrap;
+ gap: $semantic-spacing-stack-md;
+ }
+ }
+ }
+
+ // Compact variant for tighter layouts
+ &--compact {
+ margin-bottom: $semantic-spacing-stack-sm;
+
+ .ui-form-field__label {
+ margin-bottom: $semantic-spacing-stack-xs;
+ }
+
+ .ui-form-field__helper-text,
+ .ui-form-field__validation {
+ margin-top: $semantic-spacing-stack-xs;
+ }
+ }
+
+ // Full width variant
+ &--full-width {
+ width: 100%;
+
+ .ui-form-field__control,
+ .ui-form-field__control > * {
+ width: 100%;
+ }
+ }
+
+ // Responsive Design
+ @media (max-width: $semantic-breakpoint-md - 1) {
+ &--horizontal {
+ flex-direction: column;
+ gap: $semantic-spacing-stack-sm;
+
+ .ui-form-field__label {
+ min-width: unset;
+ padding-top: 0;
+ margin-bottom: $semantic-spacing-stack-xs;
+ }
+ }
+ }
+
+ @media (max-width: $semantic-breakpoint-sm - 1) {
+ &--group.ui-form-field--horizontal {
+ .ui-form-field__control {
+ flex-direction: column;
+ gap: $semantic-spacing-stack-sm;
+ }
+ }
+ }
+
+ // Animation for validation messages
+ .ui-form-field__validation,
+ .ui-form-field__helper-text {
+ animation: slideInUp 0.3s $semantic-motion-easing-ease-in-out;
+ }
+}
+
+@keyframes slideInUp {
+ from {
+ opacity: 0;
+ transform: translateY(8px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/form-field/form-field.component.ts b/projects/ui-essentials/src/lib/components/forms/form-field/form-field.component.ts
new file mode 100644
index 0000000..dc41fbc
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/form-field/form-field.component.ts
@@ -0,0 +1,381 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, ContentChild, AfterContentInit, OnDestroy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { AbstractControl, FormControl, NgControl, ValidationErrors } from '@angular/forms';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
+import { faExclamationCircle, faCheckCircle, faExclamationTriangle, faInfoCircle, faQuestionCircle } from '@fortawesome/free-solid-svg-icons';
+import { Subject, takeUntil } from 'rxjs';
+
+export type FormFieldSize = 'sm' | 'md' | 'lg';
+export type FormFieldState = 'default' | 'error' | 'success' | 'warning' | 'info';
+export type FormFieldLayout = 'vertical' | 'horizontal';
+
+export interface ValidationMessage {
+ type: string;
+ message: string;
+ icon?: IconDefinition;
+}
+
+@Component({
+ selector: 'ui-form-field',
+ standalone: true,
+ imports: [CommonModule, FontAwesomeModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ template: `
+
+ `,
+ styleUrl: './form-field.component.scss'
+})
+export class FormFieldComponent implements AfterContentInit, OnDestroy {
+ @Input() label: string = '';
+ @Input() size: FormFieldSize = 'md';
+ @Input() state: FormFieldState = 'default';
+ @Input() layout: FormFieldLayout = 'vertical';
+ @Input() required = false;
+ @Input() disabled = false;
+ @Input() showOptional = false;
+ @Input() helperText: string = '';
+ @Input() hintText: string = '';
+ @Input() errorMessage: string = '';
+ @Input() successMessage: string = '';
+ @Input() warningMessage: string = '';
+ @Input() infoMessage: string = '';
+ @Input() showCharacterCount = false;
+ @Input() maxLength: number | null = null;
+ @Input() currentLength = 0;
+ @Input() fieldId: string = `form-field-${Math.random().toString(36).substr(2, 9)}`;
+ @Input() validationMessages: { [key: string]: string } = {};
+ @Input() customValidationMessages: ValidationMessage[] = [];
+ @Input() compact = false;
+ @Input() fullWidth = false;
+ @Input() group = false;
+
+ @Output() stateChange = new EventEmitter();
+ @Output() validationChange = new EventEmitter();
+
+ @ContentChild(NgControl, { static: false }) ngControl: NgControl | null = null;
+
+ // FontAwesome icons
+ readonly faExclamationCircle = faExclamationCircle;
+ readonly faCheckCircle = faCheckCircle;
+ readonly faExclamationTriangle = faExclamationTriangle;
+ readonly faInfoCircle = faInfoCircle;
+ readonly faQuestionCircle = faQuestionCircle;
+
+ private destroy$ = new Subject();
+ private currentState: FormFieldState = 'default';
+
+ ngAfterContentInit(): void {
+ if (this.ngControl && this.ngControl.control) {
+ // Listen to control status changes
+ this.ngControl.control.statusChanges
+ .pipe(takeUntil(this.destroy$))
+ .subscribe(() => {
+ this.updateStateFromControl();
+ });
+
+ // Listen to value changes for character count
+ this.ngControl.control.valueChanges
+ .pipe(takeUntil(this.destroy$))
+ .subscribe((value: string) => {
+ this.currentLength = value ? value.length : 0;
+ });
+
+ // Initial state update
+ this.updateStateFromControl();
+ }
+ }
+
+ ngOnDestroy(): void {
+ this.destroy$.next();
+ this.destroy$.complete();
+ }
+
+ getWrapperClasses(): string {
+ const classes = [
+ `ui-form-field--${this.size}`,
+ `ui-form-field--${this.getCurrentState()}`,
+ `ui-form-field--${this.layout}`
+ ];
+
+ if (this.disabled) classes.push('ui-form-field--disabled');
+ if (this.compact) classes.push('ui-form-field--compact');
+ if (this.fullWidth) classes.push('ui-form-field--full-width');
+ if (this.group) classes.push('ui-form-field--group');
+
+ return classes.join(' ');
+ }
+
+ getHelperTextClasses(): string {
+ const state = this.getCurrentState();
+ return `ui-form-field__helper-text--${state}`;
+ }
+
+ getValidationClasses(): string {
+ const state = this.getCurrentState();
+ return `ui-form-field__validation--${state}`;
+ }
+
+ getValidationIcon(): IconDefinition {
+ const state = this.getCurrentState();
+ switch (state) {
+ case 'error':
+ return this.faExclamationCircle;
+ case 'success':
+ return this.faCheckCircle;
+ case 'warning':
+ return this.faExclamationTriangle;
+ case 'info':
+ return this.faInfoCircle;
+ default:
+ return this.faExclamationCircle;
+ }
+ }
+
+ getCurrentState(): FormFieldState {
+ // Override state takes precedence
+ if (this.state !== 'default') {
+ return this.state;
+ }
+
+ // Check if we have a form control
+ if (this.ngControl && this.ngControl.control) {
+ const control = this.ngControl.control;
+
+ // Error state
+ if (control.invalid && (control.dirty || control.touched)) {
+ return 'error';
+ }
+
+ // Success state
+ if (control.valid && control.value && (control.dirty || control.touched)) {
+ return 'success';
+ }
+ }
+
+ // Check custom messages
+ if (this.errorMessage) return 'error';
+ if (this.successMessage) return 'success';
+ if (this.warningMessage) return 'warning';
+ if (this.infoMessage) return 'info';
+
+ return 'default';
+ }
+
+ hasValidationMessages(): boolean {
+ const currentState = this.getCurrentState();
+
+ if (currentState === 'error') {
+ return !!(this.getControlErrors() || this.errorMessage);
+ }
+
+ if (currentState === 'success') {
+ return !!this.successMessage;
+ }
+
+ if (currentState === 'warning') {
+ return !!this.warningMessage;
+ }
+
+ if (currentState === 'info') {
+ return !!this.infoMessage;
+ }
+
+ return this.customValidationMessages.length > 0;
+ }
+
+ getValidationMessage(): string {
+ const currentState = this.getCurrentState();
+
+ if (currentState === 'error') {
+ const controlErrors = this.getControlErrors();
+ if (controlErrors) return controlErrors;
+ return this.errorMessage || 'Please correct the error';
+ }
+
+ if (currentState === 'success') {
+ return this.successMessage || 'Valid input';
+ }
+
+ if (currentState === 'warning') {
+ return this.warningMessage || 'Warning message';
+ }
+
+ if (currentState === 'info') {
+ return this.infoMessage || 'Information message';
+ }
+
+ if (this.customValidationMessages.length > 0) {
+ return this.customValidationMessages[0].message;
+ }
+
+ return '';
+ }
+
+ isOverCharacterLimit(): boolean {
+ return this.maxLength ? this.currentLength > this.maxLength : false;
+ }
+
+ private updateStateFromControl(): void {
+ const newState = this.getCurrentState();
+ if (newState !== this.currentState) {
+ this.currentState = newState;
+ this.stateChange.emit(newState);
+ }
+
+ if (this.ngControl && this.ngControl.control) {
+ this.validationChange.emit(this.ngControl.control.errors);
+ }
+ }
+
+ private getControlErrors(): string | null {
+ if (!this.ngControl || !this.ngControl.control || !this.ngControl.control.errors) {
+ return null;
+ }
+
+ const errors = this.ngControl.control.errors;
+ const errorKey = Object.keys(errors)[0]; // Get first error
+
+ // Check for custom validation messages first
+ if (this.validationMessages[errorKey]) {
+ return this.validationMessages[errorKey];
+ }
+
+ // Default error messages
+ switch (errorKey) {
+ case 'required':
+ return 'This field is required';
+ case 'email':
+ return 'Please enter a valid email address';
+ case 'minlength':
+ return `Minimum length is ${errors[errorKey].requiredLength} characters`;
+ case 'maxlength':
+ return `Maximum length is ${errors[errorKey].requiredLength} characters`;
+ case 'min':
+ return `Minimum value is ${errors[errorKey].min}`;
+ case 'max':
+ return `Maximum value is ${errors[errorKey].max}`;
+ case 'pattern':
+ return 'Please enter a valid format';
+ case 'url':
+ return 'Please enter a valid URL';
+ case 'number':
+ return 'Please enter a valid number';
+ case 'date':
+ return 'Please enter a valid date';
+ case 'time':
+ return 'Please enter a valid time';
+ case 'custom':
+ return errors[errorKey].message || 'Invalid input';
+ default:
+ return 'Please correct this field';
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/form-field/index.ts b/projects/ui-essentials/src/lib/components/forms/form-field/index.ts
new file mode 100644
index 0000000..5a2d58d
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/form-field/index.ts
@@ -0,0 +1 @@
+export * from './form-field.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/index.ts b/projects/ui-essentials/src/lib/components/forms/index.ts
new file mode 100644
index 0000000..a3a4d3f
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/index.ts
@@ -0,0 +1,11 @@
+export * from './checkbox';
+export * from './input';
+export * from './radio';
+export * from './search';
+export * from './switch';
+export * from './select.component';
+export * from './autocomplete';
+export * from './date-picker';
+export * from './time-picker';
+export * from './file-upload';
+export * from './form-field';
diff --git a/projects/ui-essentials/src/lib/components/forms/input/index.ts b/projects/ui-essentials/src/lib/components/forms/input/index.ts
new file mode 100644
index 0000000..aca2de3
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/input/index.ts
@@ -0,0 +1,3 @@
+export * from './text-input.component';
+export * from './textarea.component';
+export * from './input-wrapper.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/input/input-wrapper.component.ts b/projects/ui-essentials/src/lib/components/forms/input/input-wrapper.component.ts
new file mode 100644
index 0000000..48e4f10
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/input/input-wrapper.component.ts
@@ -0,0 +1,199 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, forwardRef, ViewChild, AfterViewInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
+import { TextInputComponent, TextInputSize, TextInputVariant, TextInputType, TextInputState } from './text-input.component';
+import { TextareaComponent, TextareaSize, TextareaVariant, TextareaState, TextareaResize } from './textarea.component';
+
+export type InputWrapperMode = 'input' | 'textarea';
+
+@Component({
+ selector: 'ui-input-wrapper',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [CommonModule, TextInputComponent, TextareaComponent],
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => InputWrapperComponent),
+ multi: true
+ }
+ ],
+ template: `
+ @if (mode === 'input') {
+
+ }
+
+ @if (mode === 'textarea') {
+
+ }
+ `,
+ styles: [`
+ :host {
+ display: block;
+ width: 100%;
+ }
+ `]
+})
+export class InputWrapperComponent implements ControlValueAccessor, AfterViewInit {
+ @ViewChild('textInput') textInput?: TextInputComponent;
+ @ViewChild('textArea') textArea?: TextareaComponent;
+ @Input() mode: InputWrapperMode = 'input';
+ @Input() label: string = '';
+ @Input() placeholder: string = '';
+ @Input() inputType: TextInputType = 'text';
+ @Input() size: TextInputSize | TextareaSize = 'md';
+ @Input() variant: TextInputVariant | TextareaVariant = 'outlined';
+ @Input() state: TextInputState | TextareaState = 'default';
+ @Input() disabled = false;
+ @Input() readonly = false;
+ @Input() required = false;
+ @Input() loading = false;
+ @Input() clearable = false;
+ @Input() prefixIcon: IconDefinition | null = null;
+ @Input() prefixText: string = '';
+ @Input() suffixIcon: IconDefinition | null = null;
+ @Input() helperText: string = '';
+ @Input() errorMessage: string = '';
+ @Input() maxLength: number | null = null;
+ @Input() showCharacterCount = false;
+ @Input() autocomplete: string = '';
+ @Input() min: string | number | null = null;
+ @Input() max: string | number | null = null;
+ @Input() step: string | number | null = null;
+ @Input() inputId: string = `input-wrapper-${Math.random().toString(36).substr(2, 9)}`;
+
+ // Textarea-specific inputs
+ @Input() rows: number = 4;
+ @Input() cols: number | null = null;
+ @Input() resize: TextareaResize = 'vertical';
+ @Input() autoResize = false;
+
+ @Output() inputChange = new EventEmitter();
+ @Output() inputFocus = new EventEmitter();
+ @Output() inputBlur = new EventEmitter();
+ @Output() keyDown = new EventEmitter();
+ @Output() clear = new EventEmitter();
+
+ value = '';
+
+ // ControlValueAccessor implementation
+ private onChange = (value: string) => {};
+ private onTouched = () => {};
+
+ ngAfterViewInit(): void {
+ // Set up the initial value and event forwarding for child components
+ this.updateChildComponent();
+ }
+
+ writeValue(value: string): void {
+ this.value = value || '';
+ this.updateChildComponent();
+ }
+
+ registerOnChange(fn: (value: string) => void): void {
+ this.onChange = fn;
+ this.updateChildComponent();
+ }
+
+ registerOnTouched(fn: () => void): void {
+ this.onTouched = fn;
+ this.updateChildComponent();
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ this.updateChildComponent();
+ }
+
+ private updateChildComponent(): void {
+ const activeComponent = this.mode === 'input' ? this.textInput : this.textArea;
+ if (activeComponent) {
+ activeComponent.writeValue(this.value);
+ activeComponent.registerOnChange(this.onChange);
+ activeComponent.registerOnTouched(this.onTouched);
+ activeComponent.setDisabledState(this.disabled);
+ }
+ }
+
+ onInputChange(value: string): void {
+ this.value = value;
+ this.onChange(value);
+ this.inputChange.emit(value);
+ }
+
+ onInputFocus(event: FocusEvent): void {
+ this.inputFocus.emit(event);
+ }
+
+ onInputBlur(event: FocusEvent): void {
+ this.onTouched();
+ this.inputBlur.emit(event);
+ }
+
+ onKeyDown(event: KeyboardEvent): void {
+ this.keyDown.emit(event);
+ }
+
+ onClear(): void {
+ this.clear.emit();
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/input/text-input.component.scss b/projects/ui-essentials/src/lib/components/forms/input/text-input.component.scss
new file mode 100644
index 0000000..318ab07
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/input/text-input.component.scss
@@ -0,0 +1,561 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+// Tokens available globally via main application styles
+
+// ==========================================================================
+// TEXT INPUT COMPONENT
+// ==========================================================================
+// Comprehensive text input component with multiple variants and sizes
+// Uses semantic design tokens for consistent styling across the design system
+// ==========================================================================
+
+.text-input-wrapper {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ position: relative;
+
+ // ==========================================================================
+ // SIZE VARIANTS
+ // ==========================================================================
+
+ &--sm {
+ .text-input__container {
+ min-height: 36px;
+ }
+
+ .text-input__field {
+ font-size: 0.875rem;
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ }
+
+ .text-input__label {
+ font-size: 0.8125rem;
+ margin-bottom: $semantic-spacing-component-xs;
+ }
+ }
+
+ &--md {
+ .text-input__container {
+ min-height: 48px;
+ }
+
+ .text-input__field {
+ font-size: 1rem;
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
+ }
+
+ .text-input__label {
+ font-size: 0.875rem;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+ }
+
+ &--lg {
+ .text-input__container {
+ min-height: 56px;
+ }
+
+ .text-input__field {
+ font-size: 1.125rem;
+ padding: $semantic-spacing-component-md $semantic-spacing-component-lg;
+ }
+
+ .text-input__label {
+ font-size: 1rem;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+ }
+
+ // ==========================================================================
+ // VARIANT STYLES
+ // ==========================================================================
+
+ &--outlined {
+ .text-input__container {
+ border: $semantic-border-width-2 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-md;
+ background-color: $semantic-color-surface-primary;
+ }
+
+ &.text-input-wrapper--focused .text-input__container {
+ border-color: $semantic-color-interactive-primary;
+ box-shadow: 0 0 0 2px rgba($semantic-color-brand-primary, 0.12);
+ }
+ }
+
+ &--filled {
+ .text-input__container {
+ border: none;
+ border-bottom: 2px solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-sm $semantic-border-radius-sm 0 0;
+ background-color: $semantic-color-surface-secondary;
+ }
+
+ &.text-input-wrapper--focused .text-input__container {
+ border-bottom-color: $semantic-color-interactive-primary;
+ background-color: $semantic-color-surface-elevated;
+ }
+ }
+
+ &--underlined {
+ .text-input__container {
+ border: none;
+ border-bottom: 1px solid $semantic-color-border-primary;
+ border-radius: 0;
+ background-color: transparent;
+ padding: $semantic-spacing-component-xs 0;
+ }
+
+ &.text-input-wrapper--focused .text-input__container {
+ border-bottom: 2px solid $semantic-color-interactive-primary;
+ }
+ }
+
+ // ==========================================================================
+ // STATE VARIANTS
+ // ==========================================================================
+
+ &--error {
+ .text-input__container {
+ border-color: $semantic-color-danger;
+ }
+
+ &.text-input-wrapper--focused .text-input__container {
+ border-color: $semantic-color-danger;
+ box-shadow: 0 0 0 2px rgba($semantic-color-danger, 0.12);
+ }
+
+ .text-input__label {
+ color: $semantic-color-danger;
+ }
+ }
+
+ &--success {
+ .text-input__container {
+ border-color: $semantic-color-success;
+ }
+
+ &.text-input-wrapper--focused .text-input__container {
+ border-color: $semantic-color-success;
+ box-shadow: 0 0 0 2px rgba($semantic-color-success, 0.12);
+ }
+
+ .text-input__label {
+ color: $semantic-color-success;
+ }
+ }
+
+ &--warning {
+ .text-input__container {
+ border-color: $semantic-color-warning;
+ }
+
+ &.text-input-wrapper--focused .text-input__container {
+ border-color: $semantic-color-warning;
+ box-shadow: 0 0 0 2px rgba($semantic-color-warning, 0.12);
+ }
+
+ .text-input__label {
+ color: $semantic-color-warning;
+ }
+ }
+
+ &--disabled {
+ opacity: 0.6;
+ pointer-events: none;
+
+ .text-input__container {
+ background-color: $semantic-color-surface-disabled;
+ border-color: $semantic-color-border-disabled;
+ }
+
+ .text-input__label {
+ color: $semantic-color-text-disabled;
+ }
+ }
+
+ &--readonly {
+ .text-input__field {
+ background-color: $semantic-color-surface-secondary;
+ cursor: default;
+ }
+ }
+
+ &--loading {
+ .text-input__field {
+ padding-right: calc($semantic-spacing-component-lg + 24px);
+ }
+ }
+}
+
+// ==========================================================================
+// LABEL STYLES
+// ==========================================================================
+
+.text-input__label {
+ display: block;
+ font-weight: 500;
+ color: $semantic-color-text-primary;
+ cursor: pointer;
+ user-select: none;
+ transition: color 0.2s ease-in-out;
+
+ &--required {
+ .text-input__required-indicator {
+ color: $semantic-color-danger;
+ margin-left: $semantic-spacing-micro-tight;
+ }
+ }
+}
+
+// ==========================================================================
+// INPUT CONTAINER
+// ==========================================================================
+
+.text-input__container {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ box-sizing: border-box;
+ transition: all 0.2s ease-in-out;
+ position: relative;
+
+ &:hover:not(.text-input-wrapper--disabled &) {
+ border-color: $semantic-color-border-secondary;
+ }
+
+ &--has-prefix {
+ .text-input__field {
+ padding-left: 0; // Prefix handles spacing
+ }
+ }
+
+ &--has-suffix {
+ .text-input__field {
+ padding-right: 0; // Suffix handles spacing
+ }
+ }
+}
+
+// ==========================================================================
+// INPUT FIELD
+// ==========================================================================
+
+.text-input__field {
+ flex: 1;
+ border: none;
+ outline: none;
+ background: transparent;
+ color: $semantic-color-text-primary;
+ font-family: inherit;
+ font-weight: 400;
+ line-height: 1.5;
+ min-width: 0;
+
+ &::placeholder {
+ color: $semantic-color-text-tertiary;
+ transition: opacity 0.2s ease-in-out;
+ }
+
+ &:focus::placeholder {
+ opacity: 0.7;
+ }
+
+ // Remove browser default styles
+ &::-webkit-outer-spin-button,
+ &::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
+
+ &[type="number"] {
+ -moz-appearance: textfield;
+ }
+
+ // Autofill styles
+ &:-webkit-autofill {
+ -webkit-text-fill-color: $semantic-color-text-primary !important;
+ -webkit-box-shadow: 0 0 0 1000px $semantic-color-surface-primary inset !important;
+ transition: background-color 5000s ease-in-out 0s;
+ }
+
+ // Remove password reveal button in Edge
+ &::-ms-reveal,
+ &::-ms-clear {
+ display: none;
+ }
+}
+
+// ==========================================================================
+// PREFIX STYLES
+// ==========================================================================
+
+.text-input__prefix-icon,
+.text-input__prefix-text {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ color: $semantic-color-text-secondary;
+}
+
+.text-input__prefix-icon {
+ padding-left: 12px; // 8-12px padding from left edge so icon isn't hard against side
+ padding-right: 12px; // 8-12px spacing between icon and text as requested
+
+ // Size-specific spacing adjustments
+ .text-input-wrapper--sm & {
+ padding-left: 10px; // Slightly less for smaller size
+ padding-right: 10px; // Slightly less for smaller size
+ }
+
+ .text-input-wrapper--lg & {
+ padding-left: 14px; // Slightly more for larger size
+ padding-right: 14px; // Slightly more for larger size
+ }
+
+ fa-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 1rem;
+ height: 1rem;
+ font-size: 1rem;
+
+ .text-input-wrapper--sm & {
+ width: 0.875rem;
+ height: 0.875rem;
+ font-size: 0.875rem;
+ }
+
+ .text-input-wrapper--lg & {
+ width: 1.125rem;
+ height: 1.125rem;
+ font-size: 1.125rem;
+ }
+ }
+}
+
+.text-input__prefix-text {
+ padding-left: $semantic-spacing-component-md;
+ padding-right: 10px; // Slightly less than icon to account for border
+ font-weight: 500;
+ font-size: 0.875rem;
+ white-space: nowrap;
+ border-right: 1px solid $semantic-color-border-secondary;
+ margin-right: $semantic-spacing-component-xs;
+
+ .text-input-wrapper--sm & {
+ font-size: 0.8125rem;
+ }
+
+ .text-input-wrapper--lg & {
+ font-size: 1rem;
+ }
+}
+
+// ==========================================================================
+// SUFFIX STYLES
+// ==========================================================================
+
+.text-input__suffix {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ gap: $semantic-spacing-component-xs;
+ padding-right: 12px; // 8-12px padding from right edge so icons aren't hard against side
+ padding-left: 12px; // Consistent spacing from text content
+
+ // Size-specific spacing adjustments
+ .text-input-wrapper--sm & {
+ padding-left: 10px; // Slightly less for smaller size
+ padding-right: 10px; // Slightly less for smaller size
+ }
+
+ .text-input-wrapper--lg & {
+ padding-left: 14px; // Slightly more for larger size
+ padding-right: 14px; // Slightly more for larger size
+ }
+
+ &--has-content {
+ // Maintain consistent padding even when content is present
+ }
+}
+
+.text-input__clear-btn,
+.text-input__password-toggle {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ border: none;
+ border-radius: $semantic-border-radius-sm;
+ background: transparent;
+ color: $semantic-color-text-secondary;
+ cursor: pointer;
+ transition: all 0.2s ease-in-out;
+
+ &:hover {
+ background-color: $semantic-color-surface-interactive;
+ color: $semantic-color-text-primary;
+ }
+
+ &:focus {
+ outline: 2px solid $semantic-color-border-focus;
+ outline-offset: 1px;
+ }
+
+ fa-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 0.875rem;
+ height: 0.875rem;
+ font-size: 0.875rem;
+ }
+}
+
+.text-input__spinner {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ color: $semantic-color-interactive-primary;
+
+ fa-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 0.875rem;
+ height: 0.875rem;
+ font-size: 0.875rem;
+ }
+}
+
+.text-input__suffix-icon {
+ display: flex;
+ align-items: center;
+ color: $semantic-color-text-secondary;
+
+ fa-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 1rem;
+ height: 1rem;
+ font-size: 1rem;
+ }
+}
+
+// ==========================================================================
+// HELPER TEXT AND ERROR MESSAGES
+// ==========================================================================
+
+.text-input__helper-text {
+ margin-top: $semantic-spacing-component-xs;
+ font-size: 0.8125rem;
+ line-height: 1.4;
+ color: $semantic-color-text-secondary;
+
+ &--error {
+ color: $semantic-color-danger;
+ }
+
+ &--success {
+ color: $semantic-color-success;
+ }
+
+ &--warning {
+ color: $semantic-color-warning;
+ }
+}
+
+// ==========================================================================
+// CHARACTER COUNTER
+// ==========================================================================
+
+.text-input__char-counter {
+ position: absolute;
+ bottom: -#{$semantic-spacing-component-lg};
+ right: 0;
+ font-size: 0.75rem;
+ color: $semantic-color-text-tertiary;
+ user-select: none;
+}
+
+// ==========================================================================
+// RESPONSIVE ADJUSTMENTS
+// ==========================================================================
+
+@media (max-width: 768px) {
+ .text-input-wrapper {
+ &--lg {
+ .text-input__container {
+ min-height: 48px;
+ }
+
+ .text-input__field {
+ font-size: 1rem;
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
+ }
+ }
+ }
+}
+
+// ==========================================================================
+// ACCESSIBILITY ENHANCEMENTS
+// ==========================================================================
+
+.text-input__field:focus {
+ // Focus is handled by container border/shadow
+}
+
+// High contrast mode support
+@media (prefers-contrast: high) {
+ .text-input__container {
+ border-width: 2px;
+ }
+
+ .text-input-wrapper--focused .text-input__container {
+ border-width: 3px;
+ }
+}
+
+// Reduced motion support
+@media (prefers-reduced-motion: reduce) {
+ .text-input__container,
+ .text-input__field,
+ .text-input__label,
+ .text-input__clear-btn,
+ .text-input__password-toggle {
+ transition: none;
+ }
+}
+
+// ==========================================================================
+// FLOATING LABEL VARIANT (OPTIONAL)
+// ==========================================================================
+
+.text-input-wrapper--floating {
+ .text-input__label {
+ position: absolute;
+ top: 50%;
+ left: $semantic-spacing-component-md;
+ transform: translateY(-50%);
+ background-color: $semantic-color-surface-primary;
+ padding: 0 $semantic-spacing-component-xs;
+ pointer-events: none;
+ transition: all 0.2s ease-in-out;
+ z-index: 1;
+ }
+
+ &.text-input-wrapper--focused .text-input__label,
+ &.text-input-wrapper--has-value .text-input__label {
+ top: 0;
+ transform: translateY(-50%);
+ font-size: 0.75rem;
+ color: $semantic-color-interactive-primary;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/input/text-input.component.ts b/projects/ui-essentials/src/lib/components/forms/input/text-input.component.ts
new file mode 100644
index 0000000..9055bae
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/input/text-input.component.ts
@@ -0,0 +1,275 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, forwardRef, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
+import { faSearch, faEnvelope, faTimes, faEye, faEyeSlash, faSpinner } from '@fortawesome/free-solid-svg-icons';
+
+export type TextInputSize = 'sm' | 'md' | 'lg';
+export type TextInputVariant = 'outlined' | 'filled' | 'underlined';
+export type TextInputType = 'text' | 'email' | 'password' | 'search' | 'number' | 'tel' | 'url';
+export type TextInputState = 'default' | 'error' | 'success' | 'warning';
+
+@Component({
+ selector: 'ui-text-input',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [CommonModule, FontAwesomeModule],
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => TextInputComponent),
+ multi: true
+ }
+ ],
+ template: `
+
+
+ @if (label) {
+
+ {{ label }}
+ @if (required) {
+ *
+ }
+
+ }
+
+
+
+
+ @if (prefixIcon) {
+
+
+
+ }
+
+
+ @if (prefixText) {
+
{{ prefixText }}
+ }
+
+
+
+
+
+
+
+ @if (clearable && value && !disabled) {
+
+
+
+ }
+
+
+ @if (type === 'password') {
+
+
+
+ }
+
+
+ @if (loading) {
+
+
+
+ }
+
+
+ @if (suffixIcon) {
+
+
+
+ }
+
+
+
+
+ @if (helperText || errorMessage) {
+
+ {{ state === 'error' ? errorMessage : helperText }}
+
+ }
+
+
+ @if (maxLength && showCharacterCount) {
+
+ {{ value.length }}/{{ maxLength }}
+
+ }
+
+ `,
+ styleUrls: ['./text-input.component.scss']
+})
+export class TextInputComponent implements ControlValueAccessor {
+ @Input() label: string = '';
+ @Input() placeholder: string = '';
+ @Input() type: TextInputType = 'text';
+ @Input() size: TextInputSize = 'md';
+ @Input() variant: TextInputVariant = 'outlined';
+ @Input() state: TextInputState = 'default';
+ @Input() disabled = false;
+ @Input() readonly = false;
+ @Input() required = false;
+ @Input() loading = false;
+ @Input() clearable = false;
+ @Input() prefixIcon: IconDefinition | null = null;
+ @Input() prefixText: string = '';
+ @Input() suffixIcon: IconDefinition | null = null;
+ @Input() helperText: string = '';
+ @Input() errorMessage: string = '';
+ @Input() maxLength: number | null = null;
+ @Input() showCharacterCount = false;
+ @Input() autocomplete: string = '';
+ @Input() min: string | number | null = null;
+ @Input() max: string | number | null = null;
+ @Input() step: string | number | null = null;
+ @Input() inputId: string = `text-input-${Math.random().toString(36).substr(2, 9)}`;
+
+ @Output() inputChange = new EventEmitter();
+ @Output() inputFocus = new EventEmitter();
+ @Output() inputBlur = new EventEmitter();
+ @Output() keyDown = new EventEmitter();
+ @Output() clear = new EventEmitter();
+
+ value = '';
+ focused = false;
+ passwordVisible = signal(false);
+
+ // FontAwesome icons
+ readonly faSearch = faSearch;
+ readonly faEnvelope = faEnvelope;
+ readonly faTimes = faTimes;
+ readonly faEye = faEye;
+ readonly faEyeSlash = faEyeSlash;
+ readonly faSpinner = faSpinner;
+
+ // ControlValueAccessor implementation
+ private onChange = (value: string) => {};
+ private onTouched = () => {};
+
+ writeValue(value: string): void {
+ this.value = value || '';
+ }
+
+ registerOnChange(fn: (value: string) => void): void {
+ this.onChange = fn;
+ }
+
+ registerOnTouched(fn: () => void): void {
+ this.onTouched = fn;
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ }
+
+ getWrapperClasses(): string {
+ const classes = [
+ `text-input-wrapper--${this.size}`,
+ `text-input-wrapper--${this.variant}`,
+ `text-input-wrapper--${this.state}`
+ ];
+
+ if (this.disabled) classes.push('text-input-wrapper--disabled');
+ if (this.focused) classes.push('text-input-wrapper--focused');
+ if (this.readonly) classes.push('text-input-wrapper--readonly');
+ if (this.loading) classes.push('text-input-wrapper--loading');
+
+ return classes.join(' ');
+ }
+
+ getContainerClasses(): string {
+ const classes: string[] = [];
+
+ if (this.prefixIcon || this.prefixText) classes.push('text-input__container--has-prefix');
+ if (this.hasSuffixContent()) classes.push('text-input__container--has-suffix');
+
+ return classes.join(' ');
+ }
+
+ getHelperTextClasses(): string {
+ const classes = [`text-input__helper-text--${this.state}`];
+ return classes.join(' ');
+ }
+
+ getInputType(): string {
+ if (this.type === 'password') {
+ return this.passwordVisible() ? 'text' : 'password';
+ }
+ return this.type;
+ }
+
+ hasSuffixContent(): boolean {
+ return !!(
+ this.clearable ||
+ this.type === 'password' ||
+ this.loading ||
+ this.suffixIcon
+ );
+ }
+
+ onInput(event: Event): void {
+ const target = event.target as HTMLInputElement;
+ this.value = target.value;
+ this.onChange(this.value);
+ this.inputChange.emit(this.value);
+ }
+
+ onFocus(event: FocusEvent): void {
+ this.focused = true;
+ this.inputFocus.emit(event);
+ }
+
+ onBlur(event: FocusEvent): void {
+ this.focused = false;
+ this.onTouched();
+ this.inputBlur.emit(event);
+ }
+
+ onKeyDown(event: KeyboardEvent): void {
+ this.keyDown.emit(event);
+ }
+
+ togglePasswordVisibility(): void {
+ this.passwordVisible.set(!this.passwordVisible());
+ }
+
+ clearValue(): void {
+ this.value = '';
+ this.onChange(this.value);
+ this.inputChange.emit(this.value);
+ this.clear.emit();
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/input/textarea.component.scss b/projects/ui-essentials/src/lib/components/forms/input/textarea.component.scss
new file mode 100644
index 0000000..e80ac14
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/input/textarea.component.scss
@@ -0,0 +1,523 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+// Tokens available globally via main application styles
+
+// ==========================================================================
+// TEXTAREA COMPONENT
+// ==========================================================================
+// Comprehensive textarea component with multiple variants and sizes
+// Uses semantic design tokens for consistent styling across the design system
+// ==========================================================================
+
+.textarea-wrapper {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ position: relative;
+
+ // ==========================================================================
+ // SIZE VARIANTS
+ // ==========================================================================
+
+ &--sm {
+ .textarea__container {
+ min-height: 80px;
+ }
+
+ .textarea__field {
+ font-size: 0.875rem;
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ line-height: 1.4;
+ }
+
+ .textarea__label {
+ font-size: 0.8125rem;
+ margin-bottom: $semantic-spacing-component-xs;
+ }
+ }
+
+ &--md {
+ .textarea__container {
+ min-height: 100px;
+ }
+
+ .textarea__field {
+ font-size: 1rem;
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
+ line-height: 1.5;
+ }
+
+ .textarea__label {
+ font-size: 0.875rem;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+ }
+
+ &--lg {
+ .textarea__container {
+ min-height: 120px;
+ }
+
+ .textarea__field {
+ font-size: 1.125rem;
+ padding: $semantic-spacing-component-md $semantic-spacing-component-lg;
+ line-height: 1.6;
+ }
+
+ .textarea__label {
+ font-size: 1rem;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+ }
+
+ // ==========================================================================
+ // VARIANT STYLES
+ // ==========================================================================
+
+ &--outlined {
+ .textarea__container {
+ border: 2px solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-md;
+ background-color: $semantic-color-surface-primary;
+ }
+
+ &.textarea-wrapper--focused .textarea__container {
+ border-color: $semantic-color-interactive-primary;
+ box-shadow: 0 0 0 2px rgba($semantic-color-brand-primary, 0.12);
+ }
+ }
+
+ &--filled {
+ .textarea__container {
+ border: none;
+ border-bottom: 2px solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-sm $semantic-border-radius-sm 0 0;
+ background-color: $semantic-color-surface-secondary;
+ }
+
+ &.textarea-wrapper--focused .textarea__container {
+ border-bottom-color: $semantic-color-interactive-primary;
+ background-color: $semantic-color-surface-elevated;
+ }
+ }
+
+ &--underlined {
+ .textarea__container {
+ border: none;
+ border-bottom: 1px solid $semantic-color-border-primary;
+ border-radius: 0;
+ background-color: transparent;
+ padding: $semantic-spacing-component-xs 0;
+ }
+
+ &.textarea-wrapper--focused .textarea__container {
+ border-bottom: 2px solid $semantic-color-interactive-primary;
+ }
+ }
+
+ // ==========================================================================
+ // STATE VARIANTS
+ // ==========================================================================
+
+ &--error {
+ .textarea__container {
+ border-color: $semantic-color-danger;
+ }
+
+ &.textarea-wrapper--focused .textarea__container {
+ border-color: $semantic-color-danger;
+ box-shadow: 0 0 0 2px rgba($semantic-color-danger, 0.12);
+ }
+
+ .textarea__label {
+ color: $semantic-color-danger;
+ }
+ }
+
+ &--success {
+ .textarea__container {
+ border-color: $semantic-color-success;
+ }
+
+ &.textarea-wrapper--focused .textarea__container {
+ border-color: $semantic-color-success;
+ box-shadow: 0 0 0 2px rgba($semantic-color-success, 0.12);
+ }
+
+ .textarea__label {
+ color: $semantic-color-success;
+ }
+ }
+
+ &--warning {
+ .textarea__container {
+ border-color: $semantic-color-warning;
+ }
+
+ &.textarea-wrapper--focused .textarea__container {
+ border-color: $semantic-color-warning;
+ box-shadow: 0 0 0 2px rgba($semantic-color-warning, 0.12);
+ }
+
+ .textarea__label {
+ color: $semantic-color-warning;
+ }
+ }
+
+ &--disabled {
+ opacity: 0.6;
+ pointer-events: none;
+
+ .textarea__container {
+ background-color: $semantic-color-surface-disabled;
+ border-color: $semantic-color-border-disabled;
+ }
+
+ .textarea__label {
+ color: $semantic-color-text-disabled;
+ }
+ }
+
+ &--readonly {
+ .textarea__field {
+ background-color: $semantic-color-surface-secondary;
+ cursor: default;
+ }
+ }
+
+ &--loading {
+ .textarea__field {
+ padding-right: calc($semantic-spacing-component-lg + 24px);
+ }
+ }
+
+ &--resize-none {
+ .textarea__field {
+ resize: none;
+ }
+ }
+
+ &--resize-vertical {
+ .textarea__field {
+ resize: vertical;
+ }
+ }
+
+ &--resize-horizontal {
+ .textarea__field {
+ resize: horizontal;
+ }
+ }
+
+ &--resize-both {
+ .textarea__field {
+ resize: both;
+ }
+ }
+
+ &--auto-resize {
+ .textarea__field {
+ resize: none;
+ overflow: hidden;
+ }
+ }
+}
+
+// ==========================================================================
+// LABEL STYLES
+// ==========================================================================
+
+.textarea__label {
+ display: block;
+ font-weight: 500;
+ color: $semantic-color-text-primary;
+ cursor: pointer;
+ user-select: none;
+ transition: color 0.2s ease-in-out;
+
+ &--required {
+ .textarea__required-indicator {
+ color: $semantic-color-danger;
+ margin-left: $semantic-spacing-micro-tight;
+ }
+ }
+}
+
+// ==========================================================================
+// TEXTAREA CONTAINER
+// ==========================================================================
+
+.textarea__container {
+ display: flex;
+ align-items: flex-start;
+ width: 100%;
+ box-sizing: border-box;
+ transition: all 0.2s ease-in-out;
+ position: relative;
+
+ &:hover:not(.textarea-wrapper--disabled &) {
+ border-color: $semantic-color-border-secondary;
+ }
+
+ &--has-prefix {
+ .textarea__field {
+ padding-left: 0;
+ }
+ }
+
+ &--has-suffix {
+ .textarea__field {
+ padding-right: 0;
+ }
+ }
+}
+
+// ==========================================================================
+// TEXTAREA FIELD
+// ==========================================================================
+
+.textarea__field {
+ flex: 1;
+ border: none;
+ outline: none;
+ background: transparent;
+ color: $semantic-color-text-primary;
+ font-family: inherit;
+ font-weight: 400;
+ min-width: 0;
+ resize: vertical;
+
+ &::placeholder {
+ color: $semantic-color-text-tertiary;
+ transition: opacity 0.2s ease-in-out;
+ }
+
+ &:focus::placeholder {
+ opacity: 0.7;
+ }
+}
+
+// ==========================================================================
+// PREFIX STYLES
+// ==========================================================================
+
+.textarea__prefix-icon {
+ flex-shrink: 0;
+ display: flex;
+ align-items: flex-start;
+ color: $semantic-color-text-secondary;
+ padding-left: 12px; // 8-12px padding from left edge so icon isn't hard against side
+ padding-right: 12px; // 8-12px spacing between icon and text
+ padding-top: $semantic-spacing-component-sm;
+
+ // Size-specific spacing adjustments
+ .textarea-wrapper--sm & {
+ padding-left: 10px; // Slightly less for smaller size
+ padding-right: 10px; // Slightly less for smaller size
+ }
+
+ .textarea-wrapper--lg & {
+ padding-left: 14px; // Slightly more for larger size
+ padding-right: 14px; // Slightly more for larger size
+ }
+
+ fa-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 1rem;
+ height: 1rem;
+ font-size: 1rem;
+
+ .textarea-wrapper--sm & {
+ width: 0.875rem;
+ height: 0.875rem;
+ font-size: 0.875rem;
+ }
+
+ .textarea-wrapper--lg & {
+ width: 1.125rem;
+ height: 1.125rem;
+ font-size: 1.125rem;
+ }
+ }
+}
+
+// ==========================================================================
+// SUFFIX STYLES
+// ==========================================================================
+
+.textarea__suffix {
+ flex-shrink: 0;
+ display: flex;
+ align-items: flex-start;
+ gap: $semantic-spacing-component-xs;
+ padding-right: 12px; // 8-12px padding from right edge so icons aren't hard against side
+ padding-left: 12px; // Consistent spacing from text content
+ padding-top: $semantic-spacing-component-sm;
+
+ // Size-specific spacing adjustments
+ .textarea-wrapper--sm & {
+ padding-left: 10px; // Slightly less for smaller size
+ padding-right: 10px; // Slightly less for smaller size
+ }
+
+ .textarea-wrapper--lg & {
+ padding-left: 14px; // Slightly more for larger size
+ padding-right: 14px; // Slightly more for larger size
+ }
+
+ &--has-content {
+ // Maintain consistent padding even when content is present
+ }
+}
+
+.textarea__clear-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ border: none;
+ border-radius: $semantic-border-radius-sm;
+ background: transparent;
+ color: $semantic-color-text-secondary;
+ cursor: pointer;
+ transition: all 0.2s ease-in-out;
+
+ &:hover {
+ background-color: $semantic-color-surface-interactive;
+ color: $semantic-color-text-primary;
+ }
+
+ &:focus {
+ outline: 2px solid $semantic-color-border-focus;
+ outline-offset: 1px;
+ }
+
+ fa-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 0.875rem;
+ height: 0.875rem;
+ font-size: 0.875rem;
+ }
+}
+
+.textarea__spinner {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ color: $semantic-color-interactive-primary;
+
+ fa-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 0.875rem;
+ height: 0.875rem;
+ font-size: 0.875rem;
+ }
+}
+
+.textarea__suffix-icon {
+ display: flex;
+ align-items: center;
+ color: $semantic-color-text-secondary;
+
+ fa-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 1rem;
+ height: 1rem;
+ font-size: 1rem;
+ }
+}
+
+// ==========================================================================
+// HELPER TEXT AND ERROR MESSAGES
+// ==========================================================================
+
+.textarea__helper-text {
+ margin-top: $semantic-spacing-component-xs;
+ font-size: 0.8125rem;
+ line-height: 1.4;
+ color: $semantic-color-text-secondary;
+
+ &--error {
+ color: $semantic-color-danger;
+ }
+
+ &--success {
+ color: $semantic-color-success;
+ }
+
+ &--warning {
+ color: $semantic-color-warning;
+ }
+}
+
+// ==========================================================================
+// CHARACTER COUNTER
+// ==========================================================================
+
+.textarea__char-counter {
+ position: absolute;
+ bottom: -#{$semantic-spacing-component-lg};
+ right: 0;
+ font-size: 0.75rem;
+ color: $semantic-color-text-tertiary;
+ user-select: none;
+}
+
+// ==========================================================================
+// RESPONSIVE ADJUSTMENTS
+// ==========================================================================
+
+@media (max-width: 768px) {
+ .textarea-wrapper {
+ &--lg {
+ .textarea__container {
+ min-height: 100px;
+ }
+
+ .textarea__field {
+ font-size: 1rem;
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
+ }
+ }
+ }
+}
+
+// ==========================================================================
+// ACCESSIBILITY ENHANCEMENTS
+// ==========================================================================
+
+.textarea__field:focus {
+ // Focus is handled by container border/shadow
+}
+
+// High contrast mode support
+@media (prefers-contrast: high) {
+ .textarea__container {
+ border-width: 2px;
+ }
+
+ .textarea-wrapper--focused .textarea__container {
+ border-width: 3px;
+ }
+}
+
+// Reduced motion support
+@media (prefers-reduced-motion: reduce) {
+ .textarea__container,
+ .textarea__field,
+ .textarea__label,
+ .textarea__clear-btn {
+ transition: none;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/input/textarea.component.ts b/projects/ui-essentials/src/lib/components/forms/input/textarea.component.ts
new file mode 100644
index 0000000..2395b5c
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/input/textarea.component.ts
@@ -0,0 +1,248 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, forwardRef, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
+import { faTimes, faSpinner } from '@fortawesome/free-solid-svg-icons';
+
+export type TextareaSize = 'sm' | 'md' | 'lg';
+export type TextareaVariant = 'outlined' | 'filled' | 'underlined';
+export type TextareaState = 'default' | 'error' | 'success' | 'warning';
+export type TextareaResize = 'none' | 'vertical' | 'horizontal' | 'both';
+
+@Component({
+ selector: 'ui-textarea',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [CommonModule, FontAwesomeModule],
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => TextareaComponent),
+ multi: true
+ }
+ ],
+ template: `
+
+
+ @if (label) {
+
+ {{ label }}
+ @if (required) {
+ *
+ }
+
+ }
+
+
+
+
+ @if (prefixIcon) {
+
+
+
+ }
+
+
+
+
+
+
+
+ @if (clearable && value && !disabled) {
+
+
+
+ }
+
+
+ @if (loading) {
+
+
+
+ }
+
+
+ @if (suffixIcon) {
+
+
+
+ }
+
+
+
+
+ @if (helperText || errorMessage) {
+
+ {{ state === 'error' ? errorMessage : helperText }}
+
+ }
+
+
+ @if (maxLength && showCharacterCount) {
+
+ {{ value.length }}/{{ maxLength }}
+
+ }
+
+ `,
+ styleUrls: ['./textarea.component.scss']
+})
+export class TextareaComponent implements ControlValueAccessor {
+ @Input() label: string = '';
+ @Input() placeholder: string = '';
+ @Input() size: TextareaSize = 'md';
+ @Input() variant: TextareaVariant = 'outlined';
+ @Input() state: TextareaState = 'default';
+ @Input() disabled = false;
+ @Input() readonly = false;
+ @Input() required = false;
+ @Input() loading = false;
+ @Input() clearable = false;
+ @Input() prefixIcon: IconDefinition | null = null;
+ @Input() suffixIcon: IconDefinition | null = null;
+ @Input() helperText: string = '';
+ @Input() errorMessage: string = '';
+ @Input() maxLength: number | null = null;
+ @Input() showCharacterCount = false;
+ @Input() rows: number = 4;
+ @Input() cols: number | null = null;
+ @Input() resize: TextareaResize = 'vertical';
+ @Input() autoResize = false;
+ @Input() textareaId: string = `textarea-${Math.random().toString(36).substr(2, 9)}`;
+
+ @Output() textareaChange = new EventEmitter();
+ @Output() textareaFocus = new EventEmitter();
+ @Output() textareaBlur = new EventEmitter();
+ @Output() keyDown = new EventEmitter();
+ @Output() clear = new EventEmitter();
+
+ value = '';
+ focused = false;
+
+ // FontAwesome icons
+ readonly faTimes = faTimes;
+ readonly faSpinner = faSpinner;
+
+ // ControlValueAccessor implementation
+ private onChange = (value: string) => {};
+ private onTouched = () => {};
+
+ writeValue(value: string): void {
+ this.value = value || '';
+ }
+
+ registerOnChange(fn: (value: string) => void): void {
+ this.onChange = fn;
+ }
+
+ registerOnTouched(fn: () => void): void {
+ this.onTouched = fn;
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ }
+
+ getWrapperClasses(): string {
+ const classes = [
+ `textarea-wrapper--${this.size}`,
+ `textarea-wrapper--${this.variant}`,
+ `textarea-wrapper--${this.state}`,
+ `textarea-wrapper--resize-${this.resize}`
+ ];
+
+ if (this.disabled) classes.push('textarea-wrapper--disabled');
+ if (this.focused) classes.push('textarea-wrapper--focused');
+ if (this.readonly) classes.push('textarea-wrapper--readonly');
+ if (this.loading) classes.push('textarea-wrapper--loading');
+ if (this.autoResize) classes.push('textarea-wrapper--auto-resize');
+
+ return classes.join(' ');
+ }
+
+ getContainerClasses(): string {
+ const classes: string[] = [];
+
+ if (this.prefixIcon) classes.push('textarea__container--has-prefix');
+ if (this.hasSuffixContent()) classes.push('textarea__container--has-suffix');
+
+ return classes.join(' ');
+ }
+
+ getHelperTextClasses(): string {
+ const classes = [`textarea__helper-text--${this.state}`];
+ return classes.join(' ');
+ }
+
+ hasSuffixContent(): boolean {
+ return !!(
+ this.clearable ||
+ this.loading ||
+ this.suffixIcon
+ );
+ }
+
+ onInput(event: Event): void {
+ const target = event.target as HTMLTextAreaElement;
+ this.value = target.value;
+
+ if (this.autoResize) {
+ this.adjustHeight(target);
+ }
+
+ this.onChange(this.value);
+ this.textareaChange.emit(this.value);
+ }
+
+ onFocus(event: FocusEvent): void {
+ this.focused = true;
+ this.textareaFocus.emit(event);
+ }
+
+ onBlur(event: FocusEvent): void {
+ this.focused = false;
+ this.onTouched();
+ this.textareaBlur.emit(event);
+ }
+
+ onKeyDown(event: KeyboardEvent): void {
+ this.keyDown.emit(event);
+ }
+
+ clearValue(): void {
+ this.value = '';
+ this.onChange(this.value);
+ this.textareaChange.emit(this.value);
+ this.clear.emit();
+ }
+
+ private adjustHeight(textarea: HTMLTextAreaElement): void {
+ textarea.style.height = 'auto';
+ textarea.style.height = textarea.scrollHeight + 'px';
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/radio/index.ts b/projects/ui-essentials/src/lib/components/forms/radio/index.ts
new file mode 100644
index 0000000..e9fdf90
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/radio/index.ts
@@ -0,0 +1,2 @@
+export * from './radio-button.component';
+export * from './radio-group.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/radio/radio-button.component.scss b/projects/ui-essentials/src/lib/components/forms/radio/radio-button.component.scss
new file mode 100644
index 0000000..c9a75fa
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/radio/radio-button.component.scss
@@ -0,0 +1,435 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+// Tokens available globally via main application styles
+
+// ==========================================================================
+// RADIO BUTTON COMPONENT
+// ==========================================================================
+// Comprehensive radio button component with multiple variants and sizes
+// Uses semantic design tokens for consistent styling across the design system
+// ==========================================================================
+
+.radio-button-wrapper {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ position: relative;
+
+ // ==========================================================================
+ // SIZE VARIANTS
+ // ==========================================================================
+
+ &--sm {
+ .radio-button {
+ width: 20px;
+ height: 20px;
+ }
+
+ .radio-button__circle {
+ width: 20px;
+ height: 20px;
+ border-width: 1px;
+ }
+
+ .radio-button__inner-circle {
+ width: 8px;
+ height: 8px;
+ top: 4px;
+ left: 4px;
+ }
+
+ .radio-button__label {
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ }
+
+ .radio-button__description {
+ font-size: 0.75rem;
+ line-height: 1rem;
+ }
+
+ .radio-button-container {
+ gap: $semantic-spacing-component-xs;
+ }
+ }
+
+ &--md {
+ .radio-button {
+ width: 24px;
+ height: 24px;
+ }
+
+ .radio-button__circle {
+ width: 24px;
+ height: 24px;
+ border-width: 2px;
+ }
+
+ .radio-button__inner-circle {
+ width: 10px;
+ height: 10px;
+ top: 5px;
+ left: 5px;
+ }
+
+ .radio-button__label {
+ font-size: 1rem;
+ line-height: 1.5rem;
+ }
+
+ .radio-button__description {
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ }
+
+ .radio-button-container {
+ gap: $semantic-spacing-component-sm;
+ }
+ }
+
+ &--lg {
+ .radio-button {
+ width: 28px;
+ height: 28px;
+ }
+
+ .radio-button__circle {
+ width: 28px;
+ height: 28px;
+ border-width: 2px;
+ }
+
+ .radio-button__inner-circle {
+ width: 12px;
+ height: 12px;
+ top: 6px;
+ left: 6px;
+ }
+
+ .radio-button__label {
+ font-size: 1.125rem;
+ line-height: 1.75rem;
+ }
+
+ .radio-button__description {
+ font-size: 1rem;
+ line-height: 1.5rem;
+ }
+
+ .radio-button-container {
+ gap: $semantic-spacing-component-md;
+ }
+ }
+
+ // ==========================================================================
+ // VARIANT STYLES
+ // ==========================================================================
+
+ &--primary {
+ .radio-button__circle {
+ border-color: $semantic-color-border-primary;
+ }
+
+ &.radio-button-wrapper--checked .radio-button__circle,
+ &.radio-button-wrapper--focused .radio-button__circle {
+ border-color: $semantic-color-interactive-primary;
+ }
+
+ .radio-button__inner-circle--selected {
+ background-color: $semantic-color-interactive-primary;
+ }
+ }
+
+ &--secondary {
+ .radio-button__circle {
+ border-color: $semantic-color-border-secondary;
+ }
+
+ &.radio-button-wrapper--checked .radio-button__circle,
+ &.radio-button-wrapper--focused .radio-button__circle {
+ border-color: $semantic-color-interactive-secondary;
+ }
+
+ .radio-button__inner-circle--selected {
+ background-color: $semantic-color-interactive-secondary;
+ }
+ }
+
+ &--success {
+ .radio-button__circle {
+ border-color: $semantic-color-border-primary;
+ }
+
+ &.radio-button-wrapper--checked .radio-button__circle,
+ &.radio-button-wrapper--focused .radio-button__circle {
+ border-color: $semantic-color-success;
+ }
+
+ .radio-button__inner-circle--selected {
+ background-color: $semantic-color-success;
+ }
+ }
+
+ &--warning {
+ .radio-button__circle {
+ border-color: $semantic-color-border-primary;
+ }
+
+ &.radio-button-wrapper--checked .radio-button__circle,
+ &.radio-button-wrapper--focused .radio-button__circle {
+ border-color: $semantic-color-warning;
+ }
+
+ .radio-button__inner-circle--selected {
+ background-color: $semantic-color-warning;
+ }
+ }
+
+ &--danger {
+ .radio-button__circle {
+ border-color: $semantic-color-border-primary;
+ }
+
+ &.radio-button-wrapper--checked .radio-button__circle,
+ &.radio-button-wrapper--focused .radio-button__circle {
+ border-color: $semantic-color-danger;
+ }
+
+ .radio-button__inner-circle--selected {
+ background-color: $semantic-color-danger;
+ }
+ }
+
+ // ==========================================================================
+ // STATE VARIANTS
+ // ==========================================================================
+
+ &--disabled {
+ opacity: 0.6;
+ pointer-events: none;
+
+ .radio-button__circle {
+ border-color: $semantic-color-border-disabled;
+ background-color: $semantic-color-surface-disabled;
+ }
+
+ .radio-button__inner-circle--selected {
+ background-color: $semantic-color-text-disabled;
+ }
+
+ .radio-button__label {
+ color: $semantic-color-text-disabled;
+ }
+
+ .radio-button__description {
+ color: $semantic-color-text-disabled;
+ }
+ }
+
+ &--error {
+ .radio-button__circle {
+ border-color: $semantic-color-danger;
+ }
+
+ .radio-button__label {
+ color: $semantic-color-danger;
+ }
+ }
+
+ &--focused {
+ .radio-button__circle {
+ box-shadow: 0 0 0 2px rgba($semantic-color-primary, 0.2);
+ }
+ }
+}
+
+// ==========================================================================
+// RADIO BUTTON CONTAINER
+// ==========================================================================
+
+.radio-button-container {
+ display: flex;
+ align-items: flex-start;
+ cursor: pointer;
+ transition: all 0.2s ease-in-out;
+
+ &:hover:not(.radio-button-wrapper--disabled &) {
+ .radio-button__circle {
+ border-color: $semantic-color-border-secondary;
+ }
+ }
+}
+
+// ==========================================================================
+// RADIO BUTTON
+// ==========================================================================
+
+.radio-button {
+ position: relative;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.radio-button__input {
+ position: absolute;
+ opacity: 0;
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ cursor: pointer;
+
+ &:focus {
+ outline: none;
+ }
+}
+
+.radio-button__circle {
+ position: relative;
+ border-radius: $semantic-border-radius-full;
+ border: 2px solid $semantic-color-border-primary;
+ background-color: $semantic-color-surface-primary;
+ transition: all 0.2s ease-in-out;
+ box-sizing: border-box;
+}
+
+.radio-button__inner-circle {
+ position: absolute;
+ border-radius: $semantic-border-radius-full;
+ opacity: 0;
+ transform: scale(0);
+ transition: all 0.2s ease-in-out;
+
+ &--selected {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+// ==========================================================================
+// CONTENT AREA
+// ==========================================================================
+
+.radio-button-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+}
+
+.radio-button__label {
+ font-weight: 500;
+ color: $semantic-color-text-primary;
+ cursor: pointer;
+ transition: color 0.2s ease-in-out;
+ margin: 0;
+ line-height: 1.5;
+}
+
+.radio-button__description {
+ color: $semantic-color-text-secondary;
+ margin: 0;
+ margin-top: $semantic-spacing-micro-tight;
+ line-height: 1.4;
+}
+
+// ==========================================================================
+// HELPER TEXT AND ERROR MESSAGES
+// ==========================================================================
+
+.radio-button__helper-text {
+ margin-top: $semantic-spacing-component-xs;
+ font-size: 0.8125rem;
+ line-height: 1.4;
+ color: $semantic-color-text-secondary;
+ padding-left: calc(24px + #{$semantic-spacing-component-sm}); // Align with label text
+
+ .radio-button-wrapper--sm & {
+ padding-left: calc(20px + #{$semantic-spacing-component-xs});
+ }
+
+ .radio-button-wrapper--lg & {
+ padding-left: calc(28px + #{$semantic-spacing-component-md});
+ }
+}
+
+.radio-button__error-text {
+ margin-top: $semantic-spacing-component-xs;
+ font-size: 0.8125rem;
+ line-height: 1.4;
+ color: $semantic-color-danger;
+ padding-left: calc(24px + #{$semantic-spacing-component-sm}); // Align with label text
+
+ .radio-button-wrapper--sm & {
+ padding-left: calc(20px + #{$semantic-spacing-component-xs});
+ }
+
+ .radio-button-wrapper--lg & {
+ padding-left: calc(28px + #{$semantic-spacing-component-md});
+ }
+}
+
+// ==========================================================================
+// RESPONSIVE ADJUSTMENTS
+// ==========================================================================
+
+@media (max-width: 768px) {
+ .radio-button-wrapper {
+ &--lg {
+ .radio-button {
+ width: 24px;
+ height: 24px;
+ }
+
+ .radio-button__circle {
+ width: 24px;
+ height: 24px;
+ }
+
+ .radio-button__inner-circle {
+ width: 10px;
+ height: 10px;
+ top: 5px;
+ left: 5px;
+ }
+
+ .radio-button__label {
+ font-size: 1rem;
+ }
+
+ .radio-button__description {
+ font-size: 0.875rem;
+ }
+ }
+ }
+}
+
+// ==========================================================================
+// ACCESSIBILITY ENHANCEMENTS
+// ==========================================================================
+
+.radio-button__input:focus-visible + .radio-button__circle {
+ box-shadow: 0 0 0 2px $semantic-color-border-focus;
+ outline: none;
+}
+
+// High contrast mode support
+@media (prefers-contrast: high) {
+ .radio-button__circle {
+ border-width: 3px;
+ }
+
+ .radio-button-wrapper--checked .radio-button__circle {
+ border-width: 3px;
+ }
+}
+
+// Reduced motion support
+@media (prefers-reduced-motion: reduce) {
+ .radio-button__circle,
+ .radio-button__inner-circle,
+ .radio-button-container {
+ transition: none;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/radio/radio-button.component.ts b/projects/ui-essentials/src/lib/components/forms/radio/radio-button.component.ts
new file mode 100644
index 0000000..ab76f63
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/radio/radio-button.component.ts
@@ -0,0 +1,166 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, forwardRef, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+
+export type RadioButtonSize = 'sm' | 'md' | 'lg';
+export type RadioButtonVariant = 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
+export type RadioButtonState = 'default' | 'error' | 'disabled';
+
+export interface RadioButtonData {
+ value: string;
+ label: string;
+ disabled?: boolean;
+ description?: string;
+}
+
+@Component({
+ selector: 'ui-radio-button',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [CommonModule],
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => RadioButtonComponent),
+ multi: true
+ }
+ ],
+ template: `
+
+ `,
+ styleUrls: ['./radio-button.component.scss']
+})
+export class RadioButtonComponent implements ControlValueAccessor {
+ @Input() value: string = '';
+ @Input() label: string = '';
+ @Input() name: string = '';
+ @Input() size: RadioButtonSize = 'md';
+ @Input() variant: RadioButtonVariant = 'primary';
+ @Input() state: RadioButtonState = 'default';
+ @Input() disabled = false;
+ @Input() required = false;
+ @Input() description: string = '';
+ @Input() helperText: string = '';
+ @Input() error: string = '';
+ @Input() radioId: string = `radio-${Math.random().toString(36).substr(2, 9)}`;
+
+ @Output() selectionChange = new EventEmitter();
+ @Output() radioFocus = new EventEmitter();
+ @Output() radioBlur = new EventEmitter();
+
+ private selectedValue = signal('');
+ private focused = signal(false);
+
+ // ControlValueAccessor implementation
+ private onChange = (value: string) => {};
+ private onTouched = () => {};
+
+ writeValue(value: string): void {
+ this.selectedValue.set(value || '');
+ }
+
+ registerOnChange(fn: (value: string) => void): void {
+ this.onChange = fn;
+ }
+
+ registerOnTouched(fn: () => void): void {
+ this.onTouched = fn;
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ }
+
+ isChecked(): boolean {
+ return this.selectedValue() === this.value;
+ }
+
+ getWrapperClasses(): string {
+ const classes = [
+ `radio-button-wrapper--${this.size}`,
+ `radio-button-wrapper--${this.variant}`,
+ `radio-button-wrapper--${this.state}`
+ ];
+
+ if (this.disabled) classes.push('radio-button-wrapper--disabled');
+ if (this.focused()) classes.push('radio-button-wrapper--focused');
+ if (this.isChecked()) classes.push('radio-button-wrapper--checked');
+ if (this.error) classes.push('radio-button-wrapper--error');
+ if (this.description) classes.push('radio-button-wrapper--has-description');
+
+ return classes.join(' ');
+ }
+
+ getRadioClasses(): string {
+ const classes: string[] = [];
+
+ if (this.isChecked()) classes.push('radio-button--checked');
+ if (this.disabled) classes.push('radio-button--disabled');
+
+ return classes.join(' ');
+ }
+
+ getContentClasses(): string {
+ const classes: string[] = [];
+
+ if (this.description) classes.push('radio-button-content--has-description');
+
+ return classes.join(' ');
+ }
+
+ onSelectionChange(event: Event): void {
+ const target = event.target as HTMLInputElement;
+ if (target.checked) {
+ this.selectedValue.set(this.value);
+ this.onChange(this.value);
+ this.selectionChange.emit(this.value);
+ }
+ }
+
+ onFocus(event: FocusEvent): void {
+ this.focused.set(true);
+ this.radioFocus.emit(event);
+ }
+
+ onBlur(event: FocusEvent): void {
+ this.focused.set(false);
+ this.onTouched();
+ this.radioBlur.emit(event);
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/radio/radio-group.component.scss b/projects/ui-essentials/src/lib/components/forms/radio/radio-group.component.scss
new file mode 100644
index 0000000..ab81553
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/radio/radio-group.component.scss
@@ -0,0 +1,272 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+// Tokens available globally via main application styles
+
+// ==========================================================================
+// RADIO GROUP COMPONENT
+// ==========================================================================
+// Container component for managing groups of radio buttons
+// Uses semantic design tokens for consistent styling across the design system
+// ==========================================================================
+
+.radio-group-wrapper {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ position: relative;
+
+ // ==========================================================================
+ // SIZE VARIANTS
+ // ==========================================================================
+
+ &--sm {
+ .radio-group__label {
+ font-size: 0.875rem;
+ margin-bottom: $semantic-spacing-component-xs;
+ }
+
+ .radio-group {
+ gap: $semantic-spacing-component-xs;
+ }
+
+ &.radio-group-wrapper--horizontal .radio-group {
+ gap: $semantic-spacing-component-sm;
+ }
+ }
+
+ &--md {
+ .radio-group__label {
+ font-size: 1rem;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+
+ .radio-group {
+ gap: $semantic-spacing-component-sm;
+ }
+
+ &.radio-group-wrapper--horizontal .radio-group {
+ gap: $semantic-spacing-component-md;
+ }
+ }
+
+ &--lg {
+ .radio-group__label {
+ font-size: 1.125rem;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+
+ .radio-group {
+ gap: $semantic-spacing-component-sm;
+ }
+
+ &.radio-group-wrapper--horizontal .radio-group {
+ gap: $semantic-spacing-component-lg;
+ }
+ }
+
+ // ==========================================================================
+ // VARIANT STYLES
+ // ==========================================================================
+
+ &--primary {
+ .radio-group__label {
+ color: $semantic-color-text-primary;
+ }
+ }
+
+ &--secondary {
+ .radio-group__label {
+ color: $semantic-color-text-primary;
+ }
+ }
+
+ &--success {
+ .radio-group__label {
+ color: $semantic-color-text-primary;
+ }
+
+ &.radio-group-wrapper--error .radio-group__label {
+ color: $semantic-color-success;
+ }
+ }
+
+ &--warning {
+ .radio-group__label {
+ color: $semantic-color-text-primary;
+ }
+
+ &.radio-group-wrapper--error .radio-group__label {
+ color: $semantic-color-warning;
+ }
+ }
+
+ &--danger {
+ .radio-group__label {
+ color: $semantic-color-text-primary;
+ }
+
+ &.radio-group-wrapper--error .radio-group__label {
+ color: $semantic-color-danger;
+ }
+ }
+
+ // ==========================================================================
+ // ORIENTATION VARIANTS
+ // ==========================================================================
+
+ &--horizontal {
+ .radio-group {
+ flex-direction: row;
+ flex-wrap: wrap;
+ align-items: flex-start;
+ }
+ }
+
+ &--vertical {
+ .radio-group {
+ flex-direction: column;
+ }
+ }
+
+ // ==========================================================================
+ // STATE VARIANTS
+ // ==========================================================================
+
+ &--disabled {
+ opacity: 0.6;
+ pointer-events: none;
+
+ .radio-group__label {
+ color: $semantic-color-text-disabled;
+ }
+ }
+
+ &--error {
+ .radio-group__label {
+ color: $semantic-color-danger;
+ }
+ }
+
+ &--dense {
+ .radio-group {
+ gap: $semantic-spacing-component-xs;
+ }
+
+ &.radio-group-wrapper--horizontal .radio-group {
+ gap: $semantic-spacing-component-sm;
+ }
+
+ .radio-group__label {
+ margin-bottom: $semantic-spacing-component-xs;
+ }
+ }
+}
+
+// ==========================================================================
+// RADIO GROUP LABEL
+// ==========================================================================
+
+.radio-group__label {
+ display: block;
+ font-weight: 600;
+ color: $semantic-color-text-primary;
+ line-height: 1.5;
+ user-select: none;
+
+ &--required {
+ .radio-group__required-indicator {
+ color: $semantic-color-danger;
+ margin-left: $semantic-spacing-micro-tight;
+ font-weight: 400;
+ }
+ }
+}
+
+// ==========================================================================
+// RADIO GROUP CONTAINER
+// ==========================================================================
+
+.radio-group {
+ display: flex;
+ flex-direction: column;
+ position: relative;
+
+ &--vertical {
+ align-items: flex-start;
+ }
+
+ &--horizontal {
+ flex-direction: row;
+ flex-wrap: wrap;
+ align-items: flex-start;
+ }
+
+ &--dense {
+ gap: $semantic-spacing-component-xs;
+ }
+}
+
+// ==========================================================================
+// HELPER TEXT AND ERROR MESSAGES
+// ==========================================================================
+
+.radio-group__helper-text {
+ margin-top: $semantic-spacing-component-sm;
+ font-size: 0.8125rem;
+ line-height: 1.4;
+ color: $semantic-color-text-secondary;
+}
+
+.radio-group__error-text {
+ margin-top: $semantic-spacing-component-sm;
+ font-size: 0.8125rem;
+ line-height: 1.4;
+ color: $semantic-color-danger;
+}
+
+// ==========================================================================
+// RESPONSIVE ADJUSTMENTS
+// ==========================================================================
+
+@media (max-width: 768px) {
+ .radio-group-wrapper {
+ &--horizontal {
+ .radio-group {
+ flex-direction: column;
+ gap: $semantic-spacing-component-sm;
+ }
+ }
+
+ &--lg {
+ .radio-group__label {
+ font-size: 1rem;
+ }
+ }
+ }
+}
+
+// ==========================================================================
+// ACCESSIBILITY ENHANCEMENTS
+// ==========================================================================
+
+.radio-group[role="radiogroup"]:focus-within {
+ // Focus styles are handled by individual radio buttons
+}
+
+// High contrast mode support
+@media (prefers-contrast: high) {
+ .radio-group__label {
+ font-weight: 700;
+ }
+
+ .radio-group-wrapper--error .radio-group__label {
+ text-decoration: underline;
+ }
+}
+
+// Reduced motion support
+@media (prefers-reduced-motion: reduce) {
+ .radio-group__label {
+ transition: none;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/radio/radio-group.component.ts b/projects/ui-essentials/src/lib/components/forms/radio/radio-group.component.ts
new file mode 100644
index 0000000..31a9df8
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/radio/radio-group.component.ts
@@ -0,0 +1,180 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, forwardRef, ContentChildren, QueryList, AfterContentInit, OnDestroy, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+import { Subject, takeUntil } from 'rxjs';
+import { RadioButtonComponent, RadioButtonSize, RadioButtonVariant, RadioButtonState, RadioButtonData } from './radio-button.component';
+
+export type RadioGroupOrientation = 'horizontal' | 'vertical';
+
+@Component({
+ selector: 'ui-radio-group',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [CommonModule, RadioButtonComponent],
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => RadioGroupComponent),
+ multi: true
+ }
+ ],
+ template: `
+
+ @if (label) {
+
+ {{ label }}
+ @if (required) {
+ *
+ }
+
+ }
+
+
+ @if (options && options.length > 0) {
+ @for (option of options; track option.value) {
+
+ }
+ } @else {
+
+ }
+
+
+ @if (helperText && !errorMessage) {
+
{{ helperText }}
+ }
+
+ @if (errorMessage) {
+
{{ errorMessage }}
+ }
+
+ `,
+ styleUrls: ['./radio-group.component.scss']
+})
+export class RadioGroupComponent implements ControlValueAccessor, AfterContentInit, OnDestroy {
+ @ContentChildren(RadioButtonComponent) radioButtons!: QueryList;
+
+ @Input() options: RadioButtonData[] = [];
+ @Input() label: string = '';
+ @Input() name: string = '';
+ @Input() size: RadioButtonSize = 'md';
+ @Input() variant: RadioButtonVariant = 'primary';
+ @Input() state: RadioButtonState = 'default';
+ @Input() orientation: RadioGroupOrientation = 'vertical';
+ @Input() disabled = false;
+ @Input() required = false;
+ @Input() helperText: string = '';
+ @Input() errorMessage: string = '';
+ @Input() dense = false;
+ @Input() groupId: string = `radio-group-${Math.random().toString(36).substr(2, 9)}`;
+
+ @Output() selectionChange = new EventEmitter();
+ @Output() groupFocus = new EventEmitter();
+ @Output() groupBlur = new EventEmitter();
+
+ private selectedValue = signal('');
+ private destroy$ = new Subject();
+
+ // ControlValueAccessor implementation
+ private onChange = (value: string) => {};
+ private onTouched = () => {};
+
+ ngAfterContentInit(): void {
+ this.syncRadioButtons();
+
+ // Listen for changes in radio buttons
+ this.radioButtons.changes.pipe(takeUntil(this.destroy$)).subscribe(() => {
+ this.syncRadioButtons();
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.destroy$.next();
+ this.destroy$.complete();
+ }
+
+ writeValue(value: string): void {
+ this.selectedValue.set(value || '');
+ this.syncRadioButtons();
+ }
+
+ registerOnChange(fn: (value: string) => void): void {
+ this.onChange = fn;
+ }
+
+ registerOnTouched(fn: () => void): void {
+ this.onTouched = fn;
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ this.syncRadioButtons();
+ }
+
+ private syncRadioButtons(): void {
+ if (this.radioButtons) {
+ this.radioButtons.forEach(radio => {
+ radio.name = this.name || this.groupId;
+ radio.size = this.size;
+ radio.variant = this.variant;
+ radio.state = this.state;
+ radio.writeValue(this.selectedValue());
+ radio.setDisabledState(this.disabled);
+ radio.registerOnChange(this.onChange);
+ radio.registerOnTouched(this.onTouched);
+ });
+ }
+ }
+
+ getWrapperClasses(): string {
+ const classes = [
+ `radio-group-wrapper--${this.size}`,
+ `radio-group-wrapper--${this.variant}`,
+ `radio-group-wrapper--${this.state}`,
+ `radio-group-wrapper--${this.orientation}`
+ ];
+
+ if (this.disabled) classes.push('radio-group-wrapper--disabled');
+ if (this.dense) classes.push('radio-group-wrapper--dense');
+ if (this.errorMessage) classes.push('radio-group-wrapper--error');
+
+ return classes.join(' ');
+ }
+
+ getGroupClasses(): string {
+ const classes = [
+ `radio-group--${this.orientation}`
+ ];
+
+ if (this.dense) classes.push('radio-group--dense');
+
+ return classes.join(' ');
+ }
+
+ onSelectionChange(value: string): void {
+ this.selectedValue.set(value);
+ this.onChange(value);
+ this.selectionChange.emit(value);
+ this.syncRadioButtons();
+ }
+
+ onRadioFocus(event: FocusEvent): void {
+ this.groupFocus.emit(event);
+ }
+
+ onRadioBlur(event: FocusEvent): void {
+ this.onTouched();
+ this.groupBlur.emit(event);
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/search/index.ts b/projects/ui-essentials/src/lib/components/forms/search/index.ts
new file mode 100644
index 0000000..609f7f8
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/search/index.ts
@@ -0,0 +1 @@
+export * from './search-bar.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/search/search-bar.component.scss b/projects/ui-essentials/src/lib/components/forms/search/search-bar.component.scss
new file mode 100644
index 0000000..47cd430
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/search/search-bar.component.scss
@@ -0,0 +1,542 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+// Tokens available globally via main application styles
+
+// ==========================================================================
+// SEARCH BAR COMPONENT STYLES
+// ==========================================================================
+// Modern search bar implementation using design tokens
+// Follows Material Design 3 principles with semantic token system
+// ==========================================================================
+
+.search-bar-wrapper {
+ position: relative;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+
+ // Size variants
+ &.search-bar-wrapper--sm {
+ .search-bar {
+ height: 40px;
+ }
+ }
+
+ &.search-bar-wrapper--md {
+ .search-bar {
+ height: 48px;
+ }
+ }
+
+ &.search-bar-wrapper--lg {
+ .search-bar {
+ height: 56px;
+ }
+ }
+
+ // Variant styles
+ &.search-bar-wrapper--outlined {
+ .search-bar {
+ background-color: $semantic-color-surface-primary;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+
+ &:hover:not(.search-bar--disabled) {
+ border-color: $semantic-color-border-focus;
+ }
+
+ &.search-bar--focused {
+ border-color: $semantic-color-brand-primary;
+ box-shadow: $semantic-shadow-button-focus;
+ }
+ }
+ }
+
+ &.search-bar-wrapper--filled {
+ .search-bar {
+ background-color: $semantic-color-surface-secondary;
+ border: $semantic-border-width-1 solid transparent;
+
+ &:hover:not(.search-bar--disabled) {
+ background-color: $semantic-color-surface-secondary;
+ }
+
+ &.search-bar--focused {
+ border-color: $semantic-color-brand-primary;
+ box-shadow: $semantic-shadow-button-focus;
+ }
+ }
+ }
+
+ &.search-bar-wrapper--elevated {
+ .search-bar {
+ background-color: $semantic-color-surface-primary;
+ border: none;
+ box-shadow: $semantic-shadow-elevation-1;
+
+ &:hover:not(.search-bar--disabled) {
+ box-shadow: $semantic-shadow-elevation-2;
+ }
+
+ &.search-bar--focused {
+ box-shadow: $semantic-shadow-elevation-3;
+ }
+ }
+ }
+
+ // State variants
+ &.search-bar-wrapper--error {
+ .search-bar {
+ border-color: $semantic-color-danger;
+
+ &.search-bar--focused {
+ box-shadow: $semantic-shadow-danger;
+ }
+ }
+ }
+
+ &.search-bar-wrapper--disabled {
+ opacity: 0.6;
+
+ .search-bar {
+ background-color: $semantic-color-surface-disabled;
+ border-color: $semantic-color-border-disabled;
+ cursor: not-allowed;
+ }
+ }
+}
+
+.search-bar {
+ display: flex;
+ align-items: center;
+ border-radius: $semantic-border-radius-3xl;
+ transition: all $semantic-duration-fast $semantic-easing-standard;
+ position: relative;
+ overflow: hidden;
+
+ // Size-specific heights handled by wrapper
+
+ &--disabled {
+ pointer-events: none;
+ }
+}
+
+.search-bar__leading-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: $semantic-color-text-secondary;
+ flex-shrink: 0;
+
+ &--sm {
+ width: 40px;
+ height: 40px;
+ padding-left: $semantic-spacing-component-sm;
+
+ fa-icon {
+ font-size: $semantic-typography-font-size-lg;
+ }
+ }
+
+ &--md {
+ width: 48px;
+ height: 48px;
+ padding-left: $semantic-spacing-component-md;
+
+ fa-icon {
+ font-size: $semantic-typography-font-size-2xl;
+ }
+ }
+
+ &--lg {
+ width: 56px;
+ height: 56px;
+ padding-left: $semantic-spacing-component-lg;
+
+ fa-icon {
+ font-size: $semantic-typography-font-size-lg;
+ }
+ }
+}
+
+.search-bar__input-wrapper {
+ flex: 1;
+ position: relative;
+ height: 100%;
+ display: flex;
+ align-items: center;
+
+ &--sm {
+ padding: 0 $semantic-spacing-component-xs;
+ }
+
+ &--md {
+ padding: 0 $semantic-spacing-component-sm;
+ }
+
+ &--lg {
+ padding: 0 $semantic-spacing-component-md;
+ }
+}
+
+.search-bar__label {
+ position: absolute;
+ left: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ color: $semantic-color-text-secondary;
+ font-size: $semantic-typography-font-size-md;
+ font-weight: $semantic-typography-font-weight-normal;
+ pointer-events: none;
+ transition: all $semantic-duration-fast $semantic-easing-standard;
+ z-index: 1;
+
+ &--floating {
+ top: 8px;
+ font-size: $semantic-typography-font-size-xs;
+ color: $semantic-color-brand-primary;
+ transform: translateY(0);
+ }
+
+ .search-bar__required-indicator {
+ color: $semantic-color-danger;
+ margin-left: 2px;
+ }
+}
+
+.search-bar__input {
+ width: 100%;
+ height: 100%;
+ border: none;
+ background: transparent;
+ outline: none;
+ color: $semantic-color-text-primary;
+ font-size: $semantic-typography-font-size-md;
+ font-weight: $semantic-typography-font-weight-normal;
+ line-height: $semantic-typography-line-height-normal;
+ caret-color: $semantic-color-brand-primary;
+
+ &::placeholder {
+ color: $semantic-color-text-secondary;
+ transition: color $semantic-duration-fast $semantic-easing-standard;
+ }
+
+ &:focus::placeholder {
+ color: transparent;
+ }
+
+ // Remove default search input styling
+ &::-webkit-search-cancel-button,
+ &::-webkit-search-decoration {
+ -webkit-appearance: none;
+ }
+
+ // Size variants
+ &--sm {
+ font-size: $semantic-typography-font-size-sm;
+ padding: 0 $semantic-spacing-component-xs;
+ }
+
+ &--md {
+ font-size: $semantic-typography-font-size-md;
+ padding: 0 $semantic-spacing-component-sm;
+ }
+
+ &--lg {
+ font-size: $semantic-typography-font-size-lg;
+ padding: 0 $semantic-spacing-component-md;
+ }
+
+ &--has-value {
+ font-weight: $semantic-typography-font-weight-medium;
+ }
+}
+
+.search-bar__trailing-icons {
+ display: flex;
+ align-items: center;
+ gap: $semantic-spacing-component-xs;
+ flex-shrink: 0;
+
+ &--sm {
+ padding-right: $semantic-spacing-component-sm;
+ }
+
+ &--md {
+ padding-right: $semantic-spacing-component-md;
+ }
+
+ &--lg {
+ padding-right: $semantic-spacing-component-lg;
+ }
+}
+
+.search-bar__icon-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: none;
+ background: transparent;
+ color: $semantic-color-text-secondary;
+ cursor: pointer;
+ border-radius: $semantic-border-radius-sm;
+ transition: all $semantic-duration-fast $semantic-easing-standard;
+
+ &:hover:not(:disabled) {
+ background-color: $semantic-color-surface-secondary;
+ color: $semantic-color-text-primary;
+ }
+
+ &:active:not(:disabled) {
+ transform: scale(0.95);
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+
+ // Size variants
+ &--sm {
+ width: 32px;
+ height: 32px;
+
+ fa-icon {
+ font-size: $semantic-typography-font-size-md;
+ }
+ }
+
+ &--md {
+ width: 36px;
+ height: 36px;
+
+ fa-icon {
+ font-size: $semantic-typography-font-size-lg;
+ }
+ }
+
+ &--lg {
+ width: 40px;
+ height: 40px;
+
+ fa-icon {
+ font-size: $semantic-typography-font-size-2xl;
+ }
+ }
+}
+
+.search-bar__clear-button {
+ color: $semantic-color-text-tertiary;
+
+ &:hover:not(:disabled) {
+ color: $semantic-color-danger;
+ background-color: rgba($semantic-color-danger, 0.1);
+ }
+}
+
+.search-bar__suggestions {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ background-color: $semantic-color-surface-primary;
+ border-radius: $semantic-border-radius-lg;
+ box-shadow: $semantic-shadow-elevation-3;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ max-height: 300px;
+ overflow-y: auto;
+ z-index: 1000;
+ margin-top: $semantic-spacing-component-xs;
+
+ &--sm {
+ margin-top: 4px;
+ }
+
+ &--md {
+ margin-top: 6px;
+ }
+
+ &--lg {
+ margin-top: 8px;
+ }
+}
+
+.search-bar__suggestion {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ gap: $semantic-spacing-component-sm;
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
+ border: none;
+ background: transparent;
+ color: $semantic-color-text-primary;
+ cursor: pointer;
+ text-align: left;
+ transition: background-color $semantic-duration-fast $semantic-easing-standard;
+
+ &:hover,
+ &--selected {
+ background-color: $semantic-color-surface-secondary;
+ }
+
+ &:first-child {
+ border-top-left-radius: $semantic-border-radius-lg;
+ border-top-right-radius: $semantic-border-radius-lg;
+ }
+
+ &:last-child {
+ border-bottom-left-radius: $semantic-border-radius-lg;
+ border-bottom-right-radius: $semantic-border-radius-lg;
+ }
+
+ // Size variants
+ &--sm {
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+
+ .search-bar__suggestion-text {
+ font-size: $semantic-typography-font-size-sm;
+ }
+
+ .search-bar__suggestion-category {
+ font-size: $semantic-typography-font-size-xs;
+ }
+ }
+
+ &--md {
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
+
+ .search-bar__suggestion-text {
+ font-size: $semantic-typography-font-size-md;
+ }
+
+ .search-bar__suggestion-category {
+ font-size: $semantic-typography-font-size-sm;
+ }
+ }
+
+ &--lg {
+ padding: $semantic-spacing-component-md $semantic-spacing-component-lg;
+
+ .search-bar__suggestion-text {
+ font-size: $semantic-typography-font-size-lg;
+ }
+
+ .search-bar__suggestion-category {
+ font-size: $semantic-typography-font-size-md;
+ }
+ }
+}
+
+.search-bar__suggestion-icon {
+ color: $semantic-color-text-secondary;
+ flex-shrink: 0;
+ width: 20px;
+ height: 20px;
+}
+
+.search-bar__suggestion-text {
+ flex: 1;
+ font-weight: $semantic-typography-font-weight-normal;
+}
+
+.search-bar__suggestion-category {
+ color: $semantic-color-text-tertiary;
+ font-weight: $semantic-typography-font-weight-normal;
+ flex-shrink: 0;
+}
+
+.search-bar__helper-text {
+ margin-top: $semantic-spacing-component-xs;
+ padding-left: $semantic-spacing-component-md;
+ font-size: $semantic-typography-font-size-xs;
+ color: $semantic-color-text-secondary;
+ line-height: $semantic-typography-line-height-tight;
+}
+
+.search-bar__error-text {
+ margin-top: $semantic-spacing-component-xs;
+ padding-left: $semantic-spacing-component-md;
+ font-size: $semantic-typography-font-size-xs;
+ color: $semantic-color-danger;
+ line-height: $semantic-typography-line-height-tight;
+ display: flex;
+ align-items: center;
+ gap: $semantic-spacing-component-xs;
+}
+
+// ==========================================================================
+// RESPONSIVE DESIGN
+// ==========================================================================
+
+@media (max-width: $semantic-breakpoint-md - 1) {
+ .search-bar-wrapper {
+ // Larger touch targets on mobile
+ &.search-bar-wrapper--sm {
+ .search-bar {
+ height: 44px;
+ }
+
+ .search-bar__icon-button--sm {
+ width: 36px;
+ height: 36px;
+ }
+ }
+
+ &.search-bar-wrapper--md {
+ .search-bar {
+ height: 52px;
+ }
+
+ .search-bar__icon-button--md {
+ width: 40px;
+ height: 40px;
+ }
+ }
+ }
+
+ .search-bar__suggestions {
+ max-height: 250px; // Smaller on mobile
+ }
+}
+
+// ==========================================================================
+// ACCESSIBILITY ENHANCEMENTS
+// ==========================================================================
+
+// Reduced motion preference
+@media (prefers-reduced-motion: reduce) {
+ .search-bar,
+ .search-bar__label,
+ .search-bar__input,
+ .search-bar__icon-button,
+ .search-bar__suggestion {
+ transition: none !important;
+ animation: none !important;
+ }
+}
+
+// High contrast mode
+@media (prefers-contrast: high) {
+ .search-bar-wrapper {
+ &.search-bar-wrapper--outlined,
+ &.search-bar-wrapper--filled {
+ .search-bar {
+ border-width: $semantic-border-width-2;
+ }
+ }
+ }
+
+ .search-bar__suggestions {
+ border-width: $semantic-border-width-2;
+ }
+}
+
+// Focus visible for keyboard navigation
+.search-bar__icon-button:focus-visible {
+ outline: $semantic-border-width-2 solid $semantic-color-brand-primary;
+ outline-offset: 2px;
+}
+
+.search-bar__suggestion:focus-visible {
+ outline: $semantic-border-width-2 solid $semantic-color-brand-primary;
+ outline-offset: -2px;
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/search/search-bar.component.ts b/projects/ui-essentials/src/lib/components/forms/search/search-bar.component.ts
new file mode 100644
index 0000000..fe4e289
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/search/search-bar.component.ts
@@ -0,0 +1,397 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, forwardRef, signal, ElementRef, ViewChild } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule, ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
+import { faSearch, faTimes, faMicrophone, faCamera, faFilter } from '@fortawesome/free-solid-svg-icons';
+
+export type SearchBarSize = 'sm' | 'md' | 'lg';
+export type SearchBarVariant = 'filled' | 'outlined' | 'elevated';
+export type SearchBarState = 'default' | 'error' | 'disabled';
+
+export interface SearchSuggestion {
+ id: string;
+ text: string;
+ category?: string;
+ icon?: IconDefinition;
+}
+
+@Component({
+ selector: 'ui-search-bar',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [CommonModule, FormsModule, FontAwesomeModule],
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => SearchBarComponent),
+ multi: true
+ }
+ ],
+ template: `
+
+
+
+ @if (leadingIcon) {
+
+
+
+ }
+
+
+ @if (label && size !== 'sm') {
+
+ {{ label }}
+ @if (required) {
+ *
+ }
+
+ }
+
+ 0 ? 'list' : 'none'"
+ [attr.aria-controls]="suggestions.length > 0 ? searchId + '-suggestions' : null"
+ (input)="onInput($event)"
+ (focus)="onFocus($event)"
+ (blur)="onBlur($event)"
+ (keydown)="onKeyDown($event)"
+ />
+
+
+
+ @if (searchValue() && clearable) {
+
+
+
+ }
+
+ @for (icon of trailingIcons; track icon.id) {
+
+
+
+ }
+
+
+
+ @if (suggestions.length > 0 && showSuggestions()) {
+
+ @for (suggestion of suggestions; track suggestion.id; let i = $index) {
+
+ @if (suggestion.icon) {
+
+ }
+ {{ suggestion.text }}
+ @if (suggestion.category) {
+ {{ suggestion.category }}
+ }
+
+ }
+
+ }
+
+ @if (helperText && state !== 'error') {
+
{{ helperText }}
+ }
+
+ @if (errorMessage && state === 'error') {
+
{{ errorMessage }}
+ }
+
+ `,
+ styleUrls: ['./search-bar.component.scss']
+})
+export class SearchBarComponent implements ControlValueAccessor {
+ @ViewChild('searchInput', { static: true }) searchInput!: ElementRef;
+
+ // Core inputs
+ @Input() placeholder: string = 'Search...';
+ @Input() label: string = '';
+ @Input() size: SearchBarSize = 'md';
+ @Input() variant: SearchBarVariant = 'outlined';
+ @Input() state: SearchBarState = 'default';
+
+ // Icon inputs
+ @Input() leadingIcon: IconDefinition = faSearch;
+ @Input() leadingIconLabel: string = '';
+ @Input() trailingIcons: Array<{
+ id: string;
+ icon: IconDefinition;
+ label: string;
+ disabled?: boolean;
+ }> = [];
+
+ // Behavior inputs
+ @Input() disabled = false;
+ @Input() readonly = false;
+ @Input() required = false;
+ @Input() clearable = true;
+ @Input() autofocus = false;
+
+ // Suggestions
+ @Input() suggestions: SearchSuggestion[] = [];
+ @Input() maxSuggestions = 10;
+
+ // Accessibility
+ @Input() ariaLabel: string = '';
+ @Input() helperText: string = '';
+ @Input() errorMessage: string = '';
+ @Input() searchId: string = `search-${Math.random().toString(36).substr(2, 9)}`;
+
+ // Outputs
+ @Output() searchChange = new EventEmitter();
+ @Output() searchSubmit = new EventEmitter();
+ @Output() searchFocus = new EventEmitter();
+ @Output() searchBlur = new EventEmitter();
+ @Output() suggestionSelect = new EventEmitter();
+ @Output() trailingIconClick = new EventEmitter<{id: string, icon: IconDefinition}>();
+
+ // Font Awesome icons
+ protected readonly faSearch = faSearch;
+ protected readonly faTimes = faTimes;
+ protected readonly faMicrophone = faMicrophone;
+ protected readonly faCamera = faCamera;
+ protected readonly faFilter = faFilter;
+
+ // Internal state
+ protected searchValue = signal('');
+ protected isFocused = signal(false);
+ protected showSuggestions = signal(false);
+ protected selectedSuggestionIndex = signal(-1);
+
+ // ControlValueAccessor implementation
+ private onChange = (value: string) => {};
+ private onTouched = () => {};
+
+ ngAfterViewInit(): void {
+ if (this.autofocus) {
+ this.searchInput.nativeElement.focus();
+ }
+ }
+
+ writeValue(value: string): void {
+ this.searchValue.set(value || '');
+ }
+
+ registerOnChange(fn: (value: string) => void): void {
+ this.onChange = fn;
+ }
+
+ registerOnTouched(fn: () => void): void {
+ this.onTouched = fn;
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ }
+
+ // Event handlers
+ onInput(event: Event): void {
+ const target = event.target as HTMLInputElement;
+ const value = target.value;
+
+ this.searchValue.set(value);
+ this.onChange(value);
+ this.searchChange.emit(value);
+
+ // Show/hide suggestions based on input
+ this.showSuggestions.set(value.length > 0 && this.suggestions.length > 0);
+ this.selectedSuggestionIndex.set(-1);
+ }
+
+ onFocus(event: FocusEvent): void {
+ this.isFocused.set(true);
+ this.showSuggestions.set(this.searchValue().length > 0 && this.suggestions.length > 0);
+ this.searchFocus.emit(event);
+ }
+
+ onBlur(event: FocusEvent): void {
+ this.isFocused.set(false);
+ this.onTouched();
+
+ // Delay hiding suggestions to allow for suggestion clicks
+ setTimeout(() => {
+ this.showSuggestions.set(false);
+ this.selectedSuggestionIndex.set(-1);
+ }, 150);
+
+ this.searchBlur.emit(event);
+ }
+
+ onKeyDown(event: KeyboardEvent): void {
+ const suggestions = this.suggestions.slice(0, this.maxSuggestions);
+
+ switch (event.key) {
+ case 'ArrowDown':
+ event.preventDefault();
+ if (this.showSuggestions()) {
+ const nextIndex = this.selectedSuggestionIndex() + 1;
+ this.selectedSuggestionIndex.set(nextIndex >= suggestions.length ? 0 : nextIndex);
+ }
+ break;
+
+ case 'ArrowUp':
+ event.preventDefault();
+ if (this.showSuggestions()) {
+ const prevIndex = this.selectedSuggestionIndex() - 1;
+ this.selectedSuggestionIndex.set(prevIndex < 0 ? suggestions.length - 1 : prevIndex);
+ }
+ break;
+
+ case 'Enter':
+ event.preventDefault();
+ if (this.showSuggestions() && this.selectedSuggestionIndex() >= 0) {
+ this.selectSuggestion(suggestions[this.selectedSuggestionIndex()]);
+ } else {
+ this.submitSearch();
+ }
+ break;
+
+ case 'Escape':
+ this.showSuggestions.set(false);
+ this.selectedSuggestionIndex.set(-1);
+ this.searchInput.nativeElement.blur();
+ break;
+ }
+ }
+
+ clearSearch(): void {
+ this.searchValue.set('');
+ this.onChange('');
+ this.searchChange.emit('');
+ this.showSuggestions.set(false);
+ this.searchInput.nativeElement.focus();
+ }
+
+ selectSuggestion(suggestion: SearchSuggestion): void {
+ this.searchValue.set(suggestion.text);
+ this.onChange(suggestion.text);
+ this.searchChange.emit(suggestion.text);
+ this.showSuggestions.set(false);
+ this.selectedSuggestionIndex.set(-1);
+ this.suggestionSelect.emit(suggestion);
+ }
+
+ setSelectedSuggestionIndex(index: number): void {
+ this.selectedSuggestionIndex.set(index);
+ }
+
+ submitSearch(): void {
+ this.searchSubmit.emit(this.searchValue());
+ this.showSuggestions.set(false);
+ }
+
+ onTrailingIconClick(iconData: {id: string, icon: IconDefinition, label: string, disabled?: boolean}): void {
+ if (!iconData.disabled) {
+ this.trailingIconClick.emit({id: iconData.id, icon: iconData.icon});
+ }
+ }
+
+ hasValue(): boolean {
+ return this.searchValue().length > 0;
+ }
+
+ // CSS class getters
+ getWrapperClasses(): string {
+ const classes = [
+ `search-bar-wrapper--${this.size}`,
+ `search-bar-wrapper--${this.variant}`,
+ `search-bar-wrapper--${this.state}`
+ ];
+
+ if (this.disabled) classes.push('search-bar-wrapper--disabled');
+ if (this.readonly) classes.push('search-bar-wrapper--readonly');
+ if (this.isFocused()) classes.push('search-bar-wrapper--focused');
+ if (this.hasValue()) classes.push('search-bar-wrapper--has-value');
+
+ return classes.join(' ');
+ }
+
+ getSearchBarClasses(): string {
+ const classes = [
+ `search-bar--${this.size}`,
+ `search-bar--${this.variant}`,
+ `search-bar--${this.state}`
+ ];
+
+ if (this.disabled) classes.push('search-bar--disabled');
+ if (this.readonly) classes.push('search-bar--readonly');
+ if (this.isFocused()) classes.push('search-bar--focused');
+
+ return classes.join(' ');
+ }
+
+ getLeadingIconClasses(): string {
+ return `search-bar__leading-icon--${this.size}`;
+ }
+
+ getInputWrapperClasses(): string {
+ return `search-bar__input-wrapper--${this.size}`;
+ }
+
+ getInputClasses(): string {
+ const classes = [`search-bar__input--${this.size}`];
+ if (this.hasValue()) classes.push('search-bar__input--has-value');
+ return classes.join(' ');
+ }
+
+ getTrailingIconsClasses(): string {
+ return `search-bar__trailing-icons--${this.size}`;
+ }
+
+ getIconButtonClasses(): string {
+ return `search-bar__icon-button--${this.size}`;
+ }
+
+ getSuggestionsClasses(): string {
+ return `search-bar__suggestions--${this.size}`;
+ }
+
+ getSuggestionClasses(index: number): string {
+ const classes = [`search-bar__suggestion--${this.size}`];
+ if (this.selectedSuggestionIndex() === index) {
+ classes.push('search-bar__suggestion--selected');
+ }
+ return classes.join(' ');
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/select.component.scss b/projects/ui-essentials/src/lib/components/forms/select.component.scss
new file mode 100644
index 0000000..e155e44
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/select.component.scss
@@ -0,0 +1,205 @@
+@use "../../../../../shared-ui/src/styles/semantic/index" as *;
+
+// Tokens available globally via main application styles
+
+.ui-select {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-micro-tight; // 0.25rem
+
+ &--full-width {
+ width: 100%;
+ }
+}
+
+.select-label {
+ font-family: $semantic-typography-font-family-sans;
+ font-weight: $semantic-typography-font-weight-medium;
+ color: $semantic-color-text-primary;
+ font-size: $semantic-typography-font-size-sm;
+ line-height: $semantic-typography-line-height-normal;
+ margin-bottom: $semantic-spacing-micro-tight; // 0.25rem
+}
+
+.select-required {
+ color: $semantic-color-danger;
+ margin-left: $semantic-spacing-micro-tight; // 0.125rem
+}
+
+.select-container {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-micro-tight; // 0.25rem
+}
+
+.select-field {
+ appearance: none;
+ font-family: $semantic-typography-font-family-sans;
+ font-weight: $semantic-typography-font-weight-normal;
+ background-color: $semantic-color-surface-primary;
+ border-radius: $semantic-border-radius-md;
+ transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
+ outline: none;
+ cursor: pointer;
+ padding-right: $semantic-spacing-10; // 2.5rem - space for arrow
+
+ // Size variants
+ .ui-select--small & {
+ height: $semantic-spacing-8; // 2rem
+ padding: 0 $semantic-spacing-3; // 0.75rem
+ padding-right: $semantic-spacing-8; // 2rem
+ font-size: $semantic-typography-font-size-sm;
+ }
+
+ .ui-select--medium & {
+ height: $semantic-spacing-micro-tight; // 2.5rem
+ padding: 0 $semantic-spacing-3; // 0.75rem
+ padding-right: $semantic-spacing-micro-tight; // 2.5rem
+ font-size: $semantic-typography-font-size-md;
+ }
+
+ .ui-select--large & {
+ height: $semantic-spacing-12; // 3rem
+ padding: 0 $semantic-spacing-4; // 1rem
+ padding-right: $semantic-spacing-12; // 3rem
+ font-size: $semantic-typography-font-size-lg;
+ }
+
+ // Variant styles - Outlined
+ .ui-select--outlined & {
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ background-color: transparent;
+
+ &:hover:not(:disabled) {
+ border-color: $semantic-color-border-focus;
+ }
+
+ &:focus {
+ border-color: $semantic-color-interactive-primary;
+ box-shadow: 0 0 0 1px $semantic-color-interactive-primary;
+ }
+ }
+
+ // Variant styles - Filled
+ .ui-select--filled & {
+ border: none;
+ background-color: $semantic-color-surface-elevated;
+
+ &:hover:not(:disabled) {
+ background-color: $semantic-color-surface-interactive;
+ }
+
+ &:focus {
+ background-color: $semantic-color-surface-primary;
+ box-shadow: inset 0 -2px 0 $semantic-color-interactive-primary;
+ }
+ }
+
+ // States
+ &:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ background-color: $semantic-color-surface-disabled;
+ color: $semantic-color-text-disabled;
+ }
+
+ // Option styling
+ option {
+ background-color: $semantic-color-surface-primary;
+ color: $semantic-color-text-primary;
+ padding: $semantic-spacing-2; // 0.5rem
+
+ &:disabled {
+ color: $semantic-color-text-disabled;
+ }
+ }
+
+ // Error state
+ &--error {
+ .ui-select--outlined & {
+ border-color: $semantic-color-danger;
+
+ &:focus {
+ border-color: $semantic-color-danger;
+ box-shadow: 0 0 0 1px $semantic-color-danger;
+ }
+ }
+
+ .ui-select--filled & {
+ background-color: rgba($semantic-color-danger, 0.08);
+
+ &:focus {
+ box-shadow: inset 0 -2px 0 $semantic-color-danger;
+ }
+ }
+ }
+}
+
+// Custom arrow icon
+.select-arrow {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ pointer-events: none;
+ color: $semantic-color-text-secondary;
+ transition: transform 150ms ease;
+
+ .ui-select--small & {
+ right: $semantic-spacing-2; // 0.5rem
+
+ svg {
+ width: 10px;
+ height: 6px;
+ }
+ }
+
+ .ui-select--medium & {
+ right: $semantic-spacing-3; // 0.75rem
+
+ svg {
+ width: 12px;
+ height: 8px;
+ }
+ }
+
+ .ui-select--large & {
+ right: $semantic-spacing-4; // 1rem
+
+ svg {
+ width: 14px;
+ height: 10px;
+ }
+ }
+}
+
+.select-field:focus + .select-arrow {
+ color: $semantic-color-interactive-primary;
+ transform: translateY(-50%) rotate(180deg);
+}
+
+.select-field:disabled + .select-arrow {
+ color: $semantic-color-text-disabled;
+}
+
+// Helper and error text
+.select-helper-text {
+ font-family: $semantic-typography-font-family-sans;
+ font-size: $semantic-typography-font-size-xs;
+ color: $semantic-color-text-secondary;
+ line-height: $semantic-typography-line-height-normal;
+}
+
+.select-error-text {
+ font-family: $semantic-typography-font-family-sans;
+ font-size: $semantic-typography-font-size-xs;
+ color: $semantic-color-danger;
+ line-height: $semantic-typography-line-height-normal;
+}
+
+// Disabled state for wrapper
+.ui-select--disabled {
+ .select-label {
+ color: $semantic-color-text-disabled;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/select.component.ts b/projects/ui-essentials/src/lib/components/forms/select.component.ts
new file mode 100644
index 0000000..a9159ea
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/select.component.ts
@@ -0,0 +1,151 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, forwardRef } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+
+export type SelectSize = 'small' | 'medium' | 'large';
+export type SelectVariant = 'outlined' | 'filled';
+
+export interface SelectOption {
+ value: any;
+ label: string;
+ disabled?: boolean;
+}
+
+@Component({
+ selector: 'ui-select',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => SelectComponent),
+ multi: true
+ }
+ ],
+ template: `
+
+ @if (label) {
+
+ {{ label }}
+ @if (required) {
+ *
+ }
+
+ }
+
+
+
+ @if (placeholder) {
+ {{ placeholder }}
+ }
+
+ @for (option of options; track option.value) {
+
+ {{ option.label }}
+
+ }
+
+
+
+
+ @if (helperText && !errorText) {
+
{{ helperText }}
+ }
+
+ @if (errorText) {
+
{{ errorText }}
+ }
+
+
+ `,
+ styleUrl: './select.component.scss'
+})
+export class SelectComponent implements ControlValueAccessor {
+ @Input() label: string = '';
+ @Input() placeholder: string = '';
+ @Input() helperText: string = '';
+ @Input() errorText: string = '';
+ @Input() size: SelectSize = 'medium';
+ @Input() variant: SelectVariant = 'outlined';
+ @Input() disabled: boolean = false;
+ @Input() required: boolean = false;
+ @Input() fullWidth: boolean = false;
+ @Input() options: SelectOption[] = [];
+
+ @Output() valueChange = new EventEmitter();
+ @Output() focused = new EventEmitter();
+ @Output() blurred = new EventEmitter();
+
+ value: any = '';
+ selectId = `ui-select-${Math.random().toString(36).substr(2, 9)}`;
+
+ // ControlValueAccessor implementation
+ private onChange = (value: any) => {};
+ private onTouched = () => {};
+
+ writeValue(value: any): void {
+ this.value = value;
+ }
+
+ registerOnChange(fn: (value: any) => void): void {
+ this.onChange = fn;
+ }
+
+ registerOnTouched(fn: () => void): void {
+ this.onTouched = fn;
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ }
+
+ get wrapperClasses(): string {
+ return [
+ 'ui-select',
+ `ui-select--${this.size}`,
+ `ui-select--${this.variant}`,
+ this.disabled ? 'ui-select--disabled' : '',
+ this.errorText ? 'ui-select--error' : '',
+ this.fullWidth ? 'ui-select--full-width' : ''
+ ].filter(Boolean).join(' ');
+ }
+
+ get selectClasses(): string {
+ return [
+ 'select-field',
+ this.errorText ? 'select-field--error' : ''
+ ].filter(Boolean).join(' ');
+ }
+
+ onSelectionChange(event: Event): void {
+ const target = event.target as HTMLSelectElement;
+ this.value = target.value;
+ this.onChange(this.value);
+ this.valueChange.emit(this.value);
+ }
+
+ onFocus(): void {
+ this.focused.emit();
+ }
+
+ onBlur(): void {
+ this.onTouched();
+ this.blurred.emit();
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/switch/index.ts b/projects/ui-essentials/src/lib/components/forms/switch/index.ts
new file mode 100644
index 0000000..4244256
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/switch/index.ts
@@ -0,0 +1 @@
+export * from './switch.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/switch/switch.component.scss b/projects/ui-essentials/src/lib/components/forms/switch/switch.component.scss
new file mode 100644
index 0000000..4dcc84e
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/switch/switch.component.scss
@@ -0,0 +1,364 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+// Tokens available globally via main application styles
+
+// ==========================================================================
+// SWITCH COMPONENT STYLES
+// ==========================================================================
+// Modern switch implementation using design tokens
+// Follows Material Design 3 principles with semantic token system
+// ==========================================================================
+
+.switch-wrapper {
+ display: inline-flex;
+ flex-direction: column;
+ align-items: flex-start;
+
+ // Size variants
+ &.switch-wrapper--sm {
+ gap: $semantic-spacing-component-xs;
+ }
+
+ &.switch-wrapper--md {
+ gap: $semantic-spacing-component-sm;
+ }
+
+ &.switch-wrapper--lg {
+ gap: $semantic-spacing-component-md;
+ }
+
+ // State variants
+ &.switch-wrapper--disabled {
+ opacity: 0.6;
+ pointer-events: none;
+ }
+
+ &.switch-wrapper--focused {
+ .switch__track {
+ box-shadow: $semantic-shadow-button-focus;
+ }
+ }
+}
+
+.switch {
+ display: inline-flex;
+ align-items: center;
+ cursor: pointer;
+ user-select: none;
+ position: relative;
+
+ // Size-specific gaps
+ &.switch--sm {
+ gap: $semantic-spacing-component-sm;
+ }
+
+ &.switch--md {
+ gap: $semantic-spacing-component-md;
+ }
+
+ &.switch--lg {
+ gap: $semantic-spacing-component-lg;
+ }
+
+ &.switch--disabled {
+ cursor: not-allowed;
+ }
+}
+
+.switch__input {
+ position: absolute;
+ opacity: 0;
+ width: 0;
+ height: 0;
+ pointer-events: none;
+
+ // Focus visible for keyboard navigation
+ &:focus-visible + .switch__track {
+ outline: $semantic-border-width-2 solid $semantic-color-brand-primary;
+ outline-offset: 2px;
+ }
+}
+
+.switch__track {
+ position: relative;
+ border-radius: $semantic-border-radius-full;
+ transition: all $semantic-duration-fast $semantic-easing-standard;
+ cursor: pointer;
+
+ // Size variants
+ &.switch__track--sm {
+ width: 32px;
+ height: 18px;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ background-color: $semantic-color-surface-secondary;
+ }
+
+ &.switch__track--md {
+ width: 44px;
+ height: 24px;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ background-color: $semantic-color-surface-secondary;
+ }
+
+ &.switch__track--lg {
+ width: 56px;
+ height: 32px;
+ border: $semantic-border-width-2 solid $semantic-color-border-primary;
+ background-color: $semantic-color-surface-secondary;
+ }
+
+ // Checked state - variant colors
+ &.switch__track--checked {
+ border-color: $semantic-color-brand-primary;
+ background-color: $semantic-color-brand-primary;
+
+ .switch-wrapper--primary & {
+ border-color: $semantic-color-brand-primary;
+ background-color: $semantic-color-brand-primary;
+ }
+
+ .switch-wrapper--secondary & {
+ border-color: $semantic-color-text-secondary;
+ background-color: $semantic-color-text-secondary;
+ }
+
+ .switch-wrapper--success & {
+ border-color: $semantic-color-success;
+ background-color: $semantic-color-success;
+ }
+
+ .switch-wrapper--warning & {
+ border-color: $semantic-color-warning;
+ background-color: $semantic-color-warning;
+ }
+
+ .switch-wrapper--danger & {
+ border-color: $semantic-color-danger;
+ background-color: $semantic-color-danger;
+ }
+ }
+
+ // Hover states
+ &:hover:not(.switch__track--disabled) {
+ border-color: $semantic-color-border-focus;
+
+ &.switch__track--checked {
+ .switch-wrapper--primary & {
+ background-color: $semantic-color-brand-secondary;
+ border-color: $semantic-color-brand-secondary;
+ }
+
+ .switch-wrapper--secondary & {
+ opacity: 0.9;
+ }
+
+ .switch-wrapper--success & {
+ opacity: 0.9;
+ }
+
+ .switch-wrapper--warning & {
+ opacity: 0.9;
+ }
+
+ .switch-wrapper--danger & {
+ opacity: 0.9;
+ }
+ }
+ }
+
+ // Disabled state
+ &.switch__track--disabled {
+ background-color: $semantic-color-surface-disabled;
+ border-color: $semantic-color-border-disabled;
+ cursor: not-allowed;
+ opacity: 0.5;
+ }
+}
+
+.switch__thumb {
+ position: absolute;
+ top: 50%;
+ border-radius: $semantic-border-radius-full;
+ background-color: $semantic-color-surface-primary;
+ box-shadow: $semantic-shadow-elevation-1;
+ transition: all $semantic-duration-fast $semantic-easing-standard;
+ transform: translateY(-50%);
+
+ // Size variants - unchecked position
+ &.switch__thumb--sm {
+ width: 14px;
+ height: 14px;
+ left: 2px;
+ }
+
+ &.switch__thumb--md {
+ width: 20px;
+ height: 20px;
+ left: 2px;
+ }
+
+ &.switch__thumb--lg {
+ width: 26px;
+ height: 26px;
+ left: 3px;
+ }
+
+ // Checked position
+ &.switch__thumb--checked {
+ &.switch__thumb--sm {
+ left: 16px; // 32px - 14px - 2px
+ }
+
+ &.switch__thumb--md {
+ left: 22px; // 44px - 20px - 2px
+ }
+
+ &.switch__thumb--lg {
+ left: 27px; // 56px - 26px - 3px
+ }
+ }
+
+ // Hover enhancement
+ .switch__track:hover &:not(.switch__thumb--disabled) {
+ box-shadow: $semantic-shadow-elevation-2;
+
+ &.switch__thumb--checked {
+ box-shadow: $semantic-shadow-elevation-3;
+ }
+ }
+
+ // Disabled state
+ &.switch__thumb--disabled {
+ background-color: $semantic-color-surface-disabled;
+ box-shadow: none;
+ }
+}
+
+.switch__label {
+ color: $semantic-color-text-primary;
+ font-weight: $semantic-typography-font-weight-medium;
+ line-height: $semantic-typography-line-height-tight;
+
+ // Size variants
+ &.switch__label--sm {
+ font-size: $semantic-typography-font-size-sm;
+ }
+
+ &.switch__label--md {
+ font-size: $semantic-typography-font-size-md;
+ }
+
+ &.switch__label--lg {
+ font-size: $semantic-typography-font-size-lg;
+ }
+
+ // Disabled state
+ &.switch__label--disabled {
+ color: $semantic-color-text-disabled;
+ }
+
+ .switch__required-indicator {
+ color: $semantic-color-danger;
+ margin-left: 2px;
+ }
+}
+
+.switch__helper-text {
+ font-size: $semantic-typography-font-size-xs;
+ color: $semantic-color-text-secondary;
+ line-height: $semantic-typography-line-height-tight;
+ margin-top: $semantic-spacing-component-xs;
+
+ .switch-wrapper--sm & {
+ margin-left: 40px; // Align with switch track
+ }
+
+ .switch-wrapper--md & {
+ margin-left: 52px; // Align with switch track
+ }
+
+ .switch-wrapper--lg & {
+ margin-left: 64px; // Align with switch track
+ }
+}
+
+// ==========================================================================
+// RESPONSIVE DESIGN
+// ==========================================================================
+
+@media (max-width: $semantic-breakpoint-md - 1) {
+ .switch-wrapper {
+ // Larger touch targets on mobile
+ &.switch-wrapper--sm {
+ .switch__track--sm {
+ width: 36px;
+ height: 20px;
+ }
+
+ .switch__thumb--sm {
+ width: 16px;
+ height: 16px;
+
+ &.switch__thumb--checked {
+ left: 18px; // 36px - 16px - 2px
+ }
+ }
+ }
+
+ &.switch-wrapper--md {
+ .switch__track--md {
+ width: 48px;
+ height: 26px;
+ }
+
+ .switch__thumb--md {
+ width: 22px;
+ height: 22px;
+
+ &.switch__thumb--checked {
+ left: 24px; // 48px - 22px - 2px
+ }
+ }
+ }
+ }
+}
+
+// ==========================================================================
+// ACCESSIBILITY ENHANCEMENTS
+// ==========================================================================
+
+// Reduced motion preference
+@media (prefers-reduced-motion: reduce) {
+ .switch__track,
+ .switch__thumb {
+ transition: none !important;
+ }
+}
+
+// High contrast mode
+@media (prefers-contrast: high) {
+ .switch__track {
+ border-width: $semantic-border-width-2;
+
+ &.switch__track--checked {
+ border-width: $semantic-border-width-2;
+ }
+ }
+
+ .switch__thumb {
+ box-shadow: 0 0 0 1px $semantic-color-text-primary;
+ }
+}
+
+// Focus visible for keyboard navigation
+.switch__input:focus-visible + .switch__track {
+ outline: $semantic-border-width-2 solid $semantic-color-brand-primary;
+ outline-offset: 2px;
+}
+
+// Active state for better touch feedback
+.switch:active:not(.switch--disabled) {
+ .switch__thumb:not(.switch__thumb--disabled) {
+ transform: translateY(-50%) scale(1.1);
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/switch/switch.component.ts b/projects/ui-essentials/src/lib/components/forms/switch/switch.component.ts
new file mode 100644
index 0000000..f4cbbea
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/switch/switch.component.ts
@@ -0,0 +1,205 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, forwardRef, signal, ViewChild, ElementRef } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule, ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+
+export type SwitchSize = 'sm' | 'md' | 'lg';
+export type SwitchVariant = 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
+export type SwitchState = 'default' | 'disabled';
+
+@Component({
+ selector: 'ui-switch',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [CommonModule, FormsModule],
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => SwitchComponent),
+ multi: true
+ }
+ ],
+ template: `
+
+
+
+
+
+
+ @if (label) {
+
+ {{ label }}
+ @if (required) {
+ *
+ }
+
+ }
+
+
+ @if (helperText && state !== 'disabled') {
+
{{ helperText }}
+ }
+
+ `,
+ styleUrls: ['./switch.component.scss']
+})
+export class SwitchComponent implements ControlValueAccessor {
+ @ViewChild('switchInput', { static: true }) switchInput!: ElementRef;
+
+ // Core inputs
+ @Input() label: string = '';
+ @Input() size: SwitchSize = 'md';
+ @Input() variant: SwitchVariant = 'primary';
+ @Input() state: SwitchState = 'default';
+
+ // Behavior inputs
+ @Input() disabled = false;
+ @Input() required = false;
+ @Input() autofocus = false;
+
+ // Accessibility
+ @Input() ariaLabel: string = '';
+ @Input() helperText: string = '';
+ @Input() switchId: string = `switch-${Math.random().toString(36).substr(2, 9)}`;
+
+ // Outputs
+ @Output() switchChange = new EventEmitter();
+ @Output() switchFocus = new EventEmitter();
+ @Output() switchBlur = new EventEmitter();
+
+ // Internal state
+ protected isChecked = signal(false);
+ protected isFocused = signal(false);
+
+ // ControlValueAccessor implementation
+ private onChange = (value: boolean) => {};
+ private onTouched = () => {};
+
+ ngAfterViewInit(): void {
+ if (this.autofocus) {
+ this.switchInput.nativeElement.focus();
+ }
+ }
+
+ writeValue(value: boolean): void {
+ this.isChecked.set(!!value);
+ }
+
+ registerOnChange(fn: (value: boolean) => void): void {
+ this.onChange = fn;
+ }
+
+ registerOnTouched(fn: () => void): void {
+ this.onTouched = fn;
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ }
+
+ // Event handlers
+ onToggle(event: Event): void {
+ const target = event.target as HTMLInputElement;
+ const checked = target.checked;
+
+ this.isChecked.set(checked);
+ this.onChange(checked);
+ this.switchChange.emit(checked);
+ }
+
+ onFocus(event: FocusEvent): void {
+ this.isFocused.set(true);
+ this.switchFocus.emit(event);
+ }
+
+ onBlur(event: FocusEvent): void {
+ this.isFocused.set(false);
+ this.onTouched();
+ this.switchBlur.emit(event);
+ }
+
+ onKeyDown(event: KeyboardEvent): void {
+ // Space or Enter can toggle the switch
+ if (event.key === ' ' || event.key === 'Enter') {
+ event.preventDefault();
+ if (!this.disabled) {
+ const newValue = !this.isChecked();
+ this.isChecked.set(newValue);
+ this.onChange(newValue);
+ this.switchChange.emit(newValue);
+
+ // Update the actual input element
+ this.switchInput.nativeElement.checked = newValue;
+ }
+ }
+ }
+
+ // CSS class getters
+ getWrapperClasses(): string {
+ const classes = [
+ `switch-wrapper--${this.size}`,
+ `switch-wrapper--${this.variant}`,
+ `switch-wrapper--${this.state}`
+ ];
+
+ if (this.disabled) classes.push('switch-wrapper--disabled');
+ if (this.isChecked()) classes.push('switch-wrapper--checked');
+ if (this.isFocused()) classes.push('switch-wrapper--focused');
+
+ return classes.join(' ');
+ }
+
+ getSwitchClasses(): string {
+ const classes = [
+ `switch--${this.size}`,
+ `switch--${this.variant}`,
+ `switch--${this.state}`
+ ];
+
+ if (this.disabled) classes.push('switch--disabled');
+ if (this.isChecked()) classes.push('switch--checked');
+ if (this.isFocused()) classes.push('switch--focused');
+
+ return classes.join(' ');
+ }
+
+ getTrackClasses(): string {
+ const classes = [`switch__track--${this.size}`];
+
+ if (this.isChecked()) classes.push('switch__track--checked');
+ if (this.disabled) classes.push('switch__track--disabled');
+
+ return classes.join(' ');
+ }
+
+ getThumbClasses(): string {
+ const classes = [`switch__thumb--${this.size}`];
+
+ if (this.isChecked()) classes.push('switch__thumb--checked');
+ if (this.disabled) classes.push('switch__thumb--disabled');
+
+ return classes.join(' ');
+ }
+
+ getLabelClasses(): string {
+ const classes = [`switch__label--${this.size}`];
+
+ if (this.disabled) classes.push('switch__label--disabled');
+
+ return classes.join(' ');
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/time-picker/index.ts b/projects/ui-essentials/src/lib/components/forms/time-picker/index.ts
new file mode 100644
index 0000000..20af417
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/time-picker/index.ts
@@ -0,0 +1 @@
+export * from './time-picker.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/time-picker/time-picker.component.scss b/projects/ui-essentials/src/lib/components/forms/time-picker/time-picker.component.scss
new file mode 100644
index 0000000..74ef8e4
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/time-picker/time-picker.component.scss
@@ -0,0 +1,540 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+.ui-time-picker {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ position: relative;
+
+ // ==========================================================================
+ // SIZE VARIANTS
+ // ==========================================================================
+
+ &--sm {
+ .ui-time-picker__container {
+ min-height: $semantic-sizing-input-height-sm;
+ }
+
+ .ui-time-picker__field {
+ font-size: 0.875rem;
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ }
+
+ .ui-time-picker__label {
+ font-size: $semantic-typography-font-size-sm;
+ margin-bottom: $semantic-spacing-component-xs;
+ }
+ }
+
+ &--md {
+ .ui-time-picker__container {
+ min-height: $semantic-sizing-input-height-md;
+ }
+
+ .ui-time-picker__field {
+ font-size: 1rem;
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
+ }
+
+ .ui-time-picker__label {
+ font-size: $semantic-typography-font-size-md;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+ }
+
+ &--lg {
+ .ui-time-picker__container {
+ min-height: $semantic-sizing-input-height-lg;
+ }
+
+ .ui-time-picker__field {
+ font-size: 1.125rem;
+ padding: $semantic-spacing-component-md;
+ }
+
+ .ui-time-picker__label {
+ font-size: $semantic-typography-font-size-lg;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+ }
+
+ // ==========================================================================
+ // VARIANT STYLES
+ // ==========================================================================
+
+ &--outlined {
+ .ui-time-picker__container {
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-md;
+ background: $semantic-color-surface-primary;
+ }
+
+ .ui-time-picker__field {
+ border: none;
+ background: transparent;
+ }
+ }
+
+ &--filled {
+ .ui-time-picker__container {
+ background: $semantic-color-surface-secondary;
+ border: $semantic-border-width-1 solid transparent;
+ border-radius: $semantic-border-radius-md;
+ border-bottom: $semantic-border-width-3 solid $semantic-color-border-primary;
+ }
+
+ .ui-time-picker__field {
+ border: none;
+ background: transparent;
+ }
+ }
+
+ &--underlined {
+ .ui-time-picker__container {
+ background: transparent;
+ border: none;
+ border-bottom: $semantic-border-width-1 solid $semantic-color-border-primary;
+ border-radius: 0;
+ }
+
+ .ui-time-picker__field {
+ border: none;
+ background: transparent;
+ }
+ }
+
+ // ==========================================================================
+ // STATE VARIANTS
+ // ==========================================================================
+
+ &--error {
+ .ui-time-picker__container {
+ border-color: $semantic-color-error;
+ }
+
+ .ui-time-picker__label {
+ color: $semantic-color-error;
+ }
+ }
+
+ &--success {
+ .ui-time-picker__container {
+ border-color: $semantic-color-success;
+ }
+ }
+
+ &--warning {
+ .ui-time-picker__container {
+ border-color: $semantic-color-warning;
+ }
+ }
+
+ &--disabled {
+ opacity: 0.6;
+ pointer-events: none;
+
+ .ui-time-picker__container {
+ background: $semantic-color-surface-secondary;
+ cursor: not-allowed;
+ }
+ }
+
+ &--open {
+ .ui-time-picker__container {
+ border-color: $semantic-color-primary;
+ box-shadow: $semantic-shadow-input-focus;
+ }
+ }
+
+ // ==========================================================================
+ // COMPONENT ELEMENTS
+ // ==========================================================================
+
+ &__label {
+ font-weight: $semantic-typography-font-weight-medium;
+ color: $semantic-color-text-primary;
+ line-height: $semantic-typography-line-height-tight;
+
+ &--required {
+ .ui-time-picker__required-indicator {
+ color: $semantic-color-error;
+ margin-left: $semantic-spacing-component-xs;
+ }
+ }
+ }
+
+ &__container {
+ display: flex;
+ align-items: center;
+ position: relative;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease-in-out;
+ cursor: pointer;
+
+ &:hover:not(.ui-time-picker--disabled &) {
+ border-color: $semantic-color-border-secondary;
+ }
+
+ &--has-clear {
+ .ui-time-picker__field {
+ padding-right: $semantic-spacing-component-xl;
+ }
+ }
+ }
+
+ &__prefix-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-left: $semantic-spacing-component-sm;
+ color: $semantic-color-text-tertiary;
+ pointer-events: none;
+ }
+
+ &__field {
+ flex: 1;
+ border: none;
+ outline: none;
+ background: transparent;
+ color: $semantic-color-text-primary;
+ cursor: pointer;
+ font-family: inherit;
+
+ &::placeholder {
+ color: $semantic-color-text-tertiary;
+ }
+
+ &:focus {
+ outline: none;
+ }
+ }
+
+ &__clear-btn {
+ position: absolute;
+ right: $semantic-spacing-component-sm;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: $semantic-sizing-icon-button;
+ height: $semantic-sizing-icon-button;
+ border: none;
+ background: transparent;
+ color: $semantic-color-text-tertiary;
+ cursor: pointer;
+ border-radius: $semantic-border-radius-sm;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease-in-out;
+
+ &:hover {
+ background: $semantic-color-surface-secondary;
+ color: $semantic-color-text-secondary;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+ }
+
+ &__helper-text {
+ font-size: $semantic-typography-font-size-sm;
+ margin-top: $semantic-spacing-component-xs;
+ line-height: $semantic-typography-line-height-normal;
+
+ &--error {
+ color: $semantic-color-error;
+ }
+
+ &--success {
+ color: $semantic-color-success;
+ }
+
+ &--warning {
+ color: $semantic-color-warning;
+ }
+
+ &--default {
+ color: $semantic-color-text-secondary;
+ }
+ }
+
+ // ==========================================================================
+ // TIME PICKER DROPDOWN
+ // ==========================================================================
+
+ &__dropdown {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ z-index: 1000;
+ margin-top: $semantic-spacing-component-xs;
+ background: $semantic-color-surface-primary;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-lg;
+ box-shadow: $semantic-shadow-dropdown;
+ padding: $semantic-spacing-component-md;
+ min-width: 320px;
+ }
+
+ &__time-display {
+ margin-bottom: $semantic-spacing-component-md;
+ }
+
+ &__time-sections {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: $semantic-spacing-component-sm;
+ }
+
+ &__time-section {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: $semantic-spacing-component-xs;
+ }
+
+ &__time-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: $semantic-sizing-icon-button;
+ height: $semantic-sizing-icon-button;
+ border: none;
+ background: transparent;
+ color: $semantic-color-text-secondary;
+ cursor: pointer;
+ border-radius: $semantic-border-radius-sm;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease-in-out;
+
+ &:hover {
+ background: $semantic-color-surface-secondary;
+ color: $semantic-color-text-primary;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+ }
+
+ &__time-input {
+ width: 60px;
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-md;
+ background: $semantic-color-surface-primary;
+ color: $semantic-color-text-primary;
+ font-size: 1.125rem;
+ font-weight: $semantic-typography-font-weight-medium;
+ text-align: center;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease-in-out;
+
+ &:focus {
+ outline: none;
+ border-color: $semantic-color-primary;
+ box-shadow: $semantic-shadow-input-focus;
+ }
+
+ &::-webkit-outer-spin-button,
+ &::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
+
+ &[type=number] {
+ -moz-appearance: textfield;
+ }
+ }
+
+ &__time-label {
+ font-size: $semantic-typography-font-size-xs;
+ color: $semantic-color-text-secondary;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ }
+
+ &__separator {
+ font-size: $semantic-typography-font-size-xl;
+ font-weight: $semantic-typography-font-weight-bold;
+ color: $semantic-color-text-primary;
+ margin: 0 $semantic-spacing-component-xs;
+ align-self: center;
+ margin-top: -20px; // Adjust to center with inputs
+ }
+
+ &__ampm-section {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-component-xs;
+ margin-left: $semantic-spacing-component-md;
+ }
+
+ &__ampm-btn {
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-sm;
+ background: $semantic-color-surface-primary;
+ color: $semantic-color-text-secondary;
+ cursor: pointer;
+ font-size: 0.875rem;
+ font-weight: $semantic-typography-font-weight-medium;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease-in-out;
+
+ &:hover {
+ background: $semantic-color-surface-secondary;
+ color: $semantic-color-text-primary;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+
+ &--active {
+ background: $semantic-color-primary;
+ color: $semantic-color-on-primary;
+ border-color: $semantic-color-primary;
+
+ &:hover {
+ background: $semantic-color-primary-focus;
+ border-color: $semantic-color-primary-focus;
+ }
+ }
+ }
+
+ // ==========================================================================
+ // PRESET TIMES
+ // ==========================================================================
+
+ &__presets {
+ border-top: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ padding-top: $semantic-spacing-component-md;
+ margin-bottom: $semantic-spacing-component-md;
+ }
+
+ &__presets-title {
+ font-size: $semantic-typography-font-size-sm;
+ font-weight: $semantic-typography-font-weight-medium;
+ color: $semantic-color-text-secondary;
+ margin-bottom: $semantic-spacing-component-sm;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ }
+
+ &__presets-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: $semantic-spacing-component-xs;
+ }
+
+ &__preset-btn {
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-sm;
+ background: $semantic-color-surface-primary;
+ color: $semantic-color-text-secondary;
+ cursor: pointer;
+ font-size: 0.875rem;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease-in-out;
+
+ &:hover {
+ background: $semantic-color-surface-secondary;
+ color: $semantic-color-text-primary;
+ border-color: $semantic-color-border-secondary;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+ }
+
+ // ==========================================================================
+ // ACTIONS
+ // ==========================================================================
+
+ &__actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: $semantic-spacing-component-sm;
+ border-top: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ padding-top: $semantic-spacing-component-md;
+ }
+
+ &__action-btn {
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
+ border-radius: $semantic-border-radius-md;
+ font-size: 0.875rem;
+ font-weight: $semantic-typography-font-weight-medium;
+ cursor: pointer;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease-in-out;
+ min-width: 80px;
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+
+ &--primary {
+ background: $semantic-color-primary;
+ color: $semantic-color-on-primary;
+ border: $semantic-border-width-1 solid $semantic-color-primary;
+
+ &:hover {
+ background: $semantic-color-primary-focus;
+ border-color: $semantic-color-primary-focus;
+ }
+
+ &:active {
+ background: $semantic-color-primary-pressed;
+ border-color: $semantic-color-primary-pressed;
+ }
+ }
+
+ &--secondary {
+ background: transparent;
+ color: $semantic-color-text-secondary;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+
+ &:hover {
+ background: $semantic-color-surface-secondary;
+ color: $semantic-color-text-primary;
+ border-color: $semantic-color-border-secondary;
+ }
+
+ &:active {
+ background: $semantic-color-surface-secondary;
+ }
+ }
+ }
+
+ // ==========================================================================
+ // RESPONSIVE DESIGN
+ // ==========================================================================
+
+ @media (max-width: 768px) {
+ &__dropdown {
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ right: auto;
+ transform: translate(-50%, -50%);
+ width: 90vw;
+ max-width: 350px;
+ }
+
+ &__time-sections {
+ flex-direction: column;
+ gap: $semantic-spacing-component-md;
+ }
+
+ &__separator {
+ display: none;
+ }
+
+ &__ampm-section {
+ flex-direction: row;
+ margin-left: 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/time-picker/time-picker.component.ts b/projects/ui-essentials/src/lib/components/forms/time-picker/time-picker.component.ts
new file mode 100644
index 0000000..f9e5051
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/time-picker/time-picker.component.ts
@@ -0,0 +1,609 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, forwardRef, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
+import { faClock, faTimes, faChevronUp, faChevronDown } from '@fortawesome/free-solid-svg-icons';
+
+export type TimePickerSize = 'sm' | 'md' | 'lg';
+export type TimePickerVariant = 'outlined' | 'filled' | 'underlined';
+export type TimePickerState = 'default' | 'error' | 'success' | 'warning';
+export type TimePickerFormat = '12' | '24';
+
+export interface TimeValue {
+ hours: number;
+ minutes: number;
+ seconds?: number;
+}
+
+@Component({
+ selector: 'ui-time-picker',
+ standalone: true,
+ imports: [CommonModule, FontAwesomeModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => TimePickerComponent),
+ multi: true
+ }
+ ],
+ template: `
+
+
+ @if (label) {
+
+ {{ label }}
+ @if (required) {
+ *
+ }
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+ @if (clearable && value && !disabled) {
+
+
+
+ }
+
+
+
+ @if (isOpen()) {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Hours
+
+
+
+
:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Minutes
+
+
+
+ @if (showSeconds) {
+
:
+
+
+
+
+
+
+
+
+
+
+
+
+
Seconds
+
+ }
+
+
+ @if (timeFormat === '12') {
+
+
+ AM
+
+
+
+ PM
+
+
+ }
+
+
+
+
+ @if (presetTimes.length > 0) {
+
+
Quick Select
+
+ @for (preset of presetTimes; track preset.label) {
+
+ {{ preset.label }}
+
+ }
+
+
+ }
+
+
+
+
+ Cancel
+
+
+
+ OK
+
+
+
+ }
+
+
+ @if (helperText || errorMessage) {
+
+ {{ state === 'error' ? errorMessage : helperText }}
+
+ }
+
+ `,
+ styleUrl: './time-picker.component.scss'
+})
+export class TimePickerComponent implements ControlValueAccessor {
+ @Input() label: string = '';
+ @Input() placeholder: string = 'Select time';
+ @Input() size: TimePickerSize = 'md';
+ @Input() variant: TimePickerVariant = 'outlined';
+ @Input() state: TimePickerState = 'default';
+ @Input() disabled = false;
+ @Input() required = false;
+ @Input() clearable = true;
+ @Input() helperText: string = '';
+ @Input() errorMessage: string = '';
+ @Input() timeFormat: TimePickerFormat = '12';
+ @Input() showSeconds = false;
+ @Input() minuteStep = 1;
+ @Input() secondStep = 1;
+ @Input() presetTimes: Array<{label: string, value: TimeValue}> = [];
+ @Input() inputId: string = `time-picker-${Math.random().toString(36).substr(2, 9)}`;
+
+ @Output() timeChange = new EventEmitter();
+ @Output() pickerOpen = new EventEmitter();
+ @Output() pickerClose = new EventEmitter();
+
+ value: TimeValue | null = null;
+ isOpen = signal(false);
+ ampm = signal<'AM' | 'PM'>('AM');
+
+ // Internal state for the picker
+ private tempValue: TimeValue = { hours: 12, minutes: 0, seconds: 0 };
+
+ readonly faClock = faClock;
+ readonly faTimes = faTimes;
+ readonly faChevronUp = faChevronUp;
+ readonly faChevronDown = faChevronDown;
+
+ private onChange = (value: TimeValue | null) => {};
+ private onTouched = () => {};
+
+ writeValue(value: TimeValue | null): void {
+ this.value = value;
+ if (value) {
+ this.tempValue = { ...value };
+ if (this.timeFormat === '12') {
+ this.updateAMPM();
+ }
+ }
+ }
+
+ registerOnChange(fn: (value: TimeValue | null) => void): void {
+ this.onChange = fn;
+ }
+
+ registerOnTouched(fn: () => void): void {
+ this.onTouched = fn;
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ }
+
+ get formattedValue(): string {
+ if (!this.value) return '';
+ return this.formatTime(this.value);
+ }
+
+ get displayHours(): string {
+ if (this.timeFormat === '12') {
+ const hours = this.tempValue.hours;
+ const displayHour = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
+ return displayHour.toString().padStart(2, '0');
+ }
+ return this.tempValue.hours.toString().padStart(2, '0');
+ }
+
+ get displayMinutes(): string {
+ return this.tempValue.minutes.toString().padStart(2, '0');
+ }
+
+ get displaySeconds(): string {
+ return (this.tempValue.seconds || 0).toString().padStart(2, '0');
+ }
+
+ getWrapperClasses(): string {
+ const classes = [
+ `ui-time-picker--${this.size}`,
+ `ui-time-picker--${this.variant}`,
+ `ui-time-picker--${this.state}`
+ ];
+
+ if (this.disabled) classes.push('ui-time-picker--disabled');
+ if (this.isOpen()) classes.push('ui-time-picker--open');
+
+ return classes.join(' ');
+ }
+
+ getContainerClasses(): string {
+ const classes: string[] = [];
+ if (this.clearable && this.value) classes.push('ui-time-picker__container--has-clear');
+ return classes.join(' ');
+ }
+
+ getHelperTextClasses(): string {
+ return `ui-time-picker__helper-text--${this.state}`;
+ }
+
+ toggleTimePicker(): void {
+ if (this.disabled) return;
+
+ if (this.isOpen()) {
+ this.closeTimePicker();
+ } else {
+ this.openTimePicker();
+ }
+ }
+
+ openTimePicker(): void {
+ this.isOpen.set(true);
+ // Initialize temp value with current value or default
+ if (this.value) {
+ this.tempValue = { ...this.value };
+ } else {
+ const now = new Date();
+ this.tempValue = {
+ hours: now.getHours(),
+ minutes: now.getMinutes(),
+ seconds: this.showSeconds ? now.getSeconds() : 0
+ };
+ }
+
+ if (this.timeFormat === '12') {
+ this.updateAMPM();
+ }
+
+ this.pickerOpen.emit();
+ }
+
+ closeTimePicker(): void {
+ this.isOpen.set(false);
+ this.onTouched();
+ this.pickerClose.emit();
+ }
+
+ confirmTime(): void {
+ this.value = { ...this.tempValue };
+ this.onChange(this.value);
+ this.timeChange.emit(this.value);
+ this.closeTimePicker();
+ }
+
+ clearValue(): void {
+ this.value = null;
+ this.onChange(null);
+ this.timeChange.emit(null);
+ }
+
+ incrementHours(): void {
+ const maxHours = this.timeFormat === '24' ? 23 : 12;
+ const minHours = this.timeFormat === '24' ? 0 : 1;
+
+ if (this.tempValue.hours >= maxHours) {
+ this.tempValue.hours = minHours;
+ } else {
+ this.tempValue.hours++;
+ }
+
+ if (this.timeFormat === '12') {
+ this.updateAMPM();
+ }
+ }
+
+ decrementHours(): void {
+ const maxHours = this.timeFormat === '24' ? 23 : 12;
+ const minHours = this.timeFormat === '24' ? 0 : 1;
+
+ if (this.tempValue.hours <= minHours) {
+ this.tempValue.hours = maxHours;
+ } else {
+ this.tempValue.hours--;
+ }
+
+ if (this.timeFormat === '12') {
+ this.updateAMPM();
+ }
+ }
+
+ incrementMinutes(): void {
+ if (this.tempValue.minutes >= 59) {
+ this.tempValue.minutes = 0;
+ } else {
+ this.tempValue.minutes = Math.min(59, this.tempValue.minutes + this.minuteStep);
+ }
+ }
+
+ decrementMinutes(): void {
+ if (this.tempValue.minutes <= 0) {
+ this.tempValue.minutes = 59;
+ } else {
+ this.tempValue.minutes = Math.max(0, this.tempValue.minutes - this.minuteStep);
+ }
+ }
+
+ incrementSeconds(): void {
+ if (!this.showSeconds) return;
+
+ if ((this.tempValue.seconds || 0) >= 59) {
+ this.tempValue.seconds = 0;
+ } else {
+ this.tempValue.seconds = Math.min(59, (this.tempValue.seconds || 0) + this.secondStep);
+ }
+ }
+
+ decrementSeconds(): void {
+ if (!this.showSeconds) return;
+
+ if ((this.tempValue.seconds || 0) <= 0) {
+ this.tempValue.seconds = 59;
+ } else {
+ this.tempValue.seconds = Math.max(0, (this.tempValue.seconds || 0) - this.secondStep);
+ }
+ }
+
+ onHoursInput(event: Event): void {
+ const target = event.target as HTMLInputElement;
+ const hours = parseInt(target.value, 10);
+
+ if (!isNaN(hours)) {
+ this.tempValue.hours = hours;
+ }
+ }
+
+ onMinutesInput(event: Event): void {
+ const target = event.target as HTMLInputElement;
+ const minutes = parseInt(target.value, 10);
+
+ if (!isNaN(minutes)) {
+ this.tempValue.minutes = minutes;
+ }
+ }
+
+ onSecondsInput(event: Event): void {
+ if (!this.showSeconds) return;
+
+ const target = event.target as HTMLInputElement;
+ const seconds = parseInt(target.value, 10);
+
+ if (!isNaN(seconds)) {
+ this.tempValue.seconds = seconds;
+ }
+ }
+
+ validateHours(): void {
+ const maxHours = this.timeFormat === '24' ? 23 : 12;
+ const minHours = this.timeFormat === '24' ? 0 : 1;
+
+ this.tempValue.hours = Math.max(minHours, Math.min(maxHours, this.tempValue.hours));
+ }
+
+ validateMinutes(): void {
+ this.tempValue.minutes = Math.max(0, Math.min(59, this.tempValue.minutes));
+ }
+
+ validateSeconds(): void {
+ if (!this.showSeconds) return;
+ this.tempValue.seconds = Math.max(0, Math.min(59, this.tempValue.seconds || 0));
+ }
+
+ setAMPM(period: 'AM' | 'PM'): void {
+ if (this.timeFormat !== '12') return;
+
+ this.ampm.set(period);
+
+ // Convert temp hours to 24-hour format for internal storage
+ const currentHours = this.tempValue.hours;
+
+ if (period === 'AM') {
+ if (currentHours === 12) {
+ this.tempValue.hours = 0;
+ } else if (currentHours > 12) {
+ this.tempValue.hours = currentHours - 12;
+ }
+ } else {
+ if (currentHours < 12) {
+ this.tempValue.hours = currentHours + 12;
+ }
+ }
+ }
+
+ selectPreset(timeValue: TimeValue): void {
+ this.tempValue = { ...timeValue };
+ if (this.timeFormat === '12') {
+ this.updateAMPM();
+ }
+ this.confirmTime();
+ }
+
+ onInputKeyDown(event: KeyboardEvent): void {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ this.toggleTimePicker();
+ } else if (event.key === 'Escape' && this.isOpen()) {
+ event.preventDefault();
+ this.closeTimePicker();
+ }
+ }
+
+ private formatTime(time: TimeValue): string {
+ if (this.timeFormat === '12') {
+ const hours = time.hours;
+ const displayHour = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
+ const period = hours >= 12 ? 'PM' : 'AM';
+ const minutes = time.minutes.toString().padStart(2, '0');
+
+ if (this.showSeconds && time.seconds !== undefined) {
+ const seconds = time.seconds.toString().padStart(2, '0');
+ return `${displayHour}:${minutes}:${seconds} ${period}`;
+ }
+
+ return `${displayHour}:${minutes} ${period}`;
+ } else {
+ const hours = time.hours.toString().padStart(2, '0');
+ const minutes = time.minutes.toString().padStart(2, '0');
+
+ if (this.showSeconds && time.seconds !== undefined) {
+ const seconds = time.seconds.toString().padStart(2, '0');
+ return `${hours}:${minutes}:${seconds}`;
+ }
+
+ return `${hours}:${minutes}`;
+ }
+ }
+
+ private updateAMPM(): void {
+ if (this.timeFormat !== '12') return;
+
+ const period = this.tempValue.hours >= 12 ? 'PM' : 'AM';
+ this.ampm.set(period);
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/layout/container/container.component.scss b/projects/ui-essentials/src/lib/components/layout/container/container.component.scss
new file mode 100644
index 0000000..88a5194
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/layout/container/container.component.scss
@@ -0,0 +1,288 @@
+@use '../../../../../../shared-ui/src/styles/semantic/index' as *;
+
+.ui-container {
+ display: block;
+ position: relative;
+ width: 100%;
+ margin-left: auto;
+ margin-right: auto;
+
+ // Base padding
+ padding-left: $semantic-spacing-component-md;
+ padding-right: $semantic-spacing-component-md;
+
+ // Size variants
+ &--xs {
+ max-width: 475px;
+ }
+
+ &--sm {
+ max-width: 640px;
+ }
+
+ &--md {
+ max-width: 768px;
+ }
+
+ &--lg {
+ max-width: 1024px;
+ }
+
+ &--xl {
+ max-width: 1280px;
+ }
+
+ &--2xl {
+ max-width: 1536px;
+ }
+
+ &--3xl {
+ max-width: 1792px;
+ }
+
+ &--4xl {
+ max-width: 1920px;
+ }
+
+ &--full {
+ max-width: none;
+ }
+
+ // Padding variants
+ &--padding-xs {
+ padding-left: $semantic-spacing-component-xs;
+ padding-right: $semantic-spacing-component-xs;
+ }
+
+ &--padding-sm {
+ padding-left: $semantic-spacing-component-sm;
+ padding-right: $semantic-spacing-component-sm;
+ }
+
+ &--padding-md {
+ padding-left: $semantic-spacing-component-md;
+ padding-right: $semantic-spacing-component-md;
+ }
+
+ &--padding-lg {
+ padding-left: $semantic-spacing-component-lg;
+ padding-right: $semantic-spacing-component-lg;
+ }
+
+ &--padding-xl {
+ padding-left: $semantic-spacing-component-xl;
+ padding-right: $semantic-spacing-component-xl;
+ }
+
+ &--padding-none {
+ padding-left: 0;
+ padding-right: 0;
+ }
+
+ // Vertical padding variants
+ &--padding-y-xs {
+ padding-top: $semantic-spacing-component-xs;
+ padding-bottom: $semantic-spacing-component-xs;
+ }
+
+ &--padding-y-sm {
+ padding-top: $semantic-spacing-component-sm;
+ padding-bottom: $semantic-spacing-component-sm;
+ }
+
+ &--padding-y-md {
+ padding-top: $semantic-spacing-component-md;
+ padding-bottom: $semantic-spacing-component-md;
+ }
+
+ &--padding-y-lg {
+ padding-top: $semantic-spacing-component-lg;
+ padding-bottom: $semantic-spacing-component-lg;
+ }
+
+ &--padding-y-xl {
+ padding-top: $semantic-spacing-component-xl;
+ padding-bottom: $semantic-spacing-component-xl;
+ }
+
+ &--padding-y-none {
+ padding-top: 0;
+ padding-bottom: 0;
+ }
+
+ // Background variants
+ &--surface {
+ background: $semantic-color-surface-primary;
+ }
+
+ &--surface-secondary {
+ background: $semantic-color-surface-secondary;
+ }
+
+ &--surface-elevated {
+ background: $semantic-color-surface-elevated;
+ }
+
+ &--transparent {
+ background: transparent;
+ }
+
+ // Visual variants
+ &--card {
+ background: $semantic-color-surface-primary;
+ border-radius: $semantic-border-radius-md;
+ box-shadow: $semantic-shadow-elevation-1;
+ border: 1px solid $semantic-color-border-subtle;
+
+ padding: $semantic-spacing-container-card-padding;
+
+ &.ui-container--padding-lg {
+ padding: $semantic-spacing-container-card-padding-lg;
+ }
+ }
+
+ &--section {
+ padding-top: $semantic-spacing-layout-section-sm;
+ padding-bottom: $semantic-spacing-layout-section-sm;
+ }
+
+ &--hero {
+ padding-top: $semantic-spacing-layout-section-lg;
+ padding-bottom: $semantic-spacing-layout-section-lg;
+ text-align: center;
+ }
+
+ // Flex container variants
+ &--flex {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-stack-md;
+
+ &.ui-container--flex-row {
+ flex-direction: row;
+ }
+
+ &.ui-container--flex-center {
+ align-items: center;
+ justify-content: center;
+ }
+
+ &.ui-container--flex-between {
+ justify-content: space-between;
+ }
+
+ &.ui-container--flex-around {
+ justify-content: space-around;
+ }
+
+ &.ui-container--flex-evenly {
+ justify-content: space-evenly;
+ }
+
+ &.ui-container--flex-start {
+ align-items: flex-start;
+ justify-content: flex-start;
+ }
+
+ &.ui-container--flex-end {
+ align-items: flex-end;
+ justify-content: flex-end;
+ }
+ }
+
+ // Grid container variants
+ &--grid {
+ display: grid;
+ gap: $semantic-spacing-grid-gap-md;
+
+ &.ui-container--grid-2 {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ &.ui-container--grid-3 {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ &.ui-container--grid-4 {
+ grid-template-columns: repeat(4, 1fr);
+ }
+
+ &.ui-container--grid-auto {
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ }
+ }
+
+ // Centered content
+ &--centered {
+ text-align: center;
+
+ &.ui-container--flex {
+ align-items: center;
+ justify-content: center;
+ }
+ }
+
+ // Responsive behavior
+ @media (max-width: 768px) {
+ &--responsive {
+ padding-left: $semantic-spacing-component-sm;
+ padding-right: $semantic-spacing-component-sm;
+
+ &.ui-container--card {
+ border-radius: $semantic-border-radius-sm;
+ padding: $semantic-spacing-component-sm;
+ }
+
+ &.ui-container--section {
+ padding-top: $semantic-spacing-layout-section-xs;
+ padding-bottom: $semantic-spacing-layout-section-xs;
+ }
+
+ &.ui-container--hero {
+ padding-top: $semantic-spacing-layout-section-sm;
+ padding-bottom: $semantic-spacing-layout-section-sm;
+ }
+
+ &.ui-container--grid-4,
+ &.ui-container--grid-3 {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ &.ui-container--flex-row {
+ flex-direction: column;
+ }
+ }
+ }
+
+ @media (max-width: 480px) {
+ &--responsive {
+ padding-left: $semantic-spacing-component-xs;
+ padding-right: $semantic-spacing-component-xs;
+
+ &.ui-container--grid-4,
+ &.ui-container--grid-3,
+ &.ui-container--grid-2 {
+ grid-template-columns: 1fr;
+ }
+ }
+ }
+
+ // Constrain content width while maintaining container responsiveness
+ &--prose {
+ max-width: 65ch;
+ }
+
+ // Scrollable container
+ &--scrollable {
+ overflow-y: auto;
+
+ &.ui-container--scrollable-x {
+ overflow-x: auto;
+ overflow-y: visible;
+ }
+
+ &.ui-container--scrollable-both {
+ overflow: auto;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/layout/container/container.component.ts b/projects/ui-essentials/src/lib/components/layout/container/container.component.ts
new file mode 100644
index 0000000..5126a75
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/layout/container/container.component.ts
@@ -0,0 +1,59 @@
+import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+type ContainerSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | 'full';
+type ContainerPadding = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'none';
+type ContainerVariant = 'default' | 'card' | 'section' | 'hero' | 'flex' | 'grid' | 'centered' | 'prose';
+type ContainerBackground = 'transparent' | 'surface' | 'surface-secondary' | 'surface-elevated';
+type FlexDirection = 'row' | 'column';
+type FlexJustify = 'start' | 'end' | 'center' | 'between' | 'around' | 'evenly';
+type GridColumns = 2 | 3 | 4 | 'auto';
+type ScrollDirection = 'y' | 'x' | 'both';
+
+@Component({
+ selector: 'ui-container',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ template: `
+
+
+
+
+ `,
+ styleUrl: './container.component.scss'
+})
+export class ContainerComponent {
+ @Input() size: ContainerSize = 'lg';
+ @Input() variant: ContainerVariant = 'default';
+ @Input() background: ContainerBackground = 'transparent';
+ @Input() padding?: ContainerPadding;
+ @Input() paddingY?: ContainerPadding;
+ @Input() flexDirection?: FlexDirection;
+ @Input() flexJustify?: FlexJustify;
+ @Input() gridColumns?: GridColumns;
+ @Input() scrollable?: ScrollDirection;
+ @Input() responsive = true;
+ @Input() customMaxWidth?: string;
+ @Input() customMinHeight?: string;
+ @Input() customMaxHeight?: string;
+ @Input() role?: string;
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/layout/container/index.ts b/projects/ui-essentials/src/lib/components/layout/container/index.ts
new file mode 100644
index 0000000..6a9b98f
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/layout/container/index.ts
@@ -0,0 +1 @@
+export * from './container.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/layout/grid-system/grid-system.component.scss b/projects/ui-essentials/src/lib/components/layout/grid-system/grid-system.component.scss
new file mode 100644
index 0000000..167e24c
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/layout/grid-system/grid-system.component.scss
@@ -0,0 +1,242 @@
+@use '../../../../../../shared-ui/src/styles/semantic' as tokens;
+
+.ui-grid-system {
+ display: grid;
+ position: relative;
+ width: 100%;
+
+ // Base grid layout
+ gap: tokens.$semantic-spacing-grid-gap-md;
+
+ // Size variants
+ &--gap-xs {
+ gap: tokens.$semantic-spacing-grid-gap-xs;
+ }
+
+ &--gap-sm {
+ gap: tokens.$semantic-spacing-grid-gap-sm;
+ }
+
+ &--gap-md {
+ gap: tokens.$semantic-spacing-grid-gap-md;
+ }
+
+ &--gap-lg {
+ gap: tokens.$semantic-spacing-grid-gap-lg;
+ }
+
+ &--gap-xl {
+ gap: tokens.$semantic-spacing-grid-gap-xl;
+ }
+
+ // Column variants
+ &--cols-1 {
+ grid-template-columns: repeat(1, 1fr);
+ }
+
+ &--cols-2 {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ &--cols-3 {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ &--cols-4 {
+ grid-template-columns: repeat(4, 1fr);
+ }
+
+ &--cols-6 {
+ grid-template-columns: repeat(6, 1fr);
+ }
+
+ &--cols-12 {
+ grid-template-columns: repeat(12, 1fr);
+ }
+
+ // Auto-fill responsive columns
+ &--auto-fill {
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
+ }
+
+ &--auto-fit {
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ }
+
+ // Row gap variants
+ &--row-gap-xs {
+ row-gap: tokens.$semantic-spacing-grid-gap-xs;
+ }
+
+ &--row-gap-sm {
+ row-gap: tokens.$semantic-spacing-grid-gap-sm;
+ }
+
+ &--row-gap-md {
+ row-gap: tokens.$semantic-spacing-grid-gap-md;
+ }
+
+ &--row-gap-lg {
+ row-gap: tokens.$semantic-spacing-grid-gap-lg;
+ }
+
+ &--row-gap-xl {
+ row-gap: tokens.$semantic-spacing-grid-gap-xl;
+ }
+
+ // Column gap variants
+ &--column-gap-xs {
+ column-gap: tokens.$semantic-spacing-grid-gap-xs;
+ }
+
+ &--column-gap-sm {
+ column-gap: tokens.$semantic-spacing-grid-gap-sm;
+ }
+
+ &--column-gap-md {
+ column-gap: tokens.$semantic-spacing-grid-gap-md;
+ }
+
+ &--column-gap-lg {
+ column-gap: tokens.$semantic-spacing-grid-gap-lg;
+ }
+
+ &--column-gap-xl {
+ column-gap: tokens.$semantic-spacing-grid-gap-xl;
+ }
+
+ // Alignment variants
+ &--justify-start {
+ justify-items: start;
+ }
+
+ &--justify-end {
+ justify-items: end;
+ }
+
+ &--justify-center {
+ justify-items: center;
+ }
+
+ &--justify-stretch {
+ justify-items: stretch;
+ }
+
+ &--align-start {
+ align-items: start;
+ }
+
+ &--align-end {
+ align-items: end;
+ }
+
+ &--align-center {
+ align-items: center;
+ }
+
+ &--align-stretch {
+ align-items: stretch;
+ }
+
+ // Content alignment
+ &--justify-content-start {
+ justify-content: start;
+ }
+
+ &--justify-content-end {
+ justify-content: end;
+ }
+
+ &--justify-content-center {
+ justify-content: center;
+ }
+
+ &--justify-content-stretch {
+ justify-content: stretch;
+ }
+
+ &--justify-content-space-around {
+ justify-content: space-around;
+ }
+
+ &--justify-content-space-between {
+ justify-content: space-between;
+ }
+
+ &--justify-content-space-evenly {
+ justify-content: space-evenly;
+ }
+
+ &--align-content-start {
+ align-content: start;
+ }
+
+ &--align-content-end {
+ align-content: end;
+ }
+
+ &--align-content-center {
+ align-content: center;
+ }
+
+ &--align-content-stretch {
+ align-content: stretch;
+ }
+
+ &--align-content-space-around {
+ align-content: space-around;
+ }
+
+ &--align-content-space-between {
+ align-content: space-between;
+ }
+
+ &--align-content-space-evenly {
+ align-content: space-evenly;
+ }
+
+ // Dense packing
+ &--auto-rows-min {
+ grid-auto-rows: min-content;
+ }
+
+ &--auto-rows-max {
+ grid-auto-rows: max-content;
+ }
+
+ &--auto-rows-fr {
+ grid-auto-rows: 1fr;
+ }
+
+ &--dense {
+ grid-auto-flow: dense;
+ }
+
+ // Responsive behavior
+ @media (max-width: 768px) {
+ &--responsive {
+ grid-template-columns: 1fr;
+ gap: tokens.$semantic-spacing-grid-gap-sm;
+ }
+
+ &--cols-12,
+ &--cols-6,
+ &--cols-4 {
+ grid-template-columns: repeat(2, 1fr);
+ }
+ }
+
+ @media (max-width: 480px) {
+ &--responsive {
+ gap: tokens.$semantic-spacing-grid-gap-xs;
+ }
+
+ &--cols-12,
+ &--cols-6,
+ &--cols-4,
+ &--cols-3,
+ &--cols-2 {
+ grid-template-columns: 1fr;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/layout/grid-system/grid-system.component.ts b/projects/ui-essentials/src/lib/components/layout/grid-system/grid-system.component.ts
new file mode 100644
index 0000000..ce741c1
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/layout/grid-system/grid-system.component.ts
@@ -0,0 +1,54 @@
+import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+type GridColumns = 1 | 2 | 3 | 4 | 6 | 12 | 'auto-fill' | 'auto-fit';
+type GridGap = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
+type GridAlignment = 'start' | 'end' | 'center' | 'stretch';
+type GridContentAlignment = 'start' | 'end' | 'center' | 'stretch' | 'space-around' | 'space-between' | 'space-evenly';
+type GridAutoRows = 'min' | 'max' | 'fr';
+
+@Component({
+ selector: 'ui-grid-system',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ template: `
+
+
+
+
+ `,
+ styleUrl: './grid-system.component.scss'
+})
+export class GridSystemComponent {
+ @Input() columns: GridColumns = 'auto-fit';
+ @Input() gap: GridGap = 'md';
+ @Input() rowGap?: GridGap;
+ @Input() columnGap?: GridGap;
+ @Input() justifyItems?: GridAlignment;
+ @Input() alignItems?: GridAlignment;
+ @Input() justifyContent?: GridContentAlignment;
+ @Input() alignContent?: GridContentAlignment;
+ @Input() autoRows?: GridAutoRows;
+ @Input() dense = false;
+ @Input() responsive = true;
+ @Input() customColumns?: string;
+ @Input() customRows?: string;
+ @Input() role = 'grid';
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/layout/grid-system/index.ts b/projects/ui-essentials/src/lib/components/layout/grid-system/index.ts
new file mode 100644
index 0000000..3f0f303
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/layout/grid-system/index.ts
@@ -0,0 +1 @@
+export * from './grid-system.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/layout/index.ts b/projects/ui-essentials/src/lib/components/layout/index.ts
new file mode 100644
index 0000000..8c71929
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/layout/index.ts
@@ -0,0 +1,3 @@
+export * from './container';
+export * from './grid-system';
+export * from './spacer';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/layout/spacer/index.ts b/projects/ui-essentials/src/lib/components/layout/spacer/index.ts
new file mode 100644
index 0000000..ce69c4d
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/layout/spacer/index.ts
@@ -0,0 +1 @@
+export * from './spacer.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/layout/spacer/spacer.component.scss b/projects/ui-essentials/src/lib/components/layout/spacer/spacer.component.scss
new file mode 100644
index 0000000..a04ca43
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/layout/spacer/spacer.component.scss
@@ -0,0 +1,314 @@
+@use '../../../../../../shared-ui/src/styles/semantic' as tokens;
+
+.ui-spacer {
+ display: block;
+ position: relative;
+
+ // Default spacing
+ width: tokens.$semantic-spacing-md;
+ height: tokens.$semantic-spacing-md;
+
+ // Size variants
+ &--xs {
+ width: tokens.$semantic-spacing-xs;
+ height: tokens.$semantic-spacing-xs;
+ }
+
+ &--sm {
+ width: tokens.$semantic-spacing-sm;
+ height: tokens.$semantic-spacing-sm;
+ }
+
+ &--md {
+ width: tokens.$semantic-spacing-md;
+ height: tokens.$semantic-spacing-md;
+ }
+
+ &--lg {
+ width: tokens.$semantic-spacing-lg;
+ height: tokens.$semantic-spacing-lg;
+ }
+
+ &--xl {
+ width: tokens.$semantic-spacing-xl;
+ height: tokens.$semantic-spacing-xl;
+ }
+
+ &--2xl {
+ width: tokens.$semantic-spacing-2xl;
+ height: tokens.$semantic-spacing-2xl;
+ }
+
+ &--3xl {
+ width: tokens.$semantic-spacing-3xl;
+ height: tokens.$semantic-spacing-3xl;
+ }
+
+ &--4xl {
+ width: tokens.$semantic-spacing-4xl;
+ height: tokens.$semantic-spacing-4xl;
+ }
+
+ &--5xl {
+ width: tokens.$semantic-spacing-5xl;
+ height: tokens.$semantic-spacing-5xl;
+ }
+
+ // Directional variants
+ &--horizontal {
+ height: 0;
+ width: tokens.$semantic-spacing-md;
+
+ &.ui-spacer--xs { width: tokens.$semantic-spacing-xs; }
+ &.ui-spacer--sm { width: tokens.$semantic-spacing-sm; }
+ &.ui-spacer--md { width: tokens.$semantic-spacing-md; }
+ &.ui-spacer--lg { width: tokens.$semantic-spacing-lg; }
+ &.ui-spacer--xl { width: tokens.$semantic-spacing-xl; }
+ &.ui-spacer--2xl { width: tokens.$semantic-spacing-2xl; }
+ &.ui-spacer--3xl { width: tokens.$semantic-spacing-3xl; }
+ &.ui-spacer--4xl { width: tokens.$semantic-spacing-4xl; }
+ &.ui-spacer--5xl { width: tokens.$semantic-spacing-5xl; }
+ }
+
+ &--vertical {
+ width: 0;
+ height: tokens.$semantic-spacing-md;
+
+ &.ui-spacer--xs { height: tokens.$semantic-spacing-xs; }
+ &.ui-spacer--sm { height: tokens.$semantic-spacing-sm; }
+ &.ui-spacer--md { height: tokens.$semantic-spacing-md; }
+ &.ui-spacer--lg { height: tokens.$semantic-spacing-lg; }
+ &.ui-spacer--xl { height: tokens.$semantic-spacing-xl; }
+ &.ui-spacer--2xl { height: tokens.$semantic-spacing-2xl; }
+ &.ui-spacer--3xl { height: tokens.$semantic-spacing-3xl; }
+ &.ui-spacer--4xl { height: tokens.$semantic-spacing-4xl; }
+ &.ui-spacer--5xl { height: tokens.$semantic-spacing-5xl; }
+ }
+
+ // Flexible spacer (grows to fill available space)
+ &--flexible {
+ flex: 1;
+
+ &.ui-spacer--horizontal {
+ width: auto !important;
+ min-width: tokens.$semantic-spacing-xs;
+ }
+
+ &.ui-spacer--vertical {
+ height: auto !important;
+ min-height: tokens.$semantic-spacing-xs;
+ }
+ }
+
+ // Component-specific spacing variants
+ &--component-xs {
+ width: tokens.$semantic-spacing-component-xs;
+ height: tokens.$semantic-spacing-component-xs;
+
+ &.ui-spacer--horizontal {
+ width: tokens.$semantic-spacing-component-xs;
+ height: 0;
+ }
+
+ &.ui-spacer--vertical {
+ width: 0;
+ height: tokens.$semantic-spacing-component-xs;
+ }
+ }
+
+ &--component-sm {
+ width: tokens.$semantic-spacing-component-sm;
+ height: tokens.$semantic-spacing-component-sm;
+
+ &.ui-spacer--horizontal {
+ width: tokens.$semantic-spacing-component-sm;
+ height: 0;
+ }
+
+ &.ui-spacer--vertical {
+ width: 0;
+ height: tokens.$semantic-spacing-component-sm;
+ }
+ }
+
+ &--component-md {
+ width: tokens.$semantic-spacing-component-md;
+ height: tokens.$semantic-spacing-component-md;
+
+ &.ui-spacer--horizontal {
+ width: tokens.$semantic-spacing-component-md;
+ height: 0;
+ }
+
+ &.ui-spacer--vertical {
+ width: 0;
+ height: tokens.$semantic-spacing-component-md;
+ }
+ }
+
+ &--component-lg {
+ width: tokens.$semantic-spacing-component-lg;
+ height: tokens.$semantic-spacing-component-lg;
+
+ &.ui-spacer--horizontal {
+ width: tokens.$semantic-spacing-component-lg;
+ height: 0;
+ }
+
+ &.ui-spacer--vertical {
+ width: 0;
+ height: tokens.$semantic-spacing-component-lg;
+ }
+ }
+
+ &--component-xl {
+ width: tokens.$semantic-spacing-component-xl;
+ height: tokens.$semantic-spacing-component-xl;
+
+ &.ui-spacer--horizontal {
+ width: tokens.$semantic-spacing-component-xl;
+ height: 0;
+ }
+
+ &.ui-spacer--vertical {
+ width: 0;
+ height: tokens.$semantic-spacing-component-xl;
+ }
+ }
+
+ // Layout-specific spacing variants
+ &--layout-xs {
+ width: tokens.$semantic-spacing-layout-xs;
+ height: tokens.$semantic-spacing-layout-xs;
+
+ &.ui-spacer--horizontal {
+ width: tokens.$semantic-spacing-layout-xs;
+ height: 0;
+ }
+
+ &.ui-spacer--vertical {
+ width: 0;
+ height: tokens.$semantic-spacing-layout-xs;
+ }
+ }
+
+ &--layout-sm {
+ width: tokens.$semantic-spacing-layout-sm;
+ height: tokens.$semantic-spacing-layout-sm;
+
+ &.ui-spacer--horizontal {
+ width: tokens.$semantic-spacing-layout-sm;
+ height: 0;
+ }
+
+ &.ui-spacer--vertical {
+ width: 0;
+ height: tokens.$semantic-spacing-layout-sm;
+ }
+ }
+
+ &--layout-md {
+ width: tokens.$semantic-spacing-layout-md;
+ height: tokens.$semantic-spacing-layout-md;
+
+ &.ui-spacer--horizontal {
+ width: tokens.$semantic-spacing-layout-md;
+ height: 0;
+ }
+
+ &.ui-spacer--vertical {
+ width: 0;
+ height: tokens.$semantic-spacing-layout-md;
+ }
+ }
+
+ &--layout-lg {
+ width: tokens.$semantic-spacing-layout-lg;
+ height: tokens.$semantic-spacing-layout-lg;
+
+ &.ui-spacer--horizontal {
+ width: tokens.$semantic-spacing-layout-lg;
+ height: 0;
+ }
+
+ &.ui-spacer--vertical {
+ width: 0;
+ height: tokens.$semantic-spacing-layout-lg;
+ }
+ }
+
+ &--layout-xl {
+ width: tokens.$semantic-spacing-layout-xl;
+ height: tokens.$semantic-spacing-layout-xl;
+
+ &.ui-spacer--horizontal {
+ width: tokens.$semantic-spacing-layout-xl;
+ height: 0;
+ }
+
+ &.ui-spacer--vertical {
+ width: 0;
+ height: tokens.$semantic-spacing-layout-xl;
+ }
+ }
+
+ // Visual debug mode (only in development)
+ &--debug {
+ background: repeating-linear-gradient(
+ 45deg,
+ transparent,
+ transparent 2px,
+ rgba(255, 0, 0, 0.1) 2px,
+ rgba(255, 0, 0, 0.1) 4px
+ );
+ border: 1px dashed rgba(255, 0, 0, 0.3);
+ }
+
+ // Responsive behavior
+ @media (max-width: 768px) {
+ &--responsive {
+ &.ui-spacer--xl,
+ &.ui-spacer--2xl,
+ &.ui-spacer--3xl,
+ &.ui-spacer--4xl,
+ &.ui-spacer--5xl {
+ width: tokens.$semantic-spacing-lg;
+ height: tokens.$semantic-spacing-lg;
+
+ &.ui-spacer--horizontal {
+ width: tokens.$semantic-spacing-lg;
+ height: 0;
+ }
+
+ &.ui-spacer--vertical {
+ width: 0;
+ height: tokens.$semantic-spacing-lg;
+ }
+ }
+ }
+ }
+
+ @media (max-width: 480px) {
+ &--responsive {
+ &.ui-spacer--lg,
+ &.ui-spacer--xl,
+ &.ui-spacer--2xl,
+ &.ui-spacer--3xl,
+ &.ui-spacer--4xl,
+ &.ui-spacer--5xl {
+ width: tokens.$semantic-spacing-md;
+ height: tokens.$semantic-spacing-md;
+
+ &.ui-spacer--horizontal {
+ width: tokens.$semantic-spacing-md;
+ height: 0;
+ }
+
+ &.ui-spacer--vertical {
+ width: 0;
+ height: tokens.$semantic-spacing-md;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/layout/spacer/spacer.component.ts b/projects/ui-essentials/src/lib/components/layout/spacer/spacer.component.ts
new file mode 100644
index 0000000..02c0c68
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/layout/spacer/spacer.component.ts
@@ -0,0 +1,40 @@
+import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+type SpacerSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl';
+type SpacerVariant = 'component-xs' | 'component-sm' | 'component-md' | 'component-lg' | 'component-xl' |
+ 'layout-xs' | 'layout-sm' | 'layout-md' | 'layout-lg' | 'layout-xl';
+type SpacerDirection = 'both' | 'horizontal' | 'vertical';
+
+@Component({
+ selector: 'ui-spacer',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ template: `
+
+
+ `,
+ styleUrl: './spacer.component.scss'
+})
+export class SpacerComponent {
+ @Input() size: SpacerSize = 'md';
+ @Input() variant?: SpacerVariant;
+ @Input() direction: SpacerDirection = 'both';
+ @Input() flexible = false;
+ @Input() responsive = true;
+ @Input() debug = false;
+ @Input() customWidth?: string;
+ @Input() customHeight?: string;
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/media/index.ts b/projects/ui-essentials/src/lib/components/media/index.ts
new file mode 100644
index 0000000..03fe505
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/media/index.ts
@@ -0,0 +1 @@
+export * from './video-player';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/media/video-player/index.ts b/projects/ui-essentials/src/lib/components/media/video-player/index.ts
new file mode 100644
index 0000000..52524e3
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/media/video-player/index.ts
@@ -0,0 +1 @@
+export * from './video-player.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
new file mode 100644
index 0000000..a154c14
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/media/video-player/video-player.component.scss
@@ -0,0 +1,117 @@
+@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');
+ font-size: map-get($font-map, 'font-size');
+ line-height: map-get($font-map, 'line-height');
+ font-weight: map-get($font-map, 'font-weight');
+ letter-spacing: map-get($font-map, 'letter-spacing');
+ } @else {
+ font: $font-map;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/media/video-player/video-player.component.ts b/projects/ui-essentials/src/lib/components/media/video-player/video-player.component.ts
new file mode 100644
index 0000000..19e121a
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/media/video-player/video-player.component.ts
@@ -0,0 +1,430 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, ViewChild, ElementRef, OnDestroy, signal, computed } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+export type VideoPlayerSize = 'sm' | 'md' | 'lg';
+export type VideoPlayerVariant = 'default' | 'minimal' | 'theater';
+export type VideoQuality = '240p' | '360p' | '480p' | '720p' | '1080p' | 'auto';
+
+export interface VideoSource {
+ src: string;
+ type: string;
+ quality?: VideoQuality;
+ label?: string;
+}
+
+export interface VideoTrack {
+ src: string;
+ kind: 'subtitles' | 'captions' | 'descriptions' | 'chapters' | 'metadata';
+ srclang: string;
+ label: string;
+ default?: boolean;
+}
+
+@Component({
+ selector: 'ui-video-player',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ template: `
+
+
+
+
+
+
+
+ @for (source of sources; track source.src) {
+
+ }
+
+
+ @for (track of tracks; track track.src) {
+
+ }
+
+
+
+ Your browser does not support the video tag.
+ @if (sources.length > 0) {
+ Download the video
+ }
+
+
+
+
+ @if (loading()) {
+
+ }
+
+
+ @if (showControls && !disabled) {
+
+
+
+
+ @if (isPlaying()) {
+
+
+
+
+ } @else {
+
+
+
+ }
+
+
+
+
+
+
+
+ {{ formatTime(currentTime()) }} / {{ formatTime(duration()) }}
+
+
+
+ @if (showVolumeControl) {
+
+ @if (isMuted() || volume() === 0) {
+
+
+
+
+
+ } @else if (volume() < 0.5) {
+
+
+
+
+ } @else {
+
+
+
+
+ }
+
+
+
+ }
+
+
+ @if (allowFullscreen) {
+
+ @if (isFullscreen()) {
+
+
+
+ } @else {
+
+
+
+ }
+
+ }
+
+ }
+
+
+
+ @if (hasError()) {
+
+
+
+
+
+
+
+
+
{{ errorMessage() || 'Failed to load video' }}
+ @if (sources.length > 0) {
+
+ Download video
+
+ }
+
+ }
+
+ `,
+ styleUrl: './video-player.component.scss'
+})
+export class VideoPlayerComponent implements OnDestroy {
+ @ViewChild('videoElement') videoElement!: ElementRef;
+
+ @Input() sources: VideoSource[] = [];
+ @Input() tracks: VideoTrack[] = [];
+ @Input() poster?: string;
+ @Input() size: VideoPlayerSize = 'md';
+ @Input() variant: VideoPlayerVariant = 'default';
+ @Input() disabled = false;
+ @Input() autoplay = false;
+ @Input() loop = false;
+ @Input() muted = false;
+ @Input() playsinline = true;
+ @Input() preload: 'none' | 'metadata' | 'auto' = 'metadata';
+ @Input() showControls = true;
+ @Input() showVolumeControl = true;
+ @Input() allowFullscreen = true;
+ @Input() ariaLabel = 'Video player';
+ @Input() videoAriaLabel = 'Video content';
+
+ @Output() play = new EventEmitter();
+ @Output() pause = new EventEmitter();
+ @Output() ended = new EventEmitter();
+ @Output() timeUpdate = new EventEmitter();
+ @Output() volumeChange = new EventEmitter();
+ @Output() fullscreenChange = new EventEmitter();
+ @Output() error = new EventEmitter();
+
+ // Signals for reactive state
+ private _isPlaying = signal(false);
+ private _currentTime = signal(0);
+ private _duration = signal(0);
+ private _volume = signal(1);
+ private _isMuted = signal(false);
+ private _loading = signal(false);
+ private _hasError = signal(false);
+ private _errorMessage = signal('');
+ private _isFullscreen = signal(false);
+ private _controlsVisible = signal(true);
+
+ // Public readonly signals
+ isPlaying = this._isPlaying.asReadonly();
+ currentTime = this._currentTime.asReadonly();
+ duration = this._duration.asReadonly();
+ volume = this._volume.asReadonly();
+ isMuted = this._isMuted.asReadonly();
+ loading = this._loading.asReadonly();
+ hasError = this._hasError.asReadonly();
+ errorMessage = this._errorMessage.asReadonly();
+ isFullscreen = this._isFullscreen.asReadonly();
+ controlsVisible = this._controlsVisible.asReadonly();
+
+ // Computed values
+ progressPercentage = computed(() => {
+ const duration = this.duration();
+ const currentTime = this.currentTime();
+ return duration > 0 ? (currentTime / duration) * 100 : 0;
+ });
+
+ private controlsHideTimer?: number;
+
+ ngOnDestroy(): void {
+ if (this.controlsHideTimer) {
+ clearTimeout(this.controlsHideTimer);
+ }
+ }
+
+ // Video event handlers
+ handleLoadStart(): void {
+ this._loading.set(true);
+ this._hasError.set(false);
+ }
+
+ handleLoadedData(): void {
+ if (this.videoElement?.nativeElement) {
+ this._duration.set(this.videoElement.nativeElement.duration);
+ }
+ }
+
+ handleCanPlay(): void {
+ this._loading.set(false);
+ }
+
+ handlePlay(): void {
+ this._isPlaying.set(true);
+ this.play.emit();
+ this.hideControlsAfterDelay();
+ }
+
+ handlePause(): void {
+ this._isPlaying.set(false);
+ this.pause.emit();
+ this.showControlsOverlay();
+ }
+
+ handleEnded(): void {
+ this._isPlaying.set(false);
+ this.ended.emit();
+ this.showControlsOverlay();
+ }
+
+ handleTimeUpdate(): void {
+ if (this.videoElement?.nativeElement) {
+ const currentTime = this.videoElement.nativeElement.currentTime;
+ this._currentTime.set(currentTime);
+ this.timeUpdate.emit(currentTime);
+ }
+ }
+
+ handleVideoVolumeChange(): void {
+ if (this.videoElement?.nativeElement) {
+ const video = this.videoElement.nativeElement;
+ this._volume.set(video.volume);
+ this._isMuted.set(video.muted);
+ this.volumeChange.emit(video.volume);
+ }
+ }
+
+ handleError(event: Event): void {
+ this._loading.set(false);
+ this._hasError.set(true);
+ this._errorMessage.set('Failed to load video');
+ this.error.emit(event);
+ }
+
+ // Control methods
+ togglePlayPause(): void {
+ if (!this.videoElement?.nativeElement || this.disabled) return;
+
+ if (this.isPlaying()) {
+ this.videoElement.nativeElement.pause();
+ } else {
+ this.videoElement.nativeElement.play();
+ }
+ }
+
+ handleSeek(event: Event): void {
+ if (!this.videoElement?.nativeElement || this.disabled) return;
+
+ const target = event.target as HTMLInputElement;
+ const seekTime = parseFloat(target.value);
+ this.videoElement.nativeElement.currentTime = seekTime;
+ }
+
+ toggleMute(): void {
+ if (!this.videoElement?.nativeElement || this.disabled) return;
+
+ this.videoElement.nativeElement.muted = !this.videoElement.nativeElement.muted;
+ }
+
+ handleVolumeSliderChange(event: Event): void {
+ if (!this.videoElement?.nativeElement || this.disabled) return;
+
+ const target = event.target as HTMLInputElement;
+ const newVolume = parseFloat(target.value);
+ this.videoElement.nativeElement.volume = newVolume;
+ }
+
+ toggleFullscreen(): void {
+ if (!this.allowFullscreen || this.disabled) return;
+
+ if (!document.fullscreenElement) {
+ this.videoElement?.nativeElement.requestFullscreen().then(() => {
+ this._isFullscreen.set(true);
+ this.fullscreenChange.emit(true);
+ });
+ } else {
+ document.exitFullscreen().then(() => {
+ this._isFullscreen.set(false);
+ this.fullscreenChange.emit(false);
+ });
+ }
+ }
+
+ // Control visibility
+ private showControlsOverlay(): void {
+ this._controlsVisible.set(true);
+ if (this.controlsHideTimer) {
+ clearTimeout(this.controlsHideTimer);
+ }
+ }
+
+ private hideControlsAfterDelay(): void {
+ if (this.controlsHideTimer) {
+ clearTimeout(this.controlsHideTimer);
+ }
+
+ this.controlsHideTimer = window.setTimeout(() => {
+ if (this.isPlaying()) {
+ this._controlsVisible.set(false);
+ }
+ }, 3000);
+ }
+
+ // Utility methods
+ formatTime(seconds: number): string {
+ if (isNaN(seconds)) return '0:00';
+
+ const hours = Math.floor(seconds / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+ const secs = Math.floor(seconds % 60);
+
+ if (hours > 0) {
+ return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
+ }
+
+ return `${minutes}:${secs.toString().padStart(2, '0')}`;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/navigation/README.md b/projects/ui-essentials/src/lib/components/navigation/README.md
new file mode 100644
index 0000000..1ef3912
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/navigation/README.md
@@ -0,0 +1,223 @@
+# List Components
+
+Comprehensive list component system built with Angular 19+ and semantic design tokens. Features multiple variants, sizes, and interactive states for maximum flexibility.
+
+## Components
+
+### `ListItemComponent`
+Individual list item with support for avatars, media, icons, and multiple text lines.
+
+### `ListContainerComponent`
+Container for list items with elevation, spacing, and scrolling capabilities.
+
+### `ListExamplesComponent`
+Demonstration component showcasing all variants and usage patterns.
+
+## Features
+
+- **Multiple Sizes**: `sm`, `md`, `lg`
+- **Line Variants**: One, two, or three lines of text
+- **Content Types**: Text-only, avatar, media, icon
+- **Interactive States**: Hover, focus, selected, disabled
+- **Container Options**: Elevation, spacing, scrolling, rounded corners
+- **Text Overflow**: Automatic ellipsis handling
+- **Accessibility**: Full ARIA support and keyboard navigation
+- **Responsive**: Mobile-first design with breakpoint adjustments
+
+## Usage
+
+### Basic Text List
+
+```typescript
+import { ListItemComponent, ListContainerComponent } from './shared/components';
+
+// In your component:
+items = [
+ { primary: 'Inbox' },
+ { primary: 'Starred' },
+ { primary: 'Sent' }
+];
+```
+
+```html
+
+ @for (item of items; track item.primary) {
+
+ }
+
+```
+
+### Avatar List with Two Lines
+
+```typescript
+avatarItems = [
+ {
+ primary: 'John Doe',
+ secondary: 'Software Engineer',
+ avatarSrc: 'https://example.com/avatar.jpg',
+ avatarAlt: 'John Doe'
+ }
+];
+```
+
+```html
+
+ @for (item of avatarItems; track item.primary) {
+
+
+
+
+
+ }
+
+```
+
+### Media List with Three Lines
+
+```typescript
+mediaItems = [
+ {
+ primary: 'Angular Course',
+ secondary: 'Complete guide to modern development',
+ tertiary: 'Duration: 2h 30m • Updated: Today',
+ mediaSrc: 'https://example.com/thumb.jpg',
+ mediaAlt: 'Course thumbnail'
+ }
+];
+```
+
+```html
+
+ @for (item of mediaItems; track item.primary) {
+
+
+
+ 4.8
+
+
+ }
+
+```
+
+### Scrollable List
+
+```html
+
+ @for (item of longList; track item.id) {
+
+ }
+
+```
+
+## API
+
+### ListItemComponent Props
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `data` | `ListItemData` | required | Item content and configuration |
+| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Item height and spacing |
+| `lines` | `'one' \| 'two' \| 'three'` | `'one'` | Number of text lines |
+| `variant` | `'text' \| 'avatar' \| 'media'` | `'text'` | Content type |
+| `interactive` | `boolean` | `true` | Enable hover/focus states |
+| `divider` | `boolean` | `false` | Show bottom border |
+
+### ListContainerComponent Props
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `elevation` | `'none' \| 'sm' \| 'md' \| 'lg'` | `'none'` | Shadow elevation |
+| `spacing` | `'none' \| 'xs' \| 'sm' \| 'md' \| 'lg'` | `'sm'` | Item spacing |
+| `scrollable` | `boolean` | `false` | Enable vertical scrolling |
+| `maxHeight` | `string?` | - | Maximum container height |
+| `fadeIndicator` | `boolean` | `true` | Show fade at scroll bottom |
+| `dense` | `boolean` | `false` | Reduce spacing between items |
+| `rounded` | `boolean` | `false` | Round container corners |
+| `ariaLabel` | `string?` | - | Accessibility label |
+
+### ListItemData Interface
+
+```typescript
+interface ListItemData {
+ primary: string; // Main text (required)
+ secondary?: string; // Secondary text
+ tertiary?: string; // Tertiary text (three-line only)
+ avatarSrc?: string; // Avatar image URL
+ avatarAlt?: string; // Avatar alt text
+ mediaSrc?: string; // Media image URL
+ mediaAlt?: string; // Media alt text
+ icon?: string; // Icon class name
+ disabled?: boolean; // Disabled state
+ selected?: boolean; // Selected state
+}
+```
+
+## Slots
+
+### Trailing Content
+Use the `slot="trailing"` attribute to add content to the right side of list items:
+
+```html
+
+ Action
+
+```
+
+## Design Tokens
+
+The components use semantic design tokens for consistent styling:
+
+- **Colors**: `$semantic-color-*`
+- **Spacing**: `$semantic-spacing-*`
+- **Shadows**: `$semantic-shadow-*`
+- **Borders**: `$semantic-border-radius-*`
+
+## Accessibility
+
+- Full ARIA support with proper roles and labels
+- Keyboard navigation support
+- Focus management and indicators
+- Screen reader friendly
+- High contrast mode support
+- Reduced motion support
+
+## Responsive Design
+
+- Mobile-first approach
+- Breakpoint-specific adjustments
+- Touch-friendly tap targets
+- Optimized spacing for different screen sizes
+
+## Examples
+
+Import and use the `ListExamplesComponent` to see all variants in action:
+
+```typescript
+import { ListExamplesComponent } from './shared/components';
+```
+
+```html
+
+```
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/navigation/appbar/appbar.component.scss b/projects/ui-essentials/src/lib/components/navigation/appbar/appbar.component.scss
new file mode 100644
index 0000000..cd29899
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/navigation/appbar/appbar.component.scss
@@ -0,0 +1,224 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+/**
+ * ==========================================================================
+ * APPBAR COMPONENT STYLES
+ * ==========================================================================
+ * Material Design 3 inspired appbar component with design token integration.
+ * Supports multiple height variants, positioning options, and content slots.
+ * ==========================================================================
+ */
+
+
+// Tokens available globally via main application styles
+
+.ui-appbar {
+ display: flex;
+ flex-wrap: nowrap;
+ flex-direction: row;
+ align-items: center;
+ overflow: hidden;
+ position: relative;
+ box-sizing: border-box;
+ width: 100%;
+ z-index: $semantic-layer-appbar;
+ transition: all $semantic-duration-short $semantic-easing-standard;
+ background-color: $semantic-color-surface-primary;
+ color: $semantic-color-text-primary;
+ border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ padding-left: $semantic-spacing-component-lg;
+ padding-right: $semantic-spacing-component-lg;
+
+ // Variant heights
+ &[data-variant="compact"] {
+ height: $semantic-sizing-appbar-compact;
+ min-height: $semantic-sizing-appbar-compact;
+ }
+
+ &[data-variant="standard"] {
+ height: $semantic-sizing-appbar-standard;
+ min-height: $semantic-sizing-appbar-standard;
+ }
+
+ &[data-variant="large"] {
+ height: $semantic-sizing-appbar-large;
+ min-height: $semantic-sizing-appbar-large;
+ }
+
+ &[data-variant="prominent"] {
+ height: $semantic-sizing-appbar-prominent;
+ min-height: $semantic-sizing-appbar-prominent;
+ }
+
+ // Position variants
+ &[data-position="fixed"] {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ }
+
+ &[data-position="sticky"] {
+ position: sticky;
+ top: 0;
+ }
+
+ // Elevated state
+ &[data-elevated="true"] {
+ box-shadow: $semantic-shadow-elevation-2;
+ }
+
+ // Responsive adjustments
+ @media (max-width: calc($semantic-breakpoint-md - 1px)) {
+ padding-left: $semantic-spacing-component-md;
+ padding-right: $semantic-spacing-component-md;
+
+ &[data-variant="large"] {
+ height: $semantic-sizing-appbar-standard;
+ min-height: $semantic-sizing-appbar-standard;
+ }
+
+ &[data-variant="prominent"] {
+ height: $semantic-sizing-appbar-large;
+ min-height: $semantic-sizing-appbar-large;
+ }
+ }
+
+ @media (max-width: calc($semantic-breakpoint-sm - 1px)) {
+ &[data-variant="large"],
+ &[data-variant="prominent"] {
+ height: $semantic-sizing-appbar-compact;
+ min-height: $semantic-sizing-appbar-compact;
+ }
+ }
+}
+
+// Left side components
+.ui-appbar__left-icon {
+ flex: 0 0 $semantic-sizing-touch-target;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ user-select: none;
+ padding: $semantic-spacing-component-xs;
+}
+
+.ui-appbar__left-logo {
+ flex: 0 0 auto;
+ display: flex;
+ align-items: center;
+ padding: $semantic-spacing-component-xs;
+ user-select: none;
+ overflow: hidden;
+ box-sizing: border-box;
+
+ ::ng-deep img {
+ max-height: calc(100% - #{$semantic-spacing-component-sm});
+ width: auto;
+ object-fit: contain;
+ }
+}
+
+.ui-appbar__left-avatar {
+ flex: 0 0 $semantic-sizing-touch-target;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: $semantic-spacing-component-xs;
+ box-sizing: border-box;
+ overflow: hidden;
+}
+
+// Center title
+.ui-appbar__title {
+ flex: 1 1 auto;
+ display: flex;
+ align-items: center;
+ text-align: center;
+ font-size: $semantic-typography-font-size-lg;
+ font-weight: $semantic-typography-font-weight-medium;
+ line-height: $semantic-typography-line-height-normal;
+ padding: 0 $semantic-spacing-component-sm;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+ // Adjust for prominent variant
+ [data-variant="prominent"] & {
+ font-size: $semantic-typography-font-size-lg;
+ font-weight: $semantic-typography-font-weight-semibold;
+ line-height: $semantic-typography-line-height-normal;
+ }
+
+ // Responsive adjustments
+ @media (max-width: calc($semantic-breakpoint-sm - 1px)) {
+ font-size: $semantic-typography-font-size-md;
+ font-weight: $semantic-typography-font-weight-medium;
+ line-height: $semantic-typography-line-height-normal;
+ }
+}
+
+.ui-appbar__spacer {
+ flex: 1 1 auto;
+}
+
+// Right side components
+.ui-appbar__right-icon {
+ flex: 0 0 $semantic-sizing-touch-target;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ user-select: none;
+ padding: $semantic-spacing-component-xs;
+}
+
+.ui-appbar__right-logo {
+ flex: 0 0 auto;
+ display: flex;
+ align-items: center;
+ padding: $semantic-spacing-component-xs;
+ user-select: none;
+ overflow: hidden;
+ box-sizing: border-box;
+
+ ::ng-deep img {
+ max-height: calc(100% - #{$semantic-spacing-component-sm});
+ width: auto;
+ object-fit: contain;
+ }
+}
+
+.ui-appbar__right-avatar {
+ flex: 0 0 $semantic-sizing-touch-target;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: $semantic-spacing-component-xs;
+ box-sizing: border-box;
+ overflow: hidden;
+}
+
+.ui-appbar__right-menu {
+ flex: 0 0 auto;
+ display: flex;
+ align-items: center;
+ padding: $semantic-spacing-component-xs;
+ user-select: none;
+}
+
+// Icon styling for all slots
+.ui-appbar__left-icon,
+.ui-appbar__right-icon,
+.ui-appbar__right-menu {
+ ::ng-deep i,
+ ::ng-deep mat-icon,
+ ::ng-deep [class*="icon"] {
+ font-size: $semantic-typography-font-size-lg;
+ line-height: 1;
+ color: $semantic-color-text-secondary;
+ transition: color $semantic-duration-short $semantic-easing-standard;
+
+ &:hover {
+ color: $semantic-color-text-primary;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/navigation/appbar/appbar.component.ts b/projects/ui-essentials/src/lib/components/navigation/appbar/appbar.component.ts
new file mode 100644
index 0000000..0846e5e
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/navigation/appbar/appbar.component.ts
@@ -0,0 +1,108 @@
+import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+export type AppbarVariant = 'compact' | 'standard' | 'large' | 'prominent';
+export type AppbarPosition = 'static' | 'fixed' | 'sticky';
+
+export interface AppbarSlots {
+ leftIcon?: boolean;
+ leftLogo?: boolean;
+ leftAvatar?: boolean;
+ title?: boolean;
+ rightIcon?: boolean;
+ rightLogo?: boolean;
+ rightAvatar?: boolean;
+ rightMenu?: boolean;
+}
+
+@Component({
+ selector: 'ui-appbar',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ styleUrls: ['./appbar.component.scss'],
+ template: `
+
+ @if (slots.leftIcon) {
+
+
+
+ }
+
+ @if (slots.leftLogo) {
+
+
+
+ }
+
+ @if (slots.leftAvatar) {
+
+
+
+ }
+
+ @if (slots.title) {
+
+
+
+ }
+
+
+
+ @if (slots.rightIcon) {
+
+
+
+ }
+
+ @if (slots.rightLogo) {
+
+
+
+ }
+
+ @if (slots.rightAvatar) {
+
+
+
+ }
+
+ @if (slots.rightMenu) {
+
+ }
+
+ `
+})
+export class AppbarComponent {
+ @Input() variant: AppbarVariant = 'standard';
+ @Input() position: AppbarPosition = 'static';
+ @Input() elevated: boolean = false;
+ @Input() slots: AppbarSlots = {
+ leftIcon: false,
+ leftLogo: false,
+ leftAvatar: false,
+ title: true,
+ rightIcon: false,
+ rightLogo: false,
+ rightAvatar: false,
+ rightMenu: false
+ };
+
+ getAppbarClasses(): string {
+ const classes: string[] = [];
+
+ if (this.elevated) {
+ classes.push('ui-appbar--elevated');
+ }
+
+ return classes.join(' ');
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/navigation/appbar/index.ts b/projects/ui-essentials/src/lib/components/navigation/appbar/index.ts
new file mode 100644
index 0000000..3fbc045
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/navigation/appbar/index.ts
@@ -0,0 +1 @@
+export * from './appbar.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/navigation/breadcrumb/breadcrumb.component.scss b/projects/ui-essentials/src/lib/components/navigation/breadcrumb/breadcrumb.component.scss
new file mode 100644
index 0000000..26cd922
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/navigation/breadcrumb/breadcrumb.component.scss
@@ -0,0 +1,116 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+// Tokens available globally via main application styles
+
+.ui-breadcrumb {
+ display: flex;
+ align-items: center;
+ padding: $semantic-spacing-2 0; // 0.5rem vertical
+}
+
+.breadcrumb-list {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ gap: $semantic-spacing-1; // 0.25rem
+}
+
+.breadcrumb-item {
+ display: flex;
+ align-items: center;
+ gap: $semantic-spacing-2; // 0.5rem
+
+ &:not(:last-child) {
+ margin-right: $semantic-spacing-1; // 0.25rem
+ }
+}
+
+.breadcrumb-link {
+ font-family: $semantic-typography-font-family-sans;
+ font-size: $semantic-typography-font-size-sm;
+ font-weight: $semantic-typography-font-weight-normal;
+ color: $semantic-color-text-secondary;
+ text-decoration: none;
+ line-height: $semantic-typography-line-height-normal;
+ transition: color $semantic-motion-duration-fast ease;
+ border-radius: $semantic-border-radius-sm;
+ padding: $semantic-spacing-1 $semantic-spacing-1-5; // 0.25rem 0.375rem
+
+ &:hover {
+ color: $semantic-color-interactive-primary;
+ background-color: $semantic-color-surface-hover;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-interactive-primary;
+ outline-offset: 2px;
+ }
+
+ &:active {
+ transform: scale(0.98);
+ }
+}
+
+.breadcrumb-label {
+ font-family: $semantic-typography-font-family-sans;
+ font-size: $semantic-typography-font-size-sm;
+ font-weight: $semantic-typography-font-weight-normal;
+ color: $semantic-color-text-secondary;
+ line-height: $semantic-typography-line-height-normal;
+ padding: $semantic-spacing-1 $semantic-spacing-1-5; // 0.25rem 0.375rem
+
+ &--current {
+ color: $semantic-color-text-primary;
+ font-weight: $semantic-typography-font-weight-medium;
+ }
+
+ &--disabled {
+ opacity: $semantic-opacity-disabled;
+ cursor: not-allowed;
+ }
+}
+
+.breadcrumb-separator {
+ color: $semantic-color-text-tertiary;
+ font-size: $semantic-typography-font-size-xs;
+ opacity: $semantic-opacity-heavy;
+ margin: 0 $semantic-spacing-0-5; // 0 0.125rem
+
+ svg {
+ width: 12px;
+ height: 12px;
+ }
+}
+
+// Responsive design
+@media (max-width: 768px) {
+ .breadcrumb-list {
+ gap: $semantic-spacing-0-5; // 0.125rem
+ }
+
+ .breadcrumb-item {
+ gap: $semantic-spacing-1; // 0.25rem
+
+ &:not(:last-child) {
+ margin-right: $semantic-spacing-0-5; // 0.125rem
+ }
+ }
+
+ .breadcrumb-link,
+ .breadcrumb-label {
+ font-size: $semantic-typography-font-size-xs;
+ padding: $semantic-spacing-0-5 $semantic-spacing-1; // 0.125rem 0.25rem
+ }
+
+ .breadcrumb-separator {
+ margin: 0 $semantic-spacing-0-5; // 0 0.125rem
+
+ svg {
+ width: 10px;
+ height: 10px;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/navigation/breadcrumb/breadcrumb.component.ts b/projects/ui-essentials/src/lib/components/navigation/breadcrumb/breadcrumb.component.ts
new file mode 100644
index 0000000..b32cbc8
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/navigation/breadcrumb/breadcrumb.component.ts
@@ -0,0 +1,84 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { faChevronRight } from '@fortawesome/free-solid-svg-icons';
+
+export interface BreadcrumbItem {
+ label: string;
+ route?: string;
+ disabled?: boolean;
+}
+
+@Component({
+ selector: 'ui-breadcrumb',
+ standalone: true,
+ imports: [CommonModule, FontAwesomeModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
+ @for (item of items; track $index; let isLast = $last) {
+
+ @if (!isLast && item.route && !item.disabled) {
+
+ {{ item.label }}
+
+ } @else {
+
+ {{ item.label }}
+
+ }
+
+ @if (!isLast) {
+
+ }
+
+ }
+
+
+ `,
+ styleUrl: './breadcrumb.component.scss'
+})
+export class BreadcrumbComponent {
+ @Input() items: BreadcrumbItem[] = [];
+ @Input() separator: 'chevron' | 'slash' = 'chevron';
+
+ @Output() itemClick = new EventEmitter<{item: BreadcrumbItem, index: number}>();
+
+ separatorIcon = faChevronRight;
+
+ get wrapperClasses(): string {
+ return [
+ 'ui-breadcrumb'
+ ].filter(Boolean).join(' ');
+ }
+
+ getItemClasses(index: number, isLast: boolean): string {
+ return [
+ 'breadcrumb-item',
+ isLast ? 'breadcrumb-item--current' : ''
+ ].filter(Boolean).join(' ');
+ }
+
+ getLabelClasses(isLast: boolean, disabled?: boolean): string {
+ return [
+ 'breadcrumb-label',
+ isLast ? 'breadcrumb-label--current' : '',
+ disabled ? 'breadcrumb-label--disabled' : ''
+ ].filter(Boolean).join(' ');
+ }
+
+ onItemClick(event: Event, item: BreadcrumbItem, index: number): void {
+ event.preventDefault();
+ if (!item.disabled) {
+ this.itemClick.emit({ item, index });
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/navigation/breadcrumb/index.ts b/projects/ui-essentials/src/lib/components/navigation/breadcrumb/index.ts
new file mode 100644
index 0000000..614e29a
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/navigation/breadcrumb/index.ts
@@ -0,0 +1 @@
+export * from './breadcrumb.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/navigation/index.ts b/projects/ui-essentials/src/lib/components/navigation/index.ts
new file mode 100644
index 0000000..1d0b865
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/navigation/index.ts
@@ -0,0 +1,5 @@
+export * from './appbar';
+export * from './breadcrumb';
+export * from './menu';
+export * from './pagination';
+export * from './tab-group';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/navigation/menu/index.ts b/projects/ui-essentials/src/lib/components/navigation/menu/index.ts
new file mode 100644
index 0000000..88a3549
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/navigation/menu/index.ts
@@ -0,0 +1,3 @@
+export * from './menu-container.component';
+export * from './menu-item.component';
+export * from './menu-submenu.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/navigation/menu/menu-container.component.scss b/projects/ui-essentials/src/lib/components/navigation/menu/menu-container.component.scss
new file mode 100644
index 0000000..cdc770e
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/navigation/menu/menu-container.component.scss
@@ -0,0 +1,350 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+// Tokens available globally via main application styles
+
+// ==========================================================================
+// MENU CONTAINER COMPONENT
+// ==========================================================================
+// Container component for menu items with elevation, spacing, and orientation
+// Provides consistent layout and styling using semantic design tokens
+// ==========================================================================
+
+.menu-container {
+ display: flex;
+ width: 100%;
+ box-sizing: border-box;
+ background-color: $semantic-color-surface-primary;
+ position: relative;
+ overflow: hidden;
+
+ // ==========================================================================
+ // ORIENTATION
+ // ==========================================================================
+
+ &--vertical {
+ flex-direction: column;
+ }
+
+ &--horizontal {
+ flex-direction: row;
+ align-items: center;
+ flex-wrap: wrap;
+ }
+
+ // ==========================================================================
+ // ELEVATION VARIANTS
+ // ==========================================================================
+
+ &--elevation-none {
+ box-shadow: none;
+ }
+
+ &--elevation-sm {
+ box-shadow: $semantic-shadow-elevation-1;
+ }
+
+ &--elevation-md {
+ box-shadow: $semantic-shadow-elevation-3;
+ }
+
+ &--elevation-lg {
+ box-shadow: $semantic-shadow-elevation-4;
+ }
+
+ // ==========================================================================
+ // SPACING VARIANTS
+ // ==========================================================================
+
+ &--spacing-none {
+ padding: 0;
+ gap: 0;
+ }
+
+ &--spacing-xs {
+ padding: $semantic-spacing-component-xs;
+ gap: $semantic-spacing-component-xs;
+ }
+
+ &--spacing-sm {
+ padding: $semantic-spacing-component-sm;
+ gap: $semantic-spacing-micro-tight;
+ }
+
+ &--spacing-md {
+ padding: $semantic-spacing-component-md;
+ gap: $semantic-spacing-component-xs;
+ }
+
+ &--spacing-lg {
+ padding: $semantic-spacing-component-lg;
+ gap: $semantic-spacing-component-sm;
+ }
+
+ // ==========================================================================
+ // LAYOUT MODIFIERS
+ // ==========================================================================
+
+ &--scrollable {
+ overflow-y: auto;
+ -webkit-overflow-scrolling: touch;
+
+ // Custom scrollbar styling
+ &::-webkit-scrollbar {
+ width: 6px;
+ }
+
+ &::-webkit-scrollbar-track {
+ background: $semantic-color-surface-secondary;
+ border-radius: 3px;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: $semantic-color-border-primary;
+ border-radius: 3px;
+
+ &:hover {
+ background: $semantic-color-border-secondary;
+ }
+ }
+
+ // Firefox scrollbar
+ scrollbar-width: thin;
+ scrollbar-color: $semantic-color-border-primary $semantic-color-surface-secondary;
+ }
+
+ &--max-height {
+ height: var(--menu-max-height);
+ max-height: var(--menu-max-height);
+ }
+
+ &--dense {
+ &.menu-container--spacing-xs {
+ padding: $semantic-spacing-micro-tight;
+ gap: $semantic-spacing-micro-hairline;
+ }
+
+ &.menu-container--spacing-sm {
+ padding: $semantic-spacing-component-xs;
+ gap: $semantic-spacing-micro-tight;
+ }
+
+ &.menu-container--spacing-md {
+ padding: $semantic-spacing-component-sm;
+ gap: $semantic-spacing-micro-tight;
+ }
+
+ &.menu-container--spacing-lg {
+ padding: $semantic-spacing-component-md;
+ gap: $semantic-spacing-component-xs;
+ }
+ }
+
+ &--rounded {
+ border-radius: $semantic-border-radius-md;
+ overflow: hidden;
+ }
+
+ // ==========================================================================
+ // HORIZONTAL LAYOUT ADJUSTMENTS
+ // ==========================================================================
+
+ &--horizontal {
+ .menu-item {
+ flex-shrink: 0;
+
+ &:not(:last-child) {
+ margin-right: $semantic-spacing-component-xs;
+ }
+ }
+
+ &.menu-container--spacing-none .menu-item:not(:last-child) {
+ margin-right: 0;
+ }
+
+ &.menu-container--spacing-sm .menu-item:not(:last-child) {
+ margin-right: $semantic-spacing-component-sm;
+ }
+
+ &.menu-container--spacing-md .menu-item:not(:last-child) {
+ margin-right: $semantic-spacing-component-md;
+ }
+
+ &.menu-container--spacing-lg .menu-item:not(:last-child) {
+ margin-right: $semantic-spacing-component-lg;
+ }
+ }
+}
+
+// ==========================================================================
+// DYNAMIC HEIGHT SETUP
+// ==========================================================================
+
+.menu-container--max-height {
+ --menu-max-height: 400px;
+}
+
+// ==========================================================================
+// MENU DIVIDERS
+// ==========================================================================
+
+.menu-divider {
+ height: 1px;
+ background-color: $semantic-color-border-secondary;
+ margin: $semantic-spacing-component-xs 0;
+
+ .menu-container--spacing-none & {
+ margin: 0;
+ }
+
+ .menu-container--spacing-sm & {
+ margin: $semantic-spacing-component-sm $semantic-spacing-component-sm;
+ }
+
+ .menu-container--spacing-md & {
+ margin: $semantic-spacing-component-md $semantic-spacing-component-md;
+ }
+
+ .menu-container--spacing-lg & {
+ margin: $semantic-spacing-component-lg $semantic-spacing-component-lg;
+ }
+
+ .menu-container--horizontal & {
+ height: auto;
+ width: 1px;
+ margin: 0 $semantic-spacing-component-xs;
+ align-self: stretch;
+ }
+}
+
+// ==========================================================================
+// MENU SECTION HEADERS
+// ==========================================================================
+
+.menu-section-header {
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
+ font-size: $semantic-typography-font-size-xs;
+ font-weight: $semantic-typography-font-weight-semibold;
+ text-transform: uppercase;
+ letter-spacing: $semantic-typography-letter-spacing-wider;
+ color: $semantic-color-text-tertiary;
+ background-color: $semantic-color-surface-secondary;
+
+ .menu-container--spacing-sm & {
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ }
+
+ .menu-container--spacing-lg & {
+ padding: $semantic-spacing-component-md $semantic-spacing-component-lg;
+ }
+
+ .menu-container--horizontal & {
+ display: none; // Hide section headers in horizontal layout
+ }
+}
+
+// ==========================================================================
+// RESPONSIVE ADJUSTMENTS
+// ==========================================================================
+
+@media (max-width: 768px) {
+ .menu-container {
+ &--horizontal {
+ flex-direction: column;
+ align-items: stretch;
+
+ .menu-item {
+ margin-right: 0;
+
+ &:not(:last-child) {
+ margin-bottom: $semantic-spacing-component-xs;
+ }
+ }
+ }
+
+ &--spacing-md {
+ padding: $semantic-spacing-component-sm;
+ gap: $semantic-spacing-component-xs;
+ }
+
+ &--spacing-lg {
+ padding: $semantic-spacing-component-md;
+ gap: $semantic-spacing-component-sm;
+ }
+ }
+}
+
+// ==========================================================================
+// ACCESSIBILITY ENHANCEMENTS
+// ==========================================================================
+
+.menu-container {
+ &:focus-within {
+ outline: 2px solid $semantic-color-border-focus;
+ outline-offset: 2px;
+ }
+}
+
+// ==========================================================================
+// SUBMENU STYLES
+// ==========================================================================
+
+.menu-submenu {
+ background-color: $semantic-color-surface-elevated;
+ border: $semantic-border-width-1 solid $semantic-color-border-secondary;
+ border-radius: $semantic-border-radius-md;
+ box-shadow: $semantic-shadow-elevation-3;
+ overflow: hidden;
+ // Nested submenu indentation
+ .menu-item {
+ padding-left: calc($semantic-spacing-component-md + $semantic-spacing-component-lg);
+ }
+
+ .menu-container--spacing-sm & .menu-item {
+ padding-left: calc($semantic-spacing-component-sm + $semantic-spacing-component-md);
+ }
+
+ .menu-container--spacing-lg & .menu-item {
+ padding-left: calc($semantic-spacing-component-lg + $semantic-spacing-component-xl);
+ }
+}
+
+// ==========================================================================
+// ANIMATION UTILITIES
+// ==========================================================================
+
+.menu-container {
+ // Smooth height transitions when content changes
+ transition: height $semantic-motion-duration-fast $semantic-motion-easing-ease-in-out;
+}
+
+// Menu item enter/leave animations
+@keyframes menu-item-enter {
+ from {
+ opacity: 0;
+ transform: translateY(-4px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes menu-item-leave {
+ from {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ to {
+ opacity: 0;
+ transform: translateY(-4px);
+ }
+}
+
+// Add these classes via JavaScript for dynamic content
+.menu-item-enter {
+ animation: menu-item-enter $semantic-motion-duration-fast $semantic-motion-easing-ease-out;
+}
+
+.menu-item-leave {
+ animation: menu-item-leave $semantic-motion-duration-fast $semantic-motion-easing-ease-out;
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/navigation/menu/menu-container.component.ts b/projects/ui-essentials/src/lib/components/navigation/menu/menu-container.component.ts
new file mode 100644
index 0000000..ce308a3
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/navigation/menu/menu-container.component.ts
@@ -0,0 +1,49 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+export type MenuContainerElevation = 'none' | 'sm' | 'md' | 'lg';
+export type MenuContainerSpacing = 'none' | 'xs' | 'sm' | 'md' | 'lg';
+
+@Component({
+ selector: 'ui-menu-container',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [CommonModule],
+ template: `
+
+ `,
+ styleUrls: ['./menu-container.component.scss']
+})
+export class MenuContainerComponent {
+ @Input() elevation: MenuContainerElevation = 'sm';
+ @Input() spacing: MenuContainerSpacing = 'xs';
+ @Input() scrollable = false;
+ @Input() maxHeight?: string;
+ @Input() rounded = true;
+ @Input() dense = false;
+ @Input() orientation: 'vertical' | 'horizontal' = 'vertical';
+ @Input() ariaLabel?: string;
+
+ getContainerClasses(): string {
+ const classes = [
+ `menu-container--elevation-${this.elevation}`,
+ `menu-container--spacing-${this.spacing}`,
+ `menu-container--${this.orientation}`
+ ];
+
+ if (this.scrollable) classes.push('menu-container--scrollable');
+ if (this.dense) classes.push('menu-container--dense');
+ if (this.rounded) classes.push('menu-container--rounded');
+ if (this.maxHeight) classes.push('menu-container--max-height');
+
+ return classes.join(' ');
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/navigation/menu/menu-item.component.scss b/projects/ui-essentials/src/lib/components/navigation/menu/menu-item.component.scss
new file mode 100644
index 0000000..17cc40b
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/navigation/menu/menu-item.component.scss
@@ -0,0 +1,500 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+// Tokens available globally via main application styles
+
+// ==========================================================================
+// MENU ITEM COMPONENT
+// ==========================================================================
+// Comprehensive menu item component with multiple variants and sizes
+// Uses semantic design tokens for consistent styling across the design system
+// ==========================================================================
+
+.menu-item {
+ display: flex;
+ flex-direction: row; // Changed from column to row
+ align-items: center; // Add this to vertically center content
+ width: 100%;
+ box-sizing: border-box;
+ position: relative;
+ user-select: none;
+ background-color: $semantic-color-surface-primary;
+ color: $semantic-color-text-primary;
+ cursor: pointer;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease-in-out;
+ border-radius: $semantic-border-radius-sm;
+
+
+
+ &:hover:not(.menu-item--disabled) {
+ background-color: $semantic-color-surface-interactive;
+ }
+
+ &:focus-within:not(.menu-item--disabled) {
+// outline: 2px solid $semantic-color-border-focus;
+ // outline-offset: 2px;
+ }
+
+ // States
+ &--active {
+ background-color: $semantic-color-container-primary;
+ color: $semantic-color-on-container-primary;
+
+ &:hover {
+ background-color: $semantic-color-container-primary;
+ opacity: 0.9;
+ }
+ }
+
+ &--disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ color: $semantic-color-text-disabled;
+
+ &:hover {
+ background-color: $semantic-color-surface-primary;
+ }
+ }
+
+ // ==========================================================================
+ // SIZE VARIANTS
+ // ==========================================================================
+
+ &--sm {
+ min-height: 36px;
+
+ &.menu-item--dense {
+ min-height: 32px;
+ }
+ }
+
+ &--md {
+ min-height: 48px;
+
+ &.menu-item--dense {
+ min-height: 40px;
+ }
+ }
+
+ &--lg {
+ min-height: 56px;
+
+ &.menu-item--dense {
+ min-height: 48px;
+ }
+ }
+
+ // ==========================================================================
+ // VARIANT STYLES
+ // ==========================================================================
+
+ &--primary {
+ color: $semantic-color-interactive-primary;
+
+ &.menu-item--active {
+ background-color: $semantic-color-container-primary;
+ color: $semantic-color-on-container-primary;
+ }
+
+ &:hover:not(.menu-item--disabled):not(.menu-item--active) {
+ background-color: $semantic-color-surface-hover;
+ }
+ }
+
+ &--secondary {
+ color: $semantic-color-interactive-secondary;
+
+ &.menu-item--active {
+ background-color: $semantic-color-container-secondary;
+ color: $semantic-color-on-container-secondary;
+ }
+
+ &:hover:not(.menu-item--disabled):not(.menu-item--active) {
+ background-color: $semantic-color-surface-hover;
+ }
+ }
+
+ &--danger {
+ color: $semantic-color-danger;
+
+ &.menu-item--active {
+ background-color: $semantic-color-container-error;
+ color: $semantic-color-on-container-error;
+ }
+
+ &:hover:not(.menu-item--disabled):not(.menu-item--active) {
+ background-color: $semantic-color-surface-hover;
+ }
+ }
+
+ // ==========================================================================
+ // LAYOUT MODIFIERS
+ // ==========================================================================
+
+ &--indent {
+ margin-left: $semantic-spacing-component-xl;
+ }
+
+ &--with-divider {
+ margin-bottom: $semantic-spacing-component-xs;
+ }
+}
+
+// ==========================================================================
+// MENU ITEM LINK
+// ==========================================================================
+
+.menu-item__link {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ text-decoration: none;
+ color: inherit;
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
+
+ .menu-item--sm & {
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ }
+
+ .menu-item--lg & {
+ padding: $semantic-spacing-component-md $semantic-spacing-component-lg;
+ }
+
+ &:focus {
+ outline: none;
+ }
+}
+
+// ==========================================================================
+// MENU ITEM CONTENT (when no link)
+// ==========================================================================
+
+.menu-item:not(:has(.menu-item__link)) {
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
+
+ &.menu-item--sm {
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ }
+
+ &.menu-item--lg {
+ padding: $semantic-spacing-component-md $semantic-spacing-component-lg;
+ }
+}
+
+// FontAwesome icon content wrapper
+.menu-item:not(:has(.menu-item__link)) ng-container {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: flex-start;
+ width: 100%;
+ height: 100%;
+ min-height: inherit;
+}
+
+// ==========================================================================
+// ICON STYLES
+// ==========================================================================
+
+.menu-item__icon {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: $semantic-color-text-secondary;
+
+ &--left {
+ margin-right: $semantic-spacing-component-md;
+ align-self: center;
+ }
+
+ &--right {
+ margin-left: auto; // Push right icons to the end
+ align-self: center;
+ }
+
+ &--circle {
+ border-radius: 50%;
+ background-color: $semantic-color-surface-elevated;
+ border: 1px solid $semantic-color-border-secondary;
+
+ .menu-item--sm & {
+ width: 28px;
+ height: 28px;
+ }
+
+ .menu-item--md & {
+ width: 32px;
+ height: 32px;
+ }
+
+ .menu-item--lg & {
+ width: 36px;
+ height: 36px;
+ }
+ }
+
+ i {
+ .menu-item--sm & { font-size: 14px; }
+ .menu-item--md & { font-size: 16px; }
+ .menu-item--lg & { font-size: 18px; }
+
+ .menu-item__icon--circle & {
+ .menu-item--sm & { font-size: $semantic-typography-font-size-xs; }
+ .menu-item--md & { font-size: $semantic-typography-font-size-sm; }
+ .menu-item--lg & { font-size: $semantic-typography-font-size-md; }
+ }
+ }
+
+ // Variant specific icon colors
+ .menu-item--primary & {
+ color: $semantic-color-interactive-primary;
+ }
+
+ .menu-item--secondary & {
+ color: $semantic-color-interactive-secondary;
+ }
+
+ .menu-item--danger & {
+ color: $semantic-color-danger;
+ }
+
+ // Custom emoji-based icon mappings using ::before pseudo-elements
+ // These apply to the element when it has these specific classes
+ &.icon-dashboard::before { content: '📊'; }
+ &.icon-chart::before { content: '📈'; }
+ &.icon-projects::before { content: '📁'; }
+ &.icon-team::before { content: '👥'; }
+ &.icon-documents::before { content: '📄'; }
+ &.icon-demo::before { content: '🎨'; }
+ &.icon-button::before { content: '🔘'; }
+ &.icon-badge::before { content: '🏷️'; }
+ &.icon-avatar::before { content: '👤'; }
+ &.icon-table::before { content: '📊'; }
+ &.icon-appbar::before { content: '📱'; }
+ &.icon-home::before { content: '🏠'; }
+ &.icon-settings::before { content: '⚙️'; }
+ &.icon-help::before { content: '❓'; }
+ &.icon-overview::before { content: '👁️'; }
+ &.icon-reports::before { content: '📋'; }
+ &.icon-insights::before { content: '💡'; }
+ &.icon-members::before { content: '👤'; }
+ &.icon-roles::before { content: '🏷️'; }
+ &.icon-chevron-left::before { content: '‹'; }
+ &.icon-chevron-double-left::before { content: '«'; }
+ &.icon-chevron-right::before { content: '›'; }
+
+ // FontAwesome component support
+ fa-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: currentColor;
+
+ .menu-item--sm & {
+ font-size: 14px;
+ width: 14px;
+ height: 14px;
+ }
+ .menu-item--md & {
+ font-size: 16px;
+ width: 16px;
+ height: 16px;
+ }
+ .menu-item--lg & {
+ font-size: 18px;
+ width: 18px;
+ height: 18px;
+ }
+
+ .menu-item__icon--circle & {
+ .menu-item--sm & {
+ font-size: 12px;
+ width: 12px;
+ height: 12px;
+ }
+ .menu-item--md & {
+ font-size: 14px;
+ width: 14px;
+ height: 14px;
+ }
+ .menu-item--lg & {
+ font-size: 16px;
+ width: 16px;
+ height: 16px;
+ }
+ }
+ }
+}
+
+// ==========================================================================
+// CONTENT AREA
+// ==========================================================================
+
+.menu-item__content {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ gap: $semantic-spacing-component-sm;
+ width: 100%;
+}
+
+.menu-item__label {
+ font-weight: 500;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ .menu-item--sm & {
+ font-size: $semantic-typography-font-size-sm;
+ }
+
+ .menu-item--md & {
+ font-size: $semantic-typography-font-size-md;
+ }
+
+ .menu-item--lg & {
+ font-size: $semantic-typography-font-size-lg;
+ }
+}
+
+.menu-item__badge {
+ flex-shrink: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 20px;
+ height: 20px;
+ padding: 0 6px;
+ margin-left: auto; /* Push badge to the right */
+ background-color: $semantic-color-interactive-primary;
+ color: $semantic-color-on-brand-primary;
+ border-radius: $semantic-border-radius-full;
+ font-size: $semantic-typography-font-size-xs;
+ font-weight: 600;
+ line-height: 1;
+
+ .menu-item--sm & {
+ min-width: 16px;
+ height: 16px;
+ padding: 0 4px;
+ font-size: $semantic-typography-font-size-xs;
+ }
+
+ .menu-item--lg & {
+ min-width: 24px;
+ height: 24px;
+ padding: 0 8px;
+ font-size: $semantic-typography-font-size-sm;
+ }
+}
+
+// ==========================================================================
+// SUBMENU CARET
+// ==========================================================================
+
+.menu-item__caret {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-left: $semantic-spacing-component-sm;
+ color: $semantic-color-text-tertiary;
+ transition: transform $semantic-motion-duration-normal $semantic-motion-easing-ease-in-out;
+
+ i {
+ .menu-item--sm & { font-size: $semantic-typography-font-size-xs; }
+ .menu-item--md & { font-size: $semantic-typography-font-size-sm; }
+ .menu-item--lg & { font-size: $semantic-typography-font-size-md; }
+ }
+
+ fa-icon {
+ .menu-item--sm & { font-size: $semantic-typography-font-size-xs; }
+ .menu-item--md & { font-size: $semantic-typography-font-size-sm; }
+ .menu-item--lg & { font-size: $semantic-typography-font-size-md; }
+ }
+
+ .menu-item--has-submenu.menu-item--active & {
+ transform: rotate(90deg);
+ }
+}
+
+// ==========================================================================
+// DIVIDER
+// ==========================================================================
+
+.menu-item__divider {
+ position: absolute;
+ bottom: 0;
+ left: $semantic-spacing-component-md;
+ right: $semantic-spacing-component-md;
+ height: 1px;
+ background-color: $semantic-color-border-secondary;
+
+ .menu-item--sm & {
+ left: $semantic-spacing-component-sm;
+ right: $semantic-spacing-component-sm;
+ }
+
+ .menu-item--lg & {
+ left: $semantic-spacing-component-lg;
+ right: $semantic-spacing-component-lg;
+ }
+}
+
+// ==========================================================================
+// RESPONSIVE ADJUSTMENTS
+// ==========================================================================
+
+@media (max-width: 768px) {
+ .menu-item {
+ &--lg {
+ min-height: 48px;
+
+ &.menu-item--dense {
+ min-height: 40px;
+ }
+ }
+ }
+
+ .menu-item__content {
+ padding-left: $semantic-spacing-component-sm;
+ padding-right: $semantic-spacing-component-sm;
+ }
+}
+
+// ==========================================================================
+// ACCESSIBILITY ENHANCEMENTS
+// ==========================================================================
+
+.menu-item {
+ &:focus-visible {
+ outline: 2px solid $semantic-color-border-focus;
+ outline-offset: 2px;
+ }
+}
+
+// High contrast mode support
+@media (prefers-contrast: high) {
+ .menu-item {
+ border: 1px solid transparent;
+
+ &:hover:not(.menu-item--disabled) {
+ border-color: $semantic-color-border-primary;
+ }
+
+ &--active {
+ border-color: $semantic-color-interactive-primary;
+ }
+ }
+}
+
+// Reduced motion support
+@media (prefers-reduced-motion: reduce) {
+ .menu-item,
+ .menu-item__caret {
+ transition: none;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/navigation/menu/menu-item.component.ts b/projects/ui-essentials/src/lib/components/navigation/menu/menu-item.component.ts
new file mode 100644
index 0000000..8457d41
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/navigation/menu/menu-item.component.ts
@@ -0,0 +1,135 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FaIconComponent } from '@fortawesome/angular-fontawesome';
+import { faChevronRight } from '@fortawesome/free-solid-svg-icons';
+
+export type MenuItemSize = 'sm' | 'md' | 'lg';
+export type MenuItemIconPosition = 'left' | 'right';
+export type MenuItemVariant = 'default' | 'primary' | 'secondary' | 'danger';
+
+export interface MenuItemData {
+ id?: string;
+ label: string;
+ icon?: any; // FontAwesome icon definition
+ iconCircle?: boolean;
+ disabled?: boolean;
+ active?: boolean;
+ hasSubmenu?: boolean;
+ badge?: string | number;
+ href?: string;
+ target?: string;
+}
+
+@Component({
+ selector: 'ui-menu-item',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [CommonModule, FaIconComponent],
+ template: `
+
+ `,
+ styleUrls: ['./menu-item.component.scss']
+})
+export class MenuItemComponent {
+ @Input() data!: MenuItemData;
+ @Input() size: MenuItemSize = 'md';
+ @Input() variant: MenuItemVariant = 'default';
+ @Input() iconPosition: MenuItemIconPosition = 'left';
+ @Input() showDivider = false;
+ @Input() indent = false;
+ @Input() dense = false;
+
+ @Output() itemClick = new EventEmitter();
+ @Output() submenuToggle = new EventEmitter();
+
+ // FontAwesome icons
+ faChevronRight = faChevronRight;
+
+ getItemClasses(): string {
+ const classes = [
+ `menu-item--${this.size}`,
+ `menu-item--${this.variant}`
+ ];
+
+ if (this.dense) classes.push('menu-item--dense');
+ if (this.indent) classes.push('menu-item--indent');
+ if (this.data.disabled) classes.push('menu-item--disabled');
+ if (this.data.active) classes.push('menu-item--active');
+ if (this.data.hasSubmenu) classes.push('menu-item--has-submenu');
+ if (this.showDivider) classes.push('menu-item--with-divider');
+
+ return classes.join(' ');
+ }
+
+ handleClick(event: Event): void {
+ if (this.data.disabled) {
+ event.preventDefault();
+ return;
+ }
+
+ if (this.data.hasSubmenu) {
+ event.preventDefault();
+ this.submenuToggle.emit(this.data);
+ } else if (!this.data.href) {
+ this.itemClick.emit(this.data);
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/navigation/menu/menu-submenu.component.scss b/projects/ui-essentials/src/lib/components/navigation/menu/menu-submenu.component.scss
new file mode 100644
index 0000000..025df2b
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/navigation/menu/menu-submenu.component.scss
@@ -0,0 +1,110 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+// Tokens available globally via main application styles
+
+// ==========================================================================
+// MENU SUBMENU COMPONENT
+// ==========================================================================
+// Collapsible submenu component with smooth animations
+// ==========================================================================
+
+.submenu-wrapper {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+}
+
+.submenu-content {
+ overflow: hidden;
+ transition: max-height $semantic-motion-duration-slow $semantic-motion-easing-ease-in-out, opacity $semantic-motion-duration-normal $semantic-motion-easing-ease-in-out;
+ // Apply indentation to the entire submenu content block
+ margin-left: calc($semantic-spacing-component-lg + $semantic-spacing-component-md);
+
+ .menu-submenu {
+ //border-left: 2px solid $semantic-color-border-secondary;
+ border-radius: 0;
+ background-color: $semantic-color-surface-elevated;
+ box-shadow: none;
+ position: relative;
+ }
+}
+
+// ==========================================================================
+// ANIMATION STATES
+// ==========================================================================
+
+.submenu-wrapper--open {
+ .submenu-content {
+ max-height: 1000px; // Large enough to accommodate content
+ opacity: 1;
+ }
+}
+
+.submenu-wrapper:not(.submenu-wrapper--open) {
+ .submenu-content {
+ max-height: 0;
+ opacity: 0;
+ }
+}
+
+// ==========================================================================
+// NESTED SUBMENU STYLING
+// ==========================================================================
+
+.submenu-content {
+ .menu-item {
+ position: relative;
+
+ &::before {
+ content: '';
+ position: absolute;
+ left: -2px;
+ top: 50%;
+ width: 8px;
+ height: 1px;
+ background-color: $semantic-color-border-secondary;
+ transform: translateY(-50%);
+ }
+ }
+}
+
+// Force vertical layout for all submenus
+.menu-submenu {
+ // Override any inherited horizontal layout
+ .menu-container {
+ flex-direction: column !important;
+ align-items: stretch !important;
+ flex-wrap: nowrap !important;
+ }
+
+ // Ensure menu items stack vertically and take full width
+ .menu-item {
+ width: 100% !important;
+ margin-right: 0 !important;
+ margin-bottom: 0;
+
+ &:not(:last-child) {
+ margin-bottom: 0 !important;
+ }
+ }
+}
+
+// ==========================================================================
+// RESPONSIVE ADJUSTMENTS
+// ==========================================================================
+
+@media (max-width: 768px) {
+ .submenu-content {
+ margin-left: calc($semantic-spacing-component-md + $semantic-spacing-component-sm);
+ }
+}
+
+// ==========================================================================
+// REDUCED MOTION SUPPORT
+// ==========================================================================
+
+@media (prefers-reduced-motion: reduce) {
+ .submenu-content {
+ transition: none;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/navigation/menu/menu-submenu.component.ts b/projects/ui-essentials/src/lib/components/navigation/menu/menu-submenu.component.ts
new file mode 100644
index 0000000..223a467
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/navigation/menu/menu-submenu.component.ts
@@ -0,0 +1,68 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { MenuItemComponent, MenuItemData } from './menu-item.component';
+import { MenuContainerComponent } from './menu-container.component';
+
+@Component({
+ selector: 'ui-menu-submenu',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [CommonModule, MenuItemComponent, MenuContainerComponent],
+ template: `
+
+ `,
+ styleUrls: ['./menu-submenu.component.scss'],
+ animations: []
+})
+export class MenuSubmenuComponent {
+ @Input() parentItem!: MenuItemData;
+ @Input() size: 'sm' | 'md' | 'lg' = 'md';
+ @Input() variant: 'default' | 'primary' | 'secondary' | 'danger' = 'default';
+ @Input() iconPosition: 'left' | 'right' = 'left';
+ @Input() showDivider = false;
+ @Input() indent = false;
+ @Input() dense = false;
+ @Input() isOpen = false;
+ @Input() submenuElevation: 'none' | 'sm' | 'md' | 'lg' = 'none';
+ @Input() submenuSpacing: 'none' | 'xs' | 'sm' | 'md' | 'lg' = 'xs';
+
+ @Output() submenuToggled = new EventEmitter<{ item: MenuItemData; isOpen: boolean }>();
+ @Output() parentItemClick = new EventEmitter();
+
+ toggleSubmenu(item: MenuItemData): void {
+ this.isOpen = !this.isOpen;
+ this.submenuToggled.emit({ item, isOpen: this.isOpen });
+ }
+
+ onParentClick(item: MenuItemData): void {
+ this.parentItemClick.emit(item);
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/navigation/pagination/index.ts b/projects/ui-essentials/src/lib/components/navigation/pagination/index.ts
new file mode 100644
index 0000000..03e034e
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/navigation/pagination/index.ts
@@ -0,0 +1 @@
+export * from './pagination.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/navigation/pagination/pagination.component.scss b/projects/ui-essentials/src/lib/components/navigation/pagination/pagination.component.scss
new file mode 100644
index 0000000..47bd52f
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/navigation/pagination/pagination.component.scss
@@ -0,0 +1,233 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+.ui-pagination {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: $semantic-spacing-2 0;
+
+ &--left {
+ justify-content: flex-start;
+ }
+
+ &--right {
+ justify-content: flex-end;
+ }
+
+ &--sm {
+ padding: $semantic-spacing-1 0;
+
+ .pagination-list {
+ gap: $semantic-spacing-1;
+ }
+
+ .pagination-item {
+ min-width: $semantic-sizing-button-height-sm;
+ height: $semantic-sizing-button-height-sm;
+ font-size: $semantic-typography-font-size-xs;
+ }
+ }
+
+ &--md {
+ .pagination-list {
+ gap: $semantic-spacing-1-5;
+ }
+
+ .pagination-item {
+ min-width: $semantic-sizing-button-height-md;
+ height: $semantic-sizing-button-height-md;
+ font-size: $semantic-typography-font-size-sm;
+ }
+ }
+
+ &--lg {
+ padding: $semantic-spacing-3 0;
+
+ .pagination-list {
+ gap: $semantic-spacing-2;
+ }
+
+ .pagination-item {
+ min-width: $semantic-sizing-button-height-lg;
+ height: $semantic-sizing-button-height-lg;
+ font-size: $semantic-typography-font-size-md;
+ }
+ }
+}
+
+.pagination-list {
+ display: flex;
+ align-items: center;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ gap: $semantic-spacing-1-5;
+}
+
+.pagination-item {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-width: $semantic-sizing-button-height-md;
+ height: $semantic-sizing-button-height-md;
+ border-radius: $semantic-border-radius-md;
+ font-family: $semantic-typography-font-family-sans;
+ font-size: $semantic-typography-font-size-sm;
+ font-weight: $semantic-typography-font-weight-medium;
+ line-height: $semantic-typography-line-height-none;
+ text-decoration: none;
+ cursor: pointer;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
+ user-select: none;
+
+ // Default state
+ background: $semantic-color-surface-primary;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ color: $semantic-color-text-primary;
+
+ &:hover:not(.pagination-item--disabled):not(.pagination-item--current) {
+ background: $semantic-color-surface-hover;
+ border-color: $semantic-color-border-focus;
+ transform: translateY(-1px);
+ box-shadow: $semantic-shadow-elevation-2;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-interactive-primary;
+ outline-offset: 2px;
+ }
+
+ &:active:not(.pagination-item--disabled):not(.pagination-item--current) {
+ transform: translateY(0);
+ box-shadow: $semantic-shadow-elevation-1;
+ }
+
+ // Current page state
+ &--current {
+ background: $semantic-color-interactive-primary;
+ border-color: $semantic-color-interactive-primary;
+ color: $semantic-color-on-primary;
+ cursor: default;
+
+ &:hover {
+ background: $semantic-color-interactive-primary;
+ border-color: $semantic-color-interactive-primary;
+ transform: none;
+ }
+ }
+
+ // Disabled state
+ &--disabled {
+ opacity: $semantic-opacity-disabled;
+ cursor: not-allowed;
+ pointer-events: none;
+ }
+
+ // Ellipsis state
+ &--ellipsis {
+ cursor: default;
+ pointer-events: none;
+ border: none;
+ background: transparent;
+ color: $semantic-color-text-tertiary;
+ font-weight: $semantic-typography-font-weight-normal;
+ }
+
+ // Navigation buttons (prev/next)
+ &--nav {
+ padding: 0 $semantic-spacing-2;
+ min-width: auto;
+ gap: $semantic-spacing-1;
+
+ &:hover:not(.pagination-item--disabled) {
+ color: $semantic-color-interactive-primary;
+ }
+ }
+
+ // Icon sizing
+ fa-icon {
+ font-size: inherit;
+ }
+}
+
+.pagination-info {
+ display: flex;
+ align-items: center;
+ margin-left: $semantic-spacing-4;
+ font-family: $semantic-typography-font-family-sans;
+ font-size: $semantic-typography-font-size-sm;
+ color: $semantic-color-text-secondary;
+ white-space: nowrap;
+
+ &--sm {
+ margin-left: $semantic-spacing-2;
+ font-size: $semantic-typography-font-size-xs;
+ }
+
+ &--lg {
+ margin-left: $semantic-spacing-6;
+ font-size: $semantic-typography-font-size-md;
+ }
+}
+
+// Compact variant
+.ui-pagination--compact {
+ .pagination-list {
+ gap: $semantic-spacing-1;
+ }
+
+ .pagination-item {
+ min-width: $semantic-sizing-button-height-sm;
+ height: $semantic-sizing-button-height-sm;
+ font-size: $semantic-typography-font-size-xs;
+ border-radius: $semantic-border-radius-sm;
+
+ &--nav {
+ padding: 0 $semantic-spacing-1-5;
+ }
+ }
+
+ .pagination-info {
+ margin-left: $semantic-spacing-2;
+ font-size: $semantic-typography-font-size-xs;
+ }
+}
+
+// Responsive design
+@media (max-width: 768px) {
+ .ui-pagination {
+ padding: $semantic-spacing-1 0;
+
+ .pagination-list {
+ gap: $semantic-spacing-1;
+ }
+
+ .pagination-item {
+ min-width: $semantic-sizing-button-height-sm;
+ height: $semantic-sizing-button-height-sm;
+ font-size: $semantic-typography-font-size-xs;
+
+ &--nav {
+ padding: 0 $semantic-spacing-1-5;
+ gap: $semantic-spacing-0-5;
+ }
+ }
+
+ .pagination-info {
+ margin-left: $semantic-spacing-2;
+ font-size: $semantic-typography-font-size-xs;
+
+ // Hide detailed info on very small screens
+ @media (max-width: 480px) {
+ display: none;
+ }
+ }
+ }
+
+ // Show only essential pages on mobile
+ .pagination-item--hide-mobile {
+ display: none;
+ }
+}
+
+// Dark mode support will be added in future versions
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/navigation/pagination/pagination.component.ts b/projects/ui-essentials/src/lib/components/navigation/pagination/pagination.component.ts
new file mode 100644
index 0000000..4d053e9
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/navigation/pagination/pagination.component.ts
@@ -0,0 +1,244 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, computed, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { faChevronLeft, faChevronRight, faEllipsisH } from '@fortawesome/free-solid-svg-icons';
+
+export interface PageChangeEvent {
+ page: number;
+ previousPage: number;
+}
+
+type PaginationSize = 'sm' | 'md' | 'lg';
+type PaginationAlignment = 'left' | 'center' | 'right';
+
+@Component({
+ selector: 'ui-pagination',
+ standalone: true,
+ imports: [CommonModule, FontAwesomeModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
+
+
+ @if (showInfo) {
+
+ {{ getInfoText() }}
+
+ }
+
+ `,
+ styleUrl: './pagination.component.scss'
+})
+export class PaginationComponent {
+ @Input() currentPage = signal(1);
+ @Input() totalPages = signal(1);
+ @Input() totalItems?: number;
+ @Input() itemsPerPage?: number;
+ @Input() size: PaginationSize = 'md';
+ @Input() alignment: PaginationAlignment = 'center';
+ @Input() compact = false;
+ @Input() showInfo = true;
+ @Input() showLabels = false;
+ @Input() maxVisible = 7;
+ @Input() ariaLabel = 'Pagination navigation';
+ @Input() previousLabel = 'Previous';
+ @Input() nextLabel = 'Next';
+ @Input() previousAriaLabel = 'Go to previous page';
+ @Input() nextAriaLabel = 'Go to next page';
+ @Input() disabled = false;
+
+ @Output() pageChange = new EventEmitter();
+
+ // Icons
+ prevIcon = faChevronLeft;
+ nextIcon = faChevronRight;
+ ellipsisIcon = faEllipsisH;
+
+ // Computed properties using signals
+ hasPrevious = computed(() => this.currentPage() > 1 && !this.disabled);
+ hasNext = computed(() => this.currentPage() < this.totalPages() && !this.disabled);
+
+ visiblePages = computed(() => {
+ const current = this.currentPage();
+ const total = this.totalPages();
+ const max = this.maxVisible;
+
+ if (total <= max) {
+ // Show all pages if total is within max visible
+ return Array.from({ length: total }, (_, i) => ({
+ value: i + 1,
+ display: (i + 1).toString(),
+ type: 'page' as const
+ }));
+ }
+
+ const pages: Array<{ value: number; display: string; type: 'page' | 'ellipsis' }> = [];
+ const sidePages = Math.floor((max - 3) / 2); // Reserve space for first, last, and ellipses
+
+ // Always show first page
+ pages.push({ value: 1, display: '1', type: 'page' });
+
+ if (current <= sidePages + 2) {
+ // Current is near the beginning
+ for (let i = 2; i <= Math.min(max - 1, total - 1); i++) {
+ pages.push({ value: i, display: i.toString(), type: 'page' });
+ }
+ if (total > max - 1) {
+ pages.push({ value: -1, display: '...', type: 'ellipsis' });
+ }
+ } else if (current >= total - sidePages - 1) {
+ // Current is near the end
+ if (total > max - 1) {
+ pages.push({ value: -1, display: '...', type: 'ellipsis' });
+ }
+ for (let i = Math.max(total - max + 2, 2); i <= total - 1; i++) {
+ pages.push({ value: i, display: i.toString(), type: 'page' });
+ }
+ } else {
+ // Current is in the middle
+ pages.push({ value: -1, display: '...', type: 'ellipsis' });
+ const start = Math.max(2, current - sidePages);
+ const end = Math.min(total - 1, current + sidePages);
+ for (let i = start; i <= end; i++) {
+ pages.push({ value: i, display: i.toString(), type: 'page' });
+ }
+ pages.push({ value: -2, display: '...', type: 'ellipsis' });
+ }
+
+ // Always show last page if more than 1 page
+ if (total > 1) {
+ pages.push({ value: total, display: total.toString(), type: 'page' });
+ }
+
+ return pages;
+ });
+
+ wrapperClasses = computed(() => {
+ return [
+ 'ui-pagination',
+ `ui-pagination--${this.size}`,
+ `ui-pagination--${this.alignment}`,
+ this.compact ? 'ui-pagination--compact' : '',
+ this.disabled ? 'ui-pagination--disabled' : ''
+ ].filter(Boolean).join(' ');
+ });
+
+ getItemClasses(isCurrent: boolean, isEllipsis: boolean, isDisabled: boolean): string {
+ return [
+ 'pagination-item',
+ isCurrent ? 'pagination-item--current' : '',
+ isEllipsis ? 'pagination-item--ellipsis' : '',
+ isDisabled ? 'pagination-item--disabled' : '',
+ !isCurrent && !isEllipsis ? 'pagination-item--nav' : ''
+ ].filter(Boolean).join(' ');
+ }
+
+ getInfoClasses(): string {
+ return [
+ 'pagination-info',
+ `pagination-info--${this.size}`
+ ].filter(Boolean).join(' ');
+ }
+
+ getPageAriaLabel(page: number): string {
+ return page === this.currentPage()
+ ? `Page ${page}, current page`
+ : `Go to page ${page}`;
+ }
+
+ getInfoText(): string {
+ const current = this.currentPage();
+ const total = this.totalPages();
+
+ if (this.totalItems && this.itemsPerPage) {
+ const startItem = (current - 1) * this.itemsPerPage + 1;
+ const endItem = Math.min(current * this.itemsPerPage, this.totalItems);
+ return `${startItem}–${endItem} of ${this.totalItems} items`;
+ }
+
+ return `Page ${current} of ${total}`;
+ }
+
+ goToPage(page: number): void {
+ if (page !== this.currentPage() && page >= 1 && page <= this.totalPages() && !this.disabled) {
+ const previousPage = this.currentPage();
+ this.currentPage.set(page);
+ this.pageChange.emit({ page, previousPage });
+ }
+ }
+
+ goToPrevious(): void {
+ if (this.hasPrevious()) {
+ this.goToPage(this.currentPage() - 1);
+ }
+ }
+
+ goToNext(): void {
+ if (this.hasNext()) {
+ this.goToPage(this.currentPage() + 1);
+ }
+ }
+
+ // Public methods for programmatic navigation
+ first(): void {
+ this.goToPage(1);
+ }
+
+ last(): void {
+ this.goToPage(this.totalPages());
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/navigation/tab-group/index.ts b/projects/ui-essentials/src/lib/components/navigation/tab-group/index.ts
new file mode 100644
index 0000000..20851d2
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/navigation/tab-group/index.ts
@@ -0,0 +1 @@
+export * from './tab-group.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/navigation/tab-group/tab-group.component.scss b/projects/ui-essentials/src/lib/components/navigation/tab-group/tab-group.component.scss
new file mode 100644
index 0000000..b28fa2a
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/navigation/tab-group/tab-group.component.scss
@@ -0,0 +1,245 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+// Tokens available globally via main application styles
+
+.ui-tab-group {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+
+ &--full-width .tab-list {
+ width: 100%;
+
+ .tab-button {
+ flex: 1;
+ }
+ }
+}
+
+.tab-list {
+ display: flex;
+ align-items: center;
+ position: relative;
+
+ // Line variant (default)
+ .ui-tab-group--line & {
+ border-bottom: $semantic-border-width-1 solid $semantic-color-border-primary;
+ gap: 0;
+ }
+
+ // Pills variant
+ .ui-tab-group--pills & {
+ background-color: $semantic-color-surface-elevated;
+ padding: $semantic-spacing-1; // 0.25rem
+ border-radius: $semantic-border-radius-lg;
+ gap: $semantic-spacing-1; // 0.25rem
+ }
+
+ // Cards variant
+ .ui-tab-group--cards & {
+ gap: $semantic-spacing-1; // 0.25rem
+ border-bottom: $semantic-border-width-1 solid $semantic-color-border-primary;
+ }
+}
+
+.tab-button {
+ display: flex;
+ align-items: center;
+ gap: $semantic-spacing-2; // 0.5rem
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ font-family: $semantic-typography-font-family-sans;
+ font-weight: $semantic-typography-font-weight-medium;
+ color: $semantic-color-text-secondary;
+ transition: all $semantic-motion-duration-normal $semantic-motion-easing-ease-out;
+ position: relative;
+ outline: none;
+
+ // Size variants
+ .ui-tab-group--small & {
+ padding: $semantic-spacing-2 $semantic-spacing-3; // 0.5rem 0.75rem
+ font-size: $semantic-typography-font-size-sm;
+ line-height: $semantic-typography-line-height-tight;
+ min-height: $semantic-spacing-8; // 2rem
+ }
+
+ .ui-tab-group--medium & {
+ padding: $semantic-spacing-3 $semantic-spacing-4; // 0.75rem 1rem
+ font-size: $semantic-typography-font-size-md;
+ line-height: $semantic-typography-line-height-normal;
+ min-height: $semantic-spacing-10; // 2.5rem
+ }
+
+ .ui-tab-group--large & {
+ padding: $semantic-spacing-4 $semantic-spacing-6; // 1rem 1.5rem
+ font-size: $semantic-typography-font-size-lg;
+ line-height: $semantic-typography-line-height-normal;
+ min-height: $semantic-spacing-12; // 3rem
+ }
+
+ // Line variant styling
+ .ui-tab-group--line & {
+ border-radius: 0;
+ margin-bottom: -1px;
+
+ &::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 2px;
+ background-color: transparent;
+ transition: background-color $semantic-motion-duration-normal $semantic-motion-easing-ease;
+ }
+
+ &:hover:not(:disabled) {
+ color: $semantic-color-interactive-primary;
+ background-color: $semantic-color-surface-hover;
+ }
+
+ &--active {
+ color: $semantic-color-interactive-primary;
+
+ &::after {
+ background-color: $semantic-color-interactive-primary;
+ }
+ }
+ }
+
+ // Pills variant styling
+ .ui-tab-group--pills & {
+ border-radius: $semantic-border-radius-md;
+
+ &:hover:not(:disabled) {
+ background-color: $semantic-color-surface-hover;
+ color: $semantic-color-text-primary;
+ }
+
+ &--active {
+ background-color: $semantic-color-surface-primary;
+ color: $semantic-color-interactive-primary;
+ box-shadow: $semantic-shadow-button-hover;
+ }
+ }
+
+ // Cards variant styling
+ .ui-tab-group--cards & {
+ border: $semantic-border-width-1 solid transparent;
+ border-radius: $semantic-border-radius-md $semantic-border-radius-md 0 0;
+ margin-bottom: -1px;
+
+ &:hover:not(:disabled) {
+ background-color: $semantic-color-surface-hover;
+ color: $semantic-color-text-primary;
+ }
+
+ &--active {
+ background-color: $semantic-color-surface-primary;
+ color: $semantic-color-interactive-primary;
+ border-color: $semantic-color-border-primary;
+ border-bottom-color: $semantic-color-surface-primary;
+ }
+ }
+
+ // Focus state
+ &:focus-visible {
+ outline: 2px solid $semantic-color-interactive-primary;
+ outline-offset: 2px;
+ }
+
+ // Disabled state
+ &:disabled,
+ &--disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ pointer-events: none;
+ }
+}
+
+.tab-icon {
+ flex-shrink: 0;
+
+ .ui-tab-group--small & {
+ font-size: $semantic-typography-font-size-xs;
+ }
+
+ .ui-tab-group--medium & {
+ font-size: $semantic-typography-font-size-sm;
+ }
+
+ .ui-tab-group--large & {
+ font-size: $semantic-typography-font-size-md;
+ }
+}
+
+.tab-label {
+ white-space: nowrap;
+ user-select: none;
+}
+
+.tab-badge {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-width: $semantic-spacing-5; // 1.25rem
+ height: $semantic-spacing-5; // 1.25rem
+ background-color: $semantic-color-interactive-primary;
+ color: $semantic-color-on-brand-primary;
+ border-radius: $semantic-border-radius-full;
+ font-size: $semantic-typography-font-size-xs;
+ font-weight: $semantic-typography-font-weight-semibold;
+ line-height: 1;
+ padding: 0 $semantic-spacing-1; // 0 0.25rem
+
+ .ui-tab-group--small & {
+ min-width: $semantic-spacing-4; // 1rem
+ height: $semantic-spacing-4; // 1rem
+ font-size: $semantic-typography-font-size-xs;
+ }
+
+ .ui-tab-group--large & {
+ min-width: $semantic-spacing-6; // 1.5rem
+ height: $semantic-spacing-6; // 1.5rem
+ font-size: $semantic-typography-font-size-sm;
+ }
+}
+
+.tab-panels {
+ padding-top: $semantic-spacing-4; // 1rem
+
+ .ui-tab-group--pills & {
+ padding-top: $semantic-spacing-6; // 1.5rem
+ }
+}
+
+// Responsive design
+@media (max-width: 768px) {
+ .tab-list {
+ overflow-x: auto;
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+ }
+
+ .tab-button {
+ white-space: nowrap;
+ flex-shrink: 0;
+
+ .ui-tab-group--small & {
+ padding: $semantic-spacing-1-5 $semantic-spacing-2-5; // 0.375rem 0.625rem
+ }
+
+ .ui-tab-group--medium & {
+ padding: $semantic-spacing-2 $semantic-spacing-3; // 0.5rem 0.75rem
+ }
+
+ .ui-tab-group--large & {
+ padding: $semantic-spacing-3 $semantic-spacing-4; // 0.75rem 1rem
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/navigation/tab-group/tab-group.component.ts b/projects/ui-essentials/src/lib/components/navigation/tab-group/tab-group.component.ts
new file mode 100644
index 0000000..81388bb
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/navigation/tab-group/tab-group.component.ts
@@ -0,0 +1,98 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+
+export interface TabItem {
+ id: string;
+ label: string;
+ icon?: IconDefinition;
+ disabled?: boolean;
+ badge?: string | number;
+}
+
+export type TabVariant = 'line' | 'pills' | 'cards';
+export type TabSize = 'small' | 'medium' | 'large';
+
+@Component({
+ selector: 'ui-tab-group',
+ standalone: true,
+ imports: [CommonModule, FontAwesomeModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
+ @for (tab of tabs; track tab.id) {
+
+ @if (tab.icon) {
+
+ }
+ {{ tab.label }}
+ @if (tab.badge) {
+ {{ tab.badge }}
+ }
+
+ }
+
+
+
+
+
+
+ `,
+ styleUrl: './tab-group.component.scss'
+})
+export class TabGroupComponent {
+ @Input() tabs: TabItem[] = [];
+ @Input() variant: TabVariant = 'line';
+ @Input() size: TabSize = 'medium';
+ @Input() fullWidth: boolean = false;
+
+ @Output() tabChange = new EventEmitter();
+
+ activeTabId = signal('');
+
+ ngOnInit(): void {
+ // Set the first non-disabled tab as active by default
+ if (this.tabs.length > 0 && !this.activeTabId()) {
+ const firstEnabledTab = this.tabs.find(tab => !tab.disabled);
+ if (firstEnabledTab) {
+ this.activeTabId.set(firstEnabledTab.id);
+ }
+ }
+ }
+
+ get wrapperClasses(): string {
+ return [
+ 'ui-tab-group',
+ `ui-tab-group--${this.variant}`,
+ `ui-tab-group--${this.size}`,
+ this.fullWidth ? 'ui-tab-group--full-width' : ''
+ ].filter(Boolean).join(' ');
+ }
+
+ getTabClasses(tab: TabItem): string {
+ return [
+ 'tab-button',
+ this.activeTabId() === tab.id ? 'tab-button--active' : '',
+ tab.disabled ? 'tab-button--disabled' : ''
+ ].filter(Boolean).join(' ');
+ }
+
+ selectTab(tabId: string): void {
+ const tab = this.tabs.find(t => t.id === tabId);
+ if (tab && !tab.disabled) {
+ this.activeTabId.set(tabId);
+ this.tabChange.emit(tabId);
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/overlays/backdrop/backdrop.component.scss b/projects/ui-essentials/src/lib/components/overlays/backdrop/backdrop.component.scss
new file mode 100644
index 0000000..592143e
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/overlays/backdrop/backdrop.component.scss
@@ -0,0 +1,166 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+.ui-backdrop {
+ // Core Structure
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: var(--backdrop-z-index, 1000);
+
+ // Visual Design
+ background: rgba(0, 0, 0, 0.5);
+ opacity: 0;
+
+ // Transitions
+ transition: opacity $semantic-motion-duration-normal $semantic-motion-easing-ease;
+
+ // Pointer Events
+ pointer-events: none;
+
+ // Backdrop Variants
+ &--default {
+ background: rgba(0, 0, 0, 0.5);
+ }
+
+ &--blur {
+ backdrop-filter: blur(4px);
+ -webkit-backdrop-filter: blur(4px);
+ }
+
+ &--dark {
+ background: rgba(0, 0, 0, 0.6);
+ }
+
+ &--light {
+ background: rgba(255, 255, 255, 0.6);
+ }
+
+ // State Variants
+ &--visible {
+ pointer-events: auto;
+ opacity: var(--backdrop-opacity, 0.5);
+ }
+
+ &--clickable {
+ cursor: pointer;
+
+ &:hover {
+ opacity: calc(var(--backdrop-opacity, 0.5) + 0.05);
+ }
+
+ &:active {
+ opacity: calc(var(--backdrop-opacity, 0.5) + 0.1);
+ }
+ }
+
+ // Animation States
+ &--entering {
+ opacity: 0;
+ animation: backdrop-fade-in $semantic-motion-duration-normal $semantic-motion-easing-ease forwards;
+ }
+
+ &--leaving {
+ opacity: var(--backdrop-opacity, 0.5);
+ animation: backdrop-fade-out $semantic-motion-duration-normal $semantic-motion-easing-ease forwards;
+ }
+
+ // Content Positioning
+ &__content {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: $semantic-spacing-layout-md;
+ }
+
+ // Dark Mode Support
+ :host-context(.dark-theme) & {
+ &--default {
+ background: rgba(0, 0, 0, 0.7);
+ }
+
+ &--light {
+ background: rgba(255, 255, 255, 0.1);
+ }
+ }
+
+ // High Contrast Mode
+ @media (prefers-contrast: high) {
+ &--default {
+ background: rgba(0, 0, 0, 0.8);
+ }
+
+ &--light {
+ background: rgba(255, 255, 255, 0.8);
+ }
+ }
+
+ // Reduced Motion Support
+ @media (prefers-reduced-motion: reduce) {
+ transition: none;
+ animation: none;
+
+ &--entering,
+ &--leaving {
+ animation: none;
+ }
+ }
+
+ // Responsive Design
+ @media (max-width: 768px - 1) {
+ &__content {
+ padding: $semantic-spacing-layout-sm;
+ }
+ }
+
+ @media (max-width: 576px - 1) {
+ &__content {
+ padding: $semantic-spacing-layout-xs;
+ }
+ }
+}
+
+// Keyframe Animations
+@keyframes backdrop-fade-in {
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: var(--backdrop-opacity, 0.5);
+ }
+}
+
+@keyframes backdrop-fade-out {
+ from {
+ opacity: var(--backdrop-opacity, 0.5);
+ }
+
+ to {
+ opacity: 0;
+ }
+}
+
+// Z-index Management Utilities
+.ui-backdrop {
+ // Common z-index levels
+ &--z-index-modal {
+ z-index: 1050;
+ }
+
+ &--z-index-drawer {
+ z-index: 1040;
+ }
+
+ &--z-index-popover {
+ z-index: 1030;
+ }
+
+ &--z-index-tooltip {
+ z-index: 1020;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/overlays/backdrop/backdrop.component.ts b/projects/ui-essentials/src/lib/components/overlays/backdrop/backdrop.component.ts
new file mode 100644
index 0000000..2ec1744
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/overlays/backdrop/backdrop.component.ts
@@ -0,0 +1,182 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { signal, computed } from '@angular/core';
+
+type BackdropVariant = 'default' | 'blur' | 'dark' | 'light';
+
+export interface BackdropConfig {
+ variant?: BackdropVariant;
+ visible?: boolean;
+ clickable?: boolean;
+ blur?: boolean;
+ opacity?: number;
+ zIndex?: number;
+ preventBodyScroll?: boolean;
+}
+
+@Component({
+ selector: 'ui-backdrop',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ template: `
+ @if (isVisible()) {
+
+
+
+ }
+ `,
+ styleUrl: './backdrop.component.scss'
+})
+export class BackdropComponent {
+ // Inputs
+ @Input() variant: BackdropVariant = 'default';
+ @Input() clickable = true;
+ @Input() blur = false;
+ @Input() opacity = 0.5;
+ @Input() zIndex = 1000;
+ @Input() preventBodyScroll = false;
+
+ // 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() clicked = new EventEmitter();
+ @Output() shown = new EventEmitter();
+ @Output() hidden = new EventEmitter();
+
+ // Internal state signals
+ private _visible = signal(false);
+ private _entering = signal(false);
+ private _leaving = signal(false);
+
+ // Computed signals
+ readonly isVisible = computed(() => this._visible() || this._entering() || this._leaving());
+ readonly isEntering = computed(() => this._entering());
+ readonly isLeaving = computed(() => this._leaving());
+
+ // Internal properties
+ private originalBodyOverflow = '';
+
+ /**
+ * Shows the backdrop with animation
+ */
+ show(): void {
+ if (this._visible()) return;
+
+ if (this.preventBodyScroll) {
+ this.preventBodyScrollAction();
+ }
+
+ this._visible.set(true);
+ this._entering.set(true);
+ this.visibleChange.emit(true);
+
+ // Animation timing
+ setTimeout(() => {
+ this._entering.set(false);
+ this.shown.emit();
+ }, 200);
+ }
+
+ /**
+ * Hides the backdrop with animation
+ */
+ hide(): void {
+ if (!this._visible()) return;
+
+ this._leaving.set(true);
+
+ // Animation timing
+ setTimeout(() => {
+ this._visible.set(false);
+ this._leaving.set(false);
+ this.restoreBodyScroll();
+ this.visibleChange.emit(false);
+ this.hidden.emit();
+ }, 200);
+ }
+
+ /**
+ * Toggles backdrop visibility
+ */
+ toggle(): void {
+ if (this._visible()) {
+ this.hide();
+ } else {
+ this.show();
+ }
+ }
+
+ /**
+ * Handles click events
+ */
+ handleClick(event: MouseEvent): void {
+ if (this.clickable) {
+ this.clicked.emit(event);
+ }
+ }
+
+ /**
+ * Handles animation end events
+ */
+ handleAnimationEnd(event: AnimationEvent): void {
+ // Additional animation handling can be added here if needed
+ event.stopPropagation();
+ }
+
+ /**
+ * Prevents body scrolling
+ */
+ private preventBodyScrollAction(): void {
+ const body = document.body;
+ this.originalBodyOverflow = body.style.overflow;
+ body.style.overflow = 'hidden';
+ }
+
+ /**
+ * Restores body scrolling
+ */
+ private restoreBodyScroll(): void {
+ if (this.preventBodyScroll) {
+ const body = document.body;
+ body.style.overflow = this.originalBodyOverflow;
+ }
+ }
+
+ /**
+ * Public API method to apply configuration
+ */
+ configure(config: BackdropConfig): void {
+ Object.assign(this, config);
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/overlays/backdrop/index.ts b/projects/ui-essentials/src/lib/components/overlays/backdrop/index.ts
new file mode 100644
index 0000000..1efe750
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/overlays/backdrop/index.ts
@@ -0,0 +1 @@
+export * from './backdrop.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/overlays/drawer/drawer.component.scss b/projects/ui-essentials/src/lib/components/overlays/drawer/drawer.component.scss
new file mode 100644
index 0000000..e559f6b
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/overlays/drawer/drawer.component.scss
@@ -0,0 +1,458 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+.ui-drawer {
+ // Drawer Backdrop
+ &__backdrop {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.5);
+ z-index: $semantic-z-index-overlay;
+ opacity: 0;
+ transition: opacity $semantic-motion-duration-normal $semantic-motion-easing-ease;
+ backdrop-filter: blur(2px);
+
+ &--visible {
+ opacity: 1;
+ }
+
+ &--entering {
+ animation: drawer-backdrop-fade-in $semantic-motion-duration-normal $semantic-motion-easing-ease;
+ }
+
+ &--leaving {
+ animation: drawer-backdrop-fade-out $semantic-motion-duration-normal $semantic-motion-easing-ease;
+ }
+ }
+
+ // Drawer Container
+ &__container {
+ position: fixed;
+ background: $semantic-color-surface-primary;
+ box-shadow: $semantic-shadow-elevation-4;
+ z-index: $semantic-z-index-modal;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ transform: translateX(-100%);
+ transition: transform $semantic-motion-duration-normal $semantic-motion-easing-ease;
+
+ &--visible {
+ transform: translateX(0);
+ }
+
+ &--entering {
+ animation: drawer-slide-in-left $semantic-motion-duration-normal $semantic-motion-easing-ease;
+ }
+
+ &--leaving {
+ animation: drawer-slide-out-left $semantic-motion-duration-normal $semantic-motion-easing-ease;
+ }
+ }
+
+ // Position Variants
+ &--left {
+ .ui-drawer__container {
+ top: 0;
+ left: 0;
+ height: 100vh;
+ border-right: 1px solid $semantic-color-outline;
+ transform: translateX(-100%);
+
+ &--entering {
+ animation: drawer-slide-in-left $semantic-motion-duration-normal $semantic-motion-easing-ease;
+ }
+
+ &--leaving {
+ animation: drawer-slide-out-left $semantic-motion-duration-normal $semantic-motion-easing-ease;
+ }
+ }
+ }
+
+ &--right {
+ .ui-drawer__container {
+ top: 0;
+ right: 0;
+ height: 100vh;
+ border-left: 1px solid $semantic-color-outline;
+ transform: translateX(100%);
+
+ &--visible {
+ transform: translateX(0);
+ }
+
+ &--entering {
+ animation: drawer-slide-in-right $semantic-motion-duration-normal $semantic-motion-easing-ease;
+ }
+
+ &--leaving {
+ animation: drawer-slide-out-right $semantic-motion-duration-normal $semantic-motion-easing-ease;
+ }
+ }
+ }
+
+ &--top {
+ .ui-drawer__container {
+ top: 0;
+ left: 0;
+ width: 100vw;
+ border-bottom: 1px solid $semantic-color-outline;
+ transform: translateY(-100%);
+
+ &--visible {
+ transform: translateY(0);
+ }
+
+ &--entering {
+ animation: drawer-slide-in-top $semantic-motion-duration-normal $semantic-motion-easing-ease;
+ }
+
+ &--leaving {
+ animation: drawer-slide-out-top $semantic-motion-duration-normal $semantic-motion-easing-ease;
+ }
+ }
+ }
+
+ &--bottom {
+ .ui-drawer__container {
+ bottom: 0;
+ left: 0;
+ width: 100vw;
+ border-top: 1px solid $semantic-color-outline;
+ transform: translateY(100%);
+
+ &--visible {
+ transform: translateY(0);
+ }
+
+ &--entering {
+ animation: drawer-slide-in-bottom $semantic-motion-duration-normal $semantic-motion-easing-ease;
+ }
+
+ &--leaving {
+ animation: drawer-slide-out-bottom $semantic-motion-duration-normal $semantic-motion-easing-ease;
+ }
+ }
+ }
+
+ // Size Variants
+ &--sm {
+ .ui-drawer__container {
+ width: 280px;
+ }
+
+ &.ui-drawer--top .ui-drawer__container,
+ &.ui-drawer--bottom .ui-drawer__container {
+ height: 200px;
+ }
+ }
+
+ &--md {
+ .ui-drawer__container {
+ width: 360px;
+ }
+
+ &.ui-drawer--top .ui-drawer__container,
+ &.ui-drawer--bottom .ui-drawer__container {
+ height: 300px;
+ }
+ }
+
+ &--lg {
+ .ui-drawer__container {
+ width: 480px;
+ }
+
+ &.ui-drawer--top .ui-drawer__container,
+ &.ui-drawer--bottom .ui-drawer__container {
+ height: 400px;
+ }
+ }
+
+ &--xl {
+ .ui-drawer__container {
+ width: 640px;
+ }
+
+ &.ui-drawer--top .ui-drawer__container,
+ &.ui-drawer--bottom .ui-drawer__container {
+ height: 500px;
+ }
+ }
+
+ // Drawer Header
+ &__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: $semantic-spacing-component-lg;
+ border-bottom: 1px solid $semantic-color-outline;
+ background: $semantic-color-surface-primary;
+ flex-shrink: 0;
+
+ &--no-border {
+ border-bottom: none;
+ }
+ }
+
+ &__title {
+ font-size: $semantic-typography-heading-h4-size;
+ font-weight: $semantic-typography-font-weight-semibold;
+ color: $semantic-color-on-surface;
+ margin: 0;
+ line-height: 1.4;
+ }
+
+ &__close-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 40px;
+ height: 40px;
+ border: none;
+ background: transparent;
+ border-radius: $semantic-border-radius-md;
+ color: $semantic-color-on-surface-variant;
+ cursor: pointer;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
+
+ &:hover {
+ background: $semantic-color-surface-variant;
+ color: $semantic-color-on-surface;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-primary;
+ outline-offset: 2px;
+ }
+
+ &:active {
+ background: $semantic-color-surface-container;
+ }
+ }
+
+ // Drawer Body
+ &__body {
+ padding: $semantic-spacing-component-lg;
+ overflow-y: auto;
+ flex: 1;
+
+ &--no-padding {
+ padding: 0;
+ }
+
+ &--scrollable {
+ overflow-y: auto;
+ }
+ }
+
+ &__content {
+ color: $semantic-color-on-surface;
+ font-size: $semantic-typography-font-size-md;
+ line-height: 1.6;
+ }
+
+ // Drawer Footer
+ &__footer {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: $semantic-spacing-component-sm;
+ padding: $semantic-spacing-component-lg;
+ border-top: 1px solid $semantic-color-outline;
+ background: $semantic-color-surface-primary;
+ flex-shrink: 0;
+
+ &--no-border {
+ border-top: none;
+ }
+
+ &--center {
+ justify-content: center;
+ }
+
+ &--start {
+ justify-content: flex-start;
+ }
+
+ &--between {
+ justify-content: space-between;
+ }
+ }
+
+ // State Variants
+ &--loading {
+ .ui-drawer__body {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 200px;
+ }
+ }
+
+ &--persistent {
+ .ui-drawer__backdrop {
+ display: none;
+ }
+ }
+
+ // Loading Spinner
+ &__loader {
+ display: inline-block;
+ width: 24px;
+ height: 24px;
+ border: 2px solid $semantic-color-outline;
+ border-radius: 50%;
+ border-top-color: $semantic-color-primary;
+ animation: drawer-spin 1s linear infinite;
+ }
+
+ // Dark Mode Support
+ :host-context(.dark-theme) & {
+ &__backdrop {
+ background: rgba(0, 0, 0, 0.7);
+ }
+ }
+
+ // Responsive Design
+ @media (max-width: 768px) {
+ &--sm .ui-drawer__container,
+ &--md .ui-drawer__container,
+ &--lg .ui-drawer__container,
+ &--xl .ui-drawer__container {
+ width: 85vw;
+ max-width: 400px;
+ }
+
+ &__header {
+ padding: $semantic-spacing-component-md;
+ }
+
+ &__body {
+ padding: $semantic-spacing-component-md;
+ }
+
+ &__footer {
+ padding: $semantic-spacing-component-md;
+ flex-direction: column;
+ gap: $semantic-spacing-component-xs;
+
+ .ui-button {
+ width: 100%;
+ }
+ }
+
+ &__title {
+ font-size: $semantic-typography-heading-h5-size;
+ }
+ }
+
+ @media (max-width: 480px) {
+ &--sm .ui-drawer__container,
+ &--md .ui-drawer__container,
+ &--lg .ui-drawer__container,
+ &--xl .ui-drawer__container {
+ width: 100vw;
+ max-width: none;
+ }
+ }
+}
+
+// Animations
+@keyframes drawer-backdrop-fade-in {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes drawer-backdrop-fade-out {
+ from {
+ opacity: 1;
+ }
+ to {
+ opacity: 0;
+ }
+}
+
+@keyframes drawer-slide-in-left {
+ from {
+ transform: translateX(-100%);
+ }
+ to {
+ transform: translateX(0);
+ }
+}
+
+@keyframes drawer-slide-out-left {
+ from {
+ transform: translateX(0);
+ }
+ to {
+ transform: translateX(-100%);
+ }
+}
+
+@keyframes drawer-slide-in-right {
+ from {
+ transform: translateX(100%);
+ }
+ to {
+ transform: translateX(0);
+ }
+}
+
+@keyframes drawer-slide-out-right {
+ from {
+ transform: translateX(0);
+ }
+ to {
+ transform: translateX(100%);
+ }
+}
+
+@keyframes drawer-slide-in-top {
+ from {
+ transform: translateY(-100%);
+ }
+ to {
+ transform: translateY(0);
+ }
+}
+
+@keyframes drawer-slide-out-top {
+ from {
+ transform: translateY(0);
+ }
+ to {
+ transform: translateY(-100%);
+ }
+}
+
+@keyframes drawer-slide-in-bottom {
+ from {
+ transform: translateY(100%);
+ }
+ to {
+ transform: translateY(0);
+ }
+}
+
+@keyframes drawer-slide-out-bottom {
+ from {
+ transform: translateY(0);
+ }
+ to {
+ transform: translateY(100%);
+ }
+}
+
+@keyframes drawer-spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/overlays/drawer/drawer.component.ts b/projects/ui-essentials/src/lib/components/overlays/drawer/drawer.component.ts
new file mode 100644
index 0000000..169b191
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/overlays/drawer/drawer.component.ts
@@ -0,0 +1,404 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, OnInit, OnDestroy, inject, ElementRef, HostListener } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { faXmark } from '@fortawesome/free-solid-svg-icons';
+import { signal, computed } from '@angular/core';
+
+type DrawerSize = 'sm' | 'md' | 'lg' | 'xl';
+type DrawerPosition = 'left' | 'right' | 'top' | 'bottom';
+type FooterAlignment = 'start' | 'center' | 'end' | 'between';
+
+export interface DrawerConfig {
+ size?: DrawerSize;
+ position?: DrawerPosition;
+ closable?: boolean;
+ backdropClosable?: boolean;
+ escapeClosable?: boolean;
+ showHeader?: boolean;
+ showFooter?: boolean;
+ headerBorder?: boolean;
+ footerBorder?: boolean;
+ bodyPadding?: boolean;
+ bodyScrollable?: boolean;
+ footerAlignment?: FooterAlignment;
+ preventBodyScroll?: boolean;
+ focusTrap?: boolean;
+ persistent?: boolean;
+}
+
+@Component({
+ selector: 'ui-drawer',
+ standalone: true,
+ imports: [CommonModule, FontAwesomeModule],
+ changeDetection: ChangeDetectionStrategy.Default,
+ encapsulation: ViewEncapsulation.None,
+ template: `
+ @if (isVisible()) {
+
+
+ @if (!persistent) {
+
+ }
+
+
+
+
+ @if (showHeader) {
+
+ }
+
+
+
+ @if (loading) {
+
+ } @else {
+
+
+
+ }
+
+
+
+ @if (showFooter) {
+
+ }
+
+
+ }
+ `,
+ styleUrl: './drawer.component.scss'
+})
+export class DrawerComponent implements OnInit, OnDestroy {
+ // Dependencies
+ private elementRef = inject(ElementRef);
+
+ // Icons
+ readonly faXmark = faXmark;
+
+ // Inputs
+ @Input() size: DrawerSize = 'md';
+ @Input() position: DrawerPosition = 'left';
+ @Input() title = '';
+ @Input() loading = false;
+ @Input() closable = true;
+ @Input() backdropClosable = true;
+ @Input() escapeClosable = true;
+ @Input() showHeader = true;
+ @Input() showFooter = false;
+ @Input() headerBorder = true;
+ @Input() footerBorder = true;
+ @Input() bodyPadding = true;
+ @Input() bodyScrollable = true;
+ @Input() footerAlignment: FooterAlignment = 'end';
+ @Input() preventBodyScroll = true;
+ @Input() focusTrap = true;
+ @Input() persistent = false;
+ @Input() closeAriaLabel = 'Close drawer';
+
+ // State Inputs
+ @Input() set open(value: boolean) {
+ if (value !== this._open()) {
+ if (value) {
+ this.show();
+ } else {
+ this.hide();
+ }
+ }
+ }
+
+ get open(): boolean {
+ return this._open();
+ }
+
+ // Outputs
+ @Output() openChange = new EventEmitter();
+ @Output() opened = new EventEmitter();
+ @Output() closed = new EventEmitter();
+ @Output() backdropClicked = new EventEmitter();
+ @Output() escapePressed = new EventEmitter();
+
+ // Internal state signals
+ private _open = signal(false);
+ private _entering = signal(false);
+ private _leaving = signal(false);
+
+ // Computed signals
+ readonly isVisible = computed(() => this._open() || this._entering() || this._leaving());
+ readonly isEntering = computed(() => this._entering());
+ readonly isLeaving = computed(() => this._leaving());
+
+ // Internal properties
+ private originalBodyOverflow = '';
+ private originalBodyPaddingRight = '';
+ private focusableElements: HTMLElement[] = [];
+ private previousActiveElement: Element | null = null;
+ private currentFocusIndex = 0;
+
+ // Unique IDs for accessibility
+ readonly titleId = `drawer-title-${Math.random().toString(36).substr(2, 9)}`;
+ readonly contentId = `drawer-content-${Math.random().toString(36).substr(2, 9)}`;
+
+ ngOnInit(): void {
+ if (this.open) {
+ this.show();
+ }
+ }
+
+ ngOnDestroy(): void {
+ this.restoreBodyScroll();
+ this.restoreFocus();
+ }
+
+ /**
+ * Shows the drawer with animation
+ */
+ show(): void {
+ if (this._open()) return;
+
+ if (this.preventBodyScroll && !this.persistent) {
+ this.preventBodyScrollAction();
+ }
+ this.storePreviousFocus();
+
+ this._open.set(true);
+ this._entering.set(true);
+ this.openChange.emit(true);
+
+ // Animation timing
+ setTimeout(() => {
+ this._entering.set(false);
+ this.setupFocusTrap();
+ this.opened.emit();
+ }, 300);
+ }
+
+ /**
+ * Hides the drawer with animation
+ */
+ hide(): void {
+ if (!this._open()) return;
+
+ this._leaving.set(true);
+
+ // Animation timing
+ setTimeout(() => {
+ this._open.set(false);
+ this._leaving.set(false);
+ this.restoreBodyScroll();
+ this.restoreFocus();
+ this.openChange.emit(false);
+ this.closed.emit();
+ }, 300);
+ }
+
+ /**
+ * Toggles the drawer open/closed state
+ */
+ toggle(): void {
+ if (this.open) {
+ this.close();
+ } else {
+ this.open = true;
+ }
+ }
+
+ /**
+ * Closes the drawer
+ */
+ close(): void {
+ this.open = false;
+ }
+
+ /**
+ * Handles backdrop click events
+ */
+ handleBackdropClick(event: MouseEvent): void {
+ event.stopPropagation();
+ this.backdropClicked.emit();
+
+ if (this.backdropClosable && !this.persistent) {
+ this.close();
+ }
+ }
+
+ /**
+ * Handles keyboard events for accessibility
+ */
+ @HostListener('keydown', ['$event'])
+ handleKeydown(event: KeyboardEvent): void {
+ if (event.key === 'Escape' && this.escapeClosable) {
+ event.preventDefault();
+ this.escapePressed.emit();
+ this.close();
+ }
+
+ if (this.focusTrap && this.isVisible()) {
+ this.handleFocusTrap(event);
+ }
+ }
+
+ /**
+ * Prevents body scrolling by manipulating body styles
+ */
+ private preventBodyScrollAction(): void {
+ const body = document.body;
+ this.originalBodyOverflow = body.style.overflow;
+ this.originalBodyPaddingRight = body.style.paddingRight;
+
+ // Calculate scrollbar width to prevent layout shift
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
+
+ body.style.overflow = 'hidden';
+ if (scrollbarWidth > 0) {
+ body.style.paddingRight = `${scrollbarWidth}px`;
+ }
+ }
+
+ /**
+ * Restores body scrolling
+ */
+ private restoreBodyScroll(): void {
+ const body = document.body;
+ body.style.overflow = this.originalBodyOverflow;
+ body.style.paddingRight = this.originalBodyPaddingRight;
+ }
+
+ /**
+ * Stores the currently focused element before showing drawer
+ */
+ private storePreviousFocus(): void {
+ this.previousActiveElement = document.activeElement;
+ }
+
+ /**
+ * Restores focus to the previously focused element
+ */
+ private restoreFocus(): void {
+ if (this.previousActiveElement && this.previousActiveElement instanceof HTMLElement) {
+ setTimeout(() => {
+ if (this.previousActiveElement instanceof HTMLElement) {
+ this.previousActiveElement.focus();
+ }
+ this.previousActiveElement = null;
+ }, 100);
+ }
+ }
+
+ /**
+ * Sets up focus trap for accessibility
+ */
+ private setupFocusTrap(): void {
+ if (!this.focusTrap) return;
+
+ const drawerElement = this.elementRef.nativeElement.querySelector('.ui-drawer__container');
+ if (drawerElement) {
+ this.focusableElements = this.getFocusableElements(drawerElement);
+ if (this.focusableElements.length > 0) {
+ this.focusableElements[0].focus();
+ this.currentFocusIndex = 0;
+ }
+ }
+ }
+
+ /**
+ * Gets all focusable elements within the drawer
+ */
+ private getFocusableElements(container: Element): HTMLElement[] {
+ const focusableSelectors = [
+ 'button:not([disabled])',
+ 'input:not([disabled])',
+ 'textarea:not([disabled])',
+ 'select:not([disabled])',
+ 'a[href]',
+ '[tabindex]:not([tabindex="-1"])',
+ '[contenteditable="true"]'
+ ];
+
+ return Array.from(container.querySelectorAll(focusableSelectors.join(', '))) as HTMLElement[];
+ }
+
+ /**
+ * Handles focus trap keyboard navigation
+ */
+ private handleFocusTrap(event: KeyboardEvent): void {
+ if (event.key !== 'Tab' || this.focusableElements.length === 0) return;
+
+ if (event.shiftKey) {
+ // Shift + Tab (backward)
+ this.currentFocusIndex = this.currentFocusIndex <= 0
+ ? this.focusableElements.length - 1
+ : this.currentFocusIndex - 1;
+ } else {
+ // Tab (forward)
+ this.currentFocusIndex = this.currentFocusIndex >= this.focusableElements.length - 1
+ ? 0
+ : this.currentFocusIndex + 1;
+ }
+
+ event.preventDefault();
+ this.focusableElements[this.currentFocusIndex].focus();
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/overlays/drawer/index.ts b/projects/ui-essentials/src/lib/components/overlays/drawer/index.ts
new file mode 100644
index 0000000..3ba5e42
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/overlays/drawer/index.ts
@@ -0,0 +1 @@
+export * from './drawer.component';
\ 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
new file mode 100644
index 0000000..e11e25c
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/overlays/index.ts
@@ -0,0 +1,4 @@
+export * from './modal';
+export * from './drawer';
+export * from './backdrop';
+export * from './overlay-container';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/overlays/modal/index.ts b/projects/ui-essentials/src/lib/components/overlays/modal/index.ts
new file mode 100644
index 0000000..9f26e35
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/overlays/modal/index.ts
@@ -0,0 +1 @@
+export * from './modal.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/overlays/modal/modal.component.scss b/projects/ui-essentials/src/lib/components/overlays/modal/modal.component.scss
new file mode 100644
index 0000000..c612c0b
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/overlays/modal/modal.component.scss
@@ -0,0 +1,341 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+.ui-modal {
+ // Modal Backdrop
+ &__backdrop {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.6);
+ z-index: 1000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: $semantic-spacing-layout-md;
+ backdrop-filter: blur(4px);
+ opacity: 0;
+ transition: opacity $semantic-motion-duration-normal $semantic-motion-easing-ease;
+
+ &--visible {
+ opacity: 1;
+ }
+
+ &--entering {
+ animation: backdrop-fade-in $semantic-motion-duration-normal $semantic-motion-easing-ease;
+ }
+
+ &--leaving {
+ animation: backdrop-fade-out $semantic-motion-duration-normal $semantic-motion-easing-ease;
+ }
+ }
+
+ // Modal Container
+ &__container {
+ background: $semantic-color-surface-primary;
+ border-radius: $semantic-border-radius-lg;
+ box-shadow: $semantic-shadow-elevation-3;
+ max-width: 90vw;
+ max-height: 90vh;
+ overflow: hidden;
+ transform: scale(0.8) translateY(20px);
+ opacity: 0;
+ transition: all $semantic-motion-duration-normal $semantic-motion-easing-ease;
+
+ &--visible {
+ transform: scale(1) translateY(0);
+ opacity: 1;
+ }
+
+ &--entering {
+ animation: modal-slide-in $semantic-motion-duration-normal $semantic-motion-easing-ease;
+ }
+
+ &--leaving {
+ animation: modal-slide-out $semantic-motion-duration-normal $semantic-motion-easing-ease;
+ }
+ }
+
+ // Size Variants
+ &--sm {
+ .ui-modal__container {
+ width: 400px;
+ }
+ }
+
+ &--md {
+ .ui-modal__container {
+ width: 600px;
+ }
+ }
+
+ &--lg {
+ .ui-modal__container {
+ width: 800px;
+ }
+ }
+
+ &--xl {
+ .ui-modal__container {
+ width: 1000px;
+ }
+ }
+
+ &--fullscreen {
+ .ui-modal__backdrop {
+ padding: 0;
+ }
+
+ .ui-modal__container {
+ width: 100vw;
+ height: 100vh;
+ max-width: none;
+ max-height: none;
+ border-radius: 0;
+ }
+ }
+
+ // Modal Header
+ &__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: $semantic-spacing-component-lg;
+ border-bottom: 1px solid $semantic-color-outline;
+ background: $semantic-color-surface-primary;
+
+ &--no-border {
+ border-bottom: none;
+ }
+ }
+
+ &__title {
+ font-size: $semantic-typography-heading-h4-size;
+ font-weight: $semantic-typography-font-weight-semibold;
+ color: $semantic-color-on-surface;
+ margin: 0;
+ line-height: 1.4;
+ }
+
+ &__close-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 40px;
+ height: 40px;
+ border: none;
+ background: transparent;
+ border-radius: $semantic-border-radius-md;
+ color: $semantic-color-on-surface-variant;
+ cursor: pointer;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
+
+ &:hover {
+ background: $semantic-color-surface-variant;
+ color: $semantic-color-on-surface;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-primary;
+ outline-offset: 2px;
+ }
+
+ &:active {
+ background: $semantic-color-surface-container;
+ }
+ }
+
+ // Modal Body
+ &__body {
+ padding: $semantic-spacing-component-lg;
+ overflow-y: auto;
+ max-height: calc(90vh - 200px);
+
+ &--no-padding {
+ padding: 0;
+ }
+
+ &--scrollable {
+ overflow-y: auto;
+ }
+ }
+
+ &__content {
+ color: $semantic-color-on-surface;
+ font-size: $semantic-typography-font-size-md;
+ line-height: 1.6;
+ }
+
+ // Modal Footer
+ &__footer {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: $semantic-spacing-component-sm;
+ padding: $semantic-spacing-component-lg;
+ border-top: 1px solid $semantic-color-outline;
+ background: $semantic-color-surface-primary;
+
+ &--no-border {
+ border-top: none;
+ }
+
+ &--center {
+ justify-content: center;
+ }
+
+ &--start {
+ justify-content: flex-start;
+ }
+
+ &--between {
+ justify-content: space-between;
+ }
+ }
+
+ // State Variants
+ &--loading {
+ .ui-modal__body {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 200px;
+ }
+ }
+
+ &--danger {
+ .ui-modal__header {
+ background: $semantic-color-container-error;
+ border-bottom-color: $semantic-color-error;
+ }
+
+ .ui-modal__title {
+ color: $semantic-color-on-container-error;
+ }
+ }
+
+ &--warning {
+ .ui-modal__header {
+ background: rgba($semantic-color-warning, 0.1);
+ border-bottom-color: $semantic-color-warning;
+ }
+
+ .ui-modal__title {
+ color: $semantic-color-on-warning;
+ }
+ }
+
+ &--success {
+ .ui-modal__header {
+ background: rgba($semantic-color-success, 0.1);
+ border-bottom-color: $semantic-color-success;
+ }
+
+ .ui-modal__title {
+ color: $semantic-color-on-success;
+ }
+ }
+
+ // Loading Spinner
+ &__loader {
+ display: inline-block;
+ width: 24px;
+ height: 24px;
+ border: 2px solid $semantic-color-outline;
+ border-radius: 50%;
+ border-top-color: $semantic-color-primary;
+ animation: modal-spin 1s linear infinite;
+ }
+
+ // Dark Mode Support
+ :host-context(.dark-theme) & {
+ &__backdrop {
+ background: rgba(0, 0, 0, 0.8);
+ }
+ }
+
+ // Responsive Design
+ @media (max-width: 768px) {
+ &__backdrop {
+ padding: $semantic-spacing-component-sm;
+ }
+
+ &__container {
+ width: 100% !important;
+ max-width: none;
+ margin: 0;
+ border-radius: $semantic-border-radius-md;
+ }
+
+ &__header {
+ padding: $semantic-spacing-component-md;
+ }
+
+ &__body {
+ padding: $semantic-spacing-component-md;
+ max-height: calc(90vh - 160px);
+ }
+
+ &__footer {
+ padding: $semantic-spacing-component-md;
+ flex-direction: column;
+ gap: $semantic-spacing-component-xs;
+
+ .ui-button {
+ width: 100%;
+ }
+ }
+
+ &__title {
+ font-size: $semantic-typography-heading-h5-size;
+ }
+ }
+}
+
+// Animations
+@keyframes backdrop-fade-in {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes backdrop-fade-out {
+ from {
+ opacity: 1;
+ }
+ to {
+ opacity: 0;
+ }
+}
+
+@keyframes modal-slide-in {
+ from {
+ transform: scale(0.8) translateY(20px);
+ opacity: 0;
+ }
+ to {
+ transform: scale(1) translateY(0);
+ opacity: 1;
+ }
+}
+
+@keyframes modal-slide-out {
+ from {
+ transform: scale(1) translateY(0);
+ opacity: 1;
+ }
+ to {
+ transform: scale(0.8) translateY(20px);
+ opacity: 0;
+ }
+}
+
+@keyframes modal-spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/overlays/modal/modal.component.ts b/projects/ui-essentials/src/lib/components/overlays/modal/modal.component.ts
new file mode 100644
index 0000000..5e7bde8
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/overlays/modal/modal.component.ts
@@ -0,0 +1,422 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, OnInit, OnDestroy, inject, ElementRef, HostListener } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { faXmark } from '@fortawesome/free-solid-svg-icons';
+import { signal, computed } from '@angular/core';
+
+type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'fullscreen';
+type ModalVariant = 'default' | 'danger' | 'warning' | 'success';
+type FooterAlignment = 'start' | 'center' | 'end' | 'between';
+
+export interface ModalConfig {
+ size?: ModalSize;
+ variant?: ModalVariant;
+ closable?: boolean;
+ backdropClosable?: boolean;
+ escapeClosable?: boolean;
+ showHeader?: boolean;
+ showFooter?: boolean;
+ headerBorder?: boolean;
+ footerBorder?: boolean;
+ bodyPadding?: boolean;
+ bodyScrollable?: boolean;
+ footerAlignment?: FooterAlignment;
+ preventBodyScroll?: boolean;
+ focusTrap?: boolean;
+}
+
+@Component({
+ selector: 'ui-modal',
+ standalone: true,
+ imports: [CommonModule, FontAwesomeModule],
+ changeDetection: ChangeDetectionStrategy.Default,
+ encapsulation: ViewEncapsulation.None,
+ template: `
+ @if (isVisible()) {
+
+
+
+
+
+
+ @if (showHeader) {
+
+ }
+
+
+
+ @if (loading) {
+
+ } @else {
+
+
+
+ }
+
+
+
+ @if (showFooter) {
+
+ }
+
+
+
+ }
+ `,
+ styleUrl: './modal.component.scss'
+})
+export class ModalComponent implements OnInit, OnDestroy {
+ // Dependencies
+ private elementRef = inject(ElementRef);
+
+ // Icons
+ readonly faXmark = faXmark;
+
+ // Inputs
+ @Input() size: ModalSize = 'md';
+ @Input() variant: ModalVariant = 'default';
+ @Input() title = '';
+ @Input() loading = false;
+ @Input() closable = true;
+ @Input() backdropClosable = true;
+ @Input() escapeClosable = true;
+ @Input() showHeader = true;
+ @Input() showFooter = false;
+ @Input() headerBorder = true;
+ @Input() footerBorder = true;
+ @Input() bodyPadding = true;
+ @Input() bodyScrollable = true;
+ @Input() footerAlignment: FooterAlignment = 'end';
+ @Input() preventBodyScroll = true;
+ @Input() focusTrap = true;
+ @Input() closeAriaLabel = 'Close modal';
+
+ // State Inputs
+ @Input() set open(value: boolean) {
+ if (value !== this._open()) {
+ this._open.set(value);
+ if (value) {
+ this.show();
+ } else {
+ this.hide();
+ }
+ }
+ }
+
+ get open(): boolean {
+ return this._open();
+ }
+
+ // Outputs
+ @Output() openChange = new EventEmitter();
+ @Output() opened = new EventEmitter();
+ @Output() closed = new EventEmitter();
+ @Output() backdropClicked = new EventEmitter();
+ @Output() escapePressed = new EventEmitter();
+
+ // Internal state signals
+ private _open = signal(false);
+ private _entering = signal(false);
+ private _leaving = signal(false);
+
+ // Computed signals
+ readonly isVisible = computed(() => this._open() || this._entering() || this._leaving());
+ readonly isEntering = computed(() => this._entering());
+ readonly isLeaving = computed(() => this._leaving());
+
+ // Internal properties
+ private originalBodyOverflow = '';
+ private originalBodyPaddingRight = '';
+ private focusableElements: HTMLElement[] = [];
+ private previousActiveElement: Element | null = null;
+ private currentFocusIndex = 0;
+
+ // Unique IDs for accessibility
+ readonly titleId = `modal-title-${Math.random().toString(36).substr(2, 9)}`;
+ readonly contentId = `modal-content-${Math.random().toString(36).substr(2, 9)}`;
+
+ ngOnInit(): void {
+ if (this.open) {
+ this.show();
+ }
+ }
+
+ ngOnDestroy(): void {
+ this.restoreBodyScroll();
+ this.restoreFocus();
+ }
+
+ /**
+ * Shows the modal with animation
+ */
+ show(): void {
+ if (this._open()) return;
+
+ this.preventBodyScrollIfEnabled();
+ this.storePreviousFocus();
+
+ this._open.set(true);
+ this._entering.set(true);
+ this.openChange.emit(true);
+
+ // Animation timing
+ setTimeout(() => {
+ this._entering.set(false);
+ this.setupFocusTrap();
+ this.opened.emit();
+ }, 300);
+ }
+
+ /**
+ * Hides the modal with animation
+ */
+ hide(): void {
+ if (!this._open()) return;
+
+ this._leaving.set(true);
+
+ // Animation timing
+ setTimeout(() => {
+ this._open.set(false);
+ this._leaving.set(false);
+ this.restoreBodyScroll();
+ this.restoreFocus();
+ this.openChange.emit(false);
+ this.closed.emit();
+ }, 300);
+ }
+
+ /**
+ * Closes the modal (alias for hide)
+ */
+ close(): void {
+ if (this.closable) {
+ this.hide();
+ }
+ }
+
+ /**
+ * Toggles modal visibility
+ */
+ toggle(): void {
+ if (this._open()) {
+ this.hide();
+ } else {
+ this.show();
+ }
+ }
+
+ /**
+ * Handles backdrop click
+ */
+ handleBackdropClick(event: Event): void {
+ if (this.backdropClosable) {
+ this.close();
+ this.backdropClicked.emit();
+ }
+ }
+
+ /**
+ * Handles keyboard events
+ */
+ handleKeydown(event: KeyboardEvent): void {
+ if (event.key === 'Escape' && this.escapeClosable) {
+ event.preventDefault();
+ this.close();
+ this.escapePressed.emit();
+ return;
+ }
+
+ // Focus trap handling
+ if (this.focusTrap && (event.key === 'Tab')) {
+ this.handleTabKeyForFocusTrap(event);
+ }
+ }
+
+ /**
+ * Global keyboard listener for ESC key
+ */
+ @HostListener('document:keydown', ['$event'])
+ onDocumentKeydown(event: KeyboardEvent): void {
+ if (this._open() && event.key === 'Escape' && this.escapeClosable) {
+ event.preventDefault();
+ this.close();
+ this.escapePressed.emit();
+ }
+ }
+
+ /**
+ * Prevents body scroll when modal is open
+ */
+ private preventBodyScrollIfEnabled(): void {
+ if (!this.preventBodyScroll) return;
+
+ const body = document.body;
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
+
+ this.originalBodyOverflow = body.style.overflow;
+ this.originalBodyPaddingRight = body.style.paddingRight;
+
+ body.style.overflow = 'hidden';
+ if (scrollbarWidth > 0) {
+ body.style.paddingRight = `${scrollbarWidth}px`;
+ }
+ }
+
+ /**
+ * Restores body scroll
+ */
+ private restoreBodyScroll(): void {
+ if (!this.preventBodyScroll) return;
+
+ const body = document.body;
+ body.style.overflow = this.originalBodyOverflow;
+ body.style.paddingRight = this.originalBodyPaddingRight;
+ }
+
+ /**
+ * Stores the currently focused element
+ */
+ private storePreviousFocus(): void {
+ this.previousActiveElement = document.activeElement;
+ }
+
+ /**
+ * Restores focus to the previously focused element
+ */
+ private restoreFocus(): void {
+ if (this.previousActiveElement && 'focus' in this.previousActiveElement) {
+ (this.previousActiveElement as HTMLElement).focus();
+ }
+ }
+
+ /**
+ * Sets up focus trap within modal
+ */
+ private setupFocusTrap(): void {
+ if (!this.focusTrap) return;
+
+ setTimeout(() => {
+ this.updateFocusableElements();
+ if (this.focusableElements.length > 0) {
+ this.focusableElements[0].focus();
+ this.currentFocusIndex = 0;
+ }
+ }, 100);
+ }
+
+ /**
+ * Updates the list of focusable elements
+ */
+ private updateFocusableElements(): void {
+ const modalElement = this.elementRef.nativeElement.querySelector('.ui-modal__container');
+ if (!modalElement) return;
+
+ const focusableSelectors = [
+ 'button:not([disabled])',
+ 'input:not([disabled])',
+ 'textarea:not([disabled])',
+ 'select:not([disabled])',
+ 'a[href]',
+ '[tabindex]:not([tabindex="-1"])',
+ '[contenteditable="true"]'
+ ].join(', ');
+
+ this.focusableElements = Array.from(
+ modalElement.querySelectorAll(focusableSelectors)
+ ) as HTMLElement[];
+ }
+
+ /**
+ * Handles Tab key for focus trapping
+ */
+ private handleTabKeyForFocusTrap(event: KeyboardEvent): void {
+ if (this.focusableElements.length === 0) return;
+
+ const isShiftTab = event.shiftKey;
+
+ if (isShiftTab) {
+ // Shift + Tab - move to previous focusable element
+ this.currentFocusIndex = this.currentFocusIndex <= 0
+ ? this.focusableElements.length - 1
+ : this.currentFocusIndex - 1;
+ } else {
+ // Tab - move to next focusable element
+ this.currentFocusIndex = this.currentFocusIndex >= this.focusableElements.length - 1
+ ? 0
+ : this.currentFocusIndex + 1;
+ }
+
+ event.preventDefault();
+ this.focusableElements[this.currentFocusIndex].focus();
+ }
+
+ /**
+ * Public API method to apply configuration
+ */
+ configure(config: ModalConfig): void {
+ Object.assign(this, config);
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/overlays/overlay-container/index.ts b/projects/ui-essentials/src/lib/components/overlays/overlay-container/index.ts
new file mode 100644
index 0000000..2ebe5b7
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/overlays/overlay-container/index.ts
@@ -0,0 +1 @@
+export * from './overlay-container.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/overlays/overlay-container/overlay-container.component.scss b/projects/ui-essentials/src/lib/components/overlays/overlay-container/overlay-container.component.scss
new file mode 100644
index 0000000..b67dc7c
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/overlays/overlay-container/overlay-container.component.scss
@@ -0,0 +1,331 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as tokens;
+
+.ui-overlay-container {
+ // Core Structure
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: var(--overlay-z-index, tokens.$semantic-z-index-overlay);
+
+ // Initial state (hidden)
+ visibility: hidden;
+ opacity: 0;
+ pointer-events: none;
+
+ // Transitions
+ transition: opacity tokens.$semantic-motion-duration-fast tokens.$semantic-motion-easing-ease,
+ visibility tokens.$semantic-motion-duration-fast tokens.$semantic-motion-easing-ease;
+
+ // Visible state
+ &--visible {
+ visibility: visible;
+ opacity: 1;
+ pointer-events: auto;
+ }
+
+ // Animation states
+ &--entering {
+ visibility: visible;
+ opacity: 1;
+ pointer-events: auto;
+
+ .ui-overlay-container__content {
+ animation: overlayEnter tokens.$semantic-motion-duration-fast tokens.$semantic-motion-easing-ease forwards;
+ }
+ }
+
+ &--leaving {
+ .ui-overlay-container__content {
+ animation: overlayLeave tokens.$semantic-motion-duration-fast tokens.$semantic-motion-easing-ease forwards;
+ }
+ }
+
+ // Position Variants
+ &--top {
+ align-items: flex-start;
+ padding-top: tokens.$semantic-spacing-layout-md;
+ }
+
+ &--bottom {
+ align-items: flex-end;
+ padding-bottom: tokens.$semantic-spacing-layout-md;
+ }
+
+ &--left {
+ justify-content: flex-start;
+ padding-left: tokens.$semantic-spacing-layout-md;
+ }
+
+ &--right {
+ justify-content: flex-end;
+ padding-right: tokens.$semantic-spacing-layout-md;
+ }
+
+ &--top-left {
+ align-items: flex-start;
+ justify-content: flex-start;
+ padding: tokens.$semantic-spacing-layout-md;
+ }
+
+ &--top-right {
+ align-items: flex-start;
+ justify-content: flex-end;
+ padding: tokens.$semantic-spacing-layout-md;
+ }
+
+ &--bottom-left {
+ align-items: flex-end;
+ justify-content: flex-start;
+ padding: tokens.$semantic-spacing-layout-md;
+ }
+
+ &--bottom-right {
+ align-items: flex-end;
+ justify-content: flex-end;
+ padding: tokens.$semantic-spacing-layout-md;
+ }
+
+ // Size Variants
+ &--sm .ui-overlay-container__content {
+ max-width: tokens.$semantic-sizing-modal-width-sm;
+ max-height: 50vh;
+ }
+
+ &--md .ui-overlay-container__content {
+ max-width: tokens.$semantic-sizing-modal-width-md;
+ max-height: 70vh;
+ }
+
+ &--lg .ui-overlay-container__content {
+ max-width: tokens.$semantic-sizing-modal-width-lg;
+ max-height: 80vh;
+ }
+
+ &--xl .ui-overlay-container__content {
+ max-width: tokens.$semantic-sizing-modal-width-xl;
+ max-height: 90vh;
+ }
+
+ &--fullscreen .ui-overlay-container__content {
+ width: 100vw;
+ height: 100vh;
+ max-width: none;
+ max-height: none;
+ }
+
+ // Variant Styles
+ &--modal {
+ z-index: tokens.$semantic-z-index-modal;
+
+ .ui-overlay-container__backdrop {
+ background: tokens.$semantic-color-backdrop;
+ }
+ }
+
+ &--popover {
+ z-index: tokens.$semantic-z-index-popover;
+
+ .ui-overlay-container__content {
+ box-shadow: tokens.$semantic-shadow-popover;
+ }
+ }
+
+ &--elevated {
+ z-index: tokens.$semantic-z-index-elevated;
+
+ .ui-overlay-container__content {
+ box-shadow: tokens.$semantic-shadow-elevation-3;
+ }
+ }
+
+ &--floating {
+ z-index: tokens.$semantic-z-index-floating;
+
+ .ui-overlay-container__content {
+ box-shadow: tokens.$semantic-shadow-elevation-2;
+ }
+ }
+
+ // Backdrop with blur effect
+ &--backdrop-blur {
+ backdrop-filter: blur(tokens.$semantic-glass-blur-md);
+ -webkit-backdrop-filter: blur(tokens.$semantic-glass-blur-md);
+ }
+
+ // Backdrop Element
+ &__backdrop {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: tokens.$semantic-color-backdrop;
+ opacity: 0;
+ transition: opacity tokens.$semantic-motion-duration-fast tokens.$semantic-motion-easing-ease;
+ z-index: -1;
+
+ &--visible {
+ opacity: tokens.$semantic-opacity-backdrop;
+ }
+
+ &--entering {
+ opacity: tokens.$semantic-opacity-backdrop;
+ }
+
+ &--leaving {
+ opacity: 0;
+ }
+
+ &--blur {
+ backdrop-filter: blur(tokens.$semantic-glass-blur-sm);
+ -webkit-backdrop-filter: blur(tokens.$semantic-glass-blur-sm);
+ }
+ }
+
+ // Content Element
+ &__content {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ min-height: 0;
+ max-width: calc(100vw - #{tokens.$semantic-spacing-layout-lg});
+ max-height: calc(100vh - #{tokens.$semantic-spacing-layout-lg});
+ margin: tokens.$semantic-spacing-layout-sm;
+
+ // Default styling
+ background: tokens.$semantic-color-surface-primary;
+ border: tokens.$semantic-border-card-width solid tokens.$semantic-color-border-primary;
+ border-radius: tokens.$semantic-border-card-radius;
+ box-shadow: tokens.$semantic-shadow-elevation-2;
+ color: tokens.$semantic-color-text-primary;
+
+ // Transform and scale for animations
+ transform: scale(0.95) translateY(tokens.$semantic-spacing-component-md);
+ opacity: 0;
+ transition: transform tokens.$semantic-motion-duration-fast tokens.$semantic-motion-easing-ease,
+ opacity tokens.$semantic-motion-duration-fast tokens.$semantic-motion-easing-ease;
+
+ &--visible {
+ transform: scale(1) translateY(0);
+ opacity: 1;
+ }
+
+ &--entering {
+ transform: scale(1) translateY(0);
+ opacity: 1;
+ }
+
+ &--leaving {
+ transform: scale(0.95) translateY(tokens.$semantic-spacing-component-md);
+ opacity: 0;
+ }
+
+ // Offset positioning
+ .ui-overlay-container:not(.ui-overlay-container--center) & {
+ transform: translateX(var(--overlay-offset-x, 0)) translateY(var(--overlay-offset-y, 0));
+ }
+ }
+
+ // Dark Mode Support
+ :host-context(.dark-theme) & {
+ .ui-overlay-container__content {
+ background: tokens.$semantic-color-surface-primary;
+ border-color: tokens.$semantic-color-border-primary;
+ box-shadow: tokens.$semantic-shadow-elevation-2;
+ }
+ }
+
+ // Responsive Design
+ @media (max-width: tokens.$semantic-breakpoint-md - 1) {
+ padding: tokens.$semantic-spacing-layout-sm;
+
+ .ui-overlay-container__content {
+ margin: tokens.$semantic-spacing-component-xs;
+ max-width: calc(100vw - #{tokens.$semantic-spacing-layout-md});
+ max-height: calc(100vh - #{tokens.$semantic-spacing-layout-md});
+ }
+
+ &--fullscreen .ui-overlay-container__content {
+ border-radius: 0;
+ margin: 0;
+ }
+ }
+
+ @media (max-width: tokens.$semantic-breakpoint-sm - 1) {
+ // Mobile adjustments
+ &:not(.ui-overlay-container--fullscreen) {
+ align-items: flex-end;
+
+ .ui-overlay-container__content {
+ width: 100%;
+ max-width: none;
+ margin: 0;
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+ }
+ }
+}
+
+// Keyframe Animations
+@keyframes overlayEnter {
+ 0% {
+ transform: scale(0.95) translateY(tokens.$semantic-spacing-component-md);
+ opacity: 0;
+ }
+ 100% {
+ transform: scale(1) translateY(0);
+ opacity: 1;
+ }
+}
+
+@keyframes overlayLeave {
+ 0% {
+ transform: scale(1) translateY(0);
+ opacity: 1;
+ }
+ 100% {
+ transform: scale(0.95) translateY(tokens.$semantic-spacing-component-md);
+ opacity: 0;
+ }
+}
+
+// Focus trap styling
+.ui-overlay-container:focus-within {
+ .ui-overlay-container__content {
+ outline: none;
+ }
+}
+
+// Reduced motion support
+@media (prefers-reduced-motion: reduce) {
+ .ui-overlay-container {
+ transition: none;
+
+ .ui-overlay-container__backdrop,
+ .ui-overlay-container__content {
+ transition: none;
+ animation: none;
+ }
+ }
+
+ @keyframes overlayEnter {
+ 0%, 100% {
+ transform: scale(1) translateY(0);
+ opacity: 1;
+ }
+ }
+
+ @keyframes overlayLeave {
+ 0%, 100% {
+ transform: scale(1) translateY(0);
+ opacity: 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/overlays/overlay-container/overlay-container.component.ts b/projects/ui-essentials/src/lib/components/overlays/overlay-container/overlay-container.component.ts
new file mode 100644
index 0000000..798242e
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/overlays/overlay-container/overlay-container.component.ts
@@ -0,0 +1,413 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, OnInit, OnDestroy, HostListener, inject, ElementRef } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { signal, computed } from '@angular/core';
+
+type OverlayPosition = 'center' | 'top' | 'bottom' | 'left' | 'right' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
+type OverlaySize = 'auto' | 'sm' | 'md' | 'lg' | 'xl' | 'fullscreen';
+type OverlayVariant = 'default' | 'elevated' | 'floating' | 'modal' | 'popover';
+
+export interface OverlayContainerConfig {
+ position?: OverlayPosition;
+ size?: OverlaySize;
+ variant?: OverlayVariant;
+ visible?: boolean;
+ closable?: boolean;
+ backdropClosable?: boolean;
+ escapeClosable?: boolean;
+ showBackdrop?: boolean;
+ backdropBlur?: boolean;
+ preventBodyScroll?: boolean;
+ focusTrap?: boolean;
+ autoFocus?: boolean;
+ restoreFocus?: boolean;
+ zIndex?: number;
+ offsetX?: number;
+ offsetY?: number;
+}
+
+@Component({
+ selector: 'ui-overlay-container',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ template: `
+ @if (isVisible()) {
+
+
+ @if (showBackdrop) {
+
+ }
+
+
+
+
+
+
+ }
+ `,
+ styleUrl: './overlay-container.component.scss'
+})
+export class OverlayContainerComponent implements OnInit, OnDestroy {
+ // Dependencies
+ private elementRef = inject(ElementRef);
+
+ // Inputs
+ @Input() position: OverlayPosition = 'center';
+ @Input() size: OverlaySize = 'auto';
+ @Input() variant: OverlayVariant = 'default';
+ @Input() closable = true;
+ @Input() backdropClosable = true;
+ @Input() escapeClosable = true;
+ @Input() showBackdrop = true;
+ @Input() backdropBlur = false;
+ @Input() preventBodyScroll = true;
+ @Input() focusTrap = false;
+ @Input() autoFocus = false;
+ @Input() restoreFocus = true;
+ @Input() zIndex = 1000;
+ @Input() offsetX = 0;
+ @Input() offsetY = 0;
+
+ // 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() backdropClicked = new EventEmitter();
+ @Output() escapePressed = new EventEmitter();
+
+ // Internal state signals
+ private _visible = signal(false);
+ private _entering = signal(false);
+ private _leaving = signal(false);
+
+ // Computed signals
+ readonly isVisible = computed(() => this._visible() || this._entering() || this._leaving());
+ readonly isEntering = computed(() => this._entering());
+ readonly isLeaving = computed(() => this._leaving());
+
+ // Internal properties
+ private originalBodyOverflow = '';
+ private originalBodyPaddingRight = '';
+ private focusableElements: HTMLElement[] = [];
+ private previousActiveElement: Element | null = null;
+ private currentFocusIndex = 0;
+ private animationDuration = 200; // ms
+
+ ngOnInit(): void {
+ if (this.visible) {
+ this.show();
+ }
+
+ // Set appropriate z-index based on variant
+ if (!this.zIndex || this.zIndex === 1000) {
+ this.setVariantZIndex();
+ }
+ }
+
+ ngOnDestroy(): void {
+ this.restoreBodyScroll();
+ this.restoreFocusIfNeeded();
+ }
+
+ /**
+ * Shows the overlay with animation
+ */
+ show(): void {
+ if (this._visible()) return;
+
+ this.preventBodyScrollIfEnabled();
+ this.storePreviousFocusIfNeeded();
+
+ this._visible.set(true);
+ this._entering.set(true);
+ this.visibleChange.emit(true);
+
+ // Animation timing
+ setTimeout(() => {
+ this._entering.set(false);
+ this.setupFocusTrapIfEnabled();
+ this.autoFocusIfEnabled();
+ this.shown.emit();
+ }, this.animationDuration);
+ }
+
+ /**
+ * Hides the overlay with animation
+ */
+ hide(): void {
+ if (!this._visible()) return;
+
+ this._leaving.set(true);
+
+ // Animation timing
+ setTimeout(() => {
+ this._visible.set(false);
+ this._leaving.set(false);
+ this.restoreBodyScroll();
+ this.restoreFocusIfNeeded();
+ this.visibleChange.emit(false);
+ this.hidden.emit();
+ }, this.animationDuration);
+ }
+
+ /**
+ * Closes the overlay (alias for hide)
+ */
+ close(): void {
+ if (this.closable) {
+ this.hide();
+ }
+ }
+
+ /**
+ * Toggles overlay visibility
+ */
+ toggle(): void {
+ if (this._visible()) {
+ this.hide();
+ } else {
+ this.show();
+ }
+ }
+
+ /**
+ * Handles backdrop click
+ */
+ handleBackdropClick(event: MouseEvent): void {
+ if (this.backdropClosable) {
+ this.close();
+ this.backdropClicked.emit(event);
+ }
+ }
+
+ /**
+ * Handles keyboard events
+ */
+ handleKeydown(event: KeyboardEvent): void {
+ if (event.key === 'Escape' && this.escapeClosable) {
+ event.preventDefault();
+ this.close();
+ this.escapePressed.emit(event);
+ return;
+ }
+
+ // Focus trap handling
+ if (this.focusTrap && event.key === 'Tab') {
+ this.handleTabKeyForFocusTrap(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.close();
+ this.escapePressed.emit(event);
+ }
+ }
+
+ /**
+ * Sets z-index based on variant
+ */
+ private setVariantZIndex(): void {
+ switch (this.variant) {
+ case 'modal':
+ this.zIndex = 1050; // Higher than backdrop
+ break;
+ case 'popover':
+ this.zIndex = 1030;
+ break;
+ case 'elevated':
+ this.zIndex = 1020;
+ break;
+ case 'floating':
+ this.zIndex = 1010;
+ break;
+ default:
+ this.zIndex = 1000;
+ break;
+ }
+ }
+
+ /**
+ * Prevents body scroll when overlay is open
+ */
+ private preventBodyScrollIfEnabled(): void {
+ if (!this.preventBodyScroll) return;
+
+ const body = document.body;
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
+
+ this.originalBodyOverflow = body.style.overflow;
+ this.originalBodyPaddingRight = body.style.paddingRight;
+
+ body.style.overflow = 'hidden';
+ if (scrollbarWidth > 0) {
+ body.style.paddingRight = `${scrollbarWidth}px`;
+ }
+ }
+
+ /**
+ * Restores body scroll
+ */
+ private restoreBodyScroll(): void {
+ if (!this.preventBodyScroll) return;
+
+ const body = document.body;
+ body.style.overflow = this.originalBodyOverflow;
+ body.style.paddingRight = this.originalBodyPaddingRight;
+ }
+
+ /**
+ * Stores the currently focused element
+ */
+ private storePreviousFocusIfNeeded(): void {
+ if (this.restoreFocus) {
+ this.previousActiveElement = document.activeElement;
+ }
+ }
+
+ /**
+ * Restores focus to the previously focused element
+ */
+ private restoreFocusIfNeeded(): void {
+ if (this.restoreFocus && this.previousActiveElement && 'focus' in this.previousActiveElement) {
+ (this.previousActiveElement as HTMLElement).focus();
+ }
+ }
+
+ /**
+ * Auto focuses the first focusable element if enabled
+ */
+ private autoFocusIfEnabled(): void {
+ if (!this.autoFocus) return;
+
+ setTimeout(() => {
+ this.updateFocusableElements();
+ if (this.focusableElements.length > 0) {
+ this.focusableElements[0].focus();
+ this.currentFocusIndex = 0;
+ }
+ }, 100);
+ }
+
+ /**
+ * Sets up focus trap within overlay
+ */
+ private setupFocusTrapIfEnabled(): void {
+ if (!this.focusTrap) return;
+
+ setTimeout(() => {
+ this.updateFocusableElements();
+ if (this.focusableElements.length > 0) {
+ this.focusableElements[0].focus();
+ this.currentFocusIndex = 0;
+ }
+ }, 100);
+ }
+
+ /**
+ * Updates the list of focusable elements
+ */
+ private updateFocusableElements(): void {
+ const overlayElement = this.elementRef.nativeElement.querySelector('.ui-overlay-container__content');
+ if (!overlayElement) return;
+
+ const focusableSelectors = [
+ 'button:not([disabled])',
+ 'input:not([disabled])',
+ 'textarea:not([disabled])',
+ 'select:not([disabled])',
+ 'a[href]',
+ '[tabindex]:not([tabindex="-1"])',
+ '[contenteditable="true"]'
+ ].join(', ');
+
+ this.focusableElements = Array.from(
+ overlayElement.querySelectorAll(focusableSelectors)
+ ) as HTMLElement[];
+ }
+
+ /**
+ * Handles Tab key for focus trapping
+ */
+ private handleTabKeyForFocusTrap(event: KeyboardEvent): void {
+ if (this.focusableElements.length === 0) return;
+
+ const isShiftTab = event.shiftKey;
+
+ if (isShiftTab) {
+ // Shift + Tab - move to previous focusable element
+ this.currentFocusIndex = this.currentFocusIndex <= 0
+ ? this.focusableElements.length - 1
+ : this.currentFocusIndex - 1;
+ } else {
+ // Tab - move to next focusable element
+ this.currentFocusIndex = this.currentFocusIndex >= this.focusableElements.length - 1
+ ? 0
+ : this.currentFocusIndex + 1;
+ }
+
+ event.preventDefault();
+ this.focusableElements[this.currentFocusIndex].focus();
+ }
+
+ /**
+ * Public API method to apply configuration
+ */
+ configure(config: OverlayContainerConfig): void {
+ Object.assign(this, config);
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/layouts/bento-grid-layout.component.scss b/projects/ui-essentials/src/lib/layouts/bento-grid-layout.component.scss
new file mode 100644
index 0000000..49edea0
--- /dev/null
+++ b/projects/ui-essentials/src/lib/layouts/bento-grid-layout.component.scss
@@ -0,0 +1,251 @@
+@use "../../../../shared-ui/src/styles/semantic/index" as *;
+
+.bento-grid {
+ display: grid;
+ gap: $semantic-spacing-4;
+ padding: $semantic-spacing-4;
+ transition:
+ grid-template-columns $semantic-motion-duration-normal $semantic-motion-easing-ease-in-out,
+ grid-template-rows $semantic-motion-duration-normal $semantic-motion-easing-ease-in-out,
+ gap $semantic-motion-duration-fast $semantic-motion-easing-ease-out;
+
+ // Mobile: Single column for all variants
+ grid-template-columns: 1fr;
+ grid-auto-rows: minmax(200px, auto);
+
+ // Tablet: 2x2 base grid
+ @media (min-width: $semantic-breakpoint-sm) {
+ grid-template-columns: repeat(2, 1fr);
+ gap: $semantic-spacing-5;
+ padding: $semantic-spacing-5;
+ }
+
+ // Desktop: Full bento layouts
+ @media (min-width: $semantic-breakpoint-lg) {
+ gap: $semantic-spacing-6;
+ padding: $semantic-spacing-6;
+ grid-auto-rows: minmax(240px, auto);
+ }
+
+ @media (min-width: $semantic-sizing-breakpoint-wide) {
+ gap: $semantic-spacing-8;
+ padding: $semantic-spacing-8;
+ }
+}
+
+// Balanced variant: Even distribution
+.bento-grid--balanced {
+ @media (min-width: $semantic-breakpoint-lg) {
+ grid-template-columns: repeat(4, 1fr);
+ grid-template-rows: repeat(3, 1fr);
+ }
+
+ @media (min-width: $semantic-sizing-breakpoint-wide) {
+ grid-template-columns: repeat(6, 1fr);
+ grid-template-rows: repeat(4, 1fr);
+ }
+}
+
+// Asymmetric variant: Varied cell sizes
+.bento-grid--asymmetric {
+ @media (min-width: $semantic-breakpoint-lg) {
+ grid-template-columns: repeat(12, 1fr);
+ grid-template-rows: repeat(6, minmax(120px, 1fr));
+ }
+}
+
+// Masonry variant: Pinterest-style
+.bento-grid--masonry {
+ @media (min-width: $semantic-breakpoint-lg) {
+ grid-template-columns: repeat(4, 1fr);
+ grid-auto-rows: 120px;
+ }
+
+ @media (min-width: $semantic-sizing-breakpoint-wide) {
+ grid-template-columns: repeat(5, 1fr);
+ }
+}
+
+// Grid item base styles
+.bento-grid > * {
+ background-color: var(--color-background-surface, #fff);
+ border: 1px solid var(--color-border-subtle, #e5e7eb);
+ border-radius: $semantic-border-radius-lg;
+ overflow: hidden;
+ transition:
+ transform $semantic-motion-duration-fast $semantic-motion-easing-ease-out,
+ box-shadow $semantic-motion-duration-fast $semantic-motion-easing-ease-out,
+ border-color $semantic-motion-duration-fast $semantic-motion-easing-ease-out;
+
+ &:hover {
+ transform: $semantic-motion-hover-transform-lift-sm;
+ box-shadow: $semantic-shadow-lg;
+ border-color: var(--color-border-primary, #d1d5db);
+ }
+
+ // Stagger animation on load
+ @for $i from 1 through 20 {
+ &:nth-child(#{$i}) {
+ animation: bentoSlideIn $semantic-motion-duration-normal $semantic-motion-easing-ease-out;
+ animation-delay: calc(#{$semantic-motion-transition-delay-100} * #{$i});
+ animation-fill-mode: both;
+ }
+ }
+}
+
+// Span utility classes for grid positioning
+.bento-grid {
+ // Column spans
+ ::ng-deep [data-bento-span-col="2"] {
+ @media (min-width: $semantic-breakpoint-sm) {
+ grid-column: span 2;
+ }
+ }
+
+ ::ng-deep [data-bento-span-col="3"] {
+ @media (min-width: $semantic-breakpoint-lg) {
+ grid-column: span 3;
+ }
+ }
+
+ ::ng-deep [data-bento-span-col="4"] {
+ @media (min-width: $semantic-breakpoint-lg) {
+ grid-column: span 4;
+ }
+ }
+
+ // Row spans
+ ::ng-deep [data-bento-span-row="2"] {
+ @media (min-width: $semantic-breakpoint-sm) {
+ grid-row: span 2;
+ }
+ }
+
+ ::ng-deep [data-bento-span-row="3"] {
+ @media (min-width: $semantic-breakpoint-lg) {
+ grid-row: span 3;
+ }
+ }
+
+ // Featured item (large)
+ ::ng-deep [data-bento-featured="true"] {
+ @media (min-width: $semantic-breakpoint-sm) {
+ grid-column: span 2;
+ grid-row: span 2;
+ }
+
+ @media (min-width: $semantic-breakpoint-lg) {
+ grid-column: span 3;
+ grid-row: span 2;
+ }
+ }
+
+ // Wide item
+ ::ng-deep [data-bento-wide="true"] {
+ @media (min-width: $semantic-breakpoint-sm) {
+ grid-column: span 2;
+ }
+
+ @media (min-width: $semantic-breakpoint-lg) {
+ grid-column: span 4;
+ }
+ }
+
+ // Tall item
+ ::ng-deep [data-bento-tall="true"] {
+ @media (min-width: $semantic-breakpoint-sm) {
+ grid-row: span 2;
+ }
+
+ @media (min-width: $semantic-breakpoint-lg) {
+ grid-row: span 3;
+ }
+ }
+}
+
+// Asymmetric preset layouts
+.bento-grid--asymmetric {
+ ::ng-deep [data-bento-area="hero"] {
+ @media (min-width: $semantic-breakpoint-lg) {
+ grid-area: 1 / 1 / 3 / 7;
+ }
+ }
+
+ ::ng-deep [data-bento-area="sidebar"] {
+ @media (min-width: $semantic-breakpoint-lg) {
+ grid-area: 1 / 7 / 4 / 13;
+ }
+ }
+
+ ::ng-deep [data-bento-area="feature-1"] {
+ @media (min-width: $semantic-breakpoint-lg) {
+ grid-area: 3 / 1 / 5 / 5;
+ }
+ }
+
+ ::ng-deep [data-bento-area="feature-2"] {
+ @media (min-width: $semantic-breakpoint-lg) {
+ grid-area: 3 / 5 / 5 / 7;
+ }
+ }
+
+ ::ng-deep [data-bento-area="bottom-left"] {
+ @media (min-width: $semantic-breakpoint-lg) {
+ grid-area: 5 / 1 / 7 / 4;
+ }
+ }
+
+ ::ng-deep [data-bento-area="bottom-center"] {
+ @media (min-width: $semantic-breakpoint-lg) {
+ grid-area: 5 / 4 / 6 / 7;
+ }
+ }
+
+ ::ng-deep [data-bento-area="bottom-right"] {
+ @media (min-width: $semantic-breakpoint-lg) {
+ grid-area: 4 / 7 / 7 / 13;
+ }
+ }
+}
+
+// Adaptive sizing for content
+.bento-grid[data-adaptive="true"] > * {
+ // Ensure minimum content area
+ min-height: 160px;
+ padding: $semantic-spacing-4;
+
+ @media (min-width: $semantic-breakpoint-lg) {
+ min-height: 200px;
+ padding: $semantic-spacing-6;
+ }
+
+ // Scale content based on grid area size
+ &[data-bento-featured="true"],
+ &[data-bento-span-col="2"][data-bento-span-row="2"] {
+ padding: $semantic-spacing-6;
+
+ @media (min-width: $semantic-breakpoint-lg) {
+ padding: $semantic-spacing-8;
+ }
+ }
+}
+
+// Animation keyframes
+@keyframes bentoSlideIn {
+ from {
+ opacity: 0;
+ transform: translateY($semantic-spacing-6) scale(0.95);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+}
+
+// Responsive gap adjustments
+@media (max-width: $semantic-breakpoint-sm) {
+ .bento-grid {
+ gap: $semantic-spacing-3;
+ padding: $semantic-spacing-3;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/layouts/bento-grid-layout.component.ts b/projects/ui-essentials/src/lib/layouts/bento-grid-layout.component.ts
new file mode 100644
index 0000000..5adeca6
--- /dev/null
+++ b/projects/ui-essentials/src/lib/layouts/bento-grid-layout.component.ts
@@ -0,0 +1,22 @@
+import { Component, Input } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+@Component({
+ selector: 'ui-bento-grid-layout',
+ standalone: true,
+ imports: [CommonModule],
+ template: `
+
+
+
+ `,
+ styleUrl: './bento-grid-layout.component.scss'
+})
+export class BentoGridLayoutComponent {
+ @Input() variant: 'balanced' | 'asymmetric' | 'masonry' = 'balanced';
+ @Input() adaptive: boolean = true;
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/layouts/dashboard-shell-layout.component.scss b/projects/ui-essentials/src/lib/layouts/dashboard-shell-layout.component.scss
new file mode 100644
index 0000000..b1aecdf
--- /dev/null
+++ b/projects/ui-essentials/src/lib/layouts/dashboard-shell-layout.component.scss
@@ -0,0 +1,132 @@
+@use "../../../../shared-ui/src/styles/semantic/index" as *;
+
+.dashboard-shell {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ background-color: var(--color-background-primary, #fff);
+}
+
+.dashboard-header {
+ position: sticky;
+ top: 0;
+ z-index: $semantic-z-index-fixed;
+ padding: $semantic-spacing-4;
+ background-color: var(--color-background-surface, #fff);
+ border-bottom: 1px solid var(--color-border-subtle, #e5e7eb);
+ transition:
+ box-shadow $semantic-motion-duration-fast $semantic-motion-easing-ease-out;
+
+ @media (min-width: $semantic-breakpoint-md) {
+ padding: $semantic-spacing-4 $semantic-spacing-6;
+ }
+}
+
+.dashboard-content {
+ display: flex;
+ flex: 1;
+ overflow: hidden;
+}
+
+.dashboard-sidebar {
+ width: 100%;
+ max-width: $semantic-spacing-64;
+ background-color: var(--color-background-surface, #fff);
+ border-right: 1px solid var(--color-border-subtle, #e5e7eb);
+ transition:
+ width $semantic-motion-duration-normal $semantic-motion-easing-ease-in-out,
+ transform $semantic-motion-duration-normal $semantic-motion-easing-ease-out,
+ box-shadow $semantic-motion-duration-fast $semantic-motion-easing-ease-out;
+
+ // Mobile: Hidden by default, slide in when needed
+ @media (max-width: $semantic-breakpoint-md - 1) {
+ position: fixed;
+ top: 0;
+ left: 0;
+ height: 100vh;
+ z-index: $semantic-z-index-sticky;
+ transform: translateX(-100%);
+ box-shadow: 0 0 0 rgba(0, 0, 0, 0);
+
+ &:not(.collapsed) {
+ transform: translateX(0);
+ box-shadow: $semantic-shadow-lg;
+ }
+ }
+
+ // Tablet and up: Always visible unless collapsed
+ @media (min-width: $semantic-breakpoint-md) {
+ position: relative;
+ transform: translateX(0);
+
+ &.collapsed {
+ width: $semantic-spacing-16;
+ overflow: hidden;
+ }
+ }
+
+ // Desktop: Full sidebar width
+ @media (min-width: $semantic-breakpoint-lg) {
+ max-width: $semantic-spacing-72;
+ }
+}
+
+.dashboard-main {
+ flex: 1;
+ overflow: auto;
+ padding: $semantic-spacing-4;
+ background-color: var(--color-background-primary, #fafafa);
+ transition:
+ margin-left $semantic-motion-duration-normal $semantic-motion-easing-ease-in-out;
+
+ @media (min-width: $semantic-breakpoint-md) {
+ padding: $semantic-spacing-6;
+ }
+
+ @media (min-width: $semantic-breakpoint-lg) {
+ padding: $semantic-spacing-8;
+ }
+}
+
+.dashboard-footer {
+ padding: $semantic-spacing-4;
+ background-color: var(--color-background-surface, #fff);
+ border-top: 1px solid var(--color-border-subtle, #e5e7eb);
+ transition:
+ transform $semantic-motion-duration-normal $semantic-motion-easing-ease-out;
+
+ @media (min-width: $semantic-breakpoint-md) {
+ padding: $semantic-spacing-4 $semantic-spacing-6;
+ }
+}
+
+// Sidebar collapsed state adjustments
+.dashboard-shell.sidebar-collapsed {
+ .dashboard-main {
+ @media (min-width: $semantic-breakpoint-md) {
+ margin-left: 0;
+ }
+ }
+}
+
+// Mobile overlay when sidebar is open
+@media (max-width: $semantic-breakpoint-md - 1) {
+ .dashboard-shell:not(.sidebar-collapsed)::before {
+ content: '';
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.5);
+ z-index: $semantic-z-index-overlay;
+ opacity: 0;
+ animation: fadeIn $semantic-motion-duration-fast $semantic-motion-easing-ease-out forwards;
+ }
+}
+
+@keyframes fadeIn {
+ to {
+ opacity: 1;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/layouts/dashboard-shell-layout.component.ts b/projects/ui-essentials/src/lib/layouts/dashboard-shell-layout.component.ts
new file mode 100644
index 0000000..78640c9
--- /dev/null
+++ b/projects/ui-essentials/src/lib/layouts/dashboard-shell-layout.component.ts
@@ -0,0 +1,35 @@
+import { Component, Input } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+@Component({
+ selector: 'ui-dashboard-shell-layout',
+ standalone: true,
+ imports: [CommonModule],
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ styleUrl: './dashboard-shell-layout.component.scss'
+})
+export class DashboardShellLayoutComponent {
+ @Input() sidebarCollapsed: boolean = false;
+ @Input() showFooter: boolean = false;
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/layouts/feed-layout.component.scss b/projects/ui-essentials/src/lib/layouts/feed-layout.component.scss
new file mode 100644
index 0000000..6b45a22
--- /dev/null
+++ b/projects/ui-essentials/src/lib/layouts/feed-layout.component.scss
@@ -0,0 +1,297 @@
+@use "../../../../shared-ui/src/styles/semantic/index" as *;
+
+.feed-layout {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ overflow: hidden;
+ background-color: var(--color-background-primary, #fafafa);
+}
+
+.feed-layout__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: $semantic-spacing-4;
+ padding: $semantic-spacing-4;
+ background-color: var(--color-background-surface, #fff);
+ border-bottom: 1px solid var(--color-border-subtle, #e5e7eb);
+ z-index: $semantic-z-index-sticky;
+
+ @media (min-width: $semantic-breakpoint-md) {
+ padding: $semantic-spacing-6;
+ }
+}
+
+.feed-layout__title {
+ flex: 1;
+ min-width: 0; // Prevent flex item from overflowing
+}
+
+.feed-layout__header-actions {
+ display: flex;
+ align-items: center;
+ gap: $semantic-spacing-2;
+ flex-shrink: 0;
+}
+
+.feed-layout__filters {
+ padding: $semantic-spacing-4;
+ background-color: var(--color-background-subtle, #f9fafb);
+ border-bottom: 1px solid var(--color-border-subtle, #f3f4f6);
+ transition:
+ background-color $semantic-motion-duration-fast $semantic-motion-easing-ease-out;
+
+ @media (min-width: $semantic-breakpoint-md) {
+ padding: $semantic-spacing-4 $semantic-spacing-6;
+ }
+}
+
+.feed-layout__content {
+ flex: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+ position: relative;
+
+ // Smooth scrolling
+ scroll-behavior: smooth;
+
+ // Custom scrollbar
+ &::-webkit-scrollbar {
+ width: 8px;
+ }
+
+ &::-webkit-scrollbar-track {
+ background: var(--color-background-subtle, #f3f4f6);
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: var(--color-border-primary, #d1d5db);
+ border-radius: 4px;
+
+ &:hover {
+ background: var(--color-border-strong, #9ca3af);
+ }
+ }
+}
+
+.feed-layout__items {
+ padding: $semantic-spacing-4;
+
+ @media (min-width: $semantic-breakpoint-md) {
+ padding: $semantic-spacing-6;
+ }
+}
+
+// Standard feed variant
+.feed-layout--standard {
+ .feed-layout__items {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-4;
+
+ @media (min-width: $semantic-breakpoint-md) {
+ gap: $semantic-spacing-6;
+ }
+ }
+}
+
+// Card-based feed variant
+.feed-layout--card-based {
+ .feed-layout__items {
+ display: grid;
+ gap: $semantic-spacing-4;
+
+ // Mobile: Single column
+ grid-template-columns: 1fr;
+
+ // Tablet: Two columns
+ @media (min-width: $semantic-breakpoint-sm) {
+ grid-template-columns: repeat(2, 1fr);
+ gap: $semantic-spacing-5;
+ }
+
+ // Desktop: Three columns
+ @media (min-width: $semantic-breakpoint-lg) {
+ grid-template-columns: repeat(3, 1fr);
+ gap: $semantic-spacing-6;
+ }
+
+ // Large desktop: Four columns
+ @media (min-width: $semantic-sizing-breakpoint-wide) {
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ }
+ }
+}
+
+// Compact feed variant
+.feed-layout--compact {
+ .feed-layout__items {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-2;
+
+ @media (min-width: $semantic-breakpoint-md) {
+ gap: $semantic-spacing-3;
+ }
+ }
+}
+
+// Loading state
+.feed-layout__loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: $semantic-spacing-8;
+
+ // Animate loading appearance
+ animation: fadeIn $semantic-motion-duration-fast $semantic-motion-easing-ease-out;
+}
+
+// Load more section
+.feed-layout__load-more {
+ display: flex;
+ justify-content: center;
+ padding: $semantic-spacing-6;
+ border-top: 1px solid var(--color-border-subtle, #f3f4f6);
+ background-color: var(--color-background-surface, #fff);
+
+ transition:
+ background-color $semantic-motion-duration-fast $semantic-motion-easing-ease-out,
+ border-color $semantic-motion-duration-fast $semantic-motion-easing-ease-out;
+
+ &:hover {
+ background-color: var(--color-background-subtle, #f9fafb);
+ }
+}
+
+// Empty state
+.feed-layout__empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: $semantic-spacing-12;
+ text-align: center;
+ min-height: 400px;
+
+ animation: fadeInUp $semantic-motion-duration-normal $semantic-motion-easing-ease-out;
+}
+
+// Footer
+.feed-layout__footer {
+ padding: $semantic-spacing-4;
+ background-color: var(--color-background-surface, #fff);
+ border-top: 1px solid var(--color-border-subtle, #e5e7eb);
+
+ @media (min-width: $semantic-breakpoint-md) {
+ padding: $semantic-spacing-4 $semantic-spacing-6;
+ }
+}
+
+// Feed items animation (staggered appearance)
+.feed-layout__items > * {
+ animation: feedItemSlideIn $semantic-motion-duration-normal $semantic-motion-easing-ease-out;
+ animation-fill-mode: both;
+
+ @for $i from 1 through 20 {
+ &:nth-child(#{$i}) {
+ animation-delay: calc(#{$semantic-motion-transition-delay-75} * #{$i});
+ }
+ }
+}
+
+// Infinite scroll loading indicator
+.feed-layout__content.infinite-scroll {
+ .feed-layout__loading {
+ position: sticky;
+ bottom: 0;
+ @include glass-effect('frosted', $css-glass-blur-md, true, true, true);
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 1px;
+ background: linear-gradient(
+ 90deg,
+ transparent,
+ var(--color-primary, #3b82f6),
+ transparent
+ );
+ animation: shimmer $semantic-motion-duration-slower linear infinite;
+ }
+ }
+}
+
+// Responsive adjustments for small screens
+@media (max-width: $semantic-breakpoint-sm) {
+ .feed-layout__header {
+ flex-direction: column;
+ align-items: stretch;
+ gap: $semantic-spacing-3;
+ padding: $semantic-spacing-3;
+ }
+
+ .feed-layout__header-actions {
+ justify-content: center;
+ }
+
+ .feed-layout__items {
+ padding: $semantic-spacing-3;
+ }
+
+ .feed-layout__empty {
+ padding: $semantic-spacing-8;
+ min-height: 300px;
+ }
+}
+
+// Animation keyframes
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes fadeInUp {
+ from {
+ opacity: 0;
+ transform: translateY($semantic-spacing-6);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes feedItemSlideIn {
+ from {
+ opacity: 0;
+ transform: translateY($semantic-spacing-4);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes shimmer {
+ 0% {
+ transform: translateX(-100%);
+ }
+ 100% {
+ transform: translateX(100%);
+ }
+}
+
+// Focus states for accessibility
+.feed-layout__content:focus-visible {
+ outline: 2px solid var(--color-primary, #3b82f6);
+ outline-offset: -2px;
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/layouts/feed-layout.component.ts b/projects/ui-essentials/src/lib/layouts/feed-layout.component.ts
new file mode 100644
index 0000000..9c7cb6e
--- /dev/null
+++ b/projects/ui-essentials/src/lib/layouts/feed-layout.component.ts
@@ -0,0 +1,58 @@
+import { Component, Input } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+@Component({
+ selector: 'ui-feed-layout',
+ standalone: true,
+ imports: [CommonModule],
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ styleUrl: './feed-layout.component.scss'
+})
+export class FeedLayoutComponent {
+ @Input() variant: 'standard' | 'card-based' | 'compact' = 'standard';
+ @Input() showHeader: boolean = true;
+ @Input() showFilters: boolean = false;
+ @Input() showLoadMore: boolean = true;
+ @Input() showFooter: boolean = false;
+ @Input() infiniteScroll: boolean = false;
+ @Input() loading: boolean = false;
+ @Input() isEmpty: boolean = false;
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/layouts/grid-container.component.ts b/projects/ui-essentials/src/lib/layouts/grid-container.component.ts
new file mode 100644
index 0000000..fe61884
--- /dev/null
+++ b/projects/ui-essentials/src/lib/layouts/grid-container.component.ts
@@ -0,0 +1,156 @@
+import { Component, Input, OnInit, signal, computed } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+export type GridColumns = 1 | 2 | 3 | 4 | 5 | 6 | 8 | 12;
+export type GridGap = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
+export type GridJustify = 'start' | 'end' | 'center' | 'between' | 'around' | 'evenly';
+export type GridAlign = 'start' | 'end' | 'center' | 'stretch' | 'baseline';
+
+export interface GridResponsiveConfig {
+ mobile?: GridColumns;
+ tablet?: GridColumns;
+ desktop?: GridColumns;
+ wide?: GridColumns;
+}
+
+@Component({
+ selector: 'ui-grid-container',
+ standalone: true,
+ imports: [CommonModule],
+ template: `
+
+
+
+
+ @if (showDebugGrid()) {
+
+ @for (item of debugGridItems(); track $index) {
+
{{ $index + 1 }}
+ }
+
+ }
+
+ `,
+ styles: [`
+ .grid-container {
+ display: grid;
+ width: 100%;
+ gap: 1rem;
+ }
+
+ .grid-container.debug-mode {
+ border: 2px dashed #007bff;
+ }
+
+ .debug-grid-cell {
+ background: rgba(0, 123, 255, 0.1);
+ border: 1px solid #007bff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ `]
+})
+export class GridContainerComponent implements OnInit {
+ // Grid configuration inputs
+ @Input() columns: GridColumns = 12;
+ @Input() responsive: GridResponsiveConfig | null = null;
+ @Input() gap: GridGap = 'md';
+ @Input() justify: GridJustify = 'start';
+ @Input() align: GridAlign = 'stretch';
+
+ // Behavior inputs
+ @Input() autoFit = false; // Auto-fit columns based on min-width
+ @Input() autoFill = false; // Auto-fill with available space
+ @Input() minItemWidth = '200px'; // Minimum width for auto-fit/fill
+ @Input() maxItemWidth = '1fr'; // Maximum width for auto-fit/fill
+
+ // Debug and accessibility
+ @Input() debugMode = false;
+ @Input() label: string | null = null;
+ @Input() description: string | null = null;
+
+ // Reactive state
+ private currentBreakpoint = signal<'mobile' | 'tablet' | 'desktop' | 'wide'>('desktop');
+
+ // Computed properties
+ protected effectiveColumns = computed(() => {
+ if (this.autoFit || this.autoFill) {
+ return 'auto';
+ }
+
+ if (this.responsive) {
+ const breakpoint = this.currentBreakpoint();
+ return this.responsive[breakpoint] ?? this.columns;
+ }
+
+ return this.columns;
+ });
+
+ protected gridClasses = computed(() => {
+ const classes = ['grid-container'];
+
+ if (this.autoFit) classes.push('grid-auto-fit');
+ if (this.autoFill) classes.push('grid-auto-fill');
+ if (this.debugMode) classes.push('debug-mode');
+
+ classes.push(`gap-${this.gap}`);
+ classes.push(`justify-${this.justify}`);
+ classes.push(`align-${this.align}`);
+
+ return classes.join(' ');
+ });
+
+ protected showDebugGrid = computed(() => this.debugMode);
+ protected debugGridItems = computed(() => {
+ const cols = this.effectiveColumns();
+ if (cols === 'auto') return Array(12).fill(0); // Show 12 for auto
+ return Array(typeof cols === 'number' ? cols : 12).fill(0);
+ });
+
+ ngOnInit(): void {
+ this.setupBreakpointDetection();
+ }
+
+ /**
+ * Sets up responsive breakpoint detection using ResizeObserver
+ */
+ private setupBreakpointDetection(): void {
+ if (typeof window === 'undefined' || !this.responsive) return;
+
+ const mediaQueries = {
+ mobile: '(max-width: 767px)',
+ tablet: '(min-width: 768px) and (max-width: 1023px)',
+ desktop: '(min-width: 1024px) and (max-width: 1439px)',
+ wide: '(min-width: 1440px)'
+ };
+
+ // Set initial breakpoint
+ this.updateBreakpoint(mediaQueries);
+
+ // Listen for changes
+ Object.entries(mediaQueries).forEach(([, query]) => {
+ const mql = window.matchMedia(query);
+ mql.addEventListener('change', () => this.updateBreakpoint(mediaQueries));
+ });
+ }
+
+ /**
+ * Updates the current breakpoint based on media queries
+ */
+ private updateBreakpoint(queries: Record): void {
+ for (const [breakpoint, query] of Object.entries(queries)) {
+ if (window.matchMedia(query).matches) {
+ this.currentBreakpoint.set(breakpoint as any);
+ break;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/layouts/index.ts b/projects/ui-essentials/src/lib/layouts/index.ts
new file mode 100644
index 0000000..02a526b
--- /dev/null
+++ b/projects/ui-essentials/src/lib/layouts/index.ts
@@ -0,0 +1,15 @@
+// Layout Components Barrel Export
+export * from './dashboard-shell-layout.component';
+export * from './widget-grid-layout.component';
+export * from './kpi-card-layout.component';
+export * from './bento-grid-layout.component';
+export * from './list-detail-layout.component';
+export * from './feed-layout.component';
+export * from './supporting-pane-layout.component';
+export * from './widget-container.component';
+
+// New Content Container Components
+export * from './grid-container.component';
+export * from './scroll-container.component';
+export * from './tab-container.component';
+export * from './loading-state-container.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/layouts/kpi-card-layout.component.scss b/projects/ui-essentials/src/lib/layouts/kpi-card-layout.component.scss
new file mode 100644
index 0000000..2c95af4
--- /dev/null
+++ b/projects/ui-essentials/src/lib/layouts/kpi-card-layout.component.scss
@@ -0,0 +1,222 @@
+@use "../../../../shared-ui/src/styles/semantic/index" as *;
+
+.kpi-card {
+ background-color: var(--color-background-surface, #fff);
+ border: 1px solid var(--color-border-subtle, #e5e7eb);
+ border-radius: $semantic-border-radius-md;
+ overflow: hidden;
+ transition:
+ transform $semantic-motion-duration-fast $semantic-motion-easing-ease-out,
+ box-shadow $semantic-motion-duration-fast $semantic-motion-easing-ease-out,
+ border-color $semantic-motion-duration-fast $semantic-motion-easing-ease-out;
+
+ &:hover {
+ transform: $semantic-motion-hover-transform-lift-sm;
+ box-shadow: $semantic-shadow-md;
+ border-color: var(--color-border-primary, #d1d5db);
+ }
+}
+
+.kpi-card__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: $semantic-spacing-4;
+ border-bottom: 1px solid var(--color-border-subtle, #f3f4f6);
+ background-color: var(--color-background-subtle, #fafafa);
+
+ .kpi-card--sm & {
+ padding: $semantic-spacing-3;
+ }
+
+ .kpi-card--lg & {
+ padding: $semantic-spacing-5;
+ }
+}
+
+.kpi-card__actions {
+ display: flex;
+ align-items: center;
+ gap: $semantic-spacing-2;
+}
+
+.kpi-card__content {
+ padding: $semantic-spacing-6;
+
+ .kpi-card--sm & {
+ padding: $semantic-spacing-4;
+ }
+
+ .kpi-card--lg & {
+ padding: $semantic-spacing-8;
+ }
+
+ // Mobile: Stack primary and secondary vertically
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-4;
+
+ // Tablet and up: Side by side if there's secondary content
+ @media (min-width: $semantic-breakpoint-md) {
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ gap: $semantic-spacing-6;
+ }
+}
+
+.kpi-card__primary {
+ flex: 1;
+
+ // Center align on mobile
+ text-align: center;
+
+ @media (min-width: $semantic-breakpoint-md) {
+ text-align: left;
+ }
+}
+
+.kpi-card__value {
+ font-size: $semantic-typography-font-size-3xl;
+ font-weight: $semantic-typography-font-weight-bold;
+ line-height: $semantic-typography-line-height-tight;
+ color: var(--color-text-primary, #111827);
+ margin-bottom: $semantic-spacing-2;
+
+ .kpi-card--sm & {
+ font-size: $semantic-typography-font-size-2xl;
+ }
+
+ .kpi-card--lg & {
+ font-size: $semantic-typography-font-size-4xl;
+ margin-bottom: $semantic-spacing-3;
+ }
+
+ // Animate number changes
+ transition:
+ transform $semantic-motion-duration-fast $semantic-motion-easing-spring,
+ color $semantic-motion-duration-fast $semantic-motion-easing-ease-out;
+
+ &:hover {
+ transform: $semantic-motion-hover-transform-scale-sm;
+ }
+}
+
+.kpi-card__label {
+ font-size: $semantic-typography-font-size-sm;
+ font-weight: $semantic-typography-font-weight-medium;
+ color: var(--color-text-secondary, #6b7280);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+
+ .kpi-card--lg & {
+ font-size: $semantic-typography-font-size-md;
+ }
+}
+
+.kpi-card__secondary {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: $semantic-spacing-3;
+
+ @media (min-width: $semantic-breakpoint-md) {
+ flex-shrink: 0;
+ align-items: flex-end;
+ }
+}
+
+.kpi-card__trend {
+ display: flex;
+ align-items: center;
+ gap: $semantic-spacing-2;
+ padding: $semantic-spacing-2 $semantic-spacing-3;
+ background-color: var(--color-background-subtle, #f9fafb);
+ border-radius: $semantic-border-radius-full;
+ font-size: $semantic-typography-font-size-sm;
+ font-weight: $semantic-typography-font-weight-medium;
+
+ transition:
+ background-color $semantic-motion-duration-fast $semantic-motion-easing-ease-out,
+ transform $semantic-motion-duration-fast $semantic-motion-easing-ease-out;
+
+ &:hover {
+ background-color: var(--color-background-secondary, #f3f4f6);
+ transform: $semantic-motion-hover-transform-scale-sm;
+ }
+}
+
+.kpi-card__chart {
+ width: 100%;
+ max-width: 120px;
+ height: 60px;
+
+ .kpi-card--sm & {
+ max-width: 80px;
+ height: 40px;
+ }
+
+ .kpi-card--lg & {
+ max-width: 160px;
+ height: 80px;
+ }
+}
+
+.kpi-card__footer {
+ padding: $semantic-spacing-4;
+ border-top: 1px solid var(--color-border-subtle, #f3f4f6);
+ background-color: var(--color-background-subtle, #fafafa);
+
+ .kpi-card--sm & {
+ padding: $semantic-spacing-3;
+ }
+
+ .kpi-card--lg & {
+ padding: $semantic-spacing-5;
+ }
+}
+
+// Responsive adjustments for very small screens
+@media (max-width: $semantic-breakpoint-sm) {
+ .kpi-card__content {
+ padding: $semantic-spacing-4;
+
+ .kpi-card--sm & {
+ padding: $semantic-spacing-3;
+ }
+ }
+
+ .kpi-card__value {
+ font-size: $semantic-typography-font-size-2xl;
+
+ .kpi-card--sm & {
+ font-size: $semantic-typography-font-size-xl;
+ }
+ }
+}
+
+// Animation for appearing content
+.kpi-card__value,
+.kpi-card__trend,
+.kpi-card__chart {
+ animation: fadeInUp $semantic-motion-duration-normal $semantic-motion-easing-ease-out;
+}
+
+.kpi-card__trend {
+ animation-delay: $semantic-motion-transition-delay-100;
+}
+
+.kpi-card__chart {
+ animation-delay: $semantic-motion-transition-delay-200;
+}
+
+@keyframes fadeInUp {
+ from {
+ opacity: 0;
+ transform: translateY($semantic-spacing-2);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/layouts/kpi-card-layout.component.ts b/projects/ui-essentials/src/lib/layouts/kpi-card-layout.component.ts
new file mode 100644
index 0000000..6d2bda2
--- /dev/null
+++ b/projects/ui-essentials/src/lib/layouts/kpi-card-layout.component.ts
@@ -0,0 +1,50 @@
+import { Component, Input } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+@Component({
+ selector: 'ui-kpi-card-layout',
+ standalone: true,
+ imports: [CommonModule],
+ template: `
+
+ `,
+ styleUrl: './kpi-card-layout.component.scss'
+})
+export class KpiCardLayoutComponent {
+ @Input() size: 'sm' | 'md' | 'lg' = 'md';
+ @Input() showHeader: boolean = true;
+ @Input() showTrend: boolean = true;
+ @Input() showChart: boolean = false;
+ @Input() showFooter: boolean = false;
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/layouts/list-detail-layout.component.scss b/projects/ui-essentials/src/lib/layouts/list-detail-layout.component.scss
new file mode 100644
index 0000000..cab31b1
--- /dev/null
+++ b/projects/ui-essentials/src/lib/layouts/list-detail-layout.component.scss
@@ -0,0 +1,256 @@
+@use "../../../../shared-ui/src/styles/semantic/index" as *;
+
+.list-detail-layout {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ overflow: hidden;
+ background-color: var(--color-background-primary, #fafafa);
+}
+
+.list-detail-layout__toolbar {
+ padding: $semantic-spacing-4;
+ background-color: var(--color-background-surface, #fff);
+ border-bottom: 1px solid var(--color-border-subtle, #e5e7eb);
+ z-index: $semantic-z-index-fixed;
+
+ @media (min-width: $semantic-breakpoint-md) {
+ padding: $semantic-spacing-4 $semantic-spacing-6;
+ }
+}
+
+.list-detail-layout__content {
+ flex: 1;
+ display: flex;
+ overflow: hidden;
+
+ // Mobile: Stack views, show one at a time
+ flex-direction: column;
+
+ // Tablet and up: Side by side
+ @media (min-width: $semantic-breakpoint-md) {
+ flex-direction: row;
+ }
+}
+
+.list-detail-layout__list {
+ background-color: var(--color-background-surface, #fff);
+ border-right: 1px solid var(--color-border-subtle, #e5e7eb);
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ transition:
+ width $semantic-motion-duration-normal $semantic-motion-easing-ease-in-out,
+ flex $semantic-motion-duration-normal $semantic-motion-easing-ease-in-out,
+ transform $semantic-motion-duration-normal $semantic-motion-easing-ease-out;
+
+ // Mobile: Full width when visible
+ width: 100%;
+
+ // Tablet and up: Split based on ratio
+ @media (min-width: $semantic-breakpoint-md) {
+ flex-shrink: 0;
+ }
+
+ &.hidden-on-mobile {
+ @media (max-width: $semantic-breakpoint-md - 1) {
+ transform: translateX(-100%);
+ position: absolute;
+ z-index: -1;
+ }
+ }
+}
+
+.list-detail-layout__detail {
+ flex: 1;
+ background-color: var(--color-background-primary, #fff);
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ transition:
+ transform $semantic-motion-duration-normal $semantic-motion-easing-ease-out,
+ opacity $semantic-motion-duration-normal $semantic-motion-easing-ease-out;
+
+ &.hidden-on-mobile {
+ @media (max-width: $semantic-breakpoint-md - 1) {
+ transform: translateX(100%);
+ position: absolute;
+ z-index: -1;
+ opacity: 0;
+ }
+ }
+}
+
+// Split ratio variations
+.list-detail-layout--30-70 {
+ .list-detail-layout__list {
+ @media (min-width: $semantic-breakpoint-md) {
+ width: 30%;
+ }
+ }
+}
+
+.list-detail-layout--25-75 {
+ .list-detail-layout__list {
+ @media (min-width: $semantic-breakpoint-md) {
+ width: 25%;
+ }
+ }
+}
+
+.list-detail-layout--40-60 {
+ .list-detail-layout__list {
+ @media (min-width: $semantic-breakpoint-md) {
+ width: 40%;
+ }
+ }
+}
+
+// List section
+.list-detail-layout__list-header {
+ padding: $semantic-spacing-4;
+ border-bottom: 1px solid var(--color-border-subtle, #f3f4f6);
+ background-color: var(--color-background-subtle, #fafafa);
+
+ @media (min-width: $semantic-breakpoint-md) {
+ padding: $semantic-spacing-4 $semantic-spacing-5;
+ }
+}
+
+.list-detail-layout__list-content {
+ flex: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+}
+
+// Detail section
+.list-detail-layout__detail-header {
+ display: flex;
+ align-items: center;
+ gap: $semantic-spacing-3;
+ padding: $semantic-spacing-4;
+ border-bottom: 1px solid var(--color-border-subtle, #e5e7eb);
+ background-color: var(--color-background-surface, #fff);
+
+ @media (min-width: $semantic-breakpoint-md) {
+ padding: $semantic-spacing-4 $semantic-spacing-6;
+ }
+}
+
+.list-detail-layout__back-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: $semantic-spacing-2;
+ border-radius: $semantic-border-radius-md;
+ transition:
+ background-color $semantic-motion-duration-fast $semantic-motion-easing-ease-out,
+ transform $semantic-motion-duration-fast $semantic-motion-easing-ease-out;
+
+ &:hover {
+ background-color: var(--color-background-subtle, #f3f4f6);
+ transform: $semantic-motion-hover-transform-scale-sm;
+ }
+
+ // Hide back button on desktop
+ @media (min-width: $semantic-breakpoint-md) {
+ display: none;
+ }
+}
+
+.list-detail-layout__detail-content {
+ flex: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+ padding: $semantic-spacing-6;
+
+ @media (min-width: $semantic-breakpoint-md) {
+ padding: $semantic-spacing-8;
+ }
+
+ @media (min-width: $semantic-breakpoint-lg) {
+ padding: $semantic-spacing-10;
+ }
+}
+
+// Mobile detail view animations
+.list-detail-layout.mobile-detail-view {
+ @media (max-width: $semantic-breakpoint-md - 1) {
+ .list-detail-layout__detail {
+ animation: slideInRight $semantic-motion-duration-normal $semantic-motion-easing-ease-out;
+ }
+ }
+}
+
+.list-detail-layout:not(.mobile-detail-view) {
+ @media (max-width: $semantic-breakpoint-md - 1) {
+ .list-detail-layout__list {
+ animation: slideInLeft $semantic-motion-duration-normal $semantic-motion-easing-ease-out;
+ }
+ }
+}
+
+// Responsive adjustments for small screens
+@media (max-width: $semantic-breakpoint-sm) {
+ .list-detail-layout__list-header,
+ .list-detail-layout__detail-header {
+ padding: $semantic-spacing-3;
+ }
+
+ .list-detail-layout__detail-content {
+ padding: $semantic-spacing-4;
+ }
+}
+
+// Smooth scrolling for content areas
+.list-detail-layout__list-content,
+.list-detail-layout__detail-content {
+ scroll-behavior: smooth;
+
+ // Custom scrollbar styling
+ &::-webkit-scrollbar {
+ width: 6px;
+ }
+
+ &::-webkit-scrollbar-track {
+ background: var(--color-background-subtle, #f3f4f6);
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: var(--color-border-primary, #d1d5db);
+ border-radius: 3px;
+
+ &:hover {
+ background: var(--color-border-strong, #9ca3af);
+ }
+ }
+}
+
+// Animation keyframes
+@keyframes slideInRight {
+ from {
+ transform: translateX(100%);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+}
+
+@keyframes slideInLeft {
+ from {
+ transform: translateX(-100%);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+}
+
+// Enhanced focus states for accessibility
+.list-detail-layout__back-button:focus-visible {
+ outline: 2px solid var(--color-primary, #3b82f6);
+ outline-offset: 2px;
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/layouts/list-detail-layout.component.ts b/projects/ui-essentials/src/lib/layouts/list-detail-layout.component.ts
new file mode 100644
index 0000000..2a2a019
--- /dev/null
+++ b/projects/ui-essentials/src/lib/layouts/list-detail-layout.component.ts
@@ -0,0 +1,51 @@
+import { Component, Input } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+@Component({
+ selector: 'ui-list-detail-layout',
+ standalone: true,
+ imports: [CommonModule],
+ template: `
+
+ `,
+ styleUrl: './list-detail-layout.component.scss'
+})
+export class ListDetailLayoutComponent {
+ @Input() splitRatio: '30-70' | '25-75' | '40-60' = '30-70';
+ @Input() mobileDetailView: boolean = false;
+ @Input() showToolbar: boolean = false;
+ @Input() showListHeader: boolean = true;
+ @Input() showDetailHeader: boolean = true;
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/layouts/loading-state-container.component.ts b/projects/ui-essentials/src/lib/layouts/loading-state-container.component.ts
new file mode 100644
index 0000000..4c439da
--- /dev/null
+++ b/projects/ui-essentials/src/lib/layouts/loading-state-container.component.ts
@@ -0,0 +1,473 @@
+import {
+ Component,
+ Input,
+ Output,
+ EventEmitter,
+ OnInit,
+ OnDestroy,
+ ChangeDetectionStrategy,
+ signal,
+ computed,
+ TemplateRef,
+ ContentChild,
+ ViewChild,
+ ElementRef
+} from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { Subject, timer } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+
+export type LoadingState = 'idle' | 'loading' | 'success' | 'error';
+export type LoadingVariant = 'spinner' | 'skeleton' | 'pulse' | 'shimmer';
+export type LoadingSize = 'sm' | 'md' | 'lg' | 'xl';
+
+export interface ErrorState {
+ message: string;
+ code?: string;
+ details?: any;
+ retryable?: boolean;
+}
+
+export interface LoadingConfig {
+ variant: LoadingVariant;
+ size: LoadingSize;
+ message?: string;
+ showProgress?: boolean;
+ progress?: number;
+ timeout?: number;
+}
+
+@Component({
+ selector: 'ui-loading-state-container',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
+
+ @if (state === 'loading') {
+
+
+
+ @switch (config().variant) {
+ @case ('spinner') {
+
+
+ @if (config().message) {
+
{{ config().message }}
+ }
+ @if (config().showProgress && config().progress !== undefined) {
+
+
{{ config().progress }}%
+ }
+
+ }
+
+ @case ('skeleton') {
+ @if (skeletonTemplate) {
+
+
+ } @else {
+
+ @for (item of skeletonItems(); track $index) {
+
+
+ }
+
+ }
+ }
+
+ @case ('pulse') {
+
+ @if (config().message) {
+
{{ config().message }}
+ }
+ }
+
+ @case ('shimmer') {
+
+ }
+ }
+
+ @if (showCancelButton && state === 'loading') {
+
+ {{ cancelButtonText }}
+
+ }
+
+
+ }
+
+
+ @if (state === 'error') {
+
+
+
+ @if (errorTemplate) {
+
+
+ } @else {
+
⚠
+
{{ errorTitle }}
+
{{ error()?.message || defaultErrorMessage }}
+
+ @if (error()?.details && showErrorDetails) {
+
+ Technical Details
+ {{ error()?.details | json }}
+
+ }
+
+
+ @if (error()?.retryable !== false && showRetryButton) {
+
+ {{ retryButtonText }}
+
+ }
+
+ @if (showReportButton) {
+
+ {{ reportButtonText }}
+
+ }
+
+ }
+
+
+ }
+
+
+
+
+ @if (state === 'success' && successTemplate && showSuccessState) {
+
+
+
+ } @else {
+
+ }
+
+
+ `,
+ styles: [`
+ .loading-state-container {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ }
+
+ .loading-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(255, 255, 255, 0.8);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ }
+
+ .spinner {
+ width: 40px;
+ height: 40px;
+ border: 3px solid #f3f3f3;
+ border-top: 3px solid #007bff;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ }
+
+ @keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+ }
+
+ .error-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(255, 255, 255, 0.95);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ }
+
+ .main-content {
+ width: 100%;
+ height: 100%;
+ }
+ `]
+})
+export class LoadingStateContainerComponent implements OnInit, OnDestroy {
+ @ViewChild('container') containerElement!: ElementRef;
+
+ @ContentChild('skeleton') skeletonTemplate?: TemplateRef;
+ @ContentChild('error') errorTemplate?: TemplateRef<{ error: ErrorState | null }>;
+ @ContentChild('success') successTemplate?: TemplateRef;
+
+ // State management
+ @Input() state: LoadingState = 'idle';
+ @Input() error = signal(null);
+ @Input() config = signal({
+ variant: 'spinner',
+ size: 'md',
+ showProgress: false
+ });
+
+ // Loading behavior
+ @Input() showContentWhileLoading = false;
+ @Input() transparentOverlay = false;
+ @Input() minimumLoadingTime = 500; // ms
+ @Input() automaticTimeout = 0; // ms, 0 = no timeout
+
+ // Loading cancellation
+ @Input() showCancelButton = false;
+ @Input() cancelButtonText = 'Cancel';
+
+ // Error handling
+ @Input() errorTitle = 'Something went wrong';
+ @Input() defaultErrorMessage = 'An unexpected error occurred. Please try again.';
+ @Input() showRetryButton = true;
+ @Input() retryButtonText = 'Try Again';
+ @Input() showReportButton = false;
+ @Input() reportButtonText = 'Report Issue';
+ @Input() showErrorDetails = false;
+
+ // Success state
+ @Input() showSuccessState = false;
+ @Input() successStateDuration = 2000; // ms
+
+ // Skeleton loading
+ @Input() skeletonItems = signal([
+ { width: '100%', height: '24px' },
+ { width: '80%', height: '20px' },
+ { width: '60%', height: '20px' }
+ ]);
+
+ // Events
+ @Output() cancel = new EventEmitter();
+ @Output() retryClicked = new EventEmitter();
+ @Output() reportErrorClicked = new EventEmitter();
+ @Output() timeout = new EventEmitter();
+ @Output() stateChange = new EventEmitter();
+
+ private destroy$ = new Subject();
+ private loadingStartTime = 0;
+ private timeoutTimer?: any;
+ private successTimer?: any;
+
+ // Computed properties
+ protected containerClasses = computed(() => {
+ const classes = ['loading-state-container'];
+
+ if (this.transparentOverlay) classes.push('transparent-overlay');
+ if (this.showContentWhileLoading) classes.push('show-content-while-loading');
+
+ return classes.join(' ');
+ });
+
+ ngOnInit(): void {
+ this.setupStateWatching();
+ }
+
+ ngOnDestroy(): void {
+ this.cleanup();
+ this.destroy$.next();
+ this.destroy$.complete();
+ }
+
+ /**
+ * Sets the loading state with optional configuration
+ */
+ setLoadingState(config?: Partial): void {
+ if (config) {
+ this.config.update(current => ({ ...current, ...config }));
+ }
+
+ this.loadingStartTime = Date.now();
+ this.setState('loading');
+
+ // Set up automatic timeout if configured
+ if (this.automaticTimeout > 0) {
+ this.timeoutTimer = setTimeout(() => {
+ this.handleTimeout();
+ }, this.automaticTimeout);
+ }
+ }
+
+ /**
+ * Sets the error state
+ */
+ setErrorState(error: ErrorState): void {
+ this.error.set(error);
+ this.setState('error');
+ }
+
+ /**
+ * Sets the success state
+ */
+ setSuccessState(): void {
+ this.setState('success');
+
+ if (this.showSuccessState && this.successStateDuration > 0) {
+ this.successTimer = setTimeout(() => {
+ this.setState('idle');
+ }, this.successStateDuration);
+ }
+ }
+
+ /**
+ * Sets the idle state
+ */
+ setIdleState(): void {
+ this.setState('idle');
+ }
+
+ /**
+ * Cancels the current loading operation
+ */
+ cancelLoading(): void {
+ if (this.state !== 'loading') return;
+
+ this.cleanup();
+ this.setState('idle');
+ this.cancel.emit();
+ }
+
+ /**
+ * Retries the failed operation
+ */
+ handleRetry(): void {
+ if (this.state !== 'error') return;
+
+ this.error.set(null);
+ this.retryClicked.emit();
+ }
+
+ /**
+ * Reports the current error
+ */
+ handleReportError(): void {
+ const currentError = this.error();
+ if (currentError) {
+ this.reportErrorClicked.emit(currentError);
+ }
+ }
+
+ /**
+ * Updates the loading progress (for progress variant)
+ */
+ updateProgress(progress: number): void {
+ this.config.update(config => ({
+ ...config,
+ progress: Math.max(0, Math.min(100, progress))
+ }));
+ }
+
+ /**
+ * Updates the loading message
+ */
+ updateMessage(message: string): void {
+ this.config.update(config => ({ ...config, message }));
+ }
+
+ private setState(newState: LoadingState): void {
+ const previousState = this.state;
+ this.state = newState;
+
+ // Ensure minimum loading time
+ if (previousState === 'loading' && newState !== 'loading') {
+ const elapsedTime = Date.now() - this.loadingStartTime;
+ const remainingTime = this.minimumLoadingTime - elapsedTime;
+
+ if (remainingTime > 0) {
+ setTimeout(() => {
+ this.finalizeStateChange(newState);
+ }, remainingTime);
+ return;
+ }
+ }
+
+ this.finalizeStateChange(newState);
+ }
+
+ private finalizeStateChange(state: LoadingState): void {
+ this.state = state;
+ this.stateChange.emit(state);
+ this.cleanup();
+ }
+
+ private handleTimeout(): void {
+ this.setState('error');
+ this.error.set({
+ message: 'The operation timed out. Please try again.',
+ code: 'TIMEOUT',
+ retryable: true
+ });
+ this.timeout.emit();
+ }
+
+ private setupStateWatching(): void {
+ // Watch for external state changes
+ timer(0, 100)
+ .pipe(takeUntil(this.destroy$))
+ .subscribe(() => {
+ // This allows for reactive updates if state is changed externally
+ });
+ }
+
+ private cleanup(): void {
+ if (this.timeoutTimer) {
+ clearTimeout(this.timeoutTimer);
+ this.timeoutTimer = undefined;
+ }
+
+ if (this.successTimer) {
+ clearTimeout(this.successTimer);
+ this.successTimer = undefined;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/layouts/scroll-container.component.ts b/projects/ui-essentials/src/lib/layouts/scroll-container.component.ts
new file mode 100644
index 0000000..43d024c
--- /dev/null
+++ b/projects/ui-essentials/src/lib/layouts/scroll-container.component.ts
@@ -0,0 +1,462 @@
+import {
+ Component,
+ Input,
+ OnInit,
+ OnDestroy,
+ AfterViewInit,
+ ElementRef,
+ ViewChild,
+ signal,
+ computed,
+ Output,
+ EventEmitter,
+ TemplateRef,
+ ContentChild
+} from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { Subject } from 'rxjs';
+import { takeUntil, throttleTime } from 'rxjs/operators';
+import { fromEvent } from 'rxjs';
+
+export type ScrollDirection = 'vertical' | 'horizontal' | 'both';
+export type ScrollbarVisibility = 'auto' | 'always' | 'never' | 'hover';
+
+export interface VirtualScrollConfig {
+ itemHeight: number;
+ bufferSize?: number;
+ enableDynamicHeight?: boolean;
+}
+
+export interface ScrollPosition {
+ top: number;
+ left: number;
+}
+
+export interface ScrollEvent {
+ position: ScrollPosition;
+ percentage: { x: number; y: number };
+ isAtTop: boolean;
+ isAtBottom: boolean;
+ isAtLeft: boolean;
+ isAtRight: boolean;
+}
+
+@Component({
+ selector: 'ui-scroll-container',
+ standalone: true,
+ imports: [CommonModule],
+ template: `
+
+ `,
+ styles: [`
+ .scroll-container {
+ position: relative;
+ overflow: auto;
+ width: 100%;
+ height: 100%;
+ }
+
+ .scroll-container[data-direction="vertical"] {
+ overflow-x: hidden;
+ overflow-y: auto;
+ }
+
+ .scroll-container[data-direction="horizontal"] {
+ overflow-x: auto;
+ overflow-y: hidden;
+ }
+
+ .scroll-container[data-scrollbar="never"] {
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+ }
+
+ .scroll-container[data-scrollbar="never"]::-webkit-scrollbar {
+ width: 0px;
+ background: transparent;
+ }
+
+
+ .scroll-container[data-scrollbar="hover"]::-webkit-scrollbar {
+ opacity: 0;
+ transition: opacity 0.3s ease;
+ }
+
+ .scroll-container[data-scrollbar="hover"]:hover::-webkit-scrollbar {
+ opacity: 1;
+ }
+ `]
+})
+export class ScrollContainerComponent implements OnInit, AfterViewInit, OnDestroy {
+ @ViewChild('scrollContainer', { static: true }) scrollContainer!: ElementRef;
+ @ContentChild('itemTemplate') itemTemplate?: TemplateRef;
+
+ // Configuration inputs
+ @Input() direction: ScrollDirection = 'vertical';
+ @Input() scrollbarVisibility: ScrollbarVisibility = 'auto';
+ @Input() customScrollbars = false;
+ @Input() showScrollIndicators = false;
+ @Input() smoothScrolling = true;
+
+ // Virtual scrolling
+ @Input() virtualScrolling = false;
+ @Input() virtualConfig: VirtualScrollConfig | null = null;
+ @Input() items: any[] = [];
+ @Input() trackBy?: (index: number, item: any) => any;
+
+ // Accessibility
+ @Input() role: string | null = null;
+ @Input() ariaLabel: string | null = null;
+ @Input() tabIndex = -1;
+
+ // Styling
+ @Input() maxHeight: string | null = null;
+ @Input() maxWidth: string | null = null;
+
+ // Events
+ @Output() scroll = new EventEmitter();
+ @Output() scrollEnd = new EventEmitter();
+ @Output() reachStart = new EventEmitter