Connection lost. Reconnecting… attempt 1 of 8
Paused. Your work is held on the server.
Could not reconnect.
This session has expired on the server.
An unhandled error has occurred.
Sedna.UI
v0.17.0 · main

From C#

this site, not the package

ISednaUi is the typed way into the script from a component: every member is one JavaScript call, and none of them adds behaviour the script does not already have. This page is the members that are easiest to get wrong; the rest are on the page of the thing they drive.

Register it once, and never call it while prerendering builder.Services.AddSednaUi(), then @inject ISednaUi Ui. Every member is a JavaScript call, so none of them can run during prerendering — call them from an event handler, or from OnAfterRenderAsync(firstRender: true). The wrappers do not swallow that exception.

Something to say before the circuit is up

A toast decided in OnInitializedAsync — a record that failed to load — cannot be shown there: the page is still prerendering. Keep the outcome as state, and say it once the first interactive render has happened. That is the whole recipe; a flag on ISednaUi would only move the same if somewhere harder to see.

@inject ISednaUi Ui
@inject IOrderService Orders

@code {
    private Order? _order;
    private string? _failure;     // decided while prerendering, said once interactive

    protected override async Task OnInitializedAsync()
    {
        try { _order = await Orders.GetAsync(Id); }
        catch (OrderNotFoundException) { _failure = $"Order {Id} no longer exists."; }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        // The first render with a circuit behind it. Interop works from here, and it
        // runs once, so the toast is shown once.
        if (!firstRender || _failure is null) return;
        await Ui.ToastAsync(_failure, ToastKind.Danger, timeoutMs: 0);
    }
}

Toasts

ToastAsync takes the message and a ToastKind, whose members are the CSS modifier suffixes, so ToastKind.Go and .toast-go cannot drift apart. A toast is one of the two pieces of UI the library draws for you, because a line of text has nothing to author. It returns a handle for dismissing or replacing it.

@inject ISednaUi Ui

<div style="display:flex; flex-wrap:wrap; gap:8px">
    <button class="btn btn-go" type="button" @onclick="Approved">Success toast</button>
    <button class="btn btn-warn" type="button" @onclick="Lowered">Warning toast</button>
    <button class="btn btn-danger" type="button" @onclick="Failed">Failure, stays until dismissed</button>
</div>

@code {
    private Task Approved() => Ui.ToastAsync(
        "Dispatched ORD-4209", ToastKind.Go, title: "Sent to the warehouse");

    private Task Lowered() => Ui.ToastAsync("Automation lowered to level 1", ToastKind.Warn);

    // timeout 0 stays until dismissed — right for a failure the reader has to act on.
    private Task Failed() => Ui.ToastAsync(
        "Could not reach the warehouse. Nothing was written.", ToastKind.Danger, timeoutMs: 0);
}

Appearance settings

ISednaSettings is the applied settings as state, with a Changed event — where ISednaUi.LoadSettingsAsync only answers what they are now, which goes stale when a reader on “System” changes their OS between light and dark. Call StartAsync from OnAfterRenderAsync(firstRender), and check IsLive before drawing a toggle, or someone who chose light sees “dark” selected for one frame.

Variantdark
Themesedna
CompactFalse
Read from the browserFalse
@inject ISednaSettings Settings
@implements IDisposable

<div class="sedna-col sedna-gap-2">
    <div class="segmented">
        <label class="segmented-option">
            <input type="radio" name="ex-variant" checked="@(Settings.Current.Variant == "dark")"
                   @onchange="@(() => Settings.SetVariantAsync("dark"))" />
            <span><i class="ri-moon-line"></i> Dark</span>
        </label>
        <label class="segmented-option">
            <input type="radio" name="ex-variant" checked="@(Settings.Current.Variant == "light")"
                   @onchange="@(() => Settings.SetVariantAsync("light"))" />
            <span><i class="ri-sun-line"></i> Light</span>
        </label>
        <label class="segmented-option">
            <input type="radio" name="ex-variant" checked="@(Settings.Current.Variant == "system")"
                   @onchange="@(() => Settings.SetVariantAsync("system"))" />
            <span><i class="ri-computer-line"></i> System</span>
        </label>
    </div>

    <div class="card">
        <div class="card-body">
            <div class="kv"><span class="k">Variant</span><span class="v">@Settings.Current.Variant</span></div>
            <div class="kv"><span class="k">Theme</span><span class="v">@Settings.Current.Theme</span></div>
            <div class="kv"><span class="k">Compact</span><span class="v">@Settings.Current.Compact</span></div>
            <div class="kv"><span class="k">Read from the browser</span><span class="v">@Settings.IsLive</span></div>
        </div>
    </div>
