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.
DR.Simple_UI
Showing main. This can be ahead of the version your app has installed — check what a class says it needs before copying it. Releases (opens in a new tab)

Markdown

tier 1 — the frame

Two independent pieces: .markdown-body styles rendered Markdown wherever it appears, and .md-editor is a toolbar + textarea + live preview driven by drSimpleUi.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, so a release note or a knowledge-base article looks like the rest of the app. Nested lists indent one step per level and keep their own markers.

The rendered container is the one place in an app where the HTML comes from somewhere else, so it must survive anything: a long <code> breaks inside a word rather than running past its column, 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-badgedr.paintA connection's state, as a dot and a word
.outputdr.paintLines of machine output, with follow-tail
.staledr.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>dr.paint</td><td>A connection's state, as a dot and a word</td></tr>
            <tr><td><code>.output</code></td><td>dr.paint</td><td>Lines of machine output, with follow-tail</td></tr>
            <tr><td><code>.stale</code></td><td>dr.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: the root carries data-md-view, the textarea data-md-input, the preview pane data-md-preview, and toolbar buttons data-md-cmd.

The markup alone does nothing. The toolbar, the preview and the Write/Preview switch are behaviour, so an editor renders perfectly and sits inert until something calls drSimpleUi.md.init() — from C#, IDrSimpleUi.InitMarkdownAsync() in OnAfterRenderAsync, which is what this page does. It takes no element and wires every editor in the document, and it is idempotent per editor, so calling it again after a re-render only picks up new ones.

The Write/Preview switch is the ordinary .segmented control, with data-md-tab on each radio — it is exactly what a segmented control is for, so the editor gets the radio group's keyboard behaviour and announcement instead of two buttons with a toggled class. init() assigns the shared name, because only it knows how many editors are on the page.

<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 /> Write</label>
            <label class="segmented-option">
                <input type="radio" data-md-tab="preview" /> Preview</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>

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.

A shorter toolbar is a choice, not a variant: leave out the data-md-cmd buttons the field does not need and the rest keeps working.

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 /> Write</label>
                <label class="segmented-option">
                    <input type="radio" data-md-tab="preview" /> Preview</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-primary" type="button">Save the reason</button>
    </div>
</div>
In Blazor, bind the textarea with @bind:event="oninput" and let it own the value. Toolbar edits mutate the textarea and dispatch a bubbling input event so the binding picks them up — the JS never calls back into .NET.
drSimpleUi.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. It is not a spec-complete parser. For untrusted input rendered to other users, sanitise server-side as well rather than relying on this alone.