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

The script

tier 2 — classes

sednaUi holds generic UI behaviour only: theme settings, clipboard, toasts, the dialog presenter, and delegated wiring for menus and tabs. Every class on these pages applies with scripting blocked — the script adds behaviour, never appearance. The same surface typed for a component is From C#; hover hints have a page of their own.

Loading it

Three files, and the order matters. boot.js goes in <head> so the stored theme is applied before first paint; at the end of the body it would produce a dark flash on a light-theme app.

<head>
    <link rel="stylesheet" href="_content/Sedna.UI/lib/remixicon/remixicon.css" />
    <link rel="stylesheet" href="_content/Sedna.UI/css/Sedna.UI.css" />
    <script src="_content/Sedna.UI/js/Sedna.UI.boot.js"></script>
</head>
<body>
    <!-- … -->
    <script src="_content/Sedna.UI/js/Sedna.UI.js"></script>
</body>

The default theme

data-theme-default is the theme name stamped on <html data-theme> when nothing is stored, and it has to be the same name as SednaUiOptions.Default — the theme whose palette SednaBrandStyle writes at bare :root. Every other registered theme sits behind [data-theme="<name>"], so a first visit stamped with the wrong name gets the wrong palette, and an app that registers the built-in theme beside its own default hands every first visit Sedna's colours.

ISednaUi.ConfigureAsync() pushes the same value to the main script, so the two agree once the circuit is up. An app that leaves Default alone needs neither.

<!-- In <head>, before the stylesheets. The name is SednaUiOptions.Default — the theme
     whose palette is emitted at bare :root — so render it from the options rather than
     retyping it, and leave the attribute off entirely while the default is "sedna". -->
<script src="_content/Sedna.UI/js/Sedna.UI.boot.js" data-theme-default="northwind"></script>

The browser's time zone

data-tz-cookie names a cookie, and boot.js writes Intl.DateTimeFormat().resolvedOptions().timeZone to it before first paint, with path=/, SameSite=Lax and a year's max-age — only when the value differs from the cookie already there. No reload and no event.

A first request carries no cookie yet. Configure a fallback zone for that one render and let it stand — do not reload to get the cookie, because the next navigation already carries it.

<script src="_content/Sedna.UI/js/Sedna.UI.boot.js" data-tz-cookie="tz"></script>

Toasts

sednaUi.toast(message, opts) creates the stack on first use and returns a function that removes that toast early. timeout: 0 keeps it until dismissed, and the message is inserted as text, never as markup.

<div style="display:flex; flex-wrap:wrap; gap:8px">
    <button class="btn btn-go" type="button"
            onclick="sednaUi.toast('Dispatched ORD-4209', { kind: 'go', title: 'Sent to the warehouse' })">
        Success toast
    </button>
    <button class="btn btn-warn" type="button"
            onclick="sednaUi.toast('Automation lowered to level 1', { kind: 'warn' })">
        Warning toast
    </button>
    <button class="btn btn-danger" type="button"
            onclick="sednaUi.toast('Could not reach the warehouse. Nothing was written.', { kind: 'danger', timeout: 0 })">
        Failure, stays until dismissed
    </button>
</div>

Copy without a handler

data-copy="…" copies a literal; data-copy-target="#sel" copies that element's text; an empty data-copy-target inside a .code-block copies its <pre>. The click is delegated from document, so a button rendered by a later Blazor render works with nothing wired. The outcome is data-copied="ok" or "failed" on the button for 1.4 seconds, and the script never rewrites it: mark what shows at rest with data-copied-hide and what replaces it with data-copied-show="ok" or "failed", in your own words.

np_live_8f2c1d
<div style="display:flex; flex-wrap:wrap; gap:8px; align-items:center">
    <button class="btn" type="button" data-copy="dotnet add package Sedna.UI">
        <i class="ri-file-copy-line" data-copied-hide></i><span data-copied-hide>Copy the install command</span>
        <i class="ri-check-line" data-copied-show="ok"></i><span data-copied-show="ok">Copied</span>
        <i class="ri-error-warning-line" data-copied-show="failed"></i><span data-copied-show="failed">Copy failed</span>
    </button>
    <code id="the-token">np_live_8f2c1d</code>
    <button class="btn btn-sm" type="button" data-copy-target="#the-token">
        <i class="ri-file-copy-line" data-copied-hide></i><span data-copied-hide>Copy</span>
        <i class="ri-check-line" data-copied-show="ok"></i><span data-copied-show="ok">Copied</span>
    </button>
</div>

data-menu-toggle on the trigger inside a .menu-anchor handles opening, one panel at a time, the outside click, Escape, and returning focus to the trigger. The closed state is the hidden attribute, so a closed panel is out of the tab order; for a full-width anchor set align-self: stretch on it.

<div style="padding-bottom:170px">
    <div class="menu-anchor">
        <button class="btn" type="button" data-menu-toggle aria-expanded="false">
            <i class="ri-more-2-fill"></i> Actions
        </button>
        <div class="menu menu--start" hidden>
            <button class="menu-item" type="button"><i class="ri-pencil-line"></i> Rename</button>
            <button class="menu-item" type="button"><i class="ri-file-copy-line"></i> Duplicate</button>
            <hr class="menu-sep" />
            <button class="menu-item menu-item--danger" type="button"><i class="ri-delete-bin-line"></i> Delete</button>
        </div>
    </div>
</div>

Tabs, wired for you

data-tabs on a .tabs container is mostly there for the keyboard: arrow keys, Home and End move between tabs, only the selected tab is a tab stop, and the panel named by aria-controls is shown while the rest are hidden. Leave data-tabs off when the component drives aria-selected from C#, or two things set it.

Twelve waiting on a first response.
<div>
    <div class="tabs" role="tablist" data-tabs aria-label="Views">
        <button class="tab" role="tab" aria-selected="true" aria-controls="js-p1" type="button">Open</button>
        <button class="tab" role="tab" aria-selected="false" aria-controls="js-p2" type="button" tabindex="-1">Decided</button>
        <button class="tab" role="tab" aria-selected="false" aria-controls="js-p3" type="button" tabindex="-1">Everything</button>
    </div>
    <div class="tab-panel" role="tabpanel" id="js-p1">Twelve waiting on a first response.</div>
    <div class="tab-panel" role="tabpanel" id="js-p2" hidden>Decided in the last 24 hours.</div>
    <div class="tab-panel" role="tabpanel" id="js-p3" hidden>Everything, archived included.</div>
</div>

Repairing a customizable select

A .form-select that writes its own <button><selectedcontent> shows a browser-made copy of the chosen option in its closed box, and Blazor's render does not trigger that copy — so without this the box is blank, with nothing in the console. The script repairs it for you; sednaUi.select.refresh() is here for the case the observer cannot see.

// Nothing to call in the normal case: the repair runs on load and after every render
// that adds nodes. Call it for a select the observer cannot have seen change — one
// moved into place by something other than a DOM insertion.
sednaUi.select.refresh();              // the whole document, returns how many it fixed
sednaUi.select.refresh(panelElement);  // or just one subtree