fix: page Configurations inutilisable en mode mobile BUG-071
CI / lint (push) Successful in 1m57s
CI / security (push) Successful in 1m20s
CI / test (push) Successful in 4m20s
CI / build (push) Successful in 1m20s
CI / e2e (push) Successful in 11m51s

This commit is contained in:
2026-09-22 21:24:56 -04:00
parent aeb7516445
commit dd9224e685
18 changed files with 318 additions and 19 deletions
+12
View File
@@ -1486,6 +1486,18 @@
<div class="editor-modal" id="config-modal">
<div class="editor-container">
<div class="editor-header">
<button
class="help-hamburger"
id="config-hamburger"
data-i18n-attr="title:config.toc_toggle;aria-label:config.toc_toggle"
title="Afficher le sommaire"
aria-label="Afficher le sommaire"
>
<i
data-lucide="menu"
style="width: 18px; height: 18px"
></i>
</button>
<div class="editor-title" data-i18n="header.menu_config">Configurations</div>
<div class="editor-actions">
<button
+42
View File
@@ -767,6 +767,10 @@ function initConfigModal() {
openBtn.addEventListener("click", async () => {
modal.classList.add("active");
closeHeaderMenu();
// BUG-071: reset the TOC to the CSS default (mobile: hidden, desktop:
// visible) like the help modal does on open.
var configNavOnOpen = document.getElementById("config-nav");
if (configNavOnOpen) configNavOnOpen.style.display = '';
renderConfigFilters();
loadConfigFields();
loadDiagnostics();
@@ -886,6 +890,44 @@ function initConfigModal() {
});
}
// BUG-071: mobile table of contents. #config-nav shares the .help-nav
// rule that hides it below 768px, but — unlike the help modal — the config
// modal had no toggle to reveal it, leaving mobile users with no way to
// reach a section. The header hamburger opens it as a top block; picking
// a section smooth-scrolls inside the modal and collapses it on mobile.
var configNav = document.getElementById("config-nav");
var configHamburger = document.getElementById("config-hamburger");
function _isConfigMobile() { return window.innerWidth <= 768; }
function _setConfigNav(open) {
if (!configNav) return;
configNav.style.display = open ? "flex" : "none";
if (configHamburger) configHamburger.classList.toggle("active", !!open);
}
if (configHamburger) {
configHamburger.addEventListener("click", function(e) {
e.stopPropagation();
var hidden = !configNav || configNav.style.display === "none" || configNav.style.display === "";
_setConfigNav(hidden);
});
}
if (configNav) {
configNav.querySelectorAll(".help-nav-link").forEach(function(a) {
a.addEventListener("click", function(e) {
var hash = a.getAttribute("href");
if (!hash || hash.charAt(0) !== "#") return;
var target = document.getElementById(hash.slice(1));
if (!target) return;
e.preventDefault();
configNav.querySelectorAll(".help-nav-link").forEach(function(o) { o.classList.remove("active"); });
a.classList.add("active");
if (typeof target.scrollIntoView === "function") {
target.scrollIntoView({ behavior: "smooth", block: "start" });
}
if (_isConfigMobile()) _setConfigNav(false);
});
});
}
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && modal.classList.contains("active")) {
closeConfigModal();
+13 -6
View File
@@ -5,6 +5,7 @@
* Static DOM: data-i18n="key" → textContent
* data-i18n-placeholder="key" → placeholder
* data-i18n-attr:title="key" → title attribute
* data-i18n-attr="a:k1;b:k2" → several attributes (";"-separated)
* data-i18n-html="key" → innerHTML (use sparingly)
* Dynamic JS: import { t } from './i18n.js'; t('key', {param: 'val'})
* Live reload: setLocale('en') updates every data-i18n element instantly.
@@ -183,13 +184,19 @@ function _applyDOM() {
el.innerHTML = t(key);
});
// data-i18n-attr:TITLE → sets any attribute
// data-i18n-attr:ATTR:key[;ATTR:key…] → sets any attribute(s).
// Single-pair form (data-i18n-attr="title:key") is preserved; multiple
// pairs are separated with ";" (BUG-071: the config TOC toggle needs both
// title and aria-label translated).
document.querySelectorAll('[data-i18n-attr]').forEach(function (el) {
const raw = el.getAttribute('data-i18n-attr');
const colon = raw.indexOf(':');
if (colon === -1) return;
const attr = raw.substring(0, colon);
const key = raw.substring(colon + 1);
el.setAttribute(attr, t(key));
raw.split(';').forEach(function (pair) {
const colon = pair.indexOf(':');
if (colon === -1) return;
const attr = pair.substring(0, colon).trim();
const key = pair.substring(colon + 1).trim();
if (!attr || !key) return;
el.setAttribute(attr, t(key));
});
});
}
+1
View File
@@ -569,6 +569,7 @@
"config.test": "Test",
"config.timeout_label": "Search timeout (ms)",
"config.title": "Settings",
"config.toc_toggle": "Show contents",
"config.title_boost": "Title boost",
"config.title_boost_hint": "Relevance multiplier for title matches",
"config.title_boost_label": "Title boost",
+1
View File
@@ -569,6 +569,7 @@
"config.test": "Tester",
"config.timeout_label": "Timeout recherche (ms)",
"config.title": "Configuration",
"config.toc_toggle": "Afficher le sommaire",
"config.title_boost": "Boost titre",
"config.title_boost_hint": "Multiplicateur de pertinence pour les correspondances dans le titre",
"config.title_boost_label": "Boost titre",
+76
View File
@@ -4723,6 +4723,82 @@ body.resizing-v {
}
}
/* BUG-071: Configurations modal — mobile usability (viewport ≤ 768px).
#config-nav shares the .help-nav rule that hides it, but the config modal
had no toggle (unlike the help modal): the header hamburger
(#config-hamburger, same .help-hamburger treatment) reveals it as a
collapsible top block. Two-column grids and fixed-width add-rows are
stacked/wrapped so nothing overflows a 360px viewport. */
@media (max-width: 768px) {
/* TOC as a collapsible top block (JS toggles inline display flex/none,
which wins over the hiding rule); the list scrolls within a capped nav. */
#config-modal #config-nav {
width: 100%;
min-width: 0;
max-width: 100%;
border-right: none;
border-bottom: 1px solid var(--border);
max-height: 46vh;
}
/* Two-column grids → single column. */
#config-modal .ai-default-grid,
#config-modal .ai-provider-fields {
grid-template-columns: 1fr;
}
/* Add-rows (tokens, webhooks, tag filters) wrap instead of overflowing. */
#config-modal .config-add-row,
#config-modal .config-add-pattern {
flex-wrap: wrap;
}
#config-modal .config-add-row .config-input,
#config-modal .config-add-pattern .config-input,
#config-modal .config-add-row .config-select {
flex: 1 1 140px;
width: auto !important; /* override fixed inline widths (180/140/100px) */
min-width: 0;
}
#config-modal .config-add-row .config-btn-add,
#config-modal .config-add-pattern .config-btn-add {
flex: 1 1 auto;
min-height: 44px;
}
/* Webhook / token / share rows wrap; long URLs and meta take their own
line instead of squeezing the delete control off-screen. */
#config-modal .webhook-item,
#config-modal .token-item,
#config-modal .share-item {
flex-wrap: wrap;
}
#config-modal .webhook-url,
#config-modal .token-meta,
#config-modal .share-url {
flex: 1 1 100%;
min-width: 0;
white-space: normal;
overflow-wrap: anywhere;
}
#config-modal .webhook-delete,
#config-modal .token-delete,
#config-modal .share-revoke {
min-width: 44px;
min-height: 44px;
}
/* Sticky AI-keys footer: full-width touch-friendly buttons. */
#config-modal .ai-keys-footer .config-btn-save,
#config-modal .ai-keys-footer .config-btn-secondary {
flex: 1 1 100%;
min-height: 44px;
}
/* Long inline code in tips (ex. MCP usage snippet) must wrap. */
#config-modal .qh-tip {
flex-wrap: wrap;
}
#config-modal .qh-tip span {
min-width: 0;
overflow-wrap: anywhere;
}
}
/* --- Toast notifications --- */
.toast-container {
position: fixed;