From cb32d0b9adc75555fd9ae822676003c9c8d408eb Mon Sep 17 00:00:00 2001 From: SITO Date: Sat, 2 May 2026 00:23:13 +0200 Subject: [PATCH] feat: mobile mode-buttons scroll arrows and expand/collapse Adds left/right arrow navigation to all .mode-buttons on mobile. Sections with 7+ buttons (Activity, Stats, etc.) also get a Ver todos / Ver menos toggle to expand the full grid. Co-Authored-By: Claude Sonnet 4.6 --- .../src/client/assets/styles/mobile.css | 65 +++++++++++++++ .../src/client/assets/themes/OasisMobile.css | 14 ++++ .../src/client/public/js/mobile-ui.js | 79 +++++++++++++++++++ .../nodejs-project/src/views/main_views.js | 3 +- 4 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 nodejs-project/nodejs-project/src/client/public/js/mobile-ui.js diff --git a/nodejs-project/nodejs-project/src/client/assets/styles/mobile.css b/nodejs-project/nodejs-project/src/client/assets/styles/mobile.css index 105960f8..3f83fec7 100644 --- a/nodejs-project/nodejs-project/src/client/assets/styles/mobile.css +++ b/nodejs-project/nodejs-project/src/client/assets/styles/mobile.css @@ -735,3 +735,68 @@ pre, code { max-width: 100% !important; overflow-x: hidden !important; } + +/* ============================================================ + MODE-BUTTONS MOBILE ENHANCEMENT (mobile-ui.js) + Scroll horizontal con flechas + expand/collapse + ============================================================ */ + +/* Wrapper: flechas + contenedor alineados en fila */ +.mode-buttons-wrap { + display: flex !important; + align-items: center !important; + gap: 4px !important; + width: 100% !important; + margin-top: 12px !important; +} + +/* Contenedor de botones: scroll horizontal, sin wrap */ +.mode-buttons-wrap .mode-buttons { + flex: 1 !important; + overflow-x: auto !important; + flex-wrap: nowrap !important; + scrollbar-width: none !important; + scroll-behavior: smooth !important; + margin-top: 0 !important; + padding-bottom: 4px !important; +} +.mode-buttons-wrap .mode-buttons::-webkit-scrollbar { + display: none !important; +} + +/* Cuando está expandido: vuelve a wrap para mostrar todo en grid */ +.mode-buttons-wrap.mode-buttons-expanded .mode-buttons { + overflow-x: visible !important; + flex-wrap: wrap !important; +} + +/* Flechas de scroll */ +.mode-btn-arrow { + flex-shrink: 0 !important; + width: 30px !important; + height: 34px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + border-radius: 8px !important; + font-size: 1.6rem !important; + line-height: 1 !important; + cursor: pointer !important; + padding: 0 !important; + transition: opacity 0.15s !important; + user-select: none !important; +} + +/* Botón "Ver todos / Ver menos" */ +.mode-expand-btn { + display: block !important; + width: 100% !important; + margin-top: 6px !important; + padding: 7px 12px !important; + border-radius: 8px !important; + font-size: 0.82rem !important; + font-weight: 600 !important; + text-align: center !important; + cursor: pointer !important; + letter-spacing: 0.02em !important; +} diff --git a/nodejs-project/nodejs-project/src/client/assets/themes/OasisMobile.css b/nodejs-project/nodejs-project/src/client/assets/themes/OasisMobile.css index a712ae81..aab257ee 100644 --- a/nodejs-project/nodejs-project/src/client/assets/themes/OasisMobile.css +++ b/nodejs-project/nodejs-project/src/client/assets/themes/OasisMobile.css @@ -395,3 +395,17 @@ a.user-link:focus { .mobile-menu-close { color: #FFD700; } + +/* Mode-buttons scroll arrows */ +.mode-btn-arrow { + background-color: #2a2a00; + color: #FFD700; + border: 1px solid #444; +} + +/* Expand/collapse button */ +.mode-expand-btn { + background-color: #1e1e00; + color: #FFD700; + border: 1px solid #555; +} diff --git a/nodejs-project/nodejs-project/src/client/public/js/mobile-ui.js b/nodejs-project/nodejs-project/src/client/public/js/mobile-ui.js new file mode 100644 index 00000000..df98d6c4 --- /dev/null +++ b/nodejs-project/nodejs-project/src/client/public/js/mobile-ui.js @@ -0,0 +1,79 @@ +(function () { + function setup() { + if (window.innerWidth > 768) return; + + document.querySelectorAll('.mode-buttons:not([data-ms])').forEach(function (cnt) { + cnt.dataset.ms = '1'; + + var items = cnt.querySelectorAll('form, button.filter-btn, a.filter-btn'); + var n = items.length; + if (n < 3) return; + + // Wrap container with arrows + var wrap = document.createElement('div'); + wrap.className = 'mode-buttons-wrap'; + cnt.parentNode.insertBefore(wrap, cnt); + wrap.appendChild(cnt); + + var la = document.createElement('button'); + la.type = 'button'; + la.className = 'mode-btn-arrow mode-btn-arrow-left'; + la.innerHTML = '‹'; + la.setAttribute('aria-label', 'Anterior'); + + var ra = document.createElement('button'); + ra.type = 'button'; + ra.className = 'mode-btn-arrow mode-btn-arrow-right'; + ra.innerHTML = '›'; + ra.setAttribute('aria-label', 'Siguiente'); + + wrap.insertBefore(la, cnt); + wrap.appendChild(ra); + + la.addEventListener('click', function (e) { + e.preventDefault(); + cnt.scrollBy({ left: -180, behavior: 'smooth' }); + }); + ra.addEventListener('click', function (e) { + e.preventDefault(); + cnt.scrollBy({ left: 180, behavior: 'smooth' }); + }); + + function syncArrows() { + var atStart = cnt.scrollLeft < 5; + var atEnd = cnt.scrollLeft + cnt.clientWidth >= cnt.scrollWidth - 5; + la.style.opacity = atStart ? '0.2' : '1'; + ra.style.opacity = atEnd ? '0.2' : '1'; + la.style.pointerEvents = atStart ? 'none' : 'auto'; + ra.style.pointerEvents = atEnd ? 'none' : 'auto'; + } + cnt.addEventListener('scroll', syncArrows, { passive: true }); + setTimeout(syncArrows, 120); + + // Expand/collapse toggle for large button groups + if (n > 6) { + var expanded = false; + var xb = document.createElement('button'); + xb.type = 'button'; + xb.className = 'mode-expand-btn'; + xb.textContent = 'Ver todos ▾'; + wrap.parentNode.insertBefore(xb, wrap.nextSibling); + + xb.addEventListener('click', function (e) { + e.preventDefault(); + expanded = !expanded; + cnt.classList.toggle('mode-buttons-expanded', expanded); + wrap.classList.toggle('mode-buttons-expanded', expanded); + xb.textContent = expanded ? 'Ver menos ▴' : 'Ver todos ▾'; + setTimeout(syncArrows, 50); + }); + } + }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', setup); + } else { + setup(); + } +})(); diff --git a/nodejs-project/nodejs-project/src/views/main_views.js b/nodejs-project/nodejs-project/src/views/main_views.js index dc8d8745..685bdf4f 100644 --- a/nodejs-project/nodejs-project/src/views/main_views.js +++ b/nodejs-project/nodejs-project/src/views/main_views.js @@ -23,7 +23,7 @@ const getUserId = async () => { return userId; }; -const { a, article, br, body, button, details, div, em, footer, form, h1, h2, h3, head, header, hr, html, img, input, label, li, link, main, meta, nav, option, p, pre, section, select, span, summary, table, td, textarea, title, tr, ul, strong, video: videoHyperaxe, audio: audioHyperaxe } = require("../server/node_modules/hyperaxe"); +const { a, article, br, body, button, details, div, em, footer, form, h1, h2, h3, head, header, hr, html, img, input, label, li, link, main, meta, nav, option, p, pre, script, section, select, span, summary, table, td, textarea, title, tr, ul, strong, video: videoHyperaxe, audio: audioHyperaxe } = require("../server/node_modules/hyperaxe"); const lodash = require("../server/node_modules/lodash"); const markdown = require("./markdown"); @@ -682,6 +682,7 @@ const template = (titlePrefix, ...elements) => { link({ rel: "stylesheet", href: "/assets/styles/style.css" }), themeLink, link({ rel: "stylesheet", href: "/assets/styles/mobile.css", media: "(max-width: 768px)" }), + script({ src: "/js/mobile-ui.js", defer: true }), link({ rel: "icon", href: "/assets/images/favicon.svg" }), meta({ charset: "utf-8" }), meta({ name: "description", content: i18n.oasisDescription }),