543 lines
16 KiB
PHP
543 lines
16 KiB
PHP
<?php
|
||
/**
|
||
* Docs Footer Partial
|
||
* app/Views/docs/partials/docs_footer.php
|
||
*
|
||
* Closes the body/html tags and initialises shared client-side helpers.
|
||
* Always the last partial on every docs page.
|
||
*
|
||
* Variables:
|
||
* $app_name (string) — defaults to 'Nhance PAM'
|
||
*/
|
||
|
||
$app_name = $app_name ?? 'Nhance PAM';
|
||
$year = date('Y');
|
||
?>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
||
|
||
<div id="mermaid-modal" class="mermaid-modal" hidden aria-hidden="true">
|
||
<button type="button" class="mermaid-modal__backdrop" aria-label="Close enlarged diagram"></button>
|
||
<div class="mermaid-modal__panel" role="dialog" aria-modal="true" aria-labelledby="mermaid-modal-title">
|
||
<div class="mermaid-modal__header">
|
||
<span id="mermaid-modal-title" class="mermaid-modal__title">Diagram</span>
|
||
<div class="mermaid-modal__actions">
|
||
<div class="mermaid-modal__zoom" aria-label="Zoom controls">
|
||
<button type="button" class="mermaid-modal__zoom-btn" data-zoom="out" aria-label="Zoom out">−</button>
|
||
<span class="mermaid-modal__zoom-label" aria-live="polite">100%</span>
|
||
<button type="button" class="mermaid-modal__zoom-btn" data-zoom="in" aria-label="Zoom in">+</button>
|
||
<button type="button" class="mermaid-modal__zoom-btn mermaid-modal__zoom-btn--text" data-zoom="reset" aria-label="Reset zoom and pan">Reset</button>
|
||
</div>
|
||
<button type="button" class="mermaid-modal__close" aria-label="Close enlarged diagram">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||
<path d="M18 6 6 18M6 6l12 12"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<p class="mermaid-modal__help">
|
||
<strong>Tips:</strong>
|
||
Drag to pan · Scroll or double-click to zoom in ·
|
||
<kbd>Ctrl</kbd>+double-click to zoom out ·
|
||
Use +/−/Reset or <kbd>+</kbd> <kbd>−</kbd> <kbd>0</kbd> to reset · <kbd>Esc</kbd> to close
|
||
</p>
|
||
<div class="mermaid-modal__viewport" aria-label="Diagram viewer">
|
||
<div class="mermaid-modal__stage"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
// Syntax highlighting
|
||
if (window.hljs) hljs.highlightAll();
|
||
|
||
// Mermaid diagrams + enlarge controls
|
||
if (window.mermaid && document.querySelector('.mermaid')) {
|
||
mermaid.initialize({
|
||
startOnLoad: false,
|
||
theme: 'neutral',
|
||
securityLevel: 'loose'
|
||
});
|
||
mermaid.run({ querySelector: '.mermaid' }).then(initMermaidEnlarge).catch(initMermaidEnlarge);
|
||
}
|
||
|
||
function initMermaidEnlarge() {
|
||
const modal = document.getElementById('mermaid-modal');
|
||
const modalTitle = document.getElementById('mermaid-modal-title');
|
||
const modalViewport = modal && modal.querySelector('.mermaid-modal__viewport');
|
||
const modalStage = modal && modal.querySelector('.mermaid-modal__stage');
|
||
const zoomLabel = modal && modal.querySelector('.mermaid-modal__zoom-label');
|
||
if (!modal || !modalViewport || !modalStage) return;
|
||
|
||
const MIN_SCALE = 0.25;
|
||
const MAX_SCALE = 4;
|
||
const ZOOM_FACTOR = 1.15;
|
||
const expandIcon = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>';
|
||
|
||
let view = { scale: 1, x: 0, y: 0 };
|
||
let drag = null;
|
||
|
||
function applyView() {
|
||
modalStage.style.transform = 'translate(' + view.x + 'px, ' + view.y + 'px) scale(' + view.scale + ')';
|
||
if (zoomLabel) zoomLabel.textContent = Math.round(view.scale * 100) + '%';
|
||
}
|
||
|
||
function resetView() {
|
||
view = { scale: 1, x: 0, y: 0 };
|
||
applyView();
|
||
}
|
||
|
||
function centerDiagram() {
|
||
const svg = modalStage.querySelector('svg');
|
||
if (!svg) return;
|
||
const vpRect = modalViewport.getBoundingClientRect();
|
||
const svgRect = svg.getBoundingClientRect();
|
||
view.x = Math.max(24, (vpRect.width - svgRect.width) / 2);
|
||
view.y = Math.max(24, (vpRect.height - svgRect.height) / 2);
|
||
applyView();
|
||
}
|
||
|
||
function zoomAt(factor, originX, originY) {
|
||
const vpRect = modalViewport.getBoundingClientRect();
|
||
const ox = (originX !== undefined) ? originX : vpRect.width / 2;
|
||
const oy = (originY !== undefined) ? originY : vpRect.height / 2;
|
||
const newScale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, view.scale * factor));
|
||
if (newScale === view.scale) return;
|
||
view.x = ox - (ox - view.x) * (newScale / view.scale);
|
||
view.y = oy - (oy - view.y) * (newScale / view.scale);
|
||
view.scale = newScale;
|
||
applyView();
|
||
}
|
||
|
||
document.querySelectorAll('.mermaid-wrapper').forEach(function (wrapper) {
|
||
if (wrapper.querySelector('.mermaid-enlarge-btn')) return;
|
||
|
||
const btn = document.createElement('button');
|
||
btn.type = 'button';
|
||
btn.className = 'mermaid-enlarge-btn';
|
||
btn.setAttribute('aria-label', 'View enlarged diagram');
|
||
btn.innerHTML = expandIcon;
|
||
btn.addEventListener('click', function () {
|
||
openMermaidModal(wrapper);
|
||
});
|
||
wrapper.appendChild(btn);
|
||
});
|
||
|
||
function getDiagramTitle(wrapper) {
|
||
if (wrapper.id) {
|
||
return wrapper.id.replace(/-/g, ' ').replace(/\b\w/g, function (c) { return c.toUpperCase(); });
|
||
}
|
||
var el = wrapper.previousElementSibling;
|
||
while (el) {
|
||
if (el.matches && el.matches('h2, h3')) return el.textContent.trim();
|
||
el = el.previousElementSibling;
|
||
}
|
||
return 'Diagram';
|
||
}
|
||
|
||
function openMermaidModal(wrapper) {
|
||
const svg = wrapper.querySelector('.mermaid svg');
|
||
if (!svg) return;
|
||
|
||
modalTitle.textContent = getDiagramTitle(wrapper);
|
||
modalStage.innerHTML = '';
|
||
modalStage.appendChild(svg.cloneNode(true));
|
||
resetView();
|
||
|
||
modal.hidden = false;
|
||
modal.setAttribute('aria-hidden', 'false');
|
||
document.body.classList.add('mermaid-modal-open');
|
||
requestAnimationFrame(centerDiagram);
|
||
modal.querySelector('.mermaid-modal__close').focus();
|
||
}
|
||
|
||
function closeMermaidModal() {
|
||
modal.hidden = true;
|
||
modal.setAttribute('aria-hidden', 'true');
|
||
modalStage.innerHTML = '';
|
||
drag = null;
|
||
modalViewport.classList.remove('is-dragging');
|
||
resetView();
|
||
document.body.classList.remove('mermaid-modal-open');
|
||
}
|
||
|
||
if (!modal.dataset.mermaidViewerReady) {
|
||
modal.dataset.mermaidViewerReady = '1';
|
||
|
||
modal.querySelector('.mermaid-modal__backdrop').addEventListener('click', closeMermaidModal);
|
||
modal.querySelector('.mermaid-modal__close').addEventListener('click', closeMermaidModal);
|
||
|
||
modal.addEventListener('click', function (e) {
|
||
const zoomBtn = e.target.closest('[data-zoom]');
|
||
if (!zoomBtn || modal.hidden) return;
|
||
const action = zoomBtn.getAttribute('data-zoom');
|
||
if (action === 'in') zoomAt(ZOOM_FACTOR);
|
||
else if (action === 'out') zoomAt(1 / ZOOM_FACTOR);
|
||
else if (action === 'reset') resetView();
|
||
});
|
||
|
||
modalViewport.addEventListener('wheel', function (e) {
|
||
if (modal.hidden) return;
|
||
e.preventDefault();
|
||
const rect = modalViewport.getBoundingClientRect();
|
||
const factor = e.deltaY < 0 ? ZOOM_FACTOR : 1 / ZOOM_FACTOR;
|
||
zoomAt(factor, e.clientX - rect.left, e.clientY - rect.top);
|
||
}, { passive: false });
|
||
|
||
modalViewport.addEventListener('dblclick', function (e) {
|
||
if (modal.hidden) return;
|
||
e.preventDefault();
|
||
const rect = modalViewport.getBoundingClientRect();
|
||
const factor = e.ctrlKey ? (1 / ZOOM_FACTOR) : ZOOM_FACTOR;
|
||
zoomAt(factor, e.clientX - rect.left, e.clientY - rect.top);
|
||
});
|
||
|
||
modalViewport.addEventListener('pointerdown', function (e) {
|
||
if (modal.hidden || e.button !== 0) return;
|
||
drag = {
|
||
pointerId: e.pointerId,
|
||
startX: e.clientX,
|
||
startY: e.clientY,
|
||
origX: view.x,
|
||
origY: view.y
|
||
};
|
||
modalViewport.setPointerCapture(e.pointerId);
|
||
modalViewport.classList.add('is-dragging');
|
||
});
|
||
|
||
modalViewport.addEventListener('pointermove', function (e) {
|
||
if (!drag || drag.pointerId !== e.pointerId) return;
|
||
view.x = drag.origX + (e.clientX - drag.startX);
|
||
view.y = drag.origY + (e.clientY - drag.startY);
|
||
applyView();
|
||
});
|
||
|
||
function endDrag(e) {
|
||
if (!drag || drag.pointerId !== e.pointerId) return;
|
||
drag = null;
|
||
modalViewport.classList.remove('is-dragging');
|
||
try { modalViewport.releasePointerCapture(e.pointerId); } catch (err) { /* ignore */ }
|
||
}
|
||
|
||
modalViewport.addEventListener('pointerup', endDrag);
|
||
modalViewport.addEventListener('pointercancel', endDrag);
|
||
|
||
document.addEventListener('keydown', function (e) {
|
||
if (modal.hidden) return;
|
||
if (e.key === 'Escape') {
|
||
closeMermaidModal();
|
||
return;
|
||
}
|
||
if (e.target.closest('input, textarea, select')) return;
|
||
if (e.key === '+' || e.key === '=') {
|
||
e.preventDefault();
|
||
zoomAt(ZOOM_FACTOR);
|
||
} else if (e.key === '-') {
|
||
e.preventDefault();
|
||
zoomAt(1 / ZOOM_FACTOR);
|
||
} else if (e.key === '0') {
|
||
e.preventDefault();
|
||
resetView();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// Active TOC link on scroll
|
||
const tocLinks = document.querySelectorAll('.docs-toc a');
|
||
const headings = document.querySelectorAll('main h2, main h3');
|
||
|
||
if (tocLinks.length && headings.length) {
|
||
const observer = new IntersectionObserver(
|
||
(entries) => {
|
||
entries.forEach(entry => {
|
||
if (entry.isIntersecting) {
|
||
tocLinks.forEach(l => l.classList.remove('is-active-toc'));
|
||
const match = document.querySelector(
|
||
`.docs-toc a[href="#${entry.target.id}"]`
|
||
);
|
||
if (match) match.classList.add('is-active-toc');
|
||
}
|
||
});
|
||
},
|
||
{ rootMargin: '0px 0px -70% 0px' }
|
||
);
|
||
|
||
headings.forEach(h => { if (h.id) observer.observe(h); });
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style>
|
||
.docs-toc a.is-active-toc { color: var(--accent); font-weight: 500; }
|
||
|
||
.mermaid-wrapper {
|
||
position: relative;
|
||
margin: 20px 0 24px;
|
||
padding: 16px;
|
||
padding-top: 44px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 12px;
|
||
background: var(--bg2);
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.mermaid-enlarge-btn {
|
||
position: absolute;
|
||
top: 10px;
|
||
right: 10px;
|
||
z-index: 2;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 32px;
|
||
height: 32px;
|
||
padding: 0;
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
background: var(--bg);
|
||
color: var(--muted);
|
||
cursor: pointer;
|
||
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
||
}
|
||
|
||
.mermaid-enlarge-btn:hover {
|
||
color: var(--accent);
|
||
border-color: var(--accent);
|
||
background: var(--accent-bg);
|
||
}
|
||
|
||
.mermaid {
|
||
min-width: 520px;
|
||
text-align: center;
|
||
}
|
||
|
||
.mermaid svg {
|
||
max-width: 100%;
|
||
height: auto;
|
||
}
|
||
|
||
body.mermaid-modal-open {
|
||
overflow: hidden;
|
||
}
|
||
|
||
.mermaid-modal {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 1000;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 24px;
|
||
}
|
||
|
||
.mermaid-modal[hidden] {
|
||
display: none;
|
||
}
|
||
|
||
.mermaid-modal__backdrop {
|
||
position: absolute;
|
||
inset: 0;
|
||
border: 0;
|
||
background: rgba(15, 23, 42, 0.55);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.mermaid-modal__panel {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
width: min(96vw, 1400px);
|
||
height: min(92vh, 900px);
|
||
max-height: 92vh;
|
||
background: var(--bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: 12px;
|
||
box-shadow: 0 24px 48px rgba(15, 23, 42, 0.2);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.mermaid-modal__header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
padding: 14px 18px;
|
||
border-bottom: 1px solid var(--border);
|
||
background: var(--bg2);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.mermaid-modal__title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.mermaid-modal__actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.mermaid-modal__zoom {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 2px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
background: var(--bg);
|
||
}
|
||
|
||
.mermaid-modal__zoom-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 32px;
|
||
height: 32px;
|
||
padding: 0 8px;
|
||
border: 0;
|
||
border-radius: 6px;
|
||
background: transparent;
|
||
color: var(--text);
|
||
font-size: 18px;
|
||
line-height: 1;
|
||
cursor: pointer;
|
||
transition: background 0.15s, color 0.15s;
|
||
}
|
||
|
||
.mermaid-modal__zoom-btn--text {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
padding: 0 10px;
|
||
}
|
||
|
||
.mermaid-modal__zoom-btn:hover {
|
||
background: var(--accent-bg);
|
||
color: var(--accent);
|
||
}
|
||
|
||
.mermaid-modal__zoom-label {
|
||
min-width: 44px;
|
||
font-family: var(--mono);
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
text-align: center;
|
||
user-select: none;
|
||
}
|
||
|
||
.mermaid-modal__close {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 32px;
|
||
height: 32px;
|
||
padding: 0;
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
background: var(--bg);
|
||
color: var(--muted);
|
||
cursor: pointer;
|
||
transition: color 0.15s, border-color 0.15s;
|
||
}
|
||
|
||
.mermaid-modal__close:hover {
|
||
color: var(--accent);
|
||
border-color: var(--accent);
|
||
}
|
||
|
||
.mermaid-modal__help {
|
||
flex-shrink: 0;
|
||
margin: 0;
|
||
padding: 8px 18px;
|
||
font-size: 12px;
|
||
line-height: 1.55;
|
||
color: var(--muted);
|
||
background: var(--bg2);
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.mermaid-modal__help strong {
|
||
color: var(--text);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.mermaid-modal__help kbd {
|
||
font-family: var(--mono);
|
||
font-size: 11px;
|
||
background: var(--bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: 4px;
|
||
padding: 1px 5px;
|
||
color: var(--text);
|
||
}
|
||
|
||
.mermaid-modal__viewport {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
background: var(--bg);
|
||
cursor: grab;
|
||
touch-action: none;
|
||
user-select: none;
|
||
}
|
||
|
||
.mermaid-modal__viewport.is-dragging {
|
||
cursor: grabbing;
|
||
}
|
||
|
||
.mermaid-modal__stage {
|
||
display: inline-block;
|
||
transform-origin: 0 0;
|
||
will-change: transform;
|
||
padding: 24px;
|
||
}
|
||
|
||
.mermaid-modal__stage svg {
|
||
display: block;
|
||
max-width: none !important;
|
||
width: auto;
|
||
height: auto;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* ─── GLOBAL FOOTER BAR ──────────────────── */
|
||
.docs-global-footer {
|
||
border-top : 1px solid var(--border);
|
||
padding : 16px 24px;
|
||
font-size : 12px;
|
||
color : var(--muted);
|
||
display : flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-top : auto;
|
||
}
|
||
|
||
.docs-global-footer a {
|
||
color : var(--muted);
|
||
text-decoration: none;
|
||
}
|
||
|
||
.docs-global-footer a:hover { color: var(--accent); }
|
||
</style>
|
||
|
||
<footer class="docs-global-footer">
|
||
<span>© <?= $year ?> <?= esc($app_name) ?>. Internal developer docs.</span>
|
||
<span>
|
||
Built with CodeIgniter 4 ·
|
||
<a href="<?= base_url('docs/changelog') ?>">Changelog</a> ·
|
||
<a href="<?= base_url('docs/contributing') ?>">Contributing</a>
|
||
</span>
|
||
</footer>
|
||
|
||
</body>
|
||
</html>
|