return function () {
$self = url('/@object/@action/' . urlencode('html/bs5_options'));
$svg = function ($text, $color, $w = 870, $h = 300) {
$svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' . $w . '" height="' . $h . '" viewBox="0 0 ' . $w . ' ' . $h . '">'
. '<rect width="' . $w . '" height="' . $h . '" fill="' . $color . '"/>'
. '<text x="' . ($w / 2) . '" y="' . ($h / 2 + 12) . '" fill="#fff" font-family="sans-serif" font-size="34" text-anchor="middle">' . _prepare_html($text) . '</text></svg>';
return 'data:image/svg+xml,' . rawurlencode($svg);
};
$close = fn ($text = 'Close') => html()->button($text, ['attr' => ['data-bs-dismiss' => 'modal', 'data-dismiss' => 'modal']]);
$slides = fn ($prefix) => [
['img' => $svg($prefix . ' one', '#0d6efd', 600, 220), 'alt' => 'One', 'desc' => '<h5>One</h5>'],
['img' => $svg($prefix . ' two', '#198754', 600, 220), 'alt' => 'Two', 'desc' => '<h5>Two</h5>'],
['img' => $svg($prefix . ' three', '#dc3545', 600, 220), 'alt' => 'Three', 'desc' => '<h5>Three</h5>'],
];
// the item keys (list_group link / active / disabled / variant, navbar sub, progress label / striped / animated, slide interval)
// need $PROJECT_CONF['html']['B5_ITEM_OPTIONS'] = true (default off until 4.0); the docs site sets it, this page too
_class('html')->B5_ITEM_OPTIONS = true;
$htmx = _class('htmx');
// a carousel that htmx swaps in: under bs5 the yf_bs5_carousel asset starts it
if ($htmx->_wants_fragment() && ($_GET['hx'] ?? '') === 'carousel') {
return $htmx->_result('<div id="demo-hx-carousel" style="max-width:600px">' . html()->carousel($slides('Swapped'), ['interval' => 1000, 'id' => 'demo-carousel-hx']) . '</div>');
}
asset('htmx');
$intro = '<p class="docs-demo-intro">Options of the existing <code>html()</code> widgets (<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>.
Every option is active only when it is passed: a call without them renders exactly as before. The item keys (list group
<code>link</code> / <code>active</code> / <code>disabled</code> / <code>variant</code>, navbar <code>sub</code>, progress
<code>label</code> / <code>striped</code> / <code>animated</code>, a slide <code>interval</code>) also need
<code>$PROJECT_CONF[\'html\'][\'B5_ITEM_OPTIONS\'] = true</code> (default off until 4.0), so items built from database rows do
not change. Bootstrap 3 fallbacks are in brackets. Text options (the item names, <code>subtitle</code>, progress <code>label</code>, <code>offcanvas_title</code>) are escaped;
bodies (tab content, card <code>body</code> / <code>footer</code>, list items, modal header / body / footer) are developer-supplied HTML and are not.</p>
<ul class="docs-demo-intro">
<li><code>navbar($items, [\'collapse\' => \'md\', \'dark\' => 1, \'items_right\' => [...], \'offcanvas\' => true|\'start\', \'offcanvas_title\', \'theme\'])</code>;
an item with <code>\'sub\' => [...]</code> (<code>dropdown()</code> items) is a dropdown, <code>items_right</code> items are active with <code>active</code>.
(<code>navbar-toggle</code> + collapse at 768px, <code>navbar-inverse</code>, <code>navbar-right</code>; offcanvas is a collapse.)</li>
<li><code>modal([\'size\' => \'sm|lg|xl\', \'fullscreen\' => true|\'md\', \'centered\', \'scrollable\', \'static\', \'fade\', \'theme\'])</code> and
<code>modal_button($text, $id, $button_options)</code>. (sm / lg, xl is lg; static, fade; centered, scrollable and fullscreen are ignored.)</li>
<li><code>tabs($tabs, [\'type\' => \'tabs|pills|underline\', \'fill\', \'justified\', \'vertical\'])</code>, also form2
<code>form($r, [\'tabs\' => [\'type\' => \'pills\']])</code>. (underline is tabs, fill is <code>nav-justified</code>, vertical is <code>nav-stacked</code> in a column.)</li>
<li><code>list_group($items, [\'numbered\', \'flush\', \'theme\'])</code>, items <code>[\'body\', \'link\', \'active\', \'disabled\', \'variant\' => \'success\']</code>;
with a <code>link</code> the group is a <code>div</code> of links (<code>a.list-group-item</code>; numbered is an <code>ol</code> without numbers, flush is ignored).
<code>link</code> options, navbar <code>items_right</code> / <code>sub</code> links and the card <code>img_top</code> take trusted URLs;
<code>javascript:</code>, <code>data:</code>, <code>vbscript:</code> become <code>#</code>.</li>
<li><code>card([...])</code> = <code>panel()</code> with <code>\'subtitle\'</code>, <code>\'footer\'</code>, <code>\'img_top\'</code> / <code>\'img_alt\'</code>,
<code>\'border\' => \'primary\'</code>, <code>\'theme\'</code>. (<code>panel-footer</code>, the image under the heading, <code>panel-primary</code>, subtitle as <code>small</code>.)</li>
<li><code>progress_bar($bars, [\'striped\', \'animated\', \'height\' => 24])</code>, per bar <code>\'label\'</code>, <code>\'striped\'</code>, <code>\'animated\'</code>.
(<code>progress-bar-striped active</code>.)</li>
<li><code>accordion($items, [\'flush\', \'always_open\', \'theme\'])</code>. (always open: no <code>data-parent</code>; flush is ignored.)</li>
<li><code>carousel($slides, [\'fade\', \'interval\' => 5000|false, \'pause\' => \'hover\'|false, \'ride\', \'touch\', \'wrap\', \'keyboard\', \'theme\'])</code>,
per slide <code>\'interval\'</code>; the jQuery-free <code>yf_bs5_carousel</code> asset starts carousels htmx swaps in. (interval, pause, wrap; no fade.)</li>
<li>Colour modes: <code>$CONF[\'bs_color_mode\'] = \'light|dark|auto\'</code> (or <code>bs_color_mode_user</code> / <code>_admin</code>) puts
<code>data-bs-theme</code> on <code><html></code>; with <code>bs_theme_allow_override_for_user</code> the header gets a light / dark / auto
switcher next to the theme switcher (asset <code>yf_bs5_color_modes</code>, remembered in a cookie). Per widget: <code>\'theme\' => \'dark\'</code>.
(Bootstrap 3: ignored.)</li>
</ul>';
$nav_items = [
'brand' => ['name' => 'Brand', 'link' => $self],
'home' => ['name' => 'Home', 'link' => $self],
'docs' => ['name' => 'Docs', 'link' => $self . '?p=docs'],
'more' => ['name' => 'More', 'sub' => [
$self . '?p=a' => 'Action',
['name' => 'Another action', 'link' => $self . '?p=b'],
'-',
['header' => 'Section'],
['text' => 'Disabled', 'link' => $self . '?p=c', 'disabled' => 1],
]],
];
$navbars = '<div id="demo-navbar-wrap">' . html()->navbar($nav_items, [
'id' => 'demo-navbar',
'collapse' => 'md',
'dark' => 1,
'items_right' => [
['name' => 'Sign in', 'link' => $self . '?p=login', 'active' => 1],
['name' => 'Account', 'sub' => [$self . '?p=profile' => 'Profile', $self . '?p=logout' => 'Sign out']],
],
]) . '</div>'
. '<p></p>' . html()->navbar(['brand' => ['name' => 'Offcanvas', 'link' => $self], ['name' => 'Home', 'link' => $self], ['name' => 'Docs', 'link' => $self . '?p=docs']], [
'id' => 'demo-navbar-oc',
'offcanvas' => true,
'offcanvas_title' => 'Menu',
]);
$long = str_repeat('<p>A long body: the dialog scrolls inside, the page does not.</p>', 30);
$modals = '';
$buttons = [];
foreach ([
'sm' => ['Small', ['size' => 'sm']],
'lg' => ['Large', ['size' => 'lg']],
'xl' => ['Extra large', ['size' => 'xl']],
'fs' => ['Fullscreen', ['fullscreen' => true]],
'centered' => ['Centered, scrollable', ['centered' => 1, 'scrollable' => 1, 'body' => $long]],
'static' => ['Static backdrop, fade', ['static' => 1, 'fade' => 1, 'body' => '<p>A click outside or Escape does not close it; the button does.</p>']],
'dark' => ['Dark', ['theme' => 'dark', 'fade' => 1]],
] as $key => [$title, $opts]) {
$modals .= html()->modal($opts + [
'id' => 'demo-modal-' . $key,
'header' => $title,
'show_close' => 1,
'body' => '<p>' . $title . ' modal.</p>',
'footer' => $close(),
]);
$buttons[] = html()->modal_button($title, 'demo-modal-' . $key, ['type' => 'primary', 'outline' => 1, 'id' => 'demo-modal-' . $key . '-btn']);
}
$modals = '<p id="demo-modal-buttons">' . implode(' ', $buttons) . '</p>' . $modals;
$tab_items = ['home' => '<p>Home pane.</p>', 'profile' => '<p>Profile pane.</p>', 'contact' => '<p>Contact pane.</p>'];
$tabs = '<div id="demo-tabs-pills">' . html()->tabs($tab_items, ['type' => 'pills', 'id' => 'demo-pills', 'links_prefix' => 'pills_']) . '</div>'
. '<div id="demo-tabs-underline" class="mt-3">' . html()->tabs($tab_items, ['type' => 'underline', 'fill' => 1, 'id' => 'demo-underline', 'links_prefix' => 'under_']) . '</div>'
. '<div id="demo-tabs-vertical" class="mt-3">' . html()->tabs($tab_items, ['type' => 'pills', 'vertical' => 1, 'id' => 'demo-vertical', 'links_prefix' => 'vert_']) . '</div>';
$lists = '<div class="row"><div class="col-sm-4">' . html()->list_group([
['body' => 'Active link', 'link' => $self . '?p=1', 'active' => 1],
['body' => 'A link', 'link' => $self . '?p=2', 'badge' => 4],
['body' => 'Disabled link', 'link' => $self . '?p=3', 'disabled' => 1],
['body' => 'Success', 'link' => $self . '?p=4', 'variant' => 'success'],
], ['id' => 'demo-lg-links']) . '</div>'
. '<div class="col-sm-4">' . html()->list_group(['First', 'Second', ['body' => 'Third', 'variant' => 'warning']], ['numbered' => 1, 'id' => 'demo-lg-numbered']) . '</div>'
. '<div class="col-sm-4">' . html()->list_group(['Flush one', 'Flush two', 'Dark theme'], ['flush' => 1, 'theme' => 'dark', 'id' => 'demo-lg-flush']) . '</div></div>';
$cards = '<div class="row"><div class="col-sm-6">' . html()->card([
'id' => 'demo-card',
'title' => 'Card title',
'subtitle' => 'Subtitle <escaped>',
// img_top is scheme-checked like a link: a data: URI becomes #
'img_top' => url('/dynamic/placeholder/600x200'),
'img_alt' => 'A grey placeholder',
'body' => '<p>Body text of the card.</p>',
'footer' => 'Footer',
'border' => 'primary',
]) . '</div><div class="col-sm-6">' . html()->card(['id' => 'demo-card-dark', 'title' => 'Dark card', 'body' => '<p>theme => dark</p>', 'footer' => 'Footer', 'theme' => 'dark']) . '</div></div>';
$progress = html()->progress_bar([['val' => 60, 'label' => '60%', 'type' => 'success']], ['striped' => 1, 'animated' => 1, 'height' => 24, 'id' => 'demo-progress'])
. '<p></p>' . html()->progress_bar([['val' => 30, 'label' => 'Done 30%'], ['val' => 25, 'label' => '25%', 'type' => 'warning', 'striped' => 1], ['val' => 15, 'type' => 'danger']], ['id' => 'demo-progress-stacked']);
$accordion = html()->accordion([
'first' => '<p>Open on load. Opening another one keeps this one open (always_open).</p>',
'second' => '<p>Second body.</p>',
'third' => '<p>Third body.</p>',
], ['flush' => 1, 'always_open' => 1, 'id' => 'demo-accordion']);
$carousel = '<div style="max-width:600px">' . html()->carousel($slides('Slide'), ['fade' => 1, 'interval' => 1500, 'pause' => false, 'id' => 'demo-carousel']) . '</div>'
. '<p class="mt-3">' . html()->button('Load a carousel through htmx', ['type' => 'primary', 'outline' => 1, 'id' => 'demo-hx-carousel-btn', 'attr' => [
'hx-get' => $self . '?hx=carousel',
'hx-target' => '#demo-hx-carousel',
'hx-swap' => 'outerHTML',
]]) . '</p><div id="demo-hx-carousel"></div>';
$mode = common()->bs_color_mode();
$modes = '<p id="demo-color-mode">Colour mode of this page: <b>' . ($mode !== '' ? _prepare_html($mode) : 'off (Bootstrap 3, or no bs_color_mode)') . '</b>.'
. ' Use the switcher in the header (Bootstrap 5); the dark card, the dark list group and the dark modal above keep their own mode.</p>';
return $intro
. '<h3>Navbar: collapse, dark, dropdowns, items on the right, offcanvas</h3>' . $navbars
. '<h3>Modal sizes and options</h3>' . $modals
. '<h3>Tabs: pills, underline + fill, vertical</h3>' . $tabs
. '<h3>List groups: links, numbered, flush</h3>' . $lists
. '<h3>Cards</h3>' . $cards
. '<h3>Progress</h3>' . $progress
. '<h3>Accordion: flush, always open</h3>' . $accordion
. '<h3>Carousel: fade, 1.5 s interval, no pause; one loaded through htmx</h3>' . $carousel
. '<h3>Colour modes</h3>' . $modes;
};