diff --git a/projects/demo-ui-essentials/src/app/demos/color-picker-demo/color-picker-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/color-picker-demo/color-picker-demo.component.scss
new file mode 100644
index 0000000..643a460
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/color-picker-demo/color-picker-demo.component.scss
@@ -0,0 +1,339 @@
+@use '../../../../../shared-ui/src/styles/semantic/index' as *;
+
+.demo-container {
+ padding: $semantic-spacing-layout-section-md;
+ max-width: 1200px;
+ margin: 0 auto;
+
+ h2 {
+ font-family: map-get($semantic-typography-heading-h2, font-family);
+ font-size: map-get($semantic-typography-heading-h2, font-size);
+ font-weight: map-get($semantic-typography-heading-h2, font-weight);
+ line-height: map-get($semantic-typography-heading-h2, line-height);
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-layout-section-lg;
+ text-align: center;
+ }
+}
+
+.demo-section {
+ margin-bottom: $semantic-spacing-layout-section-xl;
+
+ h3 {
+ font-family: map-get($semantic-typography-heading-h3, font-family);
+ font-size: map-get($semantic-typography-heading-h3, font-size);
+ font-weight: map-get($semantic-typography-heading-h3, font-weight);
+ line-height: map-get($semantic-typography-heading-h3, line-height);
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-layout-section-md;
+ padding-bottom: $semantic-spacing-component-sm;
+ border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ }
+
+ h4 {
+ font-family: map-get($semantic-typography-heading-h4, font-family);
+ font-size: map-get($semantic-typography-heading-h4, font-size);
+ font-weight: map-get($semantic-typography-heading-h4, font-weight);
+ line-height: map-get($semantic-typography-heading-h4, line-height);
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-component-md;
+ }
+}
+
+.demo-row {
+ display: flex;
+ gap: $semantic-spacing-layout-section-md;
+ align-items: flex-start;
+ flex-wrap: wrap;
+
+ > * {
+ flex: 1;
+ min-width: 300px;
+ }
+
+ @media (max-width: calc(#{$semantic-breakpoint-md} - 1px)) {
+ flex-direction: column;
+ gap: $semantic-spacing-layout-section-sm;
+
+ > * {
+ min-width: unset;
+ }
+ }
+}
+
+.demo-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: $semantic-spacing-layout-section-md;
+
+ @media (max-width: calc(#{$semantic-breakpoint-sm} - 1px)) {
+ grid-template-columns: 1fr;
+ gap: $semantic-spacing-layout-section-sm;
+ }
+}
+
+.demo-item {
+ padding: $semantic-spacing-component-lg;
+ background: $semantic-color-surface-secondary;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ border-radius: $semantic-border-radius-md;
+ box-shadow: $semantic-shadow-elevation-1;
+}
+
+.demo-info {
+ padding: $semantic-spacing-component-lg;
+ 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-1;
+
+ p {
+ font-family: map-get($semantic-typography-body-medium, font-family);
+ font-size: map-get($semantic-typography-body-medium, font-size);
+ font-weight: map-get($semantic-typography-body-medium, font-weight);
+ line-height: map-get($semantic-typography-body-medium, line-height);
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+
+ strong {
+ font-weight: $semantic-typography-font-weight-semibold;
+ color: $semantic-color-text-primary;
+ }
+}
+
+.color-preview {
+ width: 100px;
+ height: 60px;
+ border: $semantic-border-width-2 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-sm;
+ box-shadow: $semantic-shadow-elevation-1;
+ margin-top: $semantic-spacing-component-sm;
+}
+
+.demo-form {
+ .demo-form-info {
+ margin-top: $semantic-spacing-component-lg;
+ padding: $semantic-spacing-component-md;
+ background: $semantic-color-surface-container;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ border-radius: $semantic-border-radius-sm;
+
+ pre {
+ font-family: $semantic-typography-font-family-mono;
+ font-size: $semantic-typography-font-size-sm;
+ color: $semantic-color-text-secondary;
+ background: $semantic-color-surface-primary;
+ padding: $semantic-spacing-component-sm;
+ border-radius: $semantic-border-radius-sm;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ overflow-x: auto;
+ white-space: pre-wrap;
+ margin: 0;
+ }
+ }
+
+ .demo-theme-preview {
+ margin-top: $semantic-spacing-component-lg;
+
+ .theme-sample {
+ padding: $semantic-spacing-component-lg;
+ border-radius: $semantic-border-radius-md;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ text-align: center;
+ font-family: map-get($semantic-typography-body-medium, font-family);
+ font-size: map-get($semantic-typography-body-medium, font-size);
+ font-weight: map-get($semantic-typography-body-medium, font-weight);
+ line-height: map-get($semantic-typography-body-medium, line-height);
+ box-shadow: $semantic-shadow-elevation-1;
+ }
+ }
+}
+
+.event-log {
+ max-height: 300px;
+ overflow-y: auto;
+ background: $semantic-color-surface-primary;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ border-radius: $semantic-border-radius-sm;
+ padding: $semantic-spacing-component-sm;
+ margin-bottom: $semantic-spacing-component-md;
+
+ .event-entry {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ border-radius: $semantic-border-radius-sm;
+ margin-bottom: $semantic-spacing-component-xs;
+ font-family: $semantic-typography-font-family-mono;
+ font-size: $semantic-typography-font-size-sm;
+ transition: background-color $semantic-motion-duration-fast $semantic-motion-easing-ease;
+
+ &--recent {
+ background-color: $semantic-color-surface-elevated;
+ border: $semantic-border-width-1 solid $semantic-color-border-focus;
+ }
+
+ .event-type {
+ font-weight: $semantic-typography-font-weight-semibold;
+ color: $semantic-color-primary;
+ min-width: 80px;
+ }
+
+ .event-value {
+ flex: 1;
+ color: $semantic-color-text-primary;
+ margin: 0 $semantic-spacing-component-sm;
+ text-align: center;
+ }
+
+ .event-time {
+ color: $semantic-color-text-tertiary;
+ font-size: $semantic-typography-font-size-xs;
+ min-width: 80px;
+ text-align: right;
+ }
+ }
+}
+
+.clear-log-btn {
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ background: $semantic-color-danger;
+ color: $semantic-color-on-danger;
+ border: none;
+ border-radius: $semantic-border-button-radius;
+ font-family: map-get($semantic-typography-button-small, font-family);
+ font-size: map-get($semantic-typography-button-small, font-size);
+ font-weight: map-get($semantic-typography-button-small, font-weight);
+ line-height: map-get($semantic-typography-button-small, line-height);
+ cursor: pointer;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
+
+ &:hover {
+ box-shadow: $semantic-shadow-button-hover;
+ transform: translateY(-1px);
+ }
+
+ &:active {
+ transform: translateY(0);
+ box-shadow: $semantic-shadow-button-rest;
+ }
+}
+
+.preset-colors {
+ padding: $semantic-spacing-component-lg;
+ 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-1;
+
+ .preset-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(40px, 1fr));
+ gap: $semantic-spacing-component-sm;
+ max-width: 400px;
+ margin-top: $semantic-spacing-component-md;
+ }
+
+ .preset-color-btn {
+ width: 40px;
+ height: 40px;
+ border: $semantic-border-width-2 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-sm;
+ cursor: pointer;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
+ box-shadow: $semantic-shadow-elevation-1;
+
+ &:hover {
+ transform: scale(1.1);
+ box-shadow: $semantic-shadow-elevation-3;
+ border-color: $semantic-color-border-focus;
+ }
+
+ &:focus {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+
+ &:active {
+ transform: scale(1.05);
+ box-shadow: $semantic-shadow-elevation-2;
+ }
+
+ .sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+ }
+ }
+}
+
+// Responsive adjustments
+@media (max-width: calc(#{$semantic-breakpoint-md} - 1px)) {
+ .demo-container {
+ padding: $semantic-spacing-layout-section-sm;
+ }
+
+ .demo-item {
+ padding: $semantic-spacing-component-md;
+ }
+
+ .demo-info {
+ padding: $semantic-spacing-component-md;
+ }
+
+ .preset-colors {
+ padding: $semantic-spacing-component-md;
+ }
+}
+
+@media (max-width: calc(#{$semantic-breakpoint-sm} - 1px)) {
+ .demo-container {
+ padding: $semantic-spacing-layout-section-xs;
+ }
+
+ .demo-item {
+ padding: $semantic-spacing-component-sm;
+ }
+
+ .demo-info {
+ padding: $semantic-spacing-component-sm;
+ }
+
+ .preset-colors {
+ padding: $semantic-spacing-component-sm;
+
+ .preset-grid {
+ grid-template-columns: repeat(auto-fit, minmax(35px, 1fr));
+ gap: $semantic-spacing-component-xs;
+ }
+
+ .preset-color-btn {
+ width: 35px;
+ height: 35px;
+ }
+ }
+
+ .event-log {
+ .event-entry {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: $semantic-spacing-component-xs;
+
+ .event-type,
+ .event-value,
+ .event-time {
+ min-width: unset;
+ margin: 0;
+ text-align: left;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/color-picker-demo/color-picker-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/color-picker-demo/color-picker-demo.component.ts
new file mode 100644
index 0000000..cc64a7a
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/color-picker-demo/color-picker-demo.component.ts
@@ -0,0 +1,348 @@
+import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule, ReactiveFormsModule, FormControl, FormGroup } from '@angular/forms';
+import { ColorPickerComponent } from '../../../../../ui-essentials/src/lib/components/forms/color-picker/color-picker.component';
+
+@Component({
+ selector: 'ui-color-picker-demo',
+ standalone: true,
+ imports: [
+ CommonModule,
+ FormsModule,
+ ReactiveFormsModule,
+ ColorPickerComponent
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
Color Picker Component Showcase
+
+
+
+ Basic Color Picker
+
+
+
+
Selected: {{ basicColor() }}
+
+
+
+
+
+
+
+
+
+
+ Color Variants
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reactive Forms Integration
+
+
+
+
+
+ Interactive Demo
+
+
+
+
Event Log:
+
+ @for (event of eventLog(); track event.id) {
+
+ {{ event.type }}
+ {{ event.value }}
+ {{ event.time }}
+
+ }
+
+
+
+
+
+
+
+
+ Preset Colors
+
+
+
+
Quick Colors:
+
+ @for (color of presetColors; track color.name) {
+
+ }
+
+
+
+
+
+ `,
+ styleUrl: './color-picker-demo.component.scss'
+})
+export class ColorPickerDemoComponent {
+ // Basic demo colors
+ basicColor = signal('#3498db');
+
+ // Size variants
+ sizeSmall = signal('#e74c3c');
+ sizeMedium = signal('#2ecc71');
+ sizeLarge = signal('#f39c12');
+
+ // Color variants
+ variantPrimary = signal('#3498db');
+ variantSecondary = signal('#95a5a6');
+ variantSuccess = signal('#2ecc71');
+ variantDanger = signal('#e74c3c');
+
+ // States
+ stateDefault = signal('#9b59b6');
+ stateError = signal('#e74c3c');
+ stateDisabled = signal('#bdc3c7');
+
+ // Required field
+ requiredColor = signal('#1abc9c');
+
+ // Interactive demo
+ interactiveColor = signal('#34495e');
+ eventLog = signal>([]);
+ private eventIdCounter = 0;
+
+ // Preset colors
+ presetColor = signal('#ff6b6b');
+ presetColors = [
+ { name: 'Red', hex: '#e74c3c' },
+ { name: 'Orange', hex: '#f39c12' },
+ { name: 'Yellow', hex: '#f1c40f' },
+ { name: 'Green', hex: '#2ecc71' },
+ { name: 'Blue', hex: '#3498db' },
+ { name: 'Purple', hex: '#9b59b6' },
+ { name: 'Pink', hex: '#e91e63' },
+ { name: 'Teal', hex: '#1abc9c' },
+ { name: 'Gray', hex: '#95a5a6' },
+ { name: 'Black', hex: '#2c3e50' },
+ { name: 'White', hex: '#ecf0f1' },
+ { name: 'Brown', hex: '#8d4925' }
+ ];
+
+ // Reactive forms
+ colorForm = new FormGroup({
+ backgroundColor: new FormControl('#ffffff'),
+ textColor: new FormControl('#333333')
+ });
+
+ onColorChange(source: string, color: string): void {
+ console.log(`Color changed (${source}):`, color);
+ }
+
+ onInteractiveColorChange(color: string): void {
+ this.addEventToLog('change', color);
+ }
+
+ onColorFocus(event: FocusEvent): void {
+ this.addEventToLog('focus', 'Color picker focused');
+ }
+
+ onColorBlur(event: FocusEvent): void {
+ this.addEventToLog('blur', 'Color picker blurred');
+ }
+
+ private addEventToLog(type: string, value: string): void {
+ const newEvent = {
+ id: this.eventIdCounter++,
+ type,
+ value,
+ time: new Date().toLocaleTimeString(),
+ recent: true
+ };
+
+ const currentLog = this.eventLog();
+ const updatedLog = [newEvent, ...currentLog.slice(0, 9)].map((event, index) => ({
+ ...event,
+ recent: index === 0
+ }));
+
+ this.eventLog.set(updatedLog);
+
+ // Remove recent flag after animation
+ setTimeout(() => {
+ const logWithoutRecent = this.eventLog().map(event => ({
+ ...event,
+ recent: false
+ }));
+ this.eventLog.set(logWithoutRecent);
+ }, 1000);
+ }
+
+ clearEventLog(): void {
+ this.eventLog.set([]);
+ this.eventIdCounter = 0;
+ }
+
+ selectPresetColor(color: string): void {
+ this.presetColor.set(color);
+ }
+
+ getFormValues(): string {
+ return JSON.stringify(this.colorForm.value, null, 2);
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/demos.routes.ts b/projects/demo-ui-essentials/src/app/demos/demos.routes.ts
index 0261efa..4e910ae 100644
--- a/projects/demo-ui-essentials/src/app/demos/demos.routes.ts
+++ b/projects/demo-ui-essentials/src/app/demos/demos.routes.ts
@@ -42,6 +42,7 @@ import { OverlayContainerDemoComponent } from './overlay-container-demo/overlay-
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 { ColorPickerDemoComponent } from './color-picker-demo/color-picker-demo.component';
import { DividerDemoComponent } from './divider-demo/divider-demo.component';
import { TooltipDemoComponent } from './tooltip-demo/tooltip-demo.component';
import { AccordionDemoComponent } from './accordion-demo/accordion-demo.component';
@@ -55,6 +56,9 @@ import { StepperDemoComponent } from './stepper-demo/stepper-demo.component';
import { FabMenuDemoComponent } from './fab-menu-demo/fab-menu-demo.component';
import { EnhancedTableDemoComponent } from './enhanced-table-demo/enhanced-table-demo.component';
import { SplitButtonDemoComponent } from './split-button-demo/split-button-demo.component';
+import { CommandPaletteDemoComponent } from './command-palette-demo/command-palette-demo.component';
+import { TransferListDemoComponent } from './transfer-list-demo/transfer-list-demo.component';
+import { FloatingToolbarDemoComponent } from './floating-toolbar-demo/floating-toolbar-demo.component';
@Component({
@@ -221,6 +225,10 @@ import { SplitButtonDemoComponent } from './split-button-demo/split-button-demo.
}
+ @case ("color-picker") {
+
+ }
+
@case ("divider") {
}
@@ -273,6 +281,18 @@ import { SplitButtonDemoComponent } from './split-button-demo/split-button-demo.
}
+ @case ("command-palette") {
+
+ }
+
+ @case ("floating-toolbar") {
+
+ }
+
+ @case ("transfer-list") {
+
+ }
+
}
`,
@@ -287,8 +307,8 @@ import { SplitButtonDemoComponent } from './split-button-demo/split-button-demo.
GridSystemDemoComponent, SpacerDemoComponent, ContainerDemoComponent, PaginationDemoComponent,
SkeletonLoaderDemoComponent, EmptyStateDemoComponent, FileUploadDemoComponent, FormFieldDemoComponent,
AutocompleteDemoComponent, BackdropDemoComponent, OverlayContainerDemoComponent, LoadingSpinnerDemoComponent,
- ProgressCircleDemoComponent, RangeSliderDemoComponent, DividerDemoComponent, TooltipDemoComponent, AccordionDemoComponent,
- PopoverDemoComponent, AlertDemoComponent, SnackbarDemoComponent, ToastDemoComponent, TreeViewDemoComponent, TimelineDemoComponent, StepperDemoComponent, FabMenuDemoComponent, EnhancedTableDemoComponent, SplitButtonDemoComponent]
+ ProgressCircleDemoComponent, RangeSliderDemoComponent, ColorPickerDemoComponent, DividerDemoComponent, TooltipDemoComponent, AccordionDemoComponent,
+ PopoverDemoComponent, AlertDemoComponent, SnackbarDemoComponent, ToastDemoComponent, TreeViewDemoComponent, TimelineDemoComponent, StepperDemoComponent, FabMenuDemoComponent, EnhancedTableDemoComponent, SplitButtonDemoComponent, CommandPaletteDemoComponent, FloatingToolbarDemoComponent, TransferListDemoComponent]
})
diff --git a/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.component.ts b/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.component.ts
index 1f0c112..0a48321 100644
--- a/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.component.ts
+++ b/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.component.ts
@@ -10,7 +10,7 @@ import {
faGripVertical, faArrowsAlt, faBoxOpen, faChevronLeft, faSpinner, faExclamationTriangle,
faCloudUploadAlt, faFileText, faListAlt, faCircle, faExpandArrowsAlt, faCircleNotch, faSliders,
faMinus, faInfoCircle, faChevronDown, faCaretUp, faExclamationCircle, faSitemap, faStream,
- faBell, faRoute, faChevronUp, faEllipsisV, faCut
+ faBell, faRoute, faChevronUp, faEllipsisV, faCut, faPalette, faExchangeAlt, faTools
} from '@fortawesome/free-solid-svg-icons';
import { DemoRoutes } from '../../demos';
import { ScrollContainerComponent } from '../../../../../ui-essentials/src/lib/layouts/scroll-container.component';
@@ -104,6 +104,9 @@ export class DashboardComponent {
faChevronUp = faChevronUp;
faEllipsisV = faEllipsisV;
faCut = faCut;
+ faPalette = faPalette;
+ faExchangeAlt = faExchangeAlt;
+ faTools = faTools;
menuItems: any = []
@@ -149,7 +152,8 @@ export class DashboardComponent {
this.createChildItem("time-picker", "Time Picker", this.faClock),
this.createChildItem("file-upload", "File Upload", this.faCloudUploadAlt),
this.createChildItem("form-field", "Form Field", this.faFileText),
- this.createChildItem("range-slider", "Range Slider", this.faSliders)
+ this.createChildItem("range-slider", "Range Slider", this.faSliders),
+ this.createChildItem("color-picker", "Color Picker", this.faPalette)
];
this.addMenuItem("forms", "Forms", this.faEdit, formsChildren);
@@ -166,7 +170,8 @@ export class DashboardComponent {
this.createChildItem("divider", "Divider", this.faMinus),
this.createChildItem("timeline", "Timeline", this.faStream),
this.createChildItem("tooltip", "Tooltip", this.faInfoCircle),
- this.createChildItem("tree-view", "Tree View", this.faSitemap)
+ this.createChildItem("tree-view", "Tree View", this.faSitemap),
+ this.createChildItem("transfer-list", "Transfer List", this.faExchangeAlt)
];
this.addMenuItem("data-display", "Data Display", this.faEye, dataDisplayChildren);
@@ -224,7 +229,9 @@ export class DashboardComponent {
this.createChildItem("drawer", "Drawer/Sidebar", this.faBars),
this.createChildItem("popover", "Popover/Dropdown", this.faCaretUp),
this.createChildItem("backdrop", "Backdrop", this.faCircle),
- this.createChildItem("overlay-container", "Overlay Container", this.faExpandArrowsAlt)
+ this.createChildItem("overlay-container", "Overlay Container", this.faExpandArrowsAlt),
+ this.createChildItem("command-palette", "Command Palette", this.faPalette),
+ this.createChildItem("floating-toolbar", "Floating Toolbar", this.faTools)
];
this.addMenuItem("overlays", "Overlays", this.faLayerGroup, overlaysChildren);
diff --git a/projects/ui-essentials/src/lib/components/forms/color-picker/color-picker.component.scss b/projects/ui-essentials/src/lib/components/forms/color-picker/color-picker.component.scss
new file mode 100644
index 0000000..8ba3460
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/color-picker/color-picker.component.scss
@@ -0,0 +1,362 @@
+@use "../../../../../../shared-ui/src/styles/semantic" as *;
+
+.ui-color-picker {
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ font-family: map-get($semantic-typography-body-medium, font-family);
+ font-size: map-get($semantic-typography-body-medium, font-size);
+ font-weight: map-get($semantic-typography-body-medium, font-weight);
+ line-height: map-get($semantic-typography-body-medium, line-height);
+ color: $semantic-color-text-primary;
+
+ &__label {
+ display: block;
+ margin-bottom: $semantic-spacing-form-field-gap;
+ font-family: map-get($semantic-typography-label, font-family);
+ font-size: map-get($semantic-typography-label, font-size);
+ font-weight: map-get($semantic-typography-label, font-weight);
+ line-height: map-get($semantic-typography-label, line-height);
+ color: $semantic-color-text-primary;
+ }
+
+ &__required {
+ color: $semantic-color-danger;
+ margin-left: $semantic-spacing-component-xs;
+ }
+
+ &__container {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-component-md;
+ padding: $semantic-spacing-component-md;
+ background: $semantic-color-surface-primary;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-md;
+ box-shadow: $semantic-shadow-elevation-1;
+ }
+
+ &__wheel-container {
+ position: relative;
+ align-self: center;
+ }
+
+ &__wheel {
+ display: block;
+ border-radius: $semantic-border-radius-full;
+ cursor: crosshair;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ box-shadow: $semantic-shadow-elevation-1;
+ transition: box-shadow $semantic-motion-duration-fast $semantic-motion-easing-ease;
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ box-shadow: $semantic-shadow-elevation-2;
+ }
+ }
+
+ &__wheel-indicator {
+ position: absolute;
+ width: 12px;
+ height: 12px;
+ border: 2px solid $semantic-color-surface-primary;
+ border-radius: $semantic-border-radius-full;
+ background: transparent;
+ pointer-events: none;
+ box-shadow: $semantic-shadow-elevation-2;
+ z-index: 1;
+ }
+
+ &__hue-container {
+ position: relative;
+ align-self: center;
+ }
+
+ &__hue-bar {
+ display: block;
+ border-radius: $semantic-border-radius-sm;
+ cursor: pointer;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ box-shadow: $semantic-shadow-elevation-1;
+ transition: box-shadow $semantic-motion-duration-fast $semantic-motion-easing-ease;
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ box-shadow: $semantic-shadow-elevation-2;
+ }
+ }
+
+ &__hue-indicator {
+ position: absolute;
+ top: -2px;
+ width: 12px;
+ height: calc(100% + 4px);
+ border: 2px solid $semantic-color-surface-primary;
+ border-radius: $semantic-border-radius-sm;
+ background: transparent;
+ pointer-events: none;
+ box-shadow: $semantic-shadow-elevation-2;
+ z-index: 1;
+ }
+
+ &__preview-container {
+ align-self: center;
+ }
+
+ &__preview {
+ width: 48px;
+ height: 32px;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-sm;
+ box-shadow: $semantic-shadow-elevation-1;
+ cursor: pointer;
+ }
+
+ &__hex-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: $semantic-spacing-component-xs;
+ }
+
+ &__hex-label {
+ font-family: map-get($semantic-typography-caption, font-family);
+ font-size: map-get($semantic-typography-caption, font-size);
+ font-weight: map-get($semantic-typography-caption, font-weight);
+ line-height: map-get($semantic-typography-caption, line-height);
+ color: $semantic-color-text-secondary;
+ text-transform: uppercase;
+ }
+
+ &__hex-input {
+ width: 80px;
+ padding: $semantic-spacing-interactive-input-padding-y $semantic-spacing-interactive-input-padding-x;
+ background: $semantic-color-surface-secondary;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-input-radius;
+ font-family: map-get($semantic-typography-input, font-family);
+ font-size: map-get($semantic-typography-input, font-size);
+ font-weight: map-get($semantic-typography-input, font-weight);
+ line-height: map-get($semantic-typography-input, line-height);
+ color: $semantic-color-text-primary;
+ text-align: center;
+ text-transform: uppercase;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
+
+ &:focus {
+ outline: none;
+ border-color: $semantic-color-border-focus;
+ box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
+ }
+
+ &:disabled {
+ background: $semantic-color-surface-container;
+ color: $semantic-color-text-disabled;
+ cursor: not-allowed;
+ opacity: $semantic-opacity-disabled;
+ }
+ }
+
+ &__description {
+ margin-top: $semantic-spacing-component-xs;
+ font-family: map-get($semantic-typography-caption, font-family);
+ font-size: map-get($semantic-typography-caption, font-size);
+ font-weight: map-get($semantic-typography-caption, font-weight);
+ line-height: map-get($semantic-typography-caption, line-height);
+ color: $semantic-color-text-tertiary;
+ }
+
+ // Size variants
+ &--sm {
+ font-family: map-get($semantic-typography-body-small, font-family);
+ font-size: map-get($semantic-typography-body-small, font-size);
+ font-weight: map-get($semantic-typography-body-small, font-weight);
+ line-height: map-get($semantic-typography-body-small, line-height);
+
+ .ui-color-picker__container {
+ padding: $semantic-spacing-component-sm;
+ gap: $semantic-spacing-component-sm;
+ }
+
+ .ui-color-picker__preview {
+ width: 32px;
+ height: 24px;
+ }
+
+ .ui-color-picker__hex-input {
+ width: 70px;
+ padding: $semantic-spacing-component-xs;
+ }
+
+ .ui-color-picker__wheel-indicator {
+ width: 8px;
+ height: 8px;
+ }
+
+ .ui-color-picker__hue-indicator {
+ width: 8px;
+ height: calc(100% + 4px);
+ }
+ }
+
+ &--lg {
+ font-family: map-get($semantic-typography-body-large, font-family);
+ font-size: map-get($semantic-typography-body-large, font-size);
+ font-weight: map-get($semantic-typography-body-large, font-weight);
+ line-height: map-get($semantic-typography-body-large, line-height);
+
+ .ui-color-picker__container {
+ padding: $semantic-spacing-component-lg;
+ gap: $semantic-spacing-component-lg;
+ }
+
+ .ui-color-picker__preview {
+ width: 64px;
+ height: 40px;
+ }
+
+ .ui-color-picker__hex-input {
+ width: 90px;
+ padding: $semantic-spacing-component-sm;
+ }
+
+ .ui-color-picker__wheel-indicator {
+ width: 16px;
+ height: 16px;
+ }
+
+ .ui-color-picker__hue-indicator {
+ width: 16px;
+ height: calc(100% + 4px);
+ }
+ }
+
+ // Color variants
+ &--primary {
+ .ui-color-picker__container {
+ border-color: $semantic-color-primary;
+ }
+
+ .ui-color-picker__hex-input:focus {
+ border-color: $semantic-color-primary;
+ }
+ }
+
+ &--secondary {
+ .ui-color-picker__container {
+ border-color: $semantic-color-secondary;
+ }
+
+ .ui-color-picker__hex-input:focus {
+ border-color: $semantic-color-secondary;
+ }
+ }
+
+ &--success {
+ .ui-color-picker__container {
+ border-color: $semantic-color-success;
+ }
+
+ .ui-color-picker__hex-input:focus {
+ border-color: $semantic-color-success;
+ }
+ }
+
+ &--danger {
+ .ui-color-picker__container {
+ border-color: $semantic-color-danger;
+ }
+
+ .ui-color-picker__hex-input:focus {
+ border-color: $semantic-color-danger;
+ }
+ }
+
+ // State variants
+ &--error {
+ .ui-color-picker__container {
+ border-color: $semantic-color-border-error;
+ }
+
+ .ui-color-picker__label {
+ color: $semantic-color-danger;
+ }
+
+ .ui-color-picker__hex-input {
+ border-color: $semantic-color-border-error;
+ }
+ }
+
+ &--disabled {
+ opacity: $semantic-opacity-disabled;
+ pointer-events: none;
+
+ .ui-color-picker__container {
+ background: $semantic-color-surface-container;
+ border-color: $semantic-color-border-subtle;
+ }
+
+ .ui-color-picker__wheel,
+ .ui-color-picker__hue-bar {
+ cursor: not-allowed;
+ opacity: $semantic-opacity-disabled;
+ }
+
+ .ui-color-picker__label {
+ color: $semantic-color-text-disabled;
+ }
+ }
+
+ // Responsive design
+ @media (max-width: calc(#{$semantic-breakpoint-md} - 1px)) {
+ .ui-color-picker__container {
+ padding: $semantic-spacing-component-sm;
+ gap: $semantic-spacing-component-sm;
+ }
+ }
+
+ @media (max-width: calc(#{$semantic-breakpoint-sm} - 1px)) {
+ font-family: map-get($semantic-typography-body-small, font-family);
+ font-size: map-get($semantic-typography-body-small, font-size);
+ font-weight: map-get($semantic-typography-body-small, font-weight);
+ line-height: map-get($semantic-typography-body-small, line-height);
+
+ .ui-color-picker__container {
+ padding: $semantic-spacing-component-xs;
+ gap: $semantic-spacing-component-xs;
+ }
+
+ .ui-color-picker__preview {
+ width: 40px;
+ height: 28px;
+ }
+
+ .ui-color-picker__hex-input {
+ width: 75px;
+ }
+ }
+
+ // High contrast mode
+ @media (prefers-contrast: high) {
+ .ui-color-picker__wheel,
+ .ui-color-picker__hue-bar {
+ border-width: $semantic-border-width-2;
+ }
+
+ .ui-color-picker__wheel-indicator,
+ .ui-color-picker__hue-indicator {
+ border-width: 3px;
+ }
+ }
+
+ // Reduced motion
+ @media (prefers-reduced-motion: reduce) {
+ .ui-color-picker__wheel,
+ .ui-color-picker__hue-bar,
+ .ui-color-picker__hex-input {
+ transition: none;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/color-picker/color-picker.component.ts b/projects/ui-essentials/src/lib/components/forms/color-picker/color-picker.component.ts
new file mode 100644
index 0000000..d5c7ad2
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/color-picker/color-picker.component.ts
@@ -0,0 +1,613 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, forwardRef, signal, ElementRef, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule, ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+
+export type ColorPickerSize = 'sm' | 'md' | 'lg';
+export type ColorPickerVariant = 'primary' | 'secondary' | 'success' | 'danger';
+export type ColorPickerState = 'default' | 'error' | 'disabled';
+
+interface HSL {
+ h: number; // 0-360
+ s: number; // 0-100
+ l: number; // 0-100
+}
+
+interface RGB {
+ r: number; // 0-255
+ g: number; // 0-255
+ b: number; // 0-255
+}
+
+@Component({
+ selector: 'ui-color-picker',
+ standalone: true,
+ imports: [CommonModule, FormsModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => ColorPickerComponent),
+ multi: true
+ }
+ ],
+ template: `
+
+
+ @if (label) {
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @if (description) {
+
+ {{ description }}
+
+ }
+
+ `,
+ styleUrl: './color-picker.component.scss'
+})
+export class ColorPickerComponent implements ControlValueAccessor, AfterViewInit, OnDestroy {
+ @Input() label: string = '';
+ @Input() description: string = '';
+ @Input() size: ColorPickerSize = 'md';
+ @Input() variant: ColorPickerVariant = 'primary';
+ @Input() state: ColorPickerState = 'default';
+ @Input() disabled = false;
+ @Input() required = false;
+ @Input() pickerId: string = `color-picker-${Math.random().toString(36).substr(2, 9)}`;
+
+ @Output() colorChange = new EventEmitter();
+ @Output() colorFocus = new EventEmitter();
+ @Output() colorBlur = new EventEmitter();
+
+ @ViewChild('colorWheel') colorWheelCanvas!: ElementRef;
+ @ViewChild('hueBar') hueBarCanvas!: ElementRef;
+ @ViewChild('hexInput') hexInputElement!: ElementRef;
+
+ protected currentColor = signal({ h: 0, s: 100, l: 50 });
+ private isDraggingWheel = false;
+ private isDraggingHue = false;
+ private wheelCtx?: CanvasRenderingContext2D;
+ private hueCtx?: CanvasRenderingContext2D;
+
+ // ControlValueAccessor implementation
+ private onChange = (value: string) => {};
+ private onTouched = () => {};
+
+ ngAfterViewInit(): void {
+ this.initializeCanvases();
+ this.drawColorWheel();
+ this.drawHueBar();
+ this.setupEventListeners();
+ }
+
+ ngOnDestroy(): void {
+ this.removeEventListeners();
+ }
+
+ writeValue(value: string): void {
+ if (value && this.isValidHexColor(value)) {
+ const hsl = this.hexToHsl(value);
+ this.currentColor.set(hsl);
+ this.updateCanvases();
+ }
+ }
+
+ registerOnChange(fn: (value: string) => void): void {
+ this.onChange = fn;
+ }
+
+ registerOnTouched(fn: () => void): void {
+ this.onTouched = fn;
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ }
+
+ wheelSize(): number {
+ switch (this.size) {
+ case 'sm': return 120;
+ case 'lg': return 200;
+ default: return 160;
+ }
+ }
+
+ hueBarWidth(): number {
+ return this.wheelSize();
+ }
+
+ hueBarHeight(): number {
+ switch (this.size) {
+ case 'sm': return 16;
+ case 'lg': return 24;
+ default: return 20;
+ }
+ }
+
+ getWrapperClasses(): string {
+ const classes = [
+ `ui-color-picker--${this.size}`,
+ `ui-color-picker--${this.variant}`,
+ `ui-color-picker--${this.state}`
+ ];
+
+ if (this.disabled) classes.push('ui-color-picker--disabled');
+ if (this.required) classes.push('ui-color-picker--required');
+
+ return classes.join(' ');
+ }
+
+ wheelIndicatorPosition(): { x: number, y: number } {
+ const size = this.wheelSize();
+ const radius = size / 2;
+ const center = radius;
+
+ // Convert HSL to position on wheel
+ const saturation = this.currentColor().s / 100;
+ const lightness = this.currentColor().l / 100;
+
+ // Calculate distance from center (0 = center, 1 = edge)
+ const distance = saturation * (radius - 8);
+
+ // Calculate angle for lightness (0 = top, increases clockwise)
+ const angle = (lightness * 360 - 90) * (Math.PI / 180);
+
+ return {
+ x: center + Math.cos(angle) * distance - 6,
+ y: center + Math.sin(angle) * distance - 6
+ };
+ }
+
+ hueIndicatorPosition(): number {
+ const width = this.hueBarWidth();
+ return (this.currentColor().h / 360) * width - 6;
+ }
+
+ getHexColor(): string {
+ return this.hslToHex(this.currentColor());
+ }
+
+ getWheelAriaText(): string {
+ const { s, l } = this.currentColor();
+ return `Saturation ${Math.round(s)}%, Lightness ${Math.round(l)}%`;
+ }
+
+ private initializeCanvases(): void {
+ if (this.colorWheelCanvas?.nativeElement) {
+ this.wheelCtx = this.colorWheelCanvas.nativeElement.getContext('2d') || undefined;
+ }
+ if (this.hueBarCanvas?.nativeElement) {
+ this.hueCtx = this.hueBarCanvas.nativeElement.getContext('2d') || undefined;
+ }
+ }
+
+ private drawColorWheel(): void {
+ if (!this.wheelCtx) return;
+
+ const size = this.wheelSize();
+ const radius = size / 2;
+ const center = radius;
+
+ this.wheelCtx.clearRect(0, 0, size, size);
+
+ // Create saturation/lightness wheel with current hue
+ for (let x = 0; x < size; x++) {
+ for (let y = 0; y < size; y++) {
+ const dx = x - center;
+ const dy = y - center;
+ const distance = Math.sqrt(dx * dx + dy * dy);
+
+ if (distance <= radius - 8) {
+ const angle = Math.atan2(dy, dx);
+ const saturation = (distance / (radius - 8)) * 100;
+ const lightness = ((angle + Math.PI) / (2 * Math.PI)) * 100;
+
+ const color = this.hslToRgb({
+ h: this.currentColor().h,
+ s: saturation,
+ l: lightness
+ });
+
+ this.wheelCtx.fillStyle = `rgb(${color.r}, ${color.g}, ${color.b})`;
+ this.wheelCtx.fillRect(x, y, 1, 1);
+ }
+ }
+ }
+ }
+
+ private drawHueBar(): void {
+ if (!this.hueCtx) return;
+
+ const width = this.hueBarWidth();
+ const height = this.hueBarHeight();
+
+ this.hueCtx.clearRect(0, 0, width, height);
+
+ // Create hue gradient
+ const gradient = this.hueCtx.createLinearGradient(0, 0, width, 0);
+ for (let i = 0; i <= 360; i += 30) {
+ gradient.addColorStop(i / 360, `hsl(${i}, 100%, 50%)`);
+ }
+
+ this.hueCtx.fillStyle = gradient;
+ this.hueCtx.fillRect(0, 0, width, height);
+ }
+
+ private updateCanvases(): void {
+ this.drawColorWheel();
+ // Hue bar doesn't need redrawing unless size changes
+ }
+
+ private setupEventListeners(): void {
+ document.addEventListener('mousemove', this.handleMouseMove.bind(this));
+ document.addEventListener('mouseup', this.handleMouseUp.bind(this));
+ document.addEventListener('touchmove', this.handleTouchMove.bind(this), { passive: false });
+ document.addEventListener('touchend', this.handleTouchEnd.bind(this));
+ }
+
+ private removeEventListeners(): void {
+ document.removeEventListener('mousemove', this.handleMouseMove.bind(this));
+ document.removeEventListener('mouseup', this.handleMouseUp.bind(this));
+ document.removeEventListener('touchmove', this.handleTouchMove.bind(this));
+ document.removeEventListener('touchend', this.handleTouchEnd.bind(this));
+ }
+
+ startWheelDrag(event: MouseEvent): void {
+ if (this.disabled) return;
+
+ this.isDraggingWheel = true;
+ this.updateWheelFromEvent(event);
+ event.preventDefault();
+ }
+
+ startHueDrag(event: MouseEvent): void {
+ if (this.disabled) return;
+
+ this.isDraggingHue = true;
+ this.updateHueFromEvent(event);
+ event.preventDefault();
+ }
+
+ private handleMouseMove(event: MouseEvent): void {
+ if (this.isDraggingWheel) {
+ this.updateWheelFromEvent(event);
+ } else if (this.isDraggingHue) {
+ this.updateHueFromEvent(event);
+ }
+ }
+
+ private handleMouseUp(): void {
+ this.isDraggingWheel = false;
+ this.isDraggingHue = false;
+ }
+
+ private handleTouchMove(event: TouchEvent): void {
+ event.preventDefault();
+ const touch = event.touches[0];
+ if (this.isDraggingWheel) {
+ this.updateWheelFromEvent(touch);
+ } else if (this.isDraggingHue) {
+ this.updateHueFromEvent(touch);
+ }
+ }
+
+ private handleTouchEnd(): void {
+ this.isDraggingWheel = false;
+ this.isDraggingHue = false;
+ }
+
+ private updateWheelFromEvent(event: MouseEvent | Touch): void {
+ if (!this.colorWheelCanvas?.nativeElement) return;
+
+ const rect = this.colorWheelCanvas.nativeElement.getBoundingClientRect();
+ const x = event.clientX - rect.left;
+ const y = event.clientY - rect.top;
+
+ const size = this.wheelSize();
+ const radius = size / 2;
+ const center = radius;
+
+ const dx = x - center;
+ const dy = y - center;
+ const distance = Math.sqrt(dx * dx + dy * dy);
+
+ if (distance <= radius - 8) {
+ const angle = Math.atan2(dy, dx);
+ const saturation = Math.min(100, (distance / (radius - 8)) * 100);
+ const lightness = ((angle + Math.PI) / (2 * Math.PI)) * 100;
+
+ const newColor = {
+ ...this.currentColor(),
+ s: saturation,
+ l: lightness
+ };
+
+ this.currentColor.set(newColor);
+ this.emitColorChange();
+ }
+ }
+
+ private updateHueFromEvent(event: MouseEvent | Touch): void {
+ if (!this.hueBarCanvas?.nativeElement) return;
+
+ const rect = this.hueBarCanvas.nativeElement.getBoundingClientRect();
+ const x = event.clientX - rect.left;
+
+ const width = this.hueBarWidth();
+ const hue = Math.max(0, Math.min(360, (x / width) * 360));
+
+ const newColor = {
+ ...this.currentColor(),
+ h: hue
+ };
+
+ this.currentColor.set(newColor);
+ this.drawColorWheel(); // Redraw wheel with new hue
+ this.emitColorChange();
+ }
+
+ handleWheelKeydown(event: KeyboardEvent): void {
+ if (this.disabled) return;
+
+ const step = event.shiftKey ? 10 : 1;
+ let { h, s, l } = this.currentColor();
+
+ switch (event.key) {
+ case 'ArrowUp':
+ l = Math.min(100, l + step);
+ break;
+ case 'ArrowDown':
+ l = Math.max(0, l - step);
+ break;
+ case 'ArrowLeft':
+ s = Math.max(0, s - step);
+ break;
+ case 'ArrowRight':
+ s = Math.min(100, s + step);
+ break;
+ default:
+ return;
+ }
+
+ this.currentColor.set({ h, s, l });
+ this.emitColorChange();
+ event.preventDefault();
+ }
+
+ handleHueKeydown(event: KeyboardEvent): void {
+ if (this.disabled) return;
+
+ const step = event.shiftKey ? 10 : 1;
+ let { h, s, l } = this.currentColor();
+
+ switch (event.key) {
+ case 'ArrowLeft':
+ h = (h - step + 360) % 360;
+ break;
+ case 'ArrowRight':
+ h = (h + step) % 360;
+ break;
+ default:
+ return;
+ }
+
+ this.currentColor.set({ h, s, l });
+ this.drawColorWheel();
+ this.emitColorChange();
+ event.preventDefault();
+ }
+
+ handleHexInput(event: Event): void {
+ const input = event.target as HTMLInputElement;
+ let value = input.value.trim();
+
+ // Add # if missing
+ if (value && !value.startsWith('#')) {
+ value = '#' + value;
+ }
+
+ if (this.isValidHexColor(value)) {
+ const hsl = this.hexToHsl(value);
+ this.currentColor.set(hsl);
+ this.updateCanvases();
+ this.emitColorChange();
+ }
+ }
+
+ handleHexBlur(event: FocusEvent): void {
+ this.onTouched();
+ this.colorBlur.emit(event);
+
+ // Reset to valid color if invalid input
+ const input = event.target as HTMLInputElement;
+ if (!this.isValidHexColor(input.value)) {
+ input.value = this.getHexColor();
+ }
+ }
+
+ private emitColorChange(): void {
+ const hex = this.getHexColor();
+ this.onChange(hex);
+ this.colorChange.emit(hex);
+ }
+
+ private isValidHexColor(hex: string): boolean {
+ return /^#[0-9A-Fa-f]{6}$/.test(hex);
+ }
+
+ private hslToRgb(hsl: HSL): RGB {
+ const h = hsl.h / 360;
+ const s = hsl.s / 100;
+ const l = hsl.l / 100;
+
+ let r, g, b;
+
+ if (s === 0) {
+ r = g = b = l;
+ } else {
+ const hue2rgb = (p: number, q: number, t: number) => {
+ if (t < 0) t += 1;
+ if (t > 1) t -= 1;
+ if (t < 1/6) return p + (q - p) * 6 * t;
+ if (t < 1/2) return q;
+ if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
+ return p;
+ };
+
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ const p = 2 * l - q;
+
+ r = hue2rgb(p, q, h + 1/3);
+ g = hue2rgb(p, q, h);
+ b = hue2rgb(p, q, h - 1/3);
+ }
+
+ return {
+ r: Math.round(r * 255),
+ g: Math.round(g * 255),
+ b: Math.round(b * 255)
+ };
+ }
+
+ private rgbToHsl(rgb: RGB): HSL {
+ const r = rgb.r / 255;
+ const g = rgb.g / 255;
+ const b = rgb.b / 255;
+
+ const max = Math.max(r, g, b);
+ const min = Math.min(r, g, b);
+ const diff = max - min;
+
+ let h = 0;
+ let s = 0;
+ const l = (max + min) / 2;
+
+ if (diff !== 0) {
+ s = l > 0.5 ? diff / (2 - max - min) : diff / (max + min);
+
+ switch (max) {
+ case r:
+ h = (g - b) / diff + (g < b ? 6 : 0);
+ break;
+ case g:
+ h = (b - r) / diff + 2;
+ break;
+ case b:
+ h = (r - g) / diff + 4;
+ break;
+ }
+ h /= 6;
+ }
+
+ return {
+ h: Math.round(h * 360),
+ s: Math.round(s * 100),
+ l: Math.round(l * 100)
+ };
+ }
+
+ private hexToHsl(hex: string): HSL {
+ const rgb = this.hexToRgb(hex);
+ return this.rgbToHsl(rgb);
+ }
+
+ private hslToHex(hsl: HSL): string {
+ const rgb = this.hslToRgb(hsl);
+ return this.rgbToHex(rgb);
+ }
+
+ private hexToRgb(hex: string): RGB {
+ const r = parseInt(hex.slice(1, 3), 16);
+ const g = parseInt(hex.slice(3, 5), 16);
+ const b = parseInt(hex.slice(5, 7), 16);
+ return { r, g, b };
+ }
+
+ private rgbToHex(rgb: RGB): string {
+ const toHex = (n: number) => n.toString(16).padStart(2, '0');
+ return `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`.toUpperCase();
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/color-picker/index.ts b/projects/ui-essentials/src/lib/components/forms/color-picker/index.ts
new file mode 100644
index 0000000..a4241b4
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/forms/color-picker/index.ts
@@ -0,0 +1 @@
+export * from './color-picker.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/forms/index.ts b/projects/ui-essentials/src/lib/components/forms/index.ts
index e0fad01..e7e302a 100644
--- a/projects/ui-essentials/src/lib/components/forms/index.ts
+++ b/projects/ui-essentials/src/lib/components/forms/index.ts
@@ -3,10 +3,11 @@ export * from './input';
export * from './radio';
export * from './search';
export * from './switch';
-export * from './select.component';
+export * from './select/select.component';
export * from './autocomplete';
export * from './date-picker';
export * from './time-picker';
export * from './file-upload';
export * from './form-field';
export * from './range-slider';
+export * from './color-picker';