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

223 lines
9.3 KiB
Plaintext

@* Styleguide: the Layout components (Divider, Drawer, Footer, Hero, Indicator, Join, Mask, Stack). *@
<div class="grid gap-8 lg:grid-cols-2">
<section class="flex flex-col gap-3">
<h3 class="text-lg font-bold">DDivider</h3>
<div class="flex flex-col">
<div class="rounded-box bg-base-200 p-3">Above</div>
<DDivider>OR</DDivider>
<div class="rounded-box bg-base-200 p-3">Below</div>
<DDivider />
@foreach (var color in Enum.GetValues<DColor>().Skip(1))
{
<DDivider Color="color">@color</DDivider>
}
<DDivider Placement="DDividerPlacement.Start">Start</DDivider>
<DDivider Placement="DDividerPlacement.End" Direction="DDividerDirection.Vertical">End (vertical)</DDivider>
</div>
<div class="flex h-24">
<div class="grid grow place-items-center rounded-box bg-base-200">Left</div>
<DDivider Direction="DDividerDirection.Horizontal" Color="DColor.Primary">OR</DDivider>
<div class="grid grow place-items-center rounded-box bg-base-200">Right</div>
</div>
</section>
<section class="flex flex-col gap-3">
<h3 class="text-lg font-bold">DDrawer</h3>
<div class="flex flex-wrap items-center gap-2">
<DButton OnClick="() => drawerOpen = true">Open (@(drawerOpen ? "open" : "closed"))</DButton>
<label class="flex items-center gap-2 text-sm"><input type="checkbox" class="toggle toggle-sm" @bind="drawerEnd" /> End</label>
</div>
<p class="text-sm opacity-60">The box contains the fixed side (transform-gpu); the overlay, Escape and the side's buttons close it.</p>
<div class="h-72 transform-gpu overflow-hidden rounded-box bg-base-200">
<DDrawer @bind-IsOpen="drawerOpen" End="drawerEnd" ToggleId="styleguide-drawer" class="h-full"
ContentClass="flex flex-col items-center justify-center gap-2">
<ChildContent>
<p>Page content</p>
<label for="styleguide-drawer" class="btn btn-primary drawer-button">Open with a label</label>
</ChildContent>
<Side>
<ul class="menu min-h-full w-56 bg-base-100 p-4">
<li><button type="button" @onclick="() => drawerOpen = false"><i class="ion-md-home"></i> Home</button></li>
<li><button type="button" @onclick="() => drawerOpen = false"><i class="ion-md-settings"></i> Settings</button></li>
</ul>
</Side>
</DDrawer>
</div>
<p class="text-sm opacity-60">OpenFrom="Lg": always open beside the content from lg up.</p>
<div class="h-48 transform-gpu overflow-hidden rounded-box bg-base-200">
<DDrawer @bind-IsOpen="drawerLgOpen" OpenFrom="DDrawerOpenFrom.Lg" class="h-full" ContentClass="p-4">
<ChildContent>
<DButton Size="DSize.Sm" class="lg:hidden" OnClick="() => drawerLgOpen = true">Open</DButton>
<p>Content</p>
</ChildContent>
<Side>
<ul class="menu min-h-full w-40 bg-base-100 p-2">
<li><a>Item 1</a></li>
<li><a>Item 2</a></li>
</ul>
</Side>
</DDrawer>
</div>
</section>
<section class="flex flex-col gap-3 lg:col-span-2">
<h3 class="text-lg font-bold">DFooter</h3>
<DFooter Direction="DFooterDirection.HorizontalFromSm" class="rounded-box bg-base-200 p-10">
<aside>
<i class="ion-md-code-working text-4xl"></i>
<p>decePub<br />Responsive: a row from sm up</p>
</aside>
<DFooterNav>
<Heading>Services</Heading>
<ChildContent>
<a class="link link-hover">Branding</a>
<a class="link link-hover">Design</a>
</ChildContent>
</DFooterNav>
<DFooterNav>
<Heading>Company</Heading>
<ChildContent>
<a class="link link-hover">About us</a>
<a class="link link-hover">Contact</a>
</ChildContent>
</DFooterNav>
</DFooter>
<DFooter Center="true" Direction="DFooterDirection.Horizontal" class="rounded-box bg-base-200 p-6">
<DFooterNav class="grid-flow-col gap-4">
<a class="link link-hover">About</a>
<a class="link link-hover">Contact</a>
<a class="link link-hover">Jobs</a>
</DFooterNav>
<aside><p>Centered, horizontal</p></aside>
</DFooter>
</section>
<section class="flex flex-col gap-3">
<h3 class="text-lg font-bold">DHero</h3>
<DHero class="rounded-box bg-base-200" ContentClass="text-center">
<div class="max-w-md">
<h1 class="text-3xl font-bold">Hello there</h1>
<p class="py-4">A centered hero.</p>
<DButton Color="DColor.Primary">Get started</DButton>
</div>
</DHero>
<DHero BackgroundImage="imgs/styleguide/scene-5.svg" Overlay="true" class="min-h-64 rounded-box overflow-hidden"
ContentClass="text-neutral-content text-center">
<div class="max-w-md">
<h1 class="text-3xl font-bold">With an overlay</h1>
<p class="py-4">Background image and hero-overlay.</p>
</div>
</DHero>
<DHero class="rounded-box bg-base-200" ContentClass="flex-col lg:flex-row">
<img src="imgs/styleguide/scene-1.svg" class="max-w-32 rounded-lg shadow-2xl" alt="" />
<div>
<h1 class="text-2xl font-bold">With a figure</h1>
<p class="py-2">Column on small screens, row from lg up.</p>
</div>
</DHero>
</section>
<section class="flex flex-col gap-3">
<h3 class="text-lg font-bold">DIndicator</h3>
<div class="flex flex-wrap items-center gap-8 p-4">
<DIndicator>
<DIndicatorItem class="badge badge-secondary">new</DIndicatorItem>
<div class="grid h-24 w-24 place-items-center rounded-box bg-base-300">content</div>
</DIndicator>
<DIndicator>
<DIndicatorItem class="status status-success"></DIndicatorItem>
<DButton>Inbox</DButton>
</DIndicator>
<DIndicator>
<DIndicatorItem Vertical="DIndicatorVertical.Bottom" class="badge badge-primary">99+</DIndicatorItem>
<DButton Shape="DButtonShape.Circle"><i class="ion-md-notifications"></i></DButton>
</DIndicator>
</div>
<DIndicator class="m-4">
<DIndicatorItem Horizontal="DIndicatorHorizontal.Start" class="badge">↖︎</DIndicatorItem>
<DIndicatorItem Horizontal="DIndicatorHorizontal.Center" class="badge">↑</DIndicatorItem>
<DIndicatorItem class="badge">↗︎</DIndicatorItem>
<DIndicatorItem Horizontal="DIndicatorHorizontal.Start" Vertical="DIndicatorVertical.Middle" class="badge">←</DIndicatorItem>
<DIndicatorItem Horizontal="DIndicatorHorizontal.Center" Vertical="DIndicatorVertical.Middle" class="badge">●</DIndicatorItem>
<DIndicatorItem Vertical="DIndicatorVertical.Middle" class="badge">→</DIndicatorItem>
<DIndicatorItem Horizontal="DIndicatorHorizontal.Start" Vertical="DIndicatorVertical.Bottom" class="badge">↙︎</DIndicatorItem>
<DIndicatorItem Horizontal="DIndicatorHorizontal.Center" Vertical="DIndicatorVertical.Bottom" class="badge">↓</DIndicatorItem>
<DIndicatorItem Vertical="DIndicatorVertical.Bottom" class="badge">↘︎</DIndicatorItem>
<div class="grid h-32 w-60 place-items-center rounded-box bg-base-300">all placements</div>
</DIndicator>
</section>
<section class="flex flex-col gap-3">
<h3 class="text-lg font-bold">DJoin</h3>
<DJoin>
<DButton class="join-item">One</DButton>
<DButton class="join-item" Active="true">Two</DButton>
<DButton class="join-item">Three</DButton>
</DJoin>
<DJoin Direction="DDirection.Vertical" class="w-fit">
<DButton class="join-item">Top</DButton>
<DButton class="join-item">Middle</DButton>
<DButton class="join-item">Bottom</DButton>
</DJoin>
<DJoin Direction="DDirection.Vertical" class="lg:join-horizontal w-fit">
<DButton class="join-item">Vertical</DButton>
<DButton class="join-item">then horizontal</DButton>
<DButton class="join-item">from lg</DButton>
</DJoin>
<DJoin>
<input autocomplete="off" class="input join-item" placeholder="Email" />
<select class="select join-item w-28">
<option>Filter</option>
<option>Sci-fi</option>
</select>
<DButton class="join-item" Color="DColor.Primary"><i class="ion-md-search"></i></DButton>
</DJoin>
</section>
<section class="flex flex-col gap-3">
<h3 class="text-lg font-bold">DMask</h3>
<div class="flex flex-wrap gap-3">
@foreach (var shape in Enum.GetValues<DMaskShape>().Skip(1))
{
<div class="flex flex-col items-center gap-1 text-xs">
<DMask Shape="shape" Src="imgs/styleguide/scene-3.svg" alt="@shape" class="h-16 w-16" />
@shape
</div>
}
</div>
<div class="flex items-center gap-1">
<DMask Shape="DMaskShape.Star2" Half="DMaskHalf.First" class="h-10 w-5 bg-warning" />
<DMask Shape="DMaskShape.Star2" Half="DMaskHalf.Second" class="h-10 w-5 bg-warning" />
<DMask Shape="DMaskShape.Heart" class="ml-4 grid h-16 w-16 place-items-center bg-error text-error-content">div</DMask>
</div>
</section>
<section class="flex flex-col gap-3">
<h3 class="text-lg font-bold">DStack</h3>
<div class="flex flex-wrap gap-10 p-4">
@foreach (var placement in Enum.GetValues<DStackPlacement>())
{
<div class="flex flex-col items-center gap-3 text-sm">
<DStack Placement="placement" class="h-20 w-28">
<div class="grid place-content-center rounded-box bg-primary text-primary-content">1</div>
<div class="grid place-content-center rounded-box bg-accent text-accent-content">2</div>
<div class="grid place-content-center rounded-box bg-secondary text-secondary-content">3</div>
</DStack>
@placement
</div>
}
<DStack class="w-32">
<img src="imgs/styleguide/scene-2.svg" class="rounded-box" alt="" />
<img src="imgs/styleguide/scene-3.svg" class="rounded-box" alt="" />
<img src="imgs/styleguide/scene-4.svg" class="rounded-box" alt="" />
</DStack>
</div>
</section>
</div>
@code {
bool drawerOpen;
bool drawerEnd;
bool drawerLgOpen;
}