feat(animation logo)

This commit is contained in:
2026-03-18 17:30:53 +01:00
parent 20988e425a
commit a40a8df894
59 changed files with 3183 additions and 90 deletions

View File

@@ -0,0 +1,31 @@
<section class="animation-test-page">
<div class="animation-toolbar" role="group" aria-label="Animation variants">
<button
type="button"
class="variant-toggle"
[class.active]="variant() === 'site-intro'"
(click)="setVariant('site-intro')"
>
Site intro
</button>
<button
type="button"
class="variant-toggle"
[class.active]="variant() === 'calculator-loader'"
(click)="setVariant('calculator-loader')"
>
Calculator loader
</button>
</div>
<div
class="animation-stage"
[attr.data-variant]="variant()"
>
<app-brand-animation-logo
[variant]="variant()"
[decorative]="false"
ariaLabel="3D fab animation test"
></app-brand-animation-logo>
</div>
</section>

View File

@@ -0,0 +1,60 @@
:host {
display: block;
}
.animation-test-page {
min-height: 100vh;
display: grid;
align-content: center;
justify-items: center;
gap: 1.5rem;
padding: 2rem 1.5rem 3rem;
background: #fff;
}
.animation-toolbar {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem;
border: 1px solid rgba(16, 24, 32, 0.12);
border-radius: 999px;
background: #f7f5ef;
}
.variant-toggle {
min-height: 2.4rem;
padding: 0 1rem;
border: 0;
border-radius: 999px;
background: transparent;
color: var(--color-text-muted);
font: inherit;
font-weight: 600;
cursor: pointer;
transition:
background-color 0.18s ease,
color 0.18s ease,
box-shadow 0.18s ease;
}
.variant-toggle.active {
background: #fff;
color: var(--color-text);
box-shadow: 0 6px 16px rgba(16, 24, 32, 0.08);
}
.animation-stage {
width: min(100%, 26rem);
}
@media (max-width: 640px) {
.animation-toolbar {
flex-wrap: wrap;
justify-content: center;
}
.animation-stage {
width: min(100%, 19rem);
}
}

View File

@@ -0,0 +1,21 @@
import { CommonModule } from '@angular/common';
import { Component, signal } from '@angular/core';
import {
BrandAnimationLogoComponent,
BrandAnimationVariant,
} from '../../shared/components/brand-animation-logo/brand-animation-logo.component';
@Component({
selector: 'app-calculator-animation-test',
standalone: true,
imports: [CommonModule, BrandAnimationLogoComponent],
templateUrl: './calculator-animation-test.component.html',
styleUrl: './calculator-animation-test.component.scss',
})
export class CalculatorAnimationTestComponent {
readonly variant = signal<BrandAnimationVariant>('site-intro');
setVariant(variant: BrandAnimationVariant): void {
this.variant.set(variant);
}
}

View File

