ui: propose modal intercept + readable buttons + open in new tab

This commit is contained in:
s-FunIA
2026-01-18 20:48:10 +01:00
parent 6ea42da643
commit 0c764cd9ab
2 changed files with 255 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
<dialog id="propose-modal" aria-label="Qualifier la proposition">
<form method="dialog" class="box">
<header class="hd">
<h2>Qualifier la proposition</h2>
<button value="cancel" class="x" aria-label="Fermer">✕</button>
</header>
<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">
<small>Astuce : touche <kbd>Esc</kbd> pour fermer.</small>
</footer>
</form>
</dialog>
<style>
dialog { border: none; padding: 0; border-radius: 16px; width: min(720px, calc(100vw - 2rem)); }
dialog::backdrop { background: rgba(0,0,0,.55); }
.box { padding: 1rem 1rem 0.75rem; }
.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:.5rem 0 1rem; opacity:.9; }
.grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap:.5rem; }
.grid button { padding: .75rem .8rem; border-radius: 12px; border: 1px solid rgba(0,0,0,.18); background: rgba(255,255,255,.96); color: #111; cursor: pointer; text-align: left; }
.grid button:hover { background: #fff; }
.grid button:hover { background: #fff; }
.ft { margin-top:.75rem; opacity:.85; }
kbd { padding:0 .35rem; border:1px solid rgba(0,0,0,.2); border-radius:6px; }
</style>
<script is:inline>
(() => {
const dlg = document.getElementById("propose-modal");
if (!dlg) return;
/** @type {URL|null} */
let pending = null;
const upsertLine = (text, key, value) => {
const re = new RegExp(`^${key}:\\s*.*$`, "mi");
if (!value) return text; // "continuer sans préciser"
if (re.test(text)) return text.replace(re, `${key}: ${value}`);
const sep = text && !text.endsWith("\n") ? "\n" : "";
return text + sep + `${key}: ${value}\n`;
};
const openWith = (url) => {
pending = url;
if (typeof dlg.showModal === "function") dlg.showModal();
else window.location.href = url.toString(); // fallback robuste
};
// Intercepte UNIQUEMENT les liens marqués data-propose
document.addEventListener("click", (e) => {
const a = e.target?.closest?.("a[data-propose]");
if (!a) return;
e.preventDefault();
try {
openWith(new URL(a.href, window.location.origin));
} catch {
window.location.href = a.href;
}
});
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); // clé stable (sans accents)
pending.searchParams.set("body", body);
dlg.close();
window.open(pending.toString(), "_blank", "noopener,noreferrer");
});
})();
</script>

View File

@@ -0,0 +1,164 @@
---
import ProposeModal from "../components/ProposeModal.astro";
import SiteNav from "../components/SiteNav.astro";
import LevelToggle from "../components/LevelToggle.astro";
import BuildStamp from "../components/BuildStamp.astro";
import "../styles/global.css";
const {
title,
editionLabel,
editionKey,
statusLabel,
statusKey,
level,
version
} = Astro.props;
const lvl = level ?? 1;
const canonical = Astro.site
? new URL(Astro.url.pathname, Astro.site).href
: Astro.url.href;
// Cible Gitea (injectée au build)
const GITEA_BASE = import.meta.env.PUBLIC_GITEA_BASE ?? "";
const GITEA_OWNER = import.meta.env.PUBLIC_GITEA_OWNER ?? "";
const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
---
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title ? `${title} — Archicratie` : "Archicratie"}</title>
<link rel="canonical" href={canonical} />
<link rel="sitemap" href="/sitemap-index.xml" />
<meta data-pagefind-filter="edition[content]" content={String(editionKey ?? editionLabel)} />
<meta data-pagefind-filter="level[content]" content={String(lvl)} />
<meta data-pagefind-filter="status[content]" content={String(statusKey ?? statusLabel)} />
<meta data-pagefind-meta={`edition:${String(editionKey ?? editionLabel)}`} />
<meta data-pagefind-meta={`level:${String(lvl)}`} />
<meta data-pagefind-meta={`status:${String(statusKey ?? statusLabel)}`} />
<meta data-pagefind-meta={`version:${String(version ?? "")}`} />
</head>
<body data-doc-title={title} data-doc-version={version}>
<header>
<SiteNav />
<div class="edition-bar">
<span class="badge"><strong>Édition</strong> : {editionLabel}</span>
<span class="badge"><strong>Statut</strong> : {statusLabel}</span>
<span class="badge"><strong>Niveau</strong> : {lvl}</span>
<span class="badge"><strong>Version</strong> : {version}</span>
<LevelToggle initialLevel={lvl} />
</div>
</header>
<main>
<article class="reading" data-pagefind-body>
<slot />
<BuildStamp />
</article>
</main>
<ProposeModal />
<!-- IMPORTANT: define:vars injecte les constantes dans le JS sans templating fragile -->
<script is:inline define:vars={{ GITEA_BASE, GITEA_OWNER, GITEA_REPO }}>
(() => {
const title = document.body.dataset.docTitle || document.title;
const version = document.body.dataset.docVersion || "";
const giteaReady = Boolean(GITEA_BASE && GITEA_OWNER && GITEA_REPO);
function buildIssueURL(anchorId, excerpt) {
const base = String(GITEA_BASE).replace(/\/+$/, "");
const issue = new URL(`${base}/${GITEA_OWNER}/${GITEA_REPO}/issues/new`);
const local = new URL(window.location.href);
local.hash = anchorId;
const path = local.pathname;
const issueTitle = `[Correction] ${anchorId} — ${title}`;
const body = [
`Chemin: ${path}`,
`URL locale: ${local.toString()}`,
`Ancre: #${anchorId}`,
`Version: ${version || "(non renseignée)"}`,
`Type: type/correction`,
`State: state/recevable`,
``,
`Texte actuel (extrait):`,
`> ${excerpt}`,
``,
`Proposition (remplacer par):`,
``,
`Justification:`,
``,
`---`,
`Note: issue générée depuis le site (pré-remplissage).`
].join("\n");
issue.searchParams.set("title", issueTitle);
issue.searchParams.set("body", body);
return issue.toString();
}
const paras = Array.from(document.querySelectorAll(".reading p[id]"));
for (const p of paras) {
if (p.querySelector(".para-tools")) continue;
const tools = document.createElement("span");
tools.className = "para-tools";
const a = document.createElement("a");
a.className = "para-anchor";
a.href = `#${p.id}`;
a.setAttribute("aria-label", "Lien vers ce paragraphe");
a.textContent = "¶";
const citeBtn = document.createElement("button");
citeBtn.type = "button";
citeBtn.className = "para-cite";
citeBtn.textContent = "Citer";
citeBtn.addEventListener("click", async () => {
const url = new URL(window.location.href);
url.hash = p.id;
const cite = `${title}${version ? ` (v${version})` : ""} — ${url.toString()}`;
try {
await navigator.clipboard.writeText(cite);
const prev = citeBtn.textContent;
citeBtn.textContent = "Copié";
setTimeout(() => (citeBtn.textContent = prev), 900);
} catch {
window.prompt("Copiez la citation :", cite);
}
});
tools.appendChild(a);
tools.appendChild(citeBtn);
if (giteaReady) {
const propose = document.createElement("a");
propose.className = "para-propose";
propose.href = "#";
propose.textContent = "Proposer";
propose.setAttribute("aria-label", "Proposer une correction sur Gitea");
propose.setAttribute("data-propose","1");
tools.appendChild(propose);
}
p.appendChild(tools);
}
})();
</script>
</body>
</html>