{# ANSI art menu variant for BBS shell #}
{% if shell_art_content %}
<div class="mb-3">
{#
ANSI art is 80 columns wide at a monospace font size ? too wide for mobile.
We wrap it in an overflow:hidden container and use JS to apply a CSS
transform: scale() so it shrinks proportionally to fit the viewport.
On desktop the art renders at natural size.
#}
<div class="ansi-art-wrapper d-flex justify-content-center" id="ansiArtWrapper" style="cursor:pointer;">
<pre class="ansi-art" id="shellMenuAnsi" data-ansi-size="{{ appearance.shell.bbs_menu.ansi_size|default('80x25')|e('html_attr') }}" style="display:inline-block;">{{ shell_art_content }}</pre>
</div>
{#
Shortcut links ? hidden by default, revealed when the user clicks/taps
the ANSI art above. Using display:none (not a Bootstrap breakpoint class)
so it never auto-shows based on viewport width alone.
#}
{% set menuItems = bbs_menu_items %}
<div id="ansiTouchLinks" class="mt-3" style="display:none;">
<div class="list-group">
{% for item in menuItems %}
<a href="{{ item.url | e }}" class="list-group-item list-group-item-action d-flex align-items-center gap-2 py-3{% if item.url == '/files' %} file-menu-link{% endif %}">
<span class="badge bg-primary fs-6" style="min-width:2rem;">{{ item.key | upper | e }}</span>
<i class="fas fa-{{ item.icon | default('circle') | e }} text-primary{% if item.url == '/files' %} file-menu-icon{% endif %}"></i>
{{ item.label | e }}
</a>
{% endfor %}
</div>
</div>
</div>
<script>
(function() {
function resolveAnsiCanvas(size) {
switch (size) {
case '132x24':
return { cols: 132, rows: 24, fitViewport: false };
case '132x43':
return { cols: 132, rows: 43, fitViewport: false };
case '132x50':
return { cols: 132, rows: 50, fitViewport: false };
case 'full':
return { cols: 132, rows: 50, fitViewport: true };
case '80x25':
default:
return { cols: 80, rows: 25, fitViewport: false };
}
}
function scaleAnsiArt() {
var pre = document.getElementById('shellMenuAnsi');
if (!pre) return;
var wrapper = pre.closest('.ansi-art-wrapper');
if (!wrapper) return;
var ansiSize = pre.dataset.ansiSize || '80x25';
var canvas = resolveAnsiCanvas(ansiSize);
// Reset any previous scaling
pre.style.fontSize = '';
pre.style.transform = 'scale(1)';
pre.style.transformOrigin = 'top center';
pre.style.position = '';
pre.style.left = '';
pre.style.top = '';
wrapper.style.height = '';
wrapper.style.position = '';
wrapper.style.overflow = '';
var isMobile = window.matchMedia('(max-width: 767.98px)').matches;
var targetWidth = isMobile
? Math.floor(window.innerWidth * 0.99)
: wrapper.clientWidth;
var viewportPadding = isMobile ? 24 : 48;
var wrapperTop = wrapper.getBoundingClientRect().top;
var targetHeight = Math.max(0, window.innerHeight - wrapperTop - viewportPadding);
var naturalWidth = pre.scrollWidth;
var naturalHeight = pre.scrollHeight;
if (targetWidth <= 0 || naturalWidth <= 0 || naturalHeight <= 0) {
return;
}
var widthScale = targetWidth / naturalWidth;
var heightScale = targetHeight > 0 ? (targetHeight / naturalHeight) : widthScale;
var scale;
if (canvas.fitViewport) {
scale = Math.min(widthScale, heightScale);
} else if (ansiSize === '80x25') {
scale = 1;
if (isMobile && targetWidth > 0 && naturalWidth > targetWidth) {
scale = targetWidth / naturalWidth;
}
} else {
var baselineScale = Math.min(80 / canvas.cols, 25 / canvas.rows);
scale = baselineScale;
if (isMobile && targetWidth > 0) {
scale = Math.min(scale, widthScale);
}
}
if (scale > 0 && Math.abs(scale - 1) > 0.0001) {
wrapper.style.position = 'relative';
wrapper.style.overflow = 'hidden';
pre.style.position = 'absolute';
pre.style.left = '50%';
pre.style.top = '0';
if (canvas.fitViewport) {
pre.style.transform = 'translateX(-50%) scale(' + scale.toFixed(4) + ')';
wrapper.style.height = Math.max(1, Math.floor(naturalHeight * scale)) + 'px';
} else {
pre.style.transform = 'translateX(-50%) scale(' + scale.toFixed(4) + ')';
wrapper.style.height = Math.max(1, Math.floor(naturalHeight * scale)) + 'px';
}
} else if (!canvas.fitViewport) {
wrapper.style.height = Math.max(1, naturalHeight) + 'px';
}
}
// Run after ANSI rendering (which happens in a separate ready handler)
$(document).ready(function() {
// Small delay so the ANSI renderer has run first
setTimeout(scaleAnsiArt, 50);
// Clicking/tapping the ANSI art toggles the shortcut link list
var wrapper = document.getElementById('ansiArtWrapper');
var links = document.getElementById('ansiTouchLinks');
if (wrapper) {
wrapper.addEventListener('click', function() {
if (links) {
links.style.display = links.style.display === 'none' ? 'block' : 'none';
}
});
}
});
$(window).on('resize', scaleAnsiArt);
})();
</script>
{% else %}
{# No ANSI file configured ? fall back to text menu #}
{% include 'shells/bbs-menu/main-menu-text.twig' %}
{% endif %}
|