Files
archicratie-edition/src/components/ProposeModal.astro
archicratia 7f871ca46f
All checks were successful
CI / build-and-anchors (push) Successful in 1m31s
SMOKE / smoke (push) Successful in 24s
Fix gitea env + proposer modal + favicon/nginx — 2026-01-31
2026-01-31 16:33:13 +00:00

265 lines
8.0 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<dialog id="propose-modal" aria-label="Qualifier la proposition" data-step="1">
<form method="dialog" class="box">
<header class="hd">
<h2>Qualifier la proposition</h2>
<button value="cancel" class="x" aria-label="Fermer">✕</button>
</header>
<!-- STEP 1 -->
<section class="step step-1" aria-label="Choisir le type">
<p class="sub">
Dabord : quel type de ticket veux-tu ouvrir ?
</p>
<div class="grid">
<button type="button" data-kind="correction">✍️ Correction (rédaction / clarté)</button>
<button type="button" data-kind="fact">🔎 Vérification factuelle / sources</button>
</div>
<footer class="ft">
<small>Étape 1/2 — <kbd>Esc</kbd> pour fermer.</small>
</footer>
</section>
<!-- STEP 2 -->
<section class="step step-2" aria-label="Choisir la catégorie">
<p class="sub">
Optionnel : choisis une intention (pour tri & traitement éditorial). Sinon, continue sans préciser.
</p>
<div class="grid">
<button type="button" data-category="cat/style">Style / lisibilité</button>
<button type="button" data-category="cat/lexique">Lexique / terminologie</button>
<button type="button" data-category="cat/argument">Argument / structure</button>
<button type="button" data-category="cat/redondance">Redondance</button>
<button type="button" data-category="cat/source">Source / vérification</button>
<button type="button" data-category="">Continuer sans préciser</button>
</div>
<footer class="ft row">
<button type="button" class="back" data-back="1">← Retour</button>
<small>Étape 2/2 — <kbd>Esc</kbd> pour fermer.</small>
</footer>
</section>
</form>
</dialog>
<style>
dialog { border: none; padding: 0; border-radius: 18px; width: min(760px, calc(100vw - 2rem)); }
dialog::backdrop { background: rgba(0,0,0,.55); }
.box { padding: 1rem 1rem .85rem; }
.hd { display:flex; align-items:center; justify-content:space-between; gap:.75rem; }
.hd h2 { margin:0; font-size:1.1rem; }
.x { border:0; background:transparent; font-size:1.1rem; cursor:pointer; }
.sub { margin:.55rem 0 1rem; opacity:.92; line-height: 1.35; }
.grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap:.55rem; }
.grid button {
padding: .8rem .9rem;
border-radius: 14px;
border: 1px solid rgba(0,0,0,.18);
background: rgba(255,255,255,.96);
color: #111;
cursor: pointer;
text-align: left;
line-height: 1.2;
}
.grid button:hover { background: #fff; }
.ft { margin-top:.85rem; opacity:.88; }
.ft.row { display:flex; align-items:center; justify-content:space-between; gap:1rem; }
.back {
border: 1px solid rgba(0,0,0,.18);
background: rgba(255,255,255,.96);
color:#111;
border-radius: 12px;
padding: .55rem .7rem;
cursor: pointer;
}
kbd { padding:0 .35rem; border:1px solid rgba(0,0,0,.2); border-radius:6px; }
.step { display:none; }
dialog[data-step="1"] .step-1 { display:block; }
dialog[data-step="2"] .step-2 { display:block; }
</style>
<script is:inline>
(() => {
const dlg = document.getElementById("propose-modal");
if (!dlg) return;
/** @type {URL|null} */
let pending = null;
/** @type {"correction"|"fact"|""} */
let kind = "";
// Doit rester cohérent avec EditionLayout
const URL_HARD_LIMIT = 6500;
const esc = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const upsertLine = (text, key, value) => {
const re = new RegExp(`^\\s*${esc(key)}\\s*:\\s*.*$`, "mi");
// value vide => supprimer la ligne si elle existe
if (!value) {
if (!re.test(text)) return text;
return (
text
.replace(re, "")
.replace(/\n{3,}/g, "\n\n")
.trimEnd() + "\n"
);
}
// remplace si existe
if (re.test(text)) return text.replace(re, `${key}: ${value}`);
// sinon append
const sep = text && !text.endsWith("\n") ? "\n" : "";
return text + sep + `${key}: ${value}\n`;
};
const setTitlePrefix = (u, prefix) => {
const t = u.searchParams.get("title") || "";
const cleaned = t.replace(/^\[[^\]]+\]\s*/,"");
u.searchParams.set("title", `[${prefix}] ${cleaned}`.trim());
};
const tryUpgradeBodyWithFull = (u, full) => {
if (!full) return;
let body = u.searchParams.get("body") || "";
if (!body) return;
// Si déjà en "copie exacte", rien à faire
if (body.includes("Texte actuel (copie exacte du paragraphe)")) return;
// On ne tente que si le body est en mode extrait
if (!body.includes("Texte actuel (extrait):")) return;
const quoted = full.split(/\r?\n/).map(l => `> ${l}`.trimEnd()).join("\n");
// Remplace le bloc "Texte actuel (extrait)" jusqu'à "Proposition (remplacer par):"
const re = /Texte actuel \(extrait\):[\s\S]*?\n\nProposition \(remplacer par\):/m;
const next = body.replace(
re,
`Texte actuel (copie exacte du paragraphe):\n${quoted}\n\nProposition (remplacer par):`
);
if (next === body) return;
u.searchParams.set("body", next);
// garde-fou URL
if (u.toString().length > URL_HARD_LIMIT) {
// revert
u.searchParams.set("body", body);
}
};
// ✅ Ouvre EN NOUVEL ONGLET : stratégie unique (anchor click) => jamais 2 onglets
const openInNewTab = (url) => {
try {
const a = document.createElement("a");
a.href = url;
a.target = "_blank";
a.rel = "noopener noreferrer";
a.style.display = "none";
document.body.appendChild(a);
a.click();
a.remove();
return true;
} catch {}
// dernier recours
window.prompt("Popup bloquée. Copiez ce lien pour ouvrir le ticket :", url);
return false;
};
const openWith = (url) => {
pending = url;
kind = "";
dlg.dataset.step = "1";
if (typeof dlg.showModal === "function") dlg.showModal();
else openInNewTab(url.toString());
};
// Intercepte UNIQUEMENT les liens marqués data-propose
document.addEventListener("click", async (e) => {
const a = e.target?.closest?.("a[data-propose]");
if (!a) return;
e.preventDefault();
e.stopImmediatePropagation();
const rawUrl = a.dataset.url || a.getAttribute("href") || "";
if (!rawUrl || rawUrl === "#") return;
const full = (a.dataset.full || "").trim();
try {
const u = new URL(rawUrl);
if (full) {
try { await navigator.clipboard.writeText(full); } catch {}
tryUpgradeBodyWithFull(u, full);
}
openWith(u);
} catch {
openInNewTab(rawUrl);
}
}, { capture: true });
// Step 1: type
dlg.addEventListener("click", (e) => {
const btn = e.target?.closest?.("button[data-kind]");
if (!btn || !pending) return;
kind = btn.getAttribute("data-kind") || "";
let body = pending.searchParams.get("body") || "";
if (kind === "fact") {
body = upsertLine(body, "Type", "type/fact-check");
body = upsertLine(body, "State", "state/a-sourcer");
setTitlePrefix(pending, "Fact-check");
} else {
body = upsertLine(body, "Type", "type/correction");
body = upsertLine(body, "State", "state/recevable");
setTitlePrefix(pending, "Correction");
}
pending.searchParams.set("body", body);
dlg.dataset.step = "2";
});
// Back
dlg.addEventListener("click", (e) => {
const back = e.target?.closest?.("button[data-back]");
if (!back) return;
dlg.dataset.step = "1";
});
// Step 2: category + open
dlg.addEventListener("click", (e) => {
const btn = e.target?.closest?.("button[data-category]");
if (!btn || !pending) return;
const cat = btn.getAttribute("data-category") || "";
let body = pending.searchParams.get("body") || "";
body = upsertLine(body, "Category", cat);
pending.searchParams.set("body", body);
const u = pending.toString();
dlg.close();
// ✅ ouvre en nouvel onglet, sans jamais remplacer longlet courant
openInNewTab(u);
});
})();
</script>