feat/shop #33

Merged
JoeKung merged 4 commits from feat/shop into dev 2026-03-09 19:21:28 +01:00
25 changed files with 829 additions and 1043 deletions
Showing only changes of commit 225995c892 - Show all commits

View File

@@ -1,11 +1,18 @@
<section class="section-card"> <section class="section-card ui-section-card">
<header class="section-header"> <header class="section-header ui-section-header">
<div class="header-copy"> <div class="header-copy ui-section-header__copy">
<h2>Richieste di contatto</h2> <h2 class="ui-section-header__title">Richieste di contatto</h2>
<p>Richieste preventivo personalizzato ricevute dal sito.</p> <p class="ui-section-header__description">
<span class="total-pill">{{ requests.length }} richieste</span> Richieste preventivo personalizzato ricevute dal sito.
</p>
<span class="total-pill ui-pill">{{ requests.length }} richieste</span>
</div> </div>
<button type="button" (click)="loadRequests()" [disabled]="loading"> <button
type="button"
class="ui-button"
(click)="loadRequests()"
[disabled]="loading"
>
Aggiorna Aggiorna
</button> </button>
</header> </header>
@@ -13,11 +20,11 @@
<p class="error" *ngIf="errorMessage">{{ errorMessage }}</p> <p class="error" *ngIf="errorMessage">{{ errorMessage }}</p>
<p class="success" *ngIf="successMessage">{{ successMessage }}</p> <p class="success" *ngIf="successMessage">{{ successMessage }}</p>
<div class="workspace" *ngIf="!loading; else loadingTpl"> <div class="workspace ui-split-workspace" *ngIf="!loading; else loadingTpl">
<section class="list-panel"> <section class="list-panel">
<h3>Lista richieste</h3> <h3>Lista richieste</h3>
<div class="table-wrap"> <div class="table-wrap ui-table-wrap">
<table class="requests-table"> <table class="requests-table ui-data-table">
<thead> <thead>
<tr> <tr>
<th>Data</th> <th>Data</th>
@@ -50,14 +57,18 @@
</td> </td>
<td class="email-cell">{{ request.email }}</td> <td class="email-cell">{{ request.email }}</td>
<td> <td>
<span class="chip chip-neutral">{{ request.requestType }}</span> <span class="ui-status-chip ui-status-chip--neutral">{{
request.requestType
}}</span>
</td> </td>
<td> <td>
<span class="chip chip-light">{{ request.customerType }}</span> <span class="ui-status-chip ui-status-chip--neutral">{{
request.customerType
}}</span>
</td> </td>
<td> <td>
<span <span
class="chip" class="ui-status-chip"
[ngClass]="getStatusChipClass(request.status)" [ngClass]="getStatusChipClass(request.status)"
>{{ request.status }}</span >{{ request.status }}</span
> >
@@ -71,13 +82,14 @@
</div> </div>
</section> </section>
<section class="detail-panel" *ngIf="selectedRequest"> <section class="detail-panel ui-detail-panel" *ngIf="selectedRequest">
<header class="detail-header"> <header class="detail-header">
<div> <div>
<h3>Dettaglio richiesta</h3> <h3>Dettaglio richiesta</h3>
<p class="request-id"> <p class="request-id">
<span>ID</span> <span>ID</span>
<code <code
class="ui-code-pill"
[title]="selectedRequest.id" [title]="selectedRequest.id"
[appCopyOnClick]="selectedRequest.id" [appCopyOnClick]="selectedRequest.id"
>{{ selectedRequest.id }}</code >{{ selectedRequest.id }}</code
@@ -86,14 +98,14 @@
</div> </div>
<div class="detail-chips"> <div class="detail-chips">
<span <span
class="chip" class="ui-status-chip"
[ngClass]="getStatusChipClass(selectedRequest.status)" [ngClass]="getStatusChipClass(selectedRequest.status)"
>{{ selectedRequest.status }}</span >{{ selectedRequest.status }}</span
> >
<span class="chip chip-neutral">{{ <span class="ui-status-chip ui-status-chip--neutral">{{
selectedRequest.requestType selectedRequest.requestType
}}</span> }}</span>
<span class="chip chip-light">{{ <span class="ui-status-chip ui-status-chip--neutral">{{
selectedRequest.customerType selectedRequest.customerType
}}</span> }}</span>
</div> </div>
@@ -103,32 +115,32 @@
Caricamento dettaglio... Caricamento dettaglio...
</p> </p>
<dl class="meta-grid"> <dl class="meta-grid ui-meta-grid">
<div class="meta-item"> <div class="meta-item ui-meta-item">
<dt>Creata</dt> <dt>Creata</dt>
<dd>{{ selectedRequest.createdAt | date: "medium" }}</dd> <dd>{{ selectedRequest.createdAt | date: "medium" }}</dd>
</div> </div>
<div class="meta-item"> <div class="meta-item ui-meta-item">
<dt>Aggiornata</dt> <dt>Aggiornata</dt>
<dd>{{ selectedRequest.updatedAt | date: "medium" }}</dd> <dd>{{ selectedRequest.updatedAt | date: "medium" }}</dd>
</div> </div>
<div class="meta-item"> <div class="meta-item ui-meta-item">
<dt>Email</dt> <dt>Email</dt>
<dd>{{ selectedRequest.email }}</dd> <dd>{{ selectedRequest.email }}</dd>
</div> </div>
<div class="meta-item"> <div class="meta-item ui-meta-item">
<dt>Telefono</dt> <dt>Telefono</dt>
<dd>{{ selectedRequest.phone || "-" }}</dd> <dd>{{ selectedRequest.phone || "-" }}</dd>
</div> </div>
<div class="meta-item"> <div class="meta-item ui-meta-item">
<dt>Nome</dt> <dt>Nome</dt>
<dd>{{ selectedRequest.name || "-" }}</dd> <dd>{{ selectedRequest.name || "-" }}</dd>
</div> </div>
<div class="meta-item"> <div class="meta-item ui-meta-item">
<dt>Azienda</dt> <dt>Azienda</dt>
<dd>{{ selectedRequest.companyName || "-" }}</dd> <dd>{{ selectedRequest.companyName || "-" }}</dd>
</div> </div>
<div class="meta-item"> <div class="meta-item ui-meta-item">
<dt>Referente</dt> <dt>Referente</dt>
<dd>{{ selectedRequest.contactPerson || "-" }}</dd> <dd>{{ selectedRequest.contactPerson || "-" }}</dd>
</div> </div>
@@ -138,6 +150,7 @@
<div class="status-editor-field"> <div class="status-editor-field">
<label for="contact-request-status">Stato richiesta</label> <label for="contact-request-status">Stato richiesta</label>
<select <select
class="ui-form-control"
id="contact-request-status" id="contact-request-status"
[ngModel]="selectedStatus" [ngModel]="selectedStatus"
(ngModelChange)="selectedStatus = $event" (ngModelChange)="selectedStatus = $event"
@@ -148,6 +161,7 @@
</select> </select>
</div> </div>
<button <button
class="ui-button"
type="button" type="button"
(click)="updateRequestStatus()" (click)="updateRequestStatus()"
[disabled]=" [disabled]="
@@ -190,7 +204,7 @@
</div> </div>
<button <button
type="button" type="button"
class="ghost" class="ui-button ui-button--ghost"
(click)="downloadAttachment(attachment)" (click)="downloadAttachment(attachment)"
> >
Scarica file Scarica file
@@ -200,7 +214,10 @@
</div> </div>
</section> </section>
<section class="detail-panel empty" *ngIf="!selectedRequest"> <section
class="detail-panel ui-detail-panel ui-detail-panel--empty"
*ngIf="!selectedRequest"
>
<h3>Nessuna richiesta selezionata</h3> <h3>Nessuna richiesta selezionata</h3>
<p>Seleziona una riga dalla lista per vedere il dettaglio.</p> <p>Seleziona una riga dalla lista per vedere il dettaglio.</p>
</section> </section>

View File

