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.31.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>

An app's own preference

data-app-attrs lists the app's own preferences, separated by spaces. For each name, boot.js writes the value stored under sedna.<name> to <html data-app-<name>> before first paint, so the app's stylesheet selects :root[data-app-mochi="off"] on a statically rendered error page as on any other. Nothing stored writes no attribute, and a name that is not a lowercase token is ignored.

Store the value with sednaUi.settings.save('mochi', 'off') or SaveSettingAsync, and settings.apply() keeps the attribute in step. An app that writes the key some other way sets the attribute itself.

<script src="_content/Sedna.UI/js/Sedna.UI.boot.js" data-app-attrs="mochi"></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 btn--solid" 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 a value

A value shown cut short — a checksum, an id, a token — copies whole from an icon button beside it, with the clipboard feedback in the button's own icons. Put the full value in data-copy and only the start in the cell; the column is .col-fit, so a narrow screen scrolls the table instead of breaking the value one character per line. The button needs an aria-label that names what it copies.

Package checksum 5d41402abc4b2a76…
FileSHA-256
orders-export.csv 9f86d081884c…
<div class="sedna-col sedna-gap-2" style="max-width:520px">
    <div class="kv">
        <span class="k">Package checksum</span>
        <span class="v sedna-row sedna-gap-1">
            <code id="sum-pkg">5d41402abc4b2a76…</code>
            <button class="btn btn--sm btn--ghost btn--icon" type="button" data-copy="5d41402abc4b2a76b9719d911017c592" aria-label="Copy the checksum">
                <i class="ri-file-copy-line" data-copied-hide></i>
                <i class="ri-check-line" data-copied-show="ok"></i>
                <i class="ri-error-warning-line" data-copied-show="failed"></i>
            </button>
        </span>
    </div>
    <div class="sedna-scroll-x">
        <table class="table">
            <thead><tr><th>File</th><th class="col-fit">SHA-256</th></tr></thead>
            <tbody>
                <tr>
                    <td>orders-export.csv</td>
                    <td class="col-fit">
                        <span class="sedna-row sedna-gap-1">
                            <code>9f86d081884c…</code>
                            <button class="btn btn--sm btn--ghost btn--icon" type="button" data-copy="9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" aria-label="Copy the SHA-256 of orders-export.csv">
                                <i class="ri-file-copy-line" data-copied-hide></i>
                                <i class="ri-check-line" data-copied-show="ok"></i>
                            </button>
                        </span>
                    </td>
                </tr>
            </tbody>
        </table>
    </div>
</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> <span>Actions</span>
        </button>
        <div class="menu menu--start" hidden>
            <button class="menu-item" type="button"><i class="ri-pencil-line"></i> <span>Rename</span></button>
            <button class="menu-item" type="button"><i class="ri-file-copy-line"></i> <span>Duplicate</span></button>
            <hr class="menu-sep" />
            <button class="menu-item menu-item--danger" type="button"><i class="ri-delete-bin-line"></i> <span>Delete</span></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