Fix SCSS semantic token variable errors across components

- Replace incorrect semantic token names with correct ones:
  • $semantic-border-width-thin → $semantic-border-width-1
  • $semantic-color-border-default → $semantic-color-border-primary
  • $semantic-spacing-content-* → $semantic-spacing-component-*
  • $semantic-typography-body-* → $semantic-typography-font-size-*
  • $semantic-typography-caption-* → $semantic-typography-font-size-*
  • $semantic-motion-easing-standard → $semantic-easing-standard
  • $semantic-color-surface-tertiary → $semantic-color-surface-secondary
  • Various hover color tokens → base color tokens

- Fix typography map usage errors:
  • Replace heading map tokens with individual size tokens
  • $semantic-typography-heading-h* → $semantic-typography-heading-h*-size

- Update affected components:
  • tooltip, divider, progress-circle, range-slider components
  • Related demo components and SCSS files

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
skyai_dev
2025-09-03 07:50:34 +10:00
parent 5983722793
commit 6f0ab0cf5f
62 changed files with 3493 additions and 72 deletions

View File

@@ -1,12 +1,10 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router'; 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"; import { DashboardComponent } from "./features/dashboard/dashboard.component";
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
imports: [RouterOutlet, ButtonComponent, FabComponent, DashboardComponent], imports: [DashboardComponent],
template: ` template: `
<skyui-dashboard></skyui-dashboard> <skyui-dashboard></skyui-dashboard>

View File

