Il comando della barra si adatta alla schermata

Il tasto al centro perde l'etichetta: non ne ha una sola, perche' quello
che fa lo decide la schermata sotto. Dentro un'ispezione apre le sue
azioni (nuova scheda, concludi, elimina); altrove apre una nuova
ispezione. Il glifo resta lo stesso e resta nello stesso punto: quello
che cambia e' cosa c'e' dietro, non dove mettere il dito.

Le azioni dell'ispezione lasciano quindi la testata, dove stavano dietro
un menu a tre punti, e tornano sotto il pollice.

Il collegamento passa da PageActionsService: la pagina si registra
finche' e' a video e si sfila uscendo, cosi' la barra non tiene in vita
schermate che non ci sono piu'. Chi non registra niente lascia al
comando il suo significato di default — un'ispezione conclusa non
registra, e il tasto non resta li' a non fare nulla.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 15:44:55 +02:00
parent 9703fca306
commit 13d5400104
5 changed files with 108 additions and 37 deletions
+1
View File
@@ -63,6 +63,7 @@ public static class CoreModule
builder.Services.AddSingleton<IMessenger, WeakReferenceMessenger>(); builder.Services.AddSingleton<IMessenger, WeakReferenceMessenger>();
builder.Services.AddSingleton<OnScannerService>(); builder.Services.AddSingleton<OnScannerService>();
builder.Services.AddSingleton<BackButtonService>(); builder.Services.AddSingleton<BackButtonService>();
builder.Services.AddSingleton<PageActionsService>();
} }
public void RegisterDbServices() public void RegisterDbServices()
+24 -10
View File
@@ -1,5 +1,7 @@
@implements IDisposable @implements IDisposable
@inject IDialogService Dialog @inject IDialogService Dialog
@inject PageActionsService PageActions
@using SteUp.Shared.Core.Messages.System
@* @*
Navigazione flottante in basso (STILE_UI §6.15): il contenuto le scorre Navigazione flottante in basso (STILE_UI §6.15): il contenuto le scorre
@@ -7,11 +9,11 @@
riposo, piena da selezionata, etichetta sempre visibile in entrambe. riposo, piena da selezionata, etichetta sempre visibile in entrambe.
Da 720px diventa una colonna laterale (vedi components.css). Da 720px diventa una colonna laterale (vedi components.css).
In mezzo alle due destinazioni sta l'unica azione dell'app: aprire una In mezzo alle due destinazioni sta il comando. Non e' un pulsante che
nuova ispezione. Non e' un pulsante che galleggia sopra la barra e ne galleggia sopra la barra e ne copre il centro: e' la casella centrale,
copre il centro, e' la terza casella della barra, alla stessa altezza alla stessa altezza delle altre due. Sta sempre nello stesso punto, ma
delle altre due. Vale una cosa sola dappertutto: se cambiasse significato quello che fa lo decide la schermata sotto (vedi PageActionsService):
da una pagina all'altra sarebbe un tasto da indovinare. dentro un'ispezione apre le sue azioni, altrove apre una nuova ispezione.
*@ *@
<nav class="navbar @(IsVisible ? "" : "navbar--hidden")" aria-label="Navigazione principale"> <nav class="navbar @(IsVisible ? "" : "navbar--hidden")" aria-label="Navigazione principale">
@@ -22,12 +24,11 @@
<span class="navbar__label">Ispezioni</span> <span class="navbar__label">Ispezioni</span>
</NavLink> </NavLink>
<button type="button" class="navbar__action" @onclick="NewInspection" <button type="button" class="navbar__action" @onclick="RunAction"
disabled="@(!NetworkService.IsNetworkAvailable())" disabled="@(!NetworkService.IsNetworkAvailable())"
aria-label="Nuova ispezione" aria-label="@ActionLabel"
title="@(NetworkService.IsNetworkAvailable() ? null : "Serve la connessione per aprire una nuova ispezione")"> title="@(NetworkService.IsNetworkAvailable() ? ActionLabel : "Serve la connessione")">
<i class="ri-add-line" aria-hidden="true"></i> <i class="ri-add-line" aria-hidden="true"></i>
<span class="navbar__action-label">Nuova</span>
</button> </button>
<NavLink class="navbar__item pressable" href="user" Match="NavLinkMatch.All"> <NavLink class="navbar__item pressable" href="user" Match="NavLinkMatch.All">
@@ -49,12 +50,17 @@
private bool IsOn(string route) => CurrentRoute.EqualsIgnoreCase(route); private bool IsOn(string route) => CurrentRoute.EqualsIgnoreCase(route);
private string ActionLabel => PageActions.HasActions ? "Azioni" : "Nuova ispezione";
protected override void OnInitialized() protected override void OnInitialized()
{ {
NavigationManager.LocationChanged += OnLocationChanged; NavigationManager.LocationChanged += OnLocationChanged;
PageActions.Changed += OnPageActionsChanged;
Refresh(); Refresh();
} }
private void OnPageActionsChanged() => InvokeAsync(StateHasChanged);
private void OnLocationChanged(object? sender, LocationChangedEventArgs args) => Refresh(); private void OnLocationChanged(object? sender, LocationChangedEventArgs args) => Refresh();
private void Refresh() private void Refresh()
@@ -63,6 +69,10 @@
StateHasChanged(); StateHasChanged();
} }
/// Il comando vale quello che la schermata gli ha dato; se non gliel'ha
/// dato nessuno, vale la cosa che l'app fa piu' spesso.
private Task RunAction() => PageActions.HasActions ? PageActions.Invoke() : NewInspection();
private async Task NewInspection() private async Task NewInspection()
{ {
if (!await SteupDataService.CanOpenNewInspection()) if (!await SteupDataService.CanOpenNewInspection())
@@ -77,5 +87,9 @@
await Dialog.OpenSelectShop(); await Dialog.OpenSelectShop();
} }
public void Dispose() => NavigationManager.LocationChanged -= OnLocationChanged; public void Dispose()
{
NavigationManager.LocationChanged -= OnLocationChanged;
PageActions.Changed -= OnPageActionsChanged;
}
} }
@@ -13,27 +13,20 @@
@using SteUp.Shared.Core.Interface.IntegryApi @using SteUp.Shared.Core.Interface.IntegryApi
@using SteUp.Shared.Core.Interface.LocalDb @using SteUp.Shared.Core.Interface.LocalDb
@using SteUp.Shared.Core.Interface.System @using SteUp.Shared.Core.Interface.System
@using SteUp.Shared.Core.Messages.System
@inject IIspezioniService IspezioniService @inject IIspezioniService IspezioniService
@inject IDialogService Dialog @inject IDialogService Dialog
@inject IIntegrySteupService IntegrySteupService @inject IIntegrySteupService IntegrySteupService
@inject IFileManager FileManager @inject IFileManager FileManager
@inject ILogger<IspezionePage> Logger @inject ILogger<IspezionePage> Logger
@inject PageActionsService PageActions
@implements IDisposable
<HeaderLayout Title="@Titolo" Subtitle="@Sottotitolo" Back="true"> @* Le azioni non stanno nella testata: le apre il comando al centro della
<Actions> barra in basso, che finche' questa schermata e' a video significa "azioni
@* Su un'ispezione conclusa resta solo la consultazione: niente azioni. *@ di questa ispezione" (vedi PageActionsService). Su un'ispezione conclusa
@if (!IsCompleted) non si registra nulla e resta solo la consultazione. *@
{ <HeaderLayout Title="@Titolo" Subtitle="@Sottotitolo" Back="true"/>
@* Qui le azioni sono piu' d'una e non hanno lo stesso peso: lo
sheet le mostra con nome, icona e tono, invece di nasconderle
tutte dietro lo stesso glifo. *@
<button type="button" class="appbar__icon-action" @onclick="@(() => _actionsOpen = true)"
aria-label="Azioni sull'ispezione" title="Azioni">
<i class="ri-more-2-fill" aria-hidden="true"></i>
</button>
}
</Actions>
</HeaderLayout>
<ActionSheet @bind-Open="_actionsOpen" Title="Azioni ispezione" Subtitle="@Ispezione.CodMdep"> <ActionSheet @bind-Open="_actionsOpen" Title="Azioni ispezione" Subtitle="@Ispezione.CodMdep">
<div class="stack stack-xs"> <div class="stack stack-xs">
@@ -253,7 +246,26 @@
private string Sottotitolo => Ispezione.CodMdep; private string Sottotitolo => Ispezione.CodMdep;
protected override void OnInitialized() => LoadSchede(); private IDisposable? _actionsRegistration;
protected override void OnInitialized()
{
LoadSchede();
// Su un'ispezione conclusa non c'e' niente da fare: il comando della
// barra si tiene il suo significato di default invece di aprire uno
// sheet di azioni tutte spente.
if (!IsCompleted)
_actionsRegistration = PageActions.Register(() => InvokeAsync(OpenActions));
}
private void OpenActions()
{
_actionsOpen = true;
StateHasChanged();
}
public void Dispose() => _actionsRegistration?.Dispose();
private void LoadSchede() private void LoadSchede()
{ {
@@ -0,0 +1,46 @@
namespace SteUp.Shared.Core.Messages.System;
/// <summary>
/// Ponte fra il comando al centro della barra di navigazione e la schermata
/// che sta sotto. Il comando sta sempre nello stesso punto — sotto il pollice,
/// dove il rilevatore lo cerca — ma quello che fa appartiene alla schermata:
/// dentro un'ispezione apre le azioni di quell'ispezione, altrove apre una
/// nuova ispezione.
///
/// La pagina si registra finché è a video e si sfila uscendo, così la barra
/// non tiene in vita un riferimento a schermate che non ci sono più. Chi non
/// registra niente lascia al comando il suo significato di default: la barra
/// non resta mai con un tasto che non fa nulla.
/// </summary>
public class PageActionsService
{
private Func<Task>? _handler;
/// Avvisa la barra che il significato del comando è cambiato, così si ridisegna.
public event Action? Changed;
public bool HasActions => _handler is not null;
public IDisposable Register(Func<Task> handler)
{
_handler = handler;
Changed?.Invoke();
return new Registration(this, handler);
}
public Task Invoke() => _handler?.Invoke() ?? Task.CompletedTask;
private sealed class Registration(PageActionsService owner, Func<Task> handler) : IDisposable
{
public void Dispose()
{
// Solo chi ha registrato per ultimo può sfilare: se una schermata
// nuova si è già registrata, la vecchia uscendo non deve spegnerla.
if (owner._handler != handler) return;
owner._handler = null;
owner.Changed?.Invoke();
}
}
}
+9 -11
View File
@@ -568,30 +568,28 @@
transition: background-color var(--duration-base) var(--ease-out); transition: background-color var(--duration-base) var(--ease-out);
} }
/* L'azione al centro: e' una casella della barra come le altre, alla stessa /* Il comando al centro: una casella della barra come le altre, alla stessa
altezza, solo piena d'accento. L'unico accento della schermata sta sul solo altezza, solo piena d'accento. Senza etichetta, perche' non ne ha una sola:
comando che crea qualcosa. */ quello che fa lo decide la schermata sotto. L'unico accento della schermata
sta sull'unico comando. */
.navbar__action { .navbar__action {
flex: 0 0 auto; flex: 0 0 auto;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: var(--space-6); aspect-ratio: 1;
min-width: var(--touch-min); min-width: var(--touch-min);
padding-inline: var(--space-md); padding: 0;
border: none; border: none;
border-radius: 20px; border-radius: 20px;
background-color: var(--primary); background-color: var(--primary);
color: var(--on-primary); color: var(--on-primary);
font-family: inherit; font-family: inherit;
font-size: var(--label-lg-size);
font-weight: 600;
white-space: nowrap;
transition: transform var(--duration-fast) var(--ease-out), transition: transform var(--duration-fast) var(--ease-out),
background-color var(--duration-fast) var(--ease-out); background-color var(--duration-fast) var(--ease-out);
} }
.navbar__action i { font-size: 22px; } .navbar__action i { font-size: 26px; }
.navbar__action:active { transform: scale(0.96); background-color: var(--primary-pressed); } .navbar__action:active { transform: scale(0.96); background-color: var(--primary-pressed); }
@@ -644,8 +642,8 @@
qualcosa incastrato fra due destinazioni. */ qualcosa incastrato fra due destinazioni. */
.navbar__action { .navbar__action {
order: -1; order: -1;
align-self: stretch; align-self: center;
height: 48px; height: 52px;
margin-bottom: var(--space-xs); margin-bottom: var(--space-xs);
} }