feat: integrate FontAwesome icons across various components

- Updated apartment-list component to use FontAwesome icons for better visual representation.
- Refactored brand-mark component to utilize FontAwesome for the brand icon.
- Enhanced dashboard-page with FontAwesome icons for improved navigation.
- Replaced SVG icons with FontAwesome in settings-page for consistency and better performance.
- Updated sign-in-page to use FontAwesome for the sign-in action icon.
- Refactored tenant-card to incorporate FontAwesome icons for attachments.
- Enhanced workspace-navigation with FontAwesome icons for all navigation items.
- Applied consistent styling for fa-icon elements across components.
This commit is contained in:
nessar committed 2026-08-17 14:18:15 +02:00
1 parent ba695e2426
commit 713ab3b2d9
19 files changed
+531 -453

No files matched your search

+8
View File
@@ -86,6 +86,14 @@ test('adapts authenticated navigation to the viewport', async ({ page }, testInf
expect(Math.round((navigationBox?.y ?? 0) + (navigationBox?.height ?? 0))).toBe( expect(Math.round((navigationBox?.y ?? 0) + (navigationBox?.height ?? 0))).toBe(
viewport?.height, viewport?.height,
); );
for (const button of await navigation.locator('.navigation-item').all()) {
const buttonBox = await button.boundingBox();
const labelBox = await button.locator('.mdc-button__label > span').boundingBox();
expect(buttonBox).not.toBeNull();
expect(labelBox).not.toBeNull();
expect(labelBox!.x).toBeGreaterThanOrEqual(buttonBox!.x);
expect(labelBox!.x + labelBox!.width).toBeLessThanOrEqual(buttonBox!.x + buttonBox!.width);
}
} }
await navigation.getByRole('button', { name: 'Appartements' }).click(); await navigation.getByRole('button', { name: 'Appartements' }).click();
@@ -1,166 +1,204 @@
:host { :host {
display: block; display: block;
} }
.page-heading { .page-heading {
margin-bottom: 30px; margin-bottom: 30px;
} }
.content-region > mat-progress-bar {
margin-bottom: 16px; .content-region>mat-progress-bar {
margin-bottom: 16px;
} }
.refresh-error { .refresh-error {
margin-bottom: 16px; margin-bottom: 16px;
} }
.page-heading h1 { .page-heading h1 {
margin-bottom: 8px; margin-bottom: 8px;
font-size: clamp(2rem, 7vw, 3.2rem); font-size: clamp(2rem, 7vw, 3.2rem);
font-weight: 750; font-weight: 750;
letter-spacing: -0.055em; letter-spacing: -0.055em;
line-height: 1.02; line-height: 1.02;
} }
.page-heading > div > p:last-child {
margin-bottom: 0; .page-heading>div>p:last-child {
color: var(--text-muted); margin-bottom: 0;
line-height: 1.6; color: var(--text-muted);
line-height: 1.6;
} }
.page-heading .primary-action { .page-heading .primary-action {
width: 100%; width: 100%;
margin-top: 20px; margin-top: 20px;
}
.button-plus {
margin-right: 4px;
font-size: 1.25rem;
font-weight: 400;
} }
.empty-state, .empty-state,
.apartment-card { .apartment-card {
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 22px; border-radius: 22px;
background: var(--surface); background: var(--surface);
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04); box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04);
} }
.empty-state { .empty-state {
padding: 46px 22px; padding: 46px 22px;
text-align: center; text-align: center;
} }
.empty-illustration { .empty-illustration {
display: grid; display: grid;
width: 68px; width: 68px;
height: 68px; height: 68px;
place-items: center; place-items: center;
margin: 0 auto 22px; margin: 0 auto 22px;
border-radius: 16px; border-radius: 16px;
background: var(--surface-accent); background: var(--surface-accent);
color: var(--accent); color: var(--accent);
} }
.empty-illustration svg {
width: 34px; .empty-illustration fa-icon {
height: 34px; display: flex;
width: 34px;
height: 34px;
} }
.empty-illustration fa-icon svg {
width: 100%;
height: 100%;
}
.empty-state h2 { .empty-state h2 {
margin-bottom: 8px; margin-bottom: 8px;
font-size: 1.45rem; font-size: 1.45rem;
} }
.empty-state > p:not(.eyebrow) {
max-width: 440px; .empty-state>p:not(.eyebrow) {
margin: 0 auto 22px; max-width: 440px;
color: var(--text-muted); margin: 0 auto 22px;
line-height: 1.6; color: var(--text-muted);
line-height: 1.6;
} }
.empty-state .mat-mdc-outlined-button { .empty-state .mat-mdc-outlined-button {
--mdc-outlined-button-label-text-color: var(--accent); --mdc-outlined-button-label-text-color: var(--accent);
--mdc-outlined-button-outline-color: var(--border-strong); --mdc-outlined-button-outline-color: var(--border-strong);
border-radius: 12px; border-radius: 12px;
} }
.apartments-grid { .apartments-grid {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 16px; gap: 16px;
} }
.apartment-card { .apartment-card {
padding: 20px; padding: 20px;
} }
.apartment-card-topline { .apartment-card-topline {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
justify-content: space-between; justify-content: space-between;
gap: 14px; gap: 14px;
margin-bottom: 22px; margin-bottom: 22px;
} }
.property-label { .property-label {
margin: 0; margin: 0;
} }
.property-value { .property-value {
font-size: 1rem; font-size: 1rem;
font-weight: 800; font-weight: 800;
} }
.apt-info h2 { .apt-info h2 {
margin-bottom: 9px; margin-bottom: 9px;
font-size: 1.35rem; font-size: 1.35rem;
letter-spacing: -0.035em; letter-spacing: -0.035em;
} }
.apt-address { .apt-address {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 8px; gap: 8px;
margin-bottom: 12px; margin-bottom: 12px;
color: var(--text-muted); color: var(--text-muted);
font-size: 0.88rem; font-size: 0.88rem;
line-height: 1.5; line-height: 1.5;
} }
.apt-address svg {
width: 17px; .apt-address fa-icon {
height: 17px; display: flex;
flex: 0 0 17px; width: 17px;
margin-top: 2px; height: 17px;
flex: 0 0 17px;
margin-top: 2px;
} }
.apt-address fa-icon svg {
width: 100%;
height: 100%;
}
.apt-desc { .apt-desc {
margin-bottom: 0; margin-bottom: 0;
color: var(--text-muted); color: var(--text-muted);
font-size: 0.88rem; font-size: 0.88rem;
line-height: 1.55; line-height: 1.55;
} }
.apt-actions { .apt-actions {
display: flex; display: flex;
gap: 4px; gap: 4px;
justify-content: flex-end; justify-content: flex-end;
margin-top: 20px; margin-top: 20px;
padding-top: 12px; padding-top: 12px;
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
} }
.apt-actions .mat-mdc-button { .apt-actions .mat-mdc-button {
--mdc-text-button-label-text-color: var(--accent); --mdc-text-button-label-text-color: var(--accent);
} }
.apt-actions .danger-action.mat-mdc-button { .apt-actions .danger-action.mat-mdc-button {
--mdc-text-button-label-text-color: var(--danger); --mdc-text-button-label-text-color: var(--danger);
} }
.apt-actions mat-spinner { .apt-actions mat-spinner {
display: inline-block; display: inline-block;
margin-right: 6px; margin-right: 6px;
vertical-align: middle; vertical-align: middle;
} }
.card-error { .card-error {
margin: 14px 0 0; margin: 14px 0 0;
color: var(--danger); color: var(--danger);
font-size: 0.84rem; font-size: 0.84rem;
} }
@media (min-width: 700px) { @media (min-width: 700px) {
.page-heading { .page-heading {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
justify-content: space-between; justify-content: space-between;
gap: 28px; gap: 28px;
} }
.page-heading .primary-action {
width: auto; .page-heading .primary-action {
flex: 0 0 auto; width: auto;
margin-top: 0; flex: 0 0 auto;
} margin-top: 0;
.apartments-grid { }
grid-template-columns: repeat(2, minmax(0, 1fr));
} .apartments-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
} }
@media (min-width: 1180px) { @media (min-width: 1180px) {
.apartments-grid { .apartments-grid {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
} }
} }
@@ -1,117 +1,92 @@
<header class="page-heading"> <header class="page-heading">
<div> <div>
<p class="eyebrow">Portefeuille</p> <p class="eyebrow">Portefeuille</p>
<h1>Mes appartements</h1> <h1>Mes appartements</h1>
<p>Les biens rattachés à votre activité LMNP.</p> <p>Les biens rattachés à votre activité LMNP.</p>
</div> </div>
<button mat-flat-button type="button" class="primary-action" (click)="addRequested.emit()"> <button mat-flat-button type="button" class="primary-action" (click)="addRequested.emit()">
<span class="button-plus" aria-hidden="true">+</span>Ajouter un appartement <span aria-hidden="true">+</span> Ajouter un appartement
</button> </button>
</header> </header>
<div class="content-region" [attr.aria-busy]="loadState().loading"> <div class="content-region" [attr.aria-busy]="loadState().loading">
@if (loadState().loaded && loadState().loading) { @if (loadState().loaded && loadState().loading) {
<mat-progress-bar mode="indeterminate" aria-label="Actualisation des appartements" /> <mat-progress-bar mode="indeterminate" aria-label="Actualisation des appartements" />
} }
@if (loadState().loaded && loadState().error) { @if (loadState().loaded && loadState().error) {
<div class="refresh-error" role="alert"> <div class="refresh-error" role="alert">
<p>{{ loadState().error }}</p> <p>{{ loadState().error }}</p>
<button mat-button type="button" (click)="retryRequested.emit()">Réessayer</button> <button mat-button type="button" (click)="retryRequested.emit()">Réessayer</button>
</div> </div>
} }
@if (!loadState().loaded && loadState().loading) { @if (!loadState().loaded && loadState().loading) {
<div <div class="apartments-grid loading-region" aria-busy="true" aria-label="Chargement des appartements">
class="apartments-grid loading-region" @for (placeholder of [1, 2, 3]; track placeholder) {
aria-busy="true" <article class="apartment-card skeleton-card" aria-hidden="true">
aria-label="Chargement des appartements" <span class="skeleton-line skeleton-line-short"></span>
> <span class="skeleton-line skeleton-line-title"></span>
@for (placeholder of [1, 2, 3]; track placeholder) { <span class="skeleton-line"></span>
<article class="apartment-card skeleton-card" aria-hidden="true"> <span class="skeleton-line skeleton-line-medium"></span>
<span class="skeleton-line skeleton-line-short"></span> </article>
<span class="skeleton-line skeleton-line-title"></span> }
<span class="skeleton-line"></span> </div>
<span class="skeleton-line skeleton-line-medium"></span> } @else if (!loadState().loaded && loadState().error) {
</article> <article class="load-error" role="alert">
} <h2>Chargement impossible</h2>
</div> <p>{{ loadState().error }}</p>
} @else if (!loadState().loaded && loadState().error) { <button mat-stroked-button type="button" (click)="retryRequested.emit()">Réessayer</button>
<article class="load-error" role="alert"> </article>
<h2>Chargement impossible</h2> } @else if (apartments().length === 0) {
<p>{{ loadState().error }}</p> <article class="empty-state">
<button mat-stroked-button type="button" (click)="retryRequested.emit()">Réessayer</button> <div class="empty-illustration" aria-hidden="true">
</article> <fa-icon [icon]="faHouse" />
} @else if (apartments().length === 0) { </div>
<article class="empty-state"> <p class="eyebrow">Commencez ici</p>
<div class="empty-illustration" aria-hidden="true"> <h2>Aucun appartement enregistré</h2>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"> <p>Ajoutez votre premier bien pour construire votre portefeuille.</p>
<path d="m4 10 8-6 8 6v10H4z" /> <button mat-stroked-button type="button" (click)="addRequested.emit()">
<path d="M9 20v-6h6v6" /> Ajouter un appartement
</svg> </button>
</div> </article>
<p class="eyebrow">Commencez ici</p> } @else {
<h2>Aucun appartement enregistré</h2> <div class="apartments-grid">
<p>Ajoutez votre premier bien pour construire votre portefeuille.</p> @for (apt of apartments(); track apt.id) {
<button mat-stroked-button type="button" (click)="addRequested.emit()"> <article class="apartment-card" [attr.aria-busy]="!!apt.id && deletingIds().has(apt.id)">
Ajouter un appartement <div class="apartment-card-topline">
</button> <span class="property-label">Bien immobilier</span>
</article> <span class="property-value">{{ apt.purchasePrice.toLocaleString('fr-FR') }} €</span>
} @else { </div>
<div class="apartments-grid"> <div class="apt-info">
@for (apt of apartments(); track apt.id) { <h2>{{ apt.name }}</h2>
<article class="apartment-card" [attr.aria-busy]="!!apt.id && deletingIds().has(apt.id)"> <p class="apt-address">
<div class="apartment-card-topline"> <fa-icon [icon]="faLocationDot" aria-hidden="true" />
<span class="property-label">Bien immobilier</span> {{ apt.address }}
<span class="property-value">{{ apt.purchasePrice.toLocaleString('fr-FR') }} €</span> </p>
</div> @if (apt.description) {
<div class="apt-info"> <p class="apt-desc">{{ apt.description }}</p>
<h2>{{ apt.name }}</h2> }
<p class="apt-address"> </div>
<svg @if (apt.id && deleteErrors()[apt.id]) {
viewBox="0 0 24 24" <p class="card-error" role="alert">{{ deleteErrors()[apt.id] }}</p>
fill="none" }
stroke="currentColor" <div class="apt-actions" aria-live="polite">
stroke-width="1.8" <button mat-button type="button" [disabled]="!!apt.id && deletingIds().has(apt.id)"
aria-hidden="true" (click)="editRequested.emit(apt)">
> Modifier
<path d="M20 10c0 5-8 11-8 11S4 15 4 10a8 8 0 1 1 16 0Z" /> </button>
<circle cx="12" cy="10" r="2.5" /> @if (apt.id) {
</svg> <button mat-button type="button" class="danger-action" [disabled]="deletingIds().has(apt.id)"
{{ apt.address }} (click)="deleteRequested.emit(apt.id)">
</p> @if (deletingIds().has(apt.id)) {
@if (apt.description) { <mat-spinner diameter="16" aria-hidden="true" /> Suppression…
<p class="apt-desc">{{ apt.description }}</p> } @else {
} Supprimer
</div> }
@if (apt.id && deleteErrors()[apt.id]) { </button>
<p class="card-error" role="alert">{{ deleteErrors()[apt.id] }}</p> }
} </div>
<div class="apt-actions" aria-live="polite"> </article>
<button }
mat-button </div>
type="button" }
[disabled]="!!apt.id && deletingIds().has(apt.id)"
(click)="editRequested.emit(apt)"
>
Modifier
</button>
@if (apt.id) {
<button
mat-button
type="button"
class="danger-action"
[disabled]="deletingIds().has(apt.id)"
(click)="deleteRequested.emit(apt.id)"
>
@if (deletingIds().has(apt.id)) {
<mat-spinner diameter="16" aria-hidden="true" /> Suppression…
} @else {
Supprimer
}
</button>
}
</div>
</article>
}
</div>
}
</div> </div>
@@ -1,4 +1,6 @@
import { Component, input, output } from '@angular/core'; import { Component, input, output } from '@angular/core';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { faHouse, faLocationDot } from '@fortawesome/free-solid-svg-icons';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
@@ -7,11 +9,13 @@ import { LoadState } from '../../services/workspace-resource';
@Component({ @Component({
selector: 'app-apartment-list', selector: 'app-apartment-list',
imports: [MatButtonModule, MatProgressBarModule, MatProgressSpinnerModule], imports: [FontAwesomeModule, MatButtonModule, MatProgressBarModule, MatProgressSpinnerModule],
templateUrl: './apartment-list.html', templateUrl: './apartment-list.html',
styleUrl: './apartment-list.css', styleUrl: './apartment-list.css',
}) })
export class ApartmentList { export class ApartmentList {
readonly faHouse = faHouse;
readonly faLocationDot = faLocationDot;
readonly apartments = input.required<Apartment[]>(); readonly apartments = input.required<Apartment[]>();
readonly loadState = input.required<LoadState>(); readonly loadState = input.required<LoadState>();
readonly deletingIds = input<ReadonlySet<string>>(new Set()); readonly deletingIds = input<ReadonlySet<string>>(new Set());
+14 -8
View File
@@ -1,24 +1,30 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { faHouse } from '@fortawesome/free-solid-svg-icons';
@Component({ @Component({
selector: 'app-brand-mark', selector: 'app-brand-mark',
template: ` imports: [FontAwesomeModule],
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"> template: ` <fa-icon [icon]="faHouse" /> `,
<path d="m4 10 8-6 8 6v9a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1z" />
<path d="M9 20v-6h6v6" />
</svg>
`,
styles: ` styles: `
:host { :host {
display: grid; display: grid;
place-items: center; place-items: center;
} }
svg { fa-icon {
display: flex;
width: 55%; width: 55%;
height: 55%; height: 55%;
} }
fa-icon svg {
width: 100%;
height: 100%;
}
`, `,
host: { 'aria-hidden': 'true' }, host: { 'aria-hidden': 'true' },
}) })
export class BrandMark {} export class BrandMark {
readonly faHouse = faHouse;
}
@@ -104,10 +104,15 @@
background: var(--surface); background: var(--surface);
color: var(--accent); color: var(--accent);
} }
.feature-icon svg { .feature-icon fa-icon {
display: flex;
width: 24px; width: 24px;
height: 24px; height: 24px;
} }
.feature-icon fa-icon svg {
width: 100%;
height: 100%;
}
.user-profile { .user-profile {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -13,10 +13,7 @@
[attr.aria-busy]="workspace.apartmentLoadState().loading" [attr.aria-busy]="workspace.apartmentLoadState().loading"
> >
<div class="feature-icon" aria-hidden="true"> <div class="feature-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"> <fa-icon [icon]="faTableCellsLarge" />
<path d="M4 21V5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v16M2 21h20" />
<path d="M8 8h8M8 12h8M8 16h4" />
</svg>
</div> </div>
<div> <div>
<p class="eyebrow">Votre portefeuille</p> <p class="eyebrow">Votre portefeuille</p>
@@ -1,4 +1,6 @@
import { Component, computed, inject, signal } from '@angular/core'; import { Component, computed, inject, signal } from '@angular/core';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { faTableCellsLarge } from '@fortawesome/free-solid-svg-icons';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@@ -8,11 +10,12 @@ import { WorkspaceService } from '../../services/workspace.service';
@Component({ @Component({
selector: 'app-dashboard-page', selector: 'app-dashboard-page',
imports: [MatButtonModule, MatProgressBarModule], imports: [FontAwesomeModule, MatButtonModule, MatProgressBarModule],
templateUrl: './dashboard-page.html', templateUrl: './dashboard-page.html',
styleUrl: './dashboard-page.css', styleUrl: './dashboard-page.css',
}) })
export class DashboardPage { export class DashboardPage {
readonly faTableCellsLarge = faTableCellsLarge;
private readonly http = inject(HttpClient); private readonly http = inject(HttpClient);
private readonly router = inject(Router); private readonly router = inject(Router);
readonly session = inject(SessionService); readonly session = inject(SessionService);
@@ -61,10 +61,15 @@
background: var(--surface-accent); background: var(--surface-accent);
color: var(--accent); color: var(--accent);
} }
.settings-icon svg { .settings-icon fa-icon {
display: flex;
width: 24px; width: 24px;
height: 24px; height: 24px;
} }
.settings-icon fa-icon svg {
width: 100%;
height: 100%;
}
.settings-copy h2 { .settings-copy h2 {
margin-bottom: 5px; margin-bottom: 5px;
} }
@@ -8,10 +8,7 @@
</header> </header>
<article class="settings-card" aria-labelledby="appearance-heading"> <article class="settings-card" aria-labelledby="appearance-heading">
<div class="settings-copy"> <div class="settings-copy">
<span class="settings-icon" aria-hidden="true" <span class="settings-icon" aria-hidden="true"><fa-icon [icon]="faMoon" /></span>
><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<path d="M12 3a9 9 0 1 0 9 9c0-.5-.04-1-.12-1.47A7 7 0 0 1 12 3Z" /></svg
></span>
<div> <div>
<p class="eyebrow">Interface</p> <p class="eyebrow">Interface</p>
<h2 id="appearance-heading">Apparence</h2> <h2 id="appearance-heading">Apparence</h2>
@@ -47,11 +44,7 @@
aria-labelledby="session-heading" aria-labelledby="session-heading"
> >
<div class="settings-copy"> <div class="settings-copy">
<span class="settings-icon" aria-hidden="true" <span class="settings-icon" aria-hidden="true"><fa-icon [icon]="faUser" /></span>
><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<circle cx="12" cy="8" r="4" />
<path d="M4 21a8 8 0 0 1 16 0" /></svg
></span>
<div> <div>
<p class="eyebrow">Compte</p> <p class="eyebrow">Compte</p>
<h2 id="session-heading">Session</h2> <h2 id="session-heading">Session</h2>
@@ -1,4 +1,6 @@
import { Component, inject } from '@angular/core'; import { Component, inject } from '@angular/core';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { faMoon, faUser } from '@fortawesome/free-solid-svg-icons';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatRadioModule } from '@angular/material/radio'; import { MatRadioModule } from '@angular/material/radio';
import { SessionService } from '../../services/session.service'; import { SessionService } from '../../services/session.service';
@@ -6,13 +8,17 @@ import { ThemeService } from '../../services/theme.service';
@Component({ @Component({
selector: 'app-settings-page', selector: 'app-settings-page',
imports: [MatButtonModule, MatRadioModule], imports: [FontAwesomeModule, MatButtonModule, MatRadioModule],
templateUrl: './settings-page.html', templateUrl: './settings-page.html',
styleUrl: './settings-page.css', styleUrl: './settings-page.css',
}) })
export class SettingsPage { export class SettingsPage {
readonly faMoon = faMoon;
readonly faUser = faUser;
readonly session = inject(SessionService); readonly session = inject(SessionService);
readonly theme = inject(ThemeService); readonly theme = inject(ThemeService);
logout(): void { window.location.href = '/logout'; } logout(): void {
window.location.href = '/logout';
}
} }
@@ -51,11 +51,16 @@
width: 100%; width: 100%;
max-width: 360px; max-width: 360px;
} }
.sign-in-action svg { .sign-in-action fa-icon {
display: flex;
width: 19px; width: 19px;
height: 19px; height: 19px;
margin-left: 8px; margin-left: 8px;
} }
.sign-in-action fa-icon svg {
width: 100%;
height: 100%;
}
.security-note { .security-note {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -19,15 +19,7 @@
<button mat-flat-button class="primary-action sign-in-action" (click)="loginRequested.emit()"> <button mat-flat-button class="primary-action sign-in-action" (click)="loginRequested.emit()">
Se connecter avec OIDC Se connecter avec OIDC
<svg <fa-icon [icon]="faArrowRight" aria-hidden="true" />
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
aria-hidden="true"
>
<path d="M5 12h14M13 6l6 6-6 6" />
</svg>
</button> </button>
<p class="security-note"> <p class="security-note">
@@ -1,13 +1,16 @@
import { Component, output } from '@angular/core'; import { Component, output } from '@angular/core';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { faArrowRight } from '@fortawesome/free-solid-svg-icons';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { BrandMark } from '../brand-mark'; import { BrandMark } from '../brand-mark';
@Component({ @Component({
selector: 'app-sign-in-page', selector: 'app-sign-in-page',
imports: [BrandMark, MatButtonModule], imports: [BrandMark, FontAwesomeModule, MatButtonModule],
templateUrl: './sign-in-page.html', templateUrl: './sign-in-page.html',
styleUrl: './sign-in-page.css', styleUrl: './sign-in-page.css',
}) })
export class SignInPage { export class SignInPage {
readonly faArrowRight = faArrowRight;
readonly loginRequested = output<void>(); readonly loginRequested = output<void>();
} }
@@ -95,6 +95,10 @@
width: 16px; width: 16px;
margin-right: 8px; margin-right: 8px;
} }
.attachment-download-icon svg {
width: 100%;
height: 100%;
}
.attachment-file-name { .attachment-file-name {
min-width: 0; min-width: 0;
overflow-wrap: anywhere; overflow-wrap: anywhere;
@@ -107,6 +111,14 @@
padding: 0 5px; padding: 0 5px;
font-size: 0.9rem; font-size: 0.9rem;
} }
.attachment-delete fa-icon {
width: 1em;
height: 1em;
}
.attachment-delete fa-icon svg {
width: 100%;
height: 100%;
}
.upload-action { .upload-action {
display: block; display: block;
min-height: 40px; min-height: 40px;
@@ -1,156 +1,212 @@
:host { :host {
display: block; display: block;
} }
.app-navigation { .app-navigation {
position: fixed; position: fixed;
z-index: 20; z-index: 20;
right: 0; right: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
height: calc(72px + env(safe-area-inset-bottom)); height: calc(72px + env(safe-area-inset-bottom));
padding: 6px 10px env(safe-area-inset-bottom); padding: 6px 10px env(safe-area-inset-bottom);
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
background: color-mix(in srgb, var(--surface) 94%, transparent); background: color-mix(in srgb, var(--surface) 94%, transparent);
box-shadow: 0 -10px 30px rgba(25, 40, 35, 0.06); box-shadow: 0 -10px 30px rgba(25, 40, 35, 0.06);
backdrop-filter: blur(18px); backdrop-filter: blur(18px);
} }
.sidebar-brand, .sidebar-brand,
.sidebar-user { .sidebar-user {
display: none; display: none;
} }
.navigation-items { .navigation-items {
display: grid; display: grid;
height: 100%; height: 100%;
grid-template-columns: repeat(5, minmax(0, 1fr)); grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 4px; gap: 4px;
} }
.navigation-item.mat-mdc-button { .navigation-item.mat-mdc-button {
--mdc-text-button-label-text-color: var(--text-muted); --mdc-text-button-label-text-color: var(--text-muted);
min-width: 0; width: 100%;
height: 60px; min-width: 0;
border-radius: 14px; height: 60px;
padding: 5px 4px; border-radius: 14px;
font-weight: 650; overflow: hidden;
letter-spacing: 0; padding: 5px 4px;
font-weight: 650;
letter-spacing: 0;
} }
.navigation-item .mat-mdc-button-touch-target { .navigation-item .mat-mdc-button-touch-target {
height: 60px; height: 60px;
} }
.navigation-item svg {
width: 21px; .navigation-item fa-icon {
height: 21px; display: flex;
margin: 0 auto 4px; width: 21px;
height: 21px;
margin: 0 auto 4px;
} }
.navigation-item fa-icon svg {
width: 100%;
height: 100%;
}
.navigation-item span { .navigation-item span {
display: block; display: block;
overflow: hidden; overflow: hidden;
font-size: 0.66rem; font-size: 0.66rem;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.navigation-item.active { .navigation-item.active {
--mdc-text-button-label-text-color: var(--accent); --mdc-text-button-label-text-color: var(--accent);
background: var(--surface-accent); background: var(--surface-accent);
} }
@media (max-width: 959px) {
.navigation-item .mdc-button__label {
display: flex;
width: 100%;
height: 100%;
min-width: 0;
flex-direction: column;
align-items: center;
justify-content: center;
}
.navigation-item span {
width: 100%;
min-width: 0;
overflow-wrap: break-word;
font-size: 0.62rem;
line-height: 1.05;
text-align: center;
white-space: normal;
}
}
@media (min-width: 960px) { @media (min-width: 960px) {
.app-navigation { .app-navigation {
top: 0; top: 0;
right: auto; right: auto;
bottom: auto; bottom: auto;
width: 256px; width: 256px;
height: 100vh; height: 100vh;
padding: 26px 18px; padding: 26px 18px;
border-top: 0; border-top: 0;
border-right: 1px solid var(--border); border-right: 1px solid var(--border);
background: var(--surface); background: var(--surface);
box-shadow: none; box-shadow: none;
backdrop-filter: none; backdrop-filter: none;
} }
.sidebar-brand {
display: flex; .sidebar-brand {
align-items: center; display: flex;
gap: 12px; align-items: center;
padding: 0 8px 32px; gap: 12px;
} padding: 0 8px 32px;
.sidebar-brand > div { }
display: flex;
flex-direction: column; .sidebar-brand>div {
font-size: 0.86rem; display: flex;
line-height: 1.15; flex-direction: column;
} font-size: 0.86rem;
.sidebar-brand strong { line-height: 1.15;
font-size: 1rem; }
}
.sidebar-brand span:last-child { .sidebar-brand strong {
color: var(--text-muted); font-size: 1rem;
} }
.navigation-items {
display: flex; .sidebar-brand span:last-child {
height: auto; color: var(--text-muted);
flex-direction: column; }
gap: 6px;
} .navigation-items {
.navigation-item.mat-mdc-button { display: flex;
width: 100%; height: auto;
height: 48px; flex-direction: column;
justify-content: flex-start; gap: 6px;
border-radius: 13px; }
padding: 0 14px;
} .navigation-item.mat-mdc-button {
.navigation-item .mat-mdc-button-touch-target { width: 100%;
height: 48px; height: 48px;
} justify-content: flex-start;
.navigation-item .mdc-button__label { border-radius: 13px;
display: flex; padding: 0 14px;
align-items: center; }
gap: 12px;
} .navigation-item .mat-mdc-button-touch-target {
.navigation-item svg { height: 48px;
width: 20px; }
height: 20px;
margin: 0; .navigation-item .mdc-button__label {
} display: flex;
.navigation-item span { align-items: center;
font-size: 0.86rem; gap: 12px;
} }
.sidebar-user {
position: absolute; .navigation-item fa-icon {
right: 18px; display: flex;
bottom: 24px; width: 20px;
left: 18px; height: 20px;
display: grid; margin: 0;
grid-template-columns: 36px minmax(0, 1fr); }
gap: 10px;
padding: 14px; .navigation-item span {
border: 1px solid var(--border); font-size: 0.86rem;
border-radius: 16px; }
background: var(--surface-soft);
} .sidebar-user {
.sidebar-user > div:nth-child(2) { position: absolute;
display: flex; right: 18px;
min-width: 0; bottom: 24px;
flex-direction: column; left: 18px;
justify-content: center; display: grid;
} grid-template-columns: 36px minmax(0, 1fr);
.sidebar-user strong, gap: 10px;
.sidebar-user span { padding: 14px;
overflow: hidden; border: 1px solid var(--border);
text-overflow: ellipsis; border-radius: 16px;
white-space: nowrap; background: var(--surface-soft);
} }
.sidebar-user strong {
font-size: 0.82rem; .sidebar-user>div:nth-child(2) {
} display: flex;
.sidebar-user span { min-width: 0;
color: var(--text-muted); flex-direction: column;
font-size: 0.69rem; justify-content: center;
} }
.logout-action.mat-mdc-button {
--mdc-text-button-label-text-color: var(--text-muted); .sidebar-user strong,
grid-column: 1 / -1; .sidebar-user span {
height: 34px; overflow: hidden;
border-top: 1px solid var(--border); text-overflow: ellipsis;
border-radius: 0; white-space: nowrap;
font-size: 0.75rem; }
}
.sidebar-user strong {
font-size: 0.82rem;
}
.sidebar-user span {
color: var(--text-muted);
font-size: 0.69rem;
}
.logout-action.mat-mdc-button {
--mdc-text-button-label-text-color: var(--text-muted);
grid-column: 1 / -1;
height: 34px;
border-top: 1px solid var(--border);
border-radius: 0;
font-size: 0.75rem;
}
} }
@@ -12,18 +12,7 @@
routerLink="/dashboard" routerLink="/dashboard"
routerLinkActive="active" routerLinkActive="active"
> >
<svg <fa-icon [icon]="faTableCellsLarge" aria-hidden="true" />
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.8"
aria-hidden="true"
>
<rect x="3" y="3" width="7" height="7" rx="2" />
<rect x="14" y="3" width="7" height="7" rx="2" />
<rect x="3" y="14" width="7" height="7" rx="2" />
<rect x="14" y="14" width="7" height="7" rx="2" />
</svg>
<span>Tableau de bord</span> <span>Tableau de bord</span>
</button> </button>
<button <button
@@ -33,16 +22,7 @@
routerLink="/apartments" routerLink="/apartments"
routerLinkActive="active" routerLinkActive="active"
> >
<svg <fa-icon [icon]="faBuilding" aria-hidden="true" />
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.8"
aria-hidden="true"
>
<path d="M4 21V5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v16" />
<path d="M2 21h20M8 7h2M14 7h2M8 11h2M14 11h2M8 15h2M14 15h2" />
</svg>
<span>Appartements</span> <span>Appartements</span>
</button> </button>
<button <button
@@ -52,18 +32,7 @@
routerLink="/tenants" routerLink="/tenants"
routerLinkActive="active" routerLinkActive="active"
> >
<svg <fa-icon [icon]="faUsers" aria-hidden="true" />
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.8"
aria-hidden="true"
>
<circle cx="9" cy="8" r="3" />
<path
d="M3.5 20v-2.5A4.5 4.5 0 0 1 8 13h2a4.5 4.5 0 0 1 4.5 4.5V20M16 5.5a3 3 0 0 1 0 5.8M17 14a4.5 4.5 0 0 1 3.5 4.4V20"
/>
</svg>
<span>Locataires</span> <span>Locataires</span>
</button> </button>
<button <button
@@ -73,16 +42,7 @@
routerLink="/leases" routerLink="/leases"
routerLinkActive="active" routerLinkActive="active"
> >
<svg <fa-icon [icon]="faFileLines" aria-hidden="true" />
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.8"
aria-hidden="true"
>
<path d="M6 3h9l4 4v14H6z" />
<path d="M14 3v5h5M9 12h7M9 16h7" />
</svg>
<span>Baux</span> <span>Baux</span>
</button> </button>
<button <button
@@ -92,18 +52,7 @@
routerLink="/settings" routerLink="/settings"
routerLinkActive="active" routerLinkActive="active"
> >
<svg <fa-icon [icon]="faGear" aria-hidden="true" />
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.8"
aria-hidden="true"
>
<circle cx="12" cy="12" r="3" />
<path
d="M19.4 15a1.7 1.7 0 0 0 .34 1.88l.06.06-2.83 2.83-.06-.06A1.7 1.7 0 0 0 15 19.4a1.7 1.7 0 0 0-1 .6 1.7 1.7 0 0 0-.4 1.1V21h-4v-.09A1.7 1.7 0 0 0 8.6 19.4a1.7 1.7 0 0 0-1.88.34l-.06.06-2.83-2.83.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-.6-1 1.7 1.7 0 0 0-1.1-.4H3v-4h.09A1.7 1.7 0 0 0 4.6 8.6a1.7 1.7 0 0 0-.34-1.88l-.06-.06 2.83-2.83.06.06A1.7 1.7 0 0 0 9 4.6a1.7 1.7 0 0 0 1-.6 1.7 1.7 0 0 0 .4-1.1V3h4v.09A1.7 1.7 0 0 0 15.4 4.6a1.7 1.7 0 0 0 1.88-.34l.06-.06 2.83 2.83-.06.06A1.7 1.7 0 0 0 19.4 9c.16.38.37.72.6 1 .3.3.7.45 1.1.45h.09v4h-.09c-.4 0-.8.15-1.1.45-.23.28-.44.62-.6 1Z"
/>
</svg>
<span>Réglages</span> <span>Réglages</span>
</button> </button>
</nav> </nav>
@@ -1,4 +1,12 @@
import { Component, inject, input, output } from '@angular/core'; import { Component, inject, input, output } from '@angular/core';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import {
faBuilding,
faFileLines,
faGear,
faTableCellsLarge,
faUsers,
} from '@fortawesome/free-solid-svg-icons';
import { RouterLink, RouterLinkActive } from '@angular/router'; import { RouterLink, RouterLinkActive } from '@angular/router';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { BrandMark } from '../brand-mark'; import { BrandMark } from '../brand-mark';
@@ -7,11 +15,16 @@ import { SessionService } from '../../services/session.service';
@Component({ @Component({
selector: 'app-workspace-navigation', selector: 'app-workspace-navigation',
imports: [BrandMark, MatButtonModule, RouterLink, RouterLinkActive], imports: [BrandMark, FontAwesomeModule, MatButtonModule, RouterLink, RouterLinkActive],
templateUrl: './workspace-navigation.html', templateUrl: './workspace-navigation.html',
styleUrl: './workspace-navigation.css', styleUrl: './workspace-navigation.css',
}) })
export class WorkspaceNavigation { export class WorkspaceNavigation {
readonly faTableCellsLarge = faTableCellsLarge;
readonly faBuilding = faBuilding;
readonly faUsers = faUsers;
readonly faFileLines = faFileLines;
readonly faGear = faGear;
readonly userInitial = input.required<string>(); readonly userInitial = input.required<string>();
readonly logoutRequested = output<void>(); readonly logoutRequested = output<void>();
readonly session = inject(SessionService); readonly session = inject(SessionService);
+8
View File
@@ -98,6 +98,14 @@ svg {
display: block; display: block;
} }
fa-icon {
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
vertical-align: middle;
}
h1, h1,
h2, h2,
h3, h3,