diff --git a/projects/demo-ui-essentials/src/app/app.component.ts b/projects/demo-ui-essentials/src/app/app.component.ts
index 3f9ef7a..69d8418 100644
--- a/projects/demo-ui-essentials/src/app/app.component.ts
+++ b/projects/demo-ui-essentials/src/app/app.component.ts
@@ -1,12 +1,10 @@
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, FabComponent, DashboardComponent],
+ imports: [DashboardComponent],
template: `
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
index a7c6281..9bc13bf 100644
--- 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
@@ -1,6 +1,6 @@
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
-import { AppbarComponent } from '../../../../../ui-essentials/src/lib/components/navigation/appbar';
+import { AppbarComponent } from '../../../../../ui-essentials/src/lib/components/navigation/appbar/appbar.component';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import {
faBars,
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
index 8ded9d3..dd0eaa2 100644
--- 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
@@ -1,7 +1,7 @@
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';
+import { AutocompleteComponent, AutocompleteOption } from '../../../../../ui-essentials/src/lib/components/forms/autocomplete/autocomplete.component';
@Component({
selector: 'ui-autocomplete-demo',
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
index 8ea8501..1c6d5d0 100644
--- 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
@@ -1,6 +1,6 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
-import { AvatarComponent } from '../../../../../ui-essentials/src/lib/components/data-display/avatar';
+import { AvatarComponent } from '../../../../../ui-essentials/src/lib/components/data-display/avatar/avatar.component';
interface Activity {
user: string;
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
index 5404e45..b08e017 100644
--- 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
@@ -1,6 +1,6 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
-import { BackdropComponent } from '../../../../../ui-essentials/src/lib/components/overlays';
+import { BackdropComponent } from '../../../../../ui-essentials/src/lib/components/overlays/backdrop/backdrop.component';
@Component({
selector: 'ui-backdrop-demo',
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
index 3a52e40..85f5759 100644
--- 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
@@ -1,6 +1,6 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
-import { BadgeComponent } from '../../../../../ui-essentials/src/lib/components/data-display/badge';
+import { BadgeComponent } from '../../../../../ui-essentials/src/lib/components/data-display/badge/badge.component';
@Component({
selector: 'ui-badge-demo',
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
index f9c7eb6..fd44a04 100644
--- 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
@@ -1,10 +1,10 @@
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 { ButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons/button.component';
+import { TextButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons/text-button.component';
+import { GhostButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons/ghost-button.component';
+import { FabComponent } from '../../../../../ui-essentials/src/lib/components/buttons/fab.component';
+import { SimpleButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons/simple-button.component';
import {
faDownload,
faPlus,
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
index 6d0d7e8..9a03ba7 100644
--- 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
@@ -1,7 +1,7 @@
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';
+import { CardComponent, GlassVariant } from '../../../../../ui-essentials/src/lib/components/data-display/card/card.component';
+import { ButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons/button.component';
@Component({
selector: 'ui-card-demo',
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
index ca0c76d..ba9749d 100644
--- 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
@@ -1,6 +1,6 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
-import { CarouselComponent, CarouselItem } from '../../../../../ui-essentials/src/lib/components/data-display/carousel';
+import { CarouselComponent, CarouselItem } from '../../../../../ui-essentials/src/lib/components/data-display/carousel/carousel.component';
@Component({
selector: 'ui-carousel-demo',
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
index 57a30ef..31aa4ad 100644
--- 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
@@ -1,7 +1,7 @@
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';
+import { CheckboxComponent } from '../../../../../ui-essentials/src/lib/components/forms/checkbox/checkbox.component';
@Component({
selector: 'ui-checkbox-demo',
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
index 30f7220..370b17e 100644
--- 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
@@ -1,7 +1,7 @@
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 { ChipComponent } from '../../../../../ui-essentials/src/lib/components/data-display/chip/chip.component';
+import { ButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons/button.component';
import {
faHeart,
faUser,
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
index 58f347a..b063b78 100644
--- 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
@@ -1,6 +1,7 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
-import { ContainerComponent, GridSystemComponent } from '../../../../../ui-essentials/src/lib/components/layout';
+import { ContainerComponent } from '../../../../../ui-essentials/src/lib/components/layout/container/container.component';
+import { GridSystemComponent } from '../../../../../ui-essentials/src/lib/components/layout/grid-system/grid-system.component';
@Component({
selector: 'ui-container-demo',
diff --git a/projects/demo-ui-essentials/src/app/demos/demos.routes.ts b/projects/demo-ui-essentials/src/app/demos/demos.routes.ts
index 81281ad..b772630 100644
--- a/projects/demo-ui-essentials/src/app/demos/demos.routes.ts
+++ b/projects/demo-ui-essentials/src/app/demos/demos.routes.ts
@@ -39,6 +39,10 @@ import { AutocompleteDemoComponent } from './autocomplete-demo/autocomplete-demo
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';
+import { ProgressCircleDemoComponent } from './progress-circle-demo/progress-circle-demo.component';
+import { RangeSliderDemoComponent } from './range-slider-demo/range-slider-demo.component';
+import { DividerDemoComponent } from './divider-demo/divider-demo.component';
+import { TooltipDemoComponent } from './tooltip-demo/tooltip-demo.component';
@Component({
@@ -193,12 +197,28 @@ import { LoadingSpinnerDemoComponent } from './loading-spinner-demo/loading-spin
}
+ @case ("progress-circle") {
+
+ }
+
+ @case ("range-slider") {
+
+ }
+
+ @case ("divider") {
+
+ }
+
+ @case ("tooltip") {
+
+ }
+
}
`,
imports: [AvatarDemoComponent, ButtonDemoComponent, CardDemoComponent,
ChipDemoComponent, TableDemoComponent, BadgeDemoComponent,
- MenuDemoComponent, InputDemoComponent, InputDemoComponent,
+ MenuDemoComponent, InputDemoComponent,
LayoutDemoComponent, RadioDemoComponent, CheckboxDemoComponent,
SearchDemoComponent, SwitchDemoComponent, ProgressDemoComponent,
AppbarDemoComponent, FontAwesomeDemoComponent, ImageContainerDemoComponent,
@@ -206,7 +226,8 @@ import { LoadingSpinnerDemoComponent } from './loading-spinner-demo/loading-spin
ModalDemoComponent, DrawerDemoComponent, DatePickerDemoComponent, TimePickerDemoComponent,
GridSystemDemoComponent, SpacerDemoComponent, ContainerDemoComponent, PaginationDemoComponent,
SkeletonLoaderDemoComponent, EmptyStateDemoComponent, FileUploadDemoComponent, FormFieldDemoComponent,
- AutocompleteDemoComponent, BackdropDemoComponent, OverlayContainerDemoComponent, LoadingSpinnerDemoComponent]
+ AutocompleteDemoComponent, BackdropDemoComponent, OverlayContainerDemoComponent, LoadingSpinnerDemoComponent,
+ ProgressCircleDemoComponent, RangeSliderDemoComponent, DividerDemoComponent, TooltipDemoComponent]
})
diff --git a/projects/demo-ui-essentials/src/app/demos/divider-demo/divider-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/divider-demo/divider-demo.component.scss
new file mode 100644
index 0000000..5735c7f
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/divider-demo/divider-demo.component.scss
@@ -0,0 +1,47 @@
+@use "../../../../../shared-ui/src/styles/semantic/index" as *;
+
+.demo-container {
+ padding: $semantic-spacing-layout-md;
+ max-width: 800px;
+ margin: 0 auto;
+}
+
+.demo-section {
+ margin-bottom: $semantic-spacing-layout-lg;
+
+ h3 {
+ margin-bottom: $semantic-spacing-content-paragraph;
+ color: $semantic-color-text-primary;
+ font-size: $semantic-typography-font-size-lg;
+ }
+}
+
+.demo-row {
+ display: flex;
+ gap: $semantic-spacing-component-md;
+ align-items: center;
+ margin-bottom: $semantic-spacing-component-sm;
+}
+
+.demo-column {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-component-lg;
+
+ > div {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-component-sm;
+ }
+}
+
+p {
+ margin: 0;
+ color: $semantic-color-text-secondary;
+ font-size: $semantic-typography-font-size-md;
+}
+
+span {
+ color: $semantic-color-text-primary;
+ font-size: $semantic-typography-font-size-md;
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/divider-demo/divider-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/divider-demo/divider-demo.component.ts
new file mode 100644
index 0000000..688b332
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/divider-demo/divider-demo.component.ts
@@ -0,0 +1,104 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { DividerComponent } from '../../../../../ui-essentials/src/public-api';
+
+@Component({
+ selector: 'ui-divider-demo',
+ standalone: true,
+ imports: [CommonModule, DividerComponent],
+ template: `
+
+
Divider Demo
+
+
+
+ Orientation
+
+
+
Content above
+
+
Content below
+
+
+
+
Left content
+
+
Right content
+
+
+
+
+
+
+
+
+
+
+
+ With Content
+
+ OR
+ Section Break
+ More Content
+
+
+
+
+
+ Combined Examples
+
+ Dashed Thin
+ Dotted Thick
+
+
+
+
+
+ Vertical Examples
+
+ Item 1
+
+ Item 2
+
+ Item 3
+
+ Item 4
+
+
+
+ `,
+ styleUrl: './divider-demo.component.scss'
+})
+export class DividerDemoComponent {}
\ 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
index f7da689..259742b 100644
--- 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
@@ -3,7 +3,7 @@ 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";
+import { ButtonComponent, DrawerComponent } from '../../../../../ui-essentials/src/public-api';
@Component({
selector: 'ui-drawer-demo',
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
index db0839b..a77069d 100644
--- 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
@@ -1,6 +1,6 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
-import { EmptyStateComponent } from '../../../../../ui-essentials/src/lib/components/feedback';
+import { EmptyStateComponent } from '../../../../../ui-essentials/src/lib/components/feedback/empty-state/empty-state.component';
@Component({
selector: 'ui-empty-state-demo',
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
index 939cf2f..e518981 100644
--- 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
@@ -1,7 +1,7 @@
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';
+import { FileUploadComponent, UploadedFile } from '../../../../../ui-essentials/src/lib/components/forms/file-upload/file-upload.component';
@Component({
selector: 'ui-file-upload-demo',
@@ -295,7 +295,7 @@ export class FileUploadDemoComponent {
submittedFiles: UploadedFile[] = [];
recentEvents: Array<{type: string, fileName: string, timestamp: Date}> = [];
- readonly codeExample = `import { FileUploadComponent, UploadedFile } from 'ui-essentials';
+ readonly codeExample = `import { FileUploadComponent, UploadedFile } from '../../../../../ui-essentials/src/lib/components/forms/file-upload/file-upload.component';
// Basic usage
+ Progress Circle Demo
+
+
+
+ Sizes
+
+ @for (size of sizes; track size) {
+
+ }
+
+
+
+
+
+ Variants
+
+ @for (variant of variants; track variant) {
+
+ }
+
+
+
+
+
+ Stroke Width
+
+ @for (stroke of strokes; track stroke) {
+
+ }
+
+
+
+
+
+
+
+
+ Label Variations
+
+
+
+
+
+
+
+
+
+ ✓
+
+
Custom Content
+
+
+
+
+
+
+ Interactive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Animation Demo
+
+
+
+
+
+
+
+
+
Animated Progress
+
+
+
+
+ `,
+ styleUrl: './progress-circle-demo.component.scss'
+})
+export class ProgressCircleDemoComponent {
+ sizes = ['sm', 'md', 'lg', 'xl'] as const;
+ variants = ['primary', 'secondary', 'success', 'warning', 'danger', 'info'] as const;
+ strokes = ['thin', 'default', 'thick', 'extra-thick'] as const;
+
+ // Interactive demo state
+ interactiveValue = signal(75);
+ interactiveSize = signal<'sm' | 'md' | 'lg' | 'xl'>('lg');
+ interactiveVariant = signal<'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info'>('primary');
+ interactiveShowLabel = signal(true);
+ interactiveIndeterminate = signal(false);
+
+ // Animation demo state
+ animatedValue = signal(0);
+ progressRunning = signal(false);
+ private animationInterval: any;
+
+ updateValue(event: Event): void {
+ const target = event.target as HTMLInputElement;
+ this.interactiveValue.set(parseInt(target.value, 10));
+ }
+
+ updateSize(event: Event): void {
+ const target = event.target as HTMLSelectElement;
+ this.interactiveSize.set(target.value as any);
+ }
+
+ updateVariant(event: Event): void {
+ const target = event.target as HTMLSelectElement;
+ this.interactiveVariant.set(target.value as any);
+ }
+
+ toggleLabel(event: Event): void {
+ const target = event.target as HTMLInputElement;
+ this.interactiveShowLabel.set(target.checked);
+ }
+
+ toggleIndeterminate(event: Event): void {
+ const target = event.target as HTMLInputElement;
+ this.interactiveIndeterminate.set(target.checked);
+ }
+
+ startProgress(): void {
+ if (this.progressRunning()) return;
+
+ this.progressRunning.set(true);
+ this.animatedValue.set(0);
+
+ this.animationInterval = setInterval(() => {
+ const currentValue = this.animatedValue();
+ if (currentValue >= 100) {
+ this.stopProgress();
+ return;
+ }
+ this.animatedValue.set(currentValue + 2);
+ }, 100);
+ }
+
+ resetProgress(): void {
+ this.stopProgress();
+ this.animatedValue.set(0);
+ }
+
+ private stopProgress(): void {
+ if (this.animationInterval) {
+ clearInterval(this.animationInterval);
+ this.animationInterval = null;
+ }
+ this.progressRunning.set(false);
+ }
+
+ ngOnDestroy(): void {
+ this.stopProgress();
+ }
+}
\ 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
index b42d327..386f72e 100644
--- 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
@@ -1,7 +1,7 @@
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';
+import { ProgressBarComponent } from '../../../../../ui-essentials/src/lib/components/data-display/progress/progress-bar.component';
@Component({
selector: 'ui-progress-demo',
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
index 54ca59f..93674c7 100644
--- 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
@@ -1,11 +1,9 @@
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';
+import { RadioButtonComponent } from '../../../../../ui-essentials/src/lib/components/forms/radio/radio-button.component';
+import { RadioGroupComponent } from '../../../../../ui-essentials/src/lib/components/forms/radio/radio-group.component';
+import { RadioButtonData } from '../../../../../ui-essentials/src/lib/components/forms/radio/radio-button.component';
@Component({
selector: 'ui-radio-demo',
diff --git a/projects/demo-ui-essentials/src/app/demos/range-slider-demo/range-slider-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/range-slider-demo/range-slider-demo.component.scss
new file mode 100644
index 0000000..82a94d9
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/range-slider-demo/range-slider-demo.component.scss
@@ -0,0 +1,222 @@
+@use '../../../../../shared-ui/src/styles/semantic/index' as *;
+
+.demo-container {
+ padding: $semantic-spacing-layout-lg;
+ max-width: 1200px;
+ margin: 0 auto;
+
+ h2 {
+ color: $semantic-color-text-primary;
+ font-size: $semantic-typography-heading-h2-size;
+ margin-bottom: $semantic-spacing-layout-lg;
+ border-bottom: 1px solid $semantic-color-border-subtle;
+ padding-bottom: $semantic-spacing-content-paragraph;
+ }
+
+ h3 {
+ color: $semantic-color-text-secondary;
+ font-size: $semantic-typography-heading-h3-size;
+ margin-bottom: $semantic-spacing-component-lg;
+ margin-top: $semantic-spacing-layout-lg;
+ }
+
+ h4 {
+ color: $semantic-color-text-primary;
+ font-size: $semantic-typography-heading-h4-size;
+ margin-bottom: $semantic-spacing-component-md;
+ }
+}
+
+.demo-section {
+ margin-bottom: $semantic-spacing-layout-xl;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+}
+
+.demo-column {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-component-lg;
+}
+
+.demo-item {
+ padding: $semantic-spacing-component-lg;
+ border: 1px solid $semantic-color-border-subtle;
+ border-radius: $semantic-border-radius-md;
+ background: $semantic-color-surface-primary;
+
+ &:hover {
+ border-color: $semantic-color-border-primary;
+ box-shadow: $semantic-shadow-elevation-1;
+ }
+}
+
+.demo-controls {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-component-md;
+ margin-bottom: $semantic-spacing-component-lg;
+ padding: $semantic-spacing-component-lg;
+ background: $semantic-color-surface-secondary;
+ border-radius: $semantic-border-radius-lg;
+ border: 1px solid $semantic-color-border-subtle;
+
+ .control-row {
+ display: flex;
+ gap: $semantic-spacing-component-lg;
+ flex-wrap: wrap;
+ align-items: end;
+ }
+
+ .control-group {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-component-xs;
+ min-width: 150px;
+
+ label {
+ font-size: $semantic-typography-font-size-sm;
+ font-weight: $semantic-typography-font-weight-medium;
+ color: $semantic-color-text-primary;
+ }
+
+ input[type="range"] {
+ width: 100%;
+ }
+
+ input[type="checkbox"] {
+ margin-right: $semantic-spacing-component-xs;
+ }
+
+ 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-size: $semantic-typography-font-size-sm;
+
+ &:focus {
+ outline: 2px solid $semantic-color-primary;
+ outline-offset: 2px;
+ border-color: $semantic-color-primary;
+ }
+ }
+ }
+}
+
+.demo-interactive-result {
+ padding: $semantic-spacing-component-xl;
+ background: $semantic-color-surface-primary;
+ border-radius: $semantic-border-radius-lg;
+ border: 2px dashed $semantic-color-border-subtle;
+}
+
+.demo-form {
+ padding: $semantic-spacing-component-lg;
+ background: $semantic-color-surface-primary;
+ border-radius: $semantic-border-radius-lg;
+ border: 1px solid $semantic-color-border-subtle;
+
+ .form-row {
+ margin-bottom: $semantic-spacing-component-lg;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ .form-output {
+ margin-top: $semantic-spacing-component-xl;
+ padding: $semantic-spacing-component-md;
+ background: $semantic-color-surface-secondary;
+ border-radius: $semantic-border-radius-md;
+ border: 1px solid $semantic-color-border-subtle;
+
+ pre {
+ margin: 0;
+ font-size: $semantic-typography-font-size-sm;
+ color: $semantic-color-text-secondary;
+ white-space: pre-wrap;
+ word-break: break-word;
+ }
+ }
+}
+
+.event-log {
+ margin-top: $semantic-spacing-component-lg;
+ padding: $semantic-spacing-component-md;
+ background: $semantic-color-surface-secondary;
+ border-radius: $semantic-border-radius-md;
+ border: 1px solid $semantic-color-border-subtle;
+
+ .event-list {
+ max-height: 200px;
+ overflow-y: auto;
+ margin-bottom: $semantic-spacing-component-md;
+ padding: $semantic-spacing-component-sm;
+ background: $semantic-color-surface-primary;
+ border-radius: $semantic-border-radius-sm;
+ border: 1px solid $semantic-color-border-subtle;
+
+ .event-item {
+ font-size: $semantic-typography-font-size-xs;
+ color: $semantic-color-text-secondary;
+ padding: $semantic-spacing-component-xs 0;
+ border-bottom: 1px solid $semantic-color-border-subtle;
+
+ &:last-child {
+ border-bottom: none;
+ }
+ }
+ }
+
+ button {
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
+ border: 1px solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-sm;
+ background: $semantic-color-surface-primary;
+ color: $semantic-color-text-primary;
+ font-size: $semantic-typography-font-size-sm;
+ cursor: pointer;
+ transition: all $semantic-motion-duration-fast $semantic-easing-standard;
+
+ &:hover {
+ background: $semantic-color-surface-secondary;
+ border-color: $semantic-color-border-primary;
+ }
+
+ &:focus {
+ outline: 2px solid $semantic-color-primary;
+ outline-offset: 2px;
+ }
+ }
+}
+
+// Responsive design
+@media (max-width: $semantic-breakpoint-md - 1) {
+ .demo-container {
+ padding: $semantic-spacing-layout-md;
+ }
+
+ .demo-controls .control-row {
+ flex-direction: column;
+ align-items: stretch;
+
+ .control-group {
+ min-width: auto;
+ }
+ }
+}
+
+@media (max-width: $semantic-breakpoint-sm - 1) {
+ .demo-container {
+ padding: $semantic-spacing-layout-sm;
+ }
+
+ .demo-item {
+ padding: $semantic-spacing-component-md;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/range-slider-demo/range-slider-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/range-slider-demo/range-slider-demo.component.ts
new file mode 100644
index 0000000..ce4e2ba
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/range-slider-demo/range-slider-demo.component.ts
@@ -0,0 +1,513 @@
+import { Component, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup } from '@angular/forms';
+import { RangeSliderComponent, RangeSliderTickMark } from '../../../../../ui-essentials/src/public-api';
+
+@Component({
+ selector: 'ui-range-slider-demo',
+ standalone: true,
+ imports: [CommonModule, FormsModule, ReactiveFormsModule, RangeSliderComponent],
+ template: `
+
+
Range Slider Demo
+
+
+
+ Sizes
+
+ @for (size of sizes; track size) {
+
+
+
+
+ }
+
+
+
+
+
+ Variants
+
+ @for (variant of variants; track variant) {
+
+
+
+
+ }
+
+
+
+
+
+ States
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Range Configurations
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Tick Marks
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Interactive Configuration
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Event Handling
+
+
+
+
+
+
+
+
Event Log:
+
+ @for (event of eventLog(); track $index) {
+
{{ event }}
+ }
+
+
+
+
+
+
+ `,
+ styleUrl: './range-slider-demo.component.scss'
+})
+export class RangeSliderDemoComponent {
+ private fb = new FormBuilder();
+
+ sizes = ['sm', 'md', 'lg'] as const;
+ variants = ['primary', 'secondary', 'success', 'warning', 'danger'] as const;
+
+ // Interactive demo state
+ interactiveValue = signal(50);
+ interactiveMin = signal(0);
+ interactiveMax = signal(100);
+ interactiveStep = signal(1);
+ interactiveSize = signal<'sm' | 'md' | 'lg'>('md');
+ interactiveVariant = signal<'primary' | 'secondary' | 'success' | 'warning' | 'danger'>('primary');
+ interactiveShowValue = signal(true);
+ interactiveDisabled = signal(false);
+
+ // Event demo state
+ eventSliderValue = signal(25);
+ eventLog = signal([]);
+
+ // Tick mark configurations
+ basicTicks: RangeSliderTickMark[] = [
+ { value: 0 },
+ { value: 25 },
+ { value: 50 },
+ { value: 75 },
+ { value: 100 }
+ ];
+
+ labeledTicks: RangeSliderTickMark[] = [
+ { value: 0, label: 'Min' },
+ { value: 1, label: 'Low' },
+ { value: 2, label: 'Med' },
+ { value: 3, label: 'High' },
+ { value: 4, label: 'Very High' },
+ { value: 5, label: 'Max' }
+ ];
+
+ majorMinorTicks: RangeSliderTickMark[] = [
+ { value: 0, label: '0%', major: true },
+ { value: 25, major: false },
+ { value: 50, label: '50%', major: true },
+ { value: 75, major: false },
+ { value: 100, label: '100%', major: true }
+ ];
+
+ temperatureTicks: RangeSliderTickMark[] = [
+ { value: 16, label: '16°' },
+ { value: 18, label: '18°' },
+ { value: 20, label: '20°' },
+ { value: 22, label: '22°' },
+ { value: 24, label: '24°' },
+ { value: 26, label: '26°' },
+ { value: 28, label: '28°' },
+ { value: 30, label: '30°' }
+ ];
+
+ // Form setup
+ demoForm: FormGroup = this.fb.group({
+ volume: [75],
+ brightness: [128],
+ temperature: [22]
+ });
+
+ // Interactive controls
+ updateMin(event: Event): void {
+ const target = event.target as HTMLInputElement;
+ const newMin = parseInt(target.value, 10);
+ this.interactiveMin.set(newMin);
+ // Adjust value if it's now below min
+ if (this.interactiveValue() < newMin) {
+ this.interactiveValue.set(newMin);
+ }
+ }
+
+ updateMax(event: Event): void {
+ const target = event.target as HTMLInputElement;
+ const newMax = parseInt(target.value, 10);
+ this.interactiveMax.set(newMax);
+ // Adjust value if it's now above max
+ if (this.interactiveValue() > newMax) {
+ this.interactiveValue.set(newMax);
+ }
+ }
+
+ updateStep(event: Event): void {
+ const target = event.target as HTMLSelectElement;
+ this.interactiveStep.set(parseFloat(target.value));
+ }
+
+ updateSize(event: Event): void {
+ const target = event.target as HTMLSelectElement;
+ this.interactiveSize.set(target.value as any);
+ }
+
+ updateVariant(event: Event): void {
+ const target = event.target as HTMLSelectElement;
+ this.interactiveVariant.set(target.value as any);
+ }
+
+ toggleShowValue(event: Event): void {
+ const target = event.target as HTMLInputElement;
+ this.interactiveShowValue.set(target.checked);
+ }
+
+ toggleDisabled(event: Event): void {
+ const target = event.target as HTMLInputElement;
+ this.interactiveDisabled.set(target.checked);
+ }
+
+ onInteractiveValueChange(value: number): void {
+ this.interactiveValue.set(value);
+ }
+
+ // Event handlers
+ onValueChange(value: number): void {
+ this.eventSliderValue.set(value);
+ this.addEventLog(`Value changed: ${value}`);
+ }
+
+ onSlideStart(value: number): void {
+ this.addEventLog(`Slide started at: ${value}`);
+ }
+
+ onSlideEnd(value: number): void {
+ this.addEventLog(`Slide ended at: ${value}`);
+ }
+
+ onSliderFocus(): void {
+ this.addEventLog('Slider focused');
+ }
+
+ onSliderBlur(): void {
+ this.addEventLog('Slider blurred');
+ }
+
+ private addEventLog(message: string): void {
+ const timestamp = new Date().toLocaleTimeString();
+ const newLog = `[${timestamp}] ${message}`;
+ this.eventLog.update(log => [newLog, ...log.slice(0, 9)]); // Keep last 10 events
+ }
+
+ clearEventLog(): void {
+ this.eventLog.set([]);
+ }
+
+ getFormValues(): any {
+ return this.demoForm.value;
+ }
+}
\ 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
index 8b30265..73e63c0 100644
--- 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
@@ -1,7 +1,7 @@
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 { SearchBarComponent, SearchSuggestion } from '../../../../../ui-essentials/src/lib/components/forms/search/search-bar.component';
import {
faSearch,
faMicrophone,
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
index 60a3f5e..d920761 100644
--- 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
@@ -1,7 +1,7 @@
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';
+import { SKELETON_COMPONENTS } from '../../../../../ui-essentials/src/lib/components/feedback/skeleton-loader/skeleton-loader.component';
@Component({
selector: 'ui-skeleton-loader-demo',
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
index 8afa5b4..e07d1a7 100644
--- 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
@@ -1,6 +1,6 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
-import { SpacerComponent } from '../../../../../ui-essentials/src/lib/components/layout';
+import { SpacerComponent } from '../../../../../ui-essentials/src/lib/components/layout/spacer/spacer.component';
@Component({
selector: 'ui-spacer-demo',
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
index 0ffdcdf..b574a51 100644
--- 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
@@ -1,7 +1,7 @@
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';
+import { SwitchComponent } from '../../../../../ui-essentials/src/lib/components/forms/switch/switch.component';
@Component({
selector: 'ui-switch-demo',
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
index 5aac226..326cdbb 100644
--- 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
@@ -1,13 +1,10 @@
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';
+import { TableComponent } from '../../../../../ui-essentials/src/lib/components/data-display/table/table.component';
+import { TableAction, TableActionsComponent } from '../../../../../ui-essentials/src/lib/components/data-display/table-actions.component';
+import type { TableColumn } from '../../../../../ui-essentials/src/lib/components/data-display/table/table.component';
+import type { TableSortEvent } from '../../../../../ui-essentials/src/lib/components/data-display/table/table.component';
+import { StatusBadgeComponent } from '../../../../../ui-essentials/src/lib/components/feedback/status-badge.component';
interface User {
id: number;
name: string;
diff --git a/projects/demo-ui-essentials/src/app/demos/tooltip-demo/tooltip-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/tooltip-demo/tooltip-demo.component.scss
new file mode 100644
index 0000000..4af9a5b
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/tooltip-demo/tooltip-demo.component.scss
@@ -0,0 +1,114 @@
+@use "../../../../../shared-ui/src/styles/semantic/index" as *;
+
+.demo-container {
+ padding: $semantic-spacing-layout-md;
+ max-width: 800px;
+ margin: 0 auto;
+}
+
+.demo-section {
+ margin-bottom: $semantic-spacing-layout-lg;
+
+ h3 {
+ margin-bottom: $semantic-spacing-content-paragraph;
+ color: $semantic-color-text-primary;
+ font-size: $semantic-typography-font-size-lg;
+ }
+}
+
+.demo-row {
+ display: flex;
+ gap: $semantic-spacing-component-lg;
+ align-items: center;
+ margin-bottom: $semantic-spacing-component-sm;
+ flex-wrap: wrap;
+}
+
+.positions-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: $semantic-spacing-component-xl;
+ max-width: 400px;
+ margin: $semantic-spacing-layout-md 0;
+}
+
+.demo-button {
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
+ background: $semantic-color-primary;
+ color: $semantic-color-on-primary;
+ border: none;
+ border-radius: $semantic-border-radius-md;
+ cursor: pointer;
+ font-size: $semantic-typography-font-size-md;
+ transition: background-color $semantic-motion-duration-fast $semantic-easing-standard;
+
+ &:hover {
+ background: $semantic-color-primary-hover;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+}
+
+.demo-text {
+ color: $semantic-color-text-primary;
+ font-size: $semantic-typography-font-size-md;
+ text-decoration: underline;
+ cursor: help;
+
+ &:hover {
+ color: $semantic-color-primary;
+ }
+}
+
+.demo-icon {
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ background: $semantic-color-surface-secondary;
+ border: 2px solid $semantic-color-border-primary;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: $semantic-typography-font-size-sm;
+ font-weight: bold;
+ cursor: help;
+ color: $semantic-color-text-secondary;
+
+ &:hover {
+ background: $semantic-color-surface-secondary;
+ color: $semantic-color-text-primary;
+ }
+}
+
+p {
+ margin: 0;
+ color: $semantic-color-text-secondary;
+ font-size: $semantic-typography-font-size-md;
+}
+
+// Responsive adjustments
+@media (max-width: $semantic-breakpoint-md - 1) {
+ .positions-grid {
+ grid-template-columns: 1fr;
+ gap: $semantic-spacing-component-md;
+ }
+
+ .demo-row {
+ gap: $semantic-spacing-component-md;
+ }
+}
+
+@media (max-width: $semantic-breakpoint-sm - 1) {
+ .demo-container {
+ padding: $semantic-spacing-layout-sm;
+ }
+
+ .demo-row {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: $semantic-spacing-component-sm;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/tooltip-demo/tooltip-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/tooltip-demo/tooltip-demo.component.ts
new file mode 100644
index 0000000..d7144b2
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/tooltip-demo/tooltip-demo.component.ts
@@ -0,0 +1,139 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { TooltipComponent } from '../../../../../ui-essentials/src/public-api';
+
+@Component({
+ selector: 'ui-tooltip-demo',
+ standalone: true,
+ imports: [CommonModule, TooltipComponent],
+ template: `
+
+
Tooltip Demo
+
+
+
+ Positions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sizes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Triggers
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Different Elements
+
+
+
+
+
+ Hover me
+
+
+ ?
+
+
+
+
+
+
+ States
+
+
+
+
+
+
+
+
+
+
+
+
+ Custom Delay
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Interactive
+
+
+
+
+
Show count: {{ showCount }} | Hide count: {{ hideCount }}
+
+
+
+ `,
+ styleUrl: './tooltip-demo.component.scss'
+})
+export class TooltipDemoComponent {
+ showCount = 0;
+ hideCount = 0;
+
+ handleTooltipShow(): void {
+ this.showCount++;
+ console.log('Tooltip shown', this.showCount);
+ }
+
+ handleTooltipHide(): void {
+ this.hideCount++;
+ console.log('Tooltip hidden', this.hideCount);
+ }
+}
\ 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
index 73c4136..0f1e9de 100644
--- a/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.component.ts
+++ b/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.component.ts
@@ -8,10 +8,11 @@ import {
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
+ faCloudUploadAlt, faFileText, faListAlt, faCircle, faExpandArrowsAlt, faCircleNotch, faSliders,
+ faMinus, faInfoCircle
} from '@fortawesome/free-solid-svg-icons';
import { DemoRoutes } from '../../demos';
-import { ScrollContainerComponent } from '../../../../../ui-essentials/src/lib/layouts';
+import { ScrollContainerComponent } from '../../../../../ui-essentials/src/lib/layouts/scroll-container.component';
// import { DemoRoutes } from "../../../../../ui-essentials/src/public-api";
@Component({
@@ -89,6 +90,10 @@ export class DashboardComponent {
faListAlt = faListAlt;
faCircle = faCircle;
faExpandArrowsAlt = faExpandArrowsAlt;
+ faCircleNotch = faCircleNotch;
+ faSliders = faSliders;
+ faMinus = faMinus;
+ faInfoCircle = faInfoCircle;
menuItems: any = []
@@ -133,7 +138,8 @@ export class DashboardComponent {
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.createChildItem("form-field", "Form Field", this.faFileText),
+ this.createChildItem("range-slider", "Range Slider", this.faSliders)
];
this.addMenuItem("forms", "Forms", this.faEdit, formsChildren);
@@ -144,7 +150,9 @@ export class DashboardComponent {
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.createChildItem("cards", "Cards", this.faIdCard),
+ this.createChildItem("divider", "Divider", this.faMinus),
+ this.createChildItem("tooltip", "Tooltip", this.faInfoCircle)
];
this.addMenuItem("data-display", "Data Display", this.faEye, dataDisplayChildren);
@@ -175,7 +183,8 @@ export class DashboardComponent {
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.createChildItem("empty-state", "Empty State", this.faExclamationTriangle),
+ this.createChildItem("progress-circle", "Progress Circle", this.faCircleNotch)
];
this.addMenuItem("feedback", "Feedback", this.faComment, feedbackChildren);
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
index bf53ab0..7b0f0f5 100644
--- 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
@@ -1,7 +1,7 @@
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 { MenuItemComponent } from '../../../../../ui-essentials/src/lib/components/navigation/menu/menu-item.component';
import { faChevronRight, faChevronDown } from '@fortawesome/free-solid-svg-icons';
export interface SidebarMenuItem {
diff --git a/projects/ui-essentials/src/lib/components/data-display/divider/divider.component.scss b/projects/ui-essentials/src/lib/components/data-display/divider/divider.component.scss
new file mode 100644
index 0000000..2dd6e75
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/divider/divider.component.scss
@@ -0,0 +1,177 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+.ui-divider {
+ display: flex;
+ align-items: center;
+ position: relative;
+
+ // Horizontal divider (default)
+ &--horizontal {
+ width: 100%;
+ flex-direction: row;
+
+ &::before,
+ &::after {
+ content: '';
+ flex: 1;
+ height: $semantic-border-width-1;
+ background-color: $semantic-color-border-primary;
+ }
+
+ // Thickness variants for horizontal
+ &.ui-divider--thin {
+ &::before,
+ &::after {
+ height: $semantic-border-width-1;
+ }
+ }
+
+ &.ui-divider--thick {
+ &::before,
+ &::after {
+ height: $semantic-border-width-2;
+ }
+ }
+
+ // Style variants for horizontal
+ &.ui-divider--dashed {
+ &::before,
+ &::after {
+ background: none;
+ border-top: $semantic-border-width-1 dashed $semantic-color-border-primary;
+ height: 0;
+ }
+
+ &.ui-divider--thin {
+ &::before,
+ &::after {
+ border-top-width: $semantic-border-width-1;
+ }
+ }
+
+ &.ui-divider--thick {
+ &::before,
+ &::after {
+ border-top-width: $semantic-border-width-2;
+ }
+ }
+ }
+
+ &.ui-divider--dotted {
+ &::before,
+ &::after {
+ background: none;
+ border-top: $semantic-border-width-1 dotted $semantic-color-border-primary;
+ height: 0;
+ }
+
+ &.ui-divider--thin {
+ &::before,
+ &::after {
+ border-top-width: $semantic-border-width-1;
+ }
+ }
+
+ &.ui-divider--thick {
+ &::before,
+ &::after {
+ border-top-width: $semantic-border-width-2;
+ }
+ }
+ }
+ }
+
+ // Vertical divider
+ &--vertical {
+ height: 100%;
+ min-height: $semantic-spacing-layout-lg;
+ flex-direction: column;
+ width: $semantic-border-width-1;
+ background-color: $semantic-color-border-primary;
+
+ // Thickness variants for vertical
+ &.ui-divider--thin {
+ width: $semantic-border-width-1;
+ }
+
+ &.ui-divider--thick {
+ width: $semantic-border-width-2;
+ }
+
+ // Style variants for vertical
+ &.ui-divider--dashed {
+ background: none;
+ border-left: $semantic-border-width-1 dashed $semantic-color-border-primary;
+ width: 0;
+
+ &.ui-divider--thin {
+ border-left-width: $semantic-border-width-1;
+ }
+
+ &.ui-divider--thick {
+ border-left-width: $semantic-border-width-2;
+ }
+ }
+
+ &.ui-divider--dotted {
+ background: none;
+ border-left: $semantic-border-width-1 dotted $semantic-color-border-primary;
+ width: 0;
+
+ &.ui-divider--thin {
+ border-left-width: $semantic-border-width-1;
+ }
+
+ &.ui-divider--thick {
+ border-left-width: $semantic-border-width-2;
+ }
+ }
+ }
+
+ // Content styling (only for horizontal dividers)
+ &__content {
+ padding: 0 $semantic-spacing-component-sm;
+ background: $semantic-color-surface-primary;
+ color: $semantic-color-text-secondary;
+ font-size: $semantic-typography-font-size-sm;
+ white-space: nowrap;
+ flex-shrink: 0;
+ }
+
+ // Dark mode support
+ :host-context(.dark-theme) & {
+ &::before,
+ &::after {
+ background-color: $semantic-color-border-subtle;
+ }
+
+ &--vertical {
+ background-color: $semantic-color-border-subtle;
+ }
+
+ &--dashed,
+ &--dotted {
+ &::before,
+ &::after {
+ border-color: $semantic-color-border-subtle;
+ }
+
+ &.ui-divider--vertical {
+ border-left-color: $semantic-color-border-subtle;
+ }
+ }
+
+ .ui-divider__content {
+ background: $semantic-color-surface-primary;
+ color: $semantic-color-text-tertiary;
+ }
+ }
+
+ // Responsive adjustments
+ @media (max-width: $semantic-breakpoint-sm - 1) {
+ &__content {
+ 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/divider/divider.component.ts b/projects/ui-essentials/src/lib/components/data-display/divider/divider.component.ts
new file mode 100644
index 0000000..666ee8b
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/divider/divider.component.ts
@@ -0,0 +1,45 @@
+import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+type DividerOrientation = 'horizontal' | 'vertical';
+type DividerVariant = 'solid' | 'dashed' | 'dotted';
+type DividerThickness = 'thin' | 'default' | 'thick';
+
+@Component({
+ selector: 'ui-divider',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ template: `
+
+
+ @if (hasContent) {
+
+
+
+ }
+
+ `,
+ styleUrl: './divider.component.scss'
+})
+export class DividerComponent {
+ @Input() orientation: DividerOrientation = 'horizontal';
+ @Input() variant: DividerVariant = 'solid';
+ @Input() thickness: DividerThickness = 'default';
+
+ get hasContent(): boolean {
+ return this.orientation === 'horizontal';
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/divider/index.ts b/projects/ui-essentials/src/lib/components/data-display/divider/index.ts
new file mode 100644
index 0000000..52730b1
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/divider/index.ts
@@ -0,0 +1 @@
+export * from './divider.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
index 524223f..2cc50f0 100644
--- a/projects/ui-essentials/src/lib/components/data-display/index.ts
+++ b/projects/ui-essentials/src/lib/components/data-display/index.ts
@@ -3,10 +3,12 @@ export * from './badge';
export * from './card';
export * from './carousel';
export * from './chip';
+export * from './divider';
export * from './image-container';
export * from './list';
export * from './progress';
export * from './table';
+export * from './tooltip';
// 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/tooltip/index.ts b/projects/ui-essentials/src/lib/components/data-display/tooltip/index.ts
new file mode 100644
index 0000000..f48e8ff
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/tooltip/index.ts
@@ -0,0 +1 @@
+export * from './tooltip.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/tooltip/tooltip.component.scss b/projects/ui-essentials/src/lib/components/data-display/tooltip/tooltip.component.scss
new file mode 100644
index 0000000..b811d67
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/tooltip/tooltip.component.scss
@@ -0,0 +1,189 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+.ui-tooltip-wrapper {
+ position: relative;
+ display: inline-block;
+}
+
+.ui-tooltip-trigger {
+ display: inherit;
+ width: inherit;
+ height: inherit;
+}
+
+.ui-tooltip {
+ position: absolute;
+ z-index: $semantic-z-index-tooltip;
+ background: $semantic-color-surface-elevated;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ border-radius: $semantic-border-radius-md;
+ box-shadow: $semantic-shadow-elevation-3;
+ pointer-events: none;
+ opacity: 1;
+ transform: scale(1);
+ animation: tooltip-fade-in $semantic-motion-duration-fast $semantic-easing-standard;
+
+ // Size variants
+ &--sm {
+ max-width: 200px;
+
+ .ui-tooltip__content {
+ padding: $semantic-spacing-component-xs;
+ font-size: $semantic-typography-font-size-xs;
+ }
+ }
+
+ &--md {
+ max-width: 250px;
+
+ .ui-tooltip__content {
+ padding: $semantic-spacing-component-sm;
+ font-size: $semantic-typography-font-size-sm;
+ }
+ }
+
+ &--lg {
+ max-width: 300px;
+
+ .ui-tooltip__content {
+ padding: $semantic-spacing-component-md;
+ font-size: $semantic-typography-font-size-sm;
+ }
+ }
+
+ // Position variants
+ &--top {
+ bottom: 100%;
+ left: 50%;
+ transform: translateX(-50%) translateY(-8px);
+ margin-bottom: $semantic-spacing-component-xs;
+
+ .ui-tooltip__arrow {
+ top: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ border-top-color: $semantic-color-surface-elevated;
+ border-bottom: none;
+ }
+ }
+
+ &--bottom {
+ top: 100%;
+ left: 50%;
+ transform: translateX(-50%) translateY(8px);
+ margin-top: $semantic-spacing-component-xs;
+
+ .ui-tooltip__arrow {
+ bottom: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ border-bottom-color: $semantic-color-surface-elevated;
+ border-top: none;
+ }
+ }
+
+ &--left {
+ right: 100%;
+ top: 50%;
+ transform: translateY(-50%) translateX(-8px);
+ margin-right: $semantic-spacing-component-xs;
+
+ .ui-tooltip__arrow {
+ left: 100%;
+ top: 50%;
+ transform: translateY(-50%);
+ border-left-color: $semantic-color-surface-elevated;
+ border-right: none;
+ }
+ }
+
+ &--right {
+ left: 100%;
+ top: 50%;
+ transform: translateY(-50%) translateX(8px);
+ margin-left: $semantic-spacing-component-xs;
+
+ .ui-tooltip__arrow {
+ right: 100%;
+ top: 50%;
+ transform: translateY(-50%);
+ border-right-color: $semantic-color-surface-elevated;
+ border-left: none;
+ }
+ }
+
+ &__content {
+ color: $semantic-color-text-primary;
+ line-height: 1.4;
+ word-wrap: break-word;
+ text-align: center;
+ }
+
+ &__arrow {
+ position: absolute;
+ width: 0;
+ height: 0;
+ border: 6px solid transparent;
+
+ // Default arrow styling
+ border-top: 6px solid $semantic-color-surface-elevated;
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
+ }
+
+ // Dark mode support
+ :host-context(.dark-theme) & {
+ background: $semantic-color-surface-secondary;
+ border-color: $semantic-color-border-primary;
+
+ .ui-tooltip__content {
+ color: $semantic-color-text-primary;
+ }
+
+ .ui-tooltip__arrow {
+ border-top-color: $semantic-color-surface-secondary;
+
+ &::after {
+ border-top-color: $semantic-color-surface-secondary;
+ }
+ }
+
+ &--bottom .ui-tooltip__arrow {
+ border-bottom-color: $semantic-color-surface-secondary;
+ border-top: none;
+ }
+
+ &--left .ui-tooltip__arrow {
+ border-left-color: $semantic-color-surface-secondary;
+ border-right: none;
+ }
+
+ &--right .ui-tooltip__arrow {
+ border-right-color: $semantic-color-surface-secondary;
+ border-left: none;
+ }
+ }
+
+ // Responsive adjustments
+ @media (max-width: $semantic-breakpoint-sm - 1) {
+ &--sm { max-width: 150px; }
+ &--md { max-width: 200px; }
+ &--lg { max-width: 250px; }
+
+ .ui-tooltip__content {
+ padding: $semantic-spacing-component-xs;
+ font-size: $semantic-typography-font-size-xs;
+ }
+ }
+}
+
+// Animation keyframes
+@keyframes tooltip-fade-in {
+ from {
+ opacity: 0;
+ transform: scale(0.95);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/data-display/tooltip/tooltip.component.ts b/projects/ui-essentials/src/lib/components/data-display/tooltip/tooltip.component.ts
new file mode 100644
index 0000000..62d03de
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/data-display/tooltip/tooltip.component.ts
@@ -0,0 +1,152 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, OnDestroy, ElementRef, ViewChild, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+type TooltipPosition = 'top' | 'bottom' | 'left' | 'right';
+type TooltipTrigger = 'hover' | 'click' | 'focus';
+type TooltipSize = 'sm' | 'md' | 'lg';
+
+@Component({
+ selector: 'ui-tooltip',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ template: `
+
+ `,
+ styleUrl: './tooltip.component.scss'
+})
+export class TooltipComponent implements OnDestroy {
+ @ViewChild('tooltipElement') tooltipElement?: ElementRef;
+
+ @Input() text = '';
+ @Input() position: TooltipPosition = 'top';
+ @Input() trigger: TooltipTrigger = 'hover';
+ @Input() size: TooltipSize = 'md';
+ @Input() disabled = false;
+ @Input() delay = 500;
+
+ @Output() tooltipShow = new EventEmitter();
+ @Output() tooltipHide = new EventEmitter();
+
+ protected isVisible = signal(false);
+ private showTimeout?: number;
+ private hideTimeout?: number;
+
+ ngOnDestroy(): void {
+ this.clearTimeouts();
+ }
+
+ handleMouseEnter(): void {
+ if (this.trigger === 'hover' && !this.disabled) {
+ this.scheduleShow();
+ }
+ }
+
+ handleMouseLeave(): void {
+ if (this.trigger === 'hover') {
+ this.scheduleHide();
+ }
+ }
+
+ handleClick(event: MouseEvent): void {
+ if (this.trigger === 'click' && !this.disabled) {
+ event.preventDefault();
+ this.toggle();
+ }
+ }
+
+ handleFocus(): void {
+ if (this.trigger === 'focus' && !this.disabled) {
+ this.show();
+ }
+ }
+
+ handleBlur(): void {
+ if (this.trigger === 'focus') {
+ this.hide();
+ }
+ }
+
+ private scheduleShow(): void {
+ this.clearTimeouts();
+ this.showTimeout = window.setTimeout(() => {
+ this.show();
+ }, this.delay);
+ }
+
+ private scheduleHide(): void {
+ this.clearTimeouts();
+ this.hideTimeout = window.setTimeout(() => {
+ this.hide();
+ }, 100);
+ }
+
+ private show(): void {
+ if (!this.disabled && !this.isVisible()) {
+ this.isVisible.set(true);
+ this.tooltipShow.emit();
+ }
+ }
+
+ private hide(): void {
+ if (this.isVisible()) {
+ this.isVisible.set(false);
+ this.tooltipHide.emit();
+ }
+ }
+
+ private toggle(): void {
+ if (this.isVisible()) {
+ this.hide();
+ } else {
+ this.show();
+ }
+ }
+
+ private clearTimeouts(): void {
+ if (this.showTimeout) {
+ clearTimeout(this.showTimeout);
+ this.showTimeout = undefined;
+ }
+ if (this.hideTimeout) {
+ clearTimeout(this.hideTimeout);
+ this.hideTimeout = undefined;
+ }
+ }
+}
\ 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
index 37917c5..bb66b99 100644
--- a/projects/ui-essentials/src/lib/components/feedback/index.ts
+++ b/projects/ui-essentials/src/lib/components/feedback/index.ts
@@ -2,4 +2,5 @@ export * from "./status-badge.component";
export * from "./skeleton-loader";
export * from "./empty-state";
export * from "./loading-spinner";
+export * from "./progress-circle";
// export * from "./theme-switcher.component"; // Temporarily disabled due to CSS variable issues
diff --git a/projects/ui-essentials/src/lib/components/feedback/progress-circle/index.ts b/projects/ui-essentials/src/lib/components/feedback/progress-circle/index.ts
new file mode 100644
index 0000000..c8bc728
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/feedback/progress-circle/index.ts
@@ -0,0 +1 @@
+export * from './progress-circle.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/feedback/progress-circle/progress-circle.component.scss b/projects/ui-essentials/src/lib/components/feedback/progress-circle/progress-circle.component.scss
new file mode 100644
index 0000000..69e04e8
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/feedback/progress-circle/progress-circle.component.scss
@@ -0,0 +1,198 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+.ui-progress-circle {
+ // Core Structure
+ display: inline-flex;
+ position: relative;
+ align-items: center;
+ justify-content: center;
+
+ // Sizing variants
+ &--sm {
+ width: $semantic-sizing-icon-button;
+ height: $semantic-sizing-icon-button;
+ }
+
+ &--md {
+ width: $semantic-sizing-icon-navigation;
+ height: $semantic-sizing-icon-navigation;
+ }
+
+ &--lg {
+ width: $semantic-sizing-icon-header;
+ height: $semantic-sizing-icon-header;
+ }
+
+ &--xl {
+ width: $semantic-sizing-icon-feature;
+ height: $semantic-sizing-icon-feature;
+ }
+
+ // Color variants
+ &--primary {
+ .ui-progress-circle__progress {
+ stroke: $semantic-color-primary;
+ }
+ }
+
+ &--secondary {
+ .ui-progress-circle__progress {
+ stroke: $semantic-color-secondary;
+ }
+ }
+
+ &--success {
+ .ui-progress-circle__progress {
+ stroke: $semantic-color-success;
+ }
+ }
+
+ &--warning {
+ .ui-progress-circle__progress {
+ stroke: $semantic-color-warning;
+ }
+ }
+
+ &--danger {
+ .ui-progress-circle__progress {
+ stroke: $semantic-color-danger;
+ }
+ }
+
+ &--info {
+ .ui-progress-circle__progress {
+ stroke: $semantic-color-info;
+ }
+ }
+
+ // State variants
+ &--disabled {
+ opacity: 0.38;
+ cursor: not-allowed;
+ }
+
+ &--indeterminate {
+ .ui-progress-circle__progress {
+ animation: progress-circle-spin $semantic-motion-duration-slow
+ $semantic-easing-standard infinite linear;
+ stroke-dasharray: 85, 85;
+ stroke-dashoffset: 0;
+ }
+ }
+
+ // SVG elements
+ &__svg {
+ width: 100%;
+ height: 100%;
+ transform: rotate(-90deg); // Start progress from top
+ overflow: visible;
+ }
+
+ &__track {
+ fill: none;
+ stroke: $semantic-color-border-subtle;
+ stroke-width: 2;
+ }
+
+ &__progress {
+ fill: none;
+ stroke: $semantic-color-primary;
+ stroke-width: 2;
+ stroke-linecap: round;
+ transition: stroke-dashoffset $semantic-motion-duration-normal
+ $semantic-easing-standard;
+ }
+
+ // Label content
+ &__label {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ color: $semantic-color-text-primary;
+ font-size: $semantic-typography-font-size-xs;
+ font-weight: $semantic-typography-font-weight-medium;
+ text-align: center;
+ line-height: 1;
+
+ &--sm {
+ font-size: $semantic-typography-font-size-xs;
+ }
+
+ &--md {
+ font-size: $semantic-typography-font-size-xs;
+ }
+
+ &--lg {
+ font-size: $semantic-typography-font-size-sm;
+ }
+
+ &--xl {
+ font-size: $semantic-typography-font-size-sm;
+ }
+
+ &--disabled {
+ opacity: 0.38;
+ }
+ }
+
+ // Stroke width variants
+ &--thin {
+ .ui-progress-circle__track,
+ .ui-progress-circle__progress {
+ stroke-width: 1;
+ }
+ }
+
+ &--thick {
+ .ui-progress-circle__track,
+ .ui-progress-circle__progress {
+ stroke-width: 3;
+ }
+ }
+
+ &--extra-thick {
+ .ui-progress-circle__track,
+ .ui-progress-circle__progress {
+ stroke-width: 4;
+ }
+ }
+
+ // Dark mode support
+ :host-context(.dark-theme) & {
+ .ui-progress-circle__track {
+ stroke: $semantic-color-border-primary;
+ }
+
+ .ui-progress-circle__label {
+ color: $semantic-color-text-primary;
+ }
+ }
+}
+
+// Keyframe animations
+@keyframes progress-circle-spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 50% {
+ transform: rotate(180deg);
+ stroke-dasharray: 1, 200;
+ stroke-dashoffset: -35;
+ }
+ 100% {
+ transform: rotate(360deg);
+ stroke-dasharray: 85, 85;
+ stroke-dashoffset: -124;
+ }
+}
+
+// Responsive design
+@media (max-width: 480px) {
+ .ui-progress-circle {
+ &--xl {
+ width: $semantic-sizing-icon-header;
+ height: $semantic-sizing-icon-header;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/feedback/progress-circle/progress-circle.component.ts b/projects/ui-essentials/src/lib/components/feedback/progress-circle/progress-circle.component.ts
new file mode 100644
index 0000000..cb72069
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/feedback/progress-circle/progress-circle.component.ts
@@ -0,0 +1,167 @@
+import { Component, Input, ChangeDetectionStrategy, computed, signal, ViewEncapsulation } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+export type ProgressCircleSize = 'sm' | 'md' | 'lg' | 'xl';
+export type ProgressCircleVariant = 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info';
+export type ProgressCircleStroke = 'thin' | 'default' | 'thick' | 'extra-thick';
+
+@Component({
+ selector: 'ui-progress-circle',
+ standalone: true,
+ imports: [CommonModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ template: `
+
+
+
+
+
+ @if (showLabel) {
+
+ @if (labelContent) {
+ {{ labelContent }}
+ } @else {
+
+ }
+
+ }
+
+ `,
+ styleUrl: './progress-circle.component.scss'
+})
+export class ProgressCircleComponent {
+ @Input() value = 0;
+ @Input() max = 100;
+ @Input() size: ProgressCircleSize = 'md';
+ @Input() variant: ProgressCircleVariant = 'primary';
+ @Input() stroke: ProgressCircleStroke = 'default';
+ @Input() disabled = false;
+ @Input() indeterminate = false;
+ @Input() showLabel = false;
+ @Input() labelContent = '';
+ @Input() ariaLabel = '';
+ @Input() ariaValueText = '';
+
+ // Constants for SVG calculations
+ private readonly _center = 20;
+ private readonly _baseRadius = 16;
+
+ // Computed properties for SVG dimensions
+ center = this._center;
+ radius = this._baseRadius;
+ viewBox = `0 0 ${this._center * 2} ${this._center * 2}`;
+
+ // Computed signals
+ private _normalizedValue = computed(() => {
+ const val = Math.max(0, Math.min(this.max, this.value));
+ return val;
+ });
+
+ private _percentage = computed(() => {
+ return this.max === 0 ? 0 : (this._normalizedValue() / this.max) * 100;
+ });
+
+ circumference = computed(() => 2 * Math.PI * this.radius);
+
+ progressOffset = computed(() => {
+ const progress = this._percentage();
+ return this.circumference() - (progress / 100) * this.circumference();
+ });
+
+ svgSize = computed(() => {
+ const sizeMap = {
+ sm: 24,
+ md: 32,
+ lg: 40,
+ xl: 64
+ };
+ return sizeMap[this.size];
+ });
+
+ strokeWidth = computed(() => {
+ const strokeMap = {
+ thin: 1,
+ default: 2,
+ thick: 3,
+ 'extra-thick': 4
+ };
+ return strokeMap[this.stroke];
+ });
+
+ containerClasses = computed(() => {
+ const classes = [
+ `ui-progress-circle--${this.size}`,
+ `ui-progress-circle--${this.variant}`,
+ `ui-progress-circle--${this.stroke}`
+ ];
+
+ if (this.disabled) classes.push('ui-progress-circle--disabled');
+ if (this.indeterminate) classes.push('ui-progress-circle--indeterminate');
+
+ return classes.join(' ');
+ });
+
+ labelClasses = computed(() => {
+ const classes = [
+ `ui-progress-circle__label--${this.size}`
+ ];
+
+ if (this.disabled) classes.push('ui-progress-circle__label--disabled');
+
+ return classes.join(' ');
+ });
+
+ // Utility methods
+ getPercentage(): number {
+ return this._percentage();
+ }
+
+ getValue(): number {
+ return this._normalizedValue();
+ }
+
+ isComplete(): boolean {
+ return this._normalizedValue() >= this.max;
+ }
+}
\ 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
index a3a4d3f..e0fad01 100644
--- a/projects/ui-essentials/src/lib/components/forms/index.ts
+++ b/projects/ui-essentials/src/lib/components/forms/index.ts
@@ -9,3 +9,4 @@ export * from './date-picker';
export * from './time-picker';
export * from './file-upload';
export * from './form-field';
+export * from './range-slider';
diff --git a/projects/ui-essentials/src/lib/components/forms/range-slider/index.ts b/projects/ui-essentials/src/lib/components/forms/range-slider/index.ts
new file mode 100644
index 0000000..2a60164
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/range-slider/index.ts
@@ -0,0 +1 @@
+export * from './range-slider.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/range-slider/range-slider.component.scss b/projects/ui-essentials/src/lib/components/forms/range-slider/range-slider.component.scss
new file mode 100644
index 0000000..c7e5199
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/range-slider/range-slider.component.scss
@@ -0,0 +1,383 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+.ui-range-slider {
+ // Core Structure
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ width: 100%;
+
+ // Sizing variants
+ &--sm {
+ .ui-range-slider__track {
+ height: 4px;
+ }
+
+ .ui-range-slider__thumb {
+ width: 16px;
+ height: 16px;
+ }
+
+ .ui-range-slider__label {
+ font-size: $semantic-typography-font-size-xs;
+ }
+ }
+
+ &--md {
+ .ui-range-slider__track {
+ height: 6px;
+ }
+
+ .ui-range-slider__thumb {
+ width: 20px;
+ height: 20px;
+ }
+
+ .ui-range-slider__label {
+ font-size: $semantic-typography-font-size-xs;
+ }
+ }
+
+ &--lg {
+ .ui-range-slider__track {
+ height: 8px;
+ }
+
+ .ui-range-slider__thumb {
+ width: 24px;
+ height: 24px;
+ }
+
+ .ui-range-slider__label {
+ font-size: $semantic-typography-font-size-sm;
+ }
+ }
+
+ // Color variants
+ &--primary {
+ .ui-range-slider__fill {
+ background: $semantic-color-primary;
+ }
+
+ .ui-range-slider__thumb {
+ border-color: $semantic-color-primary;
+
+ &:hover {
+ border-color: $semantic-color-primary;
+ box-shadow: 0 0 0 8px rgba($semantic-color-primary, 0.1);
+ }
+
+ &:focus {
+ border-color: $semantic-color-primary;
+ box-shadow: 0 0 0 4px rgba($semantic-color-primary, 0.2);
+ }
+ }
+ }
+
+ &--secondary {
+ .ui-range-slider__fill {
+ background: $semantic-color-secondary;
+ }
+
+ .ui-range-slider__thumb {
+ border-color: $semantic-color-secondary;
+
+ &:hover {
+ border-color: $semantic-color-secondary;
+ box-shadow: 0 0 0 8px rgba($semantic-color-secondary, 0.1);
+ }
+
+ &:focus {
+ border-color: $semantic-color-secondary;
+ box-shadow: 0 0 0 4px rgba($semantic-color-secondary, 0.2);
+ }
+ }
+ }
+
+ &--success {
+ .ui-range-slider__fill {
+ background: $semantic-color-success;
+ }
+
+ .ui-range-slider__thumb {
+ border-color: $semantic-color-success;
+
+ &:hover {
+ border-color: $semantic-color-success;
+ box-shadow: 0 0 0 8px rgba($semantic-color-success, 0.1);
+ }
+ }
+ }
+
+ &--warning {
+ .ui-range-slider__fill {
+ background: $semantic-color-warning;
+ }
+
+ .ui-range-slider__thumb {
+ border-color: $semantic-color-warning;
+
+ &:hover {
+ border-color: $semantic-color-warning;
+ box-shadow: 0 0 0 8px rgba($semantic-color-warning, 0.1);
+ }
+ }
+ }
+
+ &--danger {
+ .ui-range-slider__fill {
+ background: $semantic-color-danger;
+ }
+
+ .ui-range-slider__thumb {
+ border-color: $semantic-color-danger;
+
+ &:hover {
+ border-color: $semantic-color-danger;
+ box-shadow: 0 0 0 8px rgba($semantic-color-danger, 0.1);
+ }
+ }
+ }
+
+ // State variants
+ &--disabled {
+ opacity: 0.38;
+ cursor: not-allowed;
+
+ .ui-range-slider__thumb {
+ cursor: not-allowed;
+ pointer-events: none;
+ }
+
+ .ui-range-slider__input {
+ cursor: not-allowed;
+ }
+ }
+
+ // Label positioning
+ &__header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+
+ &__label {
+ color: $semantic-color-text-primary;
+ font-size: $semantic-typography-font-size-sm;
+ font-weight: $semantic-typography-font-weight-medium;
+
+ &--disabled {
+ color: $semantic-color-text-tertiary;
+ }
+ }
+
+ &__value-display {
+ color: $semantic-color-text-secondary;
+ font-size: $semantic-typography-font-size-xs;
+ font-weight: $semantic-typography-font-weight-medium;
+ min-width: 40px;
+ text-align: right;
+ }
+
+ // Slider container
+ &__container {
+ position: relative;
+ padding: $semantic-spacing-component-sm 0;
+ }
+
+ // Track (background)
+ &__track {
+ position: relative;
+ width: 100%;
+ height: 6px;
+ background: $semantic-color-surface-secondary;
+ border-radius: $semantic-border-radius-xl;
+ overflow: hidden;
+ }
+
+ // Fill (progress)
+ &__fill {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ background: $semantic-color-primary;
+ border-radius: inherit;
+ transition: width $semantic-motion-duration-fast $semantic-easing-standard;
+ }
+
+ // Native input (hidden but functional)
+ &__input {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ opacity: 0;
+ cursor: pointer;
+
+ &:focus {
+ outline: none;
+ }
+
+ &:disabled {
+ cursor: not-allowed;
+ }
+ }
+
+ // Visual thumb
+ &__thumb {
+ position: absolute;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ width: 20px;
+ height: 20px;
+ background: $semantic-color-surface-primary;
+ border: 2px solid $semantic-color-primary;
+ border-radius: 50%;
+ cursor: pointer;
+ transition: all $semantic-motion-duration-fast $semantic-easing-standard;
+ z-index: 2;
+
+ &:hover {
+ box-shadow: 0 0 0 8px rgba($semantic-color-primary, 0.1);
+ border-color: $semantic-color-primary;
+ }
+
+ &:focus,
+ &--focused {
+ box-shadow: 0 0 0 4px rgba($semantic-color-primary, 0.2);
+ border-color: $semantic-color-primary;
+ }
+
+ &:active,
+ &--dragging {
+ box-shadow: 0 0 0 8px rgba($semantic-color-primary, 0.15);
+ transform: translate(-50%, -50%) scale(1.1);
+ }
+ }
+
+ // Tick marks (optional)
+ &__ticks {
+ display: flex;
+ justify-content: space-between;
+ margin-top: $semantic-spacing-component-xs;
+ padding: 0 10px; // Offset for thumb width
+ }
+
+ &__tick {
+ width: 2px;
+ height: 8px;
+ background: $semantic-color-border-subtle;
+ border-radius: $semantic-border-radius-xs;
+
+ &--major {
+ height: 12px;
+ background: $semantic-color-border-primary;
+ }
+ }
+
+ // Tick labels
+ &__tick-labels {
+ display: flex;
+ justify-content: space-between;
+ margin-top: $semantic-spacing-component-xs;
+ padding: 0 10px; // Offset for thumb width
+ }
+
+ &__tick-label {
+ font-size: $semantic-typography-font-size-xs;
+ color: $semantic-color-text-tertiary;
+ text-align: center;
+ min-width: 20px;
+ }
+
+ // Helper text
+ &__helper-text {
+ margin-top: $semantic-spacing-component-xs;
+ font-size: $semantic-typography-font-size-xs;
+ color: $semantic-color-text-secondary;
+
+ &--error {
+ color: $semantic-color-danger;
+ }
+ }
+
+ // Range variant (dual thumb)
+ &--range {
+ .ui-range-slider__fill {
+ left: auto;
+ right: auto;
+ }
+ }
+
+ // Vertical orientation
+ &--vertical {
+ flex-direction: row;
+ align-items: center;
+ height: 200px;
+ width: auto;
+
+ .ui-range-slider__container {
+ width: auto;
+ height: 100%;
+ padding: 0 $semantic-spacing-component-sm;
+ }
+
+ .ui-range-slider__track {
+ width: 6px;
+ height: 100%;
+ }
+
+ .ui-range-slider__fill {
+ width: 100%;
+ height: auto;
+ bottom: 0;
+ top: auto;
+ }
+
+ .ui-range-slider__thumb {
+ left: 50%;
+ top: auto;
+ transform: translate(-50%, 50%);
+ }
+
+ .ui-range-slider__input {
+ width: 100%;
+ height: 100%;
+ }
+ }
+
+ // Dark mode support
+ :host-context(.dark-theme) & {
+ .ui-range-slider__track {
+ background: $semantic-color-surface-secondary;
+ }
+
+ .ui-range-slider__thumb {
+ background: $semantic-color-surface-primary;
+ border-color: $semantic-color-primary;
+ }
+
+ .ui-range-slider__label {
+ color: $semantic-color-text-primary;
+ }
+ }
+}
+
+// Responsive design
+@media (max-width: $semantic-breakpoint-sm - 1) {
+ .ui-range-slider {
+ &--lg {
+ .ui-range-slider__thumb {
+ width: 20px;
+ height: 20px;
+ }
+
+ .ui-range-slider__track {
+ height: 6px;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/range-slider/range-slider.component.ts b/projects/ui-essentials/src/lib/components/forms/range-slider/range-slider.component.ts
new file mode 100644
index 0000000..625b74f
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/range-slider/range-slider.component.ts
@@ -0,0 +1,399 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, forwardRef, signal, computed, ViewChild, ElementRef, ViewEncapsulation } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule, ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+
+export type RangeSliderSize = 'sm' | 'md' | 'lg';
+export type RangeSliderVariant = 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
+export type RangeSliderOrientation = 'horizontal' | 'vertical';
+
+export interface RangeSliderTickMark {
+ value: number;
+ label?: string;
+ major?: boolean;
+}
+
+@Component({
+ selector: 'ui-range-slider',
+ standalone: true,
+ imports: [CommonModule, FormsModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => RangeSliderComponent),
+ multi: true
+ }
+ ],
+ template: `
+
+
+ @if (label || showValue) {
+
+ }
+
+
+
+ @if (ticks && ticks.length > 0) {
+
+
+ @for (tick of ticks; track tick.value) {
+
+
+ }
+
+
+
+ @if (showTickLabels) {
+
+ @for (tick of ticks; track tick.value) {
+ @if (tick.label) {
+
+ {{ tick.label }}
+
+ }
+ }
+
+ }
+ }
+
+ @if (helperText && !disabled) {
+
+ {{ helperText }}
+
+ }
+
+ `,
+ styleUrl: './range-slider.component.scss'
+})
+export class RangeSliderComponent implements ControlValueAccessor {
+ @ViewChild('sliderInput', { static: true }) sliderInput!: ElementRef;
+ @ViewChild('container', { static: true }) container!: ElementRef;
+
+ // Core inputs
+ @Input() min = 0;
+ @Input() max = 100;
+ @Input() step = 1;
+ @Input() set value(val: number) {
+ if (val !== null && val !== undefined) {
+ this._value.set(this.normalizeValue(val));
+ }
+ }
+ get value(): number {
+ return this.currentValue();
+ }
+ @Input() size: RangeSliderSize = 'md';
+ @Input() variant: RangeSliderVariant = 'primary';
+ @Input() orientation: RangeSliderOrientation = 'horizontal';
+
+ // Behavior inputs
+ @Input() disabled = false;
+ @Input() required = false;
+ @Input() readonly = false;
+
+ // Display options
+ @Input() label = '';
+ @Input() showValue = false;
+ @Input() valueUnit = '';
+ @Input() showTickLabels = false;
+ @Input() ticks: RangeSliderTickMark[] = [];
+
+ // Validation and help
+ @Input() helperText = '';
+ @Input() hasError = false;
+ @Input() ariaLabel = '';
+ @Input() ariaValueText = '';
+ @Input() sliderId = `range-slider-${Math.random().toString(36).substr(2, 9)}`;
+
+ // Outputs
+ @Output() valueChange = new EventEmitter();
+ @Output() sliderFocus = new EventEmitter();
+ @Output() sliderBlur = new EventEmitter();
+ @Output() slideStart = new EventEmitter();
+ @Output() slideEnd = new EventEmitter();
+
+ // Internal state
+ private _value = signal(0);
+ currentValue = this._value.asReadonly();
+
+ private _isFocused = signal(false);
+ isFocused = this._isFocused.asReadonly();
+
+ private _isDragging = signal(false);
+ isDragging = this._isDragging.asReadonly();
+
+ // Computed properties
+ fillWidth = computed(() => {
+ const range = this.max - this.min;
+ const progress = (this.currentValue() - this.min) / range;
+ return Math.max(0, Math.min(100, progress * 100));
+ });
+
+ thumbPosition = computed(() => {
+ return this.fillWidth();
+ });
+
+ containerClasses = computed(() => {
+ const classes = [
+ `ui-range-slider--${this.size}`,
+ `ui-range-slider--${this.variant}`,
+ `ui-range-slider--${this.orientation}`
+ ];
+
+ if (this.disabled) classes.push('ui-range-slider--disabled');
+ if (this.hasError) classes.push('ui-range-slider--error');
+ if (this.readonly) classes.push('ui-range-slider--readonly');
+
+ return classes.join(' ');
+ });
+
+ // ControlValueAccessor implementation
+ private onChange = (value: number) => {};
+ private onTouched = () => {};
+
+ writeValue(value: number): void {
+ if (value !== null && value !== undefined) {
+ this._value.set(this.normalizeValue(value));
+ }
+ }
+
+ registerOnChange(fn: (value: number) => void): void {
+ this.onChange = fn;
+ }
+
+ registerOnTouched(fn: () => void): void {
+ this.onTouched = fn;
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ }
+
+ // Event handlers
+ onInputChange(event: Event): void {
+ const target = event.target as HTMLInputElement;
+ const newValue = this.normalizeValue(parseFloat(target.value));
+ this.updateValue(newValue);
+ }
+
+ onFocus(event: FocusEvent): void {
+ this._isFocused.set(true);
+ this.sliderFocus.emit(event);
+ }
+
+ onBlur(event: FocusEvent): void {
+ this._isFocused.set(false);
+ this.onTouched();
+ this.sliderBlur.emit(event);
+ }
+
+ onKeyDown(event: KeyboardEvent): void {
+ if (this.disabled || this.readonly) return;
+
+ let delta = 0;
+ const largeStep = (this.max - this.min) * 0.1;
+
+ switch (event.key) {
+ case 'ArrowRight':
+ case 'ArrowUp':
+ delta = this.step;
+ break;
+ case 'ArrowLeft':
+ case 'ArrowDown':
+ delta = -this.step;
+ break;
+ case 'PageUp':
+ delta = largeStep;
+ break;
+ case 'PageDown':
+ delta = -largeStep;
+ break;
+ case 'Home':
+ this.updateValue(this.min);
+ event.preventDefault();
+ return;
+ case 'End':
+ this.updateValue(this.max);
+ event.preventDefault();
+ return;
+ default:
+ return;
+ }
+
+ event.preventDefault();
+ const newValue = this.normalizeValue(this.currentValue() + delta);
+ this.updateValue(newValue);
+ }
+
+ onThumbMouseDown(event: MouseEvent): void {
+ if (this.disabled || this.readonly) return;
+
+ event.preventDefault();
+ this._isDragging.set(true);
+ this.slideStart.emit(this.currentValue());
+
+ const handleMouseMove = (e: MouseEvent) => {
+ this.updateValueFromPosition(e.clientX);
+ };
+
+ const handleMouseUp = () => {
+ this._isDragging.set(false);
+ this.slideEnd.emit(this.currentValue());
+ document.removeEventListener('mousemove', handleMouseMove);
+ document.removeEventListener('mouseup', handleMouseUp);
+ };
+
+ document.addEventListener('mousemove', handleMouseMove);
+ document.addEventListener('mouseup', handleMouseUp);
+ }
+
+ onThumbTouchStart(event: TouchEvent): void {
+ if (this.disabled || this.readonly) return;
+
+ event.preventDefault();
+ this._isDragging.set(true);
+ this.slideStart.emit(this.currentValue());
+
+ const handleTouchMove = (e: TouchEvent) => {
+ if (e.touches.length > 0) {
+ this.updateValueFromPosition(e.touches[0].clientX);
+ }
+ };
+
+ const handleTouchEnd = () => {
+ this._isDragging.set(false);
+ this.slideEnd.emit(this.currentValue());
+ document.removeEventListener('touchmove', handleTouchMove);
+ document.removeEventListener('touchend', handleTouchEnd);
+ };
+
+ document.addEventListener('touchmove', handleTouchMove, { passive: false });
+ document.addEventListener('touchend', handleTouchEnd);
+ }
+
+ // Utility methods
+ private updateValue(newValue: number): void {
+ const normalizedValue = this.normalizeValue(newValue);
+ if (normalizedValue !== this.currentValue()) {
+ this._value.set(normalizedValue);
+ this.onChange(normalizedValue);
+ this.valueChange.emit(normalizedValue);
+
+ // Sync with native input
+ if (this.sliderInput) {
+ this.sliderInput.nativeElement.value = normalizedValue.toString();
+ }
+ }
+ }
+
+ private updateValueFromPosition(clientX: number): void {
+ if (!this.container) return;
+
+ const rect = this.container.nativeElement.getBoundingClientRect();
+ const percentage = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
+ const newValue = this.min + (percentage * (this.max - this.min));
+ this.updateValue(newValue);
+ }
+
+ private normalizeValue(value: number): number {
+ // Round to nearest step
+ const steppedValue = Math.round((value - this.min) / this.step) * this.step + this.min;
+ // Clamp to min/max bounds
+ return Math.max(this.min, Math.min(this.max, steppedValue));
+ }
+
+ formatValue(value: number): string {
+ // Format the value for display (could be overridden for custom formatting)
+ return value.toString();
+ }
+
+ getTickPosition(tickValue: number): number {
+ const range = this.max - this.min;
+ return ((tickValue - this.min) / range) * 100;
+ }
+
+ // Public API methods
+ setValue(value: number): void {
+ this.updateValue(value);
+ }
+
+ getValue(): number {
+ return this.currentValue();
+ }
+
+ focus(): void {
+ if (this.sliderInput) {
+ this.sliderInput.nativeElement.focus();
+ }
+ }
+}
\ 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
index 81388bb..b5c8e20 100644
--- 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
@@ -6,9 +6,10 @@ import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
export interface TabItem {
id: string;
label: string;
- icon?: IconDefinition;
+ icon?: string | IconDefinition;
disabled?: boolean;
badge?: string | number;
+ closeable?: boolean;
}
export type TabVariant = 'line' | 'pills' | 'cards';
@@ -34,7 +35,11 @@ export type TabSize = 'small' | 'medium' | 'large';
(click)="selectTab(tab.id)"
>
@if (tab.icon) {
-
+ @if (isIconDefinition(tab.icon)) {
+
+ } @else {
+
+ }
}
{{ tab.label }}
@if (tab.badge) {
@@ -95,4 +100,8 @@ export class TabGroupComponent {
this.tabChange.emit(tabId);
}
}
+
+ isIconDefinition(icon: string | IconDefinition): icon is IconDefinition {
+ return typeof icon === 'object' && icon !== null && 'iconName' in icon;
+ }
}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/layouts/tab-container.component.ts b/projects/ui-essentials/src/lib/layouts/tab-container.component.ts
index a2edb30..c93c368 100644
--- a/projects/ui-essentials/src/lib/layouts/tab-container.component.ts
+++ b/projects/ui-essentials/src/lib/layouts/tab-container.component.ts
@@ -18,11 +18,12 @@ import {
import { CommonModule } from '@angular/common';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
+import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
export interface TabItem {
id: string;
label: string;
- icon?: string;
+ icon?: string | IconDefinition;
disabled?: boolean;
closeable?: boolean;
badge?: string | number;
diff --git a/projects/ui-essentials/src/public-api.ts b/projects/ui-essentials/src/public-api.ts
index 176a4a9..f410e8d 100644
--- a/projects/ui-essentials/src/public-api.ts
+++ b/projects/ui-essentials/src/public-api.ts
@@ -10,3 +10,23 @@ export * from './lib/components/media/index';
export * from './lib/components/feedback/index';
export * from './lib/components/overlays/index';
export * from './lib/components/layout/index';
+
+// Layout Components (avoiding conflicts with navigation tab components)
+export { DashboardShellLayoutComponent } from './lib/layouts/dashboard-shell-layout.component';
+export { WidgetGridLayoutComponent } from './lib/layouts/widget-grid-layout.component';
+export { KpiCardLayoutComponent } from './lib/layouts/kpi-card-layout.component';
+export { BentoGridLayoutComponent } from './lib/layouts/bento-grid-layout.component';
+export { ListDetailLayoutComponent } from './lib/layouts/list-detail-layout.component';
+export { FeedLayoutComponent } from './lib/layouts/feed-layout.component';
+export { SupportingPaneLayoutComponent } from './lib/layouts/supporting-pane-layout.component';
+export { WidgetContainerComponent } from './lib/layouts/widget-container.component';
+export { GridContainerComponent } from './lib/layouts/grid-container.component';
+export { ScrollContainerComponent } from './lib/layouts/scroll-container.component';
+export { LoadingStateContainerComponent } from './lib/layouts/loading-state-container.component';
+
+// Layout component types
+export { TabContainerComponent } from './lib/layouts/tab-container.component';
+export type { TabItem as LayoutTabItem, TabChangeEvent as LayoutTabChangeEvent, TabCloseEvent as LayoutTabCloseEvent } from './lib/layouts/tab-container.component';
+export type { VirtualScrollConfig } from './lib/layouts/scroll-container.component';
+export type { GridResponsiveConfig } from './lib/layouts/grid-container.component';
+export type { LoadingState, ErrorState } from './lib/layouts/loading-state-container.component';