editor inline: combo mostrano il valore reale stimato dall'aspetto

Se un blocco non ha uno stile esplicito, le combo pre-selezionano il preset più
vicino all'aspetto effettivo (getComputedStyle): size dal rapporto col genitore,
color per match RGB, weight/style/align diretti. Marcato "(attuale)" con bordo
tratteggiato. Al salvataggio le stime non modificate non vengono fissate, così il
tema resta intatto; solo le scelte manuali vengono scritte. Gli styles ora sono
inviati sempre per permettere l'azzeramento a "default (tema)".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-13 06:39:46 +02:00
parent 47a93049bb
commit 8d593dce90
+67 -8
View File
@@ -103,6 +103,8 @@ const showTags = canTags && wantTags;
font: inherit; font-size: .9rem; color: #2b2320; cursor: pointer; } font: inherit; font-size: .9rem; color: #2b2320; cursor: pointer; }
.tm-field select:focus { outline: 2px solid #9c8b70; outline-offset: 1px; border-color: #9c8b70; } .tm-field select:focus { outline: 2px solid #9c8b70; outline-offset: 1px; border-color: #9c8b70; }
.tm-field select.is-set { border-color: #9c8b70; background-color: #f6f1e8; font-weight: 600; } .tm-field select.is-set { border-color: #9c8b70; background-color: #f6f1e8; font-weight: 600; }
.tm-field select.is-estimated { border-style: dashed; border-color: #c4b79f; color: #6b5b48; }
.tm-note { margin: 12px 0 0; font: 400 12px/1.5 system-ui, sans-serif; color: #8a7c68; }
.tag-modal__box input[type=file] { display: block; width: 100%; font: inherit; font-size: .9rem; padding: 10px; .tag-modal__box input[type=file] { display: block; width: 100%; font: inherit; font-size: .9rem; padding: 10px;
border: 1px dashed #cbbfad; border-radius: 10px; background: #fff; box-sizing: border-box; } border: 1px dashed #cbbfad; border-radius: 10px; background: #fff; box-sizing: border-box; }
.tm-del { margin-top: 12px; padding: 8px 14px; border: 0; border-radius: 8px; background: #b4443a; color: #fff; cursor: pointer; font: 600 .85rem system-ui, sans-serif; } .tm-del { margin-top: 12px; padding: 8px 14px; border: 0; border-radius: 8px; background: #b4443a; color: #fff; cursor: pointer; font: 600 .85rem system-ui, sans-serif; }
@@ -141,6 +143,42 @@ const showTags = canTags && wantTags;
color: { accent: 'Accento', 'accent-dark': 'Accento scuro', dark: 'Scuro', heading: 'Titolo', text: 'Testo', 'text-light': 'Testo chiaro', white: 'Bianco' }, color: { accent: 'Accento', 'accent-dark': 'Accento scuro', dark: 'Scuro', heading: 'Titolo', text: 'Testo', 'text-light': 'Testo chiaro', white: 'Bianco' },
}; };
const valLabel = (g, v) => (VALUE_LABELS[g] && VALUE_LABELS[g][v]) || v; const valLabel = (g, v) => (VALUE_LABELS[g] && VALUE_LABELS[g][v]) || v;
// Stima il preset più vicino all'aspetto REALE dell'elemento (getComputedStyle).
const px = (s) => parseFloat(s) || 0;
const rgb = (s) => { const m = String(s).match(/-?\d+(\.\d+)?/g); return m ? m.slice(0, 3).map(Number) : null; };
const SIZE_SCALE = { s: 0.85, m: 1, l: 1.25, xl: 1.5, xxl: 2 };
let colorProbe = null;
function nearestSize(el) {
const cs = getComputedStyle(el);
const ref = el.parentElement ? px(getComputedStyle(el.parentElement).fontSize) : px(cs.fontSize);
const ratio = ref ? px(cs.fontSize) / ref : 1;
let best = 'm', bd = Infinity;
for (const k in SIZE_SCALE) { const d = Math.abs(SIZE_SCALE[k] - ratio); if (d < bd) { bd = d; best = k; } }
return best;
}
function nearestColor(el) {
const a = rgb(getComputedStyle(el).color); if (!a) return null;
if (!colorProbe) { colorProbe = document.createElement('span'); colorProbe.style.cssText = 'position:absolute;left:-9999px;visibility:hidden'; document.body.appendChild(colorProbe); }
let best = null, bd = Infinity;
STYLE_GROUPS.color.forEach((c) => {
colorProbe.className = 'cs-color-' + c;
const p = rgb(getComputedStyle(colorProbe).color); if (!p) return;
const d = (p[0]-a[0])**2 + (p[1]-a[1])**2 + (p[2]-a[2])**2;
if (d < bd) { bd = d; best = c; }
});
colorProbe.className = '';
return best;
}
function effective(group, el) {
const cs = getComputedStyle(el);
if (group === 'size') return nearestSize(el);
if (group === 'color') return nearestColor(el);
if (group === 'weight') return parseInt(cs.fontWeight, 10) >= 600 ? 'bold' : 'normal';
if (group === 'style') return cs.fontStyle.indexOf('italic') >= 0 ? 'italic' : 'normal';
if (group === 'align') { let x = cs.textAlign; if (x === 'start') x = 'left'; else if (x === 'end') x = 'right'; return ['left','center','right','justify'].indexOf(x) >= 0 ? x : 'left'; }
return null;
}
const modal = document.getElementById('tag-modal'); const modal = document.getElementById('tag-modal');
const tmTag = document.getElementById('tm-tag'); const tmTag = document.getElementById('tm-tag');
const tmGuid = document.getElementById('tm-guid'); const tmGuid = document.getElementById('tm-guid');
@@ -185,7 +223,12 @@ const showTags = canTags && wantTags;
if (groups.length) { if (groups.length) {
const grid = document.createElement('div'); const grid = document.createElement('div');
grid.className = 'tm-styles'; grid.className = 'tm-styles';
let anyEstimate = false;
groups.forEach((group) => { groups.forEach((group) => {
const explicit = styles[group]; // valore salvato nel blocco
const est = explicit ? null : effective(group, el); // altrimenti stima dall'aspetto reale
const shown = explicit || est;
if (est) anyEstimate = true;
const field = document.createElement('label'); const field = document.createElement('label');
field.className = 'tm-field'; field.className = 'tm-field';
const cap = document.createElement('span'); const cap = document.createElement('span');
@@ -194,22 +237,32 @@ const showTags = canTags && wantTags;
const sel = document.createElement('select'); const sel = document.createElement('select');
sel.dataset.group = group; sel.dataset.group = group;
sel.className = 'tm-style'; sel.className = 'tm-style';
if (est) sel.dataset.estimated = est; // marcata come stima: non fissata al salvataggio se invariata
const none = document.createElement('option'); const none = document.createElement('option');
none.value = ''; none.textContent = '— default —'; none.value = ''; none.textContent = '— default (tema) —';
sel.appendChild(none); sel.appendChild(none);
STYLE_GROUPS[group].forEach((v) => { STYLE_GROUPS[group].forEach((v) => {
const o = document.createElement('option'); const o = document.createElement('option');
o.value = v; o.textContent = valLabel(group, v); o.value = v; o.textContent = valLabel(group, v) + (est === v ? ' (attuale)' : '');
if (styles[group] === v) o.selected = true; if (shown === v) o.selected = true;
sel.appendChild(o); sel.appendChild(o);
}); });
// Evidenzia la combo se un valore è impostato. sel.classList.add(explicit ? 'is-set' : (est ? 'is-estimated' : ''));
if (styles[group]) sel.classList.add('is-set'); sel.addEventListener('change', () => {
sel.addEventListener('change', () => sel.classList.toggle('is-set', !!sel.value)); delete sel.dataset.estimated; // scelta manuale → diventa esplicita
sel.classList.remove('is-estimated');
sel.classList.toggle('is-set', !!sel.value);
});
field.appendChild(cap); field.appendChild(sel); field.appendChild(cap); field.appendChild(sel);
grid.appendChild(field); grid.appendChild(field);
}); });
tmBody.appendChild(grid); tmBody.appendChild(grid);
if (anyEstimate) {
const note = document.createElement('p');
note.className = 'tm-note';
note.textContent = 'I valori "(attuale)" riflettono laspetto dal tema: cambiane uno per fissarlo sul blocco.';
tmBody.appendChild(note);
}
} }
} }
modal.hidden = false; modal.hidden = false;
@@ -227,10 +280,16 @@ const showTags = canTags && wantTags;
return; return;
} }
const value = document.getElementById('tm-value').value; const value = document.getElementById('tm-value').value;
const selects = document.querySelectorAll('.tm-style');
const styles = {}; const styles = {};
document.querySelectorAll('.tm-style').forEach((s) => { if (s.value) styles[s.dataset.group] = s.value; }); selects.forEach((s) => {
const v = s.value;
if (!v) return; // "default (tema)" → non impostato
if (s.dataset.estimated && v === s.dataset.estimated) return; // stima non modificata → non fissare
styles[s.dataset.group] = v;
});
const body = { value }; const body = { value };
if (Object.keys(styles).length) body.styles = styles; if (selects.length) body.styles = styles; // inviato sempre (anche {}) così l'azzeramento persiste
const r = await fetch('/api/admin/content/' + encodeURIComponent(current.tag), { const r = await fetch('/api/admin/content/' + encodeURIComponent(current.tag), {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
}); });