Files
decePubClient/Components/Styleguide/FeedbackSection.razor
T
thepraandClaude Opus 5.5 95428430c0 Adopt b2b.next's client patterns: services, page state, feedback
Ported from the owner's b2b.next and adapted from MudBlazor to the daisyUI
components:
- LocalizableComponentBase provides CascadingState, Navigation, Toast,
  IsLoading/IsDefaultDisabled, WhenOnline/StartOnlinePolling, and after-render
  jobs that actually run.
- CascadingState starts with initialised state and polls the online status
  with a PeriodicTimer. It reports errors through ILoggingService plus a toast,
  and disposes what it holds.
- New pieces:
  - ToastService + ToastHost, the snackbar, with AddResponseError/Success/
    Warning/Exception in SUtility;
  - ErrorViewer, which shows the validation messages plus AdditionalError(s);
  - LoggingService, which writes to the console and the IndexedDB client log.
- HttpService is rebuilt and registered:
  - auth levels Get/Post/Delete, *Anon and *TotallyAnon;
  - retry with backoff;
  - classified failures (FailureCodes) instead of exceptions;
  - HTTP 410 on cancel;
  - the "default" client.
  ReadWebResult and DefaultReadFromJsonAsync read its responses.
- ClientJsonContext is source-generated, and SUtility.DefaultSerializer and
  Blazored.LocalStorage use it.
- WebResult gains a C# 14 IsInvalid extension property.

The code follows the imported rules:
- Every service and async helper takes a CancellationToken last.
- Primary constructors replace constructor-plus-fields.
- MessagesService and IStorage return Task<WebResult> / Task<List<T>>.
  MessagesService is still mock, building its results in memory; the seed
  feed and thread moved to Faker.
- Index and ExpandMessage follow the page pattern: an offline guard with
  WhenOnline, LoadData, and early returns with toasts on the results they used
  to ignore. ExpandMessage reloads on route change and shows "not found"
  instead of a null message.
- Model and state collections are [], and the fake identity defaults moved
  out of the models.
- Components:
  - no ?. chains over initialised state;
  - [Inject] instead of @inject;
  - IsDefaultDisabled on the actions;
  - autocomplete on every text input;
  - Localizer[key, args] instead of string.Format;
  - the localizer no longer leaks its inner _pLocalizer.
- Settings → General gains a language picker (English/Italiano), and
  SetDefaultCulture limits the culture to those two.

Bugs fixed along the way:
- A compose with untouched (null) content passed the empty check.
- File errors were joined with <br/> and rendered as markup.
- ClientLogs.ErrorMessage() threw on warnings.
- CoalescingStringLocalizer[key, args] showed a raw "{0}" for missing keys.
- AppStatusService exposed a call to a JS function that doesn't exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
2026-10-03 15:26:21 +02:00

