diff --git a/src/app/app.component.html b/src/app/app.component.html index b166914..80a3f6a 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,4 +1,5 @@ -<app-progress-stepper></app-progress-stepper> +<app-progress-stepper + [steps]="['Bla', 'Noch ein Test', 'Abgeschlossen', '2', '4', '5', '2', '4', '5']"></app-progress-stepper> <div> Textbox: diff --git a/src/app/progress-stepper/progress-stepper.component.html b/src/app/progress-stepper/progress-stepper.component.html index f06f1f2..d8b7665 100644 --- a/src/app/progress-stepper/progress-stepper.component.html +++ b/src/app/progress-stepper/progress-stepper.component.html @@ -1,56 +1,64 @@ <div class="step-progress"> <div class="container" #container> - @for (item of [{status: 'completed'}, {status: 'in progress'}, test]; track item; let i = - $index) { - <ng-container> - <div class="check-box" [ngClass]="{ - complete: item.status === 'completed', - active: item.status === 'in progress' - }"> - <!-- <svg id="checkbox" viewBox="0 0 100 100"> - <circle class="circle" cx="50.5" cy="49" r="45" /> - @if (item.status == 'in progress') { - <circle class="inner-circle" cx="50.5" cy="49" r="17" /> - } - <polyline class="check" points="28.5,51.9 41.9,65.3 72.5,32.8 " /> - </svg> --> - - <svg id="checkbox" viewBox="0 0 100 100"> - @if (selectedIndex === i) { - <circle class="selection-circle" cx="50.5" cy="49" r="40" stroke-width="8" fill="none" /> - } - <circle class="circle" cx="50.5" cy="49" r="34" /> - @if (item.status == 'in progress') { - <circle class="inner-circle" cx="50.5" cy="49" r="17" /> - } - <polyline class="check" points="28.5,51.9 41.9,65.3 72.5,32.8 " /> - </svg> - - <div [ngClass]="{ - complete: item.status === 'completed', - active: item.status === 'in progress' - }" class="progress-line"> - <div class="progress-percent"></div> - </div> - <div class="info"> - <span class="step">step {{ i + 1 }}</span> - <span class="progress-info" [ngClass]="{ - 'completed': item.status === 'completed', - 'in-progress': item.status === 'in progress' - }">{{ item.status }}</span> - </div> + @for (item of steps; track item; let index = $index) { + <div class="check-box" [class]="{ + 'complete': index < status.progressIndex, + 'active': index === status.progressIndex && status.progressStatus === STATUS.InProgress, + 'canceled': index === status.progressIndex && status.progressStatus === STATUS.Canceled, + 'selectable': index <= status.progressIndex + }" [class.first]="index === 0"> + <div [class]="{ + 'complete': index < status.progressIndex + 1, + 'active': index === status.progressIndex + 1 && status.progressStatus === STATUS.InProgress, + 'canceled': index === status.progressIndex + 1 && status.progressStatus === STATUS.Canceled, + }" class="progress-line"> + <div class="progress-percent" [style.--progress]="getProgress"></div> </div> - </ng-container> + <div class="hover-box" (click)="selectStep(index)"> + <svg id="checkbox" viewBox="0 0 100 100"> + <circle class="selection-circle" [class.selected]="selectedIndex === index" cx="50.5" cy="49" r="40" + stroke-width="4" fill="none" /> + <circle class="circle" cx="50.5" cy="49" r="34" /> + @if (index === status.progressIndex) { + <circle class="inner-circle" cx="50.5" cy="49" r="17" /> + } + <!-- completed indicator --> + <polyline class="check" points="28.5,51.9 41.9,65.3 72.5,32.8 " /> + <!-- failed indicator --> + <polyline class="fail" points="30.5,28 70.5,67" /> + <polyline class="fail" points="70.5,28 30.5,67" /> + </svg> + </div> + + <div class="info" (click)="selectStep(index)"> + <span class="step">{{ item }}</span> + <span class="progress-info" [class]="{ + 'completed': index < status.progressIndex, + 'in-progress': index === status.progressIndex && status.progressStatus === STATUS.InProgress, + 'canceled': index === status.progressIndex && status.progressStatus === STATUS.Canceled, + 'never-called': index > status.progressIndex && status.progressStatus === STATUS.Canceled, + }"> + <span>{{ statusToLabel(indexStatus(index)) }}</span> + </span> + </div> + </div> } </div> </div> - -<div (click)="selectedIndex = selectedIndex + 1">next</div> -<div (click)="selectedIndex = selectedIndex - 1">prev</div> +<button style="margin: .25rem; padding: 1rem" + (click)="status.progressIndex = status.progressIndex +1; selectedIndex = selectedIndex + 1">Fertigstellen</button> +<button style="margin: .25rem; padding: 1rem" (click)="status.progressIndex = status.progressIndex -1">Fertigstellen + zurück</button> <div> - <button (click)="test.status = 'open'">open</button> - <button (click)="test.status = 'in progress'">progress</button> - <button (click)="test.status = 'completed'">complete</button> + <!-- <button (click)="test.status = 'open'">open</button> --> + <button style="margin: .25rem; padding: 1rem" (click)="status.progressStatus= STATUS.InProgress">progress</button> + <button style="margin: .25rem; padding: 1rem" (click)="status.progressStatus= STATUS.Canceled">Failed</button> + <button style="margin: .25rem; padding: 1rem" + (click)="status.progressPercent = status.progressPercent + 10">+</button> + <button style="margin: .25rem; padding: 1rem" + (click)="status.progressPercent = status.progressPercent - 10">-</button> + <button style="margin: .25rem; padding: 1rem" + (click)="status.progressStatus= STATUS.InProgress; status.progressIndex=0; status.progressPercent=50; selectedIndex=0">Reset</button> </div> \ No newline at end of file diff --git a/src/app/progress-stepper/progress-stepper.component.scss b/src/app/progress-stepper/progress-stepper.component.scss index 52f7ae7..e02b65b 100644 --- a/src/app/progress-stepper/progress-stepper.component.scss +++ b/src/app/progress-stepper/progress-stepper.component.scss @@ -1,3 +1,11 @@ +$checkboxWidth: 33px; +$checkboxHeight: 33px; + +$color-in-progress: #234dc2; +$color-completed: #23c274; +$color-canceled: #c23323; +$color-selected-step: rebeccapurple; + .step-progress { margin-bottom: 1rem; } @@ -9,12 +17,12 @@ width: 100%; padding: 30px; box-sizing: border-box; - position: relative; svg#checkbox { - width: 33px; - height: 33px; - stroke: #23c274; + position: relative; + width: $checkboxWidth; + height: $checkboxHeight; + stroke: $color-completed; stroke-width: 6; .circle { @@ -36,12 +44,16 @@ } .selection-circle { - // stroke-dasharray: 320; - // stroke-dashoffset: 320; - stroke: rebeccapurple; + stroke-dasharray: 320; + stroke-dashoffset: 320; + stroke: $color-selected-step; transition: stroke-dashoffset 0.5s, fill 0.5s 0.3s cubic-bezier(0.45, 0, 0.55, 1); + + &.selected { + stroke-dashoffset: 0; + } } .check { @@ -51,6 +63,13 @@ fill: none; transition: all 0.5s 0.5s cubic-bezier(0.45, 0, 0.55, 1); } + .fail { + stroke-dasharray: 70; + stroke-dashoffset: 70; + stroke: #fff; + fill: none; + transition: all 0.5s 0.5s cubic-bezier(0.45, 0, 0.55, 1); + } } } @@ -60,9 +79,48 @@ align-items: center; position: relative; + &.selectable { + .hover-box:hover, + .hover-box:has(+ .info:hover) { + &::before { + $hoverOffset: 7px; + + content: ""; + position: absolute; + background: #000; + top: calc($hoverOffset / -2); + left: calc($hoverOffset / -2); + width: calc($checkboxWidth + $hoverOffset); + height: calc($checkboxHeight + $hoverOffset); + + border-radius: calc($hoverOffset * 0.5); + z-index: 20; + opacity: 20%; + } + } + + .hover-box { + position: relative; + + &:hover { + cursor: pointer; + } + } + + .info:hover { + cursor: pointer; + } + } + + &.first { + flex-grow: 0.05; + } + .info { position: absolute; - bottom: -40px; + bottom: -30px; + right: 0; + text-align: right; display: grid; grid-template-columns: max-content; @@ -71,23 +129,38 @@ font-size: 11px; display: inline-block; margin-left: 5px; - padding: 2px 10px; transform: translateY(6px); font-weight: 500; color: currentColor; - background: #eee; - border-radius: 20px; - transition: background-color 0.1s ease-in-out; - } + transition: + opacity 0.5s, + fill 0.5s 0.3s cubic-bezier(0.45, 0, 0.55, 1); - span.progress-info.in-progress { - background-color: #234dc2; - color: #fff; - } + span { + padding: 2px 10px; + background: #eee; + border-radius: 20px; + transition: background-color 0.1s ease-in-out; + } - span.progress-info.completed { - background-color: #23c274; - color: #fff; + &.never-called { + opacity: 0; + } + + &.in-progress span { + background-color: $color-in-progress; + color: #fff; + } + + &.completed span { + background-color: $color-completed; + color: #fff; + } + + &.canceled span { + background-color: $color-canceled; + color: #fff; + } } span.step { @@ -102,7 +175,7 @@ } .progress-line { - width: 140px; + width: 10px; flex-grow: 1; margin: 0 10px; height: 4px; @@ -116,38 +189,58 @@ width: 0%; transition: all 0.5s 0.5s cubic-bezier(0.45, 0, 0.55, 1); } -} -.progress-line.active { - .progress-percent { - background: #234dc2; - width: 50%; + &.active .progress-percent { + background: $color-in-progress; + // width: 50%; + width: var(--progress); } -} -.progress-line.complete { - .progress-percent { + &.complete .progress-percent { width: 100%; - background: #23c274; + background: $color-completed; + } + + &.canceled .progress-percent { + width: 0; } } -.check-box.active { - svg#checkbox { - .circle { - fill: #234dc2; +.check-box { + &.active { + svg#checkbox { + .circle { + fill: $color-in-progress; + } } } -} -.check-box.complete { - svg#checkbox { - .circle { - stroke-dashoffset: 0; - fill: #23c274; - } - .check { - stroke-dashoffset: 0; + &.complete { + svg#checkbox { + .circle { + // stroke-dashoffset: 0; + fill: $color-completed; + } + + .check { + stroke-dashoffset: 0; + } + } + } + + &.canceled { + svg#checkbox { + .circle { + fill: $color-canceled; + } + + .inner-circle { + r: 0; + } + + .fail { + stroke-dashoffset: 0; + } } } } diff --git a/src/app/progress-stepper/progress-stepper.component.ts b/src/app/progress-stepper/progress-stepper.component.ts index 5dd7a62..db76da8 100644 --- a/src/app/progress-stepper/progress-stepper.component.ts +++ b/src/app/progress-stepper/progress-stepper.component.ts @@ -1,14 +1,61 @@ -import { NgClass } from '@angular/common'; -import { Component } from '@angular/core'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +export enum Status { + Open = 'open', + InProgress = 'in-progress', + Completed = 'completed', + Canceled = 'canceled' +} + +export interface ProgressStatus { + progressIndex: number, + progressStatus: Status.InProgress | Status.Canceled, + progressPercent: number, +} @Component({ selector: 'app-progress-stepper', - imports: [NgClass], + imports: [], templateUrl: './progress-stepper.component.html', styleUrl: './progress-stepper.component.scss' }) export class ProgressStepperComponent { selectedIndex = 0; + STATUS = Status; - test = { status: 'Offen' }; + @Input() steps: string[] = []; + @Input() status: ProgressStatus = { progressIndex: 0, progressStatus: Status.Canceled, progressPercent: 50 } + + @Output() selectionChange: EventEmitter<number> = new EventEmitter(); + + selectStep(index: number) { + if (index <= this.status.progressIndex) + this.selectedIndex = index; + + this.selectionChange.emit(index) + } + + indexStatus(index: number): Status { + if (index < this.status.progressIndex) + return Status.Completed; + else if (index === this.status.progressIndex) + if (this.status.progressStatus === Status.InProgress) + return Status.InProgress; + else return Status.Canceled; + else + return Status.Open + } + + statusToLabel(status: Status): string { + switch (status) { + case Status.Open: return 'Offen'; + case Status.InProgress: return 'In Bearbeitung'; + case Status.Canceled: return 'Verweigert'; + case Status.Completed: return 'Fertig'; + } + } + + get getProgress(): string { + return `${Math.min(Math.max(this.status.progressPercent, 0), 100)}%` + } }