This commit is contained in:
Hlars 2025-03-15 07:44:02 +01:00
parent 359e8b3f44
commit 26eaa39411
4 changed files with 243 additions and 94 deletions

View File

@ -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> <div>
Textbox: Textbox:

View File

@ -1,56 +1,64 @@
<div class="step-progress"> <div class="step-progress">
<div class="container" #container> <div class="container" #container>
@for (item of [{status: 'completed'}, {status: 'in progress'}, test]; track item; let i = @for (item of steps; track item; let index = $index) {
$index) { <div class="check-box" [class]="{
<ng-container> 'complete': index < status.progressIndex,
<div class="check-box" [ngClass]="{ 'active': index === status.progressIndex && status.progressStatus === STATUS.InProgress,
complete: item.status === 'completed', 'canceled': index === status.progressIndex && status.progressStatus === STATUS.Canceled,
active: item.status === 'in progress' 'selectable': index <= status.progressIndex
}"> }" [class.first]="index === 0">
<!-- <svg id="checkbox" viewBox="0 0 100 100"> <div [class]="{
<circle class="circle" cx="50.5" cy="49" r="45" /> 'complete': index < status.progressIndex + 1,
@if (item.status == 'in progress') { 'active': index === status.progressIndex + 1 && status.progressStatus === STATUS.InProgress,
<circle class="inner-circle" cx="50.5" cy="49" r="17" /> 'canceled': index === status.progressIndex + 1 && status.progressStatus === STATUS.Canceled,
} }" class="progress-line">
<polyline class="check" points="28.5,51.9 41.9,65.3 72.5,32.8 " /> <div class="progress-percent" [style.--progress]="getProgress"></div>
</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>
</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> </div>
<button style="margin: .25rem; padding: 1rem"
<div (click)="selectedIndex = selectedIndex + 1">next</div> (click)="status.progressIndex = status.progressIndex +1; selectedIndex = selectedIndex + 1">Fertigstellen</button>
<div (click)="selectedIndex = selectedIndex - 1">prev</div> <button style="margin: .25rem; padding: 1rem" (click)="status.progressIndex = status.progressIndex -1">Fertigstellen
zurück</button>
<div> <div>
<button (click)="test.status = 'open'">open</button> <!-- <button (click)="test.status = 'open'">open</button> -->
<button (click)="test.status = 'in progress'">progress</button> <button style="margin: .25rem; padding: 1rem" (click)="status.progressStatus= STATUS.InProgress">progress</button>
<button (click)="test.status = 'completed'">complete</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> </div>

View File

@ -1,3 +1,11 @@
$checkboxWidth: 33px;
$checkboxHeight: 33px;
$color-in-progress: #234dc2;
$color-completed: #23c274;
$color-canceled: #c23323;
$color-selected-step: rebeccapurple;
.step-progress { .step-progress {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
@ -9,12 +17,12 @@
width: 100%; width: 100%;
padding: 30px; padding: 30px;
box-sizing: border-box; box-sizing: border-box;
position: relative;
svg#checkbox { svg#checkbox {
width: 33px; position: relative;
height: 33px; width: $checkboxWidth;
stroke: #23c274; height: $checkboxHeight;
stroke: $color-completed;
stroke-width: 6; stroke-width: 6;
.circle { .circle {
@ -36,12 +44,16 @@
} }
.selection-circle { .selection-circle {
// stroke-dasharray: 320; stroke-dasharray: 320;
// stroke-dashoffset: 320; stroke-dashoffset: 320;
stroke: rebeccapurple; stroke: $color-selected-step;
transition: transition:
stroke-dashoffset 0.5s, stroke-dashoffset 0.5s,
fill 0.5s 0.3s cubic-bezier(0.45, 0, 0.55, 1); fill 0.5s 0.3s cubic-bezier(0.45, 0, 0.55, 1);
&.selected {
stroke-dashoffset: 0;
}
} }
.check { .check {
@ -51,6 +63,13 @@
fill: none; fill: none;
transition: all 0.5s 0.5s cubic-bezier(0.45, 0, 0.55, 1); 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; align-items: center;
position: relative; 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 { .info {
position: absolute; position: absolute;
bottom: -40px; bottom: -30px;
right: 0;
text-align: right;
display: grid; display: grid;
grid-template-columns: max-content; grid-template-columns: max-content;
@ -71,23 +129,38 @@
font-size: 11px; font-size: 11px;
display: inline-block; display: inline-block;
margin-left: 5px; margin-left: 5px;
padding: 2px 10px;
transform: translateY(6px); transform: translateY(6px);
font-weight: 500; font-weight: 500;
color: currentColor; color: currentColor;
background: #eee; transition:
border-radius: 20px; opacity 0.5s,
transition: background-color 0.1s ease-in-out; fill 0.5s 0.3s cubic-bezier(0.45, 0, 0.55, 1);
}
span.progress-info.in-progress { span {
background-color: #234dc2; padding: 2px 10px;
color: #fff; background: #eee;
} border-radius: 20px;
transition: background-color 0.1s ease-in-out;
}
span.progress-info.completed { &.never-called {
background-color: #23c274; opacity: 0;
color: #fff; }
&.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 { span.step {
@ -102,7 +175,7 @@
} }
.progress-line { .progress-line {
width: 140px; width: 10px;
flex-grow: 1; flex-grow: 1;
margin: 0 10px; margin: 0 10px;
height: 4px; height: 4px;
@ -116,38 +189,58 @@
width: 0%; width: 0%;
transition: all 0.5s 0.5s cubic-bezier(0.45, 0, 0.55, 1); transition: all 0.5s 0.5s cubic-bezier(0.45, 0, 0.55, 1);
} }
}
.progress-line.active { &.active .progress-percent {
.progress-percent { background: $color-in-progress;
background: #234dc2; // width: 50%;
width: 50%; width: var(--progress);
} }
}
.progress-line.complete { &.complete .progress-percent {
.progress-percent {
width: 100%; width: 100%;
background: #23c274; background: $color-completed;
}
&.canceled .progress-percent {
width: 0;
} }
} }
.check-box.active { .check-box {
svg#checkbox { &.active {
.circle { svg#checkbox {
fill: #234dc2; .circle {
fill: $color-in-progress;
}
} }
} }
}
.check-box.complete {
svg#checkbox {
.circle {
stroke-dashoffset: 0;
fill: #23c274;
}
.check { &.complete {
stroke-dashoffset: 0; 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;
}
} }
} }
} }

View File

@ -1,14 +1,61 @@
import { NgClass } from '@angular/common'; import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Component } 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({ @Component({
selector: 'app-progress-stepper', selector: 'app-progress-stepper',
imports: [NgClass], imports: [],
templateUrl: './progress-stepper.component.html', templateUrl: './progress-stepper.component.html',
styleUrl: './progress-stepper.component.scss' styleUrl: './progress-stepper.component.scss'
}) })
export class ProgressStepperComponent { export class ProgressStepperComponent {
selectedIndex = 0; 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)}%`
}
} }