Settings: larger dialog, sections as cards in columns, dense filter list

- The dialog uses most of the screen (up to 1280 px wide, full height);
  sections become cards laid out in two columns when there is room.
- Settings > Filters: compact tag rows in one wide column, a toolbar that
  stays in view (add, presets, filter, reset), a filter on keyword, label
  or code, and a tag count.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
cedricandClaude Opus 5.5 committed 2026-10-03 16:33:48 +02:00
1 parent 3466a29692
commit 4f4cb3e02b
3 files changed
+103 -41

No files matched your search

+31
View File
@@ -49,6 +49,9 @@ const I18N = {
confirmDelete: (p) => `Delete tag "${p}"?`,
confirmReset: 'Replace all tags with the defaults (warning, error)?',
presetAria: 'Add a preset', presetPick: '+ Preset…',
tagFilter: 'Filter tags (keyword, label or code)',
tagCount: (n) => (n.shown === n.total ? `${n.total} tag${n.total === 1 ? '' : 's'}` : `${n.shown} / ${n.total} tags`),
noTagMatch: 'No tag matches the filter.',
presetAdded: (n) => (n ? `${n} tag(s) added` : 'These tags are already in the list'),
presetsFileErr: (f) => `Invalid presets file ${f}, built-in presets used: `,
tagsLoadErr: 'Tags: ',
@@ -200,6 +203,9 @@ const I18N = {
confirmDelete: (p) => `Supprimer le tag « ${p} » ?`,
confirmReset: 'Remplacer tous les tags par les tags par défaut (warning, error) ?',
presetAria: 'Ajouter un préréglage', presetPick: '+ Préréglage…',
tagFilter: 'Filtrer les tags (mot-clé, libellé ou code)',
tagCount: (n) => (n.shown === n.total ? `${n.total} tag${n.total > 1 ? 's' : ''}` : `${n.shown} / ${n.total} tags`),
noTagMatch: 'Aucun tag ne correspond au filtre.',
presetAdded: (n) => (n ? `${n} tag(s) ajouté(s)` : 'Ces tags sont déjà dans la liste'),
presetsFileErr: (f) => `Fichier de préréglages ${f} invalide, préréglages intégrés utilisés : `,
tagsLoadErr: 'Tags : ',
@@ -2098,8 +2104,32 @@ function tagRowHTML(tag) {
function renderTagList() {
$('#tagList').innerHTML = state.tags.map(tagRowHTML).join('') || `<p class="muted small">${esc(t('noTags'))}</p>`;
filterTagList();
}
// Settings > Filters: shows only the tags whose keyword, label or code contains the filter.
function filterTagList() {
const q = $('#tagFilter').value.trim().toLowerCase();
let shown = 0;
for (const row of $('#tagList').querySelectorAll('.tag-row')) {
const tag = state.tags.find((x) => x.id === row.dataset.id);
const hit = !q || !tag || [tag.pattern, tag.label, tag.code].some((v) => String(v || '').toLowerCase().includes(q));
row.hidden = !hit;
if (hit) shown++;
}
let none = $('#tagList').querySelector('.tag-none');
if (q && !shown && state.tags.length) {
if (!none) {
none = document.createElement('p');
none.className = 'muted small tag-none';
$('#tagList').append(none);
}
none.textContent = t('noTagMatch');
} else if (none) none.remove();
$('#tagCount').textContent = state.tags.length ? t('tagCount', { shown, total: state.tags.length }) : '';
}
$('#tagFilter').addEventListener('input', filterTagList);
const saveTimers = {};
function scheduleSave(tag, rowEl) {
clearTimeout(saveTimers[tag.id]);
@@ -2150,6 +2180,7 @@ $('#addTag').addEventListener('click', async () => {
try {
const tag = await api('/api/tags', { method: 'POST', body: { pattern: t('newTag'), color, wholeWord: true, enabled: true } });
state.tags.push(tag);
$('#tagFilter').value = '';
renderTagList();
applyTags();
const input = $('#tagList').lastElementChild.querySelector('input[type="text"]');