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

+
+
+ + +
+

Style Variants

+
+
+

Solid divider

+ +
+
+

Dashed divider

+ +
+
+

Dotted divider

+ +
+
+
+ + +
+

Thickness

+
+
+

Thin divider

+ +
+
+

Default thickness

+ +
+
+

Thick divider

+ +
+
+
+ + +
+

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) { +
+ + +

{{ size }}

+
+ } +
+
+ + +
+

Variants

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

{{ variant }}

+
+ } +
+
+ + +
+

Stroke Width

+
+ @for (stroke of strokes; track stroke) { +
+ + +

{{ stroke }}

+
+ } +
+
+ + +
+

States

+
+
+ + +

Default

+
+ +
+ + +

Disabled

+
+ +
+ + +

Indeterminate

+
+ +
+ + +

Complete

+
+
+
+ + +
+

Label Variations

+
+
+ + +

No Label

+
+ +
+ + +

Percentage

+
+ +
+ + +

Fraction

+
+ +
+ + ✓ + +

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

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

Form Integration

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

Form Values:

+
{{ getFormValues() | json }}
+
+
+
+ + +
+

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: ` +
+ + +
+ +
+ + + @if (isVisible()) { +
+ +
+ {{ text }} +
+ +
+
+ } +
+ `, + 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 (!indeterminate) { + + } @else { + + } + + + + @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 (label) { + + } + + @if (showValue) { + + {{ formatValue(currentValue()) }}{{ valueUnit }} + + } +
+ } + +
+ +
+ +
+
+
+ + + + + +
+
+
+ + @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';