style: apply prettier formatting

This commit is contained in:
printcalc-ci
2026-03-03 11:46:26 +00:00
parent dd6f723271
commit 20293cc044
131 changed files with 5674 additions and 3482 deletions

View File

@@ -1,9 +1,17 @@
<div class="alert" [ngClass]="type()">
<div class="icon">
@if(type() === 'info') { }
@if(type() === 'warning') { ⚠️ }
@if(type() === 'error') { ❌ }
@if(type() === 'success') { ✅ }
@if (type() === "info") {
}
@if (type() === "warning") {
⚠️
}
@if (type() === "error") {
}
@if (type() === "success") {
}
</div>
<div class="content"><ng-content></ng-content></div>
</div>

View File

@@ -6,7 +6,22 @@
font-size: 0.875rem;
margin-bottom: var(--space-4);
}
.info { background: var(--color-neutral-100); color: var(--color-neutral-800); }
.warning { background: #fefce8; color: #854d0e; border: 1px solid #fde047; }
.error { background: #fef2f2; color: #991b1b; border: 1px solid #fecaca; }
.success { background: #f0fdf4; color: #166534; border: 1px solid #bbf7d0; }
.info {
background: var(--color-neutral-100);
color: var(--color-neutral-800);
}
.warning {
background: #fefce8;
color: #854d0e;
border: 1px solid #fde047;
}
.error {
background: #fef2f2;
color: #991b1b;
border: 1px solid #fecaca;
}
.success {
background: #f0fdf4;
color: #166534;
border: 1px solid #bbf7d0;
}

View File

@@ -6,7 +6,7 @@ import { CommonModule } from '@angular/common';
standalone: true,
imports: [CommonModule],
templateUrl: './app-alert.component.html',
styleUrl: './app-alert.component.scss'
styleUrl: './app-alert.component.scss',
})
export class AppAlertComponent {
type = input<'info' | 'warning' | 'error' | 'success'>('info');

View File

@@ -1,7 +1,8 @@
<button
[type]="type()"
<button
[type]="type()"
[class]="'btn btn-' + variant() + ' ' + (fullWidth() ? 'w-full' : '')"
[disabled]="disabled()"
(click)="handleClick($event)">
(click)="handleClick($event)"
>
<ng-content></ng-content>
</button>

View File

@@ -6,28 +6,37 @@
border-radius: var(--radius-md);
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s, color 0.2s, border-color 0.2s;
transition:
background-color 0.2s,
color 0.2s,
border-color 0.2s;
border: 1px solid transparent;
font-family: inherit;
font-size: 1rem;
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.w-full { width: 100%; }
.w-full {
width: 100%;
}
.btn-primary {
background-color: var(--color-brand);
color: var(--color-neutral-900);
&:hover:not(:disabled) { background-color: var(--color-brand-hover); }
&:hover:not(:disabled) {
background-color: var(--color-brand-hover);
}
}
.btn-secondary {
background-color: var(--color-neutral-200);
color: var(--color-neutral-900);
&:hover:not(:disabled) { background-color: var(--color-neutral-300); }
&:hover:not(:disabled) {
background-color: var(--color-neutral-300);
}
}
.btn-outline {
@@ -37,8 +46,8 @@
padding: calc(0.5rem - 1px) calc(1rem - 1px);
color: var(--color-neutral-900);
font-weight: 600;
&:hover:not(:disabled) {
background-color: var(--color-brand);
&:hover:not(:disabled) {
background-color: var(--color-brand);
color: var(--color-neutral-900);
}
}
@@ -47,5 +56,7 @@
background-color: transparent;
color: var(--color-text-muted);
padding: 0.5rem;
&:hover:not(:disabled) { color: var(--color-text); }
&:hover:not(:disabled) {
color: var(--color-text);
}
}

View File

@@ -6,7 +6,7 @@ import { CommonModule } from '@angular/common';
standalone: true,
imports: [CommonModule],
templateUrl: './app-button.component.html',
styleUrl: './app-button.component.scss'
styleUrl: './app-button.component.scss',
})
export class AppButtonComponent {
variant = input<'primary' | 'secondary' | 'outline' | 'text'>('primary');

View File

@@ -9,10 +9,13 @@
border: 1px solid var(--color-border);
box-shadow: var(--shadow-sm);
padding: var(--space-6);
transition: box-shadow 0.2s ease, transform 0.2s ease, border-color 0.2s ease;
transition:
box-shadow 0.2s ease,
transform 0.2s ease,
border-color 0.2s ease;
height: 100%;
box-sizing: border-box;
&:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-md);

View File

