diff --git a/apps/client/src/app/pages/landing/landing-page.html b/apps/client/src/app/pages/landing/landing-page.html
index bdda5d5b6..96d5c4f7a 100644
--- a/apps/client/src/app/pages/landing/landing-page.html
+++ b/apps/client/src/app/pages/landing/landing-page.html
@@ -320,49 +320,37 @@
diff --git a/apps/client/src/app/pages/landing/landing-page.module.ts b/apps/client/src/app/pages/landing/landing-page.module.ts
index 6bc321349..4357f52fc 100644
--- a/apps/client/src/app/pages/landing/landing-page.module.ts
+++ b/apps/client/src/app/pages/landing/landing-page.module.ts
@@ -4,18 +4,18 @@ import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { RouterModule } from '@angular/router';
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module';
+import { GfCarouselModule } from '@ghostfolio/ui/carousel';
import { GfLogoModule } from '@ghostfolio/ui/logo';
import { GfValueModule } from '@ghostfolio/ui/value';
-import { CarouselItem } from '@ghostfolio/ui/carousel/carousel-item.directive';
-import { Carousel } from '@ghostfolio/ui/carousel/carousel';
import { LandingPageRoutingModule } from './landing-page-routing.module';
import { LandingPageComponent } from './landing-page.component';
@NgModule({
- declarations: [Carousel, CarouselItem, LandingPageComponent],
+ declarations: [LandingPageComponent],
imports: [
CommonModule,
+ GfCarouselModule,
GfLogoModule,
GfValueModule,
GfWorldMapChartModule,
diff --git a/libs/ui/src/lib/carousel/_carousel-theme.scss. b/libs/ui/src/lib/carousel/_carousel-theme.scss.
deleted file mode 100644
index 4c28229a4..000000000
--- a/libs/ui/src/lib/carousel/_carousel-theme.scss.
+++ /dev/null
@@ -1,16 +0,0 @@
-@use 'sass:map';
-@use '@angular/material' as mat;
-
-@mixin theme($theme) {
- $primary: map.get($theme, primary);
- $accent: map.get($theme, accent);
- $warn: map.get($theme, warn);
- $background: map.get($theme, background);
- $foreground: map.get($theme, foreground);
-
- .docs-carousel-nav.mat-mdc-mini-fab {
- background: mat.get-color-from-palette($background, background);
- color: mat.get-color-from-palette($foreground, secondary-text)
- }
-
-}
\ No newline at end of file
diff --git a/libs/ui/src/lib/carousel/carousel-item.directive.ts b/libs/ui/src/lib/carousel/carousel-item.directive.ts
index f137c5bf7..95fefe5cc 100644
--- a/libs/ui/src/lib/carousel/carousel-item.directive.ts
+++ b/libs/ui/src/lib/carousel/carousel-item.directive.ts
@@ -1,14 +1,16 @@
-import { FocusableOption } from "@angular/cdk/a11y";
-import { ElementRef, HostBinding } from "@angular/core";
+import { FocusableOption } from '@angular/cdk/a11y';
+import { Directive, ElementRef, HostBinding } from '@angular/core';
+@Directive({
+ selector: '[gf-carousel-item]'
+})
+export class CarouselItem implements FocusableOption {
+ @HostBinding('attr.role') readonly role = 'listitem';
+ @HostBinding('tabindex') tabindex = '-1';
- export class CarouselItem implements FocusableOption {
- @HostBinding('attr.role') readonly role = 'listitem';
- @HostBinding('tabindex') tabindex = '-1';
-
- constructor(readonly element: ElementRef
) {}
-
- focus(): void {
- this.element.nativeElement.focus({preventScroll: true});
- }
- }
\ No newline at end of file
+ public constructor(readonly element: ElementRef) {}
+
+ public focus(): void {
+ this.element.nativeElement.focus({ preventScroll: true });
+ }
+}
diff --git a/libs/ui/src/lib/carousel/carousel.component.html b/libs/ui/src/lib/carousel/carousel.component.html
new file mode 100644
index 000000000..59966b8a6
--- /dev/null
+++ b/libs/ui/src/lib/carousel/carousel.component.html
@@ -0,0 +1,34 @@
+
+
+
+
+
diff --git a/libs/ui/src/lib/carousel/carousel.component.scss b/libs/ui/src/lib/carousel/carousel.component.scss
new file mode 100644
index 000000000..38da7c100
--- /dev/null
+++ b/libs/ui/src/lib/carousel/carousel.component.scss
@@ -0,0 +1,34 @@
+:host {
+ display: block;
+ position: relative;
+
+ ::ng-deep {
+ [gf-carousel-item] {
+ flex-shrink: 0;
+ width: 100%;
+ }
+ }
+
+ button {
+ top: 50%;
+ transform: translateY(-50%);
+
+ &.carousel-nav-prev {
+ left: -50px;
+ }
+
+ &.carousel-nav-next {
+ right: -50px;
+ }
+ }
+
+ .carousel-content {
+ flex-direction: row;
+ outline: none;
+ transition: transform 0.5s ease-in-out;
+
+ .animations-disabled & {
+ transition: none;
+ }
+ }
+}
diff --git a/libs/ui/src/lib/carousel/carousel.component.ts b/libs/ui/src/lib/carousel/carousel.component.ts
new file mode 100644
index 000000000..a0eb0f8a1
--- /dev/null
+++ b/libs/ui/src/lib/carousel/carousel.component.ts
@@ -0,0 +1,147 @@
+import { FocusKeyManager } from '@angular/cdk/a11y';
+import { LEFT_ARROW, RIGHT_ARROW, TAB } from '@angular/cdk/keycodes';
+import {
+ AfterContentInit,
+ ChangeDetectionStrategy,
+ Component,
+ ContentChildren,
+ ElementRef,
+ HostBinding,
+ Inject,
+ Input,
+ Optional,
+ QueryList,
+ ViewChild
+} from '@angular/core';
+import { ANIMATION_MODULE_TYPE } from '@angular/platform-browser/animations';
+
+import { CarouselItem } from './carousel-item.directive';
+
+@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ selector: 'gf-carousel',
+ styleUrls: ['./carousel.component.scss'],
+ templateUrl: './carousel.component.html'
+})
+export class CarouselComponent implements AfterContentInit {
+ @ContentChildren(CarouselItem) public items!: QueryList;
+
+ @HostBinding('class.animations-disabled')
+ public readonly animationsDisabled: boolean;
+
+ @Input('aria-label') public ariaLabel: string | undefined;
+
+ @ViewChild('list') public list!: ElementRef;
+
+ public showPrevArrow = false;
+ public showNextArrow = true;
+
+ private index = 0;
+ private keyManager!: FocusKeyManager;
+ private position = 0;
+
+ public constructor(
+ @Optional() @Inject(ANIMATION_MODULE_TYPE) animationsModule?: string
+ ) {
+ this.animationsDisabled = animationsModule === 'NoopAnimations';
+ }
+
+ public ngAfterContentInit() {
+ this.keyManager = new FocusKeyManager(this.items);
+ }
+
+ public next() {
+ for (let i = this.index; i < this.items.length; i++) {
+ if (this.isOutOfView(i)) {
+ this.index = i;
+ this.scrollToActiveItem();
+ break;
+ }
+ }
+ }
+
+ public onKeydown({ keyCode }: KeyboardEvent) {
+ const manager = this.keyManager;
+ const previousActiveIndex = manager.activeItemIndex;
+
+ if (keyCode === LEFT_ARROW) {
+ manager.setPreviousItemActive();
+ } else if (keyCode === RIGHT_ARROW) {
+ manager.setNextItemActive();
+ } else if (keyCode === TAB && !manager.activeItem) {
+ manager.setFirstItemActive();
+ }
+
+ if (
+ manager.activeItemIndex != null &&
+ manager.activeItemIndex !== previousActiveIndex
+ ) {
+ this.index = manager.activeItemIndex;
+ this.updateItemTabIndices();
+
+ if (this.isOutOfView(this.index)) {
+ this.scrollToActiveItem();
+ }
+ }
+ }
+
+ public previous() {
+ for (let i = this.index; i > -1; i--) {
+ if (this.isOutOfView(i)) {
+ this.index = i;
+ this.scrollToActiveItem();
+ break;
+ }
+ }
+ }
+
+ private isOutOfView(index: number, side?: 'start' | 'end') {
+ const { offsetWidth, offsetLeft } =
+ this.items.toArray()[index].element.nativeElement;
+
+ if ((!side || side === 'start') && offsetLeft - this.position < 0) {
+ return true;
+ }
+
+ return (
+ (!side || side === 'end') &&
+ offsetWidth + offsetLeft - this.position >
+ this.list.nativeElement.clientWidth
+ );
+ }
+
+ private scrollToActiveItem() {
+ if (!this.isOutOfView(this.index)) {
+ return;
+ }
+
+ const itemsArray = this.items.toArray();
+ let targetItemIndex = this.index;
+
+ if (this.index > 0 && !this.isOutOfView(this.index - 1)) {
+ targetItemIndex =
+ itemsArray.findIndex((_, i) => !this.isOutOfView(i)) + 1;
+ }
+
+ this.position =
+ itemsArray[targetItemIndex].element.nativeElement.offsetLeft;
+ this.list.nativeElement.style.transform = `translateX(-${this.position}px)`;
+ this.showPrevArrow = this.index > 0;
+ this.showNextArrow = false;
+
+ for (let i = itemsArray.length - 1; i > -1; i--) {
+ if (this.isOutOfView(i, 'end')) {
+ this.showNextArrow = true;
+ break;
+ }
+ }
+ }
+
+ private updateItemTabIndices() {
+ this.items.forEach((item: CarouselItem) => {
+ if (this.keyManager != null) {
+ item.tabindex = item === this.keyManager.activeItem ? '0' : '-1';
+ }
+ });
+ }
+}
diff --git a/libs/ui/src/lib/carousel/carousel.html b/libs/ui/src/lib/carousel/carousel.html
deleted file mode 100644
index efc91781c..000000000
--- a/libs/ui/src/lib/carousel/carousel.html
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/libs/ui/src/lib/carousel/carousel.module.ts b/libs/ui/src/lib/carousel/carousel.module.ts
new file mode 100644
index 000000000..4e43f23b0
--- /dev/null
+++ b/libs/ui/src/lib/carousel/carousel.module.ts
@@ -0,0 +1,14 @@
+import { CommonModule } from '@angular/common';
+import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
+import { MatButtonModule } from '@angular/material/button';
+
+import { CarouselItem } from './carousel-item.directive';
+import { CarouselComponent } from './carousel.component';
+
+@NgModule({
+ declarations: [CarouselComponent, CarouselItem],
+ exports: [CarouselComponent, CarouselItem],
+ imports: [CommonModule, MatButtonModule],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA]
+})
+export class GfCarouselModule {}
diff --git a/libs/ui/src/lib/carousel/carousel.scss b/libs/ui/src/lib/carousel/carousel.scss
deleted file mode 100644
index 8993db95f..000000000
--- a/libs/ui/src/lib/carousel/carousel.scss
+++ /dev/null
@@ -1,37 +0,0 @@
-gf-carousel {
- display: block;
- position: relative;
- }
-
- .docs-carousel-content {
- display: flex;
- flex-direction: row;
- outline: none;
- transition: transform 0.5s ease-in-out;
-
- .animations-disabled & {
- transition: none;
- }
- }
-
- .docs-carousel-content-wrapper {
- overflow: hidden;
- }
-
- [carousel-item] {
- flex-shrink: 0;
- }
-
- button.docs-carousel-nav {
- position: absolute;
- top: 50%;
- transform: translateY(-50%);
- }
-
- .docs-carousel-nav-prev {
- left: -50px;
- }
-
- .docs-carousel-nav-next {
- right: -50px;
- }
\ No newline at end of file
diff --git a/libs/ui/src/lib/carousel/carousel.ts b/libs/ui/src/lib/carousel/carousel.ts
deleted file mode 100644
index 400ed7a5a..000000000
--- a/libs/ui/src/lib/carousel/carousel.ts
+++ /dev/null
@@ -1,144 +0,0 @@
-import {
- AfterContentInit,
- Component,
- ContentChildren,
- Directive,
- ElementRef,
- HostBinding,
- Inject,
- Input,
- Optional,
- QueryList,
- ViewChild,
- ViewEncapsulation,
- } from '@angular/core';
- import {FocusableOption, FocusKeyManager} from '@angular/cdk/a11y';
- import {LEFT_ARROW, RIGHT_ARROW, TAB} from '@angular/cdk/keycodes';
- import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
- import {MatIconModule} from '@angular/material/icon';
- import {MatButtonModule} from '@angular/material/button';
- import {NgIf} from '@angular/common';
-import { CarouselItem } from './carousel-item.directive';
-
- @Component({
- selector: 'gf-carousel',
- imports: [
- NgIf,
- MatButtonModule,
- MatIconModule,
- ],
- })
- export class Carousel implements AfterContentInit {
- @Input('aria-label') ariaLabel: string | undefined;
- @ContentChildren(CarouselItem) items!: QueryList;
- @ViewChild('list') list!: ElementRef;
- @HostBinding('class.animations-disabled') readonly animationsDisabled: boolean;
- position = 0;
- showPrevArrow = false;
- showNextArrow = true;
- index = 0;
- private _keyManager!: FocusKeyManager;
-
- onKeydown({keyCode}: KeyboardEvent) {
- const manager = this._keyManager;
- const previousActiveIndex = manager.activeItemIndex;
-
- if (keyCode === LEFT_ARROW) {
- manager.setPreviousItemActive();
- } else if (keyCode === RIGHT_ARROW) {
- manager.setNextItemActive();
- } else if (keyCode === TAB && !manager.activeItem) {
- manager.setFirstItemActive();
- }
-
- if (manager.activeItemIndex != null && manager.activeItemIndex !== previousActiveIndex) {
- this.index = manager.activeItemIndex;
- this._updateItemTabIndices();
-
- if (this._isOutOfView(this.index)) {
- this._scrollToActiveItem();
- }
- }
- }
-
- constructor(@Optional() @Inject(ANIMATION_MODULE_TYPE) animationsModule?: string) {
- this.animationsDisabled = animationsModule === 'NoopAnimations';
- }
-
- ngAfterContentInit(): void {
- this._keyManager = new FocusKeyManager(this.items);
- }
-
- /** Goes to the next set of items */
- next() {
- for (let i = this.index; i < this.items.length; i++) {
- if (this._isOutOfView(i)) {
- this.index = i;
- this._scrollToActiveItem();
- break;
- }
- }
- }
-
- /** Goes to the previous set of items. */
- previous() {
- for (let i = this.index; i > -1; i--) {
- if (this._isOutOfView(i)) {
- this.index = i;
- this._scrollToActiveItem();
- break;
- }
- }
- }
-
- /** Updates the `tabindex` of each of the items based on their active state. */
- private _updateItemTabIndices() {
- this.items.forEach((item: CarouselItem) => {
- if (this._keyManager != null) {
- item.tabindex = item === this._keyManager.activeItem ? '0' : '-1';
- }
- });
- }
-
- /** Scrolls an item into the viewport. */
- private _scrollToActiveItem() {
- if (!this._isOutOfView(this.index)) {
- return;
- }
-
- const itemsArray = this.items.toArray();
- let targetItemIndex = this.index;
-
- // Only shift the carousel by one if we're going forwards. This
- // looks better compared to moving the carousel by an entire page.
- if (this.index > 0 && !this._isOutOfView(this.index - 1)) {
- targetItemIndex = itemsArray.findIndex((_, i) => !this._isOutOfView(i)) + 1;
- }
-
- this.position = itemsArray[targetItemIndex].element.nativeElement.offsetLeft;
- this.list.nativeElement.style.transform = `translateX(-${this.position}px)`;
- this.showPrevArrow = this.index > 0;
- this.showNextArrow = false;
-
- for (let i = itemsArray.length - 1; i > -1; i--) {
- if (this._isOutOfView(i, 'end')) {
- this.showNextArrow = true;
- break;
- }
- }
- }
-
- /** Checks whether an item at a specific index is outside of the viewport. */
- private _isOutOfView(index: number, side?: 'start' | 'end') {
- const {offsetWidth, offsetLeft} = this.items.toArray()[index].element.nativeElement;
-
- if ((!side || side === 'start') && offsetLeft - this.position < 0) {
- return true;
- }
-
- return (
- (!side || side === 'end') &&
- offsetWidth + offsetLeft - this.position > this.list.nativeElement.clientWidth
- );
- }
- }
\ No newline at end of file
diff --git a/libs/ui/src/lib/carousel/index.ts b/libs/ui/src/lib/carousel/index.ts
new file mode 100644
index 000000000..2e039a80b
--- /dev/null
+++ b/libs/ui/src/lib/carousel/index.ts
@@ -0,0 +1 @@
+export * from './carousel.module';