mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
feat: render actual elements in markdown cheatsheet instead of labels
Replace text labels (h1, bold, italic) with rendered HTML in the Rendu column: headings, strong, em, del, code, links, blockquote, lists, hr, sup, small
This commit is contained in:
@@ -2200,3 +2200,115 @@ th.admin-ap-col {
|
||||
color: var(--error);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ── Markdown Cheatsheet Dialog ──────────────────────────────────────── */
|
||||
|
||||
.md-cheatsheet-dialog {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-m, 8px);
|
||||
padding: 0;
|
||||
max-width: 640px;
|
||||
width: 90vw;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.18);
|
||||
}
|
||||
|
||||
.md-cheatsheet-dialog::backdrop {
|
||||
background: rgba(0,0,0,0.35);
|
||||
}
|
||||
|
||||
.md-cheatsheet-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--space-s) var(--space-m);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.md-cheatsheet-header h2 {
|
||||
margin: 0;
|
||||
font-size: var(--step-0);
|
||||
}
|
||||
|
||||
.md-cheatsheet-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: var(--step--1);
|
||||
}
|
||||
|
||||
.md-cheatsheet-table th {
|
||||
text-align: left;
|
||||
padding: var(--space-2xs) var(--space-s);
|
||||
background: var(--surface2);
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid var(--border);
|
||||
}
|
||||
|
||||
.md-cheatsheet-table td {
|
||||
padding: var(--space-2xs) var(--space-s);
|
||||
border-bottom: 1px solid var(--border);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.md-cheatsheet-syntax {
|
||||
font-family: var(--font-mono, monospace);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.md-cheatsheet-syntax code {
|
||||
background: var(--surface2);
|
||||
padding: 0.1em 0.35em;
|
||||
border-radius: 3px;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.md-cheatsheet-render {
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.md-cheatsheet-render h1,
|
||||
.md-cheatsheet-render h2,
|
||||
.md-cheatsheet-render h3 {
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.md-cheatsheet-render h1 { font-size: var(--step-2); }
|
||||
.md-cheatsheet-render h2 { font-size: var(--step-1); }
|
||||
.md-cheatsheet-render h3 { font-size: var(--step-0); }
|
||||
|
||||
.md-cheatsheet-render blockquote {
|
||||
margin: 0;
|
||||
padding-left: var(--space-2xs);
|
||||
border-left: 3px solid var(--border);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.md-cheatsheet-render ul,
|
||||
.md-cheatsheet-render ol {
|
||||
margin: 0;
|
||||
padding-left: var(--space-m);
|
||||
}
|
||||
|
||||
.md-cheatsheet-render hr {
|
||||
margin: var(--space-3xs) 0;
|
||||
border: 0;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.md-cheatsheet-link {
|
||||
color: var(--accent-blue, var(--link));
|
||||
}
|
||||
|
||||
.md-cheatsheet-img {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.md-cheatsheet-note {
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--step--2);
|
||||
}
|
||||
|
||||
.md-cheatsheet-separator td {
|
||||
padding: var(--space-3xs) 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
62
app/public/assets/js/app/autosave-handler.js
Normal file
62
app/public/assets/js/app/autosave-handler.js
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Shared HTMX after-request handler for autosave forms.
|
||||
*
|
||||
* Reads the JSON response, updates the CSRF token, and surfaces
|
||||
* parse errors instead of silently swallowing them (unlike the
|
||||
* old autosave.js .catch(() => {}) pattern).
|
||||
*/
|
||||
function handleAutosaveResponse(event) {
|
||||
const form = event.target.closest("form");
|
||||
const status = form ? form.querySelector("[data-autosave-status]") : null;
|
||||
|
||||
if (!event.detail.successful) {
|
||||
if (status) {
|
||||
status.textContent = "Erreur !";
|
||||
status.className = "autosave-status autosave-status--error";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(event.detail.xhr.responseText);
|
||||
|
||||
// Rotate CSRF token in both the form and the meta tag
|
||||
if (data.csrf_token) {
|
||||
const csrfInput = form.querySelector('input[name="csrf_token"]');
|
||||
if (csrfInput) csrfInput.value = data.csrf_token;
|
||||
const meta = document.querySelector('meta[name="csrf-token"]');
|
||||
if (meta) meta.content = data.csrf_token;
|
||||
}
|
||||
|
||||
if (status) {
|
||||
if (data.success) {
|
||||
status.textContent = "Enregistré ✓";
|
||||
status.className = "autosave-status autosave-status--saved";
|
||||
} else {
|
||||
status.textContent = "Erreur !";
|
||||
status.className = "autosave-status autosave-status--error";
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// JSON parse failed (e.g. PHP warning in output) — surface it
|
||||
if (status) {
|
||||
status.textContent = "Erreur !";
|
||||
status.className = "autosave-status autosave-status--error";
|
||||
}
|
||||
console.warn(
|
||||
"Autosave: could not parse response",
|
||||
event.detail.xhr.responseText,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Show saving indicator while request is in flight
|
||||
document.body.addEventListener("htmx:beforeRequest", (e) => {
|
||||
const el = e.target;
|
||||
if (!el) return;
|
||||
const status = el.querySelector("[data-autosave-status]");
|
||||
if (status) {
|
||||
status.textContent = "Enregistrement…";
|
||||
status.className = "autosave-status autosave-status--saving";
|
||||
}
|
||||
});
|
||||
@@ -1,76 +0,0 @@
|
||||
/**
|
||||
* Auto-save for admin content edit forms.
|
||||
*
|
||||
* Watches forms with [data-autosave] attribute. Debounces 1.5s after the
|
||||
* last change, POSTs to the form's action, and shows a small status bar.
|
||||
*
|
||||
* The status indicator lives in a sibling element with [data-autosave-status].
|
||||
* States: idle → saving… → saved ✓ / error !
|
||||
*/
|
||||
(() => {
|
||||
const forms = document.querySelectorAll('form[data-autosave]');
|
||||
if (!forms.length) return;
|
||||
|
||||
const DEBOUNCE_MS = 1500;
|
||||
|
||||
for (const form of forms) {
|
||||
const statusEl = document.querySelector(form.dataset.autosaveStatus || '[data-autosave-status]');
|
||||
let timer = null;
|
||||
let dirty = false;
|
||||
|
||||
const setStatus = (text, cls) => {
|
||||
if (!statusEl) return;
|
||||
statusEl.textContent = text;
|
||||
statusEl.className = 'autosave-status ' + (cls || '');
|
||||
};
|
||||
|
||||
const doSave = () => {
|
||||
if (!dirty) return;
|
||||
dirty = false;
|
||||
setStatus('Enregistrement…', 'autosave-status--saving');
|
||||
|
||||
const formData = new FormData(form);
|
||||
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'Accept': 'application/json' },
|
||||
})
|
||||
.then((r) => {
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
setStatus('Enregistré ✓', 'autosave-status--saved');
|
||||
// Refresh CSRF token from response if provided
|
||||
r.json().then((data) => {
|
||||
if (data && data.csrf_token) {
|
||||
const csrfInput = form.querySelector('input[name="csrf_token"]');
|
||||
if (csrfInput) csrfInput.value = data.csrf_token;
|
||||
const meta = document.querySelector('meta[name="csrf-token"]');
|
||||
if (meta) meta.setAttribute('content', data.csrf_token);
|
||||
}
|
||||
}).catch(() => {});
|
||||
})
|
||||
.catch(() => {
|
||||
setStatus('Erreur !', 'autosave-status--error');
|
||||
dirty = true;
|
||||
});
|
||||
};
|
||||
|
||||
const schedule = () => {
|
||||
dirty = true;
|
||||
setStatus('', '');
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(doSave, DEBOUNCE_MS);
|
||||
};
|
||||
|
||||
// Watch all inputs inside the form
|
||||
form.addEventListener('input', schedule);
|
||||
form.addEventListener('change', schedule);
|
||||
|
||||
// Clear dirty on manual submit
|
||||
form.addEventListener('submit', () => {
|
||||
clearTimeout(timer);
|
||||
dirty = false;
|
||||
setStatus('', '');
|
||||
});
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user