✨ (bootsier): Añade componentes esenciales
- Añade Dialog como modal de Bootstrap, con el JS de soporte para `htmx:confirm` y el saneado de su posición fija. - Adapta Nav, Navbar y Dropdown al reuso de los tipos movidos al core, y traduce BootsierColors/Intent en Button y Badge, con soporte para Light/Dark vía with_color(). - Reexporta en el tema los componentes de PageTop que faltaban: Block, Breadcrumb, Messages, Pager, Table y form::Number.
This commit is contained in:
parent
eb63a7ef37
commit
c0a5a8c3ab
47 changed files with 2135 additions and 1818 deletions
|
|
@ -1,4 +1,4 @@
|
|||
// Bootsier CSS rules: self-hosted fonts, form components, and regions.
|
||||
// Bootsier CSS rules: self-hosted fonts, form elements, regions and components.
|
||||
|
||||
// Self-hosted Source Sans 3 (SIL OFL 1.1), served from /bootsier/fonts.
|
||||
// Required by AdminLTE 4, which declares it as the primary font family in $font-family-sans-serif.
|
||||
|
|
@ -39,6 +39,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Button set (button::ButtonSet): spaces its buttons with a gap instead of relying on Bootstrap's
|
||||
// .btn-group, which merges adjacent buttons into a single joined control.
|
||||
.button-set {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $spacer * .5;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
// Fieldset with border and floating legend.
|
||||
fieldset {
|
||||
position: relative;
|
||||
|
|
@ -83,3 +92,153 @@ fieldset > legend {
|
|||
padding: 0.75rem 0 3rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// Messages component. Classes are fixed by pagetop core (`.message`, `.message-info`,
|
||||
// `.message-warning`, `.message-error`); reusing Bootstrap's own `.alert`/`.alert-*` styles keeps
|
||||
// the palette in sync with the active Bootstrap theme instead of duplicating it here.
|
||||
.message {
|
||||
@extend .alert;
|
||||
}
|
||||
.message-info {
|
||||
@extend .alert-info;
|
||||
}
|
||||
.message-warning {
|
||||
@extend .alert-warning;
|
||||
}
|
||||
.message-error {
|
||||
@extend .alert-danger;
|
||||
}
|
||||
|
||||
// Sort indicator on sortable table headers (`table::SortLink`), using Bootstrap Icons'
|
||||
// sort-up/sort-down glyphs instead of the plain triangle from basic.css.
|
||||
.table-sort::after {
|
||||
font-family: $bootstrap-icons-font;
|
||||
content: "\f575"; // sort-down
|
||||
margin-left: 0.25rem;
|
||||
font-size: 0.8em;
|
||||
opacity: 0.35;
|
||||
}
|
||||
.table-sort-asc::after {
|
||||
content: "\f57b"; // sort-up
|
||||
opacity: 1;
|
||||
}
|
||||
.table-sort-desc::after {
|
||||
content: "\f575"; // sort-down
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// Pager component.
|
||||
// `.pagination`, `.page-item`, `.page-link`, `.active` and `.disabled` already come styled by
|
||||
// Bootstrap, since Pager reuses that same markup (the ellipsis is a disabled `.page-link` span,
|
||||
// same as any other cell). Only what Pager adds on top needs rules here: the <nav> wrapper and
|
||||
// the jump-to-page form.
|
||||
|
||||
// <nav> wrapper: flex row that also holds the jump-to-page form.
|
||||
.pager {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0 $spacer; // row-gap column-gap: no vertical gap when the summary drops to its own line.
|
||||
margin-block: $spacer;
|
||||
}
|
||||
|
||||
// Horizontal alignment set via PagerAlign.
|
||||
.pager-align-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.pager-align-center {
|
||||
justify-content: center;
|
||||
}
|
||||
.pager-align-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
// Pushes the rest of .pager (pagination, jump form) to the opposite side. With
|
||||
// PagerAlign::Start the summary swaps to the other end instead, since otherwise it would sit
|
||||
// right next to a pagination that's already at the start. With PagerAlign::Center an auto margin
|
||||
// wouldn't work -it always claims all the free space, so the pagination could never be genuinely
|
||||
// centered- so instead the summary drops to its own line below, centered too.
|
||||
.pager-summary {
|
||||
color: var(--bs-secondary-color);
|
||||
font-size: $font-size-sm;
|
||||
align-self: flex-start;
|
||||
margin-inline-end: auto;
|
||||
}
|
||||
.pager-align-start .pager-summary {
|
||||
order: 1;
|
||||
margin-inline-end: 0;
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
.pager-align-center .pager-summary {
|
||||
order: 1;
|
||||
flex-basis: 100%;
|
||||
text-align: center;
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
|
||||
// By default, .page-link-icon shows the text of pager_previous_label/pager_next_label
|
||||
// ("Previous"/"Next"). The full accessible text lives separately, in the link's aria-label, so
|
||||
// replacing the visible text here with a symbol does not affect accessibility. To use "‹"/"›"
|
||||
// instead, uncomment:
|
||||
/*
|
||||
.page-link-icon {
|
||||
display: inline-block;
|
||||
font-size: 0;
|
||||
}
|
||||
.page-previous .page-link-icon::before {
|
||||
content: "‹";
|
||||
font-size: var(--bs-pagination-font-size);
|
||||
display: inline-block;
|
||||
transform: translateY(-0.1em) scale(1.5);
|
||||
}
|
||||
.page-next .page-link-icon::before {
|
||||
content: "›";
|
||||
font-size: var(--bs-pagination-font-size);
|
||||
display: inline-block;
|
||||
transform: translateY(-0.1em) scale(1.5);
|
||||
}
|
||||
*/
|
||||
|
||||
// Jump-to-page field and submit button, presented as a single joined control. Same block margins
|
||||
// as `ol`/`ul`/`dl` in Bootstrap's reboot (literal values there too, not variables).
|
||||
.pager-jump {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
// One focus ring for the whole control instead of one per element.
|
||||
.pager-jump:focus-within {
|
||||
border-radius: $input-border-radius;
|
||||
box-shadow: $input-focus-box-shadow;
|
||||
}
|
||||
.pager-jump-input > .form-control:focus,
|
||||
.pager-jump-button:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
.pager-jump-input > .form-control {
|
||||
// Extra width equals twice the field's own horizontal padding.
|
||||
width: calc(var(--pager-jump-width, 1ch) + #{$input-padding-x * 4});
|
||||
text-align: center;
|
||||
appearance: textfield;
|
||||
-moz-appearance: textfield;
|
||||
|
||||
// Logical (not physical) corners, so the shape flips correctly under RTL.
|
||||
border-start-end-radius: 0;
|
||||
border-end-end-radius: 0;
|
||||
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.pager-jump-input {
|
||||
margin: 0;
|
||||
}
|
||||
.pager-jump-button {
|
||||
@extend .btn-primary;
|
||||
border-start-start-radius: 0;
|
||||
border-end-start-radius: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,3 +2,4 @@
|
|||
// so that Bootstrap's !default declarations do not override these values.
|
||||
|
||||
$font-size-base: 1.125rem;
|
||||
$navbar-brand-font-size: 1.75rem;
|
||||
|
|
|
|||
62
extensions/pagetop-bootsier/assets/bootsier.confirm.js
Normal file
62
extensions/pagetop-bootsier/assets/bootsier.confirm.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
(function () {
|
||||
'use strict';
|
||||
|
||||
// htmx dispatches `htmx:confirm` in place of `window.confirm()` whenever the element that
|
||||
// would issue a request carries `hx-confirm`. Intercepting it here upgrades every
|
||||
// `hx-confirm` in the project to a Bootstrap modal, without touching the element that
|
||||
// requested it or the request itself: cancelling is a no-op, confirming resumes the exact
|
||||
// same request via `evt.detail.issueRequest(true)`.
|
||||
if (typeof bootstrap === 'undefined' || !bootstrap.Modal) { return; }
|
||||
|
||||
var modalEl = null;
|
||||
var questionEl = null;
|
||||
var pendingEvent = null;
|
||||
|
||||
function ensureModal() {
|
||||
if (modalEl) { return; }
|
||||
|
||||
var okLabel = document.body.dataset.confirmOk || 'OK';
|
||||
var cancelLabel = document.body.dataset.confirmCancel || 'Cancel';
|
||||
|
||||
modalEl = document.createElement('div');
|
||||
modalEl.className = 'modal fade';
|
||||
modalEl.tabIndex = -1;
|
||||
modalEl.setAttribute('aria-hidden', 'true');
|
||||
modalEl.innerHTML =
|
||||
'<div class="modal-dialog">' +
|
||||
'<div class="modal-content">' +
|
||||
'<div class="modal-body"></div>' +
|
||||
'<div class="modal-footer">' +
|
||||
'<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"></button>' +
|
||||
'<button type="button" class="btn btn-danger" data-confirm-accept></button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
document.body.appendChild(modalEl);
|
||||
|
||||
questionEl = modalEl.querySelector('.modal-body');
|
||||
modalEl.querySelector('[data-bs-dismiss]').textContent = cancelLabel;
|
||||
var acceptBtn = modalEl.querySelector('[data-confirm-accept]');
|
||||
acceptBtn.textContent = okLabel;
|
||||
|
||||
acceptBtn.addEventListener('click', function () {
|
||||
var evt = pendingEvent;
|
||||
pendingEvent = null;
|
||||
bootstrap.Modal.getInstance(modalEl).hide();
|
||||
if (evt) { evt.detail.issueRequest(true); }
|
||||
});
|
||||
|
||||
modalEl.addEventListener('hidden.bs.modal', function () {
|
||||
pendingEvent = null;
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('htmx:confirm', function (evt) {
|
||||
if (!evt.detail.question) { return; }
|
||||
evt.preventDefault();
|
||||
ensureModal();
|
||||
questionEl.textContent = evt.detail.question;
|
||||
pendingEvent = evt;
|
||||
bootstrap.Modal.getOrCreateInstance(modalEl).show();
|
||||
});
|
||||
}());
|
||||
18
extensions/pagetop-bootsier/assets/bootsier.dialog.js
Normal file
18
extensions/pagetop-bootsier/assets/bootsier.dialog.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
(function () {
|
||||
'use strict';
|
||||
|
||||
// Bootstrap's `.modal` is a plain `position: fixed` element: it never leaves the CSS stacking
|
||||
// context of whatever ancestor it was rendered inside. If any ancestor sets its own `z-index`
|
||||
// (e.g. pagetop's `Intro` component, for its decorative layering), the modal's `z-index` only
|
||||
// ranks it among the elements of *that* ancestor's context, so `.modal-backdrop` -always a
|
||||
// direct child of `<body>`, added by Bootstrap itself- can end up rendering above it and
|
||||
// swallowing every click, even though `.modal`'s own `z-index` is numerically higher. Basic's
|
||||
// native `<dialog>` escapes this for free via the browser's "top layer"; Bootsier's `.modal`
|
||||
// does not, so it is moved to `<body>` here, exactly like Bootstrap already does with its own
|
||||
// backdrop.
|
||||
document.querySelectorAll('.modal.dialog').forEach(function (modal) {
|
||||
if (modal.parentElement !== document.body) {
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
});
|
||||
}());
|
||||
Loading…
Add table
Add a link
Reference in a new issue