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

Markdown

tier 3 — a surface

Two independent pieces: .markdown-body styles rendered Markdown wherever it appears, and .md-editor is a toolbar + textarea + live preview driven by sednaUi.md.

.markdown-body

Put it on the container holding rendered HTML. It normalises headings, lists, code, quotes and links onto the tokens, and strips the first and last margin so it sits flush in a card body.

Escalation policy

Raise to P2 when either condition holds:

  • More than 20 users are affected
  • A VIP flag is set on the caller
Never raise priority and reroute in the same decision — the receiving group loses the audit trail.
if (affected > 20 || caller.IsVip)
priority = Priority.P2;

See the routing matrix for target groups.

<div class="card">
    <div class="card-body">
        <div class="markdown-body">
            <h2>Escalation policy</h2>
            <p>Raise to <strong>P2</strong> when <em>either</em> condition holds:</p>
            <ul>
                <li>More than 20 users are affected</li>
                <li>A <code>VIP</code> flag is set on the caller</li>
            </ul>
            <blockquote>Never raise priority and reroute in the same decision — the receiving group loses the audit trail.</blockquote>
            <pre><code>if (affected &gt; 20 || caller.IsVip)
priority = Priority.P2;</code></pre>
            <p>See the <a href="#">routing matrix</a> for target groups.</p>
        </div>
    </div>
</div>

Tables and nested lists

A table in rendered Markdown gets .table's own borders and header treatment without the class, and nested lists indent one step per level. The HTML here comes from somewhere else, so a long <code> breaks inside a word and a wide table scrolls rather than widening the page.

Release 0.5.0

Adds the live-state family. Nothing was renamed, so an upgrade needs no app changes.

ClassLayerWhat it is for
.health-badgesedna.paintA connection's state, as a dot and a word
.outputsedna.paintLines of machine output, with follow-tail
.stalesedna.paintHow old the figure beside it is

Before bumping:

  1. Grep your own stylesheets for the names above.
  2. Check each one you already style — both rule sets will apply.
    • Yours wins, because your stylesheet is unlayered.
    • The library's declarations you have not set still land.
  3. Rename yours, or delete it if the library's does the job.
<div class="markdown-body">
    <h2>Release 0.5.0</h2>
    <p>Adds the live-state family. Nothing was renamed, so an upgrade needs no app changes.</p>
    <table>
        <thead>
            <tr><th>Class</th><th>Layer</th><th>What it is for</th></tr>
        </thead>
        <tbody>
            <tr><td><code>.health-badge</code></td><td>sedna.paint</td><td>A connection's state, as a dot and a word</td></tr>
            <tr><td><code>.output</code></td><td>sedna.paint</td><td>Lines of machine output, with follow-tail</td></tr>
            <tr><td><code>.stale</code></td><td>sedna.paint</td><td>How old the figure beside it is</td></tr>
        </tbody>
    </table>
    <p>Before bumping:</p>
    <ol>
        <li>Grep your own stylesheets for the names above.</li>
        <li>Check each one you already style &mdash; both rule sets will apply.
            <ul>
                <li>Yours wins, because your stylesheet is unlayered.</li>
                <li>The library's declarations you have not set still land.</li>
            </ul>
        </li>
        <li>Rename yours, or delete it if the library's does the job.</li>
    </ol>
</div>

.md-editor

Live below — type in it, use the toolbar, then switch to Preview. The wiring is data-md-view on the root, data-md-input on the textarea, data-md-preview on the preview pane, data-md-cmd on toolbar buttons and data-md-tab on each radio of the .segmented Write/Preview switch; the markup sits inert until sednaUi.md.init() runs — from C#, ISednaUi.InitMarkdownAsync() in OnAfterRenderAsync, which wires every editor in the document and is idempotent per editor. In Blazor, bind the textarea with @bind:event="oninput" and let it own the value; toolbar edits dispatch a bubbling input event so the binding picks them up.