@@ -4,6 +4,6 @@ import { Component } from '@angular/core';
selector: 'app-card',
standalone: true,
templateUrl: './app-card.component.html',
styleUrl: './app-card.component.scss'
styleUrl: './app-card.component.scss',
})
export class AppCardComponent {}

View File

@@ -1,4 +1,4 @@
<div
<div
class="dropzone"
[class.dragover]="isDragOver()"
(dragover)="onDragOver($event)"
@@ -6,21 +6,44 @@
(drop)="onDrop($event)"
(click)="fileInput.click()"
>
<input #fileInput type="file" (change)="onFileSelected($event)" hidden [accept]="accept()" [multiple]="multiple()">
<input
#fileInput
type="file"
(change)="onFileSelected($event)"
hidden
[accept]="accept()"
[multiple]="multiple()"
/>
<div class="content">
<div class="icon">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-upload-cloud"><polyline points="16 16 12 12 8 16"></polyline><line x1="12" y1="12" x2="12" y2="21"></line><path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"></path><polyline points="16 16 12 12 8 16"></polyline></svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="feather feather-upload-cloud"
>
<polyline points="16 16 12 12 8 16"></polyline>
<line x1="12" y1="12" x2="12" y2="21"></line>
<path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"></path>
<polyline points="16 16 12 12 8 16"></polyline>
</svg>
</div>
<p class="text">{{ label() | translate }}</p>
<p class="subtext">{{ subtext() | translate }}</p>
@if (fileNames().length > 0) {
<div class="file-badges">
@for (name of fileNames(); track name) {
<div class="file-badge">{{ name }}</div>
}
</div>
<div class="file-badges">
@for (name of fileNames(); track name) {
<div class="file-badge">{{ name }}</div>
}
</div>
}
</div>
</div>

View File

@@ -6,27 +6,37 @@
cursor: pointer;
transition: all 0.2s;
background-color: var(--color-neutral-50);
&:hover, &.dragover {
&:hover,
&.dragover {
border-color: var(--color-brand);
background-color: var(--color-neutral-100);
}
}
.icon { color: var(--color-brand); margin-bottom: var(--space-4); }
.text { font-weight: 600; margin-bottom: var(--space-2); }
.subtext { font-size: 0.875rem; color: var(--color-text-muted); }
.icon {
color: var(--color-brand);
margin-bottom: var(--space-4);
}
.text {
font-weight: 600;
margin-bottom: var(--space-2);
}
.subtext {
font-size: 0.875rem;
color: var(--color-text-muted);
}
.file-badges {
margin-top: var(--space-4);
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
justify-content: center;
margin-top: var(--space-4);
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
justify-content: center;
}
.file-badge {
padding: var(--space-2) var(--space-4);
background: var(--color-neutral-200);
border-radius: var(--radius-md);
font-weight: 600;
color: var(--color-primary-700);
font-size: 0.85rem;
padding: var(--space-2) var(--space-4);
background: var(--color-neutral-200);
border-radius: var(--radius-md);
font-weight: 600;
color: var(--color-primary-700);
font-size: 0.85rem;
}

View File

@@ -7,16 +7,16 @@ import { TranslateModule } from '@ngx-translate/core';
standalone: true,
imports: [CommonModule, TranslateModule],
templateUrl: './app-dropzone.component.html',
styleUrl: './app-dropzone.component.scss'
styleUrl: './app-dropzone.component.scss',
})
export class AppDropzoneComponent {
label = input<string>('DROPZONE.DEFAULT_LABEL');
subtext = input<string>('DROPZONE.DEFAULT_SUBTEXT');
accept = input<string>('.stl,.3mf,.step,.stp');
multiple = input<boolean>(true);
filesDropped = output<File[]>();
isDragOver = signal(false);
fileNames = signal<string[]>([]);
@@ -51,8 +51,8 @@ export class AppDropzoneComponent {
}
handleFiles(files: File[]) {
const newNames = files.map(f => f.name);
this.fileNames.update(current => [...current, ...newNames]);
const newNames = files.map((f) => f.name);
this.fileNames.update((current) => [...current, ...newNames]);
this.filesDropped.emit(files);
}
}

View File

