feat(animation logo)
This commit is contained in:
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user