ui: propose modal intercept + readable buttons + open in new tab
This commit is contained in:
91
src/components/ProposeModal.astro
Normal file
91
src/components/ProposeModal.astro
Normal 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>
|
||||
164
src/layouts/EditionLayout.astro
Normal file
164
src/layouts/EditionLayout.astro
Normal 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>
|
||||
Reference in New Issue
Block a user