Files
decePubClient/Components/Styleguide/DataInputSection.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

384 lines
17 KiB
Plaintext

@using System.ComponentModel.DataAnnotations
@* Styleguide: the Data input components (Input, Select, Textarea, Checkbox, Toggle, Radio, Range, Rating, Filter, OTP,
File input, Fieldset, Label, Validator, Calendar). Everything sits in one EditForm with DataAnnotations: submit it
empty to see input-error and the validator hints. novalidate leaves the checking to Blazor. *@
<EditForm Model="model" OnValidSubmit="() => Submitted(true)" OnInvalidSubmit="() => Submitted(false)" novalidate>
<DataAnnotationsValidator />
<div class="grid gap-8 lg:grid-cols-2">
<section class="flex flex-col gap-3 lg:col-span-2">
<h3 class="text-lg font-bold">DInput</h3>
<div class="flex flex-wrap items-center gap-2">
@foreach (var color in Enum.GetValues<DColor>())
{
<DInput autocomplete="off" @bind-Value="text" Color="color" placeholder="@color.ToString()" class="w-36" />
}
</div>
<div class="flex flex-wrap items-center gap-2">
<DInput autocomplete="off" @bind-Value="text" Size="DSize.Xs" placeholder="Xs" class="w-28" />
<DInput autocomplete="off" @bind-Value="text" Size="DSize.Sm" placeholder="Sm" class="w-28" />
<DInput autocomplete="off" @bind-Value="text" placeholder="Md" class="w-28" />
<DInput autocomplete="off" @bind-Value="text" Size="DSize.Lg" placeholder="Lg" class="w-28" />
<DInput autocomplete="off" @bind-Value="text" Size="DSize.Xl" placeholder="Xl" class="w-28" />
</div>
<div class="flex flex-wrap items-center gap-2">
<DInput autocomplete="off" @bind-Value="text" Ghost="true" placeholder="Ghost" />
<DInput autocomplete="off" @bind-Value="text" disabled placeholder="Disabled" />
<DInput autocomplete="off" @bind-Value="search" Immediate="true" placeholder="Search">
<Prefix><i class="ion-md-search opacity-50"></i></Prefix>
<Suffix><kbd class="kbd kbd-sm">⌘</kbd><kbd class="kbd kbd-sm">K</kbd></Suffix>
</DInput>
<DInput autocomplete="off" @bind-Value="site" Color="DColor.Primary">
<Prefix><DLabel>https://</DLabel></Prefix>
<Suffix><DLabel>.com</DLabel></Suffix>
</DInput>
</div>
<p class="text-sm opacity-60">Immediate search: "@search", text: "@text"</p>
<div class="flex flex-col">
<DInput autocomplete="off" @bind-Value="model.Name" Validator="true" placeholder="Name (required)" class="w-full max-w-xs" />
<DValidatorHint>Name is required</DValidatorHint>
</div>
</section>
<section class="flex flex-col gap-3">
<h3 class="text-lg font-bold">DSelect</h3>
<div class="flex flex-wrap items-center gap-2">
@foreach (var color in Enum.GetValues<DColor>())
{
<DSelect @bind-Value="language" Color="color" class="w-36">
<option value="C#">C# (@color)</option>
<option value="F#">F#</option>
<option value="TypeScript">TypeScript</option>
</DSelect>
}
</div>
<div class="flex flex-wrap items-center gap-2">
<DSelect @bind-Value="language" Size="DSize.Xs" class="w-28"><option>C#</option><option>F#</option></DSelect>
<DSelect @bind-Value="language" Size="DSize.Sm" class="w-28"><option>C#</option><option>F#</option></DSelect>
<DSelect @bind-Value="language" class="w-28"><option>C#</option><option>F#</option></DSelect>
<DSelect @bind-Value="language" Size="DSize.Lg" class="w-28"><option>C#</option><option>F#</option></DSelect>
<DSelect @bind-Value="language" Size="DSize.Xl" class="w-28"><option>C#</option><option>F#</option></DSelect>
</div>
<div class="flex flex-wrap items-center gap-2">
<DSelect @bind-Value="language" Ghost="true" class="w-36"><option>C#</option><option>F#</option></DSelect>
<DSelect @bind-Value="language" disabled class="w-36"><option>C#</option><option>F#</option></DSelect>
<DSelect @bind-Value="language" class="w-48">
<Prefix><DLabel>Type</DLabel></Prefix>
<ChildContent><option>C#</option><option>F#</option></ChildContent>
</DSelect>
</div>
<div class="flex flex-col">
<DSelect @bind-Value="model.Language" Validator="true" class="w-full max-w-xs">
<option value="">Pick a language (required)</option>
<option>C#</option>
<option>F#</option>
</DSelect>
<DValidatorHint>Pick one</DValidatorHint>
</div>
</section>
<section class="flex flex-col gap-3">
<h3 class="text-lg font-bold">DTextarea</h3>
<div class="grid grid-cols-3 gap-2">
@foreach (var color in Enum.GetValues<DColor>())
{
<DTextarea autocomplete="off" @bind-Value="bio" Color="color" placeholder="@color.ToString()" class="min-h-0 h-12" />
}
</div>
<div class="flex flex-wrap items-start gap-2">
<DTextarea autocomplete="off" @bind-Value="bio" Size="DSize.Xs" placeholder="Xs" class="w-24" />
<DTextarea autocomplete="off" @bind-Value="bio" Size="DSize.Sm" placeholder="Sm" class="w-24" />
<DTextarea autocomplete="off" @bind-Value="bio" placeholder="Md" class="w-24" />
<DTextarea autocomplete="off" @bind-Value="bio" Size="DSize.Lg" placeholder="Lg" class="w-24" />
<DTextarea autocomplete="off" @bind-Value="bio" Size="DSize.Xl" placeholder="Xl" class="w-24" />
</div>
<div class="flex flex-wrap items-start gap-2">
<DTextarea autocomplete="off" @bind-Value="bio" Ghost="true" placeholder="Ghost" />
<DTextarea autocomplete="off" @bind-Value="bio" disabled placeholder="Disabled" />
</div>
<div class="flex flex-col">
<DTextarea autocomplete="off" @bind-Value="model.Bio" Immediate="true" Validator="true" placeholder="Bio, 10 to 140 characters" class="w-full" />
<DValidatorHint>Between 10 and 140 characters</DValidatorHint>
<span class="text-sm opacity-60">@(model.Bio?.Length ?? 0)/140</span>
</div>
</section>
<section class="flex flex-col gap-3">
<h3 class="text-lg font-bold">DCheckbox</h3>
<div class="flex flex-wrap items-center gap-2">
@foreach (var color in Enum.GetValues<DColor>())
{
<DCheckbox @bind-Value="check" Color="color" title="@color.ToString()" />
}
</div>
<div class="flex flex-wrap items-center gap-2">
<DCheckbox @bind-Value="check" Size="DSize.Xs" />
<DCheckbox @bind-Value="check" Size="DSize.Sm" />
<DCheckbox @bind-Value="check" />
<DCheckbox @bind-Value="check" Size="DSize.Lg" />
<DCheckbox @bind-Value="check" Size="DSize.Xl" />
<DCheckbox @bind-Value="check" disabled />
</div>
<DCheckbox @bind-Value="check" Color="DColor.Primary" LabelText="@($"Checked: {check}")" />
<DCheckbox @bind-Value="mixedCheck" @bind-Indeterminate="mixed" LabelText="@(mixed ? "Indeterminate (click to clear)" : $"Checked: {mixedCheck}")" />
<DCheckbox @bind-Value="model.Terms" LabelText="I accept the terms (must be checked)" />
<ValidationMessage For="() => model.Terms" class="text-error text-xs" />
</section>
<section class="flex flex-col gap-3">
<h3 class="text-lg font-bold">DToggle</h3>
<div class="flex flex-wrap items-center gap-2">
@foreach (var color in Enum.GetValues<DColor>())
{
<DToggle @bind-Value="toggle" Color="color" title="@color.ToString()" />
}
</div>
<div class="flex flex-wrap items-center gap-2">
<DToggle @bind-Value="toggle" Size="DSize.Xs" />
<DToggle @bind-Value="toggle" Size="DSize.Sm" />
<DToggle @bind-Value="toggle" />
<DToggle @bind-Value="toggle" Size="DSize.Lg" />
<DToggle @bind-Value="toggle" Size="DSize.Xl" />
<DToggle @bind-Value="toggle" disabled />
</div>
<div class="flex flex-wrap items-center gap-4">
<DToggle @bind-Value="toggle" Size="DSize.Lg">
<CheckedIcon><i class="ion-md-checkmark"></i></CheckedIcon>
<UncheckedIcon><i class="ion-md-close"></i></UncheckedIcon>
</DToggle>
<DToggle @bind-Value="toggle" Color="DColor.Success" Size="DSize.Lg">
<CheckedIcon><i class="ion-md-sunny"></i></CheckedIcon>
<UncheckedIcon><i class="ion-md-moon"></i></UncheckedIcon>
</DToggle>
<DToggle @bind-Value="toggle" Color="DColor.Accent" LabelText="With icons and a label">
<CheckedIcon><i class="ion-md-volume-high"></i></CheckedIcon>
<UncheckedIcon><i class="ion-md-volume-off"></i></UncheckedIcon>
</DToggle>
</div>
<DToggle @bind-Value="model.Notifications" Color="DColor.Primary" LabelText="Notifications" />
</section>
<section class="flex flex-col gap-3">
<h3 class="text-lg font-bold">DRadioGroup / DRadio</h3>
<DRadioGroup @bind-Value="radioColor" class="flex flex-wrap items-center gap-2" aria-label="Colors">
@foreach (var color in Enum.GetValues<DColor>())
{
<DRadio Value="color" Color="color" title="@color.ToString()" />
}
</DRadioGroup>
<p class="text-sm opacity-60">Picked: @radioColor</p>
<DRadioGroup @bind-Value="radioSize" class="flex flex-wrap items-center gap-2" aria-label="Sizes">
@foreach (var size in sizes)
{
<DRadio Value="size" Size="size" Color="DColor.Secondary" />
}
</DRadioGroup>
<DRadioGroup @bind-Value="disabledPick" class="flex flex-wrap items-center gap-2" aria-label="Disabled">
<DRadio Value="1" Disabled="true" LabelText="Disabled, checked" />
<DRadio Value="2" Disabled="true" LabelText="Disabled" />
</DRadioGroup>
<DRadioGroup @bind-Value="model.Plan" class="flex flex-col gap-1" aria-label="Plan">
@foreach (var plan in plans)
{
<DRadio Value="plan" LabelText="@plan" Color="DColor.Primary" />
}
</DRadioGroup>
<ValidationMessage For="() => model.Plan" class="text-error text-xs" />
</section>
<section class="flex flex-col gap-3">
<h3 class="text-lg font-bold">DRange</h3>
@foreach (var color in Enum.GetValues<DColor>())
{
<DRange @bind-Value="volume" Color="color" Immediate="true" title="@color.ToString()" />
}
<p class="text-sm opacity-60">Value: @volume</p>
<DRange @bind-Value="volume" Size="DSize.Xs" />
<DRange @bind-Value="volume" Size="DSize.Sm" />
<DRange @bind-Value="volume" Size="DSize.Lg" />
<DRange @bind-Value="volume" Size="DSize.Xl" />
<div class="w-full max-w-xs">
<DRange @bind-Value="steps" Step="25" Color="DColor.Accent" />
<div class="mt-2 flex justify-between px-2.5 text-xs"><span>|</span><span>|</span><span>|</span><span>|</span><span>|</span></div>
</div>
<div class="flex items-center gap-4">
<DRange @bind-Value="volume" Direction="DDirection.Vertical" Immediate="true" Color="DColor.Primary" />
<DRange @bind-Value="volume" disabled />
</div>
</section>
<section class="flex flex-col gap-3">
<h3 class="text-lg font-bold">DRating</h3>
<div class="flex flex-wrap items-center gap-4">
<DRating @bind-Value="model.Stars" />
<span class="text-sm opacity-60">@model.Stars stars (1 to 5)</span>
</div>
<ValidationMessage For="() => model.Stars" class="text-error text-xs" />
<DRating @bind-Value="stars" Mask="DRatingMask.Star2" ItemClass="bg-orange-400" />
<DRating @bind-Value="stars" Mask="DRatingMask.Heart" ItemClass="bg-red-400" UnitText="heart" class="gap-1" />
<div class="flex flex-wrap items-center gap-4">
<DRating @bind-Value="stars" Size="DSize.Xs" />
<DRating @bind-Value="stars" Size="DSize.Sm" />
<DRating @bind-Value="stars" />
<DRating @bind-Value="stars" Size="DSize.Lg" />
<DRating @bind-Value="stars" Size="DSize.Xl" />
</div>
<div class="flex flex-wrap items-center gap-4">
<DRating @bind-Value="halfStars" Half="true" Clearable="true" Mask="DRatingMask.Star2" Size="DSize.Lg" ItemClass="bg-green-500" />
<span class="text-sm opacity-60">@(halfStars / 2.0) stars (clearable)</span>
</div>
<div class="flex flex-wrap items-center gap-4">
<DRating @bind-Value="stars" Static="true" />
<DRating @bind-Value="stars" Disabled="true" Mask="DRatingMask.Diamond" Count="3" />
</div>
</section>
<section class="flex flex-col gap-3">
<h3 class="text-lg font-bold">DFilter</h3>
<DFilter @bind-Value="framework" Options="frameworks" />
<DFilter @bind-Value="filterColor" Options="@(Enum.GetValues<DColor>().Skip(1).Cast<DColor?>())" TextOf="c => c?.ToString()" Size="DSize.Sm" />
<p class="text-sm opacity-60">Picked: @(framework ?? "none"), @(filterColor?.ToString() ?? "none")</p>
</section>
<section class="flex flex-col gap-3">
<h3 class="text-lg font-bold">DOtp</h3>
<div class="flex flex-col">
<DOtp @bind-Value="model.Code" Immediate="true" />
<ValidationMessage For="() => model.Code" class="text-error text-xs" />
</div>
<DOtp @bind-Value="otp" Length="4" Joined="true" Color="DColor.Primary" />
<div class="flex flex-wrap items-center gap-2">
@foreach (var color in Enum.GetValues<DColor>().Skip(1))
{
<DOtp @bind-Value="otp" Length="4" Color="color" Size="DSize.Xs" title="@color.ToString()" />
}
</div>
<div class="flex flex-wrap items-end gap-2">
@foreach (var size in sizes)
{
<DOtp @bind-Value="otp" Length="4" Size="size" />
}
</div>
<DOtp @bind-Value="otp" Length="4" disabled />
</section>
<section class="flex flex-col gap-3">
<h3 class="text-lg font-bold">DFileInput</h3>
<div class="flex flex-col gap-2">
@foreach (var color in Enum.GetValues<DColor>())
{
<DFileInput Color="color" title="@color.ToString()" />
}
</div>
<DFileInput Size="DSize.Xs" />
<DFileInput Size="DSize.Sm" />
<DFileInput Size="DSize.Lg" />
<DFileInput Size="DSize.Xl" />
<DFileInput Ghost="true" />
<DFileInput disabled />
<DFileInput Multiple="true" Accept="image/*" Color="DColor.Primary" OnChange="OnFiles" />
<p class="text-sm opacity-60">Picked: @(files.Count == 0 ? "nothing" : string.Join(", ", files))</p>
</section>
<section class="flex flex-col gap-3">
<h3 class="text-lg font-bold">DFieldset / DLabel</h3>
<DFieldset class="bg-base-200 border-base-300 rounded-box w-xs border p-4">
<Legend>Page details</Legend>
<ChildContent>
<DLabel>Title</DLabel>
<DInput autocomplete="off" @bind-Value="text" placeholder="My awesome page" />
<DLabel>Slug</DLabel>
<DInput autocomplete="off" @bind-Value="site" placeholder="my-awesome-page" />
</ChildContent>
<Description>You can edit these later</Description>
</DFieldset>
<DFieldset disabled>
<Legend>Disabled fieldset</Legend>
<ChildContent><DInput autocomplete="off" @bind-Value="text" placeholder="Can't type here" /></ChildContent>
</DFieldset>
<DLabel Floating="true" Text="Your email">
<DInput autocomplete="off" @bind-Value="email" placeholder="mail@site.com" />
</DLabel>
<DLabel Floating="true" Text="Notes">
<DTextarea autocomplete="off" @bind-Value="bio" placeholder="Notes" />
</DLabel>
</section>
<section class="flex flex-col gap-3">
<h3 class="text-lg font-bold">DValidator / DValidatorHint</h3>
<p class="text-sm opacity-60">Native rules, checked by the browser once you've edited the field.</p>
<div class="flex flex-col">
<DInput autocomplete="off" @bind-Value="email" Type="email" Validator="true" required="true" placeholder="mail@site.com" />
<DValidatorHint>Enter a valid email address</DValidatorHint>
</div>
<div class="flex flex-col">
<DInput autocomplete="off" @bind-Value="username" Validator="true" required="true" pattern="[A-Za-z][A-Za-z0-9\-]*" minlength="3"
maxlength="30" placeholder="Username" title="Only letters, numbers or dash" />
<DValidatorHint Compact="true">Must be 3 to 30 letters, numbers or dashes (Compact: no space while valid)</DValidatorHint>
</div>
</section>
<section class="flex flex-col gap-3">
<h3 class="text-lg font-bold">DCalendar</h3>
<DCalendar @bind-Value="model.Date" Min="today.AddDays(-30)" Max="today.AddMonths(3)"
class="bg-base-100 border-base-300 rounded-box border shadow-lg" />
<p class="text-sm opacity-60">Picked: @(model.Date?.ToString("yyyy-MM-dd") ?? "nothing") (from 30 days ago to 3 months ahead)</p>
<DButton Size="DSize.Sm" OnClick="() => model.Date = today" class="self-start">Today</DButton>
</section>
</div>
<div class="mt-8 flex items-center gap-4">
<DButton Type="submit" Color="DColor.Primary">Validate the form</DButton>
<span class="text-sm opacity-60">@submitResult</span>
</div>
</EditForm>
@code {
bool mixedCheck;
bool mixed = true;
class DemoModel
{
[Required] public string Name { get; set; }
[Required] public string Language { get; set; }
[Required, StringLength(140, MinimumLength = 10)] public string Bio { get; set; }
[Range(typeof(bool), "true", "true", ErrorMessage = "Accept the terms")] public bool Terms { get; set; }
public bool Notifications { get; set; } = true;
[Required] public string Plan { get; set; }
[Range(1, 5, ErrorMessage = "Rate from 1 to 5 stars")] public int Stars { get; set; }
[Required, RegularExpression("[0-9]{6}", ErrorMessage = "Enter the 6 digits")] public string Code { get; set; }
public DateOnly? Date { get; set; }
}
readonly DemoModel model = new();
readonly DSize[] sizes = { DSize.Xs, DSize.Sm, DSize.Md, DSize.Lg, DSize.Xl };
readonly string[] plans = { "Free", "Pro", "Team" };
readonly string[] frameworks = { "Blazor", "Svelte", "Vue", "React" };
readonly DateOnly today = DateOnly.FromDateTime(DateTime.Today);
string submitResult;
int disabledPick = 1;
string text;
string search;
string site;
string email;
string username;
string language = "C#";
string bio;
bool check = true;
bool toggle = true;
DColor radioColor = DColor.Primary;
DSize radioSize = DSize.Md;
int volume = 40;
int steps = 25;
int stars = 3;
int halfStars = 7;
string framework;
DColor? filterColor;
string otp;
List<string> files = [];
void Submitted(bool valid) => submitResult = valid ? "Valid" : "Invalid: see the messages above";
void OnFiles(InputFileChangeEventArgs e) => files = e.GetMultipleFiles(e.FileCount).Select(f => f.Name).ToList();
}