@@ -4,23 +4,6 @@
gap: var(--space-5); gap: var(--space-5);
} }
.section-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: var(--space-4);
}
.section-header h2 {
margin: 0;
font-size: 1.4rem;
}
.section-header p {
margin: var(--space-2) 0 0;
color: var(--color-text-muted);
}
.header-copy { .header-copy {
display: grid; display: grid;
gap: var(--space-1); gap: var(--space-1);
@@ -29,21 +12,6 @@
.total-pill { .total-pill {
width: fit-content; width: fit-content;
margin-top: var(--space-1); margin-top: var(--space-1);
border-radius: 999px;
border: 1px solid var(--color-border);
background: var(--color-neutral-100);
color: var(--color-text-muted);
font-size: 0.78rem;
font-weight: 700;
line-height: 1;
padding: 6px 10px;
}
.workspace {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-4);
align-items: start;
} }
.list-panel, .list-panel,
@@ -51,72 +19,11 @@
min-width: 0; min-width: 0;
} }
button {
border: 0;
border-radius: var(--radius-md);
background: var(--color-brand);
color: var(--color-neutral-900);
padding: var(--space-2) var(--space-4);
font-weight: 600;
cursor: pointer;
transition:
background-color 0.2s ease,
opacity 0.2s ease;
line-height: 1.2;
}
button:hover:not(:disabled) {
background: var(--color-brand-hover);
}
button.ghost {
background: var(--color-bg-card);
border: 1px solid var(--color-border);
color: var(--color-text);
}
.list-panel h3 { .list-panel h3 {
margin: 0 0 var(--space-2); margin: 0 0 var(--space-2);
font-size: 1.02rem; font-size: 1.02rem;
} }
.table-wrap {
overflow: auto;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-bg-card);
max-height: 72vh;
}
table {
width: 100%;
border-collapse: collapse;
min-width: 760px;
}
thead {
position: sticky;
top: 0;
z-index: 1;
background: var(--color-neutral-100);
}
th,
td {
text-align: left;
padding: var(--space-3);
border-bottom: 1px solid var(--color-border);
font-size: 0.92rem;
vertical-align: top;
}
th {
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--color-text-muted);
}
.name-cell .primary { .name-cell .primary {
margin: 0; margin: 0;
font-weight: 600; font-weight: 600;
@@ -135,11 +42,6 @@ th {
tbody tr { tbody tr {
cursor: pointer; cursor: pointer;
transition: background-color 0.15s ease;
}
tbody tr:hover {
background: #fff9d9;
} }
tbody tr.selected { tbody tr.selected {
@@ -154,27 +56,6 @@ tbody tr.selected {
background: transparent; background: transparent;
} }
.detail-panel {
display: grid;
gap: var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-bg-card);
padding: var(--space-4);
min-height: 500px;
}
.detail-panel.empty {
display: grid;
align-content: center;
justify-items: center;
text-align: center;
}
.detail-panel.empty h3 {
margin: 0 0 var(--space-2);
}
.detail-header { .detail-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -210,11 +91,6 @@ tbody tr.selected {
text-overflow: clip; text-overflow: clip;
white-space: normal; white-space: normal;
overflow-wrap: anywhere; overflow-wrap: anywhere;
color: var(--color-text);
background: var(--color-neutral-100);
border: 1px solid var(--color-border);
border-radius: 7px;
padding: 3px 8px;
} }
.loading-detail { .loading-detail {
@@ -223,28 +99,12 @@ tbody tr.selected {
font-size: 0.85rem; font-size: 0.85rem;
} }
.meta-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
gap: var(--space-2);
margin: 0;
}
.meta-item { .meta-item {
margin: 0; margin: 0;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-3);
background: var(--color-neutral-100);
display: grid;
gap: 4px;
} }
.meta-item dt { .meta-item dt {
margin: 0; margin: 0;
font-size: 0.78rem;
font-weight: 700;
color: var(--color-text-muted);
} }
.meta-item dd { .meta-item dd {
@@ -323,7 +183,7 @@ tbody tr.selected {
.status-editor { .status-editor {
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--radius-md); border-radius: var(--radius-md);
background: var(--color-neutral-100); background: var(--color-surface-muted);
padding: var(--space-3); padding: var(--space-3);
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -343,79 +203,7 @@ tbody tr.selected {
color: var(--color-text-muted); color: var(--color-text-muted);
} }
.status-editor-field select {
width: 100%;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-2) var(--space-3);
background: var(--color-bg-card);
font: inherit;
}
.chip {
display: inline-flex;
align-items: center;
border-radius: 999px;
border: 1px solid transparent;
font-size: 0.74rem;
font-weight: 700;
letter-spacing: 0.02em;
line-height: 1;
padding: 5px 9px;
text-transform: uppercase;
}
.chip-neutral {
background: #e9f4ff;
border-color: #c8def4;
color: #1e4d78;
}
.chip-light {
background: #f4f5f8;
border-color: #dde1e8;
color: #4a5567;
}
.chip-warning {
background: #fff4cd;
border-color: #f7dd85;
color: #684b00;
}
.chip-success {
background: #dff6ea;
border-color: #b6e2cb;
color: #14543a;
}
.chip-danger {
background: #fde4e2;
border-color: #f3c0ba;
color: #812924;
}
button:disabled {
opacity: 0.68;
cursor: default;
}
@media (min-width: 1460px) {
.workspace {
grid-template-columns: minmax(0, 1.2fr) minmax(340px, 0.9fr);
}
}
@media (max-width: 760px) { @media (max-width: 760px) {
.section-card {
gap: var(--space-4);
}
.section-header {
flex-direction: column;
align-items: stretch;
}
.detail-header { .detail-header {
flex-direction: column; flex-direction: column;
} }
@@ -444,8 +232,8 @@ button:disabled {
} }
@media (max-width: 520px) { @media (max-width: 520px) {
th, .ui-data-table th,
td { .ui-data-table td {
padding: var(--space-2); padding: var(--space-2);
font-size: 0.86rem; font-size: 0.86rem;
} }

View File

@@ -118,15 +118,15 @@ export class AdminContactRequestsComponent implements OnInit {
getStatusChipClass(status?: string): string { getStatusChipClass(status?: string): string {
const normalized = (status || '').trim().toUpperCase(); const normalized = (status || '').trim().toUpperCase();
if (['PENDING', 'NEW', 'OPEN', 'IN_PROGRESS'].includes(normalized)) { if (['PENDING', 'NEW', 'OPEN', 'IN_PROGRESS'].includes(normalized)) {
return 'chip-warning'; return 'ui-status-chip--warning';
} }
if (['DONE', 'COMPLETED', 'RESOLVED', 'CLOSED'].includes(normalized)) { if (['DONE', 'COMPLETED', 'RESOLVED', 'CLOSED'].includes(normalized)) {
return 'chip-success'; return 'ui-status-chip--success';
} }
if (['REJECTED', 'FAILED', 'ERROR', 'SPAM'].includes(normalized)) { if (['REJECTED', 'FAILED', 'ERROR', 'SPAM'].includes(normalized)) {
return 'chip-danger'; return 'ui-status-chip--danger';
} }
return 'chip-light'; return 'ui-status-chip--neutral';
} }
updateRequestStatus(): void { updateRequestStatus(): void {

View File

@@ -1,11 +1,18 @@
<section class="admin-dashboard"> <section class="admin-dashboard ui-section-card">
<header class="dashboard-header"> <header class="dashboard-header ui-section-header">
<div> <div class="ui-section-header__copy">
<h1>Ordini</h1> <h1 class="ui-section-header__title">Ordini</h1>
<p>Seleziona un ordine a sinistra e gestiscilo nel dettaglio a destra.</p> <p class="ui-section-header__description">
Seleziona un ordine a sinistra e gestiscilo nel dettaglio a destra.
</p>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<button type="button" (click)="loadOrders()" [disabled]="loading"> <button
type="button"
class="ui-button"
(click)="loadOrders()"
[disabled]="loading"
>
Aggiorna Aggiorna
</button> </button>
</div> </div>
@@ -13,13 +20,14 @@
<p class="error" *ngIf="errorMessage">{{ errorMessage }}</p> <p class="error" *ngIf="errorMessage">{{ errorMessage }}</p>
<div class="workspace" *ngIf="!loading; else loadingTpl"> <div class="workspace ui-split-workspace" *ngIf="!loading; else loadingTpl">
<section class="list-panel"> <section class="list-panel">
<h2>Lista ordini</h2> <h2>Lista ordini</h2>
<div class="list-toolbar"> <div class="list-toolbar">
<label class="toolbar-field" for="order-search"> <label class="toolbar-field" for="order-search">
<span>Cerca UUID</span> <span>Cerca UUID</span>
<input <input
class="ui-form-control"
id="order-search" id="order-search"
type="search" type="search"
[ngModel]="orderSearchTerm" [ngModel]="orderSearchTerm"
@@ -30,6 +38,7 @@
<label class="toolbar-field" for="payment-status-filter"> <label class="toolbar-field" for="payment-status-filter">
<span>Stato pagamento</span> <span>Stato pagamento</span>
<select <select
class="ui-form-control"
id="payment-status-filter" id="payment-status-filter"
[ngModel]="paymentStatusFilter" [ngModel]="paymentStatusFilter"
(ngModelChange)="onPaymentStatusFilterChange($event)" (ngModelChange)="onPaymentStatusFilterChange($event)"
@@ -45,6 +54,7 @@
<label class="toolbar-field" for="order-status-filter"> <label class="toolbar-field" for="order-status-filter">
<span>Stato ordine</span> <span>Stato ordine</span>
<select <select
class="ui-form-control"
id="order-status-filter" id="order-status-filter"
[ngModel]="orderStatusFilter" [ngModel]="orderStatusFilter"
(ngModelChange)="onOrderStatusFilterChange($event)" (ngModelChange)="onOrderStatusFilterChange($event)"
@@ -58,8 +68,8 @@
</select> </select>
</label> </label>
</div> </div>
<div class="table-wrap"> <div class="table-wrap ui-table-wrap">
<table> <table class="ui-data-table">
<thead> <thead>
<tr> <tr>
<th>Ordine</th> <th>Ordine</th>
@@ -93,12 +103,13 @@
</div> </div>
</section> </section>
<section class="detail-panel" *ngIf="selectedOrder"> <section class="detail-panel ui-detail-panel" *ngIf="selectedOrder">
<div class="detail-header"> <div class="detail-header">
<h2>Dettaglio ordine {{ selectedOrder.orderNumber }}</h2> <h2>Dettaglio ordine {{ selectedOrder.orderNumber }}</h2>
<p class="order-uuid"> <p class="order-uuid">
UUID: UUID:
<code <code
class="ui-code-pill"
[title]="selectedOrder.id" [title]="selectedOrder.id"
[appCopyOnClick]="selectedOrder.id" [appCopyOnClick]="selectedOrder.id"
>{{ selectedOrder.id }}</code >{{ selectedOrder.id }}</code
@@ -107,18 +118,18 @@
<p *ngIf="detailLoading">Caricamento dettaglio...</p> <p *ngIf="detailLoading">Caricamento dettaglio...</p>
</div> </div>
<div class="meta-grid"> <div class="meta-grid ui-meta-grid">
<div> <div class="ui-meta-item">
<strong>Cliente</strong><span>{{ selectedOrder.customerEmail }}</span> <strong>Cliente</strong><span>{{ selectedOrder.customerEmail }}</span>
</div> </div>
<div> <div class="ui-meta-item">
<strong>Stato pagamento</strong <strong>Stato pagamento</strong
><span>{{ selectedOrder.paymentStatus || "PENDING" }}</span> ><span>{{ selectedOrder.paymentStatus || "PENDING" }}</span>
</div> </div>
<div> <div class="ui-meta-item">
<strong>Stato ordine</strong><span>{{ selectedOrder.status }}</span> <strong>Stato ordine</strong><span>{{ selectedOrder.status }}</span>
</div> </div>
<div> <div class="ui-meta-item">
<strong>Totale</strong <strong>Totale</strong
><span>{{ ><span>{{
selectedOrder.totalChf | currency: "CHF" : "symbol" : "1.2-2" selectedOrder.totalChf | currency: "CHF" : "symbol" : "1.2-2"
@@ -130,6 +141,7 @@
<div class="status-editor"> <div class="status-editor">
<label for="order-status">Stato ordine</label> <label for="order-status">Stato ordine</label>
<select <select
class="ui-form-control"
id="order-status" id="order-status"
[value]="selectedStatus" [value]="selectedStatus"
(change)="onStatusChange($event)" (change)="onStatusChange($event)"
@@ -139,6 +151,7 @@
</option> </option>
</select> </select>
<button <button
class="ui-button"
type="button" type="button"
(click)="updateStatus()" (click)="updateStatus()"
[disabled]="updatingStatus" [disabled]="updatingStatus"
@@ -150,6 +163,7 @@
<div class="status-editor"> <div class="status-editor">
<label for="payment-method">Metodo pagamento</label> <label for="payment-method">Metodo pagamento</label>
<select <select
class="ui-form-control"
id="payment-method" id="payment-method"
[value]="selectedPaymentMethod" [value]="selectedPaymentMethod"
(change)="onPaymentMethodChange($event)" (change)="onPaymentMethodChange($event)"
@@ -162,6 +176,7 @@
</option> </option>
</select> </select>
<button <button
class="ui-button"
type="button" type="button"
(click)="updatePaymentMethod()" (click)="updatePaymentMethod()"
[disabled]="confirmingPayment" [disabled]="confirmingPayment"
@@ -174,13 +189,25 @@
</div> </div>
<div class="doc-actions"> <div class="doc-actions">
<button type="button" class="ghost" (click)="downloadConfirmation()"> <button
type="button"
class="ui-button ui-button--ghost"
(click)="downloadConfirmation()"
>
Scarica conferma + QR bill Scarica conferma + QR bill
</button> </button>
<button type="button" class="ghost" (click)="downloadInvoice()"> <button
type="button"
class="ui-button ui-button--ghost"
(click)="downloadInvoice()"
>
Scarica fattura Scarica fattura
</button> </button>
<button type="button" class="ghost" (click)="openPrintDetails()"> <button
type="button"
class="ui-button ui-button--ghost"
(click)="openPrintDetails()"
>
Dettagli stampa Dettagli stampa
</button> </button>
</div> </div>
@@ -215,7 +242,7 @@
</div> </div>
<button <button
type="button" type="button"
class="ghost" class="ui-button ui-button--ghost"
(click)="downloadItemFile(item.id, item.originalFilename)" (click)="downloadItemFile(item.id, item.originalFilename)"
> >
Scarica file Scarica file
@@ -224,7 +251,10 @@
</div> </div>
</section> </section>
<section class="detail-panel empty" *ngIf="!selectedOrder"> <section
class="detail-panel ui-detail-panel ui-detail-panel--empty"
*ngIf="!selectedOrder"
>
<h2>Nessun ordine selezionato</h2> <h2>Nessun ordine selezionato</h2>
<p>Seleziona un ordine dalla lista per vedere i dettagli.</p> <p>Seleziona un ordine dalla lista per vedere i dettagli.</p>
</section> </section>
@@ -245,39 +275,39 @@
<h3>Dettagli stampa ordine {{ selectedOrder.orderNumber }}</h3> <h3>Dettagli stampa ordine {{ selectedOrder.orderNumber }}</h3>
<button <button
type="button" type="button"
class="ghost close-btn" class="ui-button ui-button--ghost close-btn"
(click)="closePrintDetails()" (click)="closePrintDetails()"
> >
Chiudi Chiudi
</button> </button>
</header> </header>
<div class="modal-grid"> <div class="modal-grid ui-meta-grid">
<div> <div class="ui-meta-item">
<strong>Qualità</strong <strong>Qualità</strong
><span>{{ getQualityLabel(selectedOrder.printLayerHeightMm) }}</span> ><span>{{ getQualityLabel(selectedOrder.printLayerHeightMm) }}</span>
</div> </div>
<div> <div class="ui-meta-item">
<strong>Materiale</strong <strong>Materiale</strong
><span>{{ selectedOrder.printMaterialCode || "-" }}</span> ><span>{{ selectedOrder.printMaterialCode || "-" }}</span>
</div> </div>
<div> <div class="ui-meta-item">
<strong>Layer height</strong <strong>Layer height</strong
><span>{{ selectedOrder.printLayerHeightMm || "-" }} mm</span> ><span>{{ selectedOrder.printLayerHeightMm || "-" }} mm</span>
</div> </div>
<div> <div class="ui-meta-item">
<strong>Nozzle</strong <strong>Nozzle</strong
><span>{{ selectedOrder.printNozzleDiameterMm || "-" }} mm</span> ><span>{{ selectedOrder.printNozzleDiameterMm || "-" }} mm</span>
</div> </div>
<div> <div class="ui-meta-item">
<strong>Infill pattern</strong <strong>Infill pattern</strong
><span>{{ selectedOrder.printInfillPattern || "-" }}</span> ><span>{{ selectedOrder.printInfillPattern || "-" }}</span>
</div> </div>
<div> <div class="ui-meta-item">
<strong>Infill %</strong <strong>Infill %</strong
><span>{{ selectedOrder.printInfillPercent ?? "-" }}</span> ><span>{{ selectedOrder.printInfillPercent ?? "-" }}</span>
</div> </div>
<div> <div class="ui-meta-item">
<strong>Supporti</strong <strong>Supporti</strong
><span>{{ selectedOrder.printSupportsEnabled ? "Sì" : "No" }}</span> ><span>{{ selectedOrder.printSupportsEnabled ? "Sì" : "No" }}</span>
</div> </div>

View File

@@ -4,67 +4,16 @@
gap: var(--space-5); gap: var(--space-5);
} }
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: var(--space-4);
}
.dashboard-header h1 {
margin: 0;
font-size: 1.45rem;
}
.dashboard-header p {
margin: var(--space-2) 0 0;
color: var(--color-text-muted);
}
.header-actions { .header-actions {
display: flex; display: flex;
gap: var(--space-2); gap: var(--space-2);
} }
.workspace {
display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(340px, 0.9fr);
gap: var(--space-4);
align-items: start;
}
.list-panel, .list-panel,
.detail-panel { .detail-panel {
min-width: 0; min-width: 0;
} }
button {
border: 0;
border-radius: var(--radius-md);
background: var(--color-brand);
color: var(--color-neutral-900);
padding: var(--space-2) var(--space-4);
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s ease;
line-height: 1.2;
}
button:hover:not(:disabled) {
background: var(--color-brand-hover);
}
button.ghost {
background: var(--color-bg-card);
color: var(--color-text);
border: 1px solid var(--color-border);
}
button:disabled {
opacity: 0.65;
cursor: default;
}
.list-panel h2 { .list-panel h2 {
font-size: 1.05rem; font-size: 1.05rem;
margin-bottom: var(--space-2); margin-bottom: var(--space-2);
@@ -91,50 +40,8 @@ button:disabled {
color: var(--color-text-muted); color: var(--color-text-muted);
} }
.toolbar-field input,
.toolbar-field select {
width: 100%;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-2) var(--space-3);
background: var(--color-bg-card);
font: inherit;
}
.table-wrap {
overflow: auto;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-bg-card);
max-height: 72vh;
}
table {
width: 100%;
border-collapse: collapse;
min-width: 760px;
}
thead {
background: var(--color-neutral-100);
}
th,
td {
text-align: left;
padding: var(--space-3);
border-bottom: 1px solid var(--color-border);
vertical-align: top;
font-size: 0.93rem;
}
tbody tr { tbody tr {
cursor: pointer; cursor: pointer;
transition: background-color 0.15s ease;
}
tbody tr:hover {
background: #fff9d9;
} }
tbody tr.selected { tbody tr.selected {
@@ -149,22 +56,6 @@ tbody tr.no-results:hover {
background: transparent; background: transparent;
} }
.detail-panel {
background: var(--color-bg-card);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-4);
min-height: 520px;
}
.detail-panel.empty {
display: grid;
align-content: center;
justify-items: center;
text-align: center;
color: var(--color-text-muted);
}
.order-uuid { .order-uuid {
font-size: 0.84rem; font-size: 0.84rem;
color: var(--color-text-muted); color: var(--color-text-muted);
@@ -172,38 +63,12 @@ tbody tr.no-results:hover {
.order-uuid code { .order-uuid code {
font-size: 0.82rem; font-size: 0.82rem;
color: var(--color-text);
background: var(--color-neutral-100);
border: 1px solid var(--color-border);
border-radius: 6px;
padding: 2px 6px;
} }
.detail-header h2 { .detail-header h2 {
margin: 0 0 var(--space-2); margin: 0 0 var(--space-2);
} }
.meta-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-2);
margin-bottom: var(--space-3);
}
.meta-grid > div {
background: var(--color-neutral-100);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-3);
display: grid;
gap: 2px;
}
.meta-grid strong {
font-size: 0.78rem;
color: var(--color-text-muted);
}
.actions-block { .actions-block {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -223,15 +88,6 @@ tbody tr.no-results:hover {
color: var(--color-text-muted); color: var(--color-text-muted);
} }
.status-editor select {
min-width: 210px;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-2) var(--space-3);
background: var(--color-bg-card);
font: inherit;
}
.doc-actions { .doc-actions {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -333,26 +189,9 @@ tbody tr.no-results:hover {
} }
.modal-grid { .modal-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-2);
margin-bottom: var(--space-4); margin-bottom: var(--space-4);
} }
.modal-grid > div {
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-neutral-100);
padding: var(--space-3);
display: grid;
gap: 2px;
}
.modal-grid strong {
font-size: 0.78rem;
color: var(--color-text-muted);
}
h4 { h4 {
margin: 0 0 var(--space-2); margin: 0 0 var(--space-2);
} }
@@ -390,10 +229,6 @@ h4 {
} }
@media (max-width: 1280px) { @media (max-width: 1280px) {
.workspace {
grid-template-columns: 1fr;
}
.detail-panel { .detail-panel {
min-height: unset; min-height: unset;
} }
@@ -408,15 +243,6 @@ h4 {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.dashboard-header {
flex-direction: column;
}
.meta-grid,
.modal-grid {
grid-template-columns: 1fr;
}
.item { .item {
align-items: flex-start; align-items: flex-start;
} }
@@ -446,8 +272,8 @@ h4 {
} }
@media (max-width: 520px) { @media (max-width: 520px) {
th, .ui-data-table th,
td { .ui-data-table td {
padding: var(--space-2); padding: var(--space-2);
font-size: 0.88rem; font-size: 0.88rem;
} }

View File

@@ -1,12 +1,14 @@
<section class="section-card"> <section class="section-card ui-section-card">
<header class="section-header"> <header class="section-header ui-section-header">
<div> <div class="ui-section-header__copy">
<h2>Sessioni quote</h2> <h2 class="ui-section-header__title">Sessioni quote</h2>
<p>Sessioni create dal configuratore con stato e conversione ordine.</p> <p class="ui-section-header__description">
Sessioni create dal configuratore con stato e conversione ordine.
</p>
</div> </div>
<button <button
type="button" type="button"
class="btn-primary" class="ui-button"
(click)="loadSessions()" (click)="loadSessions()"
[disabled]="loading" [disabled]="loading"
> >
@@ -17,8 +19,8 @@
<p class="error" *ngIf="errorMessage">{{ errorMessage }}</p> <p class="error" *ngIf="errorMessage">{{ errorMessage }}</p>
<p class="success" *ngIf="successMessage">{{ successMessage }}</p> <p class="success" *ngIf="successMessage">{{ successMessage }}</p>
<div class="table-wrap" *ngIf="!loading; else loadingTpl"> <div class="table-wrap ui-table-wrap" *ngIf="!loading; else loadingTpl">
<table> <table class="ui-data-table">
<thead> <thead>
<tr> <tr>
<th>Sessione</th> <th>Sessione</th>
@@ -53,14 +55,14 @@
<td class="actions"> <td class="actions">
<button <button
type="button" type="button"
class="btn-secondary" class="ui-button ui-button--ghost"
(click)="toggleSessionDetail(session)" (click)="toggleSessionDetail(session)"
> >
{{ isDetailOpen(session.id) ? "Nascondi" : "Vedi" }} {{ isDetailOpen(session.id) ? "Nascondi" : "Vedi" }}
</button> </button>
<button <button
type="button" type="button"
class="btn-danger" class="ui-button ui-button--danger"
(click)="deleteSession(session)" (click)="deleteSession(session)"
[disabled]=" [disabled]="
isDeletingSession(session.id) || !!session.convertedOrderId isDeletingSession(session.id) || !!session.convertedOrderId
@@ -92,6 +94,7 @@
<div class="detail-session-id"> <div class="detail-session-id">
<strong>UUID sessione:</strong> <strong>UUID sessione:</strong>
<code <code
class="ui-code-pill"
[title]="detail.session.id" [title]="detail.session.id"
[appCopyOnClick]="detail.session.id" [appCopyOnClick]="detail.session.id"
>{{ detail.session.id }}</code >{{ detail.session.id }}</code

View File

@@ -4,76 +4,6 @@
gap: var(--space-5); gap: var(--space-5);
} }
.section-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: var(--space-4);
}
h2 {
margin: 0;
}
p {
margin: var(--space-2) 0 0;
color: var(--color-text-muted);
}
button {
border: 0;
border-radius: var(--radius-md);
padding: var(--space-2) var(--space-4);
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s ease;
}
.btn-primary {
background: var(--color-brand);
color: var(--color-neutral-900);
}
.btn-primary:hover:not(:disabled) {
background: var(--color-brand-hover);
}
.btn-danger {
background: var(--color-danger-500);
color: #fff;
}
.btn-danger:hover:not(:disabled) {
background: #dc2626;
}
.btn-secondary {
background: transparent;
color: var(--color-text);
border: 1px solid var(--color-border);
}
.btn-secondary:hover:not(:disabled) {
background: var(--color-neutral-100);
}
.table-wrap {
overflow: auto;
}
table {
width: 100%;
border-collapse: collapse;
min-width: 920px;
}
th,
td {
text-align: left;
padding: var(--space-3);
border-bottom: 1px solid var(--color-border);
}
.error { .error {
color: var(--color-danger-500); color: var(--color-danger-500);
margin: 0; margin: 0;
@@ -111,10 +41,6 @@ td {
.detail-session-id code { .detail-session-id code {
display: block; display: block;
max-width: 100%; max-width: 100%;
padding: var(--space-2);
border-radius: var(--radius-sm);
border: 1px solid var(--color-border);
background: var(--color-neutral-100);
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
@@ -143,11 +69,6 @@ td {
} }
@media (max-width: 900px) { @media (max-width: 900px) {
.section-header {
flex-direction: column;
align-items: stretch;
}
.actions { .actions {
flex-wrap: wrap; flex-wrap: wrap;
} }
@@ -171,8 +92,8 @@ td {
gap: var(--space-4); gap: var(--space-4);
} }
th, .ui-data-table th,
td { .ui-data-table td {
padding: var(--space-2); padding: var(--space-2);
font-size: 0.86rem; font-size: 0.86rem;
} }

View File

@@ -1,7 +1,7 @@
<div class="checkout-page"> <div class="checkout-page">
<div class="container hero"> <div class="container ui-page-hero">
<h1 class="section-title">{{ "CHECKOUT.TITLE" | translate }}</h1> <h1 class="ui-page-title">{{ "CHECKOUT.TITLE" | translate }}</h1>
<p class="cad-subtitle" *ngIf="isCadSession()"> <p class="ui-page-subtitle cad-subtitle" *ngIf="isCadSession()">
Servizio CAD Servizio CAD
<ng-container *ngIf="cadRequestId()"> <ng-container *ngIf="cadRequestId()">
riferito alla richiesta contatto #{{ cadRequestId() }} riferito alla richiesta contatto #{{ cadRequestId() }}
@@ -10,7 +10,7 @@
</div> </div>
<div class="container"> <div class="container">
<div class="checkout-layout"> <div class="checkout-layout ui-two-column-layout">
<!-- LEFT COLUMN: Form --> <!-- LEFT COLUMN: Form -->
<div class="checkout-form-section"> <div class="checkout-form-section">
<!-- Error Message --> <!-- Error Message -->
@@ -21,10 +21,10 @@
<form [formGroup]="checkoutForm" (ngSubmit)="onSubmit()" *ngIf="!error"> <form [formGroup]="checkoutForm" (ngSubmit)="onSubmit()" *ngIf="!error">
<!-- Contact Info Card --> <!-- Contact Info Card -->
<app-card class="mb-6"> <app-card class="mb-6">
<div class="card-header-simple"> <div class="ui-card-header">
<h3>{{ "CHECKOUT.CONTACT_INFO" | translate }}</h3> <h3 class="ui-card-title">{{ "CHECKOUT.CONTACT_INFO" | translate }}</h3>
</div> </div>
<div class="form-row"> <div class="ui-form-row">
<app-input <app-input
formControlName="email" formControlName="email"
type="email" type="email"
@@ -47,8 +47,8 @@
<!-- Billing Address Card --> <!-- Billing Address Card -->
<app-card class="mb-6"> <app-card class="mb-6">
<div class="card-header-simple"> <div class="ui-card-header">
<h3>{{ "CHECKOUT.BILLING_ADDR" | translate }}</h3> <h3 class="ui-card-title">{{ "CHECKOUT.BILLING_ADDR" | translate }}</h3>
</div> </div>
<div formGroupName="billingAddress"> <div formGroupName="billingAddress">
<!-- User Type Selector --> <!-- User Type Selector -->
@@ -61,7 +61,7 @@
</app-toggle-selector> </app-toggle-selector>
<!-- Private Person Fields --> <!-- Private Person Fields -->
<div *ngIf="!isCompany" class="form-row"> <div *ngIf="!isCompany" class="ui-form-row">
<app-input <app-input
formControlName="firstName" formControlName="firstName"
[label]="'CHECKOUT.FIRST_NAME' | translate" [label]="'CHECKOUT.FIRST_NAME' | translate"
@@ -75,7 +75,10 @@
</div> </div>
<!-- Company Fields --> <!-- Company Fields -->
<div *ngIf="isCompany" class="company-fields mb-4"> <div
*ngIf="isCompany"
class="ui-field-stack ui-field-stack--indented mb-4"
>
<app-input <app-input
formControlName="companyName" formControlName="companyName"
[label]="'CHECKOUT.COMPANY_NAME' | translate" [label]="'CHECKOUT.COMPANY_NAME' | translate"
@@ -100,7 +103,7 @@
[label]="'CHECKOUT.ADDRESS_2' | translate" [label]="'CHECKOUT.ADDRESS_2' | translate"
></app-input> ></app-input>
<div class="form-row three-cols"> <div class="ui-form-row ui-form-row--three">
<app-input <app-input
formControlName="zip" formControlName="zip"
[label]="'CHECKOUT.ZIP' | translate" [label]="'CHECKOUT.ZIP' | translate"
@@ -123,10 +126,10 @@
</app-card> </app-card>
<!-- Shipping Option --> <!-- Shipping Option -->
<div class="shipping-option"> <div class="shipping-option ui-soft-panel">
<label class="checkbox-container"> <label class="ui-checkbox">
<input type="checkbox" formControlName="shippingSameAsBilling" /> <input type="checkbox" formControlName="shippingSameAsBilling" />
<span class="checkmark"></span> <span class="ui-checkbox__mark"></span>
{{ "CHECKOUT.SHIPPING_SAME" | translate }} {{ "CHECKOUT.SHIPPING_SAME" | translate }}
</label> </label>
</div> </div>
@@ -136,11 +139,11 @@
*ngIf="!checkoutForm.get('shippingSameAsBilling')?.value" *ngIf="!checkoutForm.get('shippingSameAsBilling')?.value"
class="mb-6" class="mb-6"
> >
<div class="card-header-simple"> <div class="ui-card-header">
<h3>{{ "CHECKOUT.SHIPPING_ADDR" | translate }}</h3> <h3 class="ui-card-title">{{ "CHECKOUT.SHIPPING_ADDR" | translate }}</h3>
</div> </div>
<div formGroupName="shippingAddress"> <div formGroupName="shippingAddress">
<div class="form-row"> <div class="ui-form-row">
<app-input <app-input
formControlName="firstName" formControlName="firstName"
[label]="'CHECKOUT.FIRST_NAME' | translate" [label]="'CHECKOUT.FIRST_NAME' | translate"
@@ -151,7 +154,7 @@
></app-input> ></app-input>
</div> </div>
<div *ngIf="isCompany" class="company-fields"> <div *ngIf="isCompany" class="ui-field-stack ui-field-stack--indented">
<app-input <app-input
formControlName="companyName" formControlName="companyName"
[label]="'CHECKOUT.COMPANY_OPTIONAL' | translate" [label]="'CHECKOUT.COMPANY_OPTIONAL' | translate"
@@ -167,7 +170,7 @@
[label]="'CHECKOUT.ADDRESS_1' | translate" [label]="'CHECKOUT.ADDRESS_1' | translate"
></app-input> ></app-input>
<div class="form-row three-cols"> <div class="ui-form-row ui-form-row--three">
<app-input <app-input
formControlName="zip" formControlName="zip"
[label]="'CHECKOUT.ZIP' | translate" [label]="'CHECKOUT.ZIP' | translate"
@@ -187,9 +190,9 @@
</app-card> </app-card>
<div class="legal-consent"> <div class="legal-consent">
<label class="checkbox-container"> <label class="ui-checkbox">
<input type="checkbox" formControlName="acceptLegal" /> <input type="checkbox" formControlName="acceptLegal" />
<span class="checkmark"></span> <span class="ui-checkbox__mark"></span>
<span> <span>
{{ "LEGAL.CONSENT.LABEL_PREFIX" | translate }} {{ "LEGAL.CONSENT.LABEL_PREFIX" | translate }}
<a href="/terms" target="_blank" rel="noopener">{{ <a href="/terms" target="_blank" rel="noopener">{{
@@ -213,7 +216,7 @@
</div> </div>
</div> </div>
<div class="actions"> <div class="ui-actions">
<app-button <app-button
type="submit" type="submit"
[disabled]="checkoutForm.invalid || isSubmitting()" [disabled]="checkoutForm.invalid || isSubmitting()"
@@ -233,8 +236,8 @@
<!-- RIGHT COLUMN: Order Summary --> <!-- RIGHT COLUMN: Order Summary -->
<div class="checkout-summary-section"> <div class="checkout-summary-section">
<app-card class="sticky-card"> <app-card class="sticky-card">
<div class="card-header-simple"> <div class="ui-card-header">
<h3>{{ "CHECKOUT.SUMMARY_TITLE" | translate }}</h3> <h3 class="ui-card-title">{{ "CHECKOUT.SUMMARY_TITLE" | translate }}</h3>
</div> </div>
<div class="summary-items" *ngIf="quoteSession() as session"> <div class="summary-items" *ngIf="quoteSession() as session">

View File

@@ -1,74 +1,5 @@
.hero {
padding: var(--space-8) 0;
text-align: center;
.section-title {
font-size: 2.5rem;
margin-bottom: var(--space-2);
}
}
.cad-subtitle { .cad-subtitle {
margin: 0; margin: 0;
color: var(--color-text-muted);
}
.checkout-layout {
display: grid;
grid-template-columns: 1fr 420px;
gap: var(--space-8);
align-items: start;
margin-bottom: var(--space-12);
@media (max-width: 1024px) {
grid-template-columns: 1fr;
gap: var(--space-8);
}
}
.card-header-simple {
margin-bottom: var(--space-6);
padding-bottom: var(--space-4);
border-bottom: 1px solid var(--color-border);
h3 {
font-size: 1.25rem;
font-weight: 600;
color: var(--color-text);
margin: 0;
}
}
.form-row {
display: flex;
flex-direction: column;
gap: var(--space-4);
margin-bottom: var(--space-4);
@media (min-width: 768px) {
flex-direction: row;
& > * {
flex: 1;
}
}
&.no-margin {
margin-bottom: 0;
}
&.three-cols {
display: grid;
grid-template-columns: 1.5fr 2fr 1fr;
gap: var(--space-4);
@media (max-width: 768px) {
grid-template-columns: 1fr;
}
}
app-input {
width: 100%;
}
} }
/* User Type Selector CSS has been extracted to app-toggle-selector component */ /* User Type Selector CSS has been extracted to app-toggle-selector component */
@@ -78,97 +9,18 @@ app-toggle-selector.user-type-selector-compact {
max-width: 400px; max-width: 400px;
} }
.company-fields {
display: flex;
flex-direction: column;
gap: var(--space-4);
padding-left: var(--space-4);
border-left: 2px solid var(--color-border);
margin-bottom: var(--space-4);
}
.shipping-option { .shipping-option {
margin: var(--space-6) 0; margin: var(--space-6) 0;
padding: var(--space-4);
background: var(--color-neutral-100);
border-radius: var(--radius-md);
} }
.legal-consent { .legal-consent {
margin: var(--space-4) 0 var(--space-4); margin: var(--space-4) 0 var(--space-4);
.checkbox-container {
font-size: 0.95rem;
line-height: 1.4;
align-items: flex-start;
min-height: 24px;
}
}
/* Custom Checkbox */
.checkbox-container {
display: flex;
align-items: center;
position: relative;
padding-left: 36px;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
user-select: none;
color: var(--color-text);
input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
&:checked ~ .checkmark {
background-color: var(--color-brand);
border-color: var(--color-brand);
&:after {
display: block;
}
}
}
.checkmark {
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
height: 24px;
width: 24px;
background-color: var(--color-bg-card);
border: 2px solid var(--color-border);
border-radius: var(--radius-sm);
transition: all 0.2s;
&:after {
content: "";
position: absolute;
display: none;
left: 7px;
top: 3px;
width: 6px;
height: 12px;
border: solid #000;
border-width: 0 2.5px 2.5px 0;
transform: rotate(45deg);
}
}
&:hover input ~ .checkmark {
border-color: var(--color-brand);
}
} }
.consent-error { .consent-error {
margin-top: var(--space-2); margin-top: var(--space-2);
margin-left: 36px; margin-left: 2.1rem;
color: var(--color-danger-500, #ef4444); color: var(--color-danger-500);
font-size: 0.9rem; font-size: 0.9rem;
} }
@@ -355,9 +207,7 @@ app-toggle-selector.user-type-selector-compact {
padding-right: var(--space-3); padding-right: var(--space-3);
} }
.actions { .ui-actions {
margin-top: var(--space-8);
app-button { app-button {
width: 100%; width: 100%;
} }
@@ -365,11 +215,11 @@ app-toggle-selector.user-type-selector-compact {
.error-message { .error-message {
color: var(--color-error); color: var(--color-error);
background: #fef2f2; background: var(--color-surface-danger);
padding: var(--space-4); padding: var(--space-4);
border-radius: var(--radius-md); border-radius: var(--radius-md);
margin-bottom: var(--space-6); margin-bottom: var(--space-6);
border: 1px solid #fee2e2; border: 1px solid #fecaca;
font-weight: 500; font-weight: 500;
} }

View File

@@ -6,16 +6,18 @@
} @else { } @else {
<form [formGroup]="form" (ngSubmit)="onSubmit()"> <form [formGroup]="form" (ngSubmit)="onSubmit()">
<!-- Request Type --> <!-- Request Type -->
<div class="form-group"> <div class="ui-form-group">
<label>{{ "CONTACT.REQ_TYPE_LABEL" | translate }} *</label> <label class="ui-form-label"
<select formControlName="requestType" class="form-control"> >{{ "CONTACT.REQ_TYPE_LABEL" | translate }} *</label
>
<select formControlName="requestType" class="ui-form-control">
<option *ngFor="let type of requestTypes" [value]="type.value"> <option *ngFor="let type of requestTypes" [value]="type.value">
{{ type.label | translate }} {{ type.label | translate }}
</option> </option>
</select> </select>
</div> </div>
<div class="row"> <div class="ui-form-row">
<!-- Phone --> <!-- Phone -->
<app-input <app-input
formControlName="email" formControlName="email"
@@ -35,22 +37,12 @@
</div> </div>
<!-- User Type Selector (Segmented Control) --> <!-- User Type Selector (Segmented Control) -->
<div class="user-type-selector"> <app-toggle-selector
<div class="mb-4"
class="type-option" [options]="customerTypeOptions"
[class.selected]="!isCompany" [selectedValue]="isCompany"
(click)="setCompanyMode(false)" (selectionChange)="setCompanyMode($event)"
> ></app-toggle-selector>
{{ "CONTACT.TYPE_PRIVATE" | translate }}
</div>
<div
class="type-option"
[class.selected]="isCompany"
(click)="setCompanyMode(true)"
>
{{ "CONTACT.TYPE_COMPANY" | translate }}
</div>
</div>
<!-- Personal Name (Only if NOT Company) --> <!-- Personal Name (Only if NOT Company) -->
<app-input <app-input
@@ -61,7 +53,7 @@
></app-input> ></app-input>
<!-- Company Fields (Only if Company) --> <!-- Company Fields (Only if Company) -->
<div *ngIf="isCompany" class="company-fields"> <div *ngIf="isCompany" class="ui-field-stack ui-field-stack--indented">
<app-input <app-input
formControlName="companyName" formControlName="companyName"
[label]="('CONTACT.COMPANY_NAME' | translate) + ' *'" [label]="('CONTACT.COMPANY_NAME' | translate) + ' *'"
@@ -74,20 +66,24 @@
></app-input> ></app-input>
</div> </div>
<div class="form-group"> <div class="ui-form-group">
<label>{{ "CONTACT.LABEL_MESSAGE" | translate }}</label> <label class="ui-form-label">{{
"CONTACT.LABEL_MESSAGE" | translate
}}</label>
<textarea <textarea
formControlName="message" formControlName="message"
class="form-control" class="ui-form-control"
rows="4" rows="4"
></textarea> ></textarea>
</div> </div>
<!-- File Upload Section --> <!-- File Upload Section -->
<div class="form-group"> <div class="ui-form-group">
<label>{{ "CONTACT.UPLOAD_LABEL" | translate }}</label> <label class="ui-form-label">{{
<p class="hint">{{ "CONTACT.UPLOAD_HINT" | translate }}</p> "CONTACT.UPLOAD_LABEL" | translate
<p class="hint upload-privacy-note"> }}</label>
<p class="ui-form-hint">{{ "CONTACT.UPLOAD_HINT" | translate }}</p>
<p class="ui-form-hint upload-privacy-note">
{{ "LEGAL.CONSENT.UPLOAD_NOTICE_PREFIX" | translate }} {{ "LEGAL.CONSENT.UPLOAD_NOTICE_PREFIX" | translate }}
<a href="/privacy" target="_blank" rel="noopener">{{ <a href="/privacy" target="_blank" rel="noopener">{{
"LEGAL.CONSENT.UPLOAD_NOTICE_LINK" | translate "LEGAL.CONSENT.UPLOAD_NOTICE_LINK" | translate
@@ -160,9 +156,9 @@
</div> </div>
<div class="legal-consent"> <div class="legal-consent">
<label class="checkbox-container"> <label class="ui-checkbox">
<input type="checkbox" formControlName="acceptLegal" /> <input type="checkbox" formControlName="acceptLegal" />
<span class="checkmark"></span> <span class="ui-checkbox__mark"></span>
<span> <span>
{{ "LEGAL.CONSENT.LABEL_PREFIX" | translate }} {{ "LEGAL.CONSENT.LABEL_PREFIX" | translate }}
<a href="/terms" target="_blank" rel="noopener">{{ <a href="/terms" target="_blank" rel="noopener">{{
@@ -185,7 +181,7 @@
</div> </div>
</div> </div>
<div class="actions"> <div class="ui-actions ui-actions--end">
<app-button type="submit" [disabled]="form.invalid || sent()"> <app-button type="submit" [disabled]="form.invalid || sent()">
{{ {{
sent() sent()

View File

@@ -1,109 +1,12 @@
.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);
}
.hint {
font-size: 0.75rem;
color: var(--color-text-muted);
margin-bottom: var(--space-2);
}
.upload-privacy-note { .upload-privacy-note {
margin-top: calc(var(--space-2) * -1); margin-top: calc(var(--space-2) * -1);
font-size: 0.78rem; font-size: 0.78rem;
} }
.form-control {
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
width: 100%;
background: var(--color-bg-card);
color: var(--color-text);
font-family: inherit;
&:focus {
outline: none;
border-color: var(--color-brand);
}
}
select.form-control {
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg 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'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 1rem center;
background-size: 1em;
}
.row {
display: flex;
flex-direction: column;
gap: var(--space-4);
margin-bottom: var(--space-4);
@media (min-width: 768px) {
flex-direction: row;
.col {
flex: 1;
margin-bottom: 0;
}
}
}
app-input.col { app-input.col {
width: 100%; width: 100%;
} }
/* User Type Selector Styles */
.user-type-selector {
display: flex;
background-color: var(--color-neutral-100);
border-radius: var(--radius-md);
padding: 4px;
margin-bottom: var(--space-4);
gap: 4px;
width: 100%; /* Full width */
max-width: 400px; /* Limit on desktop */
}
.type-option {
flex: 1; /* Equal width */
text-align: center;
padding: 8px 16px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text-muted);
transition: all 0.2s ease;
user-select: none;
&: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);
}
}
.company-fields {
display: flex;
flex-direction: column;
gap: var(--space-4);
padding-left: var(--space-4);
border-left: 2px solid var(--color-border);
margin-bottom: var(--space-4);
}
/* File Upload Styles */ /* File Upload Styles */
.drop-zone { .drop-zone {
border: 2px dashed var(--color-border); border: 2px dashed var(--color-border);
@@ -204,66 +107,6 @@ app-input.col {
.legal-consent { .legal-consent {
margin: var(--space-4) 0 var(--space-4); margin: var(--space-4) 0 var(--space-4);
}
.checkbox-container {
display: flex;
align-items: center;
position: relative;
padding-left: 36px;
cursor: pointer;
font-size: 0.95rem;
font-weight: 500;
user-select: none;
color: var(--color-text);
line-height: 1.4;
input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
&:checked ~ .checkmark {
background-color: var(--color-brand);
border-color: var(--color-brand);
&:after {
display: block;
}
}
}
.checkmark {
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
height: 24px;
width: 24px;
background-color: var(--color-bg-card);
border: 2px solid var(--color-border);
border-radius: var(--radius-sm);
transition: all 0.2s;
&:after {
content: "";
position: absolute;
display: none;
left: 7px;
top: 3px;
width: 6px;
height: 12px;
border: solid #000;
border-width: 0 2.5px 2.5px 0;
transform: rotate(45deg);
}
}
&:hover input ~ .checkmark {
border-color: var(--color-brand);
}
a { a {
color: var(--color-brand); color: var(--color-brand);
@@ -273,8 +116,8 @@ app-input.col {
.consent-error { .consent-error {
margin-top: var(--space-2); margin-top: var(--space-2);
margin-left: 36px; margin-left: 2.1rem;
color: var(--color-danger-500, #ef4444); color: var(--color-danger-500);
font-size: 0.9rem; font-size: 0.9rem;
} }

View File

@@ -9,6 +9,10 @@ import {
import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { AppInputComponent } from '../../../../shared/components/app-input/app-input.component'; import { AppInputComponent } from '../../../../shared/components/app-input/app-input.component';
import { AppButtonComponent } from '../../../../shared/components/app-button/app-button.component'; import { AppButtonComponent } from '../../../../shared/components/app-button/app-button.component';
import {
AppToggleSelectorComponent,
ToggleOption,
} from '../../../../shared/components/app-toggle-selector/app-toggle-selector.component';
import { QuoteEstimatorService } from '../../../calculator/services/quote-estimator.service'; import { QuoteEstimatorService } from '../../../calculator/services/quote-estimator.service';
import { QuoteRequestService } from '../../../../core/services/quote-request.service'; import { QuoteRequestService } from '../../../../core/services/quote-request.service';
import { LanguageService } from '../../../../core/services/language.service'; import { LanguageService } from '../../../../core/services/language.service';
@@ -30,6 +34,7 @@ import { SuccessStateComponent } from '../../../../shared/components/success-sta
TranslateModule, TranslateModule,
AppInputComponent, AppInputComponent,
AppButtonComponent, AppButtonComponent,
AppToggleSelectorComponent,
SuccessStateComponent, SuccessStateComponent,
], ],
templateUrl: './contact-form.component.html', templateUrl: './contact-form.component.html',
@@ -52,6 +57,10 @@ export class ContactFormComponent implements OnDestroy {
{ value: 'consult', label: 'CONTACT.REQ_TYPE_CONSULT' }, { value: 'consult', label: 'CONTACT.REQ_TYPE_CONSULT' },
{ value: 'question', label: 'CONTACT.REQ_TYPE_QUESTION' }, { value: 'question', label: 'CONTACT.REQ_TYPE_QUESTION' },
]; ];
customerTypeOptions: ToggleOption[] = [
{ label: 'CONTACT.TYPE_PRIVATE', value: false },
{ label: 'CONTACT.TYPE_COMPANY', value: true },
];
private quoteRequestService = inject(QuoteRequestService); private quoteRequestService = inject(QuoteRequestService);
private languageService = inject(LanguageService); private languageService = inject(LanguageService);

View File

@@ -1,5 +1,5 @@
<div class="container hero"> <div class="container ui-page-hero ui-page-hero--spacious">
<h1> <h1 class="ui-page-title">
{{ "TRACKING.TITLE" | translate }} {{ "TRACKING.TITLE" | translate }}
<ng-container *ngIf="order()"> <ng-container *ngIf="order()">
<br /><span class="order-id-title" <br /><span class="order-id-title"
@@ -7,7 +7,7 @@
> >
</ng-container> </ng-container>
</h1> </h1>
<p class="subtitle">{{ "TRACKING.SUBTITLE" | translate }}</p> <p class="ui-page-subtitle subtitle">{{ "TRACKING.SUBTITLE" | translate }}</p>
</div> </div>
<div class="container"> <div class="container">
@@ -69,18 +69,18 @@
</div> </div>
</app-card> </app-card>
<div class="payment-layout"> <div class="payment-layout ui-two-column-layout">
<div class="payment-main"> <div class="payment-main">
<app-card class="mb-6"> <app-card class="mb-6">
<div class="card-header-simple"> <div class="ui-card-header">
<h3>{{ "PAYMENT.METHOD" | translate }}</h3> <h3 class="ui-card-title">{{ "PAYMENT.METHOD" | translate }}</h3>
</div> </div>
<div class="payment-selection"> <div class="payment-selection">
<div class="methods-grid"> <div class="ui-choice-grid">
<div <div
class="type-option" class="ui-choice-card"
[class.selected]="selectedPaymentMethod === 'twint'" [class.is-selected]="selectedPaymentMethod === 'twint'"
(click)="selectPayment('twint')" (click)="selectPayment('twint')"
> >
<span class="method-name">{{ <span class="method-name">{{
@@ -88,8 +88,8 @@
}}</span> }}</span>
</div> </div>
<div <div
class="type-option" class="ui-choice-card"
[class.selected]="selectedPaymentMethod === 'bill'" [class.is-selected]="selectedPaymentMethod === 'bill'"
(click)="selectPayment('bill')" (click)="selectPayment('bill')"
> >
<span class="method-name">{{ <span class="method-name">{{
@@ -100,7 +100,7 @@
</div> </div>
<div <div
class="payment-details fade-in text-center" class="payment-details ui-soft-panel fade-in text-center"
*ngIf="selectedPaymentMethod === 'twint'" *ngIf="selectedPaymentMethod === 'twint'"
> >
<div class="details-header"> <div class="details-header">
@@ -119,22 +119,9 @@
{{ "PAYMENT.BILLING_INFO_HINT" | translate }} {{ "PAYMENT.BILLING_INFO_HINT" | translate }}
</p> </p>
<div class="twint-mobile-action twint-button-container"> <div class="twint-mobile-action twint-button-container">
<button <button type="button" class="twint-launch-button" (click)="openTwintPayment()">
style="
width: auto;
height: 58px;
border-radius: 6px;
display: flex;
justify-content: center;
cursor: pointer;
background-color: transparent;
border: none;
align-items: center;
"
(click)="openTwintPayment()"
>
<img <img
style="width: auto; height: 58px" class="twint-launch-button__image"
[attr.alt]="'PAYMENT.TWINT_BUTTON_ALT' | translate" [attr.alt]="'PAYMENT.TWINT_BUTTON_ALT' | translate"
[src]="getTwintButtonImageUrl()" [src]="getTwintButtonImageUrl()"
/> />
@@ -148,7 +135,7 @@
</div> </div>
<div <div
class="payment-details fade-in text-center" class="payment-details ui-soft-panel fade-in text-center"
*ngIf="selectedPaymentMethod === 'bill'" *ngIf="selectedPaymentMethod === 'bill'"
> >
<div class="details-header"> <div class="details-header">
@@ -167,7 +154,7 @@
</div> </div>
</div> </div>
<div class="actions"> <div class="ui-actions">
<app-button <app-button
variant="outline" variant="outline"
(click)="completeOrder()" (click)="completeOrder()"
@@ -188,9 +175,11 @@
<div class="payment-summary"> <div class="payment-summary">
<app-card class="sticky-card"> <app-card class="sticky-card">
<div class="card-header-simple"> <div class="ui-card-header">
<h3>{{ "PAYMENT.SUMMARY_TITLE" | translate }}</h3> <h3 class="ui-card-title">{{ "PAYMENT.SUMMARY_TITLE" | translate }}</h3>
<p class="order-id">#{{ getDisplayOrderNumber(o) }}</p> <p class="ui-card-subtitle order-id">
#{{ getDisplayOrderNumber(o) }}
</p>
</div> </div>
<app-price-breakdown <app-price-breakdown

View File

@@ -1,99 +1,13 @@
.hero {
padding: var(--space-12) 0 var(--space-8);
text-align: center;
h1 {
font-size: 2.5rem;
margin-bottom: var(--space-2);
}
}
.subtitle { .subtitle {
font-size: 1.125rem; font-size: 1.125rem;
color: var(--color-text-muted);
max-width: 600px;
margin: 0 auto;
}
.payment-layout {
display: grid;
grid-template-columns: 1fr 400px;
gap: var(--space-8);
align-items: start;
margin-bottom: var(--space-12);
@media (max-width: 1024px) {
grid-template-columns: 1fr;
gap: var(--space-8);
}
}
.card-header-simple {
margin-bottom: var(--space-6);
padding-bottom: var(--space-4);
border-bottom: 1px solid var(--color-border);
h3 {
font-size: 1.25rem;
font-weight: 600;
color: var(--color-text);
margin: 0;
}
.order-id {
font-size: 0.875rem;
color: var(--color-text-muted);
margin-top: 2px;
}
} }
.payment-selection { .payment-selection {
margin-bottom: var(--space-6); margin-bottom: var(--space-6);
} }
.methods-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-4);
@media (max-width: 600px) {
grid-template-columns: 1fr;
}
}
.type-option {
border: 2px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-6);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
background: var(--color-bg-card);
text-align: center;
font-weight: 600;
color: var(--color-text-muted);
&:hover {
border-color: var(--color-brand);
color: var(--color-text);
}
&.selected {
border-color: var(--color-brand);
background-color: var(--color-neutral-100);
color: #000;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
}
.payment-details { .payment-details {
background: var(--color-neutral-100);
border-radius: var(--radius-md);
padding: var(--space-6);
margin-bottom: var(--space-6); margin-bottom: var(--space-6);
border: 1px solid var(--color-border);
&.text-center { &.text-center {
text-align: center; text-align: center;
@@ -127,6 +41,23 @@
} }
} }
.twint-launch-button {
width: auto;
height: 58px;
border: none;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
cursor: pointer;
}
.twint-launch-button__image {
width: auto;
height: 58px;
}
.qr-placeholder { .qr-placeholder {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -184,10 +115,6 @@
top: var(--space-6); top: var(--space-6);
} }
.actions {
margin-top: var(--space-8);
}
.fade-in { .fade-in {
animation: fadeIn 0.4s ease-out; animation: fadeIn 0.4s ease-out;
} }

View File

@@ -60,3 +60,20 @@
color: var(--color-text); color: var(--color-text);
} }
} }
.btn-ghost {
background-color: var(--color-bg-card);
border-color: var(--color-border);
color: var(--color-text);
&:hover:not(:disabled) {
background-color: var(--color-surface-muted);
}
}
.btn-danger {
background-color: var(--color-danger-500);
color: #fff;
&:hover:not(:disabled) {
background-color: #dc2626;
}
}

View File

@@ -9,7 +9,9 @@ import { CommonModule } from '@angular/common';
styleUrl: './app-button.component.scss', styleUrl: './app-button.component.scss',
}) })
export class AppButtonComponent { export class AppButtonComponent {
variant = input<'primary' | 'secondary' | 'outline' | 'text'>('primary'); variant = input<
'primary' | 'secondary' | 'outline' | 'text' | 'ghost' | 'danger'
>('primary');
type = input<'button' | 'submit' | 'reset'>('button'); type = input<'button' | 'submit' | 'reset'>('button');
disabled = input<boolean>(false); disabled = input<boolean>(false);
fullWidth = input<boolean>(false); fullWidth = input<boolean>(false);

View File

@@ -1,3 +1,15 @@
<div class="card"> <div class="card">
@if (title() || subtitle()) {
<header class="card-header">
@if (title()) {
<h3 class="card-title">{{ title() }}</h3>
}
@if (subtitle()) {
<p class="card-subtitle">{{ subtitle() }}</p>
}
</header>
}
<div class="card-body">
<ng-content></ng-content> <ng-content></ng-content>
</div> </div>
</div>

View File

@@ -8,7 +8,6 @@
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
padding: var(--space-6);
transition: transition:
box-shadow 0.2s ease, box-shadow 0.2s ease,
transform 0.2s ease, transform 0.2s ease,
@@ -22,3 +21,23 @@
border-color: var(--color-neutral-300); border-color: var(--color-neutral-300);
} }
} }
.card-header {
padding: var(--space-6) var(--space-6) var(--space-4);
border-bottom: 1px solid var(--color-border);
}
.card-title {
margin: 0;
font-size: 1.2rem;
}
.card-subtitle {
margin: var(--space-2) 0 0;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.card-body {
padding: var(--space-6);
}

View File

@@ -1,4 +1,4 @@
import { Component } from '@angular/core'; import { Component, input } from '@angular/core';
@Component({ @Component({
selector: 'app-card', selector: 'app-card',
@@ -6,4 +6,7 @@ import { Component } from '@angular/core';
templateUrl: './app-card.component.html', templateUrl: './app-card.component.html',
styleUrl: './app-card.component.scss', styleUrl: './app-card.component.scss',
}) })
export class AppCardComponent {} export class AppCardComponent {
title = input<string>('');
subtitle = input<string>('');
}

View File

@@ -14,20 +14,24 @@ label {
margin-left: 2px; margin-left: 2px;
} }
.form-control { .form-control {
padding: 0.5rem 0.75rem; padding: 0.625rem 0.75rem;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--radius-md); border-radius: var(--radius-md);
font-size: 1rem; font-size: 1rem;
width: 100%; width: 100%;
background: var(--color-bg-card); background: var(--color-bg-card);
color: var(--color-text); color: var(--color-text);
transition:
border-color 0.2s ease,
box-shadow 0.2s ease,
background-color 0.2s ease;
&:focus { &:focus {
outline: none; outline: none;
border-color: var(--color-brand); border-color: var(--color-brand);
box-shadow: 0 0 0 2px rgba(250, 207, 10, 0.25); box-shadow: var(--focus-ring);
} }
&:disabled { &:disabled {
background: var(--color-neutral-100); background: var(--color-surface-muted);
cursor: not-allowed; cursor: not-allowed;
} }
} }

View File

@@ -10,16 +10,20 @@ label {
color: var(--color-text); color: var(--color-text);
} }
.form-control { .form-control {
padding: 0.5rem 0.75rem; padding: 0.625rem 0.75rem;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--radius-md); border-radius: var(--radius-md);
font-size: 1rem; font-size: 1rem;
width: 100%; width: 100%;
background: var(--color-bg-card); background: var(--color-bg-card);
color: var(--color-text); color: var(--color-text);
transition:
border-color 0.2s ease,
box-shadow 0.2s ease;
&:focus { &:focus {
outline: none; outline: none;
border-color: var(--color-brand); border-color: var(--color-brand);
box-shadow: var(--focus-ring);
} }
} }
.error-text { .error-text {

View File

@@ -1,6 +1,7 @@
/* src/styles.scss */ /* src/styles.scss */
@use "./styles/theme"; @use "./styles/theme";
@use "./styles/patterns"; @use "./styles/patterns";
@use "./styles/ui";
/* Reset / Base */ /* Reset / Base */
*, *,

View File

@@ -0,0 +1,509 @@
.ui-page-hero {
padding: var(--space-8) 0;
text-align: center;
}
.ui-page-hero--spacious {
padding: var(--space-12) 0 var(--space-8);
}
.ui-page-title {
margin: 0 0 var(--space-2);
font-size: clamp(2rem, 4vw, 2.75rem);
}
.ui-page-subtitle {
max-width: 680px;
margin: 0 auto;
color: var(--color-text-muted);
font-size: 1.05rem;
}
.ui-two-column-layout {
--ui-sidebar-width: 400px;
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(300px, var(--ui-sidebar-width));
gap: var(--space-8);
align-items: start;
margin-bottom: var(--space-12);
}
.ui-section-card {
background: var(--color-bg-card);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: clamp(12px, 2vw, 24px);
box-shadow: var(--shadow-sm);
}
.ui-section-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: var(--space-4);
margin-bottom: var(--space-5);
}
.ui-section-header__copy {
display: grid;
gap: var(--space-1);
}
.ui-section-header__title {
margin: 0;
font-size: 1.35rem;
}
.ui-section-header__description {
margin: 0;
color: var(--color-text-muted);
}
.ui-section-header__meta {
width: fit-content;
}
.ui-card-header {
margin-bottom: var(--space-6);
padding-bottom: var(--space-4);
border-bottom: 1px solid var(--color-border);
}
.ui-card-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: var(--color-text);
}
.ui-card-subtitle {
margin: var(--space-2) 0 0;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.ui-form-group {
display: flex;
flex-direction: column;
gap: var(--space-2);
margin-bottom: var(--space-4);
}
.ui-form-label {
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text);
}
.ui-form-hint {
margin: 0;
font-size: 0.78rem;
color: var(--color-text-muted);
}
.ui-form-control {
width: 100%;
padding: 0.625rem 0.75rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-bg-card);
color: var(--color-text);
font: inherit;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease,
background-color 0.2s ease;
}
.ui-form-control:focus {
outline: none;
border-color: var(--color-brand);
box-shadow: var(--focus-ring);
}
.ui-form-control:disabled {
background: var(--color-surface-muted);
cursor: not-allowed;
}
select.ui-form-control {
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23101820' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 0.9rem center;
background-size: 1rem;
padding-right: 2.5rem;
}
.ui-form-row {
display: flex;
flex-direction: column;
gap: var(--space-4);
margin-bottom: var(--space-4);
}
.ui-form-row > * {
min-width: 0;
}
.ui-form-row--three {
display: grid;
grid-template-columns: 1.5fr 2fr 1fr;
}
.ui-field-stack {
display: flex;
flex-direction: column;
gap: var(--space-4);
margin-bottom: var(--space-4);
}
.ui-field-stack--indented {
padding-left: var(--space-4);
border-left: 2px solid var(--color-border);
}
.ui-checkbox {
position: relative;
display: flex;
align-items: flex-start;
gap: 0.85rem;
cursor: pointer;
color: var(--color-text);
line-height: 1.45;
}
.ui-checkbox input {
position: absolute;
inset: 0 auto auto 0;
width: 1.5rem;
height: 1.5rem;
opacity: 0;
margin: 0;
}
.ui-checkbox__mark {
width: 1.25rem;
height: 1.25rem;
margin-top: 0.1rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-bg-card);
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
transition:
background-color 0.2s ease,
border-color 0.2s ease,
box-shadow 0.2s ease;
}
.ui-checkbox__mark::after {
content: "";
width: 0.4rem;
height: 0.72rem;
border: solid var(--color-neutral-900);
border-width: 0 2px 2px 0;
transform: rotate(45deg) scale(0);
transition: transform 0.16s ease;
}
.ui-checkbox input:focus-visible + .ui-checkbox__mark {
box-shadow: var(--focus-ring);
}
.ui-checkbox input:checked + .ui-checkbox__mark {
background: var(--color-brand);
border-color: var(--color-brand);
}
.ui-checkbox input:checked + .ui-checkbox__mark::after {
transform: rotate(45deg) scale(1);
}
.ui-soft-panel {
padding: var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-surface-muted);
}
.ui-choice-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-4);
}
.ui-choice-card {
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-5);
display: flex;
align-items: center;
justify-content: center;
background: var(--color-bg-card);
color: var(--color-text-muted);
text-align: center;
font-weight: 600;
cursor: pointer;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease,
background-color 0.2s ease,
color 0.2s ease,
transform 0.2s ease;
}
.ui-choice-card:hover {
border-color: var(--color-brand);
color: var(--color-text);
transform: translateY(-1px);
}
.ui-choice-card.is-selected,
.ui-choice-card.selected {
border-color: var(--color-brand);
background: var(--color-surface-muted);
color: var(--color-neutral-900);
box-shadow: var(--shadow-sm);
}
.ui-actions {
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
margin-top: var(--space-6);
}
.ui-actions--end {
justify-content: flex-end;
}
.ui-actions--stack > * {
flex: 1 1 220px;
}
.ui-button {
border: 0;
border-radius: var(--radius-md);
padding: var(--space-2) var(--space-4);
font: inherit;
font-weight: 600;
line-height: 1.2;
cursor: pointer;
transition:
background-color 0.2s ease,
border-color 0.2s ease,
color 0.2s ease,
opacity 0.2s ease;
background: var(--color-brand);
color: var(--color-neutral-900);
}
.ui-button:hover:not(:disabled) {
background: var(--color-brand-hover);
}
.ui-button:disabled {
opacity: 0.65;
cursor: default;
}
.ui-button--ghost {
background: var(--color-bg-card);
border: 1px solid var(--color-border);
color: var(--color-text);
}
.ui-button--ghost:hover:not(:disabled) {
background: var(--color-surface-muted);
}
.ui-button--danger {
background: var(--color-danger-500);
color: #fff;
}
.ui-button--danger:hover:not(:disabled) {
background: #dc2626;
}
.ui-pill,
.ui-status-chip {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.45rem 0.7rem;
border-radius: 999px;
border: 1px solid var(--color-border);
background: var(--color-surface-muted);
color: var(--color-text-muted);
font-size: 0.78rem;
font-weight: 700;
line-height: 1;
}
.ui-status-chip--neutral {
background: var(--color-surface-muted);
}
.ui-status-chip--warning {
background: var(--color-surface-warning);
color: #854d0e;
border-color: #f5d66d;
}
.ui-status-chip--success {
background: var(--color-surface-success);
color: #166534;
border-color: #bbf7d0;
}
.ui-status-chip--danger {
background: var(--color-surface-danger);
color: #991b1b;
border-color: #fecaca;
}
.ui-split-workspace {
display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(340px, 0.9fr);
gap: var(--space-4);
align-items: start;
}
.ui-table-wrap {
overflow: auto;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-bg-card);
max-height: 72vh;
}
.ui-data-table {
width: 100%;
border-collapse: collapse;
min-width: 760px;
}
.ui-data-table thead {
background: var(--color-surface-muted);
}
.ui-data-table th,
.ui-data-table td {
text-align: left;
padding: var(--space-3);
border-bottom: 1px solid var(--color-border);
vertical-align: top;
font-size: 0.93rem;
}
.ui-data-table th {
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--color-text-muted);
}
.ui-data-table tbody tr {
transition: background-color 0.15s ease;
}
.ui-data-table tbody tr:hover {
background: #fff9d9;
}
.ui-detail-panel {
background: var(--color-bg-card);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-4);
min-height: 500px;
}
.ui-detail-panel--empty {
display: grid;
align-content: center;
justify-items: center;
text-align: center;
color: var(--color-text-muted);
}
.ui-meta-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: var(--space-2);
}
.ui-meta-item {
margin: 0;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-3);
background: var(--color-surface-muted);
display: grid;
gap: 4px;
}
.ui-meta-item strong,
.ui-meta-item dt {
margin: 0;
font-size: 0.78rem;
font-weight: 700;
color: var(--color-text-muted);
}
.ui-meta-item span,
.ui-meta-item dd {
margin: 0;
overflow-wrap: anywhere;
}
.ui-code-pill {
display: inline-flex;
align-items: center;
padding: 3px 8px;
border: 1px solid var(--color-border);
border-radius: 7px;
background: var(--color-surface-muted);
color: var(--color-text);
font-size: 0.82rem;
overflow-wrap: anywhere;
}
@media (min-width: 768px) {
.ui-form-row {
flex-direction: row;
}
.ui-form-row > * {
flex: 1;
}
}
@media (max-width: 1024px) {
.ui-two-column-layout,
.ui-split-workspace {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.ui-form-row--three,
.ui-choice-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 640px) {
.ui-section-header {
flex-direction: column;
align-items: stretch;
}
.ui-page-hero {
padding: var(--space-6) 0;
}
}

View File

@@ -5,10 +5,17 @@
/* Semantic Colors - Theming Layer */ /* Semantic Colors - Theming Layer */
--color-bg: #faf9f6; --color-bg: #faf9f6;
--color-bg-card: #ffffff; --color-bg-card: #ffffff;
--color-surface-card: var(--color-bg-card);
--color-surface-muted: var(--color-neutral-100);
--color-surface-success: var(--color-success-100);
--color-surface-warning: var(--color-warning-100);
--color-surface-danger: var(--color-danger-100);
--color-text: var(--color-neutral-900); --color-text: var(--color-neutral-900);
--color-text-main: var(--color-text);
--color-text-muted: var(--color-secondary-500); --color-text-muted: var(--color-secondary-500);
--color-brand: var(--color-primary-500); --color-brand: var(--color-primary-500);
--color-brand-600: var(--color-primary-600);
--color-brand-hover: var(--color-primary-600); --color-brand-hover: var(--color-primary-600);
--color-border: var(--color-neutral-200); --color-border: var(--color-neutral-200);
@@ -16,6 +23,7 @@
--color-success: var(--color-success-500); --color-success: var(--color-success-500);
--color-warning: var(--color-warning-500); --color-warning: var(--color-warning-500);
--color-error: var(--color-danger-500); --color-error: var(--color-danger-500);
--focus-ring: 0 0 0 2px rgb(250 207 10 / 0.28);
/* Font */ /* Font */
--font-family-sans: "IBM Plex Sans", "Space Grotesk", sans-serif; --font-family-sans: "IBM Plex Sans", "Space Grotesk", sans-serif;

View File

@@ -9,13 +9,18 @@
--color-secondary-600: #514d43; --color-secondary-600: #514d43;
--color-success-500: #22c55e; --color-success-500: #22c55e;
--color-success-100: #dcfce7;
--color-warning-500: #eab308; --color-warning-500: #eab308;
--color-warning-100: #fef3c7;
--color-danger-500: #ef4444; --color-danger-500: #ef4444;
--color-danger-600: #dc2626;
--color-danger-100: #fee2e2;
--color-neutral-50: #ffffff; --color-neutral-50: #ffffff;
--color-neutral-100: #efede7; --color-neutral-100: #efede7;
--color-neutral-200: #dedad1; --color-neutral-200: #dedad1;
--color-neutral-300: #c6c1b6; --color-neutral-300: #c6c1b6;
--color-neutral-400: #a8a396;
--color-neutral-800: #1f2933; --color-neutral-800: #1f2933;
--color-neutral-900: #101820; --color-neutral-900: #101820;