237 lines
8.9 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@* Styleguide: every Feedback component (Components/Daisy/Feedback) with its main variants. *@
<div class="grid gap-8 lg:grid-cols-2">
<section class="flex flex-col gap-3">
<h3 class="text-lg font-bold">DAlert</h3>
<DAlert><Icon><i class="ion-md-information-circle text-xl"></i></Icon><ChildContent>Default alert</ChildContent></DAlert>
@foreach (var (color, icon) in alertColors)
{
<DAlert Color="color"><Icon><i class="@icon text-xl"></i></Icon><ChildContent>@color alert</ChildContent></DAlert>
}
@foreach (var variant in new[] { DAlertVariant.Soft, DAlertVariant.Outline, DAlertVariant.Dash })
{
<div class="grid gap-2 sm:grid-cols-2">
<DAlert Variant="variant">@variant</DAlert>
@foreach (var (color, _) in alertColors)
{
<DAlert Variant="variant" Color="color">@variant @color</DAlert>
}
</div>
}
<DAlert Direction="DDirection.Vertical" Color="DAlertColor.Info">
<Icon><i class="ion-md-information-circle text-xl"></i></Icon>
<ChildContent>Vertical, with actions</ChildContent>
<Actions><DButton Size="DSize.Sm">Deny</DButton><DButton Size="DSize.Sm" Color="DColor.Primary">Accept</DButton></Actions>
</DAlert>
@if (alertDismissed)
{
<DButton Size="DSize.Sm" class="self-start" OnClick="() => alertDismissed = false">Bring the alert back</DButton>
}
else
{
<DAlert Responsive="true">
<Icon><i class="ion-md-notifications text-xl"></i></Icon>
<ChildContent>
<h4 class="font-bold">Responsive, with a heading</h4>
<div class="text-xs">Vertical below sm, horizontal above. Dismiss me.</div>
</ChildContent>
<Actions><DButton Size="DSize.Sm" OnClick="() => alertDismissed = true">Dismiss</DButton></Actions>
</DAlert>
}
</section>
<section class="flex flex-col gap-3">
<h3 class="text-lg font-bold">DLoading</h3>
<div class="flex flex-wrap items-center gap-4">
@foreach (var variant in Enum.GetValues<DLoadingVariant>())
{
<DLoading Variant="variant" Size="DSize.Lg" title="@variant.ToString()" />
}
</div>
<div class="flex flex-wrap items-center gap-4">
@foreach (var size in new[] { DSize.Xs, DSize.Sm, DSize.Md, DSize.Lg, DSize.Xl })
{
<DLoading Variant="DLoadingVariant.Dots" Size="size" />
}
</div>
<div class="flex flex-wrap items-center gap-4">
@foreach (var color in colors)
{
<DLoading Variant="DLoadingVariant.Ring" Color="color" title="@color.ToString()" />
}
</div>
<DButton class="self-start" Disabled="true"><DLoading Size="DSize.Sm" /> Loading in a button</DButton>
</section>
<section class="flex flex-col gap-3">
<h3 class="text-lg font-bold">DProgress</h3>
<div class="flex flex-wrap items-center gap-2">
<DButton Size="DSize.Sm" OnClick="() => progress = Math.Max(0, progress - 10)">−10</DButton>
<DButton Size="DSize.Sm" OnClick="() => progress = Math.Min(100, progress + 10)">+10</DButton>
<span class="text-sm">@progress%</span>
</div>
<DProgress Value="progress" />
@for (var i = 0; i < colors.Length; i++)
{
<DProgress Value="10 + i * 12" Color="colors[i]" title="@colors[i].ToString()" />
}
<p class="text-xs opacity-60">Indeterminate (Value null)</p>
<DProgress Color="DColor.Primary" />
<DProgress Value="0.25" Max="1" Color="DColor.Accent" class="w-56" />
</section>
<section class="flex flex-col gap-3">
<h3 class="text-lg font-bold">DRadialProgress</h3>
<div class="flex flex-wrap items-center gap-4">
<DRadialProgress Value="progress" Color="DColor.Primary" />
@foreach (var value in new[] { 0, 20, 60, 80, 100 })
{
<DRadialProgress Value="value" />
}
</div>
<div class="flex flex-wrap items-center gap-4">
@foreach (var color in colors)
{
<DRadialProgress Value="70" Color="color" Diameter="3.5rem" title="@color.ToString()" />
}
</div>
<div class="flex flex-wrap items-center gap-4">
<DRadialProgress Value="progress" Diameter="8rem" Thickness="2px" />
<DRadialProgress Value="progress" Diameter="8rem" Thickness="1.5rem" Color="DColor.Secondary" />
<DRadialProgress Value="progress" class="border-4 border-primary bg-primary text-primary-content" />
<DRadialProgress Value="3" Max="4"><i class="ion-md-star text-2xl"></i></DRadialProgress>
</div>
</section>
<section class="flex flex-col gap-3">
<h3 class="text-lg font-bold">DSkeleton</h3>
<div class="flex w-64 flex-col gap-4">
<div class="flex items-center gap-4">
<DSkeleton class="h-16 w-16 shrink-0 rounded-full" />
<div class="flex flex-col gap-4">
<DSkeleton class="h-4 w-20" />
<DSkeleton class="h-4 w-28" />
</div>
</div>
<DSkeleton class="h-32 w-full" />
</div>
<DSkeleton IsText="true">Loading data, please wait…</DSkeleton>
</section>
<section class="flex flex-col gap-3">
<h3 class="text-lg font-bold">DToast</h3>
<p class="text-xs opacity-60">Previews pinned with class="absolute" inside a box</p>
<div class="relative h-48 overflow-hidden rounded-box border border-base-300">
<DToast Horizontal="DToastHorizontal.Start" Vertical="DToastVertical.Top" class="absolute">
<DAlert Color="DAlertColor.Info">Top start</DAlert>
</DToast>
<DToast Horizontal="DToastHorizontal.Center" Vertical="DToastVertical.Middle" class="absolute">
<DAlert Color="DAlertColor.Success">Middle center</DAlert>
</DToast>
<DToast class="absolute">
<DAlert Color="DAlertColor.Warning">Bottom end (default)</DAlert>
<DAlert Color="DAlertColor.Error">Stacked</DAlert>
</DToast>
</div>
<div class="flex flex-wrap items-center gap-2">
<div class="join">
@foreach (var horizontal in Enum.GetValues<DToastHorizontal>())
{
<DButton Size="DSize.Sm" class="join-item" Active="toastHorizontal == horizontal" OnClick="() => toastHorizontal = horizontal">@horizontal</DButton>
}
</div>
<div class="join">
@foreach (var vertical in Enum.GetValues<DToastVertical>())
{
<DButton Size="DSize.Sm" class="join-item" Active="toastVertical == vertical" OnClick="() => toastVertical = vertical">@vertical</DButton>
}
</div>
<DButton Size="DSize.Sm" Color="DColor.Primary" OnClick="ShowToast">Show a toast</DButton>
</div>
<DToast Horizontal="toastHorizontal" Vertical="toastVertical" class="z-50">
@foreach (var toast in toasts)
{
<DAlert @key="toast.Id" Color="toast.Color">
<ChildContent>Toast #@toast.Id, gone in 3 s</ChildContent>
<Actions><DButton Size="DSize.Xs" Variant="DButtonVariant.Ghost" aria-label="Close" OnClick="() => toasts.Remove(toast)"><i class="ion-md-close"></i></DButton></Actions>
</DAlert>
}
</DToast>
</section>
<section class="flex flex-col gap-3">
<h3 class="text-lg font-bold">DTooltip</h3>
<div class="flex flex-wrap items-center gap-4 py-8">
@foreach (var placement in Enum.GetValues<DTooltipPlacement>())
{
<DTooltip Tip="@placement.ToString()" Placement="placement">
<DButton Size="DSize.Sm">@placement</DButton>
</DTooltip>
}
</div>
<div class="flex flex-wrap items-center gap-4 pt-8">
@foreach (var color in colors)
{
<DTooltip Tip="@color.ToString()" Color="color" IsOpen="tooltipsOpen">
<DButton Size="DSize.Sm" Color="color">@color</DButton>
</DTooltip>
}
</div>
<label class="flex items-center gap-2 text-sm">
<input type="checkbox" class="checkbox checkbox-sm" @bind="tooltipsOpen" /> Force the colour tooltips open (IsOpen)
</label>
<div class="flex flex-wrap items-center gap-4 pt-10">
@foreach (var align in Enum.GetValues<DTooltipAlign>())
{
<DTooltip Tip="@($"Aligned {align}, a longer tip")" Align="align" IsOpen="true">
<DButton Size="DSize.Sm" class="w-28">@align</DButton>
</DTooltip>
}
</div>
<div class="pt-16">
<DTooltip Color="DColor.Accent">
<TipContent>
<div class="flex items-center gap-2"><i class="ion-md-heart text-error"></i> <b>TipContent</b> markup</div>
</TipContent>
<ChildContent><DButton Size="DSize.Sm">Hover for markup</DButton></ChildContent>
</DTooltip>
</div>
</section>
</div>
@code {
static readonly DColor[] colors =
{
DColor.Neutral, DColor.Primary, DColor.Secondary, DColor.Accent, DColor.Info, DColor.Success, DColor.Warning, DColor.Error
};
static readonly (DAlertColor Color, string Icon)[] alertColors =
{
(DAlertColor.Info, "ion-md-information-circle"),
(DAlertColor.Success, "ion-md-checkmark-circle"),
(DAlertColor.Warning, "ion-md-warning"),
(DAlertColor.Error, "ion-md-close-circle")
};
static readonly DAlertColor[] toastColors = { DAlertColor.Info, DAlertColor.Success, DAlertColor.Warning, DAlertColor.Error };
record Toast(int Id, DAlertColor Color);
readonly List<Toast> toasts = [];
DToastHorizontal toastHorizontal;
DToastVertical toastVertical;
int toastCount;
bool alertDismissed;
int progress = 40;
bool tooltipsOpen;
// Blazor renders once the handler yields (toast shown) and again when it completes (toast gone).
async Task ShowToast()
{
var toast = new Toast(++toastCount, toastColors[toastCount % toastColors.Length]);
toasts.Add(toast);
await Task.Delay(3000);
toasts.Remove(toast);
}
}