<div class="md-editor" data-md-view="write" id="demo-md">
    <div class="md-toolbar">
        <button class="md-tool" type="button" data-md-cmd="bold" aria-label="Bold" data-tip="Wrap the selection in ** **."><i class="ri-bold"></i></button>
        <button class="md-tool" type="button" data-md-cmd="italic" aria-label="Italic"><i class="ri-italic"></i></button>
        <button class="md-tool" type="button" data-md-cmd="code" aria-label="Code"><i class="ri-code-line"></i></button>
        <button class="md-tool" type="button" data-md-cmd="h2" aria-label="Heading"><i class="ri-h-2"></i></button>
        <button class="md-tool" type="button" data-md-cmd="ul" aria-label="Bullet list"><i class="ri-list-unordered"></i></button>
        <button class="md-tool" type="button" data-md-cmd="ol" aria-label="Numbered list"><i class="ri-list-ordered"></i></button>
        <button class="md-tool" type="button" data-md-cmd="quote" aria-label="Quote"><i class="ri-quote-text"></i></button>
        <button class="md-tool" type="button" data-md-cmd="link" aria-label="Link"><i class="ri-link"></i></button>
        <div class="segmented" role="group" aria-label="Editor view">
            <label class="segmented-option">
                <input type="radio" data-md-tab="write" checked /> <span>Write</span></label>
            <label class="segmented-option">
                <input type="radio" data-md-tab="preview" /> <span>Preview</span></label>
        </div>
    </div>
    <textarea class="form-input md-input" data-md-input rows="8" aria-label="Markdown source">## Escalation policy

Raise to **P2** when _either_ condition holds:

- More than 20 users are affected
- A `VIP` flag is set on the caller

> Never raise priority and reroute in the same decision.
</textarea>
    <div class="markdown-body md-preview" data-md-preview></div>
</div>

Images and videos

Live below — switch to Preview. ![description](address) is an image, and the same with the address of a video file — .mp4, .m4v, .mov, .webm or .ogv — is a video with controls that never plays by itself. data-md-cmd="image" and "video" wrap the selection as the description. Only http, https and relative addresses are kept; any other is written as its description. An app that uploads a picture inserts the text with the address it uploaded to, through ISednaUi.InsertMarkdownAsync(textarea, text) — the way Inserting at the caret below inserts a link.

<div class="md-editor" data-md-view="write">
    <div class="md-toolbar">
        <button class="md-tool" type="button" data-md-cmd="bold" aria-label="Bold"><i class="ri-bold"></i></button>
        <button class="md-tool" type="button" data-md-cmd="ol" aria-label="Numbered list"><i class="ri-list-ordered"></i></button>
        <button class="md-tool" type="button" data-md-cmd="link" aria-label="Link"><i class="ri-link"></i></button>
        <button class="md-tool" type="button" data-md-cmd="image" aria-label="Image" data-tip="Wrap the selection in ![ ](https://)."><i class="ri-image-line"></i></button>
        <button class="md-tool" type="button" data-md-cmd="video" aria-label="Video" data-tip="The same, for the address of a video file."><i class="ri-video-line"></i></button>
        <div class="segmented" role="group" aria-label="Editor view">
            <label class="segmented-option">
                <input type="radio" data-md-tab="write" checked /> <span>Write</span></label>
            <label class="segmented-option">
                <input type="radio" data-md-tab="preview" /> <span>Preview</span></label>
        </div>
    </div>
    <textarea class="form-input md-input" data-md-input rows="10" aria-label="Manual page">## Receiving a pallet

Check the racking label before the pallet leaves the dock:

![The racking in bay 4](https://images.unsplash.com/photo-1587293852726-70cdb56c2866?w=960&amp;h=540&amp;fit=crop&amp;q=70&amp;auto=format)

_Bay 4 after the racking was moved. Photo by CHUTTERSNAP on Unsplash._

1. Scan the pallet.
2. Put it in the bay the scanner names.
</textarea>
    <div class="markdown-body md-preview" data-md-preview></div>
</div>

As a form field

Live below. The editor in place: a .form-label pointing at the textarea's own id, a .form-hint reached by aria-describedby, and the .form-actions row. Label the textarea, not the .md-editor wrapper — the textarea is the control — and leave out the data-md-cmd buttons the field does not need.

Kept on the order and shown in its history. Markdown is rendered.

<div class="form-field" style="max-width:560px">
    <label class="form-label" for="md-reason">Reason for the decision</label>
    <div class="md-editor" data-md-view="write" id="reason-md">
        <div class="md-toolbar">
            <button class="md-tool" type="button" data-md-cmd="bold" aria-label="Bold"><i class="ri-bold"></i></button>
            <button class="md-tool" type="button" data-md-cmd="code" aria-label="Code"><i class="ri-code-line"></i></button>
            <button class="md-tool" type="button" data-md-cmd="ul" aria-label="Bullet list"><i class="ri-list-unordered"></i></button>
            <button class="md-tool" type="button" data-md-cmd="link" aria-label="Link"><i class="ri-link"></i></button>
            <div class="segmented" role="group" aria-label="Editor view">
                <label class="segmented-option">
                    <input type="radio" data-md-tab="write" checked /> <span>Write</span></label>
                <label class="segmented-option">
                    <input type="radio" data-md-tab="preview" /> <span>Preview</span></label>
            </div>
        </div>
        <textarea class="form-input md-input" id="md-reason" data-md-input rows="5"
                  aria-describedby="md-reason-hint">Dispatching the three reserved lines now.

- Lines 4 and 5 are out of stock until Monday
- The customer accepted a split delivery
</textarea>
        <div class="markdown-body md-preview" data-md-preview></div>
    </div>
    <p class="form-hint" id="md-reason-hint">Kept on the order and shown in its history. Markdown is rendered.</p>
    <div class="form-actions">
        <button class="btn" type="button">Discard</button>
        <button class="btn btn--brand btn--solid" type="button">Save the reason</button>
    </div>
</div>

Inserting at the caret

Live below — put the caret somewhere in the note, then link an order. The picker is the app's own .modal, awaited with ShowModalAsync, and ISednaUi.InsertMarkdownAsync(textarea, text) puts what it returns where the caret was: the selection is replaced, the caret lands after the text, focus returns to the textarea, and the bound value follows through the same input event a toolbar command fires. Assigning the bound field instead would put the text at the end. A textarea keeps its selection while a dialog has focus, so nothing has to be remembered across the picker. From script, it is sednaUi.md.insert(textarea, text).

The bound value
Picked up from the morning shift. Still open: 
@inject ISednaUi Ui

<div class="sedna-stack">
    <div class="md-editor" data-md-view="write">
        <div class="md-toolbar">
            <button class="md-tool" type="button" data-md-cmd="bold" aria-label="Bold"><i class="ri-bold"></i></button>
            <button class="md-tool" type="button" data-md-cmd="italic" aria-label="Italic"><i class="ri-italic"></i></button>
            @* Not a data-md-cmd: the editor leaves this one to the app. *@
            <button class="md-tool" type="button" aria-label="Link to an order" data-tip="Link to an order"
                    @onclick="PickOrder"><i class="ri-links-line"></i></button>
            <div class="segmented" role="group" aria-label="Editor view">
                <label class="segmented-option">
                    <input type="radio" data-md-tab="write" checked /> <span>Write</span></label>
                <label class="segmented-option">
                    <input type="radio" data-md-tab="preview" /> <span>Preview</span></label>
            </div>
        </div>
        <textarea class="form-input md-input" data-md-input rows="5" aria-label="Handover note"
                  @ref="_note" @bind="_text" @bind:event="oninput"></textarea>
        <div class="markdown-body md-preview" data-md-preview></div>
    </div>

    <div class="code-block">
        <div class="code-block-head"><span>The bound value</span></div>
        <pre><code>@_text</code></pre>
    </div>
</div>

@* The picker is the app's own modal. Each row is a dialog-method submitter, so the
   order it names becomes the dialog's returnValue. *@
<dialog id="pick-order" class="modal modal--sm" aria-labelledby="pick-order-title">
    <form method="dialog">
        <div class="modal-header">
            <h3 id="pick-order-title">Link to an order</h3>
        </div>
        <div class="modal-body">
            <ul class="list">
                @foreach (var order in _orders)
                {
                    <li>
                        <button class="list-row" value="@order.Id">
                            <span class="list-main">
                                <span class="list-title">@order.Title</span>
                                <span class="list-sub">@order.Id</span>
                            </span>
                        </button>
                    </li>
                }
            </ul>
        </div>
        <div class="modal-footer">
            <button class="btn" value="">Cancel</button>
        </div>
    </form>
</dialog>

@code {
    private readonly (string Id, string Title)[] _orders =
    [
        ("ORD-4204", "Replacement headset for the front desk"),
        ("ORD-4209", "Five monitors for the second floor"),
        ("ORD-4215", "Spare toner, returned damaged"),
    ];

    private ElementReference _note;
    private string _text = "Picked up from the morning shift. Still open: ";

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender) await Ui.InitMarkdownAsync();
    }

    // The textarea keeps its caret while the dialog has focus, so the link lands
    // where the reader left off — not at the end, which is all a binding can do.
    private async Task PickOrder()
    {
        var id = await Ui.ShowModalAsync("pick-order");
        var order = _orders.FirstOrDefault(o => o.Id == id);
        if (order.Id is null) return;

        await Ui.InsertMarkdownAsync(_note, $"[{order.Id}](https://example.com/orders/{order.Id})");
    }
}
Sanitise server-side as well sednaUi.md.render() escapes all HTML first and only re-introduces a fixed set of constructs, with link hrefs restricted to http:, https:, mailto: and root-relative paths, and image and video sources to http:, https: and relative ones. It is not a spec-complete parser: for untrusted input rendered to other users, sanitise server-side as well.