@@ -57,7 +57,10 @@
@if (loading()) {
<app-card class="loading-state">
<div class="loader-content">
<div class="spinner"></div>
<app-brand-animation-logo
class="loader-logo"
variant="calculator-loader"
></app-brand-animation-logo>
<h3 class="loading-title">
{{ "CALC.ANALYZING_TITLE" | translate }}
</h3>

View File

@@ -93,7 +93,7 @@
.loader-content {
text-align: center;
max-width: 300px;
max-width: 22rem;
margin: 0 auto;
/* Center content vertically within the stretched card */
@@ -101,12 +101,14 @@
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: var(--space-3);
}
.loading-title {
font-size: 1.1rem;
font-weight: 600;
margin: var(--space-4) 0 var(--space-2);
margin: 0;
color: var(--color-text);
}
@@ -114,23 +116,21 @@
font-size: 0.9rem;
color: var(--color-text-muted);
line-height: 1.5;
margin: 0;
}
.spinner {
border: 3px solid var(--color-neutral-200);
border-left-color: var(--color-brand);
border-radius: 50%;
width: 48px;
height: 48px;
animation: spin 1s linear infinite;
.loader-logo {
display: block;
width: min(100%, 16rem);
margin: 0 auto;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
--brand-animation-width: 16rem;
--brand-animation-height: 4.8rem;
--brand-animation-letter-width: 2.85rem;
--brand-animation-scale: 0.84;
--brand-animation-word-spacing: 0.97;
--brand-animation-width-mobile: 14rem;
--brand-animation-height-mobile: 4.1rem;
--brand-animation-letter-width-mobile: 2.45rem;
--brand-animation-scale-mobile: 0.84;
--brand-animation-loader-loop-duration: 2.5s;
}

View File

@@ -17,6 +17,7 @@ import { catchError, map } from 'rxjs/operators';
import { AppCardComponent } from '../../shared/components/app-card/app-card.component';
import { AppAlertComponent } from '../../shared/components/app-alert/app-alert.component';
import { AppButtonComponent } from '../../shared/components/app-button/app-button.component';
import { BrandAnimationLogoComponent } from '../../shared/components/brand-animation-logo/brand-animation-logo.component';
import { UploadFormComponent } from './components/upload-form/upload-form.component';
import { QuoteResultComponent } from './components/quote-result/quote-result.component';
import {
@@ -48,6 +49,7 @@ type TrackedPrintSettings = {
AppCardComponent,
AppAlertComponent,
AppButtonComponent,
BrandAnimationLogoComponent,
UploadFormComponent,
QuoteResultComponent,
SuccessStateComponent,

View File

@@ -3,6 +3,18 @@ import { CalculatorPageComponent } from './calculator-page.component';
export const CALCULATOR_ROUTES: Routes = [
{ path: '', redirectTo: 'basic', pathMatch: 'full' },
{
path: 'animation-test',
loadComponent: () =>
import('./calculator-animation-test.component').then(
(m) => m.CalculatorAnimationTestComponent,
),
data: {
seoTitleKey: 'SEO.ROUTES.CALCULATOR.TITLE',
seoDescriptionKey: 'SEO.ROUTES.CALCULATOR.DESCRIPTION',
seoRobots: 'noindex, nofollow',
},
},
{
path: 'basic',
component: CalculatorPageComponent,

View File

@@ -127,7 +127,8 @@ export class UploadFormComponent implements OnInit {
private layerHeightsByNozzle: Record<string, SimpleOption[]> = {};
private isPatchingSettings = false;
acceptedFormats = '.stl,.3mf,.step,.stp';
acceptedFormats = '.stl,.3mf';
private readonly allowedExtensions = ['stl', '3mf'] as const;
constructor() {
this.form = this.fb.group({
@@ -286,6 +287,13 @@ export class UploadFormComponent implements OnInit {
return name.endsWith('.stl');
}
isSupportedFile(file: File | null): boolean {
if (!file) return false;
const name = file.name.toLowerCase().trim();
return this.allowedExtensions.some((ext) => name.endsWith(`.${ext}`));
}
canPreviewSelectedFile(): boolean {
return this.isStlFile(this.getSelectedPreviewFile());
}
@@ -340,13 +348,19 @@ export class UploadFormComponent implements OnInit {
onFilesDropped(newFiles: File[]) {
const MAX_SIZE = 200 * 1024 * 1024;
const validItems: FormItem[] = [];
let hasError = false;
let hasInvalidType = false;
let hasOversize = false;
const defaults = this.getCurrentGlobalItemDefaults();
for (const file of newFiles) {
if (!this.isSupportedFile(file)) {
hasInvalidType = true;
continue;
}
if (file.size > MAX_SIZE) {
hasError = true;
hasOversize = true;
continue;
}
@@ -367,7 +381,11 @@ export class UploadFormComponent implements OnInit {
});
}
if (hasError) {
if (hasInvalidType) {
alert(this.translate.instant('CALC.ERR_INVALID_FILE_TYPE'));
}
if (hasOversize) {
alert(this.translate.instant('CALC.ERR_FILE_TOO_LARGE'));
}