return function () {
$self = url('/@object/@action/' . urlencode('themes/bs_theme'));
$is_bs5 = conf('css_framework') === 'bs5';
$theme = clone _class('bs_theme');
// the preview is laid over the theme of this page; pick another one with the theme switcher
$base = $theme->base_of(common()->bs_current_theme()) ?: 'bootstrap';
$defaults = ['primary' => '#0d6efd', 'body-bg' => '#ffffff', 'body-color' => '#212529', 'border-radius' => '0.375rem'];
$radii = ['0' => 'square', '0.375rem' => 'default', '0.75rem' => 'round', '1.5rem' => 'pill-like'];
$input = [];
foreach ($defaults as $k => $v) {
$input[$k] = is_string($_GET[$k] ?? null) ? $_GET[$k] : $v;
}
// Validation is yf_bs_theme's own: only #rgb / #rrggbb colours and rem / em / px lengths survive, anything else is dropped
$theme->THEMES = ['preview' => ['base' => $base] + $input];
$theme->_noticed = [];
$vars = @$theme->variables('preview');
$dropped = array_diff_key($input, $vars);
$css = $theme->css($vars, $base);
// The generated sheet targets the whole page (:root and .btn-*); scoped under .docs-theme-preview it styles the preview box only
$scoped = preg_replace_callback('~([^{}]+)\{([^{}]*)\}~', function ($m) {
$selectors = [];
foreach (explode(',', $m[1]) as $sel) {
$sel = trim($sel);
$selectors[] = in_array($sel, [':root', '[data-bs-theme=light]'], true) ? '.docs-theme-preview' : '.docs-theme-preview ' . $sel;
}
return implode(',', array_unique($selectors)) . '{' . $m[2] . '}';
}, preg_replace('~/\*.*?\*/~s', '', $css));
css($scoped . PHP_EOL . '.docs-theme-preview { background-color:var(--bs-body-bg); color:var(--bs-body-color); border:1px solid var(--bs-border-color, #ddd); border-radius:var(--bs-border-radius, 4px); padding:15px; margin-bottom:15px; }'
. PHP_EOL . '.docs-demo-bs-theme-form input[type=color] { width:4em; height:2.2em; padding:2px; }'
. PHP_EOL . '.docs-demo-css code { white-space:pre-wrap; word-break:break-all; }');
$intro = '<p class="docs-demo-intro">A site colour scheme with <code>yf_bs_theme</code>, the Bootstrap 5 runtime theme layer: pick colours,
and the framework computes the hover, active and focus shades and the contrasting text colour the way Bootstrap\'s Sass does, as a
small stylesheet of <code>--bs-*</code> variables over the base theme (here <b>' . $base . '</b>, the current theme). Every value
passes <code>yf_bs_theme::variables()</code>, which keeps only <code>#rgb</code> / <code>#rrggbb</code> colours and
<code>rem</code> / <code>em</code> / <code>px</code> lengths, so a request cannot inject CSS. Nothing is saved; the preview is
scoped to the box below. Classes: <code>yf_bs_theme</code>, <code>yf_common</code>.</p>';
$notes = '';
if ( ! $is_bs5) {
$notes .= '<div class="alert alert-warning">This page runs Bootstrap 3, which has no CSS variables: a site theme renders its base
there. Switch to <a href="' . _prepare_html(_class('docs')->_bs_switch_url('5'), 0) . '">Bootstrap 5</a> to see the preview.</div>';
}
if ($dropped) {
$notes .= '<div class="alert alert-danger">Dropped as invalid: ' . _prepare_html(implode(', ', array_keys($dropped)), 0) . '.</div>';
}
$fields = '';
foreach (['primary' => 'Primary', 'body-bg' => 'Body background', 'body-color' => 'Body text'] as $k => $label) {
$value = $vars[$k] ?? $defaults[$k];
$fields .= '<label class="docs-demo-color">' . $label . ' <input type="color" name="' . $k . '" value="' . _prepare_html($value, 0) . '"></label> ';
}
$options = '';
foreach ($radii as $value => $label) {
$options .= '<option value="' . $value . '"' . (($vars['border-radius'] ?? $defaults['border-radius']) === $value ? ' selected' : '') . '>' . $label . '</option>';
}
$form = '<form method="get" action="' . $self . '" class="form-inline docs-demo-bs-theme-form">' . $fields
. '<label>Corners <select name="border-radius" class="form-control form-select d-inline-block w-auto">' . $options . '</select></label> '
. '<button type="submit" class="btn btn-primary">Preview</button> <a class="btn btn-default btn-outline-secondary" href="' . $self . '">Reset</a></form>';
$preview = '<div class="docs-theme-preview">'
. '<h4>Preview</h4><p>Body text with <a href="#">a link</a>.</p>'
. '<p><button type="button" class="btn btn-primary">Primary</button> <button type="button" class="btn btn-outline-primary">Outline</button> '
. '<button type="button" class="btn btn-secondary">Secondary</button> <span class="badge text-bg-primary">badge</span></p>'
. '<ul class="nav nav-pills mb-2"><li class="nav-item"><a class="nav-link active" href="#">Active pill</a></li><li class="nav-item"><a class="nav-link" href="#">Pill</a></li></ul>'
. '<div class="progress mb-2"><div class="progress-bar" style="width:40%">40%</div></div>'
. '<input class="form-control" placeholder="Focus me"></div>';
$config = '$PROJECT_CONF[\'bs_theme\'][\'THEMES\'][\'brand\'] = ' . var_export(['base' => $base] + $vars, true) . ';' . PHP_EOL
. '$CONF[\'DEF_BOOTSTRAP_THEME_USER\'] = \'brand\';';
return $intro . $notes . $form . $preview
. '<h4>Project config for this theme</h4><pre><code class="php">' . _prepare_html($config, 0) . '</code></pre>'
. '<h4>Generated stylesheet</h4><pre class="docs-demo-css"><code class="css">' . _prepare_html($css, 0) . '</code></pre>';
};
A site colour scheme with yf_bs_theme, the Bootstrap 5 runtime theme layer: pick colours,
and the framework computes the hover, active and focus shades and the contrasting text colour the way Bootstrap's Sass does, as a
small stylesheet of --bs-* variables over the base theme (here slate, the current theme). Every value
passes yf_bs_theme::variables(), which keeps only #rgb / #rrggbb colours and
rem / em / px lengths, so a request cannot inject CSS. Nothing is saved; the preview is
scoped to the box below. Classes: yf_bs_theme, yf_common.
This page runs Bootstrap 3, which has no CSS variables: a site theme renders its base
there. Switch to
Bootstrap 5 to see the preview.
Project config for this theme
$PROJECT_CONF['bs_theme']['THEMES']['brand'] = array (
'base' => 'slate',
'primary' => '#0d6efd',
'body-bg' => '#ffffff',
'body-color' => '#212529',
'border-radius' => '0.375rem',
);
$CONF['DEF_BOOTSTRAP_THEME_USER'] = 'brand';
Generated stylesheet
/* yf bs_theme v1 over slate */
:root,[data-bs-theme=light]{
--bs-primary:#0d6efd;
--bs-primary-rgb:13,110,253;
--bs-primary-text-emphasis:#052c65;
--bs-primary-bg-subtle:#cfe2ff;
--bs-primary-border-subtle:#9ec5fe;
--bs-focus-ring-color:rgba(13,110,253,.25);
--bs-body-bg:#ffffff;
--bs-body-bg-rgb:255,255,255;
--bs-body-color:#212529;
--bs-body-color-rgb:33,37,41;
--bs-border-radius:0.375rem
}
.btn-primary{--bs-btn-color:#ffffff;--bs-btn-bg:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#ffffff;--bs-btn-hover-bg:#0b5ed7;--bs-btn-hover-border-color:#0a58ca;--bs-btn-focus-shadow-rgb:49,132,253;--bs-btn-active-color:#ffffff;--bs-btn-active-bg:#0a58ca;--bs-btn-active-border-color:#0a53be;--bs-btn-disabled-color:#ffffff;--bs-btn-disabled-bg:#0d6efd;--bs-btn-disabled-border-color:#0d6efd}
.btn-outline-primary{--bs-btn-color:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#ffffff;--bs-btn-hover-bg:#0d6efd;--bs-btn-hover-border-color:#0d6efd;--bs-btn-focus-shadow-rgb:13,110,253;--bs-btn-active-color:#ffffff;--bs-btn-active-bg:#0d6efd;--bs-btn-active-border-color:#0d6efd;--bs-btn-disabled-color:#0d6efd;--bs-btn-disabled-border-color:#0d6efd}
.text-bg-primary{color:#ffffff!important}
.nav-pills{--bs-nav-pills-link-active-bg:#0d6efd}
.pagination{--bs-pagination-active-bg:#0d6efd;--bs-pagination-active-border-color:#0d6efd;--bs-pagination-focus-box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}
.dropdown-menu{--bs-dropdown-link-active-bg:#0d6efd}
.list-group{--bs-list-group-active-bg:#0d6efd;--bs-list-group-active-border-color:#0d6efd}
.progress,.progress-stacked{--bs-progress-bar-bg:#0d6efd}
.form-control:focus,.form-select:focus,.form-check-input:focus{border-color:#86b7fe;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}
.form-check-input:checked,.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd}
.form-range::-webkit-slider-thumb{background-color:#0d6efd}.form-range::-moz-range-thumb{background-color:#0d6efd}