</div>

@code {
    // Subscribe before StartAsync, so the initial value arrives as a change like any other and
    // this component has one code path instead of two.
    protected override void OnInitialized() => Settings.Changed += Refresh;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender) await Settings.StartAsync();
    }

    // Raised from a JavaScript callback, so the render has to be marshalled onto the circuit's
    // thread. Calling StateHasChanged directly here throws.
    private void Refresh(SednaUiSettings settings) => InvokeAsync(StateHasChanged);

    public void Dispose() => Settings.Changed -= Refresh;
}

The browser's time zone

Live — this is your own zone. ISednaUi.GetTimeZoneAsync() reads Intl through the circuit and returns the IANA id, so a Blazor Server app gets the zone on the first session in a new browser — where the cookie boot.js writes is still absent and a navigation is not an HTTP request. The two are the same answer at two moments: the cookie serves the first server render, this serves the circuit. It is interop, so call it from OnAfterRenderAsync(firstRender: true), and keep the configured fallback for the render before it answers.

Browser time zonenot read yet
Stored instant2027-03-28 00:30 UTC
On this reader's clock2027-03-28 00:30 (UTC)
@inject ISednaUi Ui

<div class="card">
    <div class="card-body">
        <div class="kv"><span class="k">Browser time zone</span><span class="v">@(_zone ?? "not read yet")</span></div>
        <div class="kv"><span class="k">Stored instant</span><span class="v">@Stored</span></div>
        <div class="kv"><span class="k">On this reader's clock</span><span class="v">@Local</span></div>
    </div>
</div>

@code {
    // What an app stores: an instant in UTC. The zone is the one part of rendering it
    // that only the browser knows. This one is half an hour before Europe's spring
    // transition, which is why an offset would not do — it is true until it is not.
    private static readonly DateTime Instant = new(2027, 3, 28, 0, 30, 0, DateTimeKind.Utc);

    // The configured fallback: what the first server render has to use, because it has
    // no browser to ask and no cookie on a first visit.
    private TimeZoneInfo _zoneInfo = TimeZoneInfo.Utc;
    private string? _zone;

    private string Stored => Instant.ToString("yyyy-MM-dd HH:mm") + " UTC";

    private string Local =>
        TimeZoneInfo.ConvertTimeFromUtc(Instant, _zoneInfo).ToString("yyyy-MM-dd HH:mm")
        + " (" + _zoneInfo.Id + ")";

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (!firstRender) return;

        // Interop, so never during prerendering. An IANA id — TryFindSystemTimeZoneById
        // takes those on every platform, and a browser that reports nothing leaves the
        // fallback standing rather than guessing.
        _zone = await Ui.GetTimeZoneAsync();
        if (_zone is not null && TimeZoneInfo.TryFindSystemTimeZoneById(_zone, out var found))
        {
            _zoneInfo = found;
        }

        StateHasChanged();
    }
}

The rest of the surface

MemberShown on
Nav.CssClass, Nav.AriaCurrent, ActiveLink.IsActiveSidebar and nav — which link is the current page, and which group is open
ShowModalAsync, CloseModalAsyncModal — awaiting a dialog you wrote, a confirmation included
ISednaOverlays, SednaOverlayHostDrawers and sheets — a modal, drawer or sheet that is its own component, with a typed result
SetTipsEnabledAsyncHover hints — switching every hint off from a setting
FollowSpotlightAsyncSpotlight — a tour step that stays attached to its target
InitMarkdownAsyncMarkdown — wiring the editor after first render
RegisterSearchAsync, RegisterCommandsAsyncTopbar and the command palette — the index and the commands
CopyTextAsyncThe script — where data-copy needs no C# at all
ConfigureAsyncThe script — only for an app whose default theme is not the built-in one