editor inline: restyle e combo con valore impostato pre-selezionato
- GET /api/admin/content/[tag] restituisce anche gli styles correnti del blocco - Modal ridisegnato (header con chip tag, input arrotondati, griglia combo) - Combo etichettate in italiano (Dimensione/Colore/Peso/Stile/Allineamento) con valori leggibili; pre-selezione del valore già impostato ed evidenziazione Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+81
-30
@@ -77,37 +77,70 @@ const showTags = canTags && wantTags;
|
|||||||
<style is:global>
|
<style is:global>
|
||||||
[data-tag] { outline: 1px dashed #b8a98c; outline-offset: 2px; }
|
[data-tag] { outline: 1px dashed #b8a98c; outline-offset: 2px; }
|
||||||
.tag-badge { position: absolute; z-index: 9999; display: inline-flex; align-items: center; gap: 4px;
|
.tag-badge { position: absolute; z-index: 9999; display: inline-flex; align-items: center; gap: 4px;
|
||||||
background: #3a2929; color: #fff; font: 600 10px/1.6 monospace; padding: 0 4px 0 6px; opacity: .85; }
|
background: #3a2929; color: #fff; font: 600 10px/1.6 monospace; padding: 0 4px 0 6px; opacity: .85; border-radius: 3px; }
|
||||||
.tag-badge:hover { opacity: 1; }
|
.tag-badge:hover { opacity: 1; }
|
||||||
.tag-badge__edit { background: #9c8b70; color: #fff; border: 0; cursor: pointer; font: inherit; padding: 0 5px; }
|
.tag-badge__edit { background: #9c8b70; color: #fff; border: 0; cursor: pointer; font: inherit; padding: 0 5px; border-radius: 2px; }
|
||||||
.tag-modal { position: fixed; inset: 0; z-index: 10000; background: rgba(0,0,0,.5); display: flex; align-items: center; justify-content: center; }
|
.tag-modal { position: fixed; inset: 0; z-index: 10000; background: rgba(28,20,16,.55); backdrop-filter: blur(2px);
|
||||||
|
display: flex; align-items: center; justify-content: center; padding: 20px; }
|
||||||
.tag-modal[hidden] { display: none; }
|
.tag-modal[hidden] { display: none; }
|
||||||
.tag-modal__box { background: #fff; color: #222; width: min(560px, 92vw); max-height: 88vh; overflow: auto; padding: 20px; border-radius: 4px; }
|
.tag-modal__box { background: #fdfbf7; color: #2b2320; width: min(600px, 94vw); max-height: 88vh; overflow: auto;
|
||||||
.tag-modal__box h3 { margin: 0 0 4px; font: 600 14px monospace; }
|
border-radius: 14px; box-shadow: 0 24px 60px rgba(28,20,16,.35); font-family: system-ui, sans-serif; }
|
||||||
.tag-modal__box .guid { font: 11px monospace; color: #888; margin: 0 0 14px; }
|
.tag-modal__head { padding: 20px 24px 16px; border-bottom: 1px solid #ece4d8; }
|
||||||
.tag-modal__box textarea { width: 100%; min-height: 140px; font: inherit; padding: 8px; box-sizing: border-box; }
|
.tag-modal__head h3 { margin: 0; font: 600 1.05rem/1.3 system-ui, sans-serif; letter-spacing: .01em; }
|
||||||
.tag-modal__box select { margin: 6px 8px 6px 0; }
|
.tag-modal__head .tm-tagchip { display: inline-block; margin-top: 8px; background: #efe8dc; color: #6b5b48;
|
||||||
.tag-modal__row { display: flex; gap: 10px; margin-top: 14px; }
|
font: 500 11px/1.6 ui-monospace, monospace; padding: 2px 9px; border-radius: 999px; word-break: break-all; }
|
||||||
.tag-modal__row button { padding: 9px 16px; border: 0; cursor: pointer; }
|
.tag-modal__head .guid { font: 10px/1.5 ui-monospace, monospace; color: #a99f90; margin: 6px 0 0; }
|
||||||
|
.tag-modal__body { padding: 20px 24px; }
|
||||||
|
.tag-modal__box textarea { width: 100%; min-height: 150px; font: inherit; font-size: .95rem; line-height: 1.5;
|
||||||
|
padding: 12px 14px; box-sizing: border-box; border: 1px solid #dcd2c4; border-radius: 10px; background: #fff;
|
||||||
|
resize: vertical; color: #2b2320; }
|
||||||
|
.tag-modal__box textarea:focus { outline: 2px solid #9c8b70; outline-offset: 1px; border-color: #9c8b70; }
|
||||||
|
.tm-styles { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px 14px; margin-top: 18px; }
|
||||||
|
.tm-field { display: flex; flex-direction: column; gap: 5px; }
|
||||||
|
.tm-field__label { font: 600 11px/1 system-ui, sans-serif; text-transform: uppercase; letter-spacing: .08em; color: #8a7c68; }
|
||||||
|
.tm-field select { appearance: none; width: 100%; padding: 9px 30px 9px 12px; border: 1px solid #dcd2c4; border-radius: 9px;
|
||||||
|
background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%238a7c68' stroke-width='1.6'%3E%3Cpath d='M2 4l4 4 4-4'/%3E%3C/svg%3E") no-repeat right 11px center;
|
||||||
|
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.is-set { border-color: #9c8b70; background-color: #f6f1e8; font-weight: 600; }
|
||||||
|
.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; }
|
||||||
|
.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; }
|
||||||
|
.tag-modal__msg { color: #b4443a; margin: 12px 24px 0; min-height: 18px; font-size: .88rem; }
|
||||||
|
.tag-modal__row { display: flex; gap: 10px; justify-content: flex-end; padding: 16px 24px 22px; }
|
||||||
|
.tag-modal__row button { padding: 10px 20px; border: 0; border-radius: 9px; cursor: pointer; font: 600 .9rem system-ui, sans-serif; }
|
||||||
.tag-modal__save { background: #3a2d26; color: #fff; }
|
.tag-modal__save { background: #3a2d26; color: #fff; }
|
||||||
.tag-modal__cancel { background: #ddd; }
|
.tag-modal__save:hover { background: #2b211b; }
|
||||||
.tag-modal__msg { color: #a33; margin-top: 8px; min-height: 18px; }
|
.tag-modal__cancel { background: #ece4d8; color: #5b4f42; }
|
||||||
|
.tag-modal__cancel:hover { background: #e0d6c6; }
|
||||||
</style>
|
</style>
|
||||||
<div class="tag-modal" id="tag-modal" hidden>
|
<div class="tag-modal" id="tag-modal" hidden>
|
||||||
<div class="tag-modal__box">
|
<div class="tag-modal__box">
|
||||||
<h3 id="tm-tag"></h3>
|
<div class="tag-modal__head">
|
||||||
<p class="guid" id="tm-guid"></p>
|
<h3>Modifica contenuto</h3>
|
||||||
<div id="tm-body"></div>
|
<span class="tm-tagchip" id="tm-tag"></span>
|
||||||
|
<p class="guid" id="tm-guid"></p>
|
||||||
|
</div>
|
||||||
|
<div class="tag-modal__body" id="tm-body"></div>
|
||||||
<div class="tag-modal__msg" id="tm-msg"></div>
|
<div class="tag-modal__msg" id="tm-msg"></div>
|
||||||
<div class="tag-modal__row">
|
<div class="tag-modal__row">
|
||||||
<button type="button" class="tag-modal__save" id="tm-save">Salva</button>
|
|
||||||
<button type="button" class="tag-modal__cancel" id="tm-cancel">Annulla</button>
|
<button type="button" class="tag-modal__cancel" id="tm-cancel">Annulla</button>
|
||||||
|
<button type="button" class="tag-modal__save" id="tm-save">Salva</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script is:inline>
|
<script is:inline>
|
||||||
(() => {
|
(() => {
|
||||||
const STYLE_GROUPS = { size: ['s','m','l','xl','xxl'], color: ['accent','accent-dark','dark','heading','text','text-light','white'], weight: ['normal','bold'], style: ['normal','italic'], align: ['left','center','right','justify'] };
|
const STYLE_GROUPS = { size: ['s','m','l','xl','xxl'], color: ['accent','accent-dark','dark','heading','text','text-light','white'], weight: ['normal','bold'], style: ['normal','italic'], align: ['left','center','right','justify'] };
|
||||||
|
const GROUP_LABELS = { size: 'Dimensione', color: 'Colore', weight: 'Peso', style: 'Stile', align: 'Allineamento' };
|
||||||
|
const VALUE_LABELS = {
|
||||||
|
size: { s: 'S', m: 'M', l: 'L', xl: 'XL', xxl: 'XXL' },
|
||||||
|
weight: { normal: 'Normale', bold: 'Grassetto' },
|
||||||
|
style: { normal: 'Normale', italic: 'Corsivo' },
|
||||||
|
align: { left: 'Sinistra', center: 'Centro', right: 'Destra', justify: 'Giustificato' },
|
||||||
|
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 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');
|
||||||
@@ -137,8 +170,7 @@ const showTags = canTags && wantTags;
|
|||||||
inp.type = 'file'; inp.accept = 'image/*'; inp.id = 'tm-file';
|
inp.type = 'file'; inp.accept = 'image/*'; inp.id = 'tm-file';
|
||||||
tmBody.appendChild(inp);
|
tmBody.appendChild(inp);
|
||||||
const del = document.createElement('button');
|
const del = document.createElement('button');
|
||||||
del.type = 'button'; del.textContent = 'Rimuovi immagine'; del.id = 'tm-del';
|
del.type = 'button'; del.textContent = 'Rimuovi immagine'; del.id = 'tm-del'; del.className = 'tm-del';
|
||||||
del.style.cssText = 'margin-top:10px;padding:8px 14px;border:0;background:#a33;color:#fff;cursor:pointer';
|
|
||||||
tmBody.appendChild(del);
|
tmBody.appendChild(del);
|
||||||
del.addEventListener('click', async () => {
|
del.addEventListener('click', async () => {
|
||||||
const r = await fetch('/api/admin/content/' + encodeURIComponent(current.tag) + '/image', { method: 'DELETE' });
|
const r = await fetch('/api/admin/content/' + encodeURIComponent(current.tag) + '/image', { method: 'DELETE' });
|
||||||
@@ -148,18 +180,37 @@ const showTags = canTags && wantTags;
|
|||||||
const ta = document.createElement('textarea');
|
const ta = document.createElement('textarea');
|
||||||
ta.id = 'tm-value'; ta.value = data.value;
|
ta.id = 'tm-value'; ta.value = data.value;
|
||||||
tmBody.appendChild(ta);
|
tmBody.appendChild(ta);
|
||||||
(data.styleOptions || []).forEach((group) => {
|
const styles = data.styles || {};
|
||||||
const opts = STYLE_GROUPS[group];
|
const groups = (data.styleOptions || []).filter((g) => STYLE_GROUPS[g]);
|
||||||
if (!opts) return;
|
if (groups.length) {
|
||||||
const sel = document.createElement('select');
|
const grid = document.createElement('div');
|
||||||
sel.dataset.group = group;
|
grid.className = 'tm-styles';
|
||||||
sel.className = 'tm-style';
|
groups.forEach((group) => {
|
||||||
const none = document.createElement('option');
|
const field = document.createElement('label');
|
||||||
none.value = ''; none.textContent = group + ': —';
|
field.className = 'tm-field';
|
||||||
sel.appendChild(none);
|
const cap = document.createElement('span');
|
||||||
opts.forEach((v) => { const o = document.createElement('option'); o.value = v; o.textContent = group + ': ' + v; sel.appendChild(o); });
|
cap.className = 'tm-field__label';
|
||||||
tmBody.appendChild(sel);
|
cap.textContent = GROUP_LABELS[group] || group;
|
||||||
});
|
const sel = document.createElement('select');
|
||||||
|
sel.dataset.group = group;
|
||||||
|
sel.className = 'tm-style';
|
||||||
|
const none = document.createElement('option');
|
||||||
|
none.value = ''; none.textContent = '— default —';
|
||||||
|
sel.appendChild(none);
|
||||||
|
STYLE_GROUPS[group].forEach((v) => {
|
||||||
|
const o = document.createElement('option');
|
||||||
|
o.value = v; o.textContent = valLabel(group, v);
|
||||||
|
if (styles[group] === v) o.selected = true;
|
||||||
|
sel.appendChild(o);
|
||||||
|
});
|
||||||
|
// Evidenzia la combo se un valore è impostato.
|
||||||
|
if (styles[group]) sel.classList.add('is-set');
|
||||||
|
sel.addEventListener('change', () => sel.classList.toggle('is-set', !!sel.value));
|
||||||
|
field.appendChild(cap); field.appendChild(sel);
|
||||||
|
grid.appendChild(field);
|
||||||
|
});
|
||||||
|
tmBody.appendChild(grid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
modal.hidden = false;
|
modal.hidden = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,14 +10,20 @@ const json = (status: number, body: object) =>
|
|||||||
|
|
||||||
export const GET: APIRoute = ({ params }) => {
|
export const GET: APIRoute = ({ params }) => {
|
||||||
const tag = params.tag ?? '';
|
const tag = params.tag ?? '';
|
||||||
const row = getDb().prepare('SELECT tag, type, value, guid FROM content_blocks WHERE tag = ?').get(tag) as
|
const row = getDb().prepare('SELECT tag, type, value, styles, guid FROM content_blocks WHERE tag = ?').get(tag) as
|
||||||
{ tag: string; type: string; value: string; guid: string | null } | undefined;
|
{ tag: string; type: string; value: string; styles: string; guid: string | null } | undefined;
|
||||||
if (!row) return json(404, { error: 'Tag inesistente.' });
|
if (!row) return json(404, { error: 'Tag inesistente.' });
|
||||||
|
let styles: Record<string, string> = {};
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(row.styles);
|
||||||
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) styles = parsed;
|
||||||
|
} catch { /* styles corrotto: si ignora */ }
|
||||||
return json(200, {
|
return json(200, {
|
||||||
tag: row.tag,
|
tag: row.tag,
|
||||||
guid: row.guid ?? '',
|
guid: row.guid ?? '',
|
||||||
type: row.type,
|
type: row.type,
|
||||||
value: row.value,
|
value: row.value,
|
||||||
|
styles,
|
||||||
styleOptions: seedByTag.get(row.tag)?.styleOptions ?? [],
|
styleOptions: seedByTag.get(row.tag)?.styleOptions ?? [],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user