diff --git a/package.json b/package.json index 2e16cd6..b9cd240 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "private": true, "dependencies": { "@angular/animations": "^17.2.1", - "@angular/cdk": "17.1.2", + "@angular/cdk": "^17.1.2", "@angular/common": "^17.2.1", "@angular/compiler": "^17.2.1", "@angular/core": "^17.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7da0f0a..9616120 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,7 +9,7 @@ dependencies: specifier: ^17.2.1 version: 17.2.1(@angular/core@17.2.1) '@angular/cdk': - specifier: 17.1.2 + specifier: ^17.1.2 version: 17.1.2(@angular/common@17.2.1)(@angular/core@17.2.1)(rxjs@7.8.1) '@angular/common': specifier: ^17.2.1 diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html index dceae72..52f42e0 100644 --- a/src/app/home/home.component.html +++ b/src/app/home/home.component.html @@ -1,3 +1,13 @@ + + +
+
+
+ +
+
+
+ diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index 1fe99fa..60fbf9e 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -1,11 +1,12 @@ import { Component } from '@angular/core'; +import { HomeGeneratorComponent } from './ui/home-generator/home-generator.component'; import { HomeManualComponent } from './ui/home-manual/home-manual.component'; import { HomeSupportComponent } from './ui/home-support/home-support.component'; @Component({ selector: 'rp-home', standalone: true, - imports: [HomeManualComponent, HomeSupportComponent], + imports: [HomeGeneratorComponent, HomeManualComponent, HomeSupportComponent], templateUrl: './home.component.html', }) export default class HomeComponent {} diff --git a/src/app/home/ui/home-generator/home-generator.component.html b/src/app/home/ui/home-generator/home-generator.component.html new file mode 100644 index 0000000..8ff1e45 --- /dev/null +++ b/src/app/home/ui/home-generator/home-generator.component.html @@ -0,0 +1,85 @@ +
+
+

+ {{ "home.generator.title" | translate }} +

+ +

+ {{ "home.generator.description" | translate }} +

+
+ +
+
+ + + + + + + +
+ + + + + + + + + {{ scheme.label | translate }} + + + +
+
diff --git a/src/app/home/ui/home-generator/home-generator.component.spec.ts b/src/app/home/ui/home-generator/home-generator.component.spec.ts new file mode 100644 index 0000000..d3f79bf --- /dev/null +++ b/src/app/home/ui/home-generator/home-generator.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HomeGeneratorComponent } from './home-generator.component'; + +describe('HomeGeneratorComponent', () => { + let component: HomeGeneratorComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [HomeGeneratorComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(HomeGeneratorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/home/ui/home-generator/home-generator.component.ts b/src/app/home/ui/home-generator/home-generator.component.ts new file mode 100644 index 0000000..424718e --- /dev/null +++ b/src/app/home/ui/home-generator/home-generator.component.ts @@ -0,0 +1,66 @@ +import { Component, effect, model, signal, viewChild } from '@angular/core'; +import { NgIconComponent } from '@ng-icons/core'; +import { heroChevronDownMini } from '@ng-icons/heroicons/mini'; +import { TranslateModule } from '@ngx-translate/core'; +import { DropdownMenuComponent } from '../../../shared/ui/dropdown-menu/dropdown-menu.component'; + +@Component({ + selector: 'rp-home-generator', + standalone: true, + imports: [TranslateModule, DropdownMenuComponent, NgIconComponent], + templateUrl: './home-generator.component.html', +}) +export class HomeGeneratorComponent { + public readonly color = model('#3B82F6'); + + protected readonly heroChevronDownMini = heroChevronDownMini; + + protected readonly isValid = signal(true); + + private readonly _hexInput = viewChild.required<{ + nativeElement: HTMLInputElement; + }>('hexInput'); + + constructor() { + effect(() => { + this._hexInput().nativeElement.value = this.color(); + }); + } + + protected get schemeOptions() { + return [ + { value: 'rainbow', label: 'scheme.rainbow' }, + { value: 'random', label: 'scheme.random' }, + ]; + } + + protected setColor(color: string, $event: Event): void { + // Remove leading and trailing whitespace + color = color.trim(); + + // Remove characters that are not valid hex color characters + color = color.replace(/[^0-9a-fA-F]/g, ''); + + // Add leading hash if missing + if (!color.startsWith('#')) { + color = `#${color}`; + } + + // Normalize to uppercase and remove extra characters + color = color.substring(0, 7); + color = color.toUpperCase(); + + // Set color if it is a valid hex color + if (color.length === 4) { + this.color.set(color); + this.isValid.set(true); + } else if (color.length === 7) { + this.color.set(color); + this.isValid.set(true); + } else { + this.isValid.set(false); + } + + this._hexInput().nativeElement.value = color; + } +} diff --git a/src/app/layout/layout.component.html b/src/app/layout/layout.component.html index f58e3d8..71e624a 100644 --- a/src/app/layout/layout.component.html +++ b/src/app/layout/layout.component.html @@ -1,4 +1,6 @@ -
+

Rainbow Palette diff --git a/src/app/layout/ui/layout-options/layout-options.component.html b/src/app/layout/ui/layout-options/layout-options.component.html index b253057..5bf435e 100644 --- a/src/app/layout/ui/layout-options/layout-options.component.html +++ b/src/app/layout/ui/layout-options/layout-options.component.html @@ -2,6 +2,7 @@ @@ -34,3 +47,7 @@

+ + + {{ item | translate }} + diff --git a/src/app/shared/ui/dropdown-menu/dropdown-menu.component.ts b/src/app/shared/ui/dropdown-menu/dropdown-menu.component.ts index d6bc29b..7a188b1 100644 --- a/src/app/shared/ui/dropdown-menu/dropdown-menu.component.ts +++ b/src/app/shared/ui/dropdown-menu/dropdown-menu.component.ts @@ -9,11 +9,17 @@ import { CommonModule } from '@angular/common'; import { Component, TemplateRef, + booleanAttribute, contentChild, + effect, input, model, + signal, + viewChild, } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; import { TranslateModule } from '@ngx-translate/core'; +import { Subscription } from 'rxjs'; @Component({ selector: 'rp-dropdown-menu', @@ -29,13 +35,38 @@ import { TranslateModule } from '@ngx-translate/core'; templateUrl: './dropdown-menu.component.html', }) export class DropdownMenuComponent { + // Input Signals public readonly items = input.required>(); public readonly title = input(); + public readonly disabled = input(false, { + transform: booleanAttribute, + }); + public readonly closeOnScroll = input(true, { + transform: booleanAttribute, + }); + public readonly minWidth = input('12rem'); + public readonly maxWidth = input('40rem'); + public readonly minHeight = input(); + public readonly maxHeight = input('16rem'); + // Model Signals + public readonly selectedItem = model(undefined); + + // Content Signals public readonly itemTemplate = - contentChild.required>('itemTemplate'); + contentChild>('itemTemplate'); - public readonly selectedItem = model(undefined); + // View Child Signals + private readonly _trigger = viewChild(CdkMenuTrigger); + private readonly _menu = viewChild<{ nativeElement: HTMLElement }>( + 'menuGroup' + ); + + // Internal Signals and Properties + private readonly _isOpen = signal(false); + private readonly _abortControllers: Array = []; + private _openedSubscription?: Subscription; + private _closedSubscription?: Subscription; protected readonly menuPositions: Array = [ { @@ -68,6 +99,67 @@ export class DropdownMenuComponent { }, ]; + constructor() { + /** + * Effect to handle the opened and closed subscriptions of the trigger. + * These subscriptions are used to update the isOpen signal when the + * menu is opened or closed. + */ + effect(() => { + if (this._openedSubscription) { + this._openedSubscription.unsubscribe(); + } + if (this._closedSubscription) { + this._closedSubscription.unsubscribe(); + } + + if (this._trigger()) { + this._openedSubscription = this._trigger()!.opened.subscribe(() => { + this._isOpen.set(true); + }); + this._closedSubscription = this._trigger()!.closed.subscribe(() => { + this._isOpen.set(false); + }); + } + }); + + /** + * Effect to handle closing the menu when the user scrolls outside of the menu. + * This effect will add an event listener to the window scroll event when the + * menu is open (and {@link closeOnScroll} is true). + * These event listeners are also automatically removed when the menu is closed again. + */ + effect(() => { + if (this._isOpen()) { + if (!this.closeOnScroll()) { + return; + } + + const abortController = new AbortController(); + this._abortControllers.push(abortController); + + window.addEventListener( + 'scroll', + (event: Event) => { + if (event.target === this._menu()?.nativeElement) { + return; + } + + this._trigger()?.close(); + }, + { + capture: true, + signal: abortController.signal, + } + ); + } else { + while (this._abortControllers.length > 0) { + this._abortControllers.shift()?.abort(); + } + } + }); + } + protected select(item: T): void { this.selectedItem.set(item); } diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 2b1998f..72c83fc 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -1,5 +1,12 @@ { "home": { + "generator": { + "color": "Wähle eine Farbe", + "description": "Gebe einen Hex-Code ein oder wähle eine Farbe, um loszulegen.", + "generate": "Palette generieren", + "scheme": "Wähle ein Farbschema", + "title": "Erstelle deine eigene Farbpalette aus nur einer einzigen Farbe. " + }, "manual": { "cta": "Das war's!
So einfach kannst du deine ganz eigene Farbpalette erstellen.", "description": "Du kannst deine ganz eigene Farbpalette in nur 3 einfachen Schritten erstellen.", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 08d9300..5330ad4 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1,5 +1,12 @@ { "home": { + "generator": { + "color": "Enter your color", + "description": "Enter a hex code below or pick a color to get started.", + "generate": "Generate palette", + "scheme": "Pick a color scheme", + "title": "Generate an entire color palette from just a single color." + }, "manual": { "cta": "That's it!
It's that easy to create your very own color palette.", "description": "You can create your very own color palette in just 3 simple steps.", @@ -67,4 +74,4 @@ "dark": "Dark", "light": "Light" } -} \ No newline at end of file +}