The script
tier 2 — classessednaUi 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>
Menus, wired for you
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.
<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