@@ -1,9 +1,11 @@
<div class="form-group">
@if (label()) {
@if (label()) {
<label [for]="id()">
{{ label() }}
@if (required()) { <span class="required-mark">*</span> }
</label>
@if (required()) {
<span class="required-mark">*</span>
}
</label>
}
<input
[id]="id()"
@@ -15,5 +17,7 @@
[disabled]="disabled"
class="form-control"
/>
@if (error()) { <span class="error-text">{{ error() }}</span> }
@if (error()) {
<span class="error-text">{{ error() }}</span>
}
</div>

View File

@@ -1,6 +1,18 @@
.form-group { display: flex; flex-direction: column; margin-bottom: var(--space-4); }
label { font-size: 0.875rem; font-weight: 500; margin-bottom: var(--space-2); color: var(--color-text); }
.required-mark { color: var(--color-text); margin-left: 2px; }
.form-group {
display: flex;
flex-direction: column;
margin-bottom: var(--space-4);
}
label {
font-size: 0.875rem;
font-weight: 500;
margin-bottom: var(--space-2);
color: var(--color-text);
}
.required-mark {
color: var(--color-text);
margin-left: 2px;
}
.form-control {
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-border);
@@ -9,7 +21,18 @@ label { font-size: 0.875rem; font-weight: 500; margin-bottom: var(--space-2); co
width: 100%;
background: var(--color-bg-card);
color: var(--color-text);
&:focus { outline: none; border-color: var(--color-brand); box-shadow: 0 0 0 2px rgba(250, 207, 10, 0.25); }
&:disabled { background: var(--color-neutral-100); cursor: not-allowed; }
&:focus {
outline: none;
border-color: var(--color-brand);
box-shadow: 0 0 0 2px rgba(250, 207, 10, 0.25);
}
&:disabled {
background: var(--color-neutral-100);
cursor: not-allowed;
}
}
.error-text {
color: var(--color-danger-500);
font-size: 0.75rem;
margin-top: var(--space-1);
}
.error-text { color: var(--color-danger-500); font-size: 0.75rem; margin-top: var(--space-1); }

View File

@@ -1,5 +1,9 @@
import { Component, input, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
ReactiveFormsModule,
} from '@angular/forms';
import { CommonModule } from '@angular/common';
@Component({
@@ -10,11 +14,11 @@ import { CommonModule } from '@angular/common';
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AppInputComponent),
multi: true
}
multi: true,
},
],
templateUrl: './app-input.component.html',
styleUrl: './app-input.component.scss'
styleUrl: './app-input.component.scss',
})
export class AppInputComponent implements ControlValueAccessor {
label = input<string>('');
@@ -30,11 +34,19 @@ export class AppInputComponent implements ControlValueAccessor {
onChange: any = () => {};
onTouched: any = () => {};
writeValue(obj: any): void { this.value = obj || ''; }
registerOnChange(fn: any): void { this.onChange = fn; }
registerOnTouched(fn: any): void { this.onTouched = fn; }
setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; }
writeValue(obj: any): void {
this.value = obj || '';
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
onInput(event: Event) {
const val = (event.target as HTMLInputElement).value;
this.value = val;

View File

@@ -1,8 +1,8 @@
<section class="locations-section">
<div class="container">
<div class="section-header">
<h2>{{ 'LOCATIONS.TITLE' | translate }}</h2>
<p class="subtitle">{{ 'LOCATIONS.SUBTITLE' | translate }}</p>
<h2>{{ "LOCATIONS.TITLE" | translate }}</h2>
<p class="subtitle">{{ "LOCATIONS.SUBTITLE" | translate }}</p>
</div>
<div class="locations-grid">
@@ -11,23 +11,24 @@
<app-toggle-selector
[options]="locationOptions"
[selectedValue]="selectedLocation"
(selectionChange)="selectLocation($event)">
(selectionChange)="selectLocation($event)"
>
</app-toggle-selector>
</div>
<div class="location-details">
<div *ngIf="selectedLocation === 'ticino'" class="details-card">
<h3>{{ 'LOCATIONS.BIASCA' | translate }}</h3>
<p>{{ 'LOCATIONS.ADDRESS_TICINO' | translate }}</p>
<h3>{{ "LOCATIONS.BIASCA" | translate }}</h3>
<p>{{ "LOCATIONS.ADDRESS_TICINO" | translate }}</p>
</div>
<div *ngIf="selectedLocation === 'bienne'" class="details-card">
<h3>{{ 'LOCATIONS.BIENNE' | translate }}</h3>
<p>{{ 'LOCATIONS.ADDRESS_BIENNE' | translate }}</p>
<h3>{{ "LOCATIONS.BIENNE" | translate }}</h3>
<p>{{ "LOCATIONS.ADDRESS_BIENNE" | translate }}</p>
</div>
<div class="actions">
<a routerLink="/contact" class="contact-btn">
{{ 'LOCATIONS.CONTACT_US' | translate }}
{{ "LOCATIONS.CONTACT_US" | translate }}
</a>
</div>
</div>
@@ -37,12 +38,24 @@
<iframe
*ngIf="selectedLocation === 'ticino'"
src="https://www.google.com/maps?q=Via%20G.%20Pioda%2029a%2C%20Biasca&output=embed"
width="100%" height="450" style="border:0;" allowfullscreen="" loading="lazy" referrerpolicy="no-referrer-when-downgrade">
width="100%"
height="450"
style="border: 0"
allowfullscreen=""
loading="lazy"
referrerpolicy="no-referrer-when-downgrade"
>
</iframe>
<iframe
*ngIf="selectedLocation === 'bienne'"
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2729.0438104193587!2d7.240752176735282!3d47.126435979155985!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x478e1eb84efba295%3A0x95924d5ba8b6f3b0!2sLyss-Strasse%2071%2C%202560%20Nidau%2C%20Switzerland!5e0!3m2!1sen!2sch!4v1700000000000!5m2!1sen!2sch"
width="100%" height="450" style="border:0;" allowfullscreen="" loading="lazy" referrerpolicy="no-referrer-when-downgrade">
width="100%"
height="450"
style="border: 0"
allowfullscreen=""
loading="lazy"
referrerpolicy="no-referrer-when-downgrade"
>
</iframe>
</div>
</div>

View File

@@ -28,7 +28,7 @@
gap: 3rem;
align-items: start;
@media(min-width: 992px) {
@media (min-width: 992px) {
grid-template-columns: repeat(2, minmax(320px, 420px));
justify-content: center;
}
@@ -44,7 +44,6 @@
width: 100%;
}
.location-details {
padding: 2rem;
background: var(--color-bg);

View File

@@ -2,14 +2,22 @@ import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { RouterLink } from '@angular/router';
import { AppToggleSelectorComponent, ToggleOption } from '../app-toggle-selector/app-toggle-selector.component';
import {
AppToggleSelectorComponent,
ToggleOption,
} from '../app-toggle-selector/app-toggle-selector.component';
@Component({
selector: 'app-locations',
standalone: true,
imports: [CommonModule, TranslateModule, RouterLink, AppToggleSelectorComponent],
imports: [
CommonModule,
TranslateModule,
RouterLink,
AppToggleSelectorComponent,
],
templateUrl: './app-locations.component.html',
styleUrl: './app-locations.component.scss'
styleUrl: './app-locations.component.scss',
})
export class AppLocationsComponent {
selectedLocation: 'ticino' | 'bienne' = 'ticino';

View File

@@ -1,5 +1,7 @@
<div class="form-group">
@if (label()) { <label [for]="id()">{{ label() }}</label> }
@if (label()) {
<label [for]="id()">{{ label() }}</label>
}
<select
[id]="id()"
[ngModel]="value"
@@ -12,5 +14,7 @@
<option [ngValue]="opt.value">{{ opt.label }}</option>
}
</select>
@if (error()) { <span class="error-text">{{ error() }}</span> }
@if (error()) {
<span class="error-text">{{ error() }}</span>
}
</div>

View File

@@ -1,5 +1,14 @@
.form-group { display: flex; flex-direction: column; margin-bottom: var(--space-4); }
label { font-size: 0.875rem; font-weight: 500; margin-bottom: var(--space-2); color: var(--color-text); }
.form-group {
display: flex;
flex-direction: column;
margin-bottom: var(--space-4);
}
label {
font-size: 0.875rem;
font-weight: 500;
margin-bottom: var(--space-2);
color: var(--color-text);
}
.form-control {
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-border);
@@ -8,6 +17,13 @@ label { font-size: 0.875rem; font-weight: 500; margin-bottom: var(--space-2); co
width: 100%;
background: var(--color-bg-card);
color: var(--color-text);
&:focus { outline: none; border-color: var(--color-brand); }
&:focus {
outline: none;
border-color: var(--color-brand);
}
}
.error-text {
color: var(--color-danger-500);
font-size: 0.75rem;
margin-top: var(--space-1);
}
.error-text { color: var(--color-danger-500); font-size: 0.75rem; margin-top: var(--space-1); }

View File

@@ -1,5 +1,10 @@
import { Component, input, output, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, ReactiveFormsModule, FormsModule } from '@angular/forms';
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
ReactiveFormsModule,
FormsModule,
} from '@angular/forms';
import { CommonModule } from '@angular/common';
@Component({
@@ -10,16 +15,16 @@ import { CommonModule } from '@angular/common';
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AppSelectComponent),
multi: true
}
multi: true,
},
],
templateUrl: './app-select.component.html',
styleUrl: './app-select.component.scss'
styleUrl: './app-select.component.scss',
})
export class AppSelectComponent implements ControlValueAccessor {
label = input<string>('');
id = input<string>('select-' + Math.random().toString(36).substr(2, 9));
options = input<{label: string, value: any}[]>([]);
options = input<{ label: string; value: any }[]>([]);
error = input<string | null>(null);
value: any = '';
@@ -28,13 +33,21 @@ export class AppSelectComponent implements ControlValueAccessor {
onChange: any = () => {};
onTouched: any = () => {};
writeValue(obj: any): void { this.value = obj; }
registerOnChange(fn: any): void { this.onChange = fn; }
registerOnTouched(fn: any): void { this.onTouched = fn; }
setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; }
writeValue(obj: any): void {
this.value = obj;
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
onModelChange(val: any) {
this.value = val;
this.onChange(val);
this.value = val;
this.onChange(val);
}
}

View File

@@ -1,9 +1,10 @@
<div class="tabs">
@for (tab of tabs(); track tab.value) {
<button
class="tab"
<button
class="tab"
[class.active]="activeTab() === tab.value"
(click)="selectTab(tab.value)">
(click)="selectTab(tab.value)"
>
{{ tab.label | translate }}
</button>
}

View File

@@ -12,8 +12,10 @@
color: var(--color-text-muted);
border-bottom: 2px solid transparent;
transition: all 0.2s;
&:hover { color: var(--color-text); }
&:hover {
color: var(--color-text);
}
&.active {
color: var(--color-brand);
border-bottom-color: var(--color-brand);

View File

@@ -7,10 +7,10 @@ import { TranslateModule } from '@ngx-translate/core';
standalone: true,
imports: [CommonModule, TranslateModule],
templateUrl: './app-tabs.component.html',
styleUrl: './app-tabs.component.scss'
styleUrl: './app-tabs.component.scss',
})
export class AppTabsComponent {
tabs = input<{label: string, value: string}[]>([]);
tabs = input<{ label: string; value: string }[]>([]);
activeTab = input<string>('');
tabChange = output<string>();

View File

@@ -1,8 +1,10 @@
<div class="user-type-selector">
@for (option of options(); track option.value) {
<div class="type-option"
[class.selected]="selectedValue() === option.value"
(click)="selectOption(option.value)">
<div
class="type-option"
[class.selected]="selectedValue() === option.value"
(click)="selectOption(option.value)"
>
{{ option.label | translate }}
</div>
}

View File

@@ -22,13 +22,15 @@
color: var(--color-text-muted);
transition: all 0.2s ease;
user-select: none;
&:hover { color: var(--color-text); }
&:hover {
color: var(--color-text);
}
&.selected {
background-color: var(--color-brand);
color: #000;
font-weight: 600;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
}

View File

@@ -12,12 +12,12 @@ export interface ToggleOption {
standalone: true,
imports: [CommonModule, TranslateModule],
templateUrl: './app-toggle-selector.component.html',
styleUrl: './app-toggle-selector.component.scss'
styleUrl: './app-toggle-selector.component.scss',
})
export class AppToggleSelectorComponent {
options = input.required<ToggleOption[]>();
selectedValue = input.required<any>();
selectionChange = output<any>();
selectOption(value: any) {

View File

@@ -1,14 +1,14 @@
<div class="color-selector-container">
@if (isOpen()) {
<div class="backdrop" (click)="close()"></div>
<div class="backdrop" (click)="close()"></div>
}
<div
class="color-circle trigger"
<div
class="color-circle trigger"
[style.background-color]="getCurrentHex()"
[title]="selectedColor()"
(click)="toggleOpen()">
</div>
(click)="toggleOpen()"
></div>
@if (isOpen()) {
<div class="color-popup">
@@ -17,17 +17,26 @@
<div class="category-name">{{ category.name | translate }}</div>
<div class="colors-grid">
@for (color of category.colors; track color.value) {
<div
class="color-item"
<div
class="color-item"
(click)="selectColor(color)"
[class.disabled]="color.outOfStock">
<div class="selection-ring"
[class.active]="selectedVariantId() ? selectedVariantId() === color.variantId : selectedColor() === color.value"
[class.out-of-stock]="color.outOfStock">
<div class="color-circle small" [style.background-color]="color.hex"></div>
[class.disabled]="color.outOfStock"
>
<div
class="selection-ring"
[class.active]="
selectedVariantId()
? selectedVariantId() === color.variantId
: selectedColor() === color.value
"
[class.out-of-stock]="color.outOfStock"
>
<div
class="color-circle small"
[style.background-color]="color.hex"
></div>
</div>
<span class="color-name">{{ color.label | translate }}</span>
</div>
}

View File

@@ -5,14 +5,14 @@
}
.backdrop {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 999;
background: transparent;
cursor: default;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 999;
background: transparent;
cursor: default;
}
.color-circle {
@@ -21,12 +21,14 @@
border-radius: 50%;
border: 2px solid #ddd;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
transition:
transform 0.2s,
box-shadow 0.2s;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
&.trigger:hover {
transform: scale(1.1);
box-shadow: 0 2px 4px rgba(0,0,0,0.15);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
}
&.small {
@@ -44,17 +46,17 @@
border: 1px solid #eee;
border-radius: 8px; /* Slightly tighter radius */
padding: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1000;
width: 230px; /* Increased size */
max-height: min(62vh, 360px);
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
// Little triangle arrow
&::before {
content: '';
content: "";
position: absolute;
top: -6px;
left: 8px; // Align arrow to left side near trigger
@@ -72,7 +74,7 @@
width: 280px; /* Provide enough width for touch targets */
max-width: 90vw; /* Safety constraint */
max-height: min(72vh, 420px);
box-shadow: 0 10px 25px rgba(0,0,0,0.2); /* Stronger shadow for modal feel */
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); /* Stronger shadow for modal feel */
/* Hide arrow on mobile since it's detached from trigger */
&::before {
@@ -89,7 +91,7 @@
.category {
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
@@ -117,15 +119,17 @@
gap: 4px;
cursor: pointer;
text-align: center;
&:hover .selection-ring {
transform: scale(1.1);
}
&.disabled {
cursor: not-allowed;
opacity: 0.6;
&:hover .selection-ring { transform: none; }
&:hover .selection-ring {
transform: none;
}
}
}
@@ -135,29 +139,29 @@
border: 2px solid transparent;
transition: all 0.2s;
padding: 2px; /* Space for ring */
&.active {
border-color: var(--color-brand, #facf0a);
box-shadow: 0 0 0 1px var(--color-brand, #facf0a);
}
&.out-of-stock::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 2px;
background: #cc0000;
transform: translate(-50%, -50%) rotate(-45deg);
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 2px;
background: #cc0000;
transform: translate(-50%, -50%) rotate(-45deg);
}
}
.color-name {
font-size: 0.65rem;
color: #444;
line-height: 1.1;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
font-size: 0.65rem;
color: #444;
line-height: 1.1;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}

View File

@@ -1,7 +1,12 @@
import { Component, input, output, signal, computed } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { PRODUCT_COLORS, getColorHex, ColorCategory, ColorOption } from '../../../core/constants/colors.const';
import {
PRODUCT_COLORS,
getColorHex,
ColorCategory,
ColorOption,
} from '../../../core/constants/colors.const';
import { VariantOption } from '../../../features/calculator/services/quote-estimator.service';
@Component({
@@ -9,7 +14,7 @@ import { VariantOption } from '../../../features/calculator/services/quote-estim
standalone: true,
imports: [CommonModule, TranslateModule],
templateUrl: './color-selector.component.html',
styleUrl: './color-selector.component.scss'
styleUrl: './color-selector.component.scss',
})
export class ColorSelectorComponent {
selectedColor = input<string>('Black');
@@ -20,32 +25,32 @@ export class ColorSelectorComponent {
isOpen = signal(false);
categories = computed(() => {
const vars = this.variants();
if (vars && vars.length > 0) {
const byFinish = new Map<string, ColorOption[]>();
vars.forEach(v => {
const finish = v.finishType || 'AVAILABLE_COLORS';
const bucket = byFinish.get(finish) || [];
bucket.push({
label: v.colorName,
value: v.colorName,
hex: v.hexColor,
variantId: v.id,
outOfStock: v.isOutOfStock
});
byFinish.set(finish, bucket);
});
const vars = this.variants();
if (vars && vars.length > 0) {
const byFinish = new Map<string, ColorOption[]>();
vars.forEach((v) => {
const finish = v.finishType || 'AVAILABLE_COLORS';
const bucket = byFinish.get(finish) || [];
bucket.push({
label: v.colorName,
value: v.colorName,
hex: v.hexColor,
variantId: v.id,
outOfStock: v.isOutOfStock,
});
byFinish.set(finish, bucket);
});
return Array.from(byFinish.entries()).map(([finish, colors]) => ({
name: finish,
colors
})) as ColorCategory[];
}
return PRODUCT_COLORS;
return Array.from(byFinish.entries()).map(([finish, colors]) => ({
name: finish,
colors,
})) as ColorCategory[];
}
return PRODUCT_COLORS;
});
toggleOpen() {
this.isOpen.update(v => !v);
this.isOpen.update((v) => !v);
}
selectColor(color: ColorOption) {
@@ -53,24 +58,24 @@ export class ColorSelectorComponent {
this.colorSelected.emit({
colorName: color.value,
filamentVariantId: color.variantId
filamentVariantId: color.variantId,
});
this.isOpen.set(false);
}
// Helper to find hex for the current selected value
getCurrentHex(): string {
// Check in dynamic variants first
const vars = this.variants();
if (vars && vars.length > 0) {
const found = vars.find(v => v.colorName === this.selectedColor());
if (found) return found.hexColor;
}
// Check in dynamic variants first
const vars = this.variants();
if (vars && vars.length > 0) {
const found = vars.find((v) => v.colorName === this.selectedColor());
if (found) return found.hexColor;
}
return getColorHex(this.selectedColor());
}
close() {
this.isOpen.set(false);
this.isOpen.set(false);
}
}

View File

@@ -2,12 +2,12 @@
@if (loading) {
<div class="loading-overlay">
<div class="spinner"></div>
<span>{{ 'STL_VIEWER.LOADING' | translate }}</span>
<span>{{ "STL_VIEWER.LOADING" | translate }}</span>
</div>
}
@if (file && !loading) {
<div class="dims-overlay">
{{ dimensions.x }} x {{ dimensions.y }} x {{ dimensions.z }} mm
{{ dimensions.x }} x {{ dimensions.y }} x {{ dimensions.z }} mm
</div>
}
</div>

View File

@@ -28,17 +28,19 @@
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
to {
transform: rotate(360deg);
}
}
.dims-overlay {
position: absolute;
bottom: 8px;
right: 8px;
background: rgba(0,0,0,0.6);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-family: monospace;
pointer-events: none;
position: absolute;
bottom: 8px;
right: 8px;
background: rgba(0, 0, 0, 0.6);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-family: monospace;
pointer-events: none;
}

View File

@@ -1,4 +1,13 @@
import { Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, ViewChild, SimpleChanges } from '@angular/core';
import {
Component,
ElementRef,
Input,
OnChanges,
OnDestroy,
OnInit,
ViewChild,
SimpleChanges,
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import * as THREE from 'three';
@@ -12,13 +21,14 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
standalone: true,
imports: [CommonModule, TranslateModule],
templateUrl: './stl-viewer.component.html',
styleUrl: './stl-viewer.component.scss'
styleUrl: './stl-viewer.component.scss',
})
export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
@Input() file: File | null = null;
@Input() color: string = '#facf0a'; // Default Brand Color
@ViewChild('rendererContainer', { static: true }) rendererContainer!: ElementRef;
@ViewChild('rendererContainer', { static: true })
rendererContainer!: ElementRef;
private scene!: THREE.Scene;
private camera!: THREE.PerspectiveCamera;
@@ -38,7 +48,7 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
if (changes['file'] && this.file) {
this.loadFile(this.file);
}
if (changes['color'] && this.currentMesh && !changes['file']) {
this.applyColorStyle(this.color);
}
@@ -83,7 +93,11 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
this.camera.position.z = 100;
// Renderer
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false, powerPreference: 'high-performance' });
this.renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: false,
powerPreference: 'high-performance',
});
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
this.renderer.outputColorSpace = THREE.SRGBColorSpace;
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
@@ -106,12 +120,12 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
// Handle resize
const resizeObserver = new ResizeObserver(() => {
if (!this.rendererContainer) return;
const w = this.rendererContainer.nativeElement.clientWidth;
const h = this.rendererContainer.nativeElement.clientHeight;
this.camera.aspect = w / h;
this.camera.updateProjectionMatrix();
this.renderer.setSize(w, h);
if (!this.rendererContainer) return;
const w = this.rendererContainer.nativeElement.clientWidth;
const h = this.rendererContainer.nativeElement.clientHeight;
this.camera.aspect = w / h;
this.camera.updateProjectionMatrix();
this.renderer.setSize(w, h);
});
resizeObserver.observe(this.rendererContainer.nativeElement);
}
@@ -126,7 +140,7 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
try {
const loader = new STLLoader();
const geometry = loader.parse(event.target?.result as ArrayBuffer);
this.clearCurrentMesh();
geometry.computeVertexNormals();
@@ -136,12 +150,12 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
roughness: 0.42,
metalness: 0.05,
emissive: 0x000000,
emissiveIntensity: 0
emissiveIntensity: 0,
});
this.currentMesh = new THREE.Mesh(geometry, material);
this.applyColorStyle(this.color);
// Center geometry
geometry.computeBoundingBox();
geometry.center();
@@ -150,11 +164,11 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
const boundingBox = geometry.boundingBox!;
const size = new THREE.Vector3();
boundingBox.getSize(size);
this.dimensions = {
x: Math.round(size.x * 10) / 10,
y: Math.round(size.y * 10) / 10,
z: Math.round(size.z * 10) / 10
x: Math.round(size.x * 10) / 10,
y: Math.round(size.y * 10) / 10,
z: Math.round(size.z * 10) / 10,
};
// Rotate to stand upright (usually necessary for STLs)
@@ -165,16 +179,15 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
// Adjust camera to fit object
const maxDim = Math.max(size.x, size.y, size.z);
const fov = this.camera.fov * (Math.PI / 180);
// Calculate distance towards camera (z-axis)
let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2));
cameraZ *= 1.72;
this.camera.position.set(cameraZ * 0.65, cameraZ * 0.95, cameraZ * 1.1);
this.camera.lookAt(0, 0, 0);
this.camera.updateProjectionMatrix();
this.controls.update();
} catch (err) {
console.error('Error loading STL:', err);
} finally {
@@ -186,14 +199,14 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
private animate() {
this.animationId = requestAnimationFrame(() => this.animate());
if (this.currentMesh && this.autoRotate) {
this.currentMesh.rotation.z += 0.0025;
}
if (this.controls) this.controls.update();
if (this.renderer && this.scene && this.camera) {
this.renderer.render(this.scene, this.camera);
this.renderer.render(this.scene, this.camera);
}
}

View File

@@ -1,26 +1,40 @@
<div class="success-state">
<div class="success-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>
</div>
@switch (context()) {
@case ('contact') {
<h3>{{ 'CONTACT.SUCCESS_TITLE' | translate }}</h3>
<p>{{ 'CONTACT.SUCCESS_DESC' | translate }}</p>
<app-button (click)="action.emit()">{{ 'CONTACT.SEND_ANOTHER' | translate }}</app-button>
@case ("contact") {
<h3>{{ "CONTACT.SUCCESS_TITLE" | translate }}</h3>
<p>{{ "CONTACT.SUCCESS_DESC" | translate }}</p>
<app-button (click)="action.emit()">{{
"CONTACT.SEND_ANOTHER" | translate
}}</app-button>
}
@case ('calc') {
<h3>{{ 'CALC.ORDER_SUCCESS_TITLE' | translate }}</h3>
<p>{{ 'CALC.ORDER_SUCCESS_DESC' | translate }}</p>
<app-button (click)="action.emit()">{{ 'CALC.NEW_QUOTE' | translate }}</app-button>
@case ("calc") {
<h3>{{ "CALC.ORDER_SUCCESS_TITLE" | translate }}</h3>
<p>{{ "CALC.ORDER_SUCCESS_DESC" | translate }}</p>
<app-button (click)="action.emit()">{{
"CALC.NEW_QUOTE" | translate
}}</app-button>
}
@case ('shop') {
<h3>{{ 'SHOP.SUCCESS_TITLE' | translate }}</h3>
<p>{{ 'SHOP.SUCCESS_DESC' | translate }}</p>
<app-button (click)="action.emit()">{{ 'SHOP.CONTINUE' | translate }}</app-button>
@case ("shop") {
<h3>{{ "SHOP.SUCCESS_TITLE" | translate }}</h3>
<p>{{ "SHOP.SUCCESS_DESC" | translate }}</p>
<app-button (click)="action.emit()">{{
"SHOP.CONTINUE" | translate
}}</app-button>
}
}
</div>

View File

@@ -13,7 +13,7 @@
height: 64px;
color: var(--color-success, #10b981);
margin-bottom: var(--space-2);
svg {
width: 100%;
height: 100%;

View File

@@ -10,7 +10,7 @@ export type SuccessContext = 'contact' | 'calc' | 'shop';
standalone: true,
imports: [CommonModule, TranslateModule, AppButtonComponent],
templateUrl: './success-state.component.html',
styleUrl: './success-state.component.scss'
styleUrl: './success-state.component.scss',
})
export class SuccessStateComponent {
context = input.required<SuccessContext>();

View File

@@ -6,7 +6,7 @@ import { CommonModule } from '@angular/common';
standalone: true,
imports: [CommonModule],
templateUrl: './summary-card.component.html',
styleUrl: './summary-card.component.scss'
styleUrl: './summary-card.component.scss',
})
export class SummaryCardComponent {
label = input.required<string>();