diff --git a/src/main/frontend/src/app/components/pill-button/icons/github.svg b/src/main/frontend/src/app/components/pill-button/icons/github.svg new file mode 100644 index 00000000..b6ca4c06 --- /dev/null +++ b/src/main/frontend/src/app/components/pill-button/icons/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/frontend/src/app/components/pill-button/icons/help.svg b/src/main/frontend/src/app/components/pill-button/icons/help.svg new file mode 100644 index 00000000..b28e6d6e --- /dev/null +++ b/src/main/frontend/src/app/components/pill-button/icons/help.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/frontend/src/app/components/pill-button/icons/moon.svg b/src/main/frontend/src/app/components/pill-button/icons/moon.svg new file mode 100644 index 00000000..e560d8c5 --- /dev/null +++ b/src/main/frontend/src/app/components/pill-button/icons/moon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/frontend/src/app/components/pill-button/icons/spinner.svg b/src/main/frontend/src/app/components/pill-button/icons/spinner.svg new file mode 100644 index 00000000..c8364053 --- /dev/null +++ b/src/main/frontend/src/app/components/pill-button/icons/spinner.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/frontend/src/app/components/pill-button/pill-button.component.html b/src/main/frontend/src/app/components/pill-button/pill-button.component.html new file mode 100644 index 00000000..25f11cef --- /dev/null +++ b/src/main/frontend/src/app/components/pill-button/pill-button.component.html @@ -0,0 +1,11 @@ + diff --git a/src/main/frontend/src/app/components/pill-button/pill-button.component.scss b/src/main/frontend/src/app/components/pill-button/pill-button.component.scss new file mode 100644 index 00000000..22dc4762 --- /dev/null +++ b/src/main/frontend/src/app/components/pill-button/pill-button.component.scss @@ -0,0 +1,105 @@ +:host { + display: inline-flex; + align-items: center; +} + +.control-pill { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 8px; + height: 36px; + border-radius: 18px; + background-color: #ffffff; + border: 1px solid #dee2e6; + cursor: pointer; + transition: all 0.2s ease-in-out; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08); + padding: 6px 14px 6px 10px; + + .pill-icon { + width: 20px; + height: 20px; + flex-shrink: 0; + background-color: #6c757d; + transition: background-color 0.2s ease-in-out; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + + &.pill-icon--moon { + -webkit-mask-image: url('./icons/moon.svg'); + mask-image: url('./icons/moon.svg'); + } + + &.pill-icon--help { + -webkit-mask-image: url('./icons/help.svg'); + mask-image: url('./icons/help.svg'); + } + + &.pill-icon--github { + -webkit-mask-image: url('./icons/github.svg'); + mask-image: url('./icons/github.svg'); + } + + &.pill-icon--spinner { + -webkit-mask-image: url('./icons/spinner.svg'); + mask-image: url('./icons/spinner.svg'); + animation: spin 1s linear infinite; + } + } + + .toggle-label { + font-size: 14px; + font-weight: 600; + color: #495057; + white-space: nowrap; + transition: color 0.2s ease-in-out; + } + + &:hover:not(:disabled) { + background-color: #f8f9fa; + border-color: #ced4da; + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.12); + } + + &:active:not(:disabled) { + transform: scale(0.98); + } + + &:disabled { + cursor: not-allowed; + opacity: 0.6; + } + + &.active { + background-color: #1e3a8a; + border-color: #1e3a8a; + box-shadow: 0 3px 6px rgba(30, 58, 138, 0.3); + + .pill-icon { + background-color: #ffffff; + } + + .toggle-label { + color: #ffffff; + } + + &:hover { + background-color: #1d3680; + border-color: #1d3680; + } + } +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/src/main/frontend/src/app/components/pill-button/pill-button.component.ts b/src/main/frontend/src/app/components/pill-button/pill-button.component.ts new file mode 100644 index 00000000..cd10947c --- /dev/null +++ b/src/main/frontend/src/app/components/pill-button/pill-button.component.ts @@ -0,0 +1,21 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +export type PillButtonIcon = 'moon' | 'help' | 'github'; + +@Component({ + selector: 'app-pill-button', + standalone: true, + imports: [], + templateUrl: './pill-button.component.html', + styleUrl: './pill-button.component.scss', +}) +export class PillButtonComponent { + @Input() icon: PillButtonIcon = 'help'; + @Input() label = ''; + @Input() active = false; + @Input() disabled = false; + @Input() loading = false; + @Input() tooltip = ''; + + @Output() clicked = new EventEmitter(); +} diff --git a/src/main/frontend/src/app/pages/header/header.component.html b/src/main/frontend/src/app/pages/header/header.component.html index dce17c52..46f3e44d 100644 --- a/src/main/frontend/src/app/pages/header/header.component.html +++ b/src/main/frontend/src/app/pages/header/header.component.html @@ -58,20 +58,14 @@

CVE Overview

} } @else { - + } diff --git a/src/main/frontend/src/app/pages/header/header.component.scss b/src/main/frontend/src/app/pages/header/header.component.scss index 8adf0801..03203844 100644 --- a/src/main/frontend/src/app/pages/header/header.component.scss +++ b/src/main/frontend/src/app/pages/header/header.component.scss @@ -76,36 +76,8 @@ margin-left: auto; display: flex; align-items: center; - padding-right: 2.5rem; - - .github-login-btn { - display: flex; - align-items: center; - justify-content: center; - background-color: white; - border: none; - border-radius: 50%; - cursor: pointer; - padding: 8px; - transition: background-color 0.2s; - - svg { - display: block; - } - - &:hover:not(:disabled) { - background-color: #f6f8fa; - } - - &:disabled { - cursor: not-allowed; - opacity: 0.6; - } - - .spinner { - animation: spin 1s linear infinite; - } - } + padding-right: 2rem; + padding-top: 0.25rem; .user-profile { position: relative; @@ -284,12 +256,3 @@ } } } - -@keyframes spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/src/main/frontend/src/app/pages/header/header.component.spec.ts b/src/main/frontend/src/app/pages/header/header.component.spec.ts index 06ac6568..5f967c75 100644 --- a/src/main/frontend/src/app/pages/header/header.component.spec.ts +++ b/src/main/frontend/src/app/pages/header/header.component.spec.ts @@ -120,7 +120,7 @@ describe('HeaderComponent', () => { }); it('should display login button', () => { - const loginButton = fixture.debugElement.query(By.css('.github-login-btn')); + const loginButton = fixture.debugElement.query(By.css('app-pill-button')); expect(loginButton).toBeTruthy(); }); @@ -133,7 +133,7 @@ describe('HeaderComponent', () => { it('should call onLoginWithGitHub when login button is clicked', () => { spyOn(component, 'onLoginWithGitHub'); - const loginButton = fixture.debugElement.query(By.css('.github-login-btn')); + const loginButton = fixture.debugElement.query(By.css('app-pill-button button')); loginButton.nativeElement.click(); expect(component.onLoginWithGitHub).toHaveBeenCalledWith(); @@ -142,7 +142,7 @@ describe('HeaderComponent', () => { it('should disable login button when loading', () => { mockAuthService.isLoading.set(true); fixture.detectChanges(); - const loginButton = fixture.debugElement.query(By.css('.github-login-btn')); + const loginButton = fixture.debugElement.query(By.css('app-pill-button button')); expect(loginButton.nativeElement.disabled).toBe(true); }); @@ -163,7 +163,7 @@ describe('HeaderComponent', () => { }); it('should not display login button', () => { - const loginButton = fixture.debugElement.query(By.css('.github-login-btn')); + const loginButton = fixture.debugElement.query(By.css('app-pill-button')); expect(loginButton).toBeFalsy(); }); diff --git a/src/main/frontend/src/app/pages/header/header.component.ts b/src/main/frontend/src/app/pages/header/header.component.ts index 852821b6..6d9f5899 100644 --- a/src/main/frontend/src/app/pages/header/header.component.ts +++ b/src/main/frontend/src/app/pages/header/header.component.ts @@ -4,11 +4,12 @@ import { NgOptimizedImage } from '@angular/common'; import { AuthService } from '../../services/auth.service'; import { LocationService } from '../../services/location.service'; import { GraphStateService } from '../../services/graph-state.service'; +import { PillButtonComponent } from '../../components/pill-button/pill-button.component'; @Component({ selector: 'app-header', standalone: true, - imports: [NgOptimizedImage, RouterLink, RouterLinkActive], + imports: [NgOptimizedImage, RouterLink, RouterLinkActive, PillButtonComponent], templateUrl: './header.component.html', styleUrl: './header.component.scss', }) diff --git a/src/main/frontend/src/app/pages/release-graph/release-catalogus/release-catalogus.component.html b/src/main/frontend/src/app/pages/release-graph/release-catalogus/release-catalogus.component.html index ef44c23d..e378bdf2 100644 --- a/src/main/frontend/src/app/pages/release-graph/release-catalogus/release-catalogus.component.html +++ b/src/main/frontend/src/app/pages/release-graph/release-catalogus/release-catalogus.component.html @@ -1,16 +1,4 @@ - + @if (modalOpen) { diff --git a/src/main/frontend/src/app/pages/release-graph/release-catalogus/release-catalogus.component.scss b/src/main/frontend/src/app/pages/release-graph/release-catalogus/release-catalogus.component.scss index dd8c512b..574bd5d5 100644 --- a/src/main/frontend/src/app/pages/release-graph/release-catalogus/release-catalogus.component.scss +++ b/src/main/frontend/src/app/pages/release-graph/release-catalogus/release-catalogus.component.scss @@ -1,20 +1,6 @@ -.release-btn { - background: none; - border: none; - padding: 0; - margin: 0; - box-shadow: none; - cursor: pointer; - position: fixed; - z-index: 25; - - @media (min-width: 999px) { - top: 1.5rem; - } - - @media (max-width: 992px) { - bottom: 1.25rem; - } +:host { + display: inline-flex; + align-items: center; } .release-modal-content { diff --git a/src/main/frontend/src/app/pages/release-graph/release-catalogus/release-catalogus.component.ts b/src/main/frontend/src/app/pages/release-graph/release-catalogus/release-catalogus.component.ts index 36e59a81..55736c52 100644 --- a/src/main/frontend/src/app/pages/release-graph/release-catalogus/release-catalogus.component.ts +++ b/src/main/frontend/src/app/pages/release-graph/release-catalogus/release-catalogus.component.ts @@ -1,53 +1,34 @@ -import { Component, inject, Input, OnDestroy, OnInit } from '@angular/core'; -import { AsyncPipe, DatePipe, LowerCasePipe, NgStyle } from '@angular/common'; +import { Component, inject, Input, OnInit } from '@angular/core'; +import { AsyncPipe, DatePipe, LowerCasePipe } from '@angular/common'; import { ModalComponent } from '../../../components/modal/modal.component'; -import { AuthService } from '../../../services/auth.service'; import { BuildInfo, VersionService } from '../../../services/version.service'; import { Observable } from 'rxjs'; import { GestureComponent } from '../../../components/gesture/gesture.component'; +import { PillButtonComponent } from '../../../components/pill-button/pill-button.component'; @Component({ selector: 'app-release-catalogus', standalone: true, templateUrl: './release-catalogus.component.html', styleUrl: './release-catalogus.component.scss', - imports: [NgStyle, ModalComponent, AsyncPipe, DatePipe, LowerCasePipe, GestureComponent], + imports: [ModalComponent, AsyncPipe, DatePipe, LowerCasePipe, GestureComponent, PillButtonComponent], }) -export class ReleaseCatalogusComponent implements OnInit, OnDestroy { +export class ReleaseCatalogusComponent implements OnInit { private static readonly SESSION_KEY = 'releaseCatalogusShown'; @Input() showExtendedSupport = false; public modalOpen = false; - public isSmallScreen = false; public buildInfo$: Observable = inject(VersionService).getBuildInformation(); - protected authService: AuthService = inject(AuthService); - - private mediaQueryList: MediaQueryList | null = null; - private mediaListener: (() => void) | null = null; - toggleModal(): void { this.modalOpen = !this.modalOpen; } ngOnInit(): void { - this.mediaQueryList = globalThis.matchMedia('(max-width: 992px)'); - this.isSmallScreen = this.mediaQueryList?.matches ?? false; - this.mediaListener = (): void => { - this.isSmallScreen = this.mediaQueryList?.matches ?? false; - }; - this.mediaQueryList?.addEventListener('change', this.mediaListener); - if (!sessionStorage.getItem(ReleaseCatalogusComponent.SESSION_KEY)) { this.modalOpen = true; sessionStorage.setItem(ReleaseCatalogusComponent.SESSION_KEY, 'true'); } } - - ngOnDestroy(): void { - if (this.mediaQueryList && this.mediaListener) { - this.mediaQueryList.removeEventListener('change', this.mediaListener); - } - } } diff --git a/src/main/frontend/src/app/pages/release-graph/release-graph.component.html b/src/main/frontend/src/app/pages/release-graph/release-graph.component.html index 20b6fa84..663ad167 100644 --- a/src/main/frontend/src/app/pages/release-graph/release-graph.component.html +++ b/src/main/frontend/src/app/pages/release-graph/release-graph.component.html @@ -107,20 +107,18 @@ - +
+ + + +
@for (stickyLabel of stickyBranchLabels; track stickyLabel.label) { @@ -133,8 +131,6 @@ }
- - @if (dataForSkipModal) {