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
237 lines
8.9 KiB
Plaintext
237 lines
8.9 KiB
Plaintext
@* 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);
|
||
}
|
||
}
|