@@ -1,6 +1,6 @@
import { Component, ChangeDetectionStrategy, signal } from '@angular/core'; import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import { CommonModule } from '@angular/common'; 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 { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { import {
faBars, faBars,

View File

@@ -1,7 +1,7 @@
import { Component, signal } from '@angular/core'; import { Component, signal } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormControl, FormGroup } from '@angular/forms'; 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({ @Component({
selector: 'ui-autocomplete-demo', selector: 'ui-autocomplete-demo',

View File

@@ -1,6 +1,6 @@
import { Component, ChangeDetectionStrategy } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common'; 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 { interface Activity {
user: string; user: string;

View File

@@ -1,6 +1,6 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; 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({ @Component({
selector: 'ui-backdrop-demo', selector: 'ui-backdrop-demo',

View File

@@ -1,6 +1,6 @@
import { Component, ChangeDetectionStrategy } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common'; 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({ @Component({
selector: 'ui-badge-demo', selector: 'ui-badge-demo',

View File

@@ -1,10 +1,10 @@
import { Component, ChangeDetectionStrategy } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ButtonComponent } 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'; import { TextButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons/text-button.component';
import { GhostButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons'; import { GhostButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons/ghost-button.component';
import { FabComponent } from '../../../../../ui-essentials/src/lib/components/buttons'; import { FabComponent } from '../../../../../ui-essentials/src/lib/components/buttons/fab.component';
import { SimpleButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons'; import { SimpleButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons/simple-button.component';
import { import {
faDownload, faDownload,
faPlus, faPlus,

View File

@@ -1,7 +1,7 @@
import { Component, ChangeDetectionStrategy } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CardComponent, GlassVariant } from '../../../../../ui-essentials/src/lib/components/data-display/card'; import { CardComponent, GlassVariant } from '../../../../../ui-essentials/src/lib/components/data-display/card/card.component';
import { ButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons'; import { ButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons/button.component';
@Component({ @Component({
selector: 'ui-card-demo', selector: 'ui-card-demo',

View File

@@ -1,6 +1,6 @@
import { Component, ChangeDetectionStrategy } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common'; 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({ @Component({
selector: 'ui-carousel-demo', selector: 'ui-carousel-demo',

View File

@@ -1,7 +1,7 @@
import { Component, ChangeDetectionStrategy, signal } from '@angular/core'; import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; 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({ @Component({
selector: 'ui-checkbox-demo', selector: 'ui-checkbox-demo',

View File

@@ -1,7 +1,7 @@
import { Component, ChangeDetectionStrategy } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ChipComponent } from '../../../../../ui-essentials/src/lib/components/data-display/chip'; import { ChipComponent } from '../../../../../ui-essentials/src/lib/components/data-display/chip/chip.component';
import { ButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons'; import { ButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons/button.component';
import { import {
faHeart, faHeart,
faUser, faUser,

View File

@@ -1,6 +1,7 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; 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({ @Component({
selector: 'ui-container-demo', selector: 'ui-container-demo',

View File

@@ -39,6 +39,10 @@ import { AutocompleteDemoComponent } from './autocomplete-demo/autocomplete-demo
import { BackdropDemoComponent } from './backdrop-demo/backdrop-demo.component'; import { BackdropDemoComponent } from './backdrop-demo/backdrop-demo.component';
import { OverlayContainerDemoComponent } from './overlay-container-demo/overlay-container-demo.component'; import { OverlayContainerDemoComponent } from './overlay-container-demo/overlay-container-demo.component';
import { LoadingSpinnerDemoComponent } from './loading-spinner-demo/loading-spinner-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({ @Component({
@@ -193,12 +197,28 @@ import { LoadingSpinnerDemoComponent } from './loading-spinner-demo/loading-spin
<ui-loading-spinner-demo></ui-loading-spinner-demo> <ui-loading-spinner-demo></ui-loading-spinner-demo>
} }
@case ("progress-circle") {
<ui-progress-circle-demo></ui-progress-circle-demo>
}
@case ("range-slider") {
<ui-range-slider-demo></ui-range-slider-demo>
}
@case ("divider") {
<ui-divider-demo></ui-divider-demo>
}
@case ("tooltip") {
<ui-tooltip-demo></ui-tooltip-demo>
}
} }
`, `,
imports: [AvatarDemoComponent, ButtonDemoComponent, CardDemoComponent, imports: [AvatarDemoComponent, ButtonDemoComponent, CardDemoComponent,
ChipDemoComponent, TableDemoComponent, BadgeDemoComponent, ChipDemoComponent, TableDemoComponent, BadgeDemoComponent,
MenuDemoComponent, InputDemoComponent, InputDemoComponent, MenuDemoComponent, InputDemoComponent,
LayoutDemoComponent, RadioDemoComponent, CheckboxDemoComponent, LayoutDemoComponent, RadioDemoComponent, CheckboxDemoComponent,
SearchDemoComponent, SwitchDemoComponent, ProgressDemoComponent, SearchDemoComponent, SwitchDemoComponent, ProgressDemoComponent,
AppbarDemoComponent, FontAwesomeDemoComponent, ImageContainerDemoComponent, AppbarDemoComponent, FontAwesomeDemoComponent, ImageContainerDemoComponent,
@@ -206,7 +226,8 @@ import { LoadingSpinnerDemoComponent } from './loading-spinner-demo/loading-spin
ModalDemoComponent, DrawerDemoComponent, DatePickerDemoComponent, TimePickerDemoComponent, ModalDemoComponent, DrawerDemoComponent, DatePickerDemoComponent, TimePickerDemoComponent,
GridSystemDemoComponent, SpacerDemoComponent, ContainerDemoComponent, PaginationDemoComponent, GridSystemDemoComponent, SpacerDemoComponent, ContainerDemoComponent, PaginationDemoComponent,
SkeletonLoaderDemoComponent, EmptyStateDemoComponent, FileUploadDemoComponent, FormFieldDemoComponent, SkeletonLoaderDemoComponent, EmptyStateDemoComponent, FileUploadDemoComponent, FormFieldDemoComponent,
AutocompleteDemoComponent, BackdropDemoComponent, OverlayContainerDemoComponent, LoadingSpinnerDemoComponent] AutocompleteDemoComponent, BackdropDemoComponent, OverlayContainerDemoComponent, LoadingSpinnerDemoComponent,
ProgressCircleDemoComponent, RangeSliderDemoComponent, DividerDemoComponent, TooltipDemoComponent]
}) })

View File

@@ -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;
}

View File

@@ -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: `
<div class="demo-container">
<h2>Divider Demo</h2>
<!-- Orientation -->
<section class="demo-section">
<h3>Orientation</h3>
<div class="demo-row">
<div style="width: 100%;">
<p>Content above</p>
<ui-divider orientation="horizontal"></ui-divider>
<p>Content below</p>
</div>
</div>
<div class="demo-row" style="height: 100px; display: flex; align-items: center;">
<p>Left content</p>
<ui-divider orientation="vertical"></ui-divider>
<p>Right content</p>
</div>
</section>
<!-- Style Variants -->
<section class="demo-section">
<h3>Style Variants</h3>
<div class="demo-column">
<div>
<p>Solid divider</p>
<ui-divider variant="solid"></ui-divider>
</div>
<div>
<p>Dashed divider</p>
<ui-divider variant="dashed"></ui-divider>
</div>
<div>
<p>Dotted divider</p>
<ui-divider variant="dotted"></ui-divider>
</div>
</div>
</section>
<!-- Thickness Variants -->
<section class="demo-section">
<h3>Thickness</h3>
<div class="demo-column">
<div>
<p>Thin divider</p>
<ui-divider thickness="thin"></ui-divider>
</div>
<div>
<p>Default thickness</p>
<ui-divider thickness="default"></ui-divider>
</div>
<div>
<p>Thick divider</p>
<ui-divider thickness="thick"></ui-divider>
</div>
</div>
</section>
<!-- With Content -->
<section class="demo-section">
<h3>With Content</h3>
<div class="demo-column">
<ui-divider>OR</ui-divider>
<ui-divider>Section Break</ui-divider>
<ui-divider>More Content</ui-divider>
</div>
</section>
<!-- Combined Examples -->
<section class="demo-section">
<h3>Combined Examples</h3>
<div class="demo-column">
<ui-divider variant="dashed" thickness="thin">Dashed Thin</ui-divider>
<ui-divider variant="dotted" thickness="thick">Dotted Thick</ui-divider>
</div>
</section>
<!-- Vertical Examples -->
<section class="demo-section">
<h3>Vertical Examples</h3>
<div style="display: flex; height: 80px; align-items: center; gap: 16px;">
<span>Item 1</span>
<ui-divider orientation="vertical" variant="solid" thickness="thin"></ui-divider>
<span>Item 2</span>
<ui-divider orientation="vertical" variant="dashed"></ui-divider>
<span>Item 3</span>
<ui-divider orientation="vertical" variant="dotted" thickness="thick"></ui-divider>
<span>Item 4</span>
</div>
</section>
</div>
`,
styleUrl: './divider-demo.component.scss'
})
export class DividerDemoComponent {}

View File

@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { faBars, faUser, faCog, faHome, faChartLine, faEnvelope, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; 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({ @Component({
selector: 'ui-drawer-demo', selector: 'ui-drawer-demo',

View File

@@ -1,6 +1,6 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; 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({ @Component({
selector: 'ui-empty-state-demo', selector: 'ui-empty-state-demo',

View File

@@ -1,7 +1,7 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; 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({ @Component({
selector: 'ui-file-upload-demo', selector: 'ui-file-upload-demo',
@@ -295,7 +295,7 @@ export class FileUploadDemoComponent {
submittedFiles: UploadedFile[] = []; submittedFiles: UploadedFile[] = [];
recentEvents: Array<{type: string, fileName: string, timestamp: Date}> = []; 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 // Basic usage
<ui-file-upload <ui-file-upload

View File

@@ -1,7 +1,7 @@
import { Component, ChangeDetectionStrategy } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { ButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons'; import { ButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons/button.component';
import { import {
// Solid icons from shared-ui // Solid icons from shared-ui
faUser, faUser,

View File

@@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormControl, FormGroup, ReactiveFormsModule, Validators, AbstractControl, ValidationErrors } from '@angular/forms'; import { FormControl, FormGroup, ReactiveFormsModule, Validators, AbstractControl, ValidationErrors } from '@angular/forms';
import { FormFieldComponent } from '../../../../../ui-essentials/src/lib/components/forms/form-field'; import { FormFieldComponent } from '../../../../../ui-essentials/src/lib/components/forms/form-field/form-field.component';
@Component({ @Component({
selector: 'ui-form-field-demo', selector: 'ui-form-field-demo',
@@ -562,7 +562,7 @@ export class FormFieldDemoComponent implements OnInit {
custom: 'Custom validation error' custom: 'Custom validation error'
}; };
readonly codeExample = `import { FormFieldComponent } from 'ui-essentials'; readonly codeExample = `import { FormFieldComponent } from '../../../../../ui-essentials/src/lib/components/forms/form-field/form-field.component';
// Basic usage // Basic usage
<ui-form-field <ui-form-field

View File

@@ -1,6 +1,6 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { GridSystemComponent } from '../../../../../ui-essentials/src/lib/components/layout'; import { GridSystemComponent } from '../../../../../ui-essentials/src/lib/components/layout/grid-system/grid-system.component';
@Component({ @Component({
selector: 'ui-grid-system-demo', selector: 'ui-grid-system-demo',

View File

@@ -1,7 +1,7 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ImageContainerComponent, ImageContainerSize, ImageContainerAspectRatio, ImageContainerObjectFit, ImageContainerShape } from '../../../../../ui-essentials/src/lib/components/data-display/image-container'; import { ImageContainerComponent, ImageContainerSize, ImageContainerAspectRatio, ImageContainerObjectFit, ImageContainerShape } from '../../../../../ui-essentials/src/lib/components/data-display/image-container/image-container.component';
import { BadgeComponent } from '../../../../../ui-essentials/src/lib/components/data-display/badge'; import { BadgeComponent } from '../../../../../ui-essentials/src/lib/components/data-display/badge/badge.component';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { faRefresh, faHeart, faPlay } from '@fortawesome/free-solid-svg-icons'; import { faRefresh, faHeart, faPlay } from '@fortawesome/free-solid-svg-icons';

View File

@@ -1,9 +1,9 @@
import { Component, ChangeDetectionStrategy, signal } from '@angular/core'; import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { TextInputComponent } from '../../../../../ui-essentials/src/lib/components/forms/input'; import { TextInputComponent } from '../../../../../ui-essentials/src/lib/components/forms/input/text-input.component';
import { TextareaComponent } from '../../../../../ui-essentials/src/lib/components/forms/input'; import { TextareaComponent } from '../../../../../ui-essentials/src/lib/components/forms/input/textarea.component';
import { InputWrapperComponent } from '../../../../../ui-essentials/src/lib/components/forms/input'; import { InputWrapperComponent } from '../../../../../ui-essentials/src/lib/components/forms/input/input-wrapper.component';
import { faSearch, faEnvelope, faEdit } from '@fortawesome/free-solid-svg-icons'; import { faSearch, faEnvelope, faEdit } from '@fortawesome/free-solid-svg-icons';
@Component({ @Component({

View File

@@ -1,6 +1,6 @@
import { Component, ChangeDetectionStrategy, signal } from '@angular/core'; import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { DashboardShellLayoutComponent, WidgetGridLayoutComponent, WidgetContainerComponent, BentoGridLayoutComponent, KpiCardLayoutComponent, ListDetailLayoutComponent, FeedLayoutComponent, SupportingPaneLayoutComponent, GridContainerComponent, TabContainerComponent, ScrollContainerComponent, LoadingStateContainerComponent, TabItem, ErrorState, GridResponsiveConfig, LoadingState, VirtualScrollConfig } from "../../../../../ui-essentials/src/lib/layouts"; import { BentoGridLayoutComponent, DashboardShellLayoutComponent, ErrorState, FeedLayoutComponent, GridContainerComponent, GridResponsiveConfig, KpiCardLayoutComponent, ListDetailLayoutComponent, LoadingState, LoadingStateContainerComponent, ScrollContainerComponent, SupportingPaneLayoutComponent, TabContainerComponent, TabItem, VirtualScrollConfig, WidgetContainerComponent, WidgetGridLayoutComponent } from '../../../../../ui-essentials/src/public-api';
// Note: Local layout components are available but not used in this demo // Note: Local layout components are available but not used in this demo
// They can be imported if needed for specific layout demonstrations // They can be imported if needed for specific layout demonstrations

View File

@@ -1,10 +1,8 @@
import { Component, ChangeDetectionStrategy, signal } from '@angular/core'; import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { import { ListItemComponent } from '../../../../../ui-essentials/src/lib/components/data-display/list/list-item.component';
ListItemComponent, import { ListContainerComponent } from '../../../../../ui-essentials/src/lib/components/data-display/list/list-container.component';
ListContainerComponent, import { ListItemData } from '../../../../../ui-essentials/src/lib/components/data-display/list/list-item.component';
ListItemData
} from '../../../../../ui-essentials/src/lib/components/data-display/list';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { import {
faInbox, faInbox,

View File

@@ -1,7 +1,7 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { LoadingSpinnerComponent } from 'ui-essentials'; import { LoadingSpinnerComponent } from '../../../../../ui-essentials/src/lib/components/feedback/loading-spinner';
@Component({ @Component({
selector: 'ui-loading-spinner-demo', selector: 'ui-loading-spinner-demo',

View File

@@ -10,7 +10,10 @@ import {
faCircle, faRefresh, faToggleOn faCircle, faRefresh, faToggleOn
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
import { faAngular, faGithub } from '@fortawesome/free-brands-svg-icons'; import { faAngular, faGithub } from '@fortawesome/free-brands-svg-icons';
import { MenuItemComponent, MenuContainerComponent, MenuSubmenuComponent, MenuItemData } from '../../../../../ui-essentials/src/lib/components/navigation/menu'; import { MenuItemComponent } from '../../../../../ui-essentials/src/lib/components/navigation/menu/menu-item.component';
import { MenuContainerComponent } from '../../../../../ui-essentials/src/lib/components/navigation/menu/menu-container.component';
import { MenuSubmenuComponent } from '../../../../../ui-essentials/src/lib/components/navigation/menu/menu-submenu.component';
import { MenuItemData } from '../../../../../ui-essentials/src/lib/components/navigation/menu/menu-item.component';
@Component({ @Component({
selector: 'ui-menu-demo', selector: 'ui-menu-demo',

View File

@@ -3,7 +3,8 @@ import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { faExclamationTriangle, faCheckCircle, faInfoCircle } from '@fortawesome/free-solid-svg-icons'; import { faExclamationTriangle, faCheckCircle, faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import { ButtonComponent, ModalComponent } from "../../../../../ui-essentials/src/public-api"; import { ButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons';
import { ModalComponent } from '../../../../../ui-essentials/src/lib/components/overlays/modal';
@Component({ @Component({
selector: 'ui-modal-demo', selector: 'ui-modal-demo',

View File

@@ -1,6 +1,6 @@
import { Component, ChangeDetectionStrategy, signal } from '@angular/core'; import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { PaginationComponent } from '../../../../../ui-essentials/src/lib/components/navigation/pagination'; import { PaginationComponent } from '../../../../../ui-essentials/src/lib/components/navigation/pagination/pagination.component';
@Component({ @Component({
selector: 'ui-pagination-demo', selector: 'ui-pagination-demo',

View File

@@ -0,0 +1,184 @@
@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-content-heading;
margin-top: $semantic-spacing-layout-lg;
}
}
.demo-section {
margin-bottom: $semantic-spacing-layout-xl;
&:last-child {
margin-bottom: 0;
}
}
.demo-row {
display: flex;
gap: $semantic-spacing-component-lg;
flex-wrap: wrap;
align-items: start;
}
.demo-item {
display: flex;
flex-direction: column;
align-items: center;
gap: $semantic-spacing-content-line-normal;
padding: $semantic-spacing-component-md;
border: 1px solid $semantic-color-border-subtle;
border-radius: $semantic-border-radius-md;
background: $semantic-color-surface-primary;
min-width: 100px;
p {
margin: 0;
font-size: $semantic-typography-font-size-md;
color: $semantic-color-text-secondary;
text-align: center;
font-weight: $semantic-typography-font-weight-medium;
}
&:hover {
border-color: $semantic-color-border-primary;
box-shadow: $semantic-shadow-elevation-1;
}
}
.demo-controls {
display: flex;
gap: $semantic-spacing-component-lg;
flex-wrap: wrap;
margin-bottom: $semantic-spacing-component-lg;
padding: $semantic-spacing-component-lg;
background: $semantic-color-surface-secondary;
border-radius: $semantic-border-radius-lg;
border: 1px solid $semantic-color-border-subtle;
.control-group {
display: flex;
flex-direction: column;
gap: $semantic-spacing-component-xs;
min-width: 120px;
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%;
min-width: 150px;
}
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;
}
}
}
button {
padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
border: 1px solid $semantic-color-primary;
border-radius: $semantic-border-radius-md;
background: $semantic-color-primary;
color: $semantic-color-on-primary;
font-size: $semantic-typography-font-size-sm;
font-weight: $semantic-typography-font-weight-medium;
cursor: pointer;
transition: all $semantic-motion-duration-fast $semantic-easing-standard;
&:hover:not(:disabled) {
background: $semantic-color-primary-hover;
border-color: $semantic-color-primary-hover;
box-shadow: $semantic-shadow-elevation-2;
}
&:focus {
outline: 2px solid $semantic-color-primary;
outline-offset: 2px;
}
&:disabled {
opacity: 0.38;
cursor: not-allowed;
background: $semantic-color-surface-secondary;
border-color: $semantic-color-border-subtle;
color: $semantic-color-text-tertiary;
}
}
}
.demo-interactive-result {
display: flex;
justify-content: center;
padding: $semantic-spacing-component-xl;
background: $semantic-color-surface-primary;
border-radius: $semantic-border-radius-lg;
border: 2px dashed $semantic-color-border-subtle;
}
// Responsive design
@media (max-width: $semantic-breakpoint-md - 1) {
.demo-container {
padding: $semantic-spacing-layout-md;
}
.demo-row {
gap: $semantic-spacing-component-md;
}
.demo-controls {
flex-direction: column;
gap: $semantic-spacing-component-md;
.control-group {
min-width: auto;
}
}
}
@media (max-width: $semantic-breakpoint-sm - 1) {
.demo-container {
padding: $semantic-spacing-layout-sm;
}
.demo-row {
justify-content: center;
}
.demo-item {
min-width: 80px;
}
}

View File

@@ -0,0 +1,324 @@
import { Component, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ProgressCircleComponent } from '../../../../../ui-essentials/src/lib/components/feedback/progress-circle/progress-circle.component';
@Component({
selector: 'ui-progress-circle-demo',
standalone: true,
imports: [CommonModule, FormsModule, ProgressCircleComponent],
template: `
<div class="demo-container">
<h2>Progress Circle Demo</h2>
<!-- Size Variants -->
<section class="demo-section">
<h3>Sizes</h3>
<div class="demo-row">
@for (size of sizes; track size) {
<div class="demo-item">
<ui-progress-circle
[size]="size"
[value]="75"
[showLabel]="true"
[labelContent]="'75%'">
</ui-progress-circle>
<p>{{ size }}</p>
</div>
}
</div>
</section>
<!-- Color Variants -->
<section class="demo-section">
<h3>Variants</h3>
<div class="demo-row">
@for (variant of variants; track variant) {
<div class="demo-item">
<ui-progress-circle
[variant]="variant"
[value]="65"
[showLabel]="true"
[labelContent]="'65%'">
</ui-progress-circle>
<p>{{ variant }}</p>
</div>
}
</div>
</section>
<!-- Stroke Width Variants -->
<section class="demo-section">
<h3>Stroke Width</h3>
<div class="demo-row">
@for (stroke of strokes; track stroke) {
<div class="demo-item">
<ui-progress-circle
[stroke]="stroke"
[value]="80"
[showLabel]="true"
[labelContent]="'80%'">
</ui-progress-circle>
<p>{{ stroke }}</p>
</div>
}
</div>
</section>
<!-- States -->
<section class="demo-section">
<h3>States</h3>
<div class="demo-row">
<div class="demo-item">
<ui-progress-circle
[value]="45"
[showLabel]="true"
[labelContent]="'45%'">
</ui-progress-circle>
<p>Default</p>
</div>
<div class="demo-item">
<ui-progress-circle
[disabled]="true"
[value]="45"
[showLabel]="true"
[labelContent]="'45%'">
</ui-progress-circle>
<p>Disabled</p>
</div>
<div class="demo-item">
<ui-progress-circle
[indeterminate]="true"
variant="primary">
</ui-progress-circle>
<p>Indeterminate</p>
</div>
<div class="demo-item">
<ui-progress-circle
[value]="100"
variant="success"
[showLabel]="true"
[labelContent]="'✓'">
</ui-progress-circle>
<p>Complete</p>
</div>
</div>
</section>
<!-- Label Variations -->
<section class="demo-section">
<h3>Label Variations</h3>
<div class="demo-row">
<div class="demo-item">
<ui-progress-circle
[value]="75"
size="lg">
</ui-progress-circle>
<p>No Label</p>
</div>
<div class="demo-item">
<ui-progress-circle
[value]="75"
size="lg"
[showLabel]="true"
[labelContent]="'75%'">
</ui-progress-circle>
<p>Percentage</p>
</div>
<div class="demo-item">
<ui-progress-circle
[value]="45"
[max]="60"
size="lg"
[showLabel]="true"
[labelContent]="'45/60'">
</ui-progress-circle>
<p>Fraction</p>
</div>
<div class="demo-item">
<ui-progress-circle
[value]="100"
size="lg"
variant="success"
[showLabel]="true">
</ui-progress-circle>
<p>Custom Content</p>
</div>
</div>
</section>
<!-- Interactive Example -->
<section class="demo-section">
<h3>Interactive</h3>
<div class="demo-controls">
<div class="control-group">
<label>Value: {{ interactiveValue() }}</label>
<input
type="range"
min="0"
max="100"
[value]="interactiveValue()"
(input)="updateValue($event)" />
</div>
<div class="control-group">
<label>Size:</label>
<select [value]="interactiveSize()" (change)="updateSize($event)">
@for (size of sizes; track size) {
<option [value]="size">{{ size }}</option>
}
</select>
</div>
<div class="control-group">
<label>Variant:</label>
<select [value]="interactiveVariant()" (change)="updateVariant($event)">
@for (variant of variants; track variant) {
<option [value]="variant">{{ variant }}</option>
}
</select>
</div>
<div class="control-group">
<label>
<input
type="checkbox"
[checked]="interactiveShowLabel()"
(change)="toggleLabel($event)" />
Show Label
</label>
</div>
<div class="control-group">
<label>
<input
type="checkbox"
[checked]="interactiveIndeterminate()"
(change)="toggleIndeterminate($event)" />
Indeterminate
</label>
</div>
</div>
<div class="demo-interactive-result">
<ui-progress-circle
[value]="interactiveValue()"
[size]="interactiveSize()"
[variant]="interactiveVariant()"
[showLabel]="interactiveShowLabel()"
[labelContent]="interactiveValue() + '%'"
[indeterminate]="interactiveIndeterminate()">
</ui-progress-circle>
</div>
</section>
<!-- Animation Demo -->
<section class="demo-section">
<h3>Animation Demo</h3>
<div class="demo-controls">
<button (click)="startProgress()" [disabled]="progressRunning()">
Start Progress Animation
</button>
<button (click)="resetProgress()">
Reset
</button>
</div>
<div class="demo-row">
<div class="demo-item">
<ui-progress-circle
[value]="animatedValue()"
size="xl"
variant="primary"
[showLabel]="true"
[labelContent]="animatedValue() + '%'">
</ui-progress-circle>
<p>Animated Progress</p>
</div>
</div>
</section>
</div>
`,
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();
}
}

View File

@@ -1,7 +1,7 @@
import { Component, ChangeDetectionStrategy, signal } from '@angular/core'; import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; 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({ @Component({
selector: 'ui-progress-demo', selector: 'ui-progress-demo',

View File

@@ -1,11 +1,9 @@
import { Component, ChangeDetectionStrategy, signal } from '@angular/core'; import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { import { RadioButtonComponent } from '../../../../../ui-essentials/src/lib/components/forms/radio/radio-button.component';
RadioButtonComponent, import { RadioGroupComponent } from '../../../../../ui-essentials/src/lib/components/forms/radio/radio-group.component';
RadioGroupComponent, import { RadioButtonData } from '../../../../../ui-essentials/src/lib/components/forms/radio/radio-button.component';
RadioButtonData
} from '../../../../../ui-essentials/src/lib/components/forms/radio';
@Component({ @Component({
selector: 'ui-radio-demo', selector: 'ui-radio-demo',

View File

@@ -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;
}
}

View File

@@ -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: `
<div class="demo-container">
<h2>Range Slider Demo</h2>
<!-- Size Variants -->
<section class="demo-section">
<h3>Sizes</h3>
<div class="demo-column">
@for (size of sizes; track size) {
<div class="demo-item">
<ui-range-slider
[size]="size"
[value]="75"
[label]="size.toUpperCase() + ' Size'"
[showValue]="true">
</ui-range-slider>
</div>
}
</div>
</section>
<!-- Color Variants -->
<section class="demo-section">
<h3>Variants</h3>
<div class="demo-column">
@for (variant of variants; track variant) {
<div class="demo-item">
<ui-range-slider
[variant]="variant"
[value]="65"
[label]="variant + ' variant'"
[showValue]="true">
</ui-range-slider>
</div>
}
</div>
</section>
<!-- States -->
<section class="demo-section">
<h3>States</h3>
<div class="demo-column">
<div class="demo-item">
<ui-range-slider
[value]="45"
label="Default State"
[showValue]="true">
</ui-range-slider>
</div>
<div class="demo-item">
<ui-range-slider
[disabled]="true"
[value]="45"
label="Disabled State"
[showValue]="true">
</ui-range-slider>
</div>
<div class="demo-item">
<ui-range-slider
[readonly]="true"
[value]="75"
label="Readonly State"
[showValue]="true">
</ui-range-slider>
</div>
<div class="demo-item">
<ui-range-slider
[value]="30"
[hasError]="true"
label="Error State"
helperText="This field has an error"
[showValue]="true">
</ui-range-slider>
</div>
</div>
</section>
<!-- Range Configurations -->
<section class="demo-section">
<h3>Range Configurations</h3>
<div class="demo-column">
<div class="demo-item">
<ui-range-slider
[min]="0"
[max]="100"
[step]="1"
[value]="50"
label="Standard (0-100, step 1)"
[showValue]="true">
</ui-range-slider>
</div>
<div class="demo-item">
<ui-range-slider
[min]="0"
[max]="10"
[step]="0.5"
[value]="7.5"
label="Decimal Steps (0-10, step 0.5)"
[showValue]="true">
</ui-range-slider>
</div>
<div class="demo-item">
<ui-range-slider
[min]="-50"
[max]="50"
[step]="5"
[value]="-10"
label="Negative Range (-50 to 50, step 5)"
[showValue]="true">
</ui-range-slider>
</div>
<div class="demo-item">
<ui-range-slider
[min]="1000"
[max]="10000"
[step]="100"
[value]="5500"
label="Large Numbers (1K-10K)"
[showValue]="true"
valueUnit=" units">
</ui-range-slider>
</div>
</div>
</section>
<!-- With Tick Marks -->
<section class="demo-section">
<h3>Tick Marks</h3>
<div class="demo-column">
<div class="demo-item">
<ui-range-slider
[min]="0"
[max]="100"
[step]="10"
[value]="60"
[ticks]="basicTicks"
label="Basic Tick Marks"
[showValue]="true">
</ui-range-slider>
</div>
<div class="demo-item">
<ui-range-slider
[min]="0"
[max]="5"
[step]="1"
[value]="3"
[ticks]="labeledTicks"
[showTickLabels]="true"
label="Labeled Tick Marks"
[showValue]="true">
</ui-range-slider>
</div>
<div class="demo-item">
<ui-range-slider
[min]="0"
[max]="100"
[step]="5"
[value]="75"
[ticks]="majorMinorTicks"
[showTickLabels]="true"
label="Major & Minor Ticks"
[showValue]="true">
</ui-range-slider>
</div>
</div>
</section>
<!-- Interactive Example -->
<section class="demo-section">
<h3>Interactive Configuration</h3>
<div class="demo-controls">
<div class="control-row">
<div class="control-group">
<label>Min Value: {{ interactiveMin() }}</label>
<input
type="range"
min="-100"
max="100"
[value]="interactiveMin()"
(input)="updateMin($event)" />
</div>
<div class="control-group">
<label>Max Value: {{ interactiveMax() }}</label>
<input
type="range"
min="0"
max="200"
[value]="interactiveMax()"
(input)="updateMax($event)" />
</div>
<div class="control-group">
<label>Step: {{ interactiveStep() }}</label>
<select [value]="interactiveStep()" (change)="updateStep($event)">
<option value="1">1</option>
<option value="5">5</option>
<option value="10">10</option>
<option value="0.1">0.1</option>
<option value="0.5">0.5</option>
</select>
</div>
</div>
<div class="control-row">
<div class="control-group">
<label>Size:</label>
<select [value]="interactiveSize()" (change)="updateSize($event)">
@for (size of sizes; track size) {
<option [value]="size">{{ size }}</option>
}
</select>
</div>
<div class="control-group">
<label>Variant:</label>
<select [value]="interactiveVariant()" (change)="updateVariant($event)">
@for (variant of variants; track variant) {
<option [value]="variant">{{ variant }}</option>
}
</select>
</div>
<div class="control-group">
<label>
<input
type="checkbox"
[checked]="interactiveShowValue()"
(change)="toggleShowValue($event)" />
Show Value
</label>
</div>
<div class="control-group">
<label>
<input
type="checkbox"
[checked]="interactiveDisabled()"
(change)="toggleDisabled($event)" />
Disabled
</label>
</div>
</div>
</div>
<div class="demo-interactive-result">
<ui-range-slider
[min]="interactiveMin()"
[max]="interactiveMax()"
[step]="interactiveStep()"
[value]="interactiveValue()"
[size]="interactiveSize()"
[variant]="interactiveVariant()"
[showValue]="interactiveShowValue()"
[disabled]="interactiveDisabled()"
label="Interactive Slider"
helperText="Customize using controls above"
(valueChange)="onInteractiveValueChange($event)">
</ui-range-slider>
</div>
</section>
<!-- Form Integration -->
<section class="demo-section">
<h3>Form Integration</h3>
<form [formGroup]="demoForm" class="demo-form">
<div class="form-row">
<ui-range-slider
formControlName="volume"
[min]="0"
[max]="100"
[step]="1"
label="Volume"
[showValue]="true"
valueUnit="%"
helperText="Adjust the volume level">
</ui-range-slider>
</div>
<div class="form-row">
<ui-range-slider
formControlName="brightness"
[min]="0"
[max]="255"
[step]="5"
label="Brightness"
[showValue]="true"
variant="secondary"
helperText="Screen brightness setting">
</ui-range-slider>
</div>
<div class="form-row">
<ui-range-slider
formControlName="temperature"
[min]="16"
[max]="30"
[step]="0.5"
[ticks]="temperatureTicks"
[showTickLabels]="true"
label="Temperature"
[showValue]="true"
valueUnit="°C"
variant="warning">
</ui-range-slider>
</div>
<div class="form-output">
<h4>Form Values:</h4>
<pre>{{ getFormValues() | json }}</pre>
</div>
</form>
</section>
<!-- Event Demonstration -->
<section class="demo-section">
<h3>Event Handling</h3>
<div class="demo-column">
<div class="demo-item">
<ui-range-slider
[value]="eventSliderValue()"
label="Event Slider"
[showValue]="true"
(valueChange)="onValueChange($event)"
(slideStart)="onSlideStart($event)"
(slideEnd)="onSlideEnd($event)"
(sliderFocus)="onSliderFocus()"
(sliderBlur)="onSliderBlur()">
</ui-range-slider>
</div>
<div class="event-log">
<h4>Event Log:</h4>
<div class="event-list">
@for (event of eventLog(); track $index) {
<div class="event-item">{{ event }}</div>
}
</div>
<button (click)="clearEventLog()">Clear Log</button>
</div>
</div>
</section>
</div>
`,
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<string[]>([]);
// 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;
}
}

View File

@@ -1,7 +1,7 @@
import { Component, ChangeDetectionStrategy, signal } from '@angular/core'; import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; 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 { import {
faSearch, faSearch,
faMicrophone, faMicrophone,

View File

@@ -1,7 +1,7 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; 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({ @Component({
selector: 'ui-skeleton-loader-demo', selector: 'ui-skeleton-loader-demo',

View File

@@ -1,6 +1,6 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; 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({ @Component({
selector: 'ui-spacer-demo', selector: 'ui-spacer-demo',

View File

@@ -1,7 +1,7 @@
import { Component, ChangeDetectionStrategy, signal } from '@angular/core'; import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormControl, FormGroup, ReactiveFormsModule, Validators, FormsModule } from '@angular/forms'; 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({ @Component({
selector: 'ui-switch-demo', selector: 'ui-switch-demo',

View File

@@ -1,13 +1,10 @@
import { Component, ChangeDetectionStrategy, TemplateRef, ViewChild } from '@angular/core'; import { Component, ChangeDetectionStrategy, TemplateRef, ViewChild } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { import { TableComponent } from '../../../../../ui-essentials/src/lib/components/data-display/table/table.component';
TableComponent, import { TableAction, TableActionsComponent } from '../../../../../ui-essentials/src/lib/components/data-display/table-actions.component';
TableActionsComponent, import type { TableColumn } from '../../../../../ui-essentials/src/lib/components/data-display/table/table.component';
type TableColumn, import type { TableSortEvent } from '../../../../../ui-essentials/src/lib/components/data-display/table/table.component';
type TableAction, import { StatusBadgeComponent } from '../../../../../ui-essentials/src/lib/components/feedback/status-badge.component';
type TableSortEvent
} from '../../../../../ui-essentials/src/lib/components/data-display/table';
import { StatusBadgeComponent } from '../../../../../ui-essentials/src/lib/components/feedback';
interface User { interface User {
id: number; id: number;
name: string; name: string;

View File

@@ -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;
}
}

View File

@@ -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: `
<div class="demo-container">
<h2>Tooltip Demo</h2>
<!-- Positions -->
<section class="demo-section">
<h3>Positions</h3>
<div class="positions-grid">
<ui-tooltip text="Top tooltip" position="top">
<button class="demo-button">Top</button>
</ui-tooltip>
<ui-tooltip text="Bottom tooltip" position="bottom">
<button class="demo-button">Bottom</button>
</ui-tooltip>
<ui-tooltip text="Left tooltip" position="left">
<button class="demo-button">Left</button>
</ui-tooltip>
<ui-tooltip text="Right tooltip" position="right">
<button class="demo-button">Right</button>
</ui-tooltip>
</div>
</section>
<!-- Sizes -->
<section class="demo-section">
<h3>Sizes</h3>
<div class="demo-row">
<ui-tooltip text="Small tooltip with concise text" size="sm">
<button class="demo-button">Small</button>
</ui-tooltip>
<ui-tooltip text="Medium tooltip with moderate amount of text content" size="md">
<button class="demo-button">Medium</button>
</ui-tooltip>
<ui-tooltip text="Large tooltip with extensive text content that can wrap to multiple lines for better readability" size="lg">
<button class="demo-button">Large</button>
</ui-tooltip>
</div>
</section>
<!-- Triggers -->
<section class="demo-section">
<h3>Triggers</h3>
<div class="demo-row">
<ui-tooltip text="Hover to see tooltip" trigger="hover">
<button class="demo-button">Hover</button>
</ui-tooltip>
<ui-tooltip text="Click to toggle tooltip" trigger="click">
<button class="demo-button">Click</button>
</ui-tooltip>
<ui-tooltip text="Focus to see tooltip" trigger="focus">
<button class="demo-button">Focus</button>
</ui-tooltip>
</div>
</section>
<!-- Different Elements -->
<section class="demo-section">
<h3>Different Elements</h3>
<div class="demo-row">
<ui-tooltip text="Tooltip on button">
<button class="demo-button">Button</button>
</ui-tooltip>
<ui-tooltip text="Tooltip on text">
<span class="demo-text">Hover me</span>
</ui-tooltip>
<ui-tooltip text="Tooltip on icon">
<div class="demo-icon">?</div>
</ui-tooltip>
</div>
</section>
<!-- States -->
<section class="demo-section">
<h3>States</h3>
<div class="demo-row">
<ui-tooltip text="Normal tooltip">
<button class="demo-button">Normal</button>
</ui-tooltip>
<ui-tooltip text="This tooltip is disabled" [disabled]="true">
<button class="demo-button">Disabled</button>
</ui-tooltip>
</div>
</section>
<!-- Custom Delay -->
<section class="demo-section">
<h3>Custom Delay</h3>
<div class="demo-row">
<ui-tooltip text="Fast tooltip (100ms)" [delay]="100">
<button class="demo-button">Fast</button>
</ui-tooltip>
<ui-tooltip text="Normal tooltip (500ms)" [delay]="500">
<button class="demo-button">Normal</button>
</ui-tooltip>
<ui-tooltip text="Slow tooltip (1000ms)" [delay]="1000">
<button class="demo-button">Slow</button>
</ui-tooltip>
</div>
</section>
<!-- Interactive Example -->
<section class="demo-section">
<h3>Interactive</h3>
<div class="demo-row">
<ui-tooltip
text="Tooltip shown {{ showCount }} times"
(tooltipShow)="handleTooltipShow()"
(tooltipHide)="handleTooltipHide()">
<button class="demo-button">Track Events</button>
</ui-tooltip>
<p>Show count: {{ showCount }} | Hide count: {{ hideCount }}</p>
</div>
</section>
</div>
`,
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);
}
}

View File

@@ -8,10 +8,11 @@ import {
faTable, faImage, faImages, faPlay, faBars, faEdit, faEye, faCompass, faTable, faImage, faImages, faPlay, faBars, faEdit, faEye, faCompass,
faVideo, faComment, faMousePointer, faLayerGroup, faSquare, faCalendarDays, faClock, faVideo, faComment, faMousePointer, faLayerGroup, faSquare, faCalendarDays, faClock,
faGripVertical, faArrowsAlt, faBoxOpen, faChevronLeft, faSpinner, faExclamationTriangle, 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'; } from '@fortawesome/free-solid-svg-icons';
import { DemoRoutes } from '../../demos'; 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"; // import { DemoRoutes } from "../../../../../ui-essentials/src/public-api";
@Component({ @Component({
@@ -89,6 +90,10 @@ export class DashboardComponent {
faListAlt = faListAlt; faListAlt = faListAlt;
faCircle = faCircle; faCircle = faCircle;
faExpandArrowsAlt = faExpandArrowsAlt; faExpandArrowsAlt = faExpandArrowsAlt;
faCircleNotch = faCircleNotch;
faSliders = faSliders;
faMinus = faMinus;
faInfoCircle = faInfoCircle;
menuItems: any = [] menuItems: any = []
@@ -133,7 +138,8 @@ export class DashboardComponent {
this.createChildItem("date-picker", "Date Picker", this.faCalendarDays), this.createChildItem("date-picker", "Date Picker", this.faCalendarDays),
this.createChildItem("time-picker", "Time Picker", this.faClock), this.createChildItem("time-picker", "Time Picker", this.faClock),
this.createChildItem("file-upload", "File Upload", this.faCloudUploadAlt), 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); this.addMenuItem("forms", "Forms", this.faEdit, formsChildren);
@@ -144,7 +150,9 @@ export class DashboardComponent {
this.createChildItem("progress", "Progress Bars", this.faCheckSquare), this.createChildItem("progress", "Progress Bars", this.faCheckSquare),
this.createChildItem("badge", "Badges", this.faCertificate), this.createChildItem("badge", "Badges", this.faCertificate),
this.createChildItem("avatar", "Avatars", this.faUserCircle), 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); this.addMenuItem("data-display", "Data Display", this.faEye, dataDisplayChildren);
@@ -175,7 +183,8 @@ export class DashboardComponent {
this.createChildItem("chips", "Chips", this.faTags), this.createChildItem("chips", "Chips", this.faTags),
this.createChildItem("loading-spinner", "Loading Spinner", this.faSpinner), this.createChildItem("loading-spinner", "Loading Spinner", this.faSpinner),
this.createChildItem("skeleton-loader", "Skeleton Loader", 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); this.addMenuItem("feedback", "Feedback", this.faComment, feedbackChildren);

View File

@@ -1,7 +1,7 @@
import { Component, EventEmitter, Output, Input, OnInit } from '@angular/core'; import { Component, EventEmitter, Output, Input, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; 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'; import { faChevronRight, faChevronDown } from '@fortawesome/free-solid-svg-icons';
export interface SidebarMenuItem { export interface SidebarMenuItem {

View File

@@ -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;
}
}
}

View File

@@ -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: `
<div
class="ui-divider"
[class.ui-divider--horizontal]="orientation === 'horizontal'"
[class.ui-divider--vertical]="orientation === 'vertical'"
[class.ui-divider--solid]="variant === 'solid'"
[class.ui-divider--dashed]="variant === 'dashed'"
[class.ui-divider--dotted]="variant === 'dotted'"
[class.ui-divider--thin]="thickness === 'thin'"
[class.ui-divider--default]="thickness === 'default'"
[class.ui-divider--thick]="thickness === 'thick'"
[attr.role]="'separator'"
[attr.aria-orientation]="orientation">
@if (hasContent) {
<span class="ui-divider__content">
<ng-content></ng-content>
</span>
}
</div>
`,
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';
}
}

View File

@@ -0,0 +1 @@
export * from './divider.component';

View File

@@ -3,10 +3,12 @@ export * from './badge';
export * from './card'; export * from './card';
export * from './carousel'; export * from './carousel';
export * from './chip'; export * from './chip';
export * from './divider';
export * from './image-container'; export * from './image-container';
export * from './list'; export * from './list';
export * from './progress'; export * from './progress';
export * from './table'; export * from './table';
export * from './tooltip';
// Selectively export from feedback to avoid ProgressBarComponent conflict // Selectively export from feedback to avoid ProgressBarComponent conflict
export { StatusBadgeComponent } from '../feedback'; export { StatusBadgeComponent } from '../feedback';
export type { StatusBadgeVariant, StatusBadgeSize, StatusBadgeShape } from '../feedback'; export type { StatusBadgeVariant, StatusBadgeSize, StatusBadgeShape } from '../feedback';

View File

@@ -0,0 +1 @@
export * from './tooltip.component';

View File

@@ -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);
}
}

View File

@@ -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: `
<div
class="ui-tooltip-wrapper"
(mouseenter)="handleMouseEnter()"
(mouseleave)="handleMouseLeave()"
(click)="handleClick($event)"
(focus)="handleFocus()"
(blur)="handleBlur()">
<!-- Trigger content -->
<div class="ui-tooltip-trigger">
<ng-content></ng-content>
</div>
<!-- Tooltip content -->
@if (isVisible()) {
<div
#tooltipElement
class="ui-tooltip"
[class.ui-tooltip--top]="position === 'top'"
[class.ui-tooltip--bottom]="position === 'bottom'"
[class.ui-tooltip--left]="position === 'left'"
[class.ui-tooltip--right]="position === 'right'"
[class.ui-tooltip--sm]="size === 'sm'"
[class.ui-tooltip--md]="size === 'md'"
[class.ui-tooltip--lg]="size === 'lg'"
[attr.role]="'tooltip'"
[attr.aria-hidden]="!isVisible()">
<div class="ui-tooltip__content">
{{ text }}
</div>
<div class="ui-tooltip__arrow"></div>
</div>
}
</div>
`,
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<void>();
@Output() tooltipHide = new EventEmitter<void>();
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;
}
}
}

View File

@@ -2,4 +2,5 @@ export * from "./status-badge.component";
export * from "./skeleton-loader"; export * from "./skeleton-loader";
export * from "./empty-state"; export * from "./empty-state";
export * from "./loading-spinner"; export * from "./loading-spinner";
export * from "./progress-circle";
// export * from "./theme-switcher.component"; // Temporarily disabled due to CSS variable issues // export * from "./theme-switcher.component"; // Temporarily disabled due to CSS variable issues

View File

@@ -0,0 +1 @@
export * from './progress-circle.component';

View File

@@ -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;
}
}
}

View File

@@ -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: `
<div
class="ui-progress-circle"
[class]="containerClasses()"
[attr.aria-label]="ariaLabel || 'Progress indicator'"
[attr.aria-valuenow]="indeterminate ? null : value"
[attr.aria-valuemin]="indeterminate ? null : 0"
[attr.aria-valuemax]="indeterminate ? null : max"
[attr.aria-valuetext]="ariaValueText"
[attr.role]="'progressbar'">
<svg
class="ui-progress-circle__svg"
[attr.viewBox]="viewBox"
[attr.width]="svgSize()"
[attr.height]="svgSize()">
<!-- Background track -->
<circle
class="ui-progress-circle__track"
[attr.cx]="center"
[attr.cy]="center"
[attr.r]="radius"
[attr.stroke-width]="strokeWidth" />
<!-- Progress circle -->
@if (!indeterminate) {
<circle
class="ui-progress-circle__progress"
[attr.cx]="center"
[attr.cy]="center"
[attr.r]="radius"
[attr.stroke-width]="strokeWidth"
[attr.stroke-dasharray]="circumference"
[attr.stroke-dashoffset]="progressOffset()" />
} @else {
<circle
class="ui-progress-circle__progress"
[attr.cx]="center"
[attr.cy]="center"
[attr.r]="radius"
[attr.stroke-width]="strokeWidth" />
}
</svg>
<!-- Label content -->
@if (showLabel) {
<div class="ui-progress-circle__label" [class]="labelClasses()">
@if (labelContent) {
{{ labelContent }}
} @else {
<ng-content></ng-content>
}
</div>
}
</div>
`,
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;
}
}

View File

@@ -9,3 +9,4 @@ export * from './date-picker';
export * from './time-picker'; export * from './time-picker';
export * from './file-upload'; export * from './file-upload';
export * from './form-field'; export * from './form-field';
export * from './range-slider';

View File

@@ -0,0 +1 @@
export * from './range-slider.component';

View File

@@ -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;
}
}
}
}

View File

@@ -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: `
<div
class="ui-range-slider"
[class]="containerClasses()"
[attr.aria-label]="ariaLabel || label || 'Range slider'"
[attr.aria-disabled]="disabled">
@if (label || showValue) {
<div class="ui-range-slider__header">
@if (label) {
<label
class="ui-range-slider__label"
[class.ui-range-slider__label--disabled]="disabled"
[for]="sliderId">
{{ label }}
@if (required) {
<span class="ui-range-slider__required">*</span>
}
</label>
}
@if (showValue) {
<span class="ui-range-slider__value-display">
{{ formatValue(currentValue()) }}{{ valueUnit }}
</span>
}
</div>
}
<div class="ui-range-slider__container" #container>
<!-- Track background -->
<div class="ui-range-slider__track">
<!-- Fill/Progress -->
<div
class="ui-range-slider__fill"
[style.width]="fillWidth() + '%'"
[style.left]="'0%'">
</div>
</div>
<!-- Hidden native input for accessibility and form integration -->
<input
#sliderInput
type="range"
class="ui-range-slider__input"
[id]="sliderId"
[min]="min"
[max]="max"
[step]="step"
[value]="currentValue()"
[disabled]="disabled"
[required]="required"
[attr.aria-label]="ariaLabel || label || 'Range slider'"
[attr.aria-valuetext]="ariaValueText"
[attr.aria-orientation]="orientation"
(input)="onInputChange($event)"
(focus)="onFocus($event)"
(blur)="onBlur($event)"
(keydown)="onKeyDown($event)" />
<!-- Visual thumb -->
<div
class="ui-range-slider__thumb"
[class.ui-range-slider__thumb--focused]="isFocused()"
[class.ui-range-slider__thumb--dragging]="isDragging()"
[style.left]="thumbPosition() + '%'"
(mousedown)="onThumbMouseDown($event)"
(touchstart)="onThumbTouchStart($event)">
</div>
</div>
@if (ticks && ticks.length > 0) {
<!-- Tick marks -->
<div class="ui-range-slider__ticks">
@for (tick of ticks; track tick.value) {
<div
class="ui-range-slider__tick"
[class.ui-range-slider__tick--major]="tick.major"
[style.left]="getTickPosition(tick.value) + '%'">
</div>
}
</div>
<!-- Tick labels -->
@if (showTickLabels) {
<div class="ui-range-slider__tick-labels">
@for (tick of ticks; track tick.value) {
@if (tick.label) {
<div
class="ui-range-slider__tick-label"
[style.left]="getTickPosition(tick.value) + '%'">
{{ tick.label }}
</div>
}
}
</div>
}
}
@if (helperText && !disabled) {
<div
class="ui-range-slider__helper-text"
[class.ui-range-slider__helper-text--error]="hasError"
[id]="sliderId + '-helper'">
{{ helperText }}
</div>
}
</div>
`,
styleUrl: './range-slider.component.scss'
})
export class RangeSliderComponent implements ControlValueAccessor {
@ViewChild('sliderInput', { static: true }) sliderInput!: ElementRef<HTMLInputElement>;
@ViewChild('container', { static: true }) container!: ElementRef<HTMLDivElement>;
// 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<number>();
@Output() sliderFocus = new EventEmitter<FocusEvent>();
@Output() sliderBlur = new EventEmitter<FocusEvent>();
@Output() slideStart = new EventEmitter<number>();
@Output() slideEnd = new EventEmitter<number>();
// 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();
}
}
}

View File

@@ -6,9 +6,10 @@ import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
export interface TabItem { export interface TabItem {
id: string; id: string;
label: string; label: string;
icon?: IconDefinition; icon?: string | IconDefinition;
disabled?: boolean; disabled?: boolean;
badge?: string | number; badge?: string | number;
closeable?: boolean;
} }
export type TabVariant = 'line' | 'pills' | 'cards'; export type TabVariant = 'line' | 'pills' | 'cards';
@@ -34,7 +35,11 @@ export type TabSize = 'small' | 'medium' | 'large';
(click)="selectTab(tab.id)" (click)="selectTab(tab.id)"
> >
@if (tab.icon) { @if (tab.icon) {
<fa-icon class="tab-icon" [icon]="tab.icon" /> @if (isIconDefinition(tab.icon)) {
<fa-icon class="tab-icon" [icon]="tab.icon" />
} @else {
<span class="tab-icon" [innerHTML]="tab.icon"></span>
}
} }
<span class="tab-label">{{ tab.label }}</span> <span class="tab-label">{{ tab.label }}</span>
@if (tab.badge) { @if (tab.badge) {
@@ -95,4 +100,8 @@ export class TabGroupComponent {
this.tabChange.emit(tabId); this.tabChange.emit(tabId);
} }
} }
isIconDefinition(icon: string | IconDefinition): icon is IconDefinition {
return typeof icon === 'object' && icon !== null && 'iconName' in icon;
}
} }

View File

@@ -18,11 +18,12 @@ import {
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
export interface TabItem { export interface TabItem {
id: string; id: string;
label: string; label: string;
icon?: string; icon?: string | IconDefinition;
disabled?: boolean; disabled?: boolean;
closeable?: boolean; closeable?: boolean;
badge?: string | number; badge?: string | number;

View File

@@ -10,3 +10,23 @@ export * from './lib/components/media/index';
export * from './lib/components/feedback/index'; export * from './lib/components/feedback/index';
export * from './lib/components/overlays/index'; export * from './lib/components/overlays/index';
export * from './lib/components/layout/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';