Rifacimento della UI secondo STILE_UI.md

Adotta lo stile condiviso delle app Integry: superfici piatte, raggi ampi,
spaziature a scala fissa, gerarchia data dallo spazio prima che dalla
decorazione. La palette storica di SteUP resta intatta; sono stati aggiunti
solo i ruoli che mancavano (workspace, surfaceMuted, divider, inkMuted e le
triplette dei toni semantici).

Fondamenta
- tokens.css: ruoli colore, scale di spazio/raggi/tipografia, curve di
  movimento e scala z-index semantica. Unica fonte di verita': pilotano
  anche le variabili CSS di MudBlazor.
- Inter variabile ospitata in locale (133KB) al posto di Nunito da Google
  Fonts: l'app lavora offline, un font che arriva dalla rete in campo non
  arriva.
- base.css: shell applicativa e comportamenti da app nativa — niente
  selezione testo, niente menu al tocco lungo, niente lampo grigio sul tap,
  niente rimbalzo elastico, niente zoom a doppio tocco, scroll con inerzia.
- mudblazor-overrides.css: MudBlazor appiattito (elevation 0, nessun tint,
  target 44/48, etichette in caso naturale).
- Rimossi Bootstrap (CSS, icone e JS) e i 996 righe di CSS con ambito per
  componente del vecchio sistema: erano tutti inutilizzati.

Struttura
- Navigazione flottante in basso con icona piena sulla voce attiva; da 720px
  diventa colonna laterale. Il MudFabMenu e' sostituito da un bottom sheet:
  stessa funzione, bersagli molto piu' grandi.
- Testata alta col titolo a sinistra, fusa con la pagina.
- Nel form scheda il salvataggio sta in una barra ancorata in fondo, non in
  coda a uno scroll lungo.
- Scelta di punto vendita e articoli a schermo intero con ricerca; la
  selezione multipla usa righe da 56px invece di un menu a tendina.
- Urgenza scelta a pillole invece che da una select.

Stato e riscontro
- StatusBadgeHelper: tono, icona ed etichetta di ogni stato in un punto solo,
  cosi' lo stesso stato si presenta identico ovunque.
- Scheletri al posto degli spinner; l'overlay bloccante dice sempre cosa sta
  facendo; gli stati vuoti suggeriscono la mossa successiva.
- Rimosso il ritardo artificiale di 250ms in InspectionCard, che non
  attendeva nulla.

DESIGN.md riscritto sul nuovo sistema.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 11:09:26 +02:00
parent 3a9174f6fc
commit 2a0054aa30
77 changed files with 4595 additions and 3060 deletions
+153 -99
View File
@@ -1,4 +1,5 @@
@using CommunityToolkit.Mvvm.Messaging
@using SteUp.Shared.Components.Layout.Sheet
@using SteUp.Shared.Components.SingleElements.MessageBox
@using SteUp.Shared.Core.Enum
@using SteUp.Shared.Core.Interface.LocalDb
@@ -9,154 +10,205 @@
@inject IMessenger Messenger
@inject IIspezioniService IspezioniService
@inject IJSRuntime Js
@implements IDisposable
<div
class="container animated-navbar @(IsVisible ? "show-nav" : "hide-nav") @(IsVisible ? PlusVisible ? "with-plus" : "without-plus" : "with-plus")">
<nav class="navbar @(IsVisible ? PlusVisible ? "with-plus" : "without-plus" : "with-plus")">
<div class="container-navbar">
<ul class="navbar-nav flex-row nav-justified align-items-center w-100 text-center">
<li class="nav-item">
<NavLink class="nav-link" href="ispezioni" Match="NavLinkMatch.All">
<div class="d-flex flex-column">
<i class="ri-file-list-3-line"></i>
<span>Ispezioni</span>
</div>
</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="user" Match="NavLinkMatch.All">
<div class="d-flex flex-column">
<i class="ri-user-line"></i>
<span>Profilo</span>
</div>
</NavLink>
</li>
</ul>
</div>
@*
Navigazione flottante in basso (STILE_UI §6.15): il contenuto le scorre
sotto, le destinazioni restano a portata di pollice. Icona outline a
riposo, piena da selezionata, etichetta sempre visibile in entrambe.
Da 720px diventa una colonna laterale (vedi components.css).
*@
@if (PlusVisible)
<nav class="navbar @(IsVisible ? "" : "navbar--hidden")" aria-label="Navigazione principale">
<NavLink class="navbar__item pressable" href="ispezioni" Match="NavLinkMatch.All">
<span class="navbar__icon">
<i class="@(IsOn("ispezioni") ? "ri-file-list-3-fill" : "ri-file-list-3-line")" aria-hidden="true"></i>
</span>
<span class="navbar__label">Ispezioni</span>
</NavLink>
<NavLink class="navbar__item pressable" href="user" Match="NavLinkMatch.All">
<span class="navbar__icon">
<i class="@(IsOn("user") ? "ri-user-fill" : "ri-user-line")" aria-hidden="true"></i>
</span>
<span class="navbar__label">Profilo</span>
</NavLink>
@if (ActionsVisible)
{
<button type="button" class="navbar__fab" @onclick="OpenActions"
aria-label="Azioni" title="Azioni">
<i class="ri-add-line" aria-hidden="true"></i>
</button>
}
</nav>
<ActionSheet @bind-Open="_sheetOpen" Title="@SheetTitle" Subtitle="@SheetSubtitle">
<div class="stack stack-xs">
@if (OnInspectionPage)
{
<MudFabMenu ButtonClass="custom-plus-button" MenuClass="custom-menu-fab" OnClick="@OnOpenMenu"
StartIcon="@Icons.Material.Filled.Add"
IconColor="Color.Primary" Color="Color.Surface" AlignItems="AlignItems.End"
Size="Size.Medium" IconSize="Size.Medium">
@if (SchedaVisible)
{
@if (!_isCompleted)
{
<MudFabMenuItem Disabled="!NetworkService.IsNetworkAvailable()" OnClick="@NewScheda"
Label="Nuova scheda" Color="Color.Surface"/>
}
@if (!_isCompleted)
{
<button type="button" class="action-tile" disabled="@(!NetworkService.IsNetworkAvailable())"
@onclick="NewScheda">
<span class="action-tile__icon action-tile__icon--primary">
<i class="ri-add-line" aria-hidden="true"></i>
</span>
<span class="action-tile__label">Nuova scheda</span>
</button>
}
<MudFabMenuItem Disabled="!NetworkService.IsNetworkAvailable()" OnClick="@DeleteInspection"
Label="Cancella ispezione" Color="Color.Surface"/>
@if (ShowCompleteInspection)
{
<button type="button" class="action-tile" disabled="@(!NetworkService.IsNetworkAvailable())"
@onclick="CompleteInspection">
<span class="action-tile__icon action-tile__icon--success">
<i class="ri-check-double-line" aria-hidden="true"></i>
</span>
<span class="action-tile__label">Concludi ispezione</span>
</button>
}
if (ShowCompleteInspection)
{
<MudFabMenuItem Disabled="!NetworkService.IsNetworkAvailable()" OnClick="@CompleteInspection"
Label="Concludi ispezione" Color="Color.Surface"/>
}
}
else
{
<MudFabMenuItem Disabled="!NetworkService.IsNetworkAvailable()" OnClick="@NewInspection"
Label="Nuova ispezione" Color="Color.Surface"/>
}
</MudFabMenu>
<button type="button" class="action-tile" disabled="@(!NetworkService.IsNetworkAvailable())"
@onclick="DeleteInspection">
<span class="action-tile__icon action-tile__icon--danger">
<i class="ri-delete-bin-line" aria-hidden="true"></i>
</span>
<span class="action-tile__label">Elimina ispezione</span>
</button>
}
else
{
<button type="button" class="action-tile" disabled="@(!NetworkService.IsNetworkAvailable())"
@onclick="NewInspection">
<span class="action-tile__icon action-tile__icon--primary">
<i class="ri-add-line" aria-hidden="true"></i>
</span>
<span class="action-tile__label">Nuova ispezione</span>
</button>
}
</nav>
</div>
<ConfirmMessageBox @ref="_messageBox" NoText="No" YesText="Si" YesColor="Color.Primary"
Message="Completando l'ispezione non sarà più possibile editarla o aggiungere ulteriori segnalazioni. Procedere?"/>
@if (!NetworkService.IsNetworkAvailable())
{
<div class="feedback feedback--warning" role="status">
<i class="feedback__icon ri-wifi-off-line" aria-hidden="true"></i>
<span>Senza connessione queste azioni non sono disponibili. Il lavoro gia' fatto resta sul dispositivo.</span>
</div>
}
</div>
</ActionSheet>
<ConfirmMessageBox @ref="_messageBox" NoText="Annulla" YesText="Concludi" YesColor="Color.Primary"
Message="Una volta conclusa, l'ispezione non sara' piu' modificabile e non potrai aggiungere altre schede. Procedere?"/>
@code
{
private bool IsVisible { get; set; } = true;
private bool PlusVisible { get; set; } = true;
private bool ActionsVisible { get; set; } = true;
private bool OnInspectionPage { get; set; }
private bool ShowCompleteInspection { get; set; }
private bool SchedaVisible { get; set; }
private bool _isCompleted;
private bool _sheetOpen;
private ConfirmMessageBox _messageBox = null!;
private static readonly string[] NavigableRoutes = ["ispezioni", "ispezione", "user"];
private string SheetTitle => OnInspectionPage ? "Azioni ispezione" : "Nuova ispezione";
private string? SheetSubtitle => OnInspectionPage
? SteupDataService.InspectionPageState.Ispezione.CodMdep
: null;
private string CurrentRoute =>
NavigationManager.Uri.Remove(0, NavigationManager.BaseUri.Length).Split('?')[0];
private bool IsOn(string route) => CurrentRoute.EqualsIgnoreCase(route);
protected override Task OnInitializedAsync()
{
NavigationManager.LocationChanged += (_, args) =>
{
var location = args.Location.Remove(0, NavigationManager.BaseUri.Length);
var newIsVisible = new List<string> { "ispezioni", "ispezione", "user" }
.Contains(location);
var newPlusVisible = new List<string> { "ispezioni", "ispezione", "user" }
.Contains(location);
if (IsVisible == newIsVisible && PlusVisible == newPlusVisible) return;
IsVisible = newIsVisible;
PlusVisible = newPlusVisible;
if (location.EqualsIgnoreCase("ispezione"))
{
var ispezione = SteupDataService.InspectionPageState.Ispezione;
if (ispezione.Stato == StatusEnum.Completata) PlusVisible = false;
}
StateHasChanged();
};
NavigationManager.LocationChanged += OnLocationChanged;
Refresh();
return Task.CompletedTask;
}
private void OnLocationChanged(object? sender, LocationChangedEventArgs args)
{
// Cambiando schermata un menu aperto non ha piu' senso.
_sheetOpen = false;
Refresh();
}
private void Refresh()
{
var route = CurrentRoute;
IsVisible = NavigableRoutes.Contains(route, StringComparer.OrdinalIgnoreCase);
OnInspectionPage = route.EqualsIgnoreCase("ispezione");
ActionsVisible = IsVisible && !route.EqualsIgnoreCase("user");
if (OnInspectionPage)
{
var ispezione = SteupDataService.InspectionPageState.Ispezione;
_isCompleted = ispezione.Stato == StatusEnum.Completata;
// "Concludi" non ha senso su un'ispezione mai inviata o gia' conclusa.
ShowCompleteInspection = !ispezione.ActivityId.IsNullOrEmpty() && !_isCompleted;
// Su un'ispezione conclusa resta solo la consultazione.
if (_isCompleted) ActionsVisible = false;
}
StateHasChanged();
}
private void OpenActions()
{
Refresh();
_sheetOpen = true;
}
private async Task NewInspection()
{
_sheetOpen = false;
if (await SteupDataService.CanOpenNewInspection())
{
_ = Dialog.OpenSelectShop();
}
else
{
await Dialog.ShowWarning("Per aprire una nuova ispezione è necessario concludere prima tutte le ispezioni aperte più vecchie di 20 giorni!");
await Dialog.ShowWarning(
"Per aprire una nuova ispezione devi prima concludere tutte le ispezioni aperte da piu' di 20 giorni.");
}
}
private async Task CompleteInspection()
{
var result = await _messageBox.ShowAsync();
_sheetOpen = false;
if (result is true)
Messenger.Send(new CompleteInspectionMessage());
var result = await _messageBox.ShowAsync();
if (result is true) Messenger.Send(new CompleteInspectionMessage());
}
private async Task NewScheda()
{
_sheetOpen = false;
var ispezione = SteupDataService.InspectionPageState.Ispezione;
var modal = await Dialog.OpenFormScheda(ispezione.CodMdep, ispezione.Data, true);
if (modal is { Canceled: false })
Messenger.Send(new NewSchedaMessage());
}
private void OnOpenMenu()
{
var location = NavigationManager.Uri.Remove(0, NavigationManager.BaseUri.Length);
SchedaVisible = new List<string> { "ispezione" }.Contains(location);
var ispezione = SteupDataService.InspectionPageState.Ispezione;
_isCompleted = ispezione.Stato == StatusEnum.Completata;
// Concludi non ha senso su un'ispezione gia completata
ShowCompleteInspection = !ispezione.ActivityId.IsNullOrEmpty() && !_isCompleted;
StateHasChanged();
if (modal is { Canceled: false }) Messenger.Send(new NewSchedaMessage());
}
private async Task DeleteInspection()
{
_sheetOpen = false;
try
{
var ispezione = SteupDataService.InspectionPageState.Ispezione;
await IspezioniService.DeleteIspezioneAsync(ispezione.CodMdep, ispezione.Data, ispezione.Rilevatore);
await Js.InvokeVoidAsync("goBack");
}
catch (Exception e)
@@ -164,4 +216,6 @@
await Dialog.ShowError(e.Message);
}
}
}
public void Dispose() => NavigationManager.LocationChanged -= OnLocationChanged;
}