181 lines
8.1 KiB
PHP
181 lines
8.1 KiB
PHP
<?php
|
|
// Shared admin shell — close. Closes the main/content containers opened by header.php.
|
|
?>
|
|
</div><!-- /.p-3 / .p-md-4 -->
|
|
</main>
|
|
<?= script_tag('assets/vendor/jquery/jquery-3.7.1.min.js') ?>
|
|
<?= script_tag('assets/vendor/bootstrap5/js/bootstrap.bundle.min.js') ?>
|
|
<?= script_tag('assets/js/password-toggle.js') ?>
|
|
<?php // Summernote (~100 KB) só nas páginas que têm um editor: carregado a pedido, abaixo. ?>
|
|
<script>window.SUMMERNOTE_SRC = [<?= json_encode(_asset_versioned('assets/vendor/summernote/summernote-bs5.min.js')) ?>, <?= json_encode(_asset_versioned('assets/vendor/summernote/lang/summernote-pt-PT.min.js')) ?>];</script>
|
|
<script>
|
|
// Attach Summernote to any <textarea class="js-wysiwyg">. The editor keeps the
|
|
// underlying textarea in sync, so normal form POSTs submit the generated HTML.
|
|
// Inline image upload is intentionally omitted (base64 would bloat the row) —
|
|
// modules that need images use their own file-upload fields.
|
|
function initWysiwyg() {
|
|
if (!window.jQuery || !jQuery.fn || !jQuery.fn.summernote) return;
|
|
jQuery('textarea.js-wysiwyg').each(function () {
|
|
var $ta = jQuery(this);
|
|
$ta.summernote({
|
|
lang: 'pt-PT',
|
|
height: parseInt($ta.data('wysiwyg-height'), 10) || 240,
|
|
disableDragAndDrop: true,
|
|
toolbar: [
|
|
['style', ['style']],
|
|
['font', ['bold', 'italic', 'underline', 'clear']],
|
|
['para', ['ul', 'ol', 'paragraph']],
|
|
['insert', ['link']],
|
|
['view', ['codeview', 'fullscreen']]
|
|
],
|
|
styleTags: ['p', 'h2', 'h3', 'h4', 'blockquote'],
|
|
popover: { image: [], air: [] }
|
|
});
|
|
});
|
|
}
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
if (!document.querySelector('textarea.js-wysiwyg')) return;
|
|
// Carrega o editor e a tradução por ordem, só aqui.
|
|
(function next(i) {
|
|
if (i >= window.SUMMERNOTE_SRC.length) return initWysiwyg();
|
|
var s = document.createElement('script');
|
|
s.src = window.SUMMERNOTE_SRC[i];
|
|
s.onload = function () { next(i + 1); };
|
|
document.body.appendChild(s);
|
|
})(0);
|
|
});
|
|
|
|
// Tema: claro / escuro / automático (o <head> já aplicou o guardado).
|
|
(function () {
|
|
var root = document.documentElement, btn = document.getElementById('themeToggle');
|
|
var mq = window.matchMedia('(prefers-color-scheme: dark)');
|
|
var icons = { light: 'bi-sun', dark: 'bi-moon-stars', auto: 'bi-circle-half' };
|
|
var names = { light: 'claro', dark: 'escuro', auto: 'automático' };
|
|
function apply(pref) {
|
|
var dark = pref === 'dark' || (pref === 'auto' && mq.matches);
|
|
root.setAttribute('data-bs-theme', dark ? 'dark' : 'light');
|
|
root.setAttribute('data-theme-pref', pref);
|
|
if (btn) {
|
|
btn.querySelector('i').className = 'bi ' + icons[pref];
|
|
btn.setAttribute('aria-label', 'Tema: ' + names[pref]);
|
|
}
|
|
document.querySelectorAll('[data-theme-set]').forEach(function (b) {
|
|
var on = b.getAttribute('data-theme-set') === pref;
|
|
b.classList.toggle('active', on);
|
|
b.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
});
|
|
}
|
|
var pref = root.getAttribute('data-theme-pref') || 'auto';
|
|
apply(pref);
|
|
document.querySelectorAll('[data-theme-set]').forEach(function (b) {
|
|
b.addEventListener('click', function () {
|
|
pref = b.getAttribute('data-theme-set');
|
|
try { localStorage.setItem('bo-theme', pref); } catch (e) {}
|
|
apply(pref);
|
|
});
|
|
});
|
|
// O sistema mudou de tema: só conta em «automático».
|
|
mq.addEventListener('change', function () { if (pref === 'auto') apply('auto'); });
|
|
})();
|
|
|
|
// Barra lateral. Em ecrã grande o hamburger alterna entre completa e mini (so
|
|
// o texto das categorias, lembrado em localStorage); em telemovel abre/fecha a
|
|
// gaveta por cima do conteudo. Um so manipulador para os dois botoes. O CSS
|
|
// decide o que cada estado mostra; o estado inicial ja veio do <head>.
|
|
(function () {
|
|
var sb = document.getElementById('sidebar');
|
|
var bd = document.getElementById('sidebarBackdrop');
|
|
var toggles = document.querySelectorAll('[data-sidebar-toggle]');
|
|
var root = document.documentElement;
|
|
var nav = sb && sb.querySelector('.bo-sidebar__nav');
|
|
if (!sb || !toggles.length) return;
|
|
var mq = window.matchMedia('(min-width: 992px)');
|
|
function store(k, v) { try { localStorage.setItem(k, v); } catch (e) {} }
|
|
|
|
function sync() {
|
|
var desktop = mq.matches;
|
|
var open = desktop ? !root.classList.contains('bo-mini') : sb.classList.contains('open');
|
|
toggles.forEach(function (t) {
|
|
t.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
t.setAttribute('aria-label', open ? (desktop ? 'Reduzir menu' : 'Fechar menu') : (desktop ? 'Expandir menu' : 'Abrir menu'));
|
|
});
|
|
if (bd) bd.classList.toggle('show', !desktop && open);
|
|
document.body.style.overflow = (!desktop && open) ? 'hidden' : '';
|
|
}
|
|
function setDrawer(open) { sb.classList.toggle('open', open); sync(); }
|
|
|
|
toggles.forEach(function (t) {
|
|
t.addEventListener('click', function () {
|
|
if (mq.matches) {
|
|
store('bo-sidebar-mini', root.classList.toggle('bo-mini') ? '1' : '0');
|
|
sync();
|
|
} else {
|
|
setDrawer(!sb.classList.contains('open'));
|
|
}
|
|
});
|
|
});
|
|
if (bd) bd.addEventListener('click', function () { setDrawer(false); });
|
|
sb.querySelectorAll('.bo-nav-item').forEach(function (a) {
|
|
a.addEventListener('click', function () { if (!mq.matches) setDrawer(false); });
|
|
});
|
|
document.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Escape' && sb.classList.contains('open')) {
|
|
setDrawer(false);
|
|
var o = document.querySelector('.menu-toggle');
|
|
if (o && o.offsetParent) o.focus();
|
|
}
|
|
});
|
|
if (mq.addEventListener) mq.addEventListener('change', function () { setDrawer(false); });
|
|
sync();
|
|
|
|
// Grupos recolhiveis. O estado inicial veio do <head> (atributo no <html>);
|
|
// aqui so se liga o clique e se mantem atributo, armazenamento e aria alinhados.
|
|
var saved = {};
|
|
try { saved = JSON.parse(localStorage.getItem('bo-nav-groups') || '{}'); } catch (e) {}
|
|
function groupOpen(g) {
|
|
if (g.classList.contains('has-active')) return !g.classList.contains('is-closed');
|
|
return !saved[g.getAttribute('data-group')];
|
|
}
|
|
function paint(g) {
|
|
g.querySelector('[data-group-toggle]').setAttribute('aria-expanded', groupOpen(g) ? 'true' : 'false');
|
|
}
|
|
var groups = sb.querySelectorAll('.bo-nav-group[data-group]');
|
|
groups.forEach(function (g) {
|
|
g.querySelector('[data-group-toggle]').addEventListener('click', function () {
|
|
var key = g.getAttribute('data-group');
|
|
var willClose = groupOpen(g);
|
|
if (g.classList.contains('has-active')) g.classList.toggle('is-closed', willClose);
|
|
if (willClose) saved[key] = true; else delete saved[key];
|
|
store('bo-nav-groups', JSON.stringify(saved));
|
|
root.setAttribute('data-bo-groups', Object.keys(saved).join(' '));
|
|
groups.forEach(paint);
|
|
});
|
|
});
|
|
groups.forEach(paint);
|
|
|
|
// A lista rola sozinha: lembrar a posicao entre paginas e mostrar o item ativo.
|
|
if (nav) {
|
|
try { nav.scrollTop = +sessionStorage.getItem('bo-nav-scroll') || 0; } catch (e) {}
|
|
var cur = nav.querySelector('[aria-current="page"]');
|
|
if (cur) {
|
|
var r = cur.getBoundingClientRect(), n = nav.getBoundingClientRect();
|
|
if (r.top < n.top || r.bottom > n.bottom) cur.scrollIntoView({ block: 'center' });
|
|
}
|
|
addEventListener('pagehide', function () {
|
|
try { sessionStorage.setItem('bo-nav-scroll', nav.scrollTop); } catch (e) {}
|
|
});
|
|
}
|
|
})();
|
|
</script>
|
|
<style>
|
|
/* Console footer — subtle brand bar */
|
|
main:after {
|
|
content: ''; display: block; height: 4px;
|
|
background: linear-gradient(90deg, var(--bo-accent, #C2D500), transparent);
|
|
margin-top: 2rem;
|
|
}
|
|
</style>
|
|
<?php include __DIR__ . '/dev_toolbar.php'; ?>
|
|
</body>
|
|
</html>
|