return function () {
    $self = url('/@object/@action/' . urlencode('html/bs5_components'));
    $htmx = _class('htmx');
    // the htmx button asks for a fragment; the toast travels in the HX-Trigger header (text only)
    if ($htmx->_wants_fragment() && ($_GET['hx'] ?? '') === 'toast') {
        $htmx->_toast('Saved at ' . date('H:i:s') . ' (an HX-Trigger toast).', ['title' => 'htmx', 'type' => 'success']);
        return $htmx->_result('<span id="demo-hx-result">Last htmx answer: ' . date('H:i:s') . '</span>');
    }
    // the messages of this request go out as toasts (bs5); Bootstrap 3 keeps the alerts
    if (isset($_GET['messages'])) {
        common()->MESSAGES_AS_TOASTS = true;
        common()->message_success('Saved: a success message.');
        common()->message_info('An info message.');
        common()->message_error('An error message stays until it is closed.');
    }
    asset('htmx');

    $intro = '<p class="docs-demo-intro">New components of <code>html()</code> (<code>yf_html</code>, Bootstrap 5 markup in
        <code>yf_html_bs5</code>). Switch the framework with <a href="' . $self . '?bs=5">?bs=5</a> / <a href="' . $self . '?bs=3">?bs=3</a>:
        each one is a pure addition with a Bootstrap 3 fallback, and the existing widgets render exactly as before. Text options are
        escaped; bodies (<code>body</code>, the collapse / ratio content, the toast body) are developer-supplied HTML and are not.</p>
        <ul class="docs-demo-intro">
        <li><code>toast($body, [\'title\', \'type\', \'autohide\' => true, \'delay\' => 5000, \'show\', \'no_close\', \'id\', \'class_add\'])</code> in
            <code>toast_container([\'toasts\' => [...], \'position\' => \'top-end\'])</code> (top / middle / bottom - start / center / end);
            the jQuery-free asset <code>yf_bs5_toasts</code> shows them on load and after htmx swaps. Bootstrap 3: <code>alert()</code>.</li>
        <li><code>_class(\'htmx\')->_toast($text, [\'title\', \'type\', \'autohide\', \'delay\'])</code>: a toast through the <code>HX-Trigger</code>
            header (event <code>yf-toast</code>, shown as text in the page\'s toast container); bs5 only.</li>
        <li><code>$PROJECT_CONF[\'common\'][\'MESSAGES_AS_TOASTS\'] = true</code> (default off): <code>common()->message_*()</code> become toasts
            under bs5, errors stay until closed. <a href="' . $self . '?messages=1" id="demo-messages">Show messages</a>.</li>
        <li><code>spinner([\'type\' => \'border|grow\', \'size\' => \'sm\', \'color\', \'text\', \'class_add\' => \'htmx-indicator\'])</code>;
            Bootstrap 3: <code>fa fa-spinner fa-spin</code>.</li>
        <li><code>placeholder([\'lines\' => 3, \'widths\' => [12, 8, 6], \'glow\' => true|\'wave\', \'size\', \'color\'])</code>; Bootstrap 3: an empty div.</li>
        <li><code>badge($text, [\'type\', \'pill\'])</code>: <code>badge text-bg-*</code> / Bootstrap 3 <code>label label-*</code> (a pill too: Bootstrap 3 badges have no colours).</li>
        <li><code>button($text, [\'type\', \'outline\', \'size\' => \'sm|lg\', \'icon\', \'link\', \'button_type\', \'active\', \'disabled\'])</code>;
            Bootstrap 3: <code>btn-default</code> for outline, <code>btn-xs</code> for sm.</li>
        <li><code>offcanvas([\'id\', \'title\', \'body\', \'placement\' => \'start|end|top|bottom\', \'backdrop\', \'scroll\', \'show\'])</code> and
            <code>offcanvas_button($text, $id, $button_options)</code>; Bootstrap 3: a <code>modal()</code>.</li>
        <li><code>dropdown($items, [\'text\', \'split\', \'link\', \'direction\' => \'up|end|start|center\', \'align\' => \'end\', \'dark\', \'auto_close\'])</code>;
            items <code>$link => $text</code>, <code>\'-\'</code>, <code>[\'header\' => ...]</code>, <code>[\'text\', \'link\', \'active\', \'disabled\', \'icon\']</code>;
            Bootstrap 3: <code>dropup</code>, <code>dropdown-menu-right</code>. <code>link</code> options take trusted URLs; <code>javascript:</code>, <code>data:</code>, <code>vbscript:</code> become <code>#</code>.</li>
        <li><code>collapse($body, [\'button\' => \'More filters\', \'show\', \'button_extra\', \'id\'])</code>; Bootstrap 3 collapse.</li>
        <li><code>ratio($html, [\'ratio\' => \'16x9\'])</code> (4x3, 1x1, 21x9 or any W x H); Bootstrap 3: <code>embed-responsive-16by9</code>.</li>
        </ul>';

    $toasts = html()->toast_container([
        'id' => 'demo-toasts',
        // the MESSAGES_AS_TOASTS container takes the default corner (top-end); two containers in one corner overlap
        'position' => 'bottom-end',
        'toasts' => [
            html()->toast('A toast rendered with the page; it hides after 4 seconds.', ['title' => 'Welcome', 'delay' => 4000, 'id' => 'demo-toast-auto']),
            html()->toast('This one stays until it is closed.', ['type' => 'warning', 'autohide' => false, 'id' => 'demo-toast-stay']),
        ],
    ]);
    $hx = '<p>' . html()->button('Save through htmx', ['type' => 'primary', 'id' => 'demo-hx-btn', 'attr' => [
            'hx-get' => $self . '?hx=toast',
            'hx-target' => '#demo-hx-result',
            'hx-swap' => 'outerHTML',
            'hx-indicator' => '#demo-hx-spinner',
        ]])
        . ' ' . html()->spinner(['size' => 'sm', 'class_add' => 'htmx-indicator', 'id' => 'demo-hx-spinner'])
        . ' <span id="demo-hx-result"></span></p>';

    $spinners = '<p id="demo-spinners">' . html()->spinner() . ' ' . html()->spinner(['type' => 'grow', 'color' => 'success'])
        . ' ' . html()->spinner(['size' => 'sm', 'color' => 'danger']) . ' ' . html()->spinner(['type' => 'grow', 'size' => 'sm', 'color' => 'primary']) . '</p>';
    $placeholders = '<div class="row"><div class="col-sm-6">' . html()->placeholder(['id' => 'demo-placeholder']) . '</div>'
        . '<div class="col-sm-6">' . html()->placeholder(['lines' => 2, 'widths' => [10, 4], 'glow' => 'wave', 'size' => 'lg', 'color' => 'primary']) . '</div></div>';

    $badges = '<p id="demo-badges">';
    foreach (['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'] as $type) {
        $badges .= html()->badge($type, ['type' => $type]) . ' ';
    }
    $badges .= html()->badge('12', ['type' => 'danger', 'pill' => 1]) . ' ' . html()->badge('<b>escaped</b> {braces}', ['type' => 'info']) . '</p>';

    $buttons = '<p id="demo-buttons">' . html()->button('Default') . ' ' . html()->button('Primary', ['type' => 'primary'])
        . ' ' . html()->button('Outline danger', ['type' => 'danger', 'outline' => 1]) . ' ' . html()->button('Edit', ['type' => 'success', 'size' => 'sm', 'icon' => 'fa fa-pencil'])
        . ' ' . html()->button('Large link', ['type' => 'info', 'size' => 'lg', 'link' => $self]) . ' ' . html()->button('Disabled', ['type' => 'secondary', 'disabled' => 1])
        . ' ' . html()->button('', ['icon' => 'fa fa-trash', 'title' => 'Delete', 'type' => 'link']) . '</p>';

    $offcanvas = html()->offcanvas([
            'id' => 'demo-offcanvas',
            'title' => 'Filters',
            'body' => '<p>Side panel content: filters, a mobile menu...</p>' . html()->button('Close', ['attr' => ['data-bs-dismiss' => 'offcanvas', 'data-dismiss' => 'modal']]),
            'placement' => 'end',
        ])
        . '<p>' . html()->offcanvas_button('Open the panel', 'demo-offcanvas', ['type' => 'primary', 'id' => 'demo-offcanvas-btn']) . '</p>';

    $items = [
        $self . '?act=edit' => 'Edit',
        $self . '?act=view' => 'View',
        ['text' => 'Active item', 'link' => $self . '?act=active', 'active' => 1],
        ['text' => 'Disabled', 'link' => $self . '?act=x', 'disabled' => 1],
        '-',
        ['header' => 'Danger zone'],
        ['text' => 'Delete', 'link' => $self . '?act=delete', 'icon' => 'fa fa-trash', 'class_add' => 'text-danger'],
    ];
    $dropdowns = '<p>' . html()->dropdown($items, ['text' => 'Actions', 'id' => 'demo-dropdown'])
        . ' ' . html()->dropdown($items, ['text' => 'Split', 'split' => 1, 'link' => $self . '?act=main', 'type' => 'primary', 'id' => 'demo-dropdown-split'])
        . ' ' . html()->dropdown($items, ['text' => 'Up, right-aligned', 'direction' => 'up', 'align' => 'end', 'id' => 'demo-dropdown-up'])
        . ' ' . html()->dropdown($items, ['text' => 'Dark', 'dark' => 1, 'type' => 'dark', 'id' => 'demo-dropdown-dark']) . '</p>';

    $collapse = html()->collapse('<div class="well card card-body">Hidden filters: shown and hidden by the button.</div>', ['button' => 'More filters', 'id' => 'demo-collapse']);

    $ratio = '<div class="row"><div class="col-sm-6">' . html()->ratio('<div class="embed-responsive-item" style="background:#6c757d;color:#fff;padding:1em">16x9</div>', ['id' => 'demo-ratio']) . '</div>'
        . '<div class="col-sm-3">' . html()->ratio('<div class="embed-responsive-item" style="background:#0d6efd;color:#fff;padding:1em">1x1</div>', ['ratio' => '1x1']) . '</div>'
        . '<div class="col-sm-3">' . html()->ratio('<div class="embed-responsive-item" style="background:#198754;color:#fff;padding:1em">3:2</div>', ['ratio' => '3:2']) . '</div></div>';

    return $intro . $toasts
        . '<h3>Toasts, htmx toast, spinner as hx-indicator</h3>' . $hx
        . '<h3>Spinners</h3>' . $spinners
        . '<h3>Placeholders</h3>' . $placeholders
        . '<h3>Badges</h3>' . $badges
        . '<h3>Buttons</h3>' . $buttons
        . '<h3>Offcanvas</h3>' . $offcanvas
        . '<h3>Dropdowns</h3>' . $dropdowns
        . '<h3>Collapse</h3>' . $collapse
        . '<h3>Ratio</h3>' . $ratio;
};

New components of html() (yf_html, Bootstrap 5 markup in yf_html_bs5). Switch the framework with ?bs=5 / ?bs=3: each one is a pure addition with a Bootstrap 3 fallback, and the existing widgets render exactly as before. Text options are escaped; bodies (body, the collapse / ratio content, the toast body) are developer-supplied HTML and are not.

  • toast($body, ['title', 'type', 'autohide' => true, 'delay' => 5000, 'show', 'no_close', 'id', 'class_add']) in toast_container(['toasts' => [...], 'position' => 'top-end']) (top / middle / bottom - start / center / end); the jQuery-free asset yf_bs5_toasts shows them on load and after htmx swaps. Bootstrap 3: alert().
  • _class('htmx')->_toast($text, ['title', 'type', 'autohide', 'delay']): a toast through the HX-Trigger header (event yf-toast, shown as text in the page's toast container); bs5 only.
  • $PROJECT_CONF['common']['MESSAGES_AS_TOASTS'] = true (default off): common()->message_*() become toasts under bs5, errors stay until closed. Show messages.
  • spinner(['type' => 'border|grow', 'size' => 'sm', 'color', 'text', 'class_add' => 'htmx-indicator']); Bootstrap 3: fa fa-spinner fa-spin.
  • placeholder(['lines' => 3, 'widths' => [12, 8, 6], 'glow' => true|'wave', 'size', 'color']); Bootstrap 3: an empty div.
  • badge($text, ['type', 'pill']): badge text-bg-* / Bootstrap 3 label label-* (a pill too: Bootstrap 3 badges have no colours).
  • button($text, ['type', 'outline', 'size' => 'sm|lg', 'icon', 'link', 'button_type', 'active', 'disabled']); Bootstrap 3: btn-default for outline, btn-xs for sm.
  • offcanvas(['id', 'title', 'body', 'placement' => 'start|end|top|bottom', 'backdrop', 'scroll', 'show']) and offcanvas_button($text, $id, $button_options); Bootstrap 3: a modal().
  • dropdown($items, ['text', 'split', 'link', 'direction' => 'up|end|start|center', 'align' => 'end', 'dark', 'auto_close']); items $link => $text, '-', ['header' => ...], ['text', 'link', 'active', 'disabled', 'icon']; Bootstrap 3: dropup, dropdown-menu-right. link options take trusted URLs; javascript:, data:, vbscript: become #.
  • collapse($body, ['button' => 'More filters', 'show', 'button_extra', 'id']); Bootstrap 3 collapse.
  • ratio($html, ['ratio' => '16x9']) (4x3, 1x1, 21x9 or any W x H); Bootstrap 3: embed-responsive-16by9.

Welcome

A toast rendered with the page; it hides after 4 seconds.
This one stays until it is closed.

Toasts, htmx toast, spinner as hx-indicator

Loading...

Spinners

Loading... Loading... Loading... Loading...

Placeholders

Badges

primary secondary success danger warning info light dark 12 <b>escaped</b> {braces}

Buttons

Large link

Offcanvas

Dropdowns

Split

Collapse

Hidden filters: shown and hidden by the button.

Ratio

16x9
